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.branch import BranchCheckResult
20 from bzrlib.errors import (InvalidRevisionId, NoSuchRevision,
21 NotBranchError, UninitializableFormat, BzrError)
22 from bzrlib.inventory import Inventory
23 from bzrlib.lockable_files import LockableFiles, TransportLock
24 import bzrlib.osutils as osutils
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
29 from bzrlib.timestamp import unpack_highres_date, format_highres_date
30 from bzrlib.trace import mutter
32 from svn.core import SubversionException, Pool
37 from branchprops import BranchPropertyList
38 from cache import create_cache_dir, sqlite3
41 from revids import (generate_svn_revision_id, parse_svn_revision_id,
42 MAPPING_VERSION, RevidMap)
43 from tree import SvnRevisionTree
45 SVN_PROP_BZR_PREFIX = 'bzr:'
46 SVN_PROP_BZR_MERGE = 'bzr:merge'
47 SVN_PROP_BZR_FILEIDS = 'bzr:file-ids'
48 SVN_PROP_SVK_MERGE = 'svk:merge'
49 SVN_PROP_BZR_FILEIDS = 'bzr:file-ids'
50 SVN_PROP_BZR_REVISION_INFO = 'bzr:revision-info'
51 SVN_REVPROP_BZR_SIGNATURE = 'bzr:gpg-signature'
52 SVN_PROP_BZR_REVISION_ID = 'bzr:revision-id-v%d' % MAPPING_VERSION
54 def parse_revision_metadata(text, rev):
55 """Parse a revision info text (as set in bzr:revision-info).
57 :param text: text to parse
58 :param rev: Revision object to apply read parameters to
61 for l in text.splitlines():
63 key, value = l.split(": ", 2)
65 raise BzrError("Missing : in revision metadata")
66 if key == "committer":
67 rev.committer = str(value)
68 elif key == "timestamp":
69 (rev.timestamp, rev.timezone) = unpack_highres_date(value)
70 elif key == "properties":
72 elif key[0] == "\t" and in_properties:
73 rev.properties[str(key[1:])] = str(value)
75 raise BzrError("Invalid key %r" % key)
77 def generate_revision_metadata(timestamp, timezone, committer, revprops):
78 """Generate revision metadata text for the specified revision
81 :param timestamp: timestamp of the revision, in seconds since epoch
82 :param timezone: timezone, specified by offset from GMT in seconds
83 :param committer: name/email of the committer
84 :param revprops: dictionary with custom revision properties
85 :return: text with data to set bzr:revision-info to.
87 assert timestamp is None or isinstance(timestamp, float)
89 if timestamp is not None:
90 text += "timestamp: %s\n" % format_highres_date(timestamp, timezone)
91 if committer is not None:
92 text += "committer: %s\n" % committer
93 if revprops is not None and revprops != {}:
94 text += "properties: \n"
95 for k, v in sorted(revprops.items()):
96 text += "\t%s: %s\n" % (k, v)
100 def svk_feature_to_revision_id(feature):
101 """Create a revision id from a svk feature identifier.
103 :param feature: The feature identifier as string.
104 :return: Matching revision id.
106 (uuid, branch, revnum) = feature.split(":")
107 return generate_svn_revision_id(uuid, int(revnum), branch.strip("/"))
110 def revision_id_to_svk_feature(revid):
111 """Create a SVK feature identifier from a revision id.
113 :param revid: Revision id to convert.
114 :return: Matching SVK feature identifier.
116 (uuid, branch, revnum) = parse_svn_revision_id(revid)
117 return "%s:/%s:%d" % (uuid, branch, revnum)
120 class SvnRepositoryFormat(RepositoryFormat):
121 """Repository format for Subversion repositories (accessed using svn_ra).
123 rich_root_data = False
126 super(SvnRepositoryFormat, self).__init__()
127 from format import SvnFormat
128 self._matchingbzrdir = SvnFormat()
130 def get_format_description(self):
131 return "Subversion Repository"
133 def initialize(self, url, shared=False, _internal=False):
134 """Svn repositories cannot be created (yet)."""
135 raise UninitializableFormat(self)
139 class SvnRepository(Repository):
141 Provides a simplified interface to a Subversion repository
142 by using the RA (remote access) API from subversion
144 def __init__(self, bzrdir, transport):
145 from fileids import SimpleFileIdMap
146 _revision_store = None
148 assert isinstance(transport, Transport)
150 control_files = LockableFiles(transport, '', TransportLock)
151 Repository.__init__(self, SvnRepositoryFormat(), bzrdir,
152 control_files, None, None, None)
154 self.transport = transport
155 self.uuid = transport.get_uuid()
156 self.base = transport.base
157 self._serializer = None
159 self.scheme = bzrdir.scheme
165 cache_file = os.path.join(self.create_cache_dir(),
166 'cache-v%d' % MAPPING_VERSION)
167 if not cachedbs.has_key(cache_file):
168 cachedbs[cache_file] = sqlite3.connect(cache_file)
169 self.cachedb = cachedbs[cache_file]
171 self._latest_revnum = transport.get_latest_revnum()
172 self._log = logwalker.LogWalker(transport=transport,
173 cache_db=self.cachedb,
174 last_revnum=self._latest_revnum)
176 self.branchprop_list = BranchPropertyList(self._log, self.cachedb)
177 self.fileid_map = SimpleFileIdMap(self, self.cachedb)
178 self.revmap = RevidMap(self.cachedb)
180 def set_branching_scheme(self, scheme):
183 def _warn_if_deprecated(self):
184 # This class isn't deprecated
188 return '%s(%r)' % (self.__class__.__name__,
191 def create_cache_dir(self):
192 cache_dir = create_cache_dir()
193 dir = os.path.join(cache_dir, self.uuid)
194 if not os.path.exists(dir):
198 def _check(self, revision_ids):
199 return BranchCheckResult(self)
201 def get_inventory(self, revision_id):
202 assert revision_id != None
203 return self.revision_tree(revision_id).inventory
205 def get_fileid_map(self, revnum, path):
206 return self.fileid_map.get_map(self.uuid, revnum, path,
207 self.revision_fileid_renames)
209 def transform_fileid_map(self, uuid, revnum, branch, changes, renames):
210 return self.fileid_map.apply_changes(uuid, revnum, branch, changes,
213 def all_revision_ids(self):
214 for (bp, rev) in self.follow_history(self.transport.get_latest_revnum()):
215 yield self.generate_revision_id(rev, bp)
217 def get_inventory_weave(self):
218 raise NotImplementedError(self.get_inventory_weave)
220 def set_make_working_trees(self, new_value):
221 """See Repository.set_make_working_trees()."""
222 pass # FIXME: ignored, nowhere to store it...
224 def make_working_trees(self):
225 """See Repository.make_working_trees().
227 Always returns False, as working trees are never created inside
228 Subversion repositories.
232 def get_ancestry(self, revision_id):
233 """See Repository.get_ancestry().
235 Note: only the first bit is topologically ordered!
237 if revision_id is None:
240 (path, revnum) = self.lookup_revision_id(revision_id)
242 ancestry = [revision_id]
244 for l in self.branchprop_list.get_property(path, revnum,
245 SVN_PROP_BZR_MERGE, "").splitlines():
246 ancestry.extend(l.split("\n"))
249 for (branch, rev) in self.follow_branch(path, revnum - 1):
250 ancestry.append(self.generate_revision_id(rev, branch))
252 ancestry.append(None)
256 def has_revision(self, revision_id):
257 """See Repository.has_revision()."""
258 if revision_id is None:
262 (path, revnum) = self.lookup_revision_id(revision_id)
263 except NoSuchRevision:
267 return (svn.core.svn_node_none != self.transport.check_path(path.encode('utf8'), revnum))
268 except SubversionException, (_, num):
269 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
273 def revision_trees(self, revids):
274 """See Repository.revision_trees()."""
276 yield self.revision_tree(revid)
278 def revision_tree(self, revision_id):
279 """See Repository.revision_tree()."""
280 if revision_id is None:
281 revision_id = NULL_REVISION
283 if revision_id == NULL_REVISION:
284 inventory = Inventory(root_id=None)
285 inventory.revision_id = revision_id
286 return RevisionTree(self, inventory, revision_id)
288 return SvnRevisionTree(self, revision_id)
290 def revision_fileid_renames(self, revid):
291 """Check which files were renamed in a particular revision."""
292 (path, revnum) = self.lookup_revision_id(revid)
293 items = self.branchprop_list.get_property_diff(path, revnum,
294 SVN_PROP_BZR_FILEIDS).splitlines()
295 return dict(map(lambda x: x.split("\t"), items))
297 def _mainline_revision_parent(self, path, revnum):
298 assert isinstance(path, basestring)
299 assert isinstance(revnum, int)
301 if not self.scheme.is_branch(path) and \
302 not self.scheme.is_tag(path):
303 raise NoSuchRevision(self, self.generate_revision_id(revnum, path))
305 it = self.follow_branch(path, revnum)
306 # the first tuple returned should match the one specified.
307 # if it's not, then the branch, revnum didn't change in the specified
308 # revision and so it is invalid
309 if (path, revnum) != it.next():
310 raise NoSuchRevision(self, self.generate_revision_id(revnum, path))
312 (branch, rev) = it.next()
313 return self.generate_revision_id(rev, branch)
314 except StopIteration:
315 # The specified revision was the first one in the branch
318 def revision_parents(self, revision_id, merged_data=None):
320 (branch, revnum) = self.lookup_revision_id(revision_id)
321 mainline_parent = self._mainline_revision_parent(branch, revnum)
322 if mainline_parent is not None:
323 parent_ids.append(mainline_parent)
324 (parent_path, parent_revnum) = self.lookup_revision_id(mainline_parent)
328 # if the branch didn't change, bzr:merge can't have changed
329 if not self._log.touches_path(branch, revnum):
332 if merged_data is None:
333 new_merge = self.branchprop_list.get_property(branch, revnum,
334 SVN_PROP_BZR_MERGE, "").splitlines()
336 if len(new_merge) == 0 or parent_path is None:
339 old_merge = self.branchprop_list.get_property(parent_path, parent_revnum,
340 SVN_PROP_BZR_MERGE, "").splitlines()
342 assert (len(old_merge) == len(new_merge) or
343 len(old_merge) + 1 == len(new_merge))
345 if len(old_merge) < len(new_merge):
346 merged_data = new_merge[-1]
350 if ' ' in merged_data:
351 mutter('invalid revision id %r in merged property, skipping' % merged_data)
354 if merged_data != "":
355 parent_ids.extend(merged_data.split("\t"))
359 def get_revision(self, revision_id):
360 """See Repository.get_revision."""
361 if not revision_id or not isinstance(revision_id, basestring):
362 raise InvalidRevisionId(revision_id=revision_id, branch=self)
364 (path, revnum) = self.lookup_revision_id(revision_id)
366 parent_ids = self.revision_parents(revision_id)
368 # Commit SVN revision properties to a Revision object
369 rev = Revision(revision_id=revision_id, parent_ids=parent_ids)
371 (rev.committer, rev.message, date) = self._log.get_revision_info(revnum)
372 if rev.committer is None:
376 rev.timestamp = 1.0 * svn.core.secs_from_timestr(date, None)
378 rev.timestamp = 0.0 # FIXME: Obtain repository creation time
381 parse_revision_metadata(
382 self.branchprop_list.get_property(path, revnum,
383 SVN_PROP_BZR_REVISION_INFO, ""), rev)
385 rev.inventory_sha1 = property(lambda: self.get_inventory_sha1(revision_id))
389 def get_revisions(self, revision_ids):
390 # TODO: More efficient implementation?
391 return map(self.get_revision, revision_ids)
393 def add_revision(self, rev_id, rev, inv=None, config=None):
394 raise NotImplementedError(self.add_revision)
396 def fileid_involved_between_revs(self, from_revid, to_revid):
397 raise NotImplementedError(self.fileid_involved_by_set)
399 def fileid_involved(self, last_revid=None):
400 raise NotImplementedError(self.fileid_involved)
402 def fileids_altered_by_revision_ids(self, revision_ids):
403 raise NotImplementedError(self.fileids_altered_by_revision_ids)
405 def fileid_involved_by_set(self, changes):
406 raise NotImplementedError(self.fileid_involved_by_set)
408 def generate_revision_id(self, revnum, path):
409 """Generate an unambiguous revision id.
411 :param revnum: Subversion revision number.
412 :param path: Branch path.
414 :return: New revision id.
416 # Look in the cache to see if it already has a revision id
417 revid = self.revmap.lookup_branch_revnum(revnum, path)
418 if revid is not None:
421 # Lookup the revision from the bzr:revision-id-vX property
422 revid = self.branchprop_list.get_property_diff(path, revnum,
423 SVN_PROP_BZR_REVISION_ID).strip("\n")
426 revid = generate_svn_revision_id(self.uuid, revnum, path)
428 self.revmap.insert_revid(revid, path, revnum, revnum, "undefined")
432 def lookup_revision_id(self, revid):
433 """Parse an existing Subversion-based revision id.
435 :param revid: The revision id.
436 :raises: NoSuchRevision
437 :return: Tuple with branch path and revision number.
442 (uuid, branch_path, revnum) = parse_svn_revision_id(revid)
443 assert isinstance(branch_path, str)
444 if uuid == self.uuid:
445 return (branch_path, revnum)
446 # If the UUID doesn't match, this may still be a valid revision
447 # id; a revision from another SVN repository may be pushed into
449 except InvalidRevisionId:
452 # Check the record out of the revmap, if it exists
454 (branch_path, min_revnum, max_revnum, \
455 scheme) = self.revmap.lookup_revid(revid)
456 assert isinstance(branch_path, str)
457 # Entry already complete?
458 if min_revnum == max_revnum:
459 return (branch_path, min_revnum)
460 except NoSuchRevision:
461 # If there is no entry in the map, walk over all branches:
462 for (branch, revno, exists) in self.find_branches():
463 # Look at their bzr:revision-id-vX
464 revids = self.branchprop_list.get_property(branch, revno,
465 SVN_PROP_BZR_REVISION_ID, "").splitlines()
467 # If there are any new entries that are not yet in the cache,
470 self.revmap.insert_revid(r, branch, 0, revno,
476 (branch_path, min_revnum, max_revnum, scheme) = self.revmap.lookup_revid(revid)
477 assert isinstance(branch_path, str)
479 # Find the branch property between min_revnum and max_revnum that
482 for (bp, rev) in self.follow_branch(branch_path, max_revnum):
483 if self.branchprop_list.get_property_diff(bp, rev, SVN_PROP_BZR_REVISION_ID).strip("\n") == revid:
484 self.revmap.insert_revid(revid, bp, rev, rev, scheme)
487 raise AssertionError("Revision id %s was added incorrectly" % revid)
489 def get_inventory_xml(self, revision_id):
490 return bzrlib.xml5.serializer_v5.write_inventory_to_string(
491 self.get_inventory(revision_id))
493 """Get the sha1 for the XML representation of an inventory.
495 :param revision_id: Revision id of the inventory for which to return the
499 def get_inventory_sha1(self, revision_id):
500 return osutils.sha_string(self.get_inventory_xml(revision_id))
502 """Return the XML representation of a revision.
504 :param revision_id: Revision for which to return the XML.
507 def get_revision_xml(self, revision_id):
508 return bzrlib.xml5.serializer_v5.write_revision_to_string(
509 self.get_revision(revision_id))
511 """Yield all the branches found between the start of history
512 and a specified revision number.
514 :param revnum: Revision number up to which to search.
515 :return: iterator over branches in the range 0..revnum
517 def follow_history(self, revnum):
520 paths = self._log.get_revision_paths(revnum)
523 bp = self.scheme.unprefix(p)[0]
524 if not bp in yielded_paths:
525 if not paths.has_key(bp) or paths[bp][0] != 'D':
526 assert revnum > 0 or bp == ""
528 yielded_paths.append(bp)
529 except NotBranchError:
533 """Follow the history of a branch. Will yield all the
534 left-hand side ancestors of a specified revision.
536 :param branch_path: Subversion path to search.
537 :param revnum: Revision number in Subversion to start.
538 :return: iterator over the ancestors
540 def follow_branch(self, branch_path, revnum):
541 assert branch_path is not None
542 assert isinstance(revnum, int) and revnum >= 0
543 if not self.scheme.is_branch(branch_path) and \
544 not self.scheme.is_tag(branch_path):
545 raise errors.NotSvnBranchPath(branch_path, revnum)
546 branch_path = branch_path.strip("/")
549 paths = self._log.get_revision_paths(revnum)
552 # If something underneath branch_path changed, there is a
553 # revision there, so yield it.
555 if p.startswith(branch_path+"/") or branch_path == "":
556 yield (branch_path, revnum)
560 # If there are no special cases, just go try the
561 # next revnum in history
564 # Make sure we get the right location for next time, if
565 # the branch itself was copied
566 if (paths.has_key(branch_path) and
567 paths[branch_path][0] in ('R', 'A')):
569 yield (branch_path, revnum+1)
570 if paths[branch_path][1] is None:
572 if not self.scheme.is_branch(paths[branch_path][1]) and \
573 not self.scheme.is_tag(paths[branch_path][1]):
574 # FIXME: if copyfrom_path is not a branch path,
575 # should simulate a reverse "split" of a branch
576 # for now, just make it look like the branch ended here
578 revnum = paths[branch_path][2]
579 branch_path = paths[branch_path][1]
582 # Make sure we get the right location for the next time if
583 # one of the parents changed
585 # Path names need to be sorted so the longer paths
586 # override the shorter ones
587 path_names = paths.keys()
590 if branch_path.startswith(p+"/"):
591 assert paths[p][1] is not None and paths[p][0] in ('A', 'R'), "Parent didn't exist yet, but child wasn't added !?"
594 branch_path = paths[p][1] + branch_path[len(p):]
596 """Return all the changes that happened in a branch
597 between branch_path and revnum.
599 :return: iterator that returns tuples with branch path,
600 changed paths and revision number.
602 def follow_branch_history(self, branch_path, revnum):
603 assert branch_path is not None
604 if not self.scheme.is_branch(branch_path) and \
605 not self.scheme.is_tag(branch_path):
606 raise errors.NotSvnBranchPath(branch_path, revnum)
608 for (bp, paths, revnum) in self._log.follow_path(branch_path, revnum):
609 if (paths.has_key(bp) and
610 paths[bp][1] is not None and
611 not self.scheme.is_branch(paths[bp][1]) and
612 not self.scheme.is_tag(paths[bp][1])):
613 # FIXME: if copyfrom_path is not a branch path,
614 # should simulate a reverse "split" of a branch
615 # for now, just make it look like the branch ended here
616 for c in self._log.find_children(paths[bp][1], paths[bp][2]):
617 path = c.replace(paths[bp][1], bp+"/", 1).replace("//", "/")
618 paths[path] = ('A', None, -1)
619 paths[bp] = ('A', None, -1)
621 yield (bp, paths, revnum)
624 yield (bp, paths, revnum)
626 """Check whether a signature exists for a particular revision id.
628 :param revision_id: Revision id for which the signatures should be looked up.
629 :return: False, as no signatures are stored for revisions in Subversion
632 def has_signature_for_revision_id(self, revision_id):
633 # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE
634 return False # SVN doesn't store GPG signatures. Perhaps
635 # store in SVN revision property?
637 """Return the signature text for a particular revision.
639 :param revision_id: Id of the revision for which to return the signature.
640 :raises NoSuchRevision: Always
642 def get_signature_text(self, revision_id):
643 # TODO: Retrieve from SVN_PROP_BZR_SIGNATURE
644 # SVN doesn't store GPG signatures
645 raise NoSuchRevision(self, revision_id)
647 def _full_revision_graph(self):
649 for (branch, revnum) in self.follow_history(self._latest_revnum):
650 mutter('%r, %r' % (branch, revnum))
651 revid = self.generate_revision_id(revnum, branch)
652 graph[revid] = self.revision_parents(revid)
655 def get_revision_graph(self, revision_id=None):
656 if revision_id == NULL_REVISION:
659 if revision_id is None:
660 return self._full_revision_graph()
662 (path, revnum) = self.lookup_revision_id(revision_id)
664 _previous = revision_id
668 for (branch, rev) in self.follow_branch(path, revnum - 1):
669 revid = self.generate_revision_id(rev, branch)
670 self._ancestry[_previous] = [revid]
673 self._ancestry[_previous] = []
675 return self._ancestry
677 def find_branches(self, revnum=None, pb=None):
678 """Find all branches that were changed in the specified revision number.
680 :param revnum: Revision to search for branches.
681 :return: iterator that returns tuples with (path, revision number, still exists). The revision number is the revision in which the branch last existed.
684 revnum = self.transport.get_latest_revnum()
686 created_branches = {}
688 for i in range(revnum+1):
690 pb.update("finding branches", i, revnum+1)
691 paths = self._log.get_revision_paths(i)
695 if self.scheme.is_branch(p) or self.scheme.is_tag(p):
696 if paths[p][0] in ('R', 'D'):
697 del created_branches[p]
698 j = self._log.find_latest_change(p, i-1, recurse=True)
701 if paths[p][0] in ('A', 'R'):
702 created_branches[p] = i
703 elif self.scheme.is_branch_parent(p) or \
704 self.scheme.is_tag_parent(p):
705 if paths[p][0] in ('R', 'D'):
706 k = created_branches.keys()
708 if c.startswith(p+"/"):
709 del created_branches[c]
710 j = self._log.find_latest_change(c, i-1,
713 if paths[p][0] in ('A', 'R'):
717 for c in self.transport.get_dir(p, i)[0].keys():
719 if self.scheme.is_branch(n) or self.scheme.is_tag(n):
720 created_branches[n] = i
721 elif self.scheme.is_branch_parent(n) or self.scheme.is_tag_parent(n):
724 for p in created_branches:
725 j = self._log.find_latest_change(p, revnum, recurse=True)
727 j = created_branches[p]
731 """Return True if this repository is flagged as a shared repository."""
734 def get_physical_lock_status(self):
737 def get_commit_builder(self, branch, parents, config, timestamp=None,
738 timezone=None, committer=None, revprops=None,
740 from commit import SvnCommitBuilder
741 return SvnCommitBuilder(self, branch, parents, config, timestamp,
742 timezone, committer, revprops, revision_id)