Support possible_transports argument.
[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 remote import SvnRemoteFormat
246         return SvnRemoteFormat()
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 bzrlib.plugins.svn import lazy_register_optimizers
272         lazy_register_optimizers()
273         from fileids import SimpleFileIdMap
274         _revision_store = None
275
276         assert isinstance(transport, Transport)
277
278         control_files = LockableFiles(transport, '', TransportLock)
279         Repository.__init__(self, SvnRepositoryFormat(), bzrdir, 
280             control_files, None, None, None)
281
282         self.transport = transport
283         self.uuid = transport.get_uuid()
284         assert self.uuid is not None
285         self.base = transport.base
286         assert self.base is not None
287         self._serializer = None
288         self.dir_cache = {}
289         self.pool = Pool()
290         self.config = SvnRepositoryConfig(self.uuid)
291         self.config.add_location(self.base)
292         self._revids_seen = {}
293         cache_dir = self.create_cache_dir()
294         cachedir_transport = get_transport(cache_dir)
295         cache_file = os.path.join(cache_dir, 'cache-v%d' % MAPPING_VERSION)
296         if not cachedbs.has_key(cache_file):
297             cachedbs[cache_file] = sqlite3.connect(cache_file)
298         self.cachedb = cachedbs[cache_file]
299
300         self._log = logwalker.LogWalker(transport=transport, 
301                                         cache_db=self.cachedb)
302
303         self.branchprop_list = BranchPropertyList(self._log, self.cachedb)
304         self.fileid_map = SimpleFileIdMap(self, cachedir_transport)
305         self.revmap = RevidMap(self.cachedb)
306         self._scheme = None
307         self._hinted_branch_path = branch_path
308
309     def lhs_missing_revisions(self, revhistory, stop_revision):
310         missing = []
311         slice = revhistory[:revhistory.index(stop_revision)+1]
312         for revid in reversed(slice):
313             if self.has_revision(revid):
314                 missing.reverse()
315                 return missing
316             missing.append(revid)
317         raise UnrelatedBranches()
318     
319     def get_transaction(self):
320         raise NotImplementedError(self.get_transaction)
321
322     def get_scheme(self):
323         """Determine the branching scheme to use for this repository.
324
325         :return: Branching scheme.
326         """
327         if self._scheme is not None:
328             return self._scheme
329
330         scheme = self.config.get_branching_scheme()
331         if scheme is not None:
332             self._scheme = scheme
333             return scheme
334
335         last_revnum = self.transport.get_latest_revnum()
336         scheme = self._get_property_scheme(last_revnum)
337         if scheme is not None:
338             self.set_branching_scheme(scheme)
339             return scheme
340
341         self.set_branching_scheme(
342             self._guess_scheme(last_revnum, self._hinted_branch_path),
343             store=(last_revnum > 20))
344
345         return self._scheme
346
347     def _get_property_scheme(self, revnum=None):
348         if revnum is None:
349             revnum = self.transport.get_latest_revnum()
350         text = self.branchprop_list.get_property("", 
351             revnum, SVN_PROP_BZR_BRANCHING_SCHEME, None)
352         if text is None:
353             return None
354         return ListBranchingScheme(parse_list_scheme_text(text))
355
356     def set_property_scheme(self, scheme):
357         def done(revision, date, author):
358             pass
359         editor = self.transport.get_commit_editor(
360                 {svn.core.SVN_PROP_REVISION_LOG: "Updating branching scheme for Bazaar."},
361                 done, None, False)
362         root = editor.open_root(-1)
363         editor.change_dir_prop(root, SVN_PROP_BZR_BRANCHING_SCHEME, 
364                 "".join(map(lambda x: x+"\n", scheme.branch_list)).encode("utf-8"))
365         editor.close_directory(root)
366         editor.close()
367
368     def _guess_scheme(self, last_revnum, branch_path=None):
369         scheme = guess_scheme_from_history(
370             self._log.follow_path("", last_revnum), last_revnum, 
371             branch_path)
372         mutter("Guessed branching scheme: %r" % scheme)
373         return scheme
374
375     def set_branching_scheme(self, scheme, store=True):
376         self._scheme = scheme
377         if store:
378             self.config.set_branching_scheme(str(scheme))
379
380     def _warn_if_deprecated(self):
381         # This class isn't deprecated
382         pass
383
384     def __repr__(self):
385         return '%s(%r)' % (self.__class__.__name__, 
386                            self.base)
387
388     def create_cache_dir(self):
389         cache_dir = create_cache_dir()
390         dir = os.path.join(cache_dir, self.uuid)
391         if not os.path.exists(dir):
392             os.mkdir(dir)
393         return dir
394
395     def _check(self, revision_ids):
396         return BranchCheckResult(self)
397
398     def get_inventory(self, revision_id):
399         assert revision_id != None
400         return self.revision_tree(revision_id).inventory
401
402     def get_fileid_map(self, revnum, path, scheme):
403         return self.fileid_map.get_map(self.uuid, revnum, path, 
404                                        self.revision_fileid_renames, scheme)
405
406     def transform_fileid_map(self, uuid, revnum, branch, changes, renames, 
407                              scheme):
408         return self.fileid_map.apply_changes(uuid, revnum, branch, changes, 
409                                              renames, scheme)
410
411     def all_revision_ids(self, scheme=None):
412         if scheme is None:
413             scheme = self.get_scheme()
414         for (bp, rev) in self.follow_history(
415                 self.transport.get_latest_revnum(), scheme):
416             yield self.generate_revision_id(rev, bp, str(scheme))
417
418     def get_inventory_weave(self):
419         """See Repository.get_inventory_weave()."""
420         raise NotImplementedError(self.get_inventory_weave)
421
422     def set_make_working_trees(self, new_value):
423         """See Repository.set_make_working_trees()."""
424         pass # FIXME: ignored, nowhere to store it... 
425
426     def make_working_trees(self):
427         """See Repository.make_working_trees().
428
429         Always returns False, as working trees are never created inside 
430         Subversion repositories.
431         """
432         return False
433
434     def get_ancestry(self, revision_id, topo_sorted=True):
435         """See Repository.get_ancestry().
436         
437         Note: only the first bit is topologically ordered!
438         """
439         if revision_id is None: 
440             return [None]
441
442         (path, revnum, scheme) = self.lookup_revision_id(revision_id)
443
444         ancestry = [revision_id]
445
446         for l in self.branchprop_list.get_property(path, revnum, 
447                                     SVN_PROP_BZR_ANCESTRY+str(scheme), "").splitlines():
448             ancestry.extend(l.split("\n"))
449
450         if revnum > 0:
451             for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
452                 ancestry.append(
453                     self.generate_revision_id(rev, branch, str(scheme)))
454
455         ancestry.append(None)
456         ancestry.reverse()
457         return ancestry
458
459     def has_revision(self, revision_id):
460         """See Repository.has_revision()."""
461         if revision_id is None:
462             return True
463
464         try:
465             (path, revnum, _) = self.lookup_revision_id(revision_id)
466         except NoSuchRevision:
467             return False
468
469         try:
470             return (svn.core.svn_node_dir == self.transport.check_path(path, revnum))
471         except SubversionException, (_, num):
472             if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
473                 return False
474             raise
475
476
477     def revision_trees(self, revids):
478         """See Repository.revision_trees()."""
479         for revid in revids:
480             yield self.revision_tree(revid)
481
482     def revision_tree(self, revision_id):
483         """See Repository.revision_tree()."""
484         if revision_id is None:
485             revision_id = NULL_REVISION
486
487         if revision_id == NULL_REVISION:
488             inventory = Inventory(root_id=None)
489             inventory.revision_id = revision_id
490             return RevisionTree(self, inventory, revision_id)
491
492         return SvnRevisionTree(self, revision_id)
493
494     def revision_fileid_renames(self, revid):
495         """Check which files were renamed in a particular revision.
496         
497         :param revid: Id of revision to look up.
498         :return: dictionary with paths as keys, file ids as values
499         """
500         (path, revnum, _) = self.lookup_revision_id(revid)
501         # Only consider bzr:file-ids if this is a bzr revision
502         if not self.branchprop_list.touches_property(path, revnum, 
503                 SVN_PROP_BZR_REVISION_INFO):
504             return {}
505         fileids = self.branchprop_list.get_property(path, revnum, 
506                                                     SVN_PROP_BZR_FILEIDS)
507         if fileids is None:
508             return {}
509         ret = {}
510         for line in fileids.splitlines():
511             (path, key) = line.split("\t", 2)
512             ret[urllib.unquote(path)] = osutils.safe_file_id(key)
513         return ret
514
515     def _mainline_revision_parent(self, path, revnum, scheme):
516         """Find the mainline parent of the specified revision.
517
518         :param path: Path of the revision in Subversion
519         :param revnum: Subversion revision number
520         :param scheme: Name of branching scheme to use
521         :return: Revision id of the left-hand-side parent or None if 
522                   this is the first revision
523         """
524         assert isinstance(path, basestring)
525         assert isinstance(revnum, int)
526
527         if not scheme.is_branch(path) and \
528            not scheme.is_tag(path):
529             raise NoSuchRevision(self, 
530                     self.generate_revision_id(revnum, path, str(scheme)))
531
532         it = self.follow_branch(path, revnum, scheme)
533         # the first tuple returned should match the one specified. 
534         # if it's not, then the branch, revnum didn't change in the specified 
535         # revision and so it is invalid
536         if (path, revnum) != it.next():
537             raise NoSuchRevision(self, 
538                     self.generate_revision_id(revnum, path, str(scheme)))
539         try:
540             (branch, rev) = it.next()
541             return self.generate_revision_id(rev, branch, str(scheme))
542         except StopIteration:
543             # The specified revision was the first one in the branch
544             return None
545
546     def _bzr_merged_revisions(self, branch, revnum, scheme):
547         """Find out what revisions were merged by Bazaar in a revision.
548
549         :param branch: Subversion branch path.
550         :param revnum: Subversion revision number.
551         :param scheme: Branching scheme.
552         """
553         change = self.branchprop_list.get_property_diff(branch, revnum, 
554                                        SVN_PROP_BZR_ANCESTRY+str(scheme)).splitlines()
555         if len(change) == 0:
556             return []
557
558         assert len(change) == 1
559
560         return parse_merge_property(change[0])
561
562     def _svk_feature_to_revision_id(self, scheme, feature):
563         """Convert a SVK feature to a revision id for this repository.
564
565         :param scheme: Branching scheme.
566         :param feature: SVK feature.
567         :return: revision id.
568         """
569         try:
570             (uuid, bp, revnum) = parse_svk_feature(feature)
571         except errors.InvalidPropertyValue:
572             return None
573         if uuid != self.uuid:
574             return None
575         if not scheme.is_branch(bp) and not scheme.is_tag(bp):
576             return None
577         return self.generate_revision_id(revnum, bp, str(scheme))
578
579     def _svk_merged_revisions(self, branch, revnum, scheme):
580         """Find out what SVK features were merged in a revision.
581
582         :param branch: Subversion branch path.
583         :param revnum: Subversion revision number.
584         :param scheme: Branching scheme.
585         """
586         current = set(self.branchprop_list.get_property(branch, revnum, SVN_PROP_SVK_MERGE, "").splitlines())
587         (prev_path, prev_revnum) = self._log.get_previous(branch, revnum)
588         if prev_path is None and prev_revnum == -1:
589             previous = set()
590         else:
591             previous = set(self.branchprop_list.get_property(prev_path.encode("utf-8"), 
592                          prev_revnum, SVN_PROP_SVK_MERGE, "").splitlines())
593         for feature in current.difference(previous):
594             revid = self._svk_feature_to_revision_id(scheme, feature)
595             if revid is not None:
596                 yield revid
597
598     def revision_parents(self, revision_id, bzr_merges=None, svk_merges=None):
599         """See Repository.revision_parents()."""
600         parent_ids = []
601         (branch, revnum, scheme) = self.lookup_revision_id(revision_id)
602         mainline_parent = self._mainline_revision_parent(branch, revnum, scheme)
603         if mainline_parent is not None:
604             parent_ids.append(mainline_parent)
605
606         # if the branch didn't change, bzr:merge or svk:merge can't have changed
607         if not self._log.touches_path(branch, revnum):
608             return parent_ids
609        
610         if bzr_merges is None:
611             bzr_merges = self._bzr_merged_revisions(branch, revnum, scheme)
612         if svk_merges is None:
613             svk_merges = self._svk_merged_revisions(branch, revnum, scheme)
614
615         parent_ids.extend(bzr_merges)
616
617         if bzr_merges == []:
618             # Commit was doing using svk apparently
619             parent_ids.extend(svk_merges)
620
621         return parent_ids
622
623     def get_revision(self, revision_id):
624         """See Repository.get_revision."""
625         if not revision_id or not isinstance(revision_id, basestring):
626             raise InvalidRevisionId(revision_id=revision_id, branch=self)
627
628         (path, revnum, _) = self.lookup_revision_id(revision_id)
629         
630         parent_ids = self.revision_parents(revision_id)
631
632         # Commit SVN revision properties to a Revision object
633         rev = Revision(revision_id=revision_id, parent_ids=parent_ids)
634
635         (rev.committer, rev.message, date) = self._log.get_revision_info(revnum)
636         if rev.committer is None:
637             rev.committer = ""
638
639         if date is not None:
640             rev.timestamp = 1.0 * svn.core.secs_from_timestr(date, None)
641         else:
642             rev.timestamp = 0.0 # FIXME: Obtain repository creation time
643         rev.timezone = None
644         rev.properties = {}
645         parse_revision_metadata(
646                 self.branchprop_list.get_property(path, revnum, 
647                      SVN_PROP_BZR_REVISION_INFO, ""), rev)
648
649         rev.inventory_sha1 = property(
650             lambda: self.get_inventory_sha1(revision_id))
651
652         return rev
653
654     def get_revisions(self, revision_ids):
655         """See Repository.get_revisions()."""
656         # TODO: More efficient implementation?
657         return map(self.get_revision, revision_ids)
658
659     def add_revision(self, rev_id, rev, inv=None, config=None):
660         raise NotImplementedError(self.add_revision)
661
662     def generate_revision_id(self, revnum, path, scheme):
663         """Generate an unambiguous revision id. 
664         
665         :param revnum: Subversion revision number.
666         :param path: Branch path.
667         :param scheme: Branching scheme name
668
669         :return: New revision id.
670         """
671         assert isinstance(path, str)
672         assert isinstance(revnum, int)
673
674         # Look in the cache to see if it already has a revision id
675         revid = self.revmap.lookup_branch_revnum(revnum, path, scheme)
676         if revid is not None:
677             return revid
678
679         # Lookup the revision from the bzr:revision-id-vX property
680         line = self.branchprop_list.get_property_diff(path, revnum, 
681                 SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n")
682         # Or generate it
683         if line == "":
684             revid = generate_svn_revision_id(self.uuid, revnum, path, 
685                                              scheme)
686         else:
687             try:
688                 (bzr_revno, revid) = parse_revid_property(line)
689                 self.revmap.insert_revid(revid, path, revnum, revnum, 
690                         scheme, bzr_revno)
691             except errors.InvalidPropertyValue, e:
692                 mutter(str(e))
693                 revid = generate_svn_revision_id(self.uuid, revnum, path, 
694                                                  scheme)
695                 self.revmap.insert_revid(revid, path, revnum, revnum, 
696                         scheme)
697
698         return revid
699
700     def lookup_revision_id(self, revid, scheme=None):
701         """Parse an existing Subversion-based revision id.
702
703         :param revid: The revision id.
704         :param scheme: Optional branching scheme to use when searching for 
705                        revisions
706         :raises: NoSuchRevision
707         :return: Tuple with branch path, revision number and scheme.
708         """
709         def get_scheme(name):
710             assert isinstance(name, basestring)
711             return BranchingScheme.find_scheme(name)
712
713         # Try a simple parse
714         try:
715             (uuid, branch_path, revnum, schemen) = parse_svn_revision_id(revid)
716             assert isinstance(branch_path, str)
717             if uuid == self.uuid:
718                 return (branch_path, revnum, get_scheme(schemen))
719             # If the UUID doesn't match, this may still be a valid revision
720             # id; a revision from another SVN repository may be pushed into 
721             # this one.
722         except InvalidRevisionId:
723             pass
724
725         # Check the record out of the revmap, if it exists
726         try:
727             (branch_path, min_revnum, max_revnum, \
728                     scheme) = self.revmap.lookup_revid(revid)
729             assert isinstance(branch_path, str)
730             # Entry already complete?
731             if min_revnum == max_revnum:
732                 return (branch_path, min_revnum, get_scheme(scheme))
733         except NoSuchRevision, e:
734             # If there is no entry in the map, walk over all branches:
735             if scheme is None:
736                 scheme = self.get_scheme()
737             last_revnum = self.transport.get_latest_revnum()
738             if (self._revids_seen.has_key(str(scheme)) and 
739                 last_revnum <= self._revids_seen[str(scheme)]):
740                 # All revision ids in this repository for the current 
741                 # scheme have already been discovered. No need to 
742                 # check again.
743                 raise e
744             found = False
745             for (branch, revno, _) in self.find_branches(scheme, last_revnum):
746                 # Look at their bzr:revision-id-vX
747                 revids = []
748                 for line in self.branchprop_list.get_property(branch, revno, 
749                         SVN_PROP_BZR_REVISION_ID+str(scheme), "").splitlines():
750                     try:
751                         revids.append(parse_revid_property(line))
752                     except errors.InvalidPropertyValue, ie:
753                         mutter(str(ie))
754
755                 # If there are any new entries that are not yet in the cache, 
756                 # add them
757                 for (entry_revno, entry_revid) in revids:
758                     if entry_revid == revid:
759                         found = True
760                     self.revmap.insert_revid(entry_revid, branch, 0, revno, 
761                             str(scheme), entry_revno)
762
763                 if found:
764                     break
765                 
766             if not found:
767                 # We've added all the revision ids for this scheme in the repository,
768                 # so no need to check again unless new revisions got added
769                 self._revids_seen[str(scheme)] = last_revnum
770                 raise e
771             (branch_path, min_revnum, max_revnum, scheme) = self.revmap.lookup_revid(revid)
772             assert isinstance(branch_path, str)
773
774         # Find the branch property between min_revnum and max_revnum that 
775         # added revid
776         for (bp, rev) in self.follow_branch(branch_path, max_revnum, 
777                                             get_scheme(scheme)):
778             try:
779                 (entry_revno, entry_revid) = parse_revid_property(
780                  self.branchprop_list.get_property_diff(bp, rev, 
781                      SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n"))
782             except errors.InvalidPropertyValue:
783                 # Don't warn about encountering an invalid property, 
784                 # that will already have happened earlier
785                 continue
786             if entry_revid == revid:
787                 self.revmap.insert_revid(revid, bp, rev, rev, scheme, 
788                                          entry_revno)
789                 return (bp, rev, get_scheme(scheme))
790
791         raise AssertionError("Revision id %s was added incorrectly" % revid)
792
793     def get_inventory_xml(self, revision_id):
794         """See Repository.get_inventory_xml()."""
795         return bzrlib.xml5.serializer_v5.write_inventory_to_string(
796             self.get_inventory(revision_id))
797
798     def get_inventory_sha1(self, revision_id):
799         """Get the sha1 for the XML representation of an inventory.
800
801         :param revision_id: Revision id of the inventory for which to return 
802          the SHA1.
803         :return: XML string
804         """
805
806         return osutils.sha_string(self.get_inventory_xml(revision_id))
807
808     def get_revision_xml(self, revision_id):
809         """Return the XML representation of a revision.
810
811         :param revision_id: Revision for which to return the XML.
812         :return: XML string
813         """
814         return bzrlib.xml5.serializer_v5.write_revision_to_string(
815             self.get_revision(revision_id))
816
817     def follow_history(self, revnum, scheme):
818         """Yield all the branches found between the start of history 
819         and a specified revision number.
820
821         :param revnum: Revision number up to which to search.
822         :return: iterator over branches in the range 0..revnum
823         """
824         assert scheme is not None
825
826         while revnum >= 0:
827             yielded_paths = []
828             paths = self._log.get_revision_paths(revnum)
829             for p in paths:
830                 try:
831                     bp = scheme.unprefix(p)[0]
832                     if not bp in yielded_paths:
833                         if not paths.has_key(bp) or paths[bp][0] != 'D':
834                             assert revnum > 0 or bp == ""
835                             yield (bp, revnum)
836                         yielded_paths.append(bp)
837                 except NotBranchError:
838                     pass
839             revnum -= 1
840
841     def follow_branch(self, branch_path, revnum, scheme):
842         """Follow the history of a branch. Will yield all the 
843         left-hand side ancestors of a specified revision.
844     
845         :param branch_path: Subversion path to search.
846         :param revnum: Revision number in Subversion to start.
847         :param scheme: Name of the branching scheme to use
848         :return: iterator over the ancestors
849         """
850         assert branch_path is not None
851         assert isinstance(branch_path, str)
852         assert isinstance(revnum, int) and revnum >= 0
853         assert scheme.is_branch(branch_path) or scheme.is_tag(branch_path)
854         branch_path = branch_path.strip("/")
855
856         while revnum >= 0:
857             assert revnum > 0 or branch_path == ""
858             paths = self._log.get_revision_paths(revnum)
859
860             yielded = False
861             # If something underneath branch_path changed, there is a 
862             # revision there, so yield it.
863             for p in paths:
864                 assert isinstance(p, str)
865                 if (p == branch_path or 
866                     p.startswith(branch_path+"/") or 
867                     branch_path == ""):
868                     yield (branch_path, revnum)
869                     yielded = True
870                     break
871             
872             # If there are no special cases, just go try the 
873             # next revnum in history
874             revnum -= 1
875
876             # Make sure we get the right location for next time, if 
877             # the branch itself was copied
878             if (paths.has_key(branch_path) and 
879                 paths[branch_path][0] in ('R', 'A')):
880                 if not yielded:
881                     yield (branch_path, revnum+1)
882                 if paths[branch_path][1] is None:
883                     return
884                 if not scheme.is_branch(paths[branch_path][1]) and \
885                    not scheme.is_tag(paths[branch_path][1]):
886                     # FIXME: if copyfrom_path is not a branch path, 
887                     # should simulate a reverse "split" of a branch
888                     # for now, just make it look like the branch ended here
889                     return
890                 revnum = paths[branch_path][2]
891                 branch_path = paths[branch_path][1].encode("utf-8")
892                 continue
893             
894             # Make sure we get the right location for the next time if 
895             # one of the parents changed
896
897             # Path names need to be sorted so the longer paths 
898             # override the shorter ones
899             for p in sorted(paths.keys(), reverse=True):
900                 if paths[p][0] == 'M':
901                     continue
902                 if branch_path.startswith(p+"/"):
903                     assert paths[p][0] in ('A', 'R'), "Parent wasn't added"
904                     assert paths[p][1] is not None, \
905                         "Empty parent added, but child wasn't added !?"
906
907                     revnum = paths[p][2]
908                     branch_path = paths[p][1].encode("utf-8") + branch_path[len(p):]
909                     break
910
911     def follow_branch_history(self, branch_path, revnum, scheme):
912         """Return all the changes that happened in a branch 
913         between branch_path and revnum. 
914
915         :return: iterator that returns tuples with branch path, 
916             changed paths and revision number.
917         """
918         assert branch_path is not None
919         assert scheme.is_branch(branch_path) or scheme.is_tag(branch_path)
920
921         for (bp, paths, revnum) in self._log.follow_path(branch_path, revnum):
922             assert revnum > 0 or bp == ""
923             assert scheme.is_branch(bp) or schee.is_tag(bp)
924             # Remove non-bp paths from paths
925             for p in paths.keys():
926                 if not p.startswith(bp+"/") and bp != p and bp != "":
927                     del paths[p]
928
929             if paths == {}:
930                 continue
931
932             if (paths.has_key(bp) and paths[bp][1] is not None and 
933                 not scheme.is_branch(paths[bp][1]) and
934                 not scheme.is_tag(paths[bp][1])):
935                 # FIXME: if copyfrom_path is not a branch path, 
936                 # should simulate a reverse "split" of a branch
937                 # for now, just make it look like the branch ended here
938                 for c in self._log.find_children(paths[bp][1], paths[bp][2]):
939                     path = c.replace(paths[bp][1], bp+"/", 1).replace("//", "/")
940                     paths[path] = ('A', None, -1)
941                 paths[bp] = ('A', None, -1)
942
943                 yield (bp, paths, revnum)
944                 return
945                      
946             yield (bp, paths, revnum)
947
948     def has_signature_for_revision_id(self, revision_id):
949         """Check whether a signature exists for a particular revision id.
950
951         :param revision_id: Revision id for which the signatures should be looked up.
952         :return: False, as no signatures are stored for revisions in Subversion 
953             at the moment.
954         """
955         # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE 
956         return False # SVN doesn't store GPG signatures. Perhaps 
957                      # store in SVN revision property?
958
959
960     def get_signature_text(self, revision_id):
961         """Return the signature text for a particular revision.
962
963         :param revision_id: Id of the revision for which to return the 
964                             signature.
965         :raises NoSuchRevision: Always
966         """
967         # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE 
968         # SVN doesn't store GPG signatures
969         raise NoSuchRevision(self, revision_id)
970
971     def _full_revision_graph(self, scheme, _latest_revnum=None):
972         if _latest_revnum is None:
973             _latest_revnum = self.transport.get_latest_revnum()
974         graph = {}
975         for (branch, revnum) in self.follow_history(_latest_revnum, 
976                                                     scheme):
977             mutter('%r, %r' % (branch, revnum))
978             revid = self.generate_revision_id(revnum, branch, str(scheme))
979             graph[revid] = self.revision_parents(revid)
980         return graph
981
982     def get_revision_graph(self, revision_id=None):
983         """See Repository.get_revision_graph()."""
984         if revision_id == NULL_REVISION:
985             return {}
986
987         if revision_id is None:
988             return self._full_revision_graph(self.get_scheme())
989
990         (path, revnum, scheme) = self.lookup_revision_id(revision_id)
991
992         _previous = revision_id
993         self._ancestry = {}
994         
995         if revnum > 0:
996             for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
997                 revid = self.generate_revision_id(rev, branch, str(scheme))
998                 self._ancestry[_previous] = [revid]
999                 _previous = revid
1000
1001         self._ancestry[_previous] = []
1002
1003         return self._ancestry
1004
1005     def find_branches(self, scheme, revnum=None):
1006         """Find all branches that were changed in the specified revision number.
1007
1008         :param revnum: Revision to search for branches.
1009         :return: iterator that returns tuples with (path, revision number, still exists). The revision number is the revision in which the branch last existed.
1010         """
1011         assert scheme is not None
1012         if revnum is None:
1013             revnum = self.transport.get_latest_revnum()
1014
1015         created_branches = {}
1016
1017         ret = []
1018
1019         pb = ui.ui_factory.nested_progress_bar()
1020         try:
1021             for i in range(revnum+1):
1022                 pb.update("finding branches", i, revnum+1)
1023                 paths = self._log.get_revision_paths(i)
1024                 for p in sorted(paths.keys()):
1025                     if scheme.is_branch(p) or scheme.is_tag(p):
1026                         if paths[p][0] in ('R', 'D'):
1027                             del created_branches[p]
1028                             j = self._log.find_latest_change(p, i-1, 
1029                                 include_parents=True, include_children=True)
1030                             ret.append((p, j, False))
1031
1032                         if paths[p][0] in ('A', 'R'): 
1033                             created_branches[p] = i
1034                     elif scheme.is_branch_parent(p) or \
1035                             scheme.is_tag_parent(p):
1036                         if paths[p][0] in ('R', 'D'):
1037                             k = created_branches.keys()
1038                             for c in k:
1039                                 if c.startswith(p+"/"):
1040                                     del created_branches[c] 
1041                                     j = self._log.find_latest_change(c, i-1, 
1042                                             include_parents=True, 
1043                                             include_children=True)
1044                                     ret.append((c, j, False))
1045                         if paths[p][0] in ('A', 'R'):
1046                             parents = [p]
1047                             while parents:
1048                                 p = parents.pop()
1049                                 for c in self.transport.get_dir(p, i)[0].keys():
1050                                     n = p+"/"+c
1051                                     if scheme.is_branch(n) or scheme.is_tag(n):
1052                                         created_branches[n] = i
1053                                     elif (scheme.is_branch_parent(n) or 
1054                                           scheme.is_tag_parent(n)):
1055                                         parents.append(n)
1056         finally:
1057             pb.finished()
1058
1059         for p in created_branches:
1060             j = self._log.find_latest_change(p, revnum, 
1061                                              include_parents=True,
1062                                              include_children=True)
1063             if j is None:
1064                 j = created_branches[p]
1065             ret.append((p, j, True))
1066
1067         return ret
1068
1069     def is_shared(self):
1070         """Return True if this repository is flagged as a shared repository."""
1071         return True
1072
1073     def get_physical_lock_status(self):
1074         return False
1075
1076     def get_commit_builder(self, branch, parents, config, timestamp=None, 
1077                            timezone=None, committer=None, revprops=None, 
1078                            revision_id=None):
1079         from commit import SvnCommitBuilder
1080         return SvnCommitBuilder(self, branch, parents, config, timestamp, 
1081                 timezone, committer, revprops, revision_id)
1082
1083
1084