Fix the build.
[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, ui
20 from bzrlib.branch import BranchCheckResult
21 from bzrlib.errors import (InvalidRevisionId, NoSuchRevision, NotBranchError, 
22                            UninitializableFormat, UnrelatedBranches)
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, get_transport
29 from bzrlib.trace import mutter
30
31 from svn.core import SubversionException, Pool
32 import svn.core
33
34 import os
35
36 from branchprops import BranchPropertyList
37 from cache import create_cache_dir, sqlite3
38 import calendar
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, 
45                     parse_list_scheme_text, guess_scheme_from_history)
46 from tree import SvnRevisionTree
47 import time
48 import urllib
49
50 SVN_PROP_BZR_PREFIX = 'bzr:'
51 SVN_PROP_BZR_ANCESTRY = 'bzr:ancestry:v%d-' % MAPPING_VERSION
52 SVN_PROP_BZR_FILEIDS = 'bzr:file-ids'
53 SVN_PROP_BZR_MERGE = 'bzr:merge'
54 SVN_PROP_SVK_MERGE = 'svk:merge'
55 SVN_PROP_BZR_REVISION_INFO = 'bzr:revision-info'
56 SVN_PROP_BZR_REVISION_ID = 'bzr:revision-id:v%d-' % MAPPING_VERSION
57 SVN_PROP_BZR_BRANCHING_SCHEME = 'bzr:branching-scheme'
58
59 SVN_REVPROP_BZR_COMMITTER = 'bzr:committer'
60 SVN_REVPROP_BZR_FILEIDS = 'bzr:file-ids'
61 SVN_REVPROP_BZR_MERGE = 'bzr:merge'
62 SVN_REVPROP_BZR_REVISION_ID = 'bzr:revision-id'
63 SVN_REVPROP_BZR_REVPROP_PREFIX = 'bzr:revprop:'
64 SVN_REVPROP_BZR_ROOT = 'bzr:root'
65 SVN_REVPROP_BZR_SCHEME = 'bzr:scheme'
66 SVN_REVPROP_BZR_SIGNATURE = 'bzr:gpg-signature'
67
68 # The following two functions don't use day names (which can vary by 
69 # locale) unlike the alternatives in bzrlib.timestamp
70
71 def format_highres_date(t, offset=0):
72     """Format a date, such that it includes higher precision in the
73     seconds field.
74
75     :param t:   The local time in fractional seconds since the epoch
76     :type t: float
77     :param offset:  The timezone offset in integer seconds
78     :type offset: int
79     """
80     assert isinstance(t, float)
81
82     # This has to be formatted for "original" date, so that the
83     # revision XML entry will be reproduced faithfully.
84     if offset is None:
85         offset = 0
86     tt = time.gmtime(t + offset)
87
88     return (time.strftime("%Y-%m-%d %H:%M:%S", tt)
89             # Get the high-res seconds, but ignore the 0
90             + ('%.9f' % (t - int(t)))[1:]
91             + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60))
92
93
94 def unpack_highres_date(date):
95     """This takes the high-resolution date stamp, and
96     converts it back into the tuple (timestamp, timezone)
97     Where timestamp is in real UTC since epoch seconds, and timezone is an
98     integer number of seconds offset.
99
100     :param date: A date formated by format_highres_date
101     :type date: string
102     """
103     # skip day if applicable
104     if not date[0].isdigit():
105         space_loc = date.find(' ')
106         if space_loc == -1:
107             raise ValueError("No valid date: %r" % date)
108         date = date[space_loc+1:]
109     # Up until the first period is a datestamp that is generated
110     # as normal from time.strftime, so use time.strptime to
111     # parse it
112     dot_loc = date.find('.')
113     if dot_loc == -1:
114         raise ValueError(
115             'Date string does not contain high-precision seconds: %r' % date)
116     base_time = time.strptime(date[:dot_loc], "%Y-%m-%d %H:%M:%S")
117     fract_seconds, offset = date[dot_loc:].split()
118     fract_seconds = float(fract_seconds)
119
120     offset = int(offset)
121
122     hours = int(offset / 100)
123     minutes = (offset % 100)
124     seconds_offset = (hours * 3600) + (minutes * 60)
125
126     # time.mktime returns localtime, but calendar.timegm returns UTC time
127     timestamp = calendar.timegm(base_time)
128     timestamp -= seconds_offset
129     # Add back in the fractional seconds
130     timestamp += fract_seconds
131     return (timestamp, seconds_offset)
132
133
134 def parse_merge_property(line):
135     """Parse a bzr:merge property value.
136
137     :param line: Line to parse
138     :return: List of revisions merged
139     """
140     if ' ' in line:
141         mutter('invalid revision id %r in merged property, skipping' % line)
142         return []
143
144     return filter(lambda x: x != "", line.split("\t"))
145
146
147 def parse_revid_property(line):
148     """Parse a (revnum, revid) tuple as set in revision id properties.
149     :param line: line to parse
150     :return: tuple with (bzr_revno, revid)
151     """
152     assert not '\n' in line
153     try:
154         (revno, revid) = line.split(' ', 1)
155     except ValueError:
156         raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_ID, 
157                 "missing space")
158     if revid == "":
159         raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_ID,
160                 "empty revision id")
161     return (int(revno), revid)
162
163
164 def parse_revision_metadata(text, rev):
165     """Parse a revision info text (as set in bzr:revision-info).
166
167     :param text: text to parse
168     :param rev: Revision object to apply read parameters to
169     """
170     in_properties = False
171     for l in text.splitlines():
172         try:
173             key, value = l.split(": ", 2)
174         except ValueError:
175             raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_INFO, 
176                     "Missing : in revision metadata")
177         if key == "committer":
178             rev.committer = str(value)
179         elif key == "timestamp":
180             (rev.timestamp, rev.timezone) = unpack_highres_date(value)
181         elif key == "properties":
182             in_properties = True
183         elif key[0] == "\t" and in_properties:
184             rev.properties[str(key[1:])] = str(value)
185         else:
186             raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_INFO, 
187                     "Invalid key %r" % key)
188
189
190 def generate_revision_metadata(timestamp, timezone, committer, revprops):
191     """Generate revision metadata text for the specified revision 
192     properties.
193
194     :param timestamp: timestamp of the revision, in seconds since epoch
195     :param timezone: timezone, specified by offset from GMT in seconds
196     :param committer: name/email of the committer
197     :param revprops: dictionary with custom revision properties
198     :return: text with data to set bzr:revision-info to.
199     """
200     assert timestamp is None or isinstance(timestamp, float)
201     text = ""
202     if timestamp is not None:
203         text += "timestamp: %s\n" % format_highres_date(timestamp, timezone) 
204     if committer is not None:
205         text += "committer: %s\n" % committer
206     if revprops is not None and revprops != {}:
207         text += "properties: \n"
208         for k, v in sorted(revprops.items()):
209             text += "\t%s: %s\n" % (k, v)
210     return text
211
212
213 def parse_svk_feature(feature):
214     """Parse a svk feature identifier.
215
216     :param feature: The feature identifier as string.
217     :return: tuple with uuid, branch path and revnum
218     """
219     try:
220         (uuid, branch, revnum) = feature.split(":", 3)
221     except ValueError:
222         raise errors.InvalidPropertyValue(SVN_PROP_SVK_MERGE, 
223                 "not enough colons")
224     return (uuid, branch.strip("/"), int(revnum))
225
226
227 def revision_id_to_svk_feature(revid):
228     """Create a SVK feature identifier from a revision id.
229
230     :param revid: Revision id to convert.
231     :return: Matching SVK feature identifier.
232     """
233     (uuid, branch, revnum, _) = parse_svn_revision_id(revid)
234     # TODO: What about renamed revisions? Should use 
235     # repository.lookup_revision_id here.
236     return "%s:/%s:%d" % (uuid, branch, revnum)
237
238
239 class SvnRepositoryFormat(RepositoryFormat):
240     """Repository format for Subversion repositories (accessed using svn_ra).
241     """
242     rich_root_data = True
243
244     def __get_matchingbzrdir(self):
245         from format import SvnFormat
246         return SvnFormat()
247
248     _matchingbzrdir = property(__get_matchingbzrdir)
249
250     def __init__(self):
251         super(SvnRepositoryFormat, self).__init__()
252
253     def get_format_description(self):
254         return "Subversion Repository"
255
256     def initialize(self, url, shared=False, _internal=False):
257         """Svn repositories cannot be created (yet)."""
258         raise UninitializableFormat(self)
259
260     def check_conversion_target(self, target_repo_format):
261         return target_repo_format.rich_root_data
262
263 cachedbs = {}
264
265 class SvnRepository(Repository):
266     """
267     Provides a simplified interface to a Subversion repository 
268     by using the RA (remote access) API from subversion
269     """
270     def __init__(self, bzrdir, transport, branch_path=None):
271         from fileids import SimpleFileIdMap
272         _revision_store = None
273
274         assert isinstance(transport, Transport)
275
276         control_files = LockableFiles(transport, '', TransportLock)
277         Repository.__init__(self, SvnRepositoryFormat(), bzrdir, 
278             control_files, None, None, None)
279
280         self.transport = transport
281         self.uuid = transport.get_uuid()
282         assert self.uuid is not None
283         self.base = transport.base
284         assert self.base is not None
285         self._serializer = None
286         self.dir_cache = {}
287         self.pool = Pool()
288         self.config = SvnRepositoryConfig(self.uuid)
289         self.config.add_location(self.base)
290         self._revids_seen = {}
291         cache_dir = self.create_cache_dir()
292         cachedir_transport = get_transport(cache_dir)
293         cache_file = os.path.join(cache_dir, 'cache-v%d' % MAPPING_VERSION)
294         if not cachedbs.has_key(cache_file):
295             cachedbs[cache_file] = sqlite3.connect(cache_file)
296         self.cachedb = cachedbs[cache_file]
297
298         self._log = logwalker.LogWalker(transport=transport, 
299                                         cache_db=self.cachedb)
300
301         self.branchprop_list = BranchPropertyList(self._log, self.cachedb)
302         self.fileid_map = SimpleFileIdMap(self, cachedir_transport)
303         self.revmap = RevidMap(self.cachedb)
304         self._scheme = None
305         self._hinted_branch_path = branch_path
306
307     def lhs_missing_revisions(self, revhistory, stop_revision):
308         missing = []
309         slice = revhistory[:revhistory.index(stop_revision)+1]
310         for revid in reversed(slice):
311             if self.has_revision(revid):
312                 missing.reverse()
313                 return missing
314             missing.append(revid)
315         raise UnrelatedBranches()
316     
317     def get_transaction(self):
318         raise NotImplementedError(self.get_transaction)
319
320     def get_scheme(self):
321         """Determine the branching scheme to use for this repository.
322
323         :return: Branching scheme.
324         """
325         if self._scheme is not None:
326             return self._scheme
327
328         scheme = self.config.get_branching_scheme()
329         if scheme is not None:
330             self._scheme = scheme
331             return scheme
332
333         last_revnum = self.transport.get_latest_revnum()
334         scheme = self._get_property_scheme(last_revnum)
335         if scheme is not None:
336             self.set_branching_scheme(scheme)
337             return scheme
338
339         self.set_branching_scheme(
340             self._guess_scheme(last_revnum, self._hinted_branch_path),
341             store=(last_revnum > 20))
342
343         return self._scheme
344
345     def _get_property_scheme(self, revnum=None):
346         if revnum is None:
347             revnum = self.transport.get_latest_revnum()
348         text = self.branchprop_list.get_property("", 
349             revnum, SVN_PROP_BZR_BRANCHING_SCHEME, None)
350         if text is None:
351             return None
352         return ListBranchingScheme(parse_list_scheme_text(text))
353
354     def set_property_scheme(self, scheme):
355         def done(revision, date, author):
356             pass
357         editor = self.transport.get_commit_editor(
358                 "Updating branching scheme for Bazaar.",
359                 done, None, False)
360         root = editor.open_root(-1)
361         editor.change_dir_prop(root, SVN_PROP_BZR_BRANCHING_SCHEME, 
362                 "".join(map(lambda x: x+"\n", scheme.branch_list)).encode("utf-8"))
363         editor.close_directory(root)
364         editor.close()
365
366     def _guess_scheme(self, last_revnum, branch_path=None):
367         scheme = guess_scheme_from_history(
368             self._log.follow_path("", last_revnum), last_revnum, 
369             branch_path)
370         mutter("Guessed branching scheme: %r" % scheme)
371         return scheme
372
373     def set_branching_scheme(self, scheme, store=True):
374         self._scheme = scheme
375         if store:
376             self.config.set_branching_scheme(str(scheme))
377
378     def _warn_if_deprecated(self):
379         # This class isn't deprecated
380         pass
381
382     def __repr__(self):
383         return '%s(%r)' % (self.__class__.__name__, 
384                            self.base)
385
386     def create_cache_dir(self):
387         cache_dir = create_cache_dir()
388         dir = os.path.join(cache_dir, self.uuid)
389         if not os.path.exists(dir):
390             os.mkdir(dir)
391         return dir
392
393     def _check(self, revision_ids):
394         return BranchCheckResult(self)
395
396     def get_inventory(self, revision_id):
397         assert revision_id != None
398         return self.revision_tree(revision_id).inventory
399
400     def get_fileid_map(self, revnum, path, scheme):
401         return self.fileid_map.get_map(self.uuid, revnum, path, 
402                                        self.revision_fileid_renames, scheme)
403
404     def transform_fileid_map(self, uuid, revnum, branch, changes, renames, 
405                              scheme):
406         return self.fileid_map.apply_changes(uuid, revnum, branch, changes, 
407                                              renames, scheme)
408
409     def all_revision_ids(self, scheme=None):
410         if scheme is None:
411             scheme = self.get_scheme()
412         for (bp, rev) in self.follow_history(
413                 self.transport.get_latest_revnum(), scheme):
414             yield self.generate_revision_id(rev, bp, str(scheme))
415
416     def get_inventory_weave(self):
417         """See Repository.get_inventory_weave()."""
418         raise NotImplementedError(self.get_inventory_weave)
419
420     def set_make_working_trees(self, new_value):
421         """See Repository.set_make_working_trees()."""
422         pass # FIXME: ignored, nowhere to store it... 
423
424     def make_working_trees(self):
425         """See Repository.make_working_trees().
426
427         Always returns False, as working trees are never created inside 
428         Subversion repositories.
429         """
430         return False
431
432     def get_ancestry(self, revision_id, topo_sorted=True):
433         """See Repository.get_ancestry().
434         
435         Note: only the first bit is topologically ordered!
436         """
437         if revision_id is None: 
438             return [None]
439
440         (path, revnum, scheme) = self.lookup_revision_id(revision_id)
441
442         ancestry = [revision_id]
443
444         for l in self.branchprop_list.get_property(path, revnum, 
445                                     SVN_PROP_BZR_ANCESTRY+str(scheme), "").splitlines():
446             ancestry.extend(l.split("\n"))
447
448         if revnum > 0:
449             for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
450                 ancestry.append(
451                     self.generate_revision_id(rev, branch, str(scheme)))
452
453         ancestry.append(None)
454         ancestry.reverse()
455         return ancestry
456
457     def has_revision(self, revision_id):
458         """See Repository.has_revision()."""
459         if revision_id is None:
460             return True
461
462         try:
463             (path, revnum, _) = self.lookup_revision_id(revision_id)
464         except NoSuchRevision:
465             return False
466
467         try:
468             return (svn.core.svn_node_dir == self.transport.check_path(path, revnum))
469         except SubversionException, (_, num):
470             if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
471                 return False
472             raise
473
474
475     def revision_trees(self, revids):
476         """See Repository.revision_trees()."""
477         for revid in revids:
478             yield self.revision_tree(revid)
479
480     def revision_tree(self, revision_id):
481         """See Repository.revision_tree()."""
482         if revision_id is None:
483             revision_id = NULL_REVISION
484
485         if revision_id == NULL_REVISION:
486             inventory = Inventory(root_id=None)
487             inventory.revision_id = revision_id
488             return RevisionTree(self, inventory, revision_id)
489
490         return SvnRevisionTree(self, revision_id)
491
492     def revision_fileid_renames(self, revid):
493         """Check which files were renamed in a particular revision.
494         
495         :param revid: Id of revision to look up.
496         :return: dictionary with paths as keys, file ids as values
497         """
498         (path, revnum, _) = self.lookup_revision_id(revid)
499         # Only consider bzr:file-ids if this is a bzr revision
500         if not self.branchprop_list.touches_property(path, revnum, 
501                 SVN_PROP_BZR_REVISION_INFO):
502             return {}
503         fileids = self.branchprop_list.get_property(path, revnum, 
504                                                     SVN_PROP_BZR_FILEIDS)
505         if fileids is None:
506             return {}
507         ret = {}
508         for line in fileids.splitlines():
509             (path, key) = line.split("\t", 2)
510             ret[urllib.unquote(path)] = osutils.safe_file_id(key)
511         return ret
512
513     def _mainline_revision_parent(self, path, revnum, scheme):
514         """Find the mainline parent of the specified revision.
515
516         :param path: Path of the revision in Subversion
517         :param revnum: Subversion revision number
518         :param scheme: Name of branching scheme to use
519         :return: Revision id of the left-hand-side parent or None if 
520                   this is the first revision
521         """
522         assert isinstance(path, basestring)
523         assert isinstance(revnum, int)
524
525         if not scheme.is_branch(path) and \
526            not scheme.is_tag(path):
527             raise NoSuchRevision(self, 
528                     self.generate_revision_id(revnum, path, str(scheme)))
529
530         it = self.follow_branch(path, revnum, scheme)
531         # the first tuple returned should match the one specified. 
532         # if it's not, then the branch, revnum didn't change in the specified 
533         # revision and so it is invalid
534         if (path, revnum) != it.next():
535             raise NoSuchRevision(self, 
536                     self.generate_revision_id(revnum, path, str(scheme)))
537         try:
538             (branch, rev) = it.next()
539             return self.generate_revision_id(rev, branch, str(scheme))
540         except StopIteration:
541             # The specified revision was the first one in the branch
542             return None
543
544     def _bzr_merged_revisions(self, branch, revnum, scheme):
545         """Find out what revisions were merged by Bazaar in a revision.
546
547         :param branch: Subversion branch path.
548         :param revnum: Subversion revision number.
549         :param scheme: Branching scheme.
550         """
551         change = self.branchprop_list.get_property_diff(branch, revnum, 
552                                        SVN_PROP_BZR_ANCESTRY+str(scheme)).splitlines()
553         if len(change) == 0:
554             return []
555
556         assert len(change) == 1
557
558         return parse_merge_property(change[0])
559
560     def _svk_feature_to_revision_id(self, scheme, feature):
561         """Convert a SVK feature to a revision id for this repository.
562
563         :param scheme: Branching scheme.
564         :param feature: SVK feature.
565         :return: revision id.
566         """
567         try:
568             (uuid, bp, revnum) = parse_svk_feature(feature)
569         except errors.InvalidPropertyValue:
570             return None
571         if uuid != self.uuid:
572             return None
573         if not scheme.is_branch(bp) and not scheme.is_tag(bp):
574             return None
575         return self.generate_revision_id(revnum, bp, str(scheme))
576
577     def _svk_merged_revisions(self, branch, revnum, scheme):
578         """Find out what SVK features were merged in a revision.
579
580         :param branch: Subversion branch path.
581         :param revnum: Subversion revision number.
582         :param scheme: Branching scheme.
583         """
584         current = set(self.branchprop_list.get_property(branch, revnum, SVN_PROP_SVK_MERGE, "").splitlines())
585         (prev_path, prev_revnum) = self._log.get_previous(branch, revnum)
586         if prev_path is None and prev_revnum == -1:
587             previous = set()
588         else:
589             previous = set(self.branchprop_list.get_property(prev_path.encode("utf-8"), 
590                          prev_revnum, SVN_PROP_SVK_MERGE, "").splitlines())
591         for feature in current.difference(previous):
592             revid = self._svk_feature_to_revision_id(scheme, feature)
593             if revid is not None:
594                 yield revid
595
596     def revision_parents(self, revision_id, bzr_merges=None, svk_merges=None):
597         """See Repository.revision_parents()."""
598         parent_ids = []
599         (branch, revnum, scheme) = self.lookup_revision_id(revision_id)
600         mainline_parent = self._mainline_revision_parent(branch, revnum, scheme)
601         if mainline_parent is not None:
602             parent_ids.append(mainline_parent)
603
604         # if the branch didn't change, bzr:merge or svk:merge can't have changed
605         if not self._log.touches_path(branch, revnum):
606             return parent_ids
607        
608         if bzr_merges is None:
609             bzr_merges = self._bzr_merged_revisions(branch, revnum, scheme)
610         if svk_merges is None:
611             svk_merges = self._svk_merged_revisions(branch, revnum, scheme)
612
613         parent_ids.extend(bzr_merges)
614
615         if bzr_merges == []:
616             # Commit was doing using svk apparently
617             parent_ids.extend(svk_merges)
618
619         return parent_ids
620
621     def get_revision(self, revision_id):
622         """See Repository.get_revision."""
623         if not revision_id or not isinstance(revision_id, basestring):
624             raise InvalidRevisionId(revision_id=revision_id, branch=self)
625
626         (path, revnum, _) = self.lookup_revision_id(revision_id)
627         
628         parent_ids = self.revision_parents(revision_id)
629
630         # Commit SVN revision properties to a Revision object
631         rev = Revision(revision_id=revision_id, parent_ids=parent_ids)
632
633         (rev.committer, rev.message, date) = self._log.get_revision_info(revnum)
634         if rev.committer is None:
635             rev.committer = ""
636
637         if date is not None:
638             rev.timestamp = 1.0 * svn.core.secs_from_timestr(date, None)
639         else:
640             rev.timestamp = 0.0 # FIXME: Obtain repository creation time
641         rev.timezone = None
642         rev.properties = {}
643         parse_revision_metadata(
644                 self.branchprop_list.get_property(path, revnum, 
645                      SVN_PROP_BZR_REVISION_INFO, ""), rev)
646
647         rev.inventory_sha1 = property(
648             lambda: self.get_inventory_sha1(revision_id))
649
650         return rev
651
652     def get_revisions(self, revision_ids):
653         """See Repository.get_revisions()."""
654         # TODO: More efficient implementation?
655         return map(self.get_revision, revision_ids)
656
657     def add_revision(self, rev_id, rev, inv=None, config=None):
658         raise NotImplementedError(self.add_revision)
659
660     def generate_revision_id(self, revnum, path, scheme):
661         """Generate an unambiguous revision id. 
662         
663         :param revnum: Subversion revision number.
664         :param path: Branch path.
665         :param scheme: Branching scheme name
666
667         :return: New revision id.
668         """
669         assert isinstance(path, str)
670         assert isinstance(revnum, int)
671
672         # Look in the cache to see if it already has a revision id
673         revid = self.revmap.lookup_branch_revnum(revnum, path, scheme)
674         if revid is not None:
675             return revid
676
677         # Lookup the revision from the bzr:revision-id-vX property
678         line = self.branchprop_list.get_property_diff(path, revnum, 
679                 SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n")
680         # Or generate it
681         if line == "":
682             revid = generate_svn_revision_id(self.uuid, revnum, path, 
683                                              scheme)
684         else:
685             try:
686                 (bzr_revno, revid) = parse_revid_property(line)
687                 self.revmap.insert_revid(revid, path, revnum, revnum, 
688                         scheme, bzr_revno)
689             except errors.InvalidPropertyValue, e:
690                 mutter(str(e))
691                 revid = generate_svn_revision_id(self.uuid, revnum, path, 
692                                                  scheme)
693                 self.revmap.insert_revid(revid, path, revnum, revnum, 
694                         scheme)
695
696         return revid
697
698     def lookup_revision_id(self, revid, scheme=None):
699         """Parse an existing Subversion-based revision id.
700
701         :param revid: The revision id.
702         :param scheme: Optional branching scheme to use when searching for 
703                        revisions
704         :raises: NoSuchRevision
705         :return: Tuple with branch path, revision number and scheme.
706         """
707         def get_scheme(name):
708             assert isinstance(name, basestring)
709             return BranchingScheme.find_scheme(name)
710
711         # Try a simple parse
712         try:
713             (uuid, branch_path, revnum, schemen) = parse_svn_revision_id(revid)
714             assert isinstance(branch_path, str)
715             if uuid == self.uuid:
716                 return (branch_path, revnum, get_scheme(schemen))
717             # If the UUID doesn't match, this may still be a valid revision
718             # id; a revision from another SVN repository may be pushed into 
719             # this one.
720         except InvalidRevisionId:
721             pass
722
723         # Check the record out of the revmap, if it exists
724         try:
725             (branch_path, min_revnum, max_revnum, \
726                     scheme) = self.revmap.lookup_revid(revid)
727             assert isinstance(branch_path, str)
728             # Entry already complete?
729             if min_revnum == max_revnum:
730                 return (branch_path, min_revnum, get_scheme(scheme))
731         except NoSuchRevision, e:
732             # If there is no entry in the map, walk over all branches:
733             if scheme is None:
734                 scheme = self.get_scheme()
735             last_revnum = self.transport.get_latest_revnum()
736             if (self._revids_seen.has_key(str(scheme)) and 
737                 last_revnum <= self._revids_seen[str(scheme)]):
738                 # All revision ids in this repository for the current 
739                 # scheme have already been discovered. No need to 
740                 # check again.
741                 raise e
742             found = False
743             for (branch, revno, _) in self.find_branches(scheme, last_revnum):
744                 # Look at their bzr:revision-id-vX
745                 revids = []
746                 for line in self.branchprop_list.get_property(branch, revno, 
747                         SVN_PROP_BZR_REVISION_ID+str(scheme), "").splitlines():
748                     try:
749                         revids.append(parse_revid_property(line))
750                     except errors.InvalidPropertyValue, ie:
751                         mutter(str(ie))
752
753                 # If there are any new entries that are not yet in the cache, 
754                 # add them
755                 for (entry_revno, entry_revid) in revids:
756                     if entry_revid == revid:
757                         found = True
758                     self.revmap.insert_revid(entry_revid, branch, 0, revno, 
759                             str(scheme), entry_revno)
760
761                 if found:
762                     break
763                 
764             if not found:
765                 # We've added all the revision ids for this scheme in the repository,
766                 # so no need to check again unless new revisions got added
767                 self._revids_seen[str(scheme)] = last_revnum
768                 raise e
769             (branch_path, min_revnum, max_revnum, scheme) = self.revmap.lookup_revid(revid)
770             assert isinstance(branch_path, str)
771
772         # Find the branch property between min_revnum and max_revnum that 
773         # added revid
774         for (bp, rev) in self.follow_branch(branch_path, max_revnum, 
775                                             get_scheme(scheme)):
776             try:
777                 (entry_revno, entry_revid) = parse_revid_property(
778                  self.branchprop_list.get_property_diff(bp, rev, 
779                      SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n"))
780             except errors.InvalidPropertyValue:
781                 # Don't warn about encountering an invalid property, 
782                 # that will already have happened earlier
783                 continue
784             if entry_revid == revid:
785                 self.revmap.insert_revid(revid, bp, rev, rev, scheme, 
786                                          entry_revno)
787                 return (bp, rev, get_scheme(scheme))
788
789         raise AssertionError("Revision id %s was added incorrectly" % revid)
790
791     def get_inventory_xml(self, revision_id):
792         """See Repository.get_inventory_xml()."""
793         return bzrlib.xml5.serializer_v5.write_inventory_to_string(
794             self.get_inventory(revision_id))
795
796     def get_inventory_sha1(self, revision_id):
797         """Get the sha1 for the XML representation of an inventory.
798
799         :param revision_id: Revision id of the inventory for which to return 
800          the SHA1.
801         :return: XML string
802         """
803
804         return osutils.sha_string(self.get_inventory_xml(revision_id))
805
806     def get_revision_xml(self, revision_id):
807         """Return the XML representation of a revision.
808
809         :param revision_id: Revision for which to return the XML.
810         :return: XML string
811         """
812         return bzrlib.xml5.serializer_v5.write_revision_to_string(
813             self.get_revision(revision_id))
814
815     def follow_history(self, revnum, scheme):
816         """Yield all the branches found between the start of history 
817         and a specified revision number.
818
819         :param revnum: Revision number up to which to search.
820         :return: iterator over branches in the range 0..revnum
821         """
822         assert scheme is not None
823
824         while revnum >= 0:
825             yielded_paths = []
826             paths = self._log.get_revision_paths(revnum)
827             for p in paths:
828                 try:
829                     bp = scheme.unprefix(p)[0]
830                     if not bp in yielded_paths:
831                         if not paths.has_key(bp) or paths[bp][0] != 'D':
832                             assert revnum > 0 or bp == ""
833                             yield (bp, revnum)
834                         yielded_paths.append(bp)
835                 except NotBranchError:
836                     pass
837             revnum -= 1
838
839     def follow_branch(self, branch_path, revnum, scheme):
840         """Follow the history of a branch. Will yield all the 
841         left-hand side ancestors of a specified revision.
842     
843         :param branch_path: Subversion path to search.
844         :param revnum: Revision number in Subversion to start.
845         :param scheme: Name of the branching scheme to use
846         :return: iterator over the ancestors
847         """
848         assert branch_path is not None
849         assert isinstance(branch_path, str)
850         assert isinstance(revnum, int) and revnum >= 0
851         assert scheme.is_branch(branch_path) or scheme.is_tag(branch_path)
852         branch_path = branch_path.strip("/")
853
854         while revnum >= 0:
855             assert revnum > 0 or branch_path == ""
856             paths = self._log.get_revision_paths(revnum)
857
858             yielded = False
859             # If something underneath branch_path changed, there is a 
860             # revision there, so yield it.
861             for p in paths:
862                 assert isinstance(p, str)
863                 if (p == branch_path or 
864                     p.startswith(branch_path+"/") or 
865                     branch_path == ""):
866                     yield (branch_path, revnum)
867                     yielded = True
868                     break
869             
870             # If there are no special cases, just go try the 
871             # next revnum in history
872             revnum -= 1
873
874             # Make sure we get the right location for next time, if 
875             # the branch itself was copied
876             if (paths.has_key(branch_path) and 
877                 paths[branch_path][0] in ('R', 'A')):
878                 if not yielded:
879                     yield (branch_path, revnum+1)
880                 if paths[branch_path][1] is None:
881                     return
882                 if not scheme.is_branch(paths[branch_path][1]) and \
883                    not scheme.is_tag(paths[branch_path][1]):
884                     # FIXME: if copyfrom_path is not a branch path, 
885                     # should simulate a reverse "split" of a branch
886                     # for now, just make it look like the branch ended here
887                     return
888                 revnum = paths[branch_path][2]
889                 branch_path = paths[branch_path][1].encode("utf-8")
890                 continue
891             
892             # Make sure we get the right location for the next time if 
893             # one of the parents changed
894
895             # Path names need to be sorted so the longer paths 
896             # override the shorter ones
897             for p in sorted(paths.keys(), reverse=True):
898                 if paths[p][0] == 'M':
899                     continue
900                 if branch_path.startswith(p+"/"):
901                     assert paths[p][0] in ('A', 'R'), "Parent wasn't added"
902                     assert paths[p][1] is not None, \
903                         "Empty parent added, but child wasn't added !?"
904
905                     revnum = paths[p][2]
906                     branch_path = paths[p][1].encode("utf-8") + branch_path[len(p):]
907                     break
908
909     def follow_branch_history(self, branch_path, revnum, scheme):
910         """Return all the changes that happened in a branch 
911         between branch_path and revnum. 
912
913         :return: iterator that returns tuples with branch path, 
914             changed paths and revision number.
915         """
916         assert branch_path is not None
917         assert scheme.is_branch(branch_path) or scheme.is_tag(branch_path)
918
919         for (bp, paths, revnum) in self._log.follow_path(branch_path, revnum):
920             assert revnum > 0 or bp == ""
921             assert scheme.is_branch(bp) or schee.is_tag(bp)
922             # Remove non-bp paths from paths
923             for p in paths.keys():
924                 if not p.startswith(bp+"/") and bp != p and bp != "":
925                     del paths[p]
926
927             if paths == {}:
928                 continue
929
930             if (paths.has_key(bp) and paths[bp][1] is not None and 
931                 not scheme.is_branch(paths[bp][1]) and
932                 not scheme.is_tag(paths[bp][1])):
933                 # FIXME: if copyfrom_path is not a branch path, 
934                 # should simulate a reverse "split" of a branch
935                 # for now, just make it look like the branch ended here
936                 for c in self._log.find_children(paths[bp][1], paths[bp][2]):
937                     path = c.replace(paths[bp][1], bp+"/", 1).replace("//", "/")
938                     paths[path] = ('A', None, -1)
939                 paths[bp] = ('A', None, -1)
940
941                 yield (bp, paths, revnum)
942                 return
943                      
944             yield (bp, paths, revnum)
945
946     def has_signature_for_revision_id(self, revision_id):
947         """Check whether a signature exists for a particular revision id.
948
949         :param revision_id: Revision id for which the signatures should be looked up.
950         :return: False, as no signatures are stored for revisions in Subversion 
951             at the moment.
952         """
953         # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE 
954         return False # SVN doesn't store GPG signatures. Perhaps 
955                      # store in SVN revision property?
956
957
958     def get_signature_text(self, revision_id):
959         """Return the signature text for a particular revision.
960
961         :param revision_id: Id of the revision for which to return the 
962                             signature.
963         :raises NoSuchRevision: Always
964         """
965         # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE 
966         # SVN doesn't store GPG signatures
967         raise NoSuchRevision(self, revision_id)
968
969     def _full_revision_graph(self, scheme, _latest_revnum=None):
970         if _latest_revnum is None:
971             _latest_revnum = self.transport.get_latest_revnum()
972         graph = {}
973         for (branch, revnum) in self.follow_history(_latest_revnum, 
974                                                     scheme):
975             mutter('%r, %r' % (branch, revnum))
976             revid = self.generate_revision_id(revnum, branch, str(scheme))
977             graph[revid] = self.revision_parents(revid)
978         return graph
979
980     def get_revision_graph(self, revision_id=None):
981         """See Repository.get_revision_graph()."""
982         if revision_id == NULL_REVISION:
983             return {}
984
985         if revision_id is None:
986             return self._full_revision_graph(self.get_scheme())
987
988         (path, revnum, scheme) = self.lookup_revision_id(revision_id)
989
990         _previous = revision_id
991         self._ancestry = {}
992         
993         if revnum > 0:
994             for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
995                 revid = self.generate_revision_id(rev, branch, str(scheme))
996                 self._ancestry[_previous] = [revid]
997                 _previous = revid
998
999         self._ancestry[_previous] = []
1000
1001         return self._ancestry
1002
1003     def find_branches(self, scheme, revnum=None):
1004         """Find all branches that were changed in the specified revision number.
1005
1006         :param revnum: Revision to search for branches.
1007         :return: iterator that returns tuples with (path, revision number, still exists). The revision number is the revision in which the branch last existed.
1008         """
1009         assert scheme is not None
1010         if revnum is None:
1011             revnum = self.transport.get_latest_revnum()
1012
1013         created_branches = {}
1014
1015         ret = []
1016
1017         pb = ui.ui_factory.nested_progress_bar()
1018         try:
1019             for i in range(revnum+1):
1020                 pb.update("finding branches", i, revnum+1)
1021                 paths = self._log.get_revision_paths(i)
1022                 for p in sorted(paths.keys()):
1023                     if scheme.is_branch(p) or scheme.is_tag(p):
1024                         if paths[p][0] in ('R', 'D'):
1025                             del created_branches[p]
1026                             j = self._log.find_latest_change(p, i-1, 
1027                                 recurse=True)
1028                             ret.append((p, j, False))
1029
1030                         if paths[p][0] in ('A', 'R'): 
1031                             created_branches[p] = i
1032                     elif scheme.is_branch_parent(p) or \
1033                             scheme.is_tag_parent(p):
1034                         if paths[p][0] in ('R', 'D'):
1035                             k = created_branches.keys()
1036                             for c in k:
1037                                 if c.startswith(p+"/"):
1038                                     del created_branches[c] 
1039                                     j = self._log.find_latest_change(c, i-1, 
1040                                             recurse=True)
1041                                     ret.append((c, j, False))
1042                         if paths[p][0] in ('A', 'R'):
1043                             parents = [p]
1044                             while parents:
1045                                 p = parents.pop()
1046                                 for c in self.transport.get_dir(p, i)[0].keys():
1047                                     n = p+"/"+c
1048                                     if scheme.is_branch(n) or scheme.is_tag(n):
1049                                         created_branches[n] = i
1050                                     elif (scheme.is_branch_parent(n) or 
1051                                           scheme.is_tag_parent(n)):
1052                                         parents.append(n)
1053         finally:
1054             pb.finished()
1055
1056         for p in created_branches:
1057             j = self._log.find_latest_change(p, revnum, recurse=True)
1058             if j is None:
1059                 j = created_branches[p]
1060             ret.append((p, j, True))
1061
1062         return ret
1063
1064     def is_shared(self):
1065         """Return True if this repository is flagged as a shared repository."""
1066         return True
1067
1068     def get_physical_lock_status(self):
1069         return False
1070
1071     def get_commit_builder(self, branch, parents, config, timestamp=None, 
1072                            timezone=None, committer=None, revprops=None, 
1073                            revision_id=None):
1074         from commit import SvnCommitBuilder
1075         return SvnCommitBuilder(self, branch, parents, config, timestamp, 
1076                 timezone, committer, revprops, revision_id)
1077
1078
1079