1 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
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.
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.
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."""
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
31 from svn.core import SubversionException, Pool
36 from branchprops import BranchPropertyList
37 from cache import create_cache_dir, sqlite3
39 from config import SvnRepositoryConfig
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
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'
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'
68 # The following two functions don't use day names (which can vary by
69 # locale) unlike the alternatives in bzrlib.timestamp
71 def format_highres_date(t, offset=0):
72 """Format a date, such that it includes higher precision in the
75 :param t: The local time in fractional seconds since the epoch
77 :param offset: The timezone offset in integer seconds
80 assert isinstance(t, float)
82 # This has to be formatted for "original" date, so that the
83 # revision XML entry will be reproduced faithfully.
86 tt = time.gmtime(t + offset)
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))
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.
100 :param date: A date formated by format_highres_date
103 # skip day if applicable
104 if not date[0].isdigit():
105 space_loc = date.find(' ')
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
112 dot_loc = date.find('.')
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)
122 hours = int(offset / 100)
123 minutes = (offset % 100)
124 seconds_offset = (hours * 3600) + (minutes * 60)
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)
134 def parse_merge_property(line):
135 """Parse a bzr:merge property value.
137 :param line: Line to parse
138 :return: List of revisions merged
141 mutter('invalid revision id %r in merged property, skipping' % line)
144 return filter(lambda x: x != "", line.split("\t"))
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)
152 assert not '\n' in line
154 (revno, revid) = line.split(' ', 1)
156 raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_ID,
159 raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_ID,
161 return (int(revno), revid)
164 def parse_revision_metadata(text, rev):
165 """Parse a revision info text (as set in bzr:revision-info).
167 :param text: text to parse
168 :param rev: Revision object to apply read parameters to
170 in_properties = False
171 for l in text.splitlines():
173 key, value = l.split(": ", 2)
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":
183 elif key[0] == "\t" and in_properties:
184 rev.properties[str(key[1:])] = str(value)
186 raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_INFO,
187 "Invalid key %r" % key)
190 def generate_revision_metadata(timestamp, timezone, committer, revprops):
191 """Generate revision metadata text for the specified revision
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.
200 assert timestamp is None or isinstance(timestamp, float)
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)
213 def parse_svk_feature(feature):
214 """Parse a svk feature identifier.
216 :param feature: The feature identifier as string.
217 :return: tuple with uuid, branch path and revnum
220 (uuid, branch, revnum) = feature.split(":", 3)
222 raise errors.InvalidPropertyValue(SVN_PROP_SVK_MERGE,
224 return (uuid, branch.strip("/"), int(revnum))
227 def revision_id_to_svk_feature(revid):
228 """Create a SVK feature identifier from a revision id.
230 :param revid: Revision id to convert.
231 :return: Matching SVK feature identifier.
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)
239 class SvnRepositoryFormat(RepositoryFormat):
240 """Repository format for Subversion repositories (accessed using svn_ra).
242 rich_root_data = True
244 def __get_matchingbzrdir(self):
245 from format import SvnFormat
248 _matchingbzrdir = property(__get_matchingbzrdir)
251 super(SvnRepositoryFormat, self).__init__()
253 def get_format_description(self):
254 return "Subversion Repository"
256 def initialize(self, url, shared=False, _internal=False):
257 """Svn repositories cannot be created (yet)."""
258 raise UninitializableFormat(self)
260 def check_conversion_target(self, target_repo_format):
261 return target_repo_format.rich_root_data
265 class SvnRepository(Repository):
267 Provides a simplified interface to a Subversion repository
268 by using the RA (remote access) API from subversion
270 def __init__(self, bzrdir, transport, branch_path=None):
271 from fileids import SimpleFileIdMap
272 _revision_store = None
274 assert isinstance(transport, Transport)
276 control_files = LockableFiles(transport, '', TransportLock)
277 Repository.__init__(self, SvnRepositoryFormat(), bzrdir,
278 control_files, None, None, None)
280 self.transport = transport
281 self.uuid = transport.get_uuid()
282 assert self.uuid is not None
283 self.base = transport.base
284 assert self.base is not None
285 self._serializer = None
288 self.config = SvnRepositoryConfig(self.uuid)
289 self.config.add_location(self.base)
290 self._revids_seen = {}
291 cache_dir = self.create_cache_dir()
292 cachedir_transport = get_transport(cache_dir)
293 cache_file = os.path.join(cache_dir, 'cache-v%d' % MAPPING_VERSION)
294 if not cachedbs.has_key(cache_file):
295 cachedbs[cache_file] = sqlite3.connect(cache_file)
296 self.cachedb = cachedbs[cache_file]
298 self._log = logwalker.LogWalker(transport=transport,
299 cache_db=self.cachedb)
301 self.branchprop_list = BranchPropertyList(self._log, self.cachedb)
302 self.fileid_map = SimpleFileIdMap(self, cachedir_transport)
303 self.revmap = RevidMap(self.cachedb)
305 self._hinted_branch_path = branch_path
307 def lhs_missing_revisions(self, revhistory, stop_revision):
309 slice = revhistory[:revhistory.index(stop_revision)+1]
310 for revid in reversed(slice):
311 if self.has_revision(revid):
314 missing.append(revid)
315 raise UnrelatedBranches()
317 def get_transaction(self):
318 raise NotImplementedError(self.get_transaction)
320 def get_scheme(self):
321 """Determine the branching scheme to use for this repository.
323 :return: Branching scheme.
325 if self._scheme is not None:
328 scheme = self.config.get_branching_scheme()
329 if scheme is not None:
330 self._scheme = scheme
333 last_revnum = self.transport.get_latest_revnum()
334 scheme = self._get_property_scheme(last_revnum)
335 if scheme is not None:
336 self.set_branching_scheme(scheme)
339 self.set_branching_scheme(
340 self._guess_scheme(last_revnum, self._hinted_branch_path),
341 store=(last_revnum > 20))
345 def _get_property_scheme(self, revnum=None):
347 revnum = self.transport.get_latest_revnum()
348 text = self.branchprop_list.get_property("",
349 revnum, SVN_PROP_BZR_BRANCHING_SCHEME, None)
352 return ListBranchingScheme(parse_list_scheme_text(text))
354 def set_property_scheme(self, scheme):
355 def done(revision, date, author):
357 editor = self.transport.get_commit_editor(
358 "Updating branching scheme for Bazaar.",
360 root = editor.open_root(-1)
361 editor.change_dir_prop(root, SVN_PROP_BZR_BRANCHING_SCHEME,
362 "".join(map(lambda x: x+"\n", scheme.branch_list)).encode("utf-8"))
363 editor.close_directory(root)
366 def _guess_scheme(self, last_revnum, branch_path=None):
367 scheme = guess_scheme_from_history(
368 self._log.follow_path("", last_revnum), last_revnum,
370 mutter("Guessed branching scheme: %r" % scheme)
373 def set_branching_scheme(self, scheme, store=True):
374 self._scheme = scheme
376 self.config.set_branching_scheme(str(scheme))
378 def _warn_if_deprecated(self):
379 # This class isn't deprecated
383 return '%s(%r)' % (self.__class__.__name__,
386 def create_cache_dir(self):
387 cache_dir = create_cache_dir()
388 dir = os.path.join(cache_dir, self.uuid)
389 if not os.path.exists(dir):
393 def _check(self, revision_ids):
394 return BranchCheckResult(self)
396 def get_inventory(self, revision_id):
397 assert revision_id != None
398 return self.revision_tree(revision_id).inventory
400 def get_fileid_map(self, revnum, path, scheme):
401 return self.fileid_map.get_map(self.uuid, revnum, path,
402 self.revision_fileid_renames, scheme)
404 def transform_fileid_map(self, uuid, revnum, branch, changes, renames,
406 return self.fileid_map.apply_changes(uuid, revnum, branch, changes,
409 def all_revision_ids(self, scheme=None):
411 scheme = self.get_scheme()
412 for (bp, rev) in self.follow_history(
413 self.transport.get_latest_revnum(), scheme):
414 yield self.generate_revision_id(rev, bp, str(scheme))
416 def get_inventory_weave(self):
417 """See Repository.get_inventory_weave()."""
418 raise NotImplementedError(self.get_inventory_weave)
420 def set_make_working_trees(self, new_value):
421 """See Repository.set_make_working_trees()."""
422 pass # FIXME: ignored, nowhere to store it...
424 def make_working_trees(self):
425 """See Repository.make_working_trees().
427 Always returns False, as working trees are never created inside
428 Subversion repositories.
432 def get_ancestry(self, revision_id, topo_sorted=True):
433 """See Repository.get_ancestry().
435 Note: only the first bit is topologically ordered!
437 if revision_id is None:
440 (path, revnum, scheme) = self.lookup_revision_id(revision_id)
442 ancestry = [revision_id]
444 for l in self.branchprop_list.get_property(path, revnum,
445 SVN_PROP_BZR_ANCESTRY+str(scheme), "").splitlines():
446 ancestry.extend(l.split("\n"))
449 for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
451 self.generate_revision_id(rev, branch, str(scheme)))
453 ancestry.append(None)
457 def has_revision(self, revision_id):
458 """See Repository.has_revision()."""
459 if revision_id is None:
463 (path, revnum, _) = self.lookup_revision_id(revision_id)
464 except NoSuchRevision:
468 return (svn.core.svn_node_dir == self.transport.check_path(path, revnum))
469 except SubversionException, (_, num):
470 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
475 def revision_trees(self, revids):
476 """See Repository.revision_trees()."""
478 yield self.revision_tree(revid)
480 def revision_tree(self, revision_id):
481 """See Repository.revision_tree()."""
482 if revision_id is None:
483 revision_id = NULL_REVISION
485 if revision_id == NULL_REVISION:
486 inventory = Inventory(root_id=None)
487 inventory.revision_id = revision_id
488 return RevisionTree(self, inventory, revision_id)
490 return SvnRevisionTree(self, revision_id)
492 def revision_fileid_renames(self, revid):
493 """Check which files were renamed in a particular revision.
495 :param revid: Id of revision to look up.
496 :return: dictionary with paths as keys, file ids as values
498 (path, revnum, _) = self.lookup_revision_id(revid)
499 # Only consider bzr:file-ids if this is a bzr revision
500 if not self.branchprop_list.touches_property(path, revnum,
501 SVN_PROP_BZR_REVISION_INFO):
503 fileids = self.branchprop_list.get_property(path, revnum,
504 SVN_PROP_BZR_FILEIDS)
508 for line in fileids.splitlines():
509 (path, key) = line.split("\t", 2)
510 ret[urllib.unquote(path)] = osutils.safe_file_id(key)
513 def _mainline_revision_parent(self, path, revnum, scheme):
514 """Find the mainline parent of the specified revision.
516 :param path: Path of the revision in Subversion
517 :param revnum: Subversion revision number
518 :param scheme: Name of branching scheme to use
519 :return: Revision id of the left-hand-side parent or None if
520 this is the first revision
522 assert isinstance(path, basestring)
523 assert isinstance(revnum, int)
525 if not scheme.is_branch(path) and \
526 not scheme.is_tag(path):
527 raise NoSuchRevision(self,
528 self.generate_revision_id(revnum, path, str(scheme)))
530 it = self.follow_branch(path, revnum, scheme)
531 # the first tuple returned should match the one specified.
532 # if it's not, then the branch, revnum didn't change in the specified
533 # revision and so it is invalid
534 if (path, revnum) != it.next():
535 raise NoSuchRevision(self,
536 self.generate_revision_id(revnum, path, str(scheme)))
538 (branch, rev) = it.next()
539 return self.generate_revision_id(rev, branch, str(scheme))
540 except StopIteration:
541 # The specified revision was the first one in the branch
544 def _bzr_merged_revisions(self, branch, revnum, scheme):
545 """Find out what revisions were merged by Bazaar in a revision.
547 :param branch: Subversion branch path.
548 :param revnum: Subversion revision number.
549 :param scheme: Branching scheme.
551 change = self.branchprop_list.get_property_diff(branch, revnum,
552 SVN_PROP_BZR_ANCESTRY+str(scheme)).splitlines()
556 assert len(change) == 1
558 return parse_merge_property(change[0])
560 def _svk_feature_to_revision_id(self, scheme, feature):
561 """Convert a SVK feature to a revision id for this repository.
563 :param scheme: Branching scheme.
564 :param feature: SVK feature.
565 :return: revision id.
568 (uuid, bp, revnum) = parse_svk_feature(feature)
569 except errors.InvalidPropertyValue:
571 if uuid != self.uuid:
573 if not scheme.is_branch(bp) and not scheme.is_tag(bp):
575 return self.generate_revision_id(revnum, bp, str(scheme))
577 def _svk_merged_revisions(self, branch, revnum, scheme):
578 """Find out what SVK features were merged in a revision.
580 :param branch: Subversion branch path.
581 :param revnum: Subversion revision number.
582 :param scheme: Branching scheme.
584 current = set(self.branchprop_list.get_property(branch, revnum, SVN_PROP_SVK_MERGE, "").splitlines())
585 (prev_path, prev_revnum) = self._log.get_previous(branch, revnum)
586 if prev_path is None and prev_revnum == -1:
589 previous = set(self.branchprop_list.get_property(prev_path.encode("utf-8"),
590 prev_revnum, SVN_PROP_SVK_MERGE, "").splitlines())
591 for feature in current.difference(previous):
592 revid = self._svk_feature_to_revision_id(scheme, feature)
593 if revid is not None:
596 def revision_parents(self, revision_id, bzr_merges=None, svk_merges=None):
597 """See Repository.revision_parents()."""
599 (branch, revnum, scheme) = self.lookup_revision_id(revision_id)
600 mainline_parent = self._mainline_revision_parent(branch, revnum, scheme)
601 if mainline_parent is not None:
602 parent_ids.append(mainline_parent)
604 # if the branch didn't change, bzr:merge or svk:merge can't have changed
605 if not self._log.touches_path(branch, revnum):
608 if bzr_merges is None:
609 bzr_merges = self._bzr_merged_revisions(branch, revnum, scheme)
610 if svk_merges is None:
611 svk_merges = self._svk_merged_revisions(branch, revnum, scheme)
613 parent_ids.extend(bzr_merges)
616 # Commit was doing using svk apparently
617 parent_ids.extend(svk_merges)
621 def get_revision(self, revision_id):
622 """See Repository.get_revision."""
623 if not revision_id or not isinstance(revision_id, basestring):
624 raise InvalidRevisionId(revision_id=revision_id, branch=self)
626 (path, revnum, _) = self.lookup_revision_id(revision_id)
628 parent_ids = self.revision_parents(revision_id)
630 # Commit SVN revision properties to a Revision object
631 rev = Revision(revision_id=revision_id, parent_ids=parent_ids)
633 (rev.committer, rev.message, date) = self._log.get_revision_info(revnum)
634 if rev.committer is None:
638 rev.timestamp = 1.0 * svn.core.secs_from_timestr(date, None)
640 rev.timestamp = 0.0 # FIXME: Obtain repository creation time
643 parse_revision_metadata(
644 self.branchprop_list.get_property(path, revnum,
645 SVN_PROP_BZR_REVISION_INFO, ""), rev)
647 rev.inventory_sha1 = property(
648 lambda: self.get_inventory_sha1(revision_id))
652 def get_revisions(self, revision_ids):
653 """See Repository.get_revisions()."""
654 # TODO: More efficient implementation?
655 return map(self.get_revision, revision_ids)
657 def add_revision(self, rev_id, rev, inv=None, config=None):
658 raise NotImplementedError(self.add_revision)
660 def generate_revision_id(self, revnum, path, scheme):
661 """Generate an unambiguous revision id.
663 :param revnum: Subversion revision number.
664 :param path: Branch path.
665 :param scheme: Branching scheme name
667 :return: New revision id.
669 assert isinstance(path, str)
670 assert isinstance(revnum, int)
672 # Look in the cache to see if it already has a revision id
673 revid = self.revmap.lookup_branch_revnum(revnum, path, scheme)
674 if revid is not None:
677 # Lookup the revision from the bzr:revision-id-vX property
678 line = self.branchprop_list.get_property_diff(path, revnum,
679 SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n")
682 revid = generate_svn_revision_id(self.uuid, revnum, path,
686 (bzr_revno, revid) = parse_revid_property(line)
687 self.revmap.insert_revid(revid, path, revnum, revnum,
689 except errors.InvalidPropertyValue, e:
691 revid = generate_svn_revision_id(self.uuid, revnum, path,
693 self.revmap.insert_revid(revid, path, revnum, revnum,
698 def lookup_revision_id(self, revid, scheme=None):
699 """Parse an existing Subversion-based revision id.
701 :param revid: The revision id.
702 :param scheme: Optional branching scheme to use when searching for
704 :raises: NoSuchRevision
705 :return: Tuple with branch path, revision number and scheme.
707 def get_scheme(name):
708 assert isinstance(name, basestring)
709 return BranchingScheme.find_scheme(name)
713 (uuid, branch_path, revnum, schemen) = parse_svn_revision_id(revid)
714 assert isinstance(branch_path, str)
715 if uuid == self.uuid:
716 return (branch_path, revnum, get_scheme(schemen))
717 # If the UUID doesn't match, this may still be a valid revision
718 # id; a revision from another SVN repository may be pushed into
720 except InvalidRevisionId:
723 # Check the record out of the revmap, if it exists
725 (branch_path, min_revnum, max_revnum, \
726 scheme) = self.revmap.lookup_revid(revid)
727 assert isinstance(branch_path, str)
728 # Entry already complete?
729 if min_revnum == max_revnum:
730 return (branch_path, min_revnum, get_scheme(scheme))
731 except NoSuchRevision, e:
732 # If there is no entry in the map, walk over all branches:
734 scheme = self.get_scheme()
735 last_revnum = self.transport.get_latest_revnum()
736 if (self._revids_seen.has_key(str(scheme)) and
737 last_revnum <= self._revids_seen[str(scheme)]):
738 # All revision ids in this repository for the current
739 # scheme have already been discovered. No need to
743 for (branch, revno, _) in self.find_branches(scheme, last_revnum):
744 # Look at their bzr:revision-id-vX
746 for line in self.branchprop_list.get_property(branch, revno,
747 SVN_PROP_BZR_REVISION_ID+str(scheme), "").splitlines():
749 revids.append(parse_revid_property(line))
750 except errors.InvalidPropertyValue, ie:
753 # If there are any new entries that are not yet in the cache,
755 for (entry_revno, entry_revid) in revids:
756 if entry_revid == revid:
758 self.revmap.insert_revid(entry_revid, branch, 0, revno,
759 str(scheme), entry_revno)
765 # We've added all the revision ids for this scheme in the repository,
766 # so no need to check again unless new revisions got added
767 self._revids_seen[str(scheme)] = last_revnum
769 (branch_path, min_revnum, max_revnum, scheme) = self.revmap.lookup_revid(revid)
770 assert isinstance(branch_path, str)
772 # Find the branch property between min_revnum and max_revnum that
774 for (bp, rev) in self.follow_branch(branch_path, max_revnum,
777 (entry_revno, entry_revid) = parse_revid_property(
778 self.branchprop_list.get_property_diff(bp, rev,
779 SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n"))
780 except errors.InvalidPropertyValue:
781 # Don't warn about encountering an invalid property,
782 # that will already have happened earlier
784 if entry_revid == revid:
785 self.revmap.insert_revid(revid, bp, rev, rev, scheme,
787 return (bp, rev, get_scheme(scheme))
789 raise AssertionError("Revision id %s was added incorrectly" % revid)
791 def get_inventory_xml(self, revision_id):
792 """See Repository.get_inventory_xml()."""
793 return bzrlib.xml5.serializer_v5.write_inventory_to_string(
794 self.get_inventory(revision_id))
796 def get_inventory_sha1(self, revision_id):
797 """Get the sha1 for the XML representation of an inventory.
799 :param revision_id: Revision id of the inventory for which to return
804 return osutils.sha_string(self.get_inventory_xml(revision_id))
806 def get_revision_xml(self, revision_id):
807 """Return the XML representation of a revision.
809 :param revision_id: Revision for which to return the XML.
812 return bzrlib.xml5.serializer_v5.write_revision_to_string(
813 self.get_revision(revision_id))
815 def follow_history(self, revnum, scheme):
816 """Yield all the branches found between the start of history
817 and a specified revision number.
819 :param revnum: Revision number up to which to search.
820 :return: iterator over branches in the range 0..revnum
822 assert scheme is not None
826 paths = self._log.get_revision_paths(revnum)
829 bp = scheme.unprefix(p)[0]
830 if not bp in yielded_paths:
831 if not paths.has_key(bp) or paths[bp][0] != 'D':
832 assert revnum > 0 or bp == ""
834 yielded_paths.append(bp)
835 except NotBranchError:
839 def follow_branch(self, branch_path, revnum, scheme):
840 """Follow the history of a branch. Will yield all the
841 left-hand side ancestors of a specified revision.
843 :param branch_path: Subversion path to search.
844 :param revnum: Revision number in Subversion to start.
845 :param scheme: Name of the branching scheme to use
846 :return: iterator over the ancestors
848 assert branch_path is not None
849 assert isinstance(branch_path, str)
850 assert isinstance(revnum, int) and revnum >= 0
851 assert scheme.is_branch(branch_path) or scheme.is_tag(branch_path)
852 branch_path = branch_path.strip("/")
855 assert revnum > 0 or branch_path == ""
856 paths = self._log.get_revision_paths(revnum)
859 # If something underneath branch_path changed, there is a
860 # revision there, so yield it.
862 assert isinstance(p, str)
863 if (p == branch_path or
864 p.startswith(branch_path+"/") or
866 yield (branch_path, revnum)
870 # If there are no special cases, just go try the
871 # next revnum in history
874 # Make sure we get the right location for next time, if
875 # the branch itself was copied
876 if (paths.has_key(branch_path) and
877 paths[branch_path][0] in ('R', 'A')):
879 yield (branch_path, revnum+1)
880 if paths[branch_path][1] is None:
882 if not scheme.is_branch(paths[branch_path][1]) and \
883 not scheme.is_tag(paths[branch_path][1]):
884 # FIXME: if copyfrom_path is not a branch path,
885 # should simulate a reverse "split" of a branch
886 # for now, just make it look like the branch ended here
888 revnum = paths[branch_path][2]
889 branch_path = paths[branch_path][1].encode("utf-8")
892 # Make sure we get the right location for the next time if
893 # one of the parents changed
895 # Path names need to be sorted so the longer paths
896 # override the shorter ones
897 for p in sorted(paths.keys(), reverse=True):
898 if paths[p][0] == 'M':
900 if branch_path.startswith(p+"/"):
901 assert paths[p][0] in ('A', 'R'), "Parent wasn't added"
902 assert paths[p][1] is not None, \
903 "Empty parent added, but child wasn't added !?"
906 branch_path = paths[p][1].encode("utf-8") + branch_path[len(p):]
909 def follow_branch_history(self, branch_path, revnum, scheme):
910 """Return all the changes that happened in a branch
911 between branch_path and revnum.
913 :return: iterator that returns tuples with branch path,
914 changed paths and revision number.
916 assert branch_path is not None
917 assert scheme.is_branch(branch_path) or scheme.is_tag(branch_path)
919 for (bp, paths, revnum) in self._log.follow_path(branch_path, revnum):
920 assert revnum > 0 or bp == ""
921 assert scheme.is_branch(bp) or schee.is_tag(bp)
922 # Remove non-bp paths from paths
923 for p in paths.keys():
924 if not p.startswith(bp+"/") and bp != p and bp != "":
930 if (paths.has_key(bp) and paths[bp][1] is not None and
931 not scheme.is_branch(paths[bp][1]) and
932 not scheme.is_tag(paths[bp][1])):
933 # FIXME: if copyfrom_path is not a branch path,
934 # should simulate a reverse "split" of a branch
935 # for now, just make it look like the branch ended here
936 for c in self._log.find_children(paths[bp][1], paths[bp][2]):
937 path = c.replace(paths[bp][1], bp+"/", 1).replace("//", "/")
938 paths[path] = ('A', None, -1)
939 paths[bp] = ('A', None, -1)
941 yield (bp, paths, revnum)
944 yield (bp, paths, revnum)
946 def has_signature_for_revision_id(self, revision_id):
947 """Check whether a signature exists for a particular revision id.
949 :param revision_id: Revision id for which the signatures should be looked up.
950 :return: False, as no signatures are stored for revisions in Subversion
953 # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE
954 return False # SVN doesn't store GPG signatures. Perhaps
955 # store in SVN revision property?
958 def get_signature_text(self, revision_id):
959 """Return the signature text for a particular revision.
961 :param revision_id: Id of the revision for which to return the
963 :raises NoSuchRevision: Always
965 # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE
966 # SVN doesn't store GPG signatures
967 raise NoSuchRevision(self, revision_id)
969 def _full_revision_graph(self, scheme, _latest_revnum=None):
970 if _latest_revnum is None:
971 _latest_revnum = self.transport.get_latest_revnum()
973 for (branch, revnum) in self.follow_history(_latest_revnum,
975 mutter('%r, %r' % (branch, revnum))
976 revid = self.generate_revision_id(revnum, branch, str(scheme))
977 graph[revid] = self.revision_parents(revid)
980 def get_revision_graph(self, revision_id=None):
981 """See Repository.get_revision_graph()."""
982 if revision_id == NULL_REVISION:
985 if revision_id is None:
986 return self._full_revision_graph(self.get_scheme())
988 (path, revnum, scheme) = self.lookup_revision_id(revision_id)
990 _previous = revision_id
994 for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
995 revid = self.generate_revision_id(rev, branch, str(scheme))
996 self._ancestry[_previous] = [revid]
999 self._ancestry[_previous] = []
1001 return self._ancestry
1003 def find_branches(self, scheme, revnum=None):
1004 """Find all branches that were changed in the specified revision number.
1006 :param revnum: Revision to search for branches.
1007 :return: iterator that returns tuples with (path, revision number, still exists). The revision number is the revision in which the branch last existed.
1009 assert scheme is not None
1011 revnum = self.transport.get_latest_revnum()
1013 created_branches = {}
1017 pb = ui.ui_factory.nested_progress_bar()
1019 for i in range(revnum+1):
1020 pb.update("finding branches", i, revnum+1)
1021 paths = self._log.get_revision_paths(i)
1022 for p in sorted(paths.keys()):
1023 if scheme.is_branch(p) or scheme.is_tag(p):
1024 if paths[p][0] in ('R', 'D'):
1025 del created_branches[p]
1026 j = self._log.find_latest_change(p, i-1,
1028 ret.append((p, j, False))
1030 if paths[p][0] in ('A', 'R'):
1031 created_branches[p] = i
1032 elif scheme.is_branch_parent(p) or \
1033 scheme.is_tag_parent(p):
1034 if paths[p][0] in ('R', 'D'):
1035 k = created_branches.keys()
1037 if c.startswith(p+"/"):
1038 del created_branches[c]
1039 j = self._log.find_latest_change(c, i-1,
1041 ret.append((c, j, False))
1042 if paths[p][0] in ('A', 'R'):
1046 for c in self.transport.get_dir(p, i)[0].keys():
1048 if scheme.is_branch(n) or scheme.is_tag(n):
1049 created_branches[n] = i
1050 elif (scheme.is_branch_parent(n) or
1051 scheme.is_tag_parent(n)):
1056 for p in created_branches:
1057 j = self._log.find_latest_change(p, revnum, recurse=True)
1059 j = created_branches[p]
1060 ret.append((p, j, True))
1064 def is_shared(self):
1065 """Return True if this repository is flagged as a shared repository."""
1068 def get_physical_lock_status(self):
1071 def get_commit_builder(self, branch, parents, config, timestamp=None,
1072 timezone=None, committer=None, revprops=None,
1074 from commit import SvnCommitBuilder
1075 return SvnCommitBuilder(self, branch, parents, config, timestamp,
1076 timezone, committer, revprops, revision_id)