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 = value.decode("utf-8")
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:])] = value.decode("utf-8")
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.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]
299 self._log = logwalker.LogWalker(transport=transport,
300 cache_db=self.cachedb)
302 self.branchprop_list = BranchPropertyList(self._log, self.cachedb)
303 self.fileid_map = SimpleFileIdMap(self, cachedir_transport)
304 self.revmap = RevidMap(self.cachedb)
306 self._hinted_branch_path = branch_path
308 def lhs_missing_revisions(self, revhistory, stop_revision):
310 slice = revhistory[:revhistory.index(stop_revision)+1]
311 for revid in reversed(slice):
312 if self.has_revision(revid):
315 missing.append(revid)
316 raise UnrelatedBranches()
318 def get_transaction(self):
319 raise NotImplementedError(self.get_transaction)
321 def get_scheme(self):
322 """Determine the branching scheme to use for this repository.
324 :return: Branching scheme.
326 if self._scheme is not None:
329 scheme = self.get_config().get_branching_scheme()
330 if scheme is not None:
331 self._scheme = scheme
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)
340 self.set_branching_scheme(
341 self._guess_scheme(last_revnum, self._hinted_branch_path),
342 store=(last_revnum > 20))
346 def _get_property_scheme(self, revnum=None):
348 revnum = self.transport.get_latest_revnum()
349 text = self.branchprop_list.get_property("",
350 revnum, SVN_PROP_BZR_BRANCHING_SCHEME, None)
353 return ListBranchingScheme(parse_list_scheme_text(text))
355 def set_property_scheme(self, scheme):
356 def done(revision, date, author):
358 editor = self.transport.get_commit_editor(
359 {svn.core.SVN_PROP_REVISION_LOG: "Updating branching scheme for Bazaar."},
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)
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,
371 mutter("Guessed branching scheme: %r" % scheme)
374 def set_branching_scheme(self, scheme, store=True):
375 self._scheme = scheme
377 self.get_config().set_branching_scheme(str(scheme))
379 def _warn_if_deprecated(self):
380 # This class isn't deprecated
384 return '%s(%r)' % (self.__class__.__name__,
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):
394 def _check(self, revision_ids):
395 return BranchCheckResult(self)
397 def get_inventory(self, revision_id):
398 assert revision_id != None
399 return self.revision_tree(revision_id).inventory
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)
405 def transform_fileid_map(self, uuid, revnum, branch, changes, renames,
407 return self.fileid_map.apply_changes(uuid, revnum, branch, changes,
410 def all_revision_ids(self, scheme=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))
417 def get_inventory_weave(self):
418 """See Repository.get_inventory_weave()."""
419 raise NotImplementedError(self.get_inventory_weave)
421 def set_make_working_trees(self, new_value):
422 """See Repository.set_make_working_trees()."""
423 pass # FIXME: ignored, nowhere to store it...
425 def make_working_trees(self):
426 """See Repository.make_working_trees().
428 Always returns False, as working trees are never created inside
429 Subversion repositories.
433 def get_ancestry(self, revision_id, topo_sorted=True):
434 """See Repository.get_ancestry().
436 Note: only the first bit is topologically ordered!
438 if revision_id is None:
441 (path, revnum, scheme) = self.lookup_revision_id(revision_id)
443 ancestry = [revision_id]
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"))
450 for (branch, rev) in self.follow_branch(path, revnum - 1, scheme):
452 self.generate_revision_id(rev, branch, str(scheme)))
454 ancestry.append(None)
458 def has_revision(self, revision_id):
459 """See Repository.has_revision()."""
460 if revision_id is None:
464 (path, revnum, _) = self.lookup_revision_id(revision_id)
465 except NoSuchRevision:
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:
476 def revision_trees(self, revids):
477 """See Repository.revision_trees()."""
479 yield self.revision_tree(revid)
481 def revision_tree(self, revision_id):
482 """See Repository.revision_tree()."""
483 if revision_id is None:
484 revision_id = NULL_REVISION
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)
491 return SvnRevisionTree(self, revision_id)
493 def revision_fileid_renames(self, revid):
494 """Check which files were renamed in a particular revision.
496 :param revid: Id of revision to look up.
497 :return: dictionary with paths as keys, file ids as values
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):
504 fileids = self.branchprop_list.get_property(path, revnum,
505 SVN_PROP_BZR_FILEIDS)
509 for line in fileids.splitlines():
510 (path, key) = line.split("\t", 2)
511 ret[urllib.unquote(path)] = osutils.safe_file_id(key)
514 def _mainline_revision_parent(self, path, revnum, scheme):
515 """Find the mainline parent of the specified revision.
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
523 assert isinstance(path, basestring)
524 assert isinstance(revnum, int)
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)))
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)))
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
545 def _bzr_merged_revisions(self, branch, revnum, scheme):
546 """Find out what revisions were merged by Bazaar in a revision.
548 :param branch: Subversion branch path.
549 :param revnum: Subversion revision number.
550 :param scheme: Branching scheme.
552 change = self.branchprop_list.get_property_diff(branch, revnum,
553 SVN_PROP_BZR_ANCESTRY+str(scheme)).splitlines()
557 assert len(change) == 1
559 return parse_merge_property(change[0])
561 def _svk_feature_to_revision_id(self, scheme, feature):
562 """Convert a SVK feature to a revision id for this repository.
564 :param scheme: Branching scheme.
565 :param feature: SVK feature.
566 :return: revision id.
569 (uuid, bp, revnum) = parse_svk_feature(feature)
570 except errors.InvalidPropertyValue:
572 if uuid != self.uuid:
574 if not scheme.is_branch(bp) and not scheme.is_tag(bp):
576 return self.generate_revision_id(revnum, bp, str(scheme))
578 def _svk_merged_revisions(self, branch, revnum, scheme):
579 """Find out what SVK features were merged in a revision.
581 :param branch: Subversion branch path.
582 :param revnum: Subversion revision number.
583 :param scheme: Branching scheme.
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:
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:
597 def get_parents(self, revids):
599 for revision_id in revids:
600 if revision_id == NULL_REVISION:
604 parents = self.revision_parents(revision_id)
605 except NoSuchRevision:
608 if len(parents) == 0:
609 parents = [NULL_REVISION]
610 parents_list.append(parents)
613 def revision_parents(self, revision_id, bzr_merges=None, svk_merges=None):
614 """See Repository.revision_parents()."""
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)
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):
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)
630 parent_ids.extend(bzr_merges)
633 # Commit was doing using svk apparently
634 parent_ids.extend(svk_merges)
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)
643 (path, revnum, _) = self.lookup_revision_id(revision_id)
645 parent_ids = self.revision_parents(revision_id)
647 # Commit SVN revision properties to a Revision object
648 rev = Revision(revision_id=revision_id, parent_ids=parent_ids)
650 svn_revprops = self.transport.revprop_list(revnum)
652 if svn_revprops.has_key(svn.core.SVN_PROP_REVISION_AUTHOR):
653 rev.committer = svn_revprops[svn.core.SVN_PROP_REVISION_AUTHOR]
657 rev.message = svn_revprops.get(svn.core.SVN_PROP_REVISION_LOG)
661 rev.message = rev.message.decode("utf-8")
662 except UnicodeDecodeError:
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)
668 rev.timestamp = 0.0 # FIXME: Obtain repository creation time
671 parse_revision_metadata(
672 self.branchprop_list.get_property(path, revnum,
673 SVN_PROP_BZR_REVISION_INFO, ""), rev)
675 rev.inventory_sha1 = property(
676 lambda: self.get_inventory_sha1(revision_id))
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)
685 def add_revision(self, rev_id, rev, inv=None, config=None):
686 raise NotImplementedError(self.add_revision)
688 def generate_revision_id(self, revnum, path, scheme):
689 """Generate an unambiguous revision id.
691 :param revnum: Subversion revision number.
692 :param path: Branch path.
693 :param scheme: Branching scheme name
695 :return: New revision id.
697 assert isinstance(path, str)
698 assert isinstance(revnum, int)
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:
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")
710 revid = generate_svn_revision_id(self.uuid, revnum, path,
714 (bzr_revno, revid) = parse_revid_property(line)
715 self.revmap.insert_revid(revid, path, revnum, revnum,
717 except errors.InvalidPropertyValue, e:
719 revid = generate_svn_revision_id(self.uuid, revnum, path,
721 self.revmap.insert_revid(revid, path, revnum, revnum,
726 def lookup_revision_id(self, revid, scheme=None):
727 """Parse an existing Subversion-based revision id.
729 :param revid: The revision id.
730 :param scheme: Optional branching scheme to use when searching for
732 :raises: NoSuchRevision
733 :return: Tuple with branch path, revision number and scheme.
735 def get_scheme(name):
736 assert isinstance(name, basestring)
737 return BranchingScheme.find_scheme(name)
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
748 except InvalidRevisionId:
751 # Check the record out of the revmap, if it exists
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:
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
771 for (branch, revno, _) in self.find_branches(scheme, last_revnum):
772 # Look at their bzr:revision-id-vX
774 for line in self.branchprop_list.get_property(branch, revno,
775 SVN_PROP_BZR_REVISION_ID+str(scheme), "").splitlines():
777 revids.append(parse_revid_property(line))
778 except errors.InvalidPropertyValue, ie:
781 # If there are any new entries that are not yet in the cache,
783 for (entry_revno, entry_revid) in revids:
784 if entry_revid == revid:
786 self.revmap.insert_revid(entry_revid, branch, 0, revno,
787 str(scheme), entry_revno)
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
797 (branch_path, min_revnum, max_revnum, scheme) = self.revmap.lookup_revid(revid)
798 assert isinstance(branch_path, str)
800 # Find the branch property between min_revnum and max_revnum that
802 for (bp, rev) in self.follow_branch(branch_path, max_revnum,
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
812 if entry_revid == revid:
813 self.revmap.insert_revid(revid, bp, rev, rev, scheme,
815 return (bp, rev, get_scheme(scheme))
817 raise AssertionError("Revision id %s was added incorrectly" % revid)
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))
824 def get_inventory_sha1(self, revision_id):
825 """Get the sha1 for the XML representation of an inventory.
827 :param revision_id: Revision id of the inventory for which to return
832 return osutils.sha_string(self.get_inventory_xml(revision_id))
834 def get_revision_xml(self, revision_id):
835 """Return the XML representation of a revision.
837 :param revision_id: Revision for which to return the XML.
840 return bzrlib.xml5.serializer_v5.write_revision_to_string(
841 self.get_revision(revision_id))
843 def follow_history(self, revnum, scheme):
844 """Yield all the branches found between the start of history
845 and a specified revision number.
847 :param revnum: Revision number up to which to search.
848 :return: iterator over branches in the range 0..revnum
850 assert scheme is not None
854 paths = self._log.get_revision_paths(revnum)
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 == ""
862 yielded_paths.append(bp)
863 except NotBranchError:
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.
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
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("/")
883 assert revnum > 0 or branch_path == ""
884 paths = self._log.get_revision_paths(revnum)
887 # If something underneath branch_path changed, there is a
888 # revision there, so yield it.
890 assert isinstance(p, str)
891 if (p == branch_path or
892 p.startswith(branch_path+"/") or
894 yield (branch_path, revnum)
898 # If there are no special cases, just go try the
899 # next revnum in history
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')):
907 yield (branch_path, revnum+1)
908 if paths[branch_path][1] is None:
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
916 revnum = paths[branch_path][2]
917 branch_path = paths[branch_path][1].encode("utf-8")
920 # Make sure we get the right location for the next time if
921 # one of the parents changed
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':
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 !?"
934 branch_path = paths[p][1].encode("utf-8") + branch_path[len(p):]
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.
941 :return: iterator that returns tuples with branch path,
942 changed paths and revision number.
944 assert branch_path is not None
945 assert scheme.is_branch(branch_path) or scheme.is_tag(branch_path)
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 != "":
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)
969 yield (bp, paths, revnum)
972 yield (bp, paths, revnum)
974 def get_config(self):
975 return SvnRepositoryConfig(self.uuid)
977 def has_signature_for_revision_id(self, revision_id):
978 """Check whether a signature exists for a particular revision id.
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
984 # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE
985 return False # SVN doesn't store GPG signatures. Perhaps
986 # store in SVN revision property?
989 def get_signature_text(self, revision_id):
990 """Return the signature text for a particular revision.
992 :param revision_id: Id of the revision for which to return the
994 :raises NoSuchRevision: Always
996 # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE
997 # SVN doesn't store GPG signatures
998 raise NoSuchRevision(self, revision_id)
1000 def _full_revision_graph(self, scheme, _latest_revnum=None):
1001 if _latest_revnum is None:
1002 _latest_revnum = self.transport.get_latest_revnum()
1004 for (branch, revnum) in self.follow_history(_latest_revnum,
1006 mutter('%r, %r' % (branch, revnum))
1007 revid = self.generate_revision_id(revnum, branch, str(scheme))
1008 graph[revid] = self.revision_parents(revid)
1011 def get_revision_graph(self, revision_id=None):
1012 """See Repository.get_revision_graph()."""
1013 if revision_id == NULL_REVISION:
1016 if revision_id is None:
1017 return self._full_revision_graph(self.get_scheme())
1019 (path, revnum, scheme) = self.lookup_revision_id(revision_id)
1021 _previous = revision_id
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]
1030 self._ancestry[_previous] = []
1032 return self._ancestry
1034 def find_branches(self, scheme, revnum=None):
1035 """Find all branches that were changed in the specified revision number.
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.
1040 assert scheme is not None
1042 revnum = self.transport.get_latest_revnum()
1044 created_branches = {}
1048 pb = ui.ui_factory.nested_progress_bar()
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))
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()
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'):
1079 for c in self.transport.get_dir(p, i)[0].keys():
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)):
1086 except SubversionException, (_, svn.core.SVN_ERR_FS_NOT_DIRECTORY):
1091 for p in created_branches:
1092 j = self._log.find_latest_change(p, revnum,
1093 include_parents=True,
1094 include_children=True)
1096 j = created_branches[p]
1097 ret.append((p, j, True))
1101 def is_shared(self):
1102 """Return True if this repository is flagged as a shared repository."""
1105 def get_physical_lock_status(self):
1108 def get_commit_builder(self, branch, parents, config, timestamp=None,
1109 timezone=None, committer=None, revprops=None,
1111 from commit import SvnCommitBuilder
1112 return SvnCommitBuilder(self, branch, parents, config, timestamp,
1113 timezone, committer, revprops, revision_id)