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 remote import SvnRemoteFormat
246 return SvnRemoteFormat()
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 bzrlib.plugins.svn import lazy_register_optimizers
272 lazy_register_optimizers()
273 from fileids import SimpleFileIdMap
274 _revision_store = None
276 assert isinstance(transport, Transport)
278 control_files = LockableFiles(transport, '', TransportLock)
279 Repository.__init__(self, SvnRepositoryFormat(), bzrdir,
280 control_files, None, None, None)
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
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]
300 self._log = logwalker.LogWalker(transport=transport,
301 cache_db=self.cachedb)
303 self.branchprop_list = BranchPropertyList(self._log, self.cachedb)
304 self.fileid_map = SimpleFileIdMap(self, cachedir_transport)
305 self.revmap = RevidMap(self.cachedb)
307 self._hinted_branch_path = branch_path
309 def lhs_missing_revisions(self, revhistory, stop_revision):
311 slice = revhistory[:revhistory.index(stop_revision)+1]
312 for revid in reversed(slice):
313 if self.has_revision(revid):
316 missing.append(revid)
317 raise UnrelatedBranches()
319 def get_transaction(self):
320 raise NotImplementedError(self.get_transaction)
322 def get_scheme(self):
323 """Determine the branching scheme to use for this repository.
325 :return: Branching scheme.
327 if self._scheme is not None:
330 scheme = self.config.get_branching_scheme()
331 if scheme is not None:
332 self._scheme = scheme
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)
341 self.set_branching_scheme(
342 self._guess_scheme(last_revnum, self._hinted_branch_path),
343 store=(last_revnum > 20))
347 def _get_property_scheme(self, revnum=None):
349 revnum = self.transport.get_latest_revnum()
350 text = self.branchprop_list.get_property("",
351 revnum, SVN_PROP_BZR_BRANCHING_SCHEME, None)
354 return ListBranchingScheme(parse_list_scheme_text(text))
356 def set_property_scheme(self, scheme):
357 def done(revision, date, author):
359 editor = self.transport.get_commit_editor(
360 {svn.core.SVN_PROP_REVISION_LOG: "Updating branching scheme for Bazaar."},
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)
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,
372 mutter("Guessed branching scheme: %r" % scheme)
375 def set_branching_scheme(self, scheme, store=True):
376 self._scheme = scheme
378 self.config.set_branching_scheme(str(scheme))
380 def _warn_if_deprecated(self):
381 # This class isn't deprecated
385 return '%s(%r)' % (self.__class__.__name__,
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):
395 def _check(self, revision_ids):
396 return BranchCheckResult(self)
398 def get_inventory(self, revision_id):
399 assert revision_id != None
400 return self.revision_tree(revision_id).inventory
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)
406 def transform_fileid_map(self, uuid, revnum, branch, changes, renames,
408 return self.fileid_map.apply_changes(uuid, revnum, branch, changes,
411 def all_revision_ids(self, scheme=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))
418 def get_inventory_weave(self):
419 """See Repository.get_inventory_weave()."""
420 raise NotImplementedError(self.get_inventory_weave)
422 def set_make_working_trees(self, new_value):
423 """See Repository.set_make_working_trees()."""
424 pass # FIXME: ignored, nowhere to store it...
426 def make_working_trees(self):
427 """See Repository.make_working_trees().
429 Always returns False, as working trees are never created inside
430 Subversion repositories.
434 def get_ancestry(self, revision_id, topo_sorted=True):
435 """See Repository.get_ancestry().
437 Note: only the first bit is topologically ordered!
439 if revision_id is None:
442 (path, revnum, scheme) = self.lookup_revision_id(revision_id)
444 ancestry = [revision_id]
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"))
451 for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
453 self.generate_revision_id(rev, branch, str(scheme)))
455 ancestry.append(None)
459 def has_revision(self, revision_id):
460 """See Repository.has_revision()."""
461 if revision_id is None:
465 (path, revnum, _) = self.lookup_revision_id(revision_id)
466 except NoSuchRevision:
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:
477 def revision_trees(self, revids):
478 """See Repository.revision_trees()."""
480 yield self.revision_tree(revid)
482 def revision_tree(self, revision_id):
483 """See Repository.revision_tree()."""
484 if revision_id is None:
485 revision_id = NULL_REVISION
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)
492 return SvnRevisionTree(self, revision_id)
494 def revision_fileid_renames(self, revid):
495 """Check which files were renamed in a particular revision.
497 :param revid: Id of revision to look up.
498 :return: dictionary with paths as keys, file ids as values
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):
505 fileids = self.branchprop_list.get_property(path, revnum,
506 SVN_PROP_BZR_FILEIDS)
510 for line in fileids.splitlines():
511 (path, key) = line.split("\t", 2)
512 ret[urllib.unquote(path)] = osutils.safe_file_id(key)
515 def _mainline_revision_parent(self, path, revnum, scheme):
516 """Find the mainline parent of the specified revision.
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
524 assert isinstance(path, basestring)
525 assert isinstance(revnum, int)
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)))
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)))
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
546 def _bzr_merged_revisions(self, branch, revnum, scheme):
547 """Find out what revisions were merged by Bazaar in a revision.
549 :param branch: Subversion branch path.
550 :param revnum: Subversion revision number.
551 :param scheme: Branching scheme.
553 change = self.branchprop_list.get_property_diff(branch, revnum,
554 SVN_PROP_BZR_ANCESTRY+str(scheme)).splitlines()
558 assert len(change) == 1
560 return parse_merge_property(change[0])
562 def _svk_feature_to_revision_id(self, scheme, feature):
563 """Convert a SVK feature to a revision id for this repository.
565 :param scheme: Branching scheme.
566 :param feature: SVK feature.
567 :return: revision id.
570 (uuid, bp, revnum) = parse_svk_feature(feature)
571 except errors.InvalidPropertyValue:
573 if uuid != self.uuid:
575 if not scheme.is_branch(bp) and not scheme.is_tag(bp):
577 return self.generate_revision_id(revnum, bp, str(scheme))
579 def _svk_merged_revisions(self, branch, revnum, scheme):
580 """Find out what SVK features were merged in a revision.
582 :param branch: Subversion branch path.
583 :param revnum: Subversion revision number.
584 :param scheme: Branching scheme.
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:
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:
598 def revision_parents(self, revision_id, bzr_merges=None, svk_merges=None):
599 """See Repository.revision_parents()."""
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)
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):
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)
615 parent_ids.extend(bzr_merges)
618 # Commit was doing using svk apparently
619 parent_ids.extend(svk_merges)
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)
628 (path, revnum, _) = self.lookup_revision_id(revision_id)
630 parent_ids = self.revision_parents(revision_id)
632 # Commit SVN revision properties to a Revision object
633 rev = Revision(revision_id=revision_id, parent_ids=parent_ids)
635 (rev.committer, rev.message, date) = self._log.get_revision_info(revnum)
636 if rev.committer is None:
640 rev.timestamp = 1.0 * svn.core.secs_from_timestr(date, None)
642 rev.timestamp = 0.0 # FIXME: Obtain repository creation time
645 parse_revision_metadata(
646 self.branchprop_list.get_property(path, revnum,
647 SVN_PROP_BZR_REVISION_INFO, ""), rev)
649 rev.inventory_sha1 = property(
650 lambda: self.get_inventory_sha1(revision_id))
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)
659 def add_revision(self, rev_id, rev, inv=None, config=None):
660 raise NotImplementedError(self.add_revision)
662 def generate_revision_id(self, revnum, path, scheme):
663 """Generate an unambiguous revision id.
665 :param revnum: Subversion revision number.
666 :param path: Branch path.
667 :param scheme: Branching scheme name
669 :return: New revision id.
671 assert isinstance(path, str)
672 assert isinstance(revnum, int)
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:
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")
684 revid = generate_svn_revision_id(self.uuid, revnum, path,
688 (bzr_revno, revid) = parse_revid_property(line)
689 self.revmap.insert_revid(revid, path, revnum, revnum,
691 except errors.InvalidPropertyValue, e:
693 revid = generate_svn_revision_id(self.uuid, revnum, path,
695 self.revmap.insert_revid(revid, path, revnum, revnum,
700 def lookup_revision_id(self, revid, scheme=None):
701 """Parse an existing Subversion-based revision id.
703 :param revid: The revision id.
704 :param scheme: Optional branching scheme to use when searching for
706 :raises: NoSuchRevision
707 :return: Tuple with branch path, revision number and scheme.
709 def get_scheme(name):
710 assert isinstance(name, basestring)
711 return BranchingScheme.find_scheme(name)
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
722 except InvalidRevisionId:
725 # Check the record out of the revmap, if it exists
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:
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
745 for (branch, revno, _) in self.find_branches(scheme, last_revnum):
746 # Look at their bzr:revision-id-vX
748 for line in self.branchprop_list.get_property(branch, revno,
749 SVN_PROP_BZR_REVISION_ID+str(scheme), "").splitlines():
751 revids.append(parse_revid_property(line))
752 except errors.InvalidPropertyValue, ie:
755 # If there are any new entries that are not yet in the cache,
757 for (entry_revno, entry_revid) in revids:
758 if entry_revid == revid:
760 self.revmap.insert_revid(entry_revid, branch, 0, revno,
761 str(scheme), entry_revno)
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
771 (branch_path, min_revnum, max_revnum, scheme) = self.revmap.lookup_revid(revid)
772 assert isinstance(branch_path, str)
774 # Find the branch property between min_revnum and max_revnum that
776 for (bp, rev) in self.follow_branch(branch_path, max_revnum,
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
786 if entry_revid == revid:
787 self.revmap.insert_revid(revid, bp, rev, rev, scheme,
789 return (bp, rev, get_scheme(scheme))
791 raise AssertionError("Revision id %s was added incorrectly" % revid)
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))
798 def get_inventory_sha1(self, revision_id):
799 """Get the sha1 for the XML representation of an inventory.
801 :param revision_id: Revision id of the inventory for which to return
806 return osutils.sha_string(self.get_inventory_xml(revision_id))
808 def get_revision_xml(self, revision_id):
809 """Return the XML representation of a revision.
811 :param revision_id: Revision for which to return the XML.
814 return bzrlib.xml5.serializer_v5.write_revision_to_string(
815 self.get_revision(revision_id))
817 def follow_history(self, revnum, scheme):
818 """Yield all the branches found between the start of history
819 and a specified revision number.
821 :param revnum: Revision number up to which to search.
822 :return: iterator over branches in the range 0..revnum
824 assert scheme is not None
828 paths = self._log.get_revision_paths(revnum)
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 == ""
836 yielded_paths.append(bp)
837 except NotBranchError:
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.
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
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("/")
857 assert revnum > 0 or branch_path == ""
858 paths = self._log.get_revision_paths(revnum)
861 # If something underneath branch_path changed, there is a
862 # revision there, so yield it.
864 assert isinstance(p, str)
865 if (p == branch_path or
866 p.startswith(branch_path+"/") or
868 yield (branch_path, revnum)
872 # If there are no special cases, just go try the
873 # next revnum in history
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')):
881 yield (branch_path, revnum+1)
882 if paths[branch_path][1] is None:
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
890 revnum = paths[branch_path][2]
891 branch_path = paths[branch_path][1].encode("utf-8")
894 # Make sure we get the right location for the next time if
895 # one of the parents changed
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':
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 !?"
908 branch_path = paths[p][1].encode("utf-8") + branch_path[len(p):]
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.
915 :return: iterator that returns tuples with branch path,
916 changed paths and revision number.
918 assert branch_path is not None
919 assert scheme.is_branch(branch_path) or scheme.is_tag(branch_path)
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 != "":
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)
943 yield (bp, paths, revnum)
946 yield (bp, paths, revnum)
948 def has_signature_for_revision_id(self, revision_id):
949 """Check whether a signature exists for a particular revision id.
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
955 # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE
956 return False # SVN doesn't store GPG signatures. Perhaps
957 # store in SVN revision property?
960 def get_signature_text(self, revision_id):
961 """Return the signature text for a particular revision.
963 :param revision_id: Id of the revision for which to return the
965 :raises NoSuchRevision: Always
967 # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE
968 # SVN doesn't store GPG signatures
969 raise NoSuchRevision(self, revision_id)
971 def _full_revision_graph(self, scheme, _latest_revnum=None):
972 if _latest_revnum is None:
973 _latest_revnum = self.transport.get_latest_revnum()
975 for (branch, revnum) in self.follow_history(_latest_revnum,
977 mutter('%r, %r' % (branch, revnum))
978 revid = self.generate_revision_id(revnum, branch, str(scheme))
979 graph[revid] = self.revision_parents(revid)
982 def get_revision_graph(self, revision_id=None):
983 """See Repository.get_revision_graph()."""
984 if revision_id == NULL_REVISION:
987 if revision_id is None:
988 return self._full_revision_graph(self.get_scheme())
990 (path, revnum, scheme) = self.lookup_revision_id(revision_id)
992 _previous = revision_id
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]
1001 self._ancestry[_previous] = []
1003 return self._ancestry
1005 def find_branches(self, scheme, revnum=None):
1006 """Find all branches that were changed in the specified revision number.
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.
1011 assert scheme is not None
1013 revnum = self.transport.get_latest_revnum()
1015 created_branches = {}
1019 pb = ui.ui_factory.nested_progress_bar()
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))
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()
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'):
1049 for c in self.transport.get_dir(p, i)[0].keys():
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)):
1059 for p in created_branches:
1060 j = self._log.find_latest_change(p, revnum,
1061 include_parents=True,
1062 include_children=True)
1064 j = created_branches[p]
1065 ret.append((p, j, True))
1069 def is_shared(self):
1070 """Return True if this repository is flagged as a shared repository."""
1073 def get_physical_lock_status(self):
1076 def get_commit_builder(self, branch, parents, config, timestamp=None,
1077 timezone=None, committer=None, revprops=None,
1079 from commit import SvnCommitBuilder
1080 return SvnCommitBuilder(self, branch, parents, config, timestamp,
1081 timezone, committer, revprops, revision_id)