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