hgext/convert/subversion.py
changeset 43076 2372284d9457
parent 42419 c004340dc687
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    50     import svn.core
    50     import svn.core
    51     import svn.ra
    51     import svn.ra
    52     import svn.delta
    52     import svn.delta
    53     from . import transport
    53     from . import transport
    54     import warnings
    54     import warnings
    55     warnings.filterwarnings('ignore',
    55 
    56             module='svn.core',
    56     warnings.filterwarnings(
    57             category=DeprecationWarning)
    57         'ignore', module='svn.core', category=DeprecationWarning
    58     svn.core.SubversionException # trigger import to catch error
    58     )
       
    59     svn.core.SubversionException  # trigger import to catch error
    59 
    60 
    60 except ImportError:
    61 except ImportError:
    61     svn = None
    62     svn = None
    62 
    63 
       
    64 
    63 class SvnPathNotFound(Exception):
    65 class SvnPathNotFound(Exception):
    64     pass
    66     pass
       
    67 
    65 
    68 
    66 def revsplit(rev):
    69 def revsplit(rev):
    67     """Parse a revision string and return (uuid, path, revnum).
    70     """Parse a revision string and return (uuid, path, revnum).
    68     >>> revsplit(b'svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
    71     >>> revsplit(b'svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
    69     ...          b'/proj%20B/mytrunk/mytrunk@1')
    72     ...          b'/proj%20B/mytrunk/mytrunk@1')
    87     if len(parts) > 1 and parts[0].startswith('svn:'):
    90     if len(parts) > 1 and parts[0].startswith('svn:'):
    88         uuid = parts[0][4:]
    91         uuid = parts[0][4:]
    89         mod = '/' + parts[1]
    92         mod = '/' + parts[1]
    90     return uuid, mod, revnum
    93     return uuid, mod, revnum
    91 
    94 
       
    95 
    92 def quote(s):
    96 def quote(s):
    93     # As of svn 1.7, many svn calls expect "canonical" paths. In
    97     # As of svn 1.7, many svn calls expect "canonical" paths. In
    94     # theory, we should call svn.core.*canonicalize() on all paths
    98     # theory, we should call svn.core.*canonicalize() on all paths
    95     # before passing them to the API.  Instead, we assume the base url
    99     # before passing them to the API.  Instead, we assume the base url
    96     # is canonical and copy the behaviour of svn URL encoding function
   100     # is canonical and copy the behaviour of svn URL encoding function
    97     # so we can extend it safely with new components. The "safe"
   101     # so we can extend it safely with new components. The "safe"
    98     # characters were taken from the "svn_uri__char_validity" table in
   102     # characters were taken from the "svn_uri__char_validity" table in
    99     # libsvn_subr/path.c.
   103     # libsvn_subr/path.c.
   100     return urlreq.quote(s, "!$&'()*+,-./:=@_~")
   104     return urlreq.quote(s, "!$&'()*+,-./:=@_~")
       
   105 
   101 
   106 
   102 def geturl(path):
   107 def geturl(path):
   103     try:
   108     try:
   104         return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
   109         return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
   105     except svn.core.SubversionException:
   110     except svn.core.SubversionException:
   113         # by svn API, which is UTF-8.
   118         # by svn API, which is UTF-8.
   114         path = encoding.tolocal(path)
   119         path = encoding.tolocal(path)
   115         path = 'file://%s' % quote(path)
   120         path = 'file://%s' % quote(path)
   116     return svn.core.svn_path_canonicalize(path)
   121     return svn.core.svn_path_canonicalize(path)
   117 
   122 
       
   123 
   118 def optrev(number):
   124 def optrev(number):
   119     optrev = svn.core.svn_opt_revision_t()
   125     optrev = svn.core.svn_opt_revision_t()
   120     optrev.kind = svn.core.svn_opt_revision_number
   126     optrev.kind = svn.core.svn_opt_revision_number
   121     optrev.value.number = number
   127     optrev.value.number = number
   122     return optrev
   128     return optrev
   123 
   129 
       
   130 
   124 class changedpath(object):
   131 class changedpath(object):
   125     def __init__(self, p):
   132     def __init__(self, p):
   126         self.copyfrom_path = p.copyfrom_path
   133         self.copyfrom_path = p.copyfrom_path
   127         self.copyfrom_rev = p.copyfrom_rev
   134         self.copyfrom_rev = p.copyfrom_rev
   128         self.action = p.action
   135         self.action = p.action
   129 
   136 
   130 def get_log_child(fp, url, paths, start, end, limit=0,
   137 
   131                   discover_changed_paths=True, strict_node_history=False):
   138 def get_log_child(
       
   139     fp,
       
   140     url,
       
   141     paths,
       
   142     start,
       
   143     end,
       
   144     limit=0,
       
   145     discover_changed_paths=True,
       
   146     strict_node_history=False,
       
   147 ):
   132     protocol = -1
   148     protocol = -1
       
   149 
   133     def receiver(orig_paths, revnum, author, date, message, pool):
   150     def receiver(orig_paths, revnum, author, date, message, pool):
   134         paths = {}
   151         paths = {}
   135         if orig_paths is not None:
   152         if orig_paths is not None:
   136             for k, v in orig_paths.iteritems():
   153             for k, v in orig_paths.iteritems():
   137                 paths[k] = changedpath(v)
   154                 paths[k] = changedpath(v)
   138         pickle.dump((paths, revnum, author, date, message),
   155         pickle.dump((paths, revnum, author, date, message), fp, protocol)
   139                     fp, protocol)
       
   140 
   156 
   141     try:
   157     try:
   142         # Use an ra of our own so that our parent can consume
   158         # Use an ra of our own so that our parent can consume
   143         # our results without confusing the server.
   159         # our results without confusing the server.
   144         t = transport.SvnRaTransport(url=url)
   160         t = transport.SvnRaTransport(url=url)
   145         svn.ra.get_log(t.ra, paths, start, end, limit,
   161         svn.ra.get_log(
   146                        discover_changed_paths,
   162             t.ra,
   147                        strict_node_history,
   163             paths,
   148                        receiver)
   164             start,
       
   165             end,
       
   166             limit,
       
   167             discover_changed_paths,
       
   168             strict_node_history,
       
   169             receiver,
       
   170         )
   149     except IOError:
   171     except IOError:
   150         # Caller may interrupt the iteration
   172         # Caller may interrupt the iteration
   151         pickle.dump(None, fp, protocol)
   173         pickle.dump(None, fp, protocol)
   152     except Exception as inst:
   174     except Exception as inst:
   153         pickle.dump(stringutil.forcebytestr(inst), fp, protocol)
   175         pickle.dump(stringutil.forcebytestr(inst), fp, protocol)
   157     # With large history, cleanup process goes crazy and suddenly
   179     # With large history, cleanup process goes crazy and suddenly
   158     # consumes *huge* amount of memory. The output file being closed,
   180     # consumes *huge* amount of memory. The output file being closed,
   159     # there is no need for clean termination.
   181     # there is no need for clean termination.
   160     os._exit(0)
   182     os._exit(0)
   161 
   183 
       
   184 
   162 def debugsvnlog(ui, **opts):
   185 def debugsvnlog(ui, **opts):
   163     """Fetch SVN log in a subprocess and channel them back to parent to
   186     """Fetch SVN log in a subprocess and channel them back to parent to
   164     avoid memory collection issues.
   187     avoid memory collection issues.
   165     """
   188     """
   166     if svn is None:
   189     if svn is None:
   167         raise error.Abort(_('debugsvnlog could not load Subversion python '
   190         raise error.Abort(
   168                            'bindings'))
   191             _('debugsvnlog could not load Subversion python ' 'bindings')
       
   192         )
   169 
   193 
   170     args = decodeargs(ui.fin.read())
   194     args = decodeargs(ui.fin.read())
   171     get_log_child(ui.fout, *args)
   195     get_log_child(ui.fout, *args)
   172 
   196 
       
   197 
   173 class logstream(object):
   198 class logstream(object):
   174     """Interruptible revision log iterator."""
   199     """Interruptible revision log iterator."""
       
   200 
   175     def __init__(self, stdout):
   201     def __init__(self, stdout):
   176         self._stdout = stdout
   202         self._stdout = stdout
   177 
   203 
   178     def __iter__(self):
   204     def __iter__(self):
   179         while True:
   205         while True:
   180             try:
   206             try:
   181                 entry = pickle.load(self._stdout)
   207                 entry = pickle.load(self._stdout)
   182             except EOFError:
   208             except EOFError:
   183                 raise error.Abort(_('Mercurial failed to run itself, check'
   209                 raise error.Abort(
   184                                    ' hg executable is in PATH'))
   210                     _(
       
   211                         'Mercurial failed to run itself, check'
       
   212                         ' hg executable is in PATH'
       
   213                     )
       
   214                 )
   185             try:
   215             try:
   186                 orig_paths, revnum, author, date, message = entry
   216                 orig_paths, revnum, author, date, message = entry
   187             except (TypeError, ValueError):
   217             except (TypeError, ValueError):
   188                 if entry is None:
   218                 if entry is None:
   189                     break
   219                     break
   193     def close(self):
   223     def close(self):
   194         if self._stdout:
   224         if self._stdout:
   195             self._stdout.close()
   225             self._stdout.close()
   196             self._stdout = None
   226             self._stdout = None
   197 
   227 
       
   228 
   198 class directlogstream(list):
   229 class directlogstream(list):
   199     """Direct revision log iterator.
   230     """Direct revision log iterator.
   200     This can be used for debugging and development but it will probably leak
   231     This can be used for debugging and development but it will probably leak
   201     memory and is not suitable for real conversions."""
   232     memory and is not suitable for real conversions."""
   202     def __init__(self, url, paths, start, end, limit=0,
   233 
   203                   discover_changed_paths=True, strict_node_history=False):
   234     def __init__(
   204 
   235         self,
       
   236         url,
       
   237         paths,
       
   238         start,
       
   239         end,
       
   240         limit=0,
       
   241         discover_changed_paths=True,
       
   242         strict_node_history=False,
       
   243     ):
   205         def receiver(orig_paths, revnum, author, date, message, pool):
   244         def receiver(orig_paths, revnum, author, date, message, pool):
   206             paths = {}
   245             paths = {}
   207             if orig_paths is not None:
   246             if orig_paths is not None:
   208                 for k, v in orig_paths.iteritems():
   247                 for k, v in orig_paths.iteritems():
   209                     paths[k] = changedpath(v)
   248                     paths[k] = changedpath(v)
   210             self.append((paths, revnum, author, date, message))
   249             self.append((paths, revnum, author, date, message))
   211 
   250 
   212         # Use an ra of our own so that our parent can consume
   251         # Use an ra of our own so that our parent can consume
   213         # our results without confusing the server.
   252         # our results without confusing the server.
   214         t = transport.SvnRaTransport(url=url)
   253         t = transport.SvnRaTransport(url=url)
   215         svn.ra.get_log(t.ra, paths, start, end, limit,
   254         svn.ra.get_log(
   216                        discover_changed_paths,
   255             t.ra,
   217                        strict_node_history,
   256             paths,
   218                        receiver)
   257             start,
       
   258             end,
       
   259             limit,
       
   260             discover_changed_paths,
       
   261             strict_node_history,
       
   262             receiver,
       
   263         )
   219 
   264 
   220     def close(self):
   265     def close(self):
   221         pass
   266         pass
       
   267 
   222 
   268 
   223 # Check to see if the given path is a local Subversion repo. Verify this by
   269 # Check to see if the given path is a local Subversion repo. Verify this by
   224 # looking for several svn-specific files and directories in the given
   270 # looking for several svn-specific files and directories in the given
   225 # directory.
   271 # directory.
   226 def filecheck(ui, path, proto):
   272 def filecheck(ui, path, proto):
   227     for x in ('locks', 'hooks', 'format', 'db'):
   273     for x in ('locks', 'hooks', 'format', 'db'):
   228         if not os.path.exists(os.path.join(path, x)):
   274         if not os.path.exists(os.path.join(path, x)):
   229             return False
   275             return False
   230     return True
   276     return True
       
   277 
   231 
   278 
   232 # Check to see if a given path is the root of an svn repo over http. We verify
   279 # Check to see if a given path is the root of an svn repo over http. We verify
   233 # this by requesting a version-controlled URL we know can't exist and looking
   280 # this by requesting a version-controlled URL we know can't exist and looking
   234 # for the svn-specific "not found" XML.
   281 # for the svn-specific "not found" XML.
   235 def httpcheck(ui, path, proto):
   282 def httpcheck(ui, path, proto):
   238         rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path), 'rb')
   285         rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path), 'rb')
   239         data = rsp.read()
   286         data = rsp.read()
   240     except urlerr.httperror as inst:
   287     except urlerr.httperror as inst:
   241         if inst.code != 404:
   288         if inst.code != 404:
   242             # Except for 404 we cannot know for sure this is not an svn repo
   289             # Except for 404 we cannot know for sure this is not an svn repo
   243             ui.warn(_('svn: cannot probe remote repository, assume it could '
   290             ui.warn(
   244                       'be a subversion repository. Use --source-type if you '
   291                 _(
   245                       'know better.\n'))
   292                     'svn: cannot probe remote repository, assume it could '
       
   293                     'be a subversion repository. Use --source-type if you '
       
   294                     'know better.\n'
       
   295                 )
       
   296             )
   246             return True
   297             return True
   247         data = inst.fp.read()
   298         data = inst.fp.read()
   248     except Exception:
   299     except Exception:
   249         # Could be urlerr.urlerror if the URL is invalid or anything else.
   300         # Could be urlerr.urlerror if the URL is invalid or anything else.
   250         return False
   301         return False
   251     return '<m:human-readable errcode="160013">' in data
   302     return '<m:human-readable errcode="160013">' in data
   252 
   303 
   253 protomap = {'http': httpcheck,
   304 
   254             'https': httpcheck,
   305 protomap = {
   255             'file': filecheck,
   306     'http': httpcheck,
   256             }
   307     'https': httpcheck,
       
   308     'file': filecheck,
       
   309 }
       
   310 
       
   311 
   257 def issvnurl(ui, url):
   312 def issvnurl(ui, url):
   258     try:
   313     try:
   259         proto, path = url.split('://', 1)
   314         proto, path = url.split('://', 1)
   260         if proto == 'file':
   315         if proto == 'file':
   261             if (pycompat.iswindows and path[:1] == '/'
   316             if (
   262                   and path[1:2].isalpha() and path[2:6].lower() == '%3a/'):
   317                 pycompat.iswindows
       
   318                 and path[:1] == '/'
       
   319                 and path[1:2].isalpha()
       
   320                 and path[2:6].lower() == '%3a/'
       
   321             ):
   263                 path = path[:2] + ':/' + path[6:]
   322                 path = path[:2] + ':/' + path[6:]
   264             path = urlreq.url2pathname(path)
   323             path = urlreq.url2pathname(path)
   265     except ValueError:
   324     except ValueError:
   266         proto = 'file'
   325         proto = 'file'
   267         path = os.path.abspath(url)
   326         path = os.path.abspath(url)
   271     while '/' in path:
   330     while '/' in path:
   272         if check(ui, path, proto):
   331         if check(ui, path, proto):
   273             return True
   332             return True
   274         path = path.rsplit('/', 1)[0]
   333         path = path.rsplit('/', 1)[0]
   275     return False
   334     return False
       
   335 
   276 
   336 
   277 # SVN conversion code stolen from bzr-svn and tailor
   337 # SVN conversion code stolen from bzr-svn and tailor
   278 #
   338 #
   279 # Subversion looks like a versioned filesystem, branches structures
   339 # Subversion looks like a versioned filesystem, branches structures
   280 # are defined by conventions and not enforced by the tool. First,
   340 # are defined by conventions and not enforced by the tool. First,
   290 #
   350 #
   291 class svn_source(converter_source):
   351 class svn_source(converter_source):
   292     def __init__(self, ui, repotype, url, revs=None):
   352     def __init__(self, ui, repotype, url, revs=None):
   293         super(svn_source, self).__init__(ui, repotype, url, revs=revs)
   353         super(svn_source, self).__init__(ui, repotype, url, revs=revs)
   294 
   354 
   295         if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
   355         if not (
   296                 (os.path.exists(url) and
   356             url.startswith('svn://')
   297                  os.path.exists(os.path.join(url, '.svn'))) or
   357             or url.startswith('svn+ssh://')
   298                 issvnurl(ui, url)):
   358             or (
   299             raise NoRepo(_("%s does not look like a Subversion repository")
   359                 os.path.exists(url)
   300                          % url)
   360                 and os.path.exists(os.path.join(url, '.svn'))
       
   361             )
       
   362             or issvnurl(ui, url)
       
   363         ):
       
   364             raise NoRepo(
       
   365                 _("%s does not look like a Subversion repository") % url
       
   366             )
   301         if svn is None:
   367         if svn is None:
   302             raise MissingTool(_('could not load Subversion python bindings'))
   368             raise MissingTool(_('could not load Subversion python bindings'))
   303 
   369 
   304         try:
   370         try:
   305             version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
   371             version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
   306             if version < (1, 4):
   372             if version < (1, 4):
   307                 raise MissingTool(_('Subversion python bindings %d.%d found, '
   373                 raise MissingTool(
   308                                     '1.4 or later required') % version)
   374                     _(
       
   375                         'Subversion python bindings %d.%d found, '
       
   376                         '1.4 or later required'
       
   377                     )
       
   378                     % version
       
   379                 )
   309         except AttributeError:
   380         except AttributeError:
   310             raise MissingTool(_('Subversion python bindings are too old, 1.4 '
   381             raise MissingTool(
   311                                 'or later required'))
   382                 _(
       
   383                     'Subversion python bindings are too old, 1.4 '
       
   384                     'or later required'
       
   385                 )
       
   386             )
   312 
   387 
   313         self.lastrevs = {}
   388         self.lastrevs = {}
   314 
   389 
   315         latest = None
   390         latest = None
   316         try:
   391         try:
   317             # Support file://path@rev syntax. Useful e.g. to convert
   392             # Support file://path@rev syntax. Useful e.g. to convert
   318             # deleted branches.
   393             # deleted branches.
   319             at = url.rfind('@')
   394             at = url.rfind('@')
   320             if at >= 0:
   395             if at >= 0:
   321                 latest = int(url[at + 1:])
   396                 latest = int(url[at + 1 :])
   322                 url = url[:at]
   397                 url = url[:at]
   323         except ValueError:
   398         except ValueError:
   324             pass
   399             pass
   325         self.url = geturl(url)
   400         self.url = geturl(url)
   326         self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
   401         self.encoding = 'UTF-8'  # Subversion is always nominal UTF-8
   327         try:
   402         try:
   328             self.transport = transport.SvnRaTransport(url=self.url)
   403             self.transport = transport.SvnRaTransport(url=self.url)
   329             self.ra = self.transport.ra
   404             self.ra = self.transport.ra
   330             self.ctx = self.transport.client
   405             self.ctx = self.transport.client
   331             self.baseurl = svn.ra.get_repos_root(self.ra)
   406             self.baseurl = svn.ra.get_repos_root(self.ra)
   332             # Module is either empty or a repository path starting with
   407             # Module is either empty or a repository path starting with
   333             # a slash and not ending with a slash.
   408             # a slash and not ending with a slash.
   334             self.module = urlreq.unquote(self.url[len(self.baseurl):])
   409             self.module = urlreq.unquote(self.url[len(self.baseurl) :])
   335             self.prevmodule = None
   410             self.prevmodule = None
   336             self.rootmodule = self.module
   411             self.rootmodule = self.module
   337             self.commits = {}
   412             self.commits = {}
   338             self.paths = {}
   413             self.paths = {}
   339             self.uuid = svn.ra.get_uuid(self.ra)
   414             self.uuid = svn.ra.get_uuid(self.ra)
   340         except svn.core.SubversionException:
   415         except svn.core.SubversionException:
   341             ui.traceback()
   416             ui.traceback()
   342             svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
   417             svnversion = '%d.%d.%d' % (
   343                                        svn.core.SVN_VER_MINOR,
   418                 svn.core.SVN_VER_MAJOR,
   344                                        svn.core.SVN_VER_MICRO)
   419                 svn.core.SVN_VER_MINOR,
   345             raise NoRepo(_("%s does not look like a Subversion repository "
   420                 svn.core.SVN_VER_MICRO,
   346                            "to libsvn version %s")
   421             )
   347                          % (self.url, svnversion))
   422             raise NoRepo(
       
   423                 _(
       
   424                     "%s does not look like a Subversion repository "
       
   425                     "to libsvn version %s"
       
   426                 )
       
   427                 % (self.url, svnversion)
       
   428             )
   348 
   429 
   349         if revs:
   430         if revs:
   350             if len(revs) > 1:
   431             if len(revs) > 1:
   351                 raise error.Abort(_('subversion source does not support '
   432                 raise error.Abort(
   352                                    'specifying multiple revisions'))
   433                     _(
       
   434                         'subversion source does not support '
       
   435                         'specifying multiple revisions'
       
   436                     )
       
   437                 )
   353             try:
   438             try:
   354                 latest = int(revs[0])
   439                 latest = int(revs[0])
   355             except ValueError:
   440             except ValueError:
   356                 raise error.Abort(_('svn: revision %s is not an integer') %
   441                 raise error.Abort(
   357                                  revs[0])
   442                     _('svn: revision %s is not an integer') % revs[0]
       
   443                 )
   358 
   444 
   359         trunkcfg = self.ui.config('convert', 'svn.trunk')
   445         trunkcfg = self.ui.config('convert', 'svn.trunk')
   360         if trunkcfg is None:
   446         if trunkcfg is None:
   361             trunkcfg = 'trunk'
   447             trunkcfg = 'trunk'
   362         self.trunkname = trunkcfg.strip('/')
   448         self.trunkname = trunkcfg.strip('/')
   364         try:
   450         try:
   365             self.startrev = int(self.startrev)
   451             self.startrev = int(self.startrev)
   366             if self.startrev < 0:
   452             if self.startrev < 0:
   367                 self.startrev = 0
   453                 self.startrev = 0
   368         except ValueError:
   454         except ValueError:
   369             raise error.Abort(_('svn: start revision %s is not an integer')
   455             raise error.Abort(
   370                              % self.startrev)
   456                 _('svn: start revision %s is not an integer') % self.startrev
       
   457             )
   371 
   458 
   372         try:
   459         try:
   373             self.head = self.latest(self.module, latest)
   460             self.head = self.latest(self.module, latest)
   374         except SvnPathNotFound:
   461         except SvnPathNotFound:
   375             self.head = None
   462             self.head = None
   376         if not self.head:
   463         if not self.head:
   377             raise error.Abort(_('no revision found in module %s')
   464             raise error.Abort(_('no revision found in module %s') % self.module)
   378                              % self.module)
       
   379         self.last_changed = self.revnum(self.head)
   465         self.last_changed = self.revnum(self.head)
   380 
   466 
   381         self._changescache = (None, None)
   467         self._changescache = (None, None)
   382 
   468 
   383         if os.path.exists(os.path.join(url, '.svn/entries')):
   469         if os.path.exists(os.path.join(url, '.svn/entries')):
   395                 lastrevs[module] = revnum
   481                 lastrevs[module] = revnum
   396         self.lastrevs = lastrevs
   482         self.lastrevs = lastrevs
   397 
   483 
   398     def exists(self, path, optrev):
   484     def exists(self, path, optrev):
   399         try:
   485         try:
   400             svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
   486             svn.client.ls(
   401                                  optrev, False, self.ctx)
   487                 self.url.rstrip('/') + '/' + quote(path),
       
   488                 optrev,
       
   489                 False,
       
   490                 self.ctx,
       
   491             )
   402             return True
   492             return True
   403         except svn.core.SubversionException:
   493         except svn.core.SubversionException:
   404             return False
   494             return False
   405 
   495 
   406     def getheads(self):
   496     def getheads(self):
   407 
       
   408         def isdir(path, revnum):
   497         def isdir(path, revnum):
   409             kind = self._checkpath(path, revnum)
   498             kind = self._checkpath(path, revnum)
   410             return kind == svn.core.svn_node_dir
   499             return kind == svn.core.svn_node_dir
   411 
   500 
   412         def getcfgpath(name, rev):
   501         def getcfgpath(name, rev):
   417             if not self.exists(path, rev):
   506             if not self.exists(path, rev):
   418                 if self.module.endswith(path) and name == 'trunk':
   507                 if self.module.endswith(path) and name == 'trunk':
   419                     # we are converting from inside this directory
   508                     # we are converting from inside this directory
   420                     return None
   509                     return None
   421                 if cfgpath:
   510                 if cfgpath:
   422                     raise error.Abort(_('expected %s to be at %r, but not found'
   511                     raise error.Abort(
   423                                        ) % (name, path))
   512                         _('expected %s to be at %r, but not found')
       
   513                         % (name, path)
       
   514                     )
   424                 return None
   515                 return None
   425             self.ui.note(_('found %s at %r\n') % (name, path))
   516             self.ui.note(_('found %s at %r\n') % (name, path))
   426             return path
   517             return path
   427 
   518 
   428         rev = optrev(self.last_changed)
   519         rev = optrev(self.last_changed)
   436         if trunk:
   527         if trunk:
   437             oldmodule = self.module or ''
   528             oldmodule = self.module or ''
   438             self.module += '/' + trunk
   529             self.module += '/' + trunk
   439             self.head = self.latest(self.module, self.last_changed)
   530             self.head = self.latest(self.module, self.last_changed)
   440             if not self.head:
   531             if not self.head:
   441                 raise error.Abort(_('no revision found in module %s')
   532                 raise error.Abort(
   442                                  % self.module)
   533                     _('no revision found in module %s') % self.module
       
   534                 )
   443 
   535 
   444         # First head in the list is the module's head
   536         # First head in the list is the module's head
   445         self.heads = [self.head]
   537         self.heads = [self.head]
   446         if self.tags is not None:
   538         if self.tags is not None:
   447             self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
   539             self.tags = '%s/%s' % (oldmodule, (self.tags or 'tags'))
   448 
   540 
   449         # Check if branches bring a few more heads to the list
   541         # Check if branches bring a few more heads to the list
   450         if branches:
   542         if branches:
   451             rpath = self.url.strip('/')
   543             rpath = self.url.strip('/')
   452             branchnames = svn.client.ls(rpath + '/' + quote(branches),
   544             branchnames = svn.client.ls(
   453                                         rev, False, self.ctx)
   545                 rpath + '/' + quote(branches), rev, False, self.ctx
       
   546             )
   454             for branch in sorted(branchnames):
   547             for branch in sorted(branchnames):
   455                 module = '%s/%s/%s' % (oldmodule, branches, branch)
   548                 module = '%s/%s/%s' % (oldmodule, branches, branch)
   456                 if not isdir(module, self.last_changed):
   549                 if not isdir(module, self.last_changed):
   457                     continue
   550                     continue
   458                 brevid = self.latest(module, self.last_changed)
   551                 brevid = self.latest(module, self.last_changed)
   459                 if not brevid:
   552                 if not brevid:
   460                     self.ui.note(_('ignoring empty branch %s\n') % branch)
   553                     self.ui.note(_('ignoring empty branch %s\n') % branch)
   461                     continue
   554                     continue
   462                 self.ui.note(_('found branch %s at %d\n') %
   555                 self.ui.note(
   463                              (branch, self.revnum(brevid)))
   556                     _('found branch %s at %d\n') % (branch, self.revnum(brevid))
       
   557                 )
   464                 self.heads.append(brevid)
   558                 self.heads.append(brevid)
   465 
   559 
   466         if self.startrev and self.heads:
   560         if self.startrev and self.heads:
   467             if len(self.heads) > 1:
   561             if len(self.heads) > 1:
   468                 raise error.Abort(_('svn: start revision is not supported '
   562                 raise error.Abort(
   469                                    'with more than one branch'))
   563                     _(
       
   564                         'svn: start revision is not supported '
       
   565                         'with more than one branch'
       
   566                     )
       
   567                 )
   470             revnum = self.revnum(self.heads[0])
   568             revnum = self.revnum(self.heads[0])
   471             if revnum < self.startrev:
   569             if revnum < self.startrev:
   472                 raise error.Abort(
   570                 raise error.Abort(
   473                     _('svn: no revision found after start revision %d')
   571                     _('svn: no revision found after start revision %d')
   474                                  % self.startrev)
   572                     % self.startrev
       
   573                 )
   475 
   574 
   476         return self.heads
   575         return self.heads
   477 
   576 
   478     def _getchanges(self, rev, full):
   577     def _getchanges(self, rev, full):
   479         (paths, parents) = self.paths[rev]
   578         (paths, parents) = self.paths[rev]
   481         if parents:
   580         if parents:
   482             files, self.removed, copies = self.expandpaths(rev, paths, parents)
   581             files, self.removed, copies = self.expandpaths(rev, paths, parents)
   483         if full or not parents:
   582         if full or not parents:
   484             # Perform a full checkout on roots
   583             # Perform a full checkout on roots
   485             uuid, module, revnum = revsplit(rev)
   584             uuid, module, revnum = revsplit(rev)
   486             entries = svn.client.ls(self.baseurl + quote(module),
   585             entries = svn.client.ls(
   487                                     optrev(revnum), True, self.ctx)
   586                 self.baseurl + quote(module), optrev(revnum), True, self.ctx
   488             files = [n for n, e in entries.iteritems()
   587             )
   489                      if e.kind == svn.core.svn_node_file]
   588             files = [
       
   589                 n
       
   590                 for n, e in entries.iteritems()
       
   591                 if e.kind == svn.core.svn_node_file
       
   592             ]
   490             self.removed = set()
   593             self.removed = set()
   491 
   594 
   492         files.sort()
   595         files.sort()
   493         files = zip(files, [rev] * len(files))
   596         files = zip(files, [rev] * len(files))
   494         return (files, copies)
   597         return (files, copies)
   531         del self.commits[rev]
   634         del self.commits[rev]
   532         return revcommit
   635         return revcommit
   533 
   636 
   534     def checkrevformat(self, revstr, mapname='splicemap'):
   637     def checkrevformat(self, revstr, mapname='splicemap'):
   535         """ fails if revision format does not match the correct format"""
   638         """ fails if revision format does not match the correct format"""
   536         if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
   639         if not re.match(
   537                               r'[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
   640             r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
   538                               r'{12,12}(.*)\@[0-9]+$',revstr):
   641             r'[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
   539             raise error.Abort(_('%s entry %s is not a valid revision'
   642             r'{12,12}(.*)\@[0-9]+$',
   540                                ' identifier') % (mapname, revstr))
   643             revstr,
       
   644         ):
       
   645             raise error.Abort(
       
   646                 _('%s entry %s is not a valid revision' ' identifier')
       
   647                 % (mapname, revstr)
       
   648             )
   541 
   649 
   542     def numcommits(self):
   650     def numcommits(self):
   543         return int(self.head.rsplit('@', 1)[1]) - self.startrev
   651         return int(self.head.rsplit('@', 1)[1]) - self.startrev
   544 
   652 
   545     def gettags(self):
   653     def gettags(self):
   565         try:
   673         try:
   566             for entry in stream:
   674             for entry in stream:
   567                 origpaths, revnum, author, date, message = entry
   675                 origpaths, revnum, author, date, message = entry
   568                 if not origpaths:
   676                 if not origpaths:
   569                     origpaths = []
   677                     origpaths = []
   570                 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
   678                 copies = [
   571                           in origpaths.iteritems() if e.copyfrom_path]
   679                     (e.copyfrom_path, e.copyfrom_rev, p)
       
   680                     for p, e in origpaths.iteritems()
       
   681                     if e.copyfrom_path
       
   682                 ]
   572                 # Apply moves/copies from more specific to general
   683                 # Apply moves/copies from more specific to general
   573                 copies.sort(reverse=True)
   684                 copies.sort(reverse=True)
   574 
   685 
   575                 srctagspath = tagspath
   686                 srctagspath = tagspath
   576                 if copies and copies[-1][2] == tagspath:
   687                 if copies and copies[-1][2] == tagspath:
   580                 for source, sourcerev, dest in copies:
   691                 for source, sourcerev, dest in copies:
   581                     if not dest.startswith(tagspath + '/'):
   692                     if not dest.startswith(tagspath + '/'):
   582                         continue
   693                         continue
   583                     for tag in pendings:
   694                     for tag in pendings:
   584                         if tag[0].startswith(dest):
   695                         if tag[0].startswith(dest):
   585                             tagpath = source + tag[0][len(dest):]
   696                             tagpath = source + tag[0][len(dest) :]
   586                             tag[:2] = [tagpath, sourcerev]
   697                             tag[:2] = [tagpath, sourcerev]
   587                             break
   698                             break
   588                     else:
   699                     else:
   589                         pendings.append([source, sourcerev, dest])
   700                         pendings.append([source, sourcerev, dest])
   590 
   701 
   593                 # /tags/tag.1 (from /trunk:10)
   704                 # /tags/tag.1 (from /trunk:10)
   594                 # /tags/tag.1/foo (from /branches/foo:12)
   705                 # /tags/tag.1/foo (from /branches/foo:12)
   595                 # Here/tags/tag.1 discarded as well as its children.
   706                 # Here/tags/tag.1 discarded as well as its children.
   596                 # It happens with tools like cvs2svn. Such tags cannot
   707                 # It happens with tools like cvs2svn. Such tags cannot
   597                 # be represented in mercurial.
   708                 # be represented in mercurial.
   598                 addeds = dict((p, e.copyfrom_path) for p, e
   709                 addeds = dict(
   599                               in origpaths.iteritems()
   710                     (p, e.copyfrom_path)
   600                               if e.action == 'A' and e.copyfrom_path)
   711                     for p, e in origpaths.iteritems()
       
   712                     if e.action == 'A' and e.copyfrom_path
       
   713                 )
   601                 badroots = set()
   714                 badroots = set()
   602                 for destroot in addeds:
   715                 for destroot in addeds:
   603                     for source, sourcerev, dest in pendings:
   716                     for source, sourcerev, dest in pendings:
   604                         if (not dest.startswith(destroot + '/')
   717                         if not dest.startswith(
   605                             or source.startswith(addeds[destroot] + '/')):
   718                             destroot + '/'
       
   719                         ) or source.startswith(addeds[destroot] + '/'):
   606                             continue
   720                             continue
   607                         badroots.add(destroot)
   721                         badroots.add(destroot)
   608                         break
   722                         break
   609 
   723 
   610                 for badroot in badroots:
   724                 for badroot in badroots:
   611                     pendings = [p for p in pendings if p[2] != badroot
   725                     pendings = [
   612                                 and not p[2].startswith(badroot + '/')]
   726                         p
       
   727                         for p in pendings
       
   728                         if p[2] != badroot
       
   729                         and not p[2].startswith(badroot + '/')
       
   730                     ]
   613 
   731 
   614                 # Tell tag renamings from tag creations
   732                 # Tell tag renamings from tag creations
   615                 renamings = []
   733                 renamings = []
   616                 for source, sourcerev, dest in pendings:
   734                 for source, sourcerev, dest in pendings:
   617                     tagname = dest.split('/')[-1]
   735                     tagname = dest.split('/')[-1]
   640 
   758 
   641     def converted(self, rev, destrev):
   759     def converted(self, rev, destrev):
   642         if not self.wc:
   760         if not self.wc:
   643             return
   761             return
   644         if self.convertfp is None:
   762         if self.convertfp is None:
   645             self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
   763             self.convertfp = open(
   646                                   'ab')
   764                 os.path.join(self.wc, '.svn', 'hg-shamap'), 'ab'
   647         self.convertfp.write(util.tonativeeol('%s %d\n'
   765             )
   648                                               % (destrev, self.revnum(rev))))
   766         self.convertfp.write(
       
   767             util.tonativeeol('%s %d\n' % (destrev, self.revnum(rev)))
       
   768         )
   649         self.convertfp.flush()
   769         self.convertfp.flush()
   650 
   770 
   651     def revid(self, revnum, module=None):
   771     def revid(self, revnum, module=None):
   652         return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
   772         return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
   653 
   773 
   660         revision. It may return a revision in a different module,
   780         revision. It may return a revision in a different module,
   661         since a branch may be moved without a change being
   781         since a branch may be moved without a change being
   662         reported. Return None if computed module does not belong to
   782         reported. Return None if computed module does not belong to
   663         rootmodule subtree.
   783         rootmodule subtree.
   664         """
   784         """
       
   785 
   665         def findchanges(path, start, stop=None):
   786         def findchanges(path, start, stop=None):
   666             stream = self._getlog([path], start, stop or 1)
   787             stream = self._getlog([path], start, stop or 1)
   667             try:
   788             try:
   668                 for entry in stream:
   789                 for entry in stream:
   669                     paths, revnum, author, date, message = entry
   790                     paths, revnum, author, date, message = entry
   673                         break
   794                         break
   674                     if revnum <= stop:
   795                     if revnum <= stop:
   675                         break
   796                         break
   676 
   797 
   677                     for p in paths:
   798                     for p in paths:
   678                         if (not path.startswith(p) or
   799                         if not path.startswith(p) or not paths[p].copyfrom_path:
   679                             not paths[p].copyfrom_path):
       
   680                             continue
   800                             continue
   681                         newpath = paths[p].copyfrom_path + path[len(p):]
   801                         newpath = paths[p].copyfrom_path + path[len(p) :]
   682                         self.ui.debug("branch renamed from %s to %s at %d\n" %
   802                         self.ui.debug(
   683                                       (path, newpath, revnum))
   803                             "branch renamed from %s to %s at %d\n"
       
   804                             % (path, newpath, revnum)
       
   805                         )
   684                         path = newpath
   806                         path = newpath
   685                         break
   807                         break
   686                 if not paths:
   808                 if not paths:
   687                     revnum = None
   809                     revnum = None
   688                 return revnum, path
   810                 return revnum, path
   701             dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
   823             dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
   702             self.reparent(prevmodule)
   824             self.reparent(prevmodule)
   703         except svn.core.SubversionException:
   825         except svn.core.SubversionException:
   704             dirent = None
   826             dirent = None
   705         if not dirent:
   827         if not dirent:
   706             raise SvnPathNotFound(_('%s not found up to revision %d')
   828             raise SvnPathNotFound(
   707                                   % (path, stop))
   829                 _('%s not found up to revision %d') % (path, stop)
       
   830             )
   708 
   831 
   709         # stat() gives us the previous revision on this line of
   832         # stat() gives us the previous revision on this line of
   710         # development, but it might be in *another module*. Fetch the
   833         # development, but it might be in *another module*. Fetch the
   711         # log and detect renames down to the latest revision.
   834         # log and detect renames down to the latest revision.
   712         revnum, realpath = findchanges(path, stop, dirent.created_rev)
   835         revnum, realpath = findchanges(path, stop, dirent.created_rev)
   748         new_module, revnum = revsplit(rev)[1:]
   871         new_module, revnum = revsplit(rev)[1:]
   749         if new_module != self.module:
   872         if new_module != self.module:
   750             self.module = new_module
   873             self.module = new_module
   751             self.reparent(self.module)
   874             self.reparent(self.module)
   752 
   875 
   753         progress = self.ui.makeprogress(_('scanning paths'), unit=_('paths'),
   876         progress = self.ui.makeprogress(
   754                                         total=len(paths))
   877             _('scanning paths'), unit=_('paths'), total=len(paths)
       
   878         )
   755         for i, (path, ent) in enumerate(paths):
   879         for i, (path, ent) in enumerate(paths):
   756             progress.update(i, item=path)
   880             progress.update(i, item=path)
   757             entrypath = self.getrelpath(path)
   881             entrypath = self.getrelpath(path)
   758 
   882 
   759             kind = self._checkpath(entrypath, revnum)
   883             kind = self._checkpath(entrypath, revnum)
   767                 if ent.copyfrom_rev < prevnum:
   891                 if ent.copyfrom_rev < prevnum:
   768                     continue
   892                     continue
   769                 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
   893                 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
   770                 if not copyfrom_path:
   894                 if not copyfrom_path:
   771                     continue
   895                     continue
   772                 self.ui.debug("copied to %s from %s@%s\n" %
   896                 self.ui.debug(
   773                               (entrypath, copyfrom_path, ent.copyfrom_rev))
   897                     "copied to %s from %s@%s\n"
       
   898                     % (entrypath, copyfrom_path, ent.copyfrom_rev)
       
   899                 )
   774                 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
   900                 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
   775             elif kind == 0: # gone, but had better be a deleted *file*
   901             elif kind == 0:  # gone, but had better be a deleted *file*
   776                 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
   902                 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
   777                 pmodule, prevnum = revsplit(parents[0])[1:]
   903                 pmodule, prevnum = revsplit(parents[0])[1:]
   778                 parentpath = pmodule + "/" + entrypath
   904                 parentpath = pmodule + "/" + entrypath
   779                 fromkind = self._checkpath(entrypath, prevnum, pmodule)
   905                 fromkind = self._checkpath(entrypath, prevnum, pmodule)
   780 
   906 
   788                         childpath = childpath.replace(oroot, nroot)
   914                         childpath = childpath.replace(oroot, nroot)
   789                         childpath = self.getrelpath("/" + childpath, pmodule)
   915                         childpath = self.getrelpath("/" + childpath, pmodule)
   790                         if childpath:
   916                         if childpath:
   791                             removed.add(self.recode(childpath))
   917                             removed.add(self.recode(childpath))
   792                 else:
   918                 else:
   793                     self.ui.debug('unknown path in revision %d: %s\n' %
   919                     self.ui.debug(
   794                                   (revnum, path))
   920                         'unknown path in revision %d: %s\n' % (revnum, path)
       
   921                     )
   795             elif kind == svn.core.svn_node_dir:
   922             elif kind == svn.core.svn_node_dir:
   796                 if ent.action == 'M':
   923                 if ent.action == 'M':
   797                     # If the directory just had a prop change,
   924                     # If the directory just had a prop change,
   798                     # then we shouldn't need to look for its children.
   925                     # then we shouldn't need to look for its children.
   799                     continue
   926                     continue
   826                 if ent.copyfrom_rev < prevnum:
   953                 if ent.copyfrom_rev < prevnum:
   827                     continue
   954                     continue
   828                 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
   955                 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
   829                 if not copyfrompath:
   956                 if not copyfrompath:
   830                     continue
   957                     continue
   831                 self.ui.debug("mark %s came from %s:%d\n"
   958                 self.ui.debug(
   832                               % (path, copyfrompath, ent.copyfrom_rev))
   959                     "mark %s came from %s:%d\n"
       
   960                     % (path, copyfrompath, ent.copyfrom_rev)
       
   961                 )
   833                 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
   962                 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
   834                 for childpath in children:
   963                 for childpath in children:
   835                     childpath = self.getrelpath("/" + childpath, pmodule)
   964                     childpath = self.getrelpath("/" + childpath, pmodule)
   836                     if not childpath:
   965                     if not childpath:
   837                         continue
   966                         continue
   838                     copytopath = path + childpath[len(copyfrompath):]
   967                     copytopath = path + childpath[len(copyfrompath) :]
   839                     copytopath = self.getrelpath(copytopath)
   968                     copytopath = self.getrelpath(copytopath)
   840                     copies[self.recode(copytopath)] = self.recode(childpath)
   969                     copies[self.recode(copytopath)] = self.recode(childpath)
   841 
   970 
   842         progress.complete()
   971         progress.complete()
   843         changed.update(removed)
   972         changed.update(removed)
   851 
   980 
   852         def parselogentry(orig_paths, revnum, author, date, message):
   981         def parselogentry(orig_paths, revnum, author, date, message):
   853             """Return the parsed commit object or None, and True if
   982             """Return the parsed commit object or None, and True if
   854             the revision is a branch root.
   983             the revision is a branch root.
   855             """
   984             """
   856             self.ui.debug("parsing revision %d (%d changes)\n" %
   985             self.ui.debug(
   857                           (revnum, len(orig_paths)))
   986                 "parsing revision %d (%d changes)\n" % (revnum, len(orig_paths))
       
   987             )
   858 
   988 
   859             branched = False
   989             branched = False
   860             rev = self.revid(revnum)
   990             rev = self.revid(revnum)
   861             # branch log might return entries for a parent we already have
   991             # branch log might return entries for a parent we already have
   862 
   992 
   865 
   995 
   866             parents = []
   996             parents = []
   867             # check whether this revision is the start of a branch or part
   997             # check whether this revision is the start of a branch or part
   868             # of a branch renaming
   998             # of a branch renaming
   869             orig_paths = sorted(orig_paths.iteritems())
   999             orig_paths = sorted(orig_paths.iteritems())
   870             root_paths = [(p, e) for p, e in orig_paths
  1000             root_paths = [
   871                           if self.module.startswith(p)]
  1001                 (p, e) for p, e in orig_paths if self.module.startswith(p)
       
  1002             ]
   872             if root_paths:
  1003             if root_paths:
   873                 path, ent = root_paths[-1]
  1004                 path, ent = root_paths[-1]
   874                 if ent.copyfrom_path:
  1005                 if ent.copyfrom_path:
   875                     branched = True
  1006                     branched = True
   876                     newpath = ent.copyfrom_path + self.module[len(path):]
  1007                     newpath = ent.copyfrom_path + self.module[len(path) :]
   877                     # ent.copyfrom_rev may not be the actual last revision
  1008                     # ent.copyfrom_rev may not be the actual last revision
   878                     previd = self.latest(newpath, ent.copyfrom_rev)
  1009                     previd = self.latest(newpath, ent.copyfrom_rev)
   879                     if previd is not None:
  1010                     if previd is not None:
   880                         prevmodule, prevnum = revsplit(previd)[1:]
  1011                         prevmodule, prevnum = revsplit(previd)[1:]
   881                         if prevnum >= self.startrev:
  1012                         if prevnum >= self.startrev:
   882                             parents = [previd]
  1013                             parents = [previd]
   883                             self.ui.note(
  1014                             self.ui.note(
   884                                 _('found parent of branch %s at %d: %s\n') %
  1015                                 _('found parent of branch %s at %d: %s\n')
   885                                 (self.module, prevnum, prevmodule))
  1016                                 % (self.module, prevnum, prevmodule)
       
  1017                             )
   886                 else:
  1018                 else:
   887                     self.ui.debug("no copyfrom path, don't know what to do.\n")
  1019                     self.ui.debug("no copyfrom path, don't know what to do.\n")
   888 
  1020 
   889             paths = []
  1021             paths = []
   890             # filter out unrelated paths
  1022             # filter out unrelated paths
   915                 if branch == self.trunkname:
  1047                 if branch == self.trunkname:
   916                     branch = None
  1048                     branch = None
   917             except IndexError:
  1049             except IndexError:
   918                 branch = None
  1050                 branch = None
   919 
  1051 
   920             cset = commit(author=author,
  1052             cset = commit(
   921                           date=dateutil.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
  1053                 author=author,
   922                           desc=log,
  1054                 date=dateutil.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
   923                           parents=parents,
  1055                 desc=log,
   924                           branch=branch,
  1056                 parents=parents,
   925                           rev=rev)
  1057                 branch=branch,
       
  1058                 rev=rev,
       
  1059             )
   926 
  1060 
   927             self.commits[rev] = cset
  1061             self.commits[rev] = cset
   928             # The parents list is *shared* among self.paths and the
  1062             # The parents list is *shared* among self.paths and the
   929             # commit object. Both will be updated below.
  1063             # commit object. Both will be updated below.
   930             self.paths[rev] = (paths, cset.parents)
  1064             self.paths[rev] = (paths, cset.parents)
   931             if self.child_cset and not self.child_cset.parents:
  1065             if self.child_cset and not self.child_cset.parents:
   932                 self.child_cset.parents[:] = [rev]
  1066                 self.child_cset.parents[:] = [rev]
   933             self.child_cset = cset
  1067             self.child_cset = cset
   934             return cset, branched
  1068             return cset, branched
   935 
  1069 
   936         self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
  1070         self.ui.note(
   937                      (self.module, from_revnum, to_revnum))
  1071             _('fetching revision log for "%s" from %d to %d\n')
       
  1072             % (self.module, from_revnum, to_revnum)
       
  1073         )
   938 
  1074 
   939         try:
  1075         try:
   940             firstcset = None
  1076             firstcset = None
   941             lastonbranch = False
  1077             lastonbranch = False
   942             stream = self._getlog([self.module], from_revnum, to_revnum)
  1078             stream = self._getlog([self.module], from_revnum, to_revnum)
   950                         self.ui.debug('revision %d has no entries\n' % revnum)
  1086                         self.ui.debug('revision %d has no entries\n' % revnum)
   951                         # If we ever leave the loop on an empty
  1087                         # If we ever leave the loop on an empty
   952                         # revision, do not try to get a parent branch
  1088                         # revision, do not try to get a parent branch
   953                         lastonbranch = lastonbranch or revnum == 0
  1089                         lastonbranch = lastonbranch or revnum == 0
   954                         continue
  1090                         continue
   955                     cset, lastonbranch = parselogentry(paths, revnum, author,
  1091                     cset, lastonbranch = parselogentry(
   956                                                        date, message)
  1092                         paths, revnum, author, date, message
       
  1093                     )
   957                     if cset:
  1094                     if cset:
   958                         firstcset = cset
  1095                         firstcset = cset
   959                     if lastonbranch:
  1096                     if lastonbranch:
   960                         break
  1097                         break
   961             finally:
  1098             finally:
   974                 except SvnPathNotFound:
  1111                 except SvnPathNotFound:
   975                     pass
  1112                     pass
   976         except svn.core.SubversionException as xxx_todo_changeme:
  1113         except svn.core.SubversionException as xxx_todo_changeme:
   977             (inst, num) = xxx_todo_changeme.args
  1114             (inst, num) = xxx_todo_changeme.args
   978             if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
  1115             if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
   979                 raise error.Abort(_('svn: branch has no revision %s')
  1116                 raise error.Abort(
   980                                  % to_revnum)
  1117                     _('svn: branch has no revision %s') % to_revnum
       
  1118                 )
   981             raise
  1119             raise
   982 
  1120 
   983     def getfile(self, file, rev):
  1121     def getfile(self, file, rev):
   984         # TODO: ra.get_file transmits the whole file instead of diffs.
  1122         # TODO: ra.get_file transmits the whole file instead of diffs.
   985         if file in self.removed:
  1123         if file in self.removed:
   998             if isinstance(info, list):
  1136             if isinstance(info, list):
   999                 info = info[-1]
  1137                 info = info[-1]
  1000             mode = ("svn:executable" in info) and 'x' or ''
  1138             mode = ("svn:executable" in info) and 'x' or ''
  1001             mode = ("svn:special" in info) and 'l' or mode
  1139             mode = ("svn:special" in info) and 'l' or mode
  1002         except svn.core.SubversionException as e:
  1140         except svn.core.SubversionException as e:
  1003             notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
  1141             notfound = (
  1004                 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
  1142                 svn.core.SVN_ERR_FS_NOT_FOUND,
  1005             if e.apr_err in notfound: # File not found
  1143                 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND,
       
  1144             )
       
  1145             if e.apr_err in notfound:  # File not found
  1006                 return None, None
  1146                 return None, None
  1007             raise
  1147             raise
  1008         if mode == 'l':
  1148         if mode == 'l':
  1009             link_prefix = "link "
  1149             link_prefix = "link "
  1010             if data.startswith(link_prefix):
  1150             if data.startswith(link_prefix):
  1011                 data = data[len(link_prefix):]
  1151                 data = data[len(link_prefix) :]
  1012         return data, mode
  1152         return data, mode
  1013 
  1153 
  1014     def _iterfiles(self, path, revnum):
  1154     def _iterfiles(self, path, revnum):
  1015         """Enumerate all files in path at revnum, recursively."""
  1155         """Enumerate all files in path at revnum, recursively."""
  1016         path = path.strip('/')
  1156         path = path.strip('/')
  1017         pool = svn.core.Pool()
  1157         pool = svn.core.Pool()
  1018         rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
  1158         rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
  1019         entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
  1159         entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
  1020         if path:
  1160         if path:
  1021             path += '/'
  1161             path += '/'
  1022         return ((path + p) for p, e in entries.iteritems()
  1162         return (
  1023                 if e.kind == svn.core.svn_node_file)
  1163             (path + p)
       
  1164             for p, e in entries.iteritems()
       
  1165             if e.kind == svn.core.svn_node_file
       
  1166         )
  1024 
  1167 
  1025     def getrelpath(self, path, module=None):
  1168     def getrelpath(self, path, module=None):
  1026         if module is None:
  1169         if module is None:
  1027             module = self.module
  1170             module = self.module
  1028         # Given the repository url of this wc, say
  1171         # Given the repository url of this wc, say
  1030         # extract the "entry" portion (a relative path) from what
  1173         # extract the "entry" portion (a relative path) from what
  1031         # svn log --xml says, i.e.
  1174         # svn log --xml says, i.e.
  1032         #   "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
  1175         #   "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
  1033         # that is to say "tests/PloneTestCase.py"
  1176         # that is to say "tests/PloneTestCase.py"
  1034         if path.startswith(module):
  1177         if path.startswith(module):
  1035             relative = path.rstrip('/')[len(module):]
  1178             relative = path.rstrip('/')[len(module) :]
  1036             if relative.startswith('/'):
  1179             if relative.startswith('/'):
  1037                 return relative[1:]
  1180                 return relative[1:]
  1038             elif relative == '':
  1181             elif relative == '':
  1039                 return relative
  1182                 return relative
  1040 
  1183 
  1052             return svn.ra.check_path(self.ra, path.strip('/'), revnum)
  1195             return svn.ra.check_path(self.ra, path.strip('/'), revnum)
  1053         finally:
  1196         finally:
  1054             if module is not None:
  1197             if module is not None:
  1055                 self.reparent(prevmodule)
  1198                 self.reparent(prevmodule)
  1056 
  1199 
  1057     def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
  1200     def _getlog(
  1058                 strict_node_history=False):
  1201         self,
       
  1202         paths,
       
  1203         start,
       
  1204         end,
       
  1205         limit=0,
       
  1206         discover_changed_paths=True,
       
  1207         strict_node_history=False,
       
  1208     ):
  1059         # Normalize path names, svn >= 1.5 only wants paths relative to
  1209         # Normalize path names, svn >= 1.5 only wants paths relative to
  1060         # supplied URL
  1210         # supplied URL
  1061         relpaths = []
  1211         relpaths = []
  1062         for p in paths:
  1212         for p in paths:
  1063             if not p.startswith('/'):
  1213             if not p.startswith('/'):
  1064                 p = self.module + '/' + p
  1214                 p = self.module + '/' + p
  1065             relpaths.append(p.strip('/'))
  1215             relpaths.append(p.strip('/'))
  1066         args = [self.baseurl, relpaths, start, end, limit,
  1216         args = [
  1067                 discover_changed_paths, strict_node_history]
  1217             self.baseurl,
       
  1218             relpaths,
       
  1219             start,
       
  1220             end,
       
  1221             limit,
       
  1222             discover_changed_paths,
       
  1223             strict_node_history,
       
  1224         ]
  1068         # developer config: convert.svn.debugsvnlog
  1225         # developer config: convert.svn.debugsvnlog
  1069         if not self.ui.configbool('convert', 'svn.debugsvnlog'):
  1226         if not self.ui.configbool('convert', 'svn.debugsvnlog'):
  1070             return directlogstream(*args)
  1227             return directlogstream(*args)
  1071         arg = encodeargs(args)
  1228         arg = encodeargs(args)
  1072         hgexe = procutil.hgexecutable()
  1229         hgexe = procutil.hgexecutable()
  1074         stdin, stdout = procutil.popen2(procutil.quotecommand(cmd))
  1231         stdin, stdout = procutil.popen2(procutil.quotecommand(cmd))
  1075         stdin.write(arg)
  1232         stdin.write(arg)
  1076         try:
  1233         try:
  1077             stdin.close()
  1234             stdin.close()
  1078         except IOError:
  1235         except IOError:
  1079             raise error.Abort(_('Mercurial failed to run itself, check'
  1236             raise error.Abort(
  1080                                ' hg executable is in PATH'))
  1237                 _(
       
  1238                     'Mercurial failed to run itself, check'
       
  1239                     ' hg executable is in PATH'
       
  1240                 )
       
  1241             )
  1081         return logstream(stdout)
  1242         return logstream(stdout)
       
  1243 
  1082 
  1244 
  1083 pre_revprop_change = b'''#!/bin/sh
  1245 pre_revprop_change = b'''#!/bin/sh
  1084 
  1246 
  1085 REPOS="$1"
  1247 REPOS="$1"
  1086 REV="$2"
  1248 REV="$2"
  1093 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
  1255 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
  1094 
  1256 
  1095 echo "Changing prohibited revision property" >&2
  1257 echo "Changing prohibited revision property" >&2
  1096 exit 1
  1258 exit 1
  1097 '''
  1259 '''
       
  1260 
  1098 
  1261 
  1099 class svn_sink(converter_sink, commandline):
  1262 class svn_sink(converter_sink, commandline):
  1100     commit_re = re.compile(br'Committed revision (\d+).', re.M)
  1263     commit_re = re.compile(br'Committed revision (\d+).', re.M)
  1101     uuid_re = re.compile(br'Repository UUID:\s*(\S+)', re.M)
  1264     uuid_re = re.compile(br'Repository UUID:\s*(\S+)', re.M)
  1102 
  1265 
  1135         else:
  1298         else:
  1136             if not re.search(br'^(file|http|https|svn|svn\+ssh)\://', path):
  1299             if not re.search(br'^(file|http|https|svn|svn\+ssh)\://', path):
  1137                 path = os.path.realpath(path)
  1300                 path = os.path.realpath(path)
  1138                 if os.path.isdir(os.path.dirname(path)):
  1301                 if os.path.isdir(os.path.dirname(path)):
  1139                     if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
  1302                     if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
  1140                         ui.status(_("initializing svn repository '%s'\n") %
  1303                         ui.status(
  1141                                   os.path.basename(path))
  1304                             _("initializing svn repository '%s'\n")
       
  1305                             % os.path.basename(path)
       
  1306                         )
  1142                         commandline(ui, 'svnadmin').run0('create', path)
  1307                         commandline(ui, 'svnadmin').run0('create', path)
  1143                         created = path
  1308                         created = path
  1144                     path = util.normpath(path)
  1309                     path = util.normpath(path)
  1145                     if not path.startswith('/'):
  1310                     if not path.startswith('/'):
  1146                         path = '/' + path
  1311                         path = '/' + path
  1147                     path = 'file://' + path
  1312                     path = 'file://' + path
  1148 
  1313 
  1149             wcpath = os.path.join(encoding.getcwd(), os.path.basename(path) +
  1314             wcpath = os.path.join(
  1150                                 '-wc')
  1315                 encoding.getcwd(), os.path.basename(path) + '-wc'
  1151             ui.status(_("initializing svn working copy '%s'\n")
  1316             )
  1152                       % os.path.basename(wcpath))
  1317             ui.status(
       
  1318                 _("initializing svn working copy '%s'\n")
       
  1319                 % os.path.basename(wcpath)
       
  1320             )
  1153             self.run0('checkout', path, wcpath)
  1321             self.run0('checkout', path, wcpath)
  1154 
  1322 
  1155             self.wc = wcpath
  1323             self.wc = wcpath
  1156         self.opener = vfsmod.vfs(self.wc)
  1324         self.opener = vfsmod.vfs(self.wc)
  1157         self.wopener = vfsmod.vfs(self.wc)
  1325         self.wopener = vfsmod.vfs(self.wc)
  1184         doc = xml.dom.minidom.parseString(output)
  1352         doc = xml.dom.minidom.parseString(output)
  1185         for e in doc.getElementsByTagName(r'entry'):
  1353         for e in doc.getElementsByTagName(r'entry'):
  1186             for n in e.childNodes:
  1354             for n in e.childNodes:
  1187                 if n.nodeType != n.ELEMENT_NODE or n.tagName != r'name':
  1355                 if n.nodeType != n.ELEMENT_NODE or n.tagName != r'name':
  1188                     continue
  1356                     continue
  1189                 name = r''.join(c.data for c in n.childNodes
  1357                 name = r''.join(
  1190                                 if c.nodeType == c.TEXT_NODE)
  1358                     c.data for c in n.childNodes if c.nodeType == c.TEXT_NODE
       
  1359                 )
  1191                 # Entries are compared with names coming from
  1360                 # Entries are compared with names coming from
  1192                 # mercurial, so bytes with undefined encoding. Our
  1361                 # mercurial, so bytes with undefined encoding. Our
  1193                 # best bet is to assume they are in local
  1362                 # best bet is to assume they are in local
  1194                 # encoding. They will be passed to command line calls
  1363                 # encoding. They will be passed to command line calls
  1195                 # later anyway, so they better be.
  1364                 # later anyway, so they better be.
  1231         # already occurred.  Cross the semantic gap.
  1400         # already occurred.  Cross the semantic gap.
  1232         wdest = self.wjoin(dest)
  1401         wdest = self.wjoin(dest)
  1233         exists = os.path.lexists(wdest)
  1402         exists = os.path.lexists(wdest)
  1234         if exists:
  1403         if exists:
  1235             fd, tempname = pycompat.mkstemp(
  1404             fd, tempname = pycompat.mkstemp(
  1236                 prefix='hg-copy-', dir=os.path.dirname(wdest))
  1405                 prefix='hg-copy-', dir=os.path.dirname(wdest)
       
  1406             )
  1237             os.close(fd)
  1407             os.close(fd)
  1238             os.unlink(tempname)
  1408             os.unlink(tempname)
  1239             os.rename(wdest, tempname)
  1409             os.rename(wdest, tempname)
  1240         try:
  1410         try:
  1241             self.run0('copy', source, dest)
  1411             self.run0('copy', source, dest)
  1257             for i in iter(lambda: f.rfind('/', 0, i), -1):
  1427             for i in iter(lambda: f.rfind('/', 0, i), -1):
  1258                 dirs.add(f[:i])
  1428                 dirs.add(f[:i])
  1259         return dirs
  1429         return dirs
  1260 
  1430 
  1261     def add_dirs(self, files):
  1431     def add_dirs(self, files):
  1262         add_dirs = [d for d in sorted(self.dirs_of(files))
  1432         add_dirs = [
  1263                     if d not in self.manifest]
  1433             d for d in sorted(self.dirs_of(files)) if d not in self.manifest
       
  1434         ]
  1264         if add_dirs:
  1435         if add_dirs:
  1265             self.manifest.update(add_dirs)
  1436             self.manifest.update(add_dirs)
  1266             self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
  1437             self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
  1267         return add_dirs
  1438         return add_dirs
  1268 
  1439 
  1277         self.childmap[parent] = child
  1448         self.childmap[parent] = child
  1278 
  1449 
  1279     def revid(self, rev):
  1450     def revid(self, rev):
  1280         return "svn:%s@%s" % (self.uuid, rev)
  1451         return "svn:%s@%s" % (self.uuid, rev)
  1281 
  1452 
  1282     def putcommit(self, files, copies, parents, commit, source, revmap, full,
  1453     def putcommit(
  1283                   cleanp2):
  1454         self, files, copies, parents, commit, source, revmap, full, cleanp2
       
  1455     ):
  1284         for parent in parents:
  1456         for parent in parents:
  1285             try:
  1457             try:
  1286                 return self.revid(self.childmap[parent])
  1458                 return self.revid(self.childmap[parent])
  1287             except KeyError:
  1459             except KeyError:
  1288                 pass
  1460                 pass
  1323         fd, messagefile = pycompat.mkstemp(prefix='hg-convert-')
  1495         fd, messagefile = pycompat.mkstemp(prefix='hg-convert-')
  1324         fp = os.fdopen(fd, r'wb')
  1496         fp = os.fdopen(fd, r'wb')
  1325         fp.write(util.tonativeeol(commit.desc))
  1497         fp.write(util.tonativeeol(commit.desc))
  1326         fp.close()
  1498         fp.close()
  1327         try:
  1499         try:
  1328             output = self.run0('commit',
  1500             output = self.run0(
  1329                                username=stringutil.shortuser(commit.author),
  1501                 'commit',
  1330                                file=messagefile,
  1502                 username=stringutil.shortuser(commit.author),
  1331                                encoding='utf-8')
  1503                 file=messagefile,
       
  1504                 encoding='utf-8',
       
  1505             )
  1332             try:
  1506             try:
  1333                 rev = self.commit_re.search(output).group(1)
  1507                 rev = self.commit_re.search(output).group(1)
  1334             except AttributeError:
  1508             except AttributeError:
  1335                 if not files:
  1509                 if not files:
  1336                     return parents[0] if parents else 'None'
  1510                     return parents[0] if parents else 'None'
  1337                 self.ui.warn(_('unexpected svn output:\n'))
  1511                 self.ui.warn(_('unexpected svn output:\n'))
  1338                 self.ui.warn(output)
  1512                 self.ui.warn(output)
  1339                 raise error.Abort(_('unable to cope with svn output'))
  1513                 raise error.Abort(_('unable to cope with svn output'))
  1340             if commit.rev:
  1514             if commit.rev:
  1341                 self.run('propset', 'hg:convert-rev', commit.rev,
  1515                 self.run(
  1342                          revprop=True, revision=rev)
  1516                     'propset',
       
  1517                     'hg:convert-rev',
       
  1518                     commit.rev,
       
  1519                     revprop=True,
       
  1520                     revision=rev,
       
  1521                 )
  1343             if commit.branch and commit.branch != 'default':
  1522             if commit.branch and commit.branch != 'default':
  1344                 self.run('propset', 'hg:convert-branch', commit.branch,
  1523                 self.run(
  1345                          revprop=True, revision=rev)
  1524                     'propset',
       
  1525                     'hg:convert-branch',
       
  1526                     commit.branch,
       
  1527                     revprop=True,
       
  1528                     revision=rev,
       
  1529                 )
  1346             for parent in parents:
  1530             for parent in parents:
  1347                 self.addchild(parent, rev)
  1531                 self.addchild(parent, rev)
  1348             return self.revid(rev)
  1532             return self.revid(rev)
  1349         finally:
  1533         finally:
  1350             os.unlink(messagefile)
  1534             os.unlink(messagefile)
  1361     def hascommitforsplicemap(self, rev):
  1545     def hascommitforsplicemap(self, rev):
  1362         # This is not correct as one can convert to an existing subversion
  1546         # This is not correct as one can convert to an existing subversion
  1363         # repository and childmap would not list all revisions. Too bad.
  1547         # repository and childmap would not list all revisions. Too bad.
  1364         if rev in self.childmap:
  1548         if rev in self.childmap:
  1365             return True
  1549             return True
  1366         raise error.Abort(_('splice map revision %s not found in subversion '
  1550         raise error.Abort(
  1367                            'child map (revision lookups are not implemented)')
  1551             _(
  1368                          % rev)
  1552                 'splice map revision %s not found in subversion '
       
  1553                 'child map (revision lookups are not implemented)'
       
  1554             )
       
  1555             % rev
       
  1556         )