"""Subversion repository access."""
import bzrlib
-from bzrlib import osutils, ui
-from bzrlib.branch import BranchCheckResult
+from bzrlib import osutils, ui, urlutils
+from bzrlib.branch import Branch, BranchCheckResult
from bzrlib.errors import (InvalidRevisionId, NoSuchRevision, NotBranchError,
UninitializableFormat, UnrelatedBranches)
from bzrlib.inventory import Inventory
from bzrlib.revisiontree import RevisionTree
from bzrlib.revision import Revision, NULL_REVISION
from bzrlib.transport import Transport, get_transport
-from bzrlib.trace import mutter
+from bzrlib.trace import info, mutter
from svn.core import SubversionException, Pool
import svn.core
from config import SvnRepositoryConfig
import errors
import logwalker
-from revids import (generate_svn_revision_id, parse_svn_revision_id,
- MAPPING_VERSION, RevidMap)
+from mapping import (default_mapping, SVN_PROP_BZR_REVISION_ID,
+ SVN_PROP_BZR_REVISION_INFO, SVN_PROP_BZR_BRANCHING_SCHEME,
+ SVN_PROP_BZR_ANCESTRY, SVN_PROP_BZR_FILEIDS,
+ parse_revision_metadata, parse_revid_property)
+
+from revids import RevidMap
from scheme import (BranchingScheme, ListBranchingScheme,
parse_list_scheme_text, guess_scheme_from_history)
from tree import SvnRevisionTree
-import time
import urllib
-SVN_PROP_BZR_PREFIX = 'bzr:'
-SVN_PROP_BZR_ANCESTRY = 'bzr:ancestry:v%d-' % MAPPING_VERSION
-SVN_PROP_BZR_FILEIDS = 'bzr:file-ids'
-SVN_PROP_BZR_MERGE = 'bzr:merge'
SVN_PROP_SVK_MERGE = 'svk:merge'
-SVN_PROP_BZR_REVISION_INFO = 'bzr:revision-info'
-SVN_PROP_BZR_REVISION_ID = 'bzr:revision-id:v%d-' % MAPPING_VERSION
-SVN_PROP_BZR_BRANCHING_SCHEME = 'bzr:branching-scheme'
-
-SVN_REVPROP_BZR_COMMITTER = 'bzr:committer'
-SVN_REVPROP_BZR_FILEIDS = 'bzr:file-ids'
-SVN_REVPROP_BZR_MERGE = 'bzr:merge'
-SVN_REVPROP_BZR_REVISION_ID = 'bzr:revision-id'
-SVN_REVPROP_BZR_REVPROP_PREFIX = 'bzr:revprop:'
-SVN_REVPROP_BZR_ROOT = 'bzr:root'
-SVN_REVPROP_BZR_SCHEME = 'bzr:scheme'
-SVN_REVPROP_BZR_SIGNATURE = 'bzr:gpg-signature'
-
-# The following two functions don't use day names (which can vary by
-# locale) unlike the alternatives in bzrlib.timestamp
-
-def format_highres_date(t, offset=0):
- """Format a date, such that it includes higher precision in the
- seconds field.
-
- :param t: The local time in fractional seconds since the epoch
- :type t: float
- :param offset: The timezone offset in integer seconds
- :type offset: int
- """
- assert isinstance(t, float)
-
- # This has to be formatted for "original" date, so that the
- # revision XML entry will be reproduced faithfully.
- if offset is None:
- offset = 0
- tt = time.gmtime(t + offset)
-
- return (time.strftime("%Y-%m-%d %H:%M:%S", tt)
- # Get the high-res seconds, but ignore the 0
- + ('%.9f' % (t - int(t)))[1:]
- + ' %+03d%02d' % (offset / 3600, (offset / 60) % 60))
-
-
-def unpack_highres_date(date):
- """This takes the high-resolution date stamp, and
- converts it back into the tuple (timestamp, timezone)
- Where timestamp is in real UTC since epoch seconds, and timezone is an
- integer number of seconds offset.
-
- :param date: A date formated by format_highres_date
- :type date: string
- """
- # skip day if applicable
- if not date[0].isdigit():
- space_loc = date.find(' ')
- if space_loc == -1:
- raise ValueError("No valid date: %r" % date)
- date = date[space_loc+1:]
- # Up until the first period is a datestamp that is generated
- # as normal from time.strftime, so use time.strptime to
- # parse it
- dot_loc = date.find('.')
- if dot_loc == -1:
- raise ValueError(
- 'Date string does not contain high-precision seconds: %r' % date)
- base_time = time.strptime(date[:dot_loc], "%Y-%m-%d %H:%M:%S")
- fract_seconds, offset = date[dot_loc:].split()
- fract_seconds = float(fract_seconds)
-
- offset = int(offset)
-
- hours = int(offset / 100)
- minutes = (offset % 100)
- seconds_offset = (hours * 3600) + (minutes * 60)
-
- # time.mktime returns localtime, but calendar.timegm returns UTC time
- timestamp = calendar.timegm(base_time)
- timestamp -= seconds_offset
- # Add back in the fractional seconds
- timestamp += fract_seconds
- return (timestamp, seconds_offset)
-
-
-def parse_merge_property(line):
- """Parse a bzr:merge property value.
-
- :param line: Line to parse
- :return: List of revisions merged
- """
- if ' ' in line:
- mutter('invalid revision id %r in merged property, skipping' % line)
- return []
-
- return filter(lambda x: x != "", line.split("\t"))
-
-
-def parse_revid_property(line):
- """Parse a (revnum, revid) tuple as set in revision id properties.
- :param line: line to parse
- :return: tuple with (bzr_revno, revid)
- """
- assert not '\n' in line
- try:
- (revno, revid) = line.split(' ', 1)
- except ValueError:
- raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_ID,
- "missing space")
- if revid == "":
- raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_ID,
- "empty revision id")
- return (int(revno), revid)
-
-
-def parse_revision_metadata(text, rev):
- """Parse a revision info text (as set in bzr:revision-info).
-
- :param text: text to parse
- :param rev: Revision object to apply read parameters to
- """
- in_properties = False
- for l in text.splitlines():
- try:
- key, value = l.split(": ", 2)
- except ValueError:
- raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_INFO,
- "Missing : in revision metadata")
- if key == "committer":
- rev.committer = str(value)
- elif key == "timestamp":
- (rev.timestamp, rev.timezone) = unpack_highres_date(value)
- elif key == "properties":
- in_properties = True
- elif key[0] == "\t" and in_properties:
- rev.properties[str(key[1:])] = str(value)
- else:
- raise errors.InvalidPropertyValue(SVN_PROP_BZR_REVISION_INFO,
- "Invalid key %r" % key)
-
-
-def generate_revision_metadata(timestamp, timezone, committer, revprops):
- """Generate revision metadata text for the specified revision
- properties.
-
- :param timestamp: timestamp of the revision, in seconds since epoch
- :param timezone: timezone, specified by offset from GMT in seconds
- :param committer: name/email of the committer
- :param revprops: dictionary with custom revision properties
- :return: text with data to set bzr:revision-info to.
- """
- assert timestamp is None or isinstance(timestamp, float)
- text = ""
- if timestamp is not None:
- text += "timestamp: %s\n" % format_highres_date(timestamp, timezone)
- if committer is not None:
- text += "committer: %s\n" % committer
- if revprops is not None and revprops != {}:
- text += "properties: \n"
- for k, v in sorted(revprops.items()):
- text += "\t%s: %s\n" % (k, v)
- return text
-
def parse_svk_feature(feature):
"""Parse a svk feature identifier.
:param revid: Revision id to convert.
:return: Matching SVK feature identifier.
"""
- (uuid, branch, revnum, _) = parse_svn_revision_id(revid)
+ assert isinstance(revid, str)
+ (uuid, branch, revnum, _) = default_mapping.parse_revision_id(revid)
# TODO: What about renamed revisions? Should use
# repository.lookup_revision_id here.
return "%s:/%s:%d" % (uuid, branch, revnum)
rich_root_data = True
def __get_matchingbzrdir(self):
- from format import SvnFormat
- return SvnFormat()
+ from remote import SvnRemoteFormat
+ return SvnRemoteFormat()
_matchingbzrdir = property(__get_matchingbzrdir)
def check_conversion_target(self, target_repo_format):
return target_repo_format.rich_root_data
+CACHE_DB_VERSION = 3
+
cachedbs = {}
class SvnRepository(Repository):
self._serializer = None
self.dir_cache = {}
self.pool = Pool()
- self.config = SvnRepositoryConfig(self.uuid)
- self.config.add_location(self.base)
- self._revids_seen = {}
+ self.get_config().add_location(self.base)
cache_dir = self.create_cache_dir()
cachedir_transport = get_transport(cache_dir)
- cache_file = os.path.join(cache_dir, 'cache-v%d' % MAPPING_VERSION)
+ cache_file = os.path.join(cache_dir, 'cache-v%d' % CACHE_DB_VERSION)
if not cachedbs.has_key(cache_file):
cachedbs[cache_file] = sqlite3.connect(cache_file)
self.cachedb = cachedbs[cache_file]
def get_transaction(self):
raise NotImplementedError(self.get_transaction)
+ def get_stored_scheme(self):
+ """Retrieve the stored branching scheme, either in the repository
+ or in the configuration file.
+ """
+ scheme = self.get_config().get_branching_scheme()
+ if scheme is not None:
+ return (scheme, self.get_config().branching_scheme_is_mandatory())
+
+ last_revnum = self.transport.get_latest_revnum()
+ scheme = self._get_property_scheme(last_revnum)
+ if scheme is not None:
+ return (scheme, True)
+
+ return (None, False)
+
def get_scheme(self):
"""Determine the branching scheme to use for this repository.
:return: Branching scheme.
"""
+ # First, try to use the branching scheme we already know
if self._scheme is not None:
return self._scheme
- scheme = self.config.get_branching_scheme()
- if scheme is not None:
+ (scheme, mandatory) = self.get_stored_scheme()
+ if mandatory:
self._scheme = scheme
return scheme
- last_revnum = self.transport.get_latest_revnum()
- scheme = self._get_property_scheme(last_revnum)
if scheme is not None:
- self.set_branching_scheme(scheme)
- return scheme
+ if (self._hinted_branch_path is None or
+ scheme.is_branch(self._hinted_branch_path)):
+ self._scheme = scheme
+ return scheme
+ last_revnum = self.transport.get_latest_revnum()
self.set_branching_scheme(
self._guess_scheme(last_revnum, self._hinted_branch_path),
- store=(last_revnum > 20))
+ store=(last_revnum > 20),
+ mandatory=False)
return self._scheme
def done(revision, date, author):
pass
editor = self.transport.get_commit_editor(
- "Updating branching scheme for Bazaar.",
+ {svn.core.SVN_PROP_REVISION_LOG: "Updating branching scheme for Bazaar."},
done, None, False)
root = editor.open_root(-1)
editor.change_dir_prop(root, SVN_PROP_BZR_BRANCHING_SCHEME,
mutter("Guessed branching scheme: %r" % scheme)
return scheme
- def set_branching_scheme(self, scheme, store=True):
+ def set_branching_scheme(self, scheme, store=True, mandatory=False):
self._scheme = scheme
if store:
- self.config.set_branching_scheme(str(scheme))
+ self.get_config().set_branching_scheme(str(scheme),
+ mandatory=mandatory)
def _warn_if_deprecated(self):
# This class isn't deprecated
cache_dir = create_cache_dir()
dir = os.path.join(cache_dir, self.uuid)
if not os.path.exists(dir):
+ info("Initialising Subversion metadata cache in %s" % dir)
os.mkdir(dir)
return dir
:return: Revision id of the left-hand-side parent or None if
this is the first revision
"""
- assert isinstance(path, basestring)
+ assert isinstance(path, str)
assert isinstance(revnum, int)
if not scheme.is_branch(path) and \
if revid is not None:
yield revid
+ def get_parents(self, revids):
+ parents_list = []
+ for revision_id in revids:
+ if revision_id == NULL_REVISION:
+ parents = []
+ else:
+ try:
+ parents = self.revision_parents(revision_id)
+ except NoSuchRevision:
+ parents = None
+ else:
+ if len(parents) == 0:
+ parents = [NULL_REVISION]
+ parents_list.append(parents)
+ return parents_list
+
def revision_parents(self, revision_id, bzr_merges=None, svk_merges=None):
"""See Repository.revision_parents()."""
parent_ids = []
def get_revision(self, revision_id):
"""See Repository.get_revision."""
- if not revision_id or not isinstance(revision_id, basestring):
+ if not revision_id or not isinstance(revision_id, str):
raise InvalidRevisionId(revision_id=revision_id, branch=self)
(path, revnum, _) = self.lookup_revision_id(revision_id)
parent_ids = self.revision_parents(revision_id)
# Commit SVN revision properties to a Revision object
- rev = Revision(revision_id=revision_id, parent_ids=parent_ids)
+ class LazySvnRevision(Revision):
+ inventory_sha1 = property(lambda rev: self.get_inventory_sha1(rev.revision_id))
+
+ rev = LazySvnRevision(revision_id=revision_id, parent_ids=parent_ids)
+
+ svn_revprops = self.transport.revprop_list(revnum)
- (rev.committer, rev.message, date) = self._log.get_revision_info(revnum)
- if rev.committer is None:
+ if svn_revprops.has_key(svn.core.SVN_PROP_REVISION_AUTHOR):
+ rev.committer = svn_revprops[svn.core.SVN_PROP_REVISION_AUTHOR]
+ else:
rev.committer = ""
- if date is not None:
- rev.timestamp = 1.0 * svn.core.secs_from_timestr(date, None)
+ rev.message = svn_revprops.get(svn.core.SVN_PROP_REVISION_LOG)
+
+ if rev.message:
+ try:
+ rev.message = rev.message.decode("utf-8")
+ except UnicodeDecodeError:
+ pass
+
+ if svn_revprops.has_key(svn.core.SVN_PROP_REVISION_DATE):
+ rev.timestamp = 1.0 * svn.core.secs_from_timestr(svn_revprops[svn.core.SVN_PROP_REVISION_DATE], None)
else:
rev.timestamp = 0.0 # FIXME: Obtain repository creation time
rev.timezone = None
self.branchprop_list.get_property(path, revnum,
SVN_PROP_BZR_REVISION_INFO, ""), rev)
- rev.inventory_sha1 = property(
- lambda: self.get_inventory_sha1(revision_id))
-
return rev
def get_revisions(self, revision_ids):
SVN_PROP_BZR_REVISION_ID+str(scheme)).strip("\n")
# Or generate it
if line == "":
- revid = generate_svn_revision_id(self.uuid, revnum, path,
- scheme)
+ revid = default_mapping.generate_revision_id(
+ self.uuid, revnum, path, scheme)
else:
try:
(bzr_revno, revid) = parse_revid_property(line)
scheme, bzr_revno)
except errors.InvalidPropertyValue, e:
mutter(str(e))
- revid = generate_svn_revision_id(self.uuid, revnum, path,
- scheme)
+ revid = default_mapping.generate_revision_id(self.uuid,
+ revnum, path, scheme)
self.revmap.insert_revid(revid, path, revnum, revnum,
scheme)
:return: Tuple with branch path, revision number and scheme.
"""
def get_scheme(name):
- assert isinstance(name, basestring)
+ assert isinstance(name, str)
return BranchingScheme.find_scheme(name)
# Try a simple parse
try:
- (uuid, branch_path, revnum, schemen) = parse_svn_revision_id(revid)
+ (uuid, branch_path, revnum, schemen) = default_mapping.parse_revision_id(revid)
assert isinstance(branch_path, str)
+ assert isinstance(schemen, str)
if uuid == self.uuid:
return (branch_path, revnum, get_scheme(schemen))
# If the UUID doesn't match, this may still be a valid revision
(branch_path, min_revnum, max_revnum, \
scheme) = self.revmap.lookup_revid(revid)
assert isinstance(branch_path, str)
+ assert isinstance(scheme, str)
# Entry already complete?
if min_revnum == max_revnum:
return (branch_path, min_revnum, get_scheme(scheme))
if scheme is None:
scheme = self.get_scheme()
last_revnum = self.transport.get_latest_revnum()
- if (self._revids_seen.has_key(str(scheme)) and
- last_revnum <= self._revids_seen[str(scheme)]):
+ if (last_revnum <= self.revmap.last_revnum_checked(str(scheme))):
# All revision ids in this repository for the current
# scheme have already been discovered. No need to
# check again.
raise e
found = False
- for (branch, revno, _) in self.find_branches(scheme, last_revnum):
+ for (branch, revno, _) in self.find_branchpaths(scheme,
+ self.revmap.last_revnum_checked(str(scheme)),
+ last_revnum):
+ assert isinstance(branch, str)
+ assert isinstance(revno, int)
# Look at their bzr:revision-id-vX
revids = []
- for line in self.branchprop_list.get_property(branch, revno,
- SVN_PROP_BZR_REVISION_ID+str(scheme), "").splitlines():
- try:
- revids.append(parse_revid_property(line))
- except errors.InvalidPropertyValue, ie:
- mutter(str(ie))
+ try:
+ for line in self.branchprop_list.get_property(branch, revno,
+ SVN_PROP_BZR_REVISION_ID+str(scheme), "").splitlines():
+ try:
+ revids.append(parse_revid_property(line))
+ except errors.InvalidPropertyValue, ie:
+ mutter(str(ie))
+ except SubversionException, (_, svn.core.SVN_ERR_FS_NOT_DIRECTORY):
+ continue
# If there are any new entries that are not yet in the cache,
# add them
found = True
self.revmap.insert_revid(entry_revid, branch, 0, revno,
str(scheme), entry_revno)
-
- if found:
- break
+ # We've added all the revision ids for this scheme in the repository,
+ # so no need to check again unless new revisions got added
+ self.revmap.set_last_revnum_checked(str(scheme), last_revnum)
if not found:
- # We've added all the revision ids for this scheme in the repository,
- # so no need to check again unless new revisions got added
- self._revids_seen[str(scheme)] = last_revnum
raise e
(branch_path, min_revnum, max_revnum, scheme) = self.revmap.lookup_revid(revid)
assert isinstance(branch_path, str)
# Find the branch property between min_revnum and max_revnum that
# added revid
for (bp, rev) in self.follow_branch(branch_path, max_revnum,
- get_scheme(scheme)):
+ get_scheme(str(scheme))):
try:
(entry_revno, entry_revid) = parse_revid_property(
self.branchprop_list.get_property_diff(bp, rev,
for (bp, paths, revnum) in self._log.follow_path(branch_path, revnum):
assert revnum > 0 or bp == ""
- assert scheme.is_branch(bp) or schee.is_tag(bp)
+ assert scheme.is_branch(bp) or scheme.is_tag(bp)
# Remove non-bp paths from paths
for p in paths.keys():
if not p.startswith(bp+"/") and bp != p and bp != "":
yield (bp, paths, revnum)
+ def get_config(self):
+ return SvnRepositoryConfig(self.uuid)
+
def has_signature_for_revision_id(self, revision_id):
"""Check whether a signature exists for a particular revision id.
return self._ancestry
- def find_branches(self, scheme, revnum=None):
- """Find all branches that were changed in the specified revision number.
+ def find_branches(self, using=False):
+ """Find branches underneath this repository.
+
+ This will include branches inside other branches.
+
+ :param using: If True, list only branches using this repository.
+ """
+ # All branches use this repository, so the using argument can be
+ # ignored.
+ scheme = self.get_scheme()
+
+ existing_branches = [bp for (bp, revnum, _) in
+ filter(lambda (bp, rev, exists): exists,
+ self.find_branchpaths(scheme))]
+
+ branches = []
+ for bp in existing_branches:
+ try:
+ branches.append(Branch.open(urlutils.join(self.base, bp)))
+ except NotBranchError: # Skip non-directories
+ pass
+ return branches
+
+ def find_branchpaths(self, scheme, from_revnum=0, to_revnum=None):
+ """Find all branch paths that were changed in the specified revision
+ range.
:param revnum: Revision to search for branches.
:return: iterator that returns tuples with (path, revision number, still exists). The revision number is the revision in which the branch last existed.
"""
assert scheme is not None
- if revnum is None:
- revnum = self.transport.get_latest_revnum()
+ if to_revnum is None:
+ to_revnum = self.transport.get_latest_revnum()
created_branches = {}
pb = ui.ui_factory.nested_progress_bar()
try:
- for i in range(revnum+1):
- pb.update("finding branches", i, revnum+1)
+ for i in range(from_revnum, to_revnum+1):
+ pb.update("finding branches", i, to_revnum+1)
paths = self._log.get_revision_paths(i)
for p in sorted(paths.keys()):
if scheme.is_branch(p) or scheme.is_tag(p):
- if paths[p][0] in ('R', 'D'):
+ if paths[p][0] in ('R', 'D') and p in created_branches:
del created_branches[p]
- j = self._log.find_latest_change(p, i-1,
- include_parents=True, include_children=True)
- ret.append((p, j, False))
+ if paths[p][1]:
+ prev_path = paths[p][1]
+ prev_rev = paths[p][2]
+ else:
+ prev_path = p
+ prev_rev = self._log.find_latest_change(p,
+ i-1, include_parents=True,
+ include_children=True)
+ assert isinstance(prev_rev, int)
+ ret.append((prev_path, prev_rev, False))
if paths[p][0] in ('A', 'R'):
created_branches[p] = i
if paths[p][0] in ('R', 'D'):
k = created_branches.keys()
for c in k:
- if c.startswith(p+"/"):
+ if c.startswith(p+"/") and c in created_branches:
del created_branches[c]
j = self._log.find_latest_change(c, i-1,
include_parents=True,
include_children=True)
+ assert isinstance(j, int)
ret.append((c, j, False))
if paths[p][0] in ('A', 'R'):
parents = [p]
while parents:
p = parents.pop()
- for c in self.transport.get_dir(p, i)[0].keys():
- n = p+"/"+c
- if scheme.is_branch(n) or scheme.is_tag(n):
- created_branches[n] = i
- elif (scheme.is_branch_parent(n) or
- scheme.is_tag_parent(n)):
- parents.append(n)
+ try:
+ for c in self.transport.get_dir(p, i)[0].keys():
+ n = p+"/"+c
+ if scheme.is_branch(n) or scheme.is_tag(n):
+ created_branches[n] = i
+ elif (scheme.is_branch_parent(n) or
+ scheme.is_tag_parent(n)):
+ parents.append(n)
+ except SubversionException, (_, svn.core.SVN_ERR_FS_NOT_DIRECTORY):
+ pass
finally:
pb.finished()
for p in created_branches:
- j = self._log.find_latest_change(p, revnum,
+ j = self._log.find_latest_change(p, to_revnum,
include_parents=True,
include_children=True)
if j is None:
j = created_branches[p]
+ assert isinstance(j, int)
ret.append((p, j, True))
return ret