##// END OF EJS Templates
splicemap: improve error handling when source is subversion (issue2084)...
Ben Goswami -
r19122:83973dc1 default
parent child Browse files
Show More
@@ -1,448 +1,447 b''
1 # common.py - common code for the convert extension
1 # common.py - common code for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import base64, errno, subprocess, os, datetime, re
8 import base64, errno, subprocess, os, datetime, re
9 import cPickle as pickle
9 import cPickle as pickle
10 from mercurial import util
10 from mercurial import util
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 def encodeargs(args):
15 def encodeargs(args):
16 def encodearg(s):
16 def encodearg(s):
17 lines = base64.encodestring(s)
17 lines = base64.encodestring(s)
18 lines = [l.splitlines()[0] for l in lines]
18 lines = [l.splitlines()[0] for l in lines]
19 return ''.join(lines)
19 return ''.join(lines)
20
20
21 s = pickle.dumps(args)
21 s = pickle.dumps(args)
22 return encodearg(s)
22 return encodearg(s)
23
23
24 def decodeargs(s):
24 def decodeargs(s):
25 s = base64.decodestring(s)
25 s = base64.decodestring(s)
26 return pickle.loads(s)
26 return pickle.loads(s)
27
27
28 class MissingTool(Exception):
28 class MissingTool(Exception):
29 pass
29 pass
30
30
31 def checktool(exe, name=None, abort=True):
31 def checktool(exe, name=None, abort=True):
32 name = name or exe
32 name = name or exe
33 if not util.findexe(exe):
33 if not util.findexe(exe):
34 exc = abort and util.Abort or MissingTool
34 exc = abort and util.Abort or MissingTool
35 raise exc(_('cannot find required "%s" tool') % name)
35 raise exc(_('cannot find required "%s" tool') % name)
36
36
37 class NoRepo(Exception):
37 class NoRepo(Exception):
38 pass
38 pass
39
39
40 SKIPREV = 'SKIP'
40 SKIPREV = 'SKIP'
41
41
42 class commit(object):
42 class commit(object):
43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
44 extra={}, sortkey=None):
44 extra={}, sortkey=None):
45 self.author = author or 'unknown'
45 self.author = author or 'unknown'
46 self.date = date or '0 0'
46 self.date = date or '0 0'
47 self.desc = desc
47 self.desc = desc
48 self.parents = parents
48 self.parents = parents
49 self.branch = branch
49 self.branch = branch
50 self.rev = rev
50 self.rev = rev
51 self.extra = extra
51 self.extra = extra
52 self.sortkey = sortkey
52 self.sortkey = sortkey
53
53
54 class converter_source(object):
54 class converter_source(object):
55 """Conversion source interface"""
55 """Conversion source interface"""
56
56
57 def __init__(self, ui, path=None, rev=None):
57 def __init__(self, ui, path=None, rev=None):
58 """Initialize conversion source (or raise NoRepo("message")
58 """Initialize conversion source (or raise NoRepo("message")
59 exception if path is not a valid repository)"""
59 exception if path is not a valid repository)"""
60 self.ui = ui
60 self.ui = ui
61 self.path = path
61 self.path = path
62 self.rev = rev
62 self.rev = rev
63
63
64 self.encoding = 'utf-8'
64 self.encoding = 'utf-8'
65
65
66 def checkhexformat(self, revstr):
66 def checkhexformat(self, revstr):
67 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
67 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
68 such format for their revision numbering
68 such format for their revision numbering
69 """
69 """
70 matchobj = re.match(r'[0-9a-fA-F]{40,40}$', revstr)
70 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
71 if matchobj is None:
72 raise util.Abort(_('splicemap entry %s is not a valid revision'
71 raise util.Abort(_('splicemap entry %s is not a valid revision'
73 ' identifier') % revstr)
72 ' identifier') % revstr)
74
73
75 def before(self):
74 def before(self):
76 pass
75 pass
77
76
78 def after(self):
77 def after(self):
79 pass
78 pass
80
79
81 def setrevmap(self, revmap):
80 def setrevmap(self, revmap):
82 """set the map of already-converted revisions"""
81 """set the map of already-converted revisions"""
83 pass
82 pass
84
83
85 def getheads(self):
84 def getheads(self):
86 """Return a list of this repository's heads"""
85 """Return a list of this repository's heads"""
87 raise NotImplementedError
86 raise NotImplementedError
88
87
89 def getfile(self, name, rev):
88 def getfile(self, name, rev):
90 """Return a pair (data, mode) where data is the file content
89 """Return a pair (data, mode) where data is the file content
91 as a string and mode one of '', 'x' or 'l'. rev is the
90 as a string and mode one of '', 'x' or 'l'. rev is the
92 identifier returned by a previous call to getchanges(). Raise
91 identifier returned by a previous call to getchanges(). Raise
93 IOError to indicate that name was deleted in rev.
92 IOError to indicate that name was deleted in rev.
94 """
93 """
95 raise NotImplementedError
94 raise NotImplementedError
96
95
97 def getchanges(self, version):
96 def getchanges(self, version):
98 """Returns a tuple of (files, copies).
97 """Returns a tuple of (files, copies).
99
98
100 files is a sorted list of (filename, id) tuples for all files
99 files is a sorted list of (filename, id) tuples for all files
101 changed between version and its first parent returned by
100 changed between version and its first parent returned by
102 getcommit(). id is the source revision id of the file.
101 getcommit(). id is the source revision id of the file.
103
102
104 copies is a dictionary of dest: source
103 copies is a dictionary of dest: source
105 """
104 """
106 raise NotImplementedError
105 raise NotImplementedError
107
106
108 def getcommit(self, version):
107 def getcommit(self, version):
109 """Return the commit object for version"""
108 """Return the commit object for version"""
110 raise NotImplementedError
109 raise NotImplementedError
111
110
112 def gettags(self):
111 def gettags(self):
113 """Return the tags as a dictionary of name: revision
112 """Return the tags as a dictionary of name: revision
114
113
115 Tag names must be UTF-8 strings.
114 Tag names must be UTF-8 strings.
116 """
115 """
117 raise NotImplementedError
116 raise NotImplementedError
118
117
119 def recode(self, s, encoding=None):
118 def recode(self, s, encoding=None):
120 if not encoding:
119 if not encoding:
121 encoding = self.encoding or 'utf-8'
120 encoding = self.encoding or 'utf-8'
122
121
123 if isinstance(s, unicode):
122 if isinstance(s, unicode):
124 return s.encode("utf-8")
123 return s.encode("utf-8")
125 try:
124 try:
126 return s.decode(encoding).encode("utf-8")
125 return s.decode(encoding).encode("utf-8")
127 except UnicodeError:
126 except UnicodeError:
128 try:
127 try:
129 return s.decode("latin-1").encode("utf-8")
128 return s.decode("latin-1").encode("utf-8")
130 except UnicodeError:
129 except UnicodeError:
131 return s.decode(encoding, "replace").encode("utf-8")
130 return s.decode(encoding, "replace").encode("utf-8")
132
131
133 def getchangedfiles(self, rev, i):
132 def getchangedfiles(self, rev, i):
134 """Return the files changed by rev compared to parent[i].
133 """Return the files changed by rev compared to parent[i].
135
134
136 i is an index selecting one of the parents of rev. The return
135 i is an index selecting one of the parents of rev. The return
137 value should be the list of files that are different in rev and
136 value should be the list of files that are different in rev and
138 this parent.
137 this parent.
139
138
140 If rev has no parents, i is None.
139 If rev has no parents, i is None.
141
140
142 This function is only needed to support --filemap
141 This function is only needed to support --filemap
143 """
142 """
144 raise NotImplementedError
143 raise NotImplementedError
145
144
146 def converted(self, rev, sinkrev):
145 def converted(self, rev, sinkrev):
147 '''Notify the source that a revision has been converted.'''
146 '''Notify the source that a revision has been converted.'''
148 pass
147 pass
149
148
150 def hasnativeorder(self):
149 def hasnativeorder(self):
151 """Return true if this source has a meaningful, native revision
150 """Return true if this source has a meaningful, native revision
152 order. For instance, Mercurial revisions are store sequentially
151 order. For instance, Mercurial revisions are store sequentially
153 while there is no such global ordering with Darcs.
152 while there is no such global ordering with Darcs.
154 """
153 """
155 return False
154 return False
156
155
157 def hasnativeclose(self):
156 def hasnativeclose(self):
158 """Return true if this source has ability to close branch.
157 """Return true if this source has ability to close branch.
159 """
158 """
160 return False
159 return False
161
160
162 def lookuprev(self, rev):
161 def lookuprev(self, rev):
163 """If rev is a meaningful revision reference in source, return
162 """If rev is a meaningful revision reference in source, return
164 the referenced identifier in the same format used by getcommit().
163 the referenced identifier in the same format used by getcommit().
165 return None otherwise.
164 return None otherwise.
166 """
165 """
167 return None
166 return None
168
167
169 def getbookmarks(self):
168 def getbookmarks(self):
170 """Return the bookmarks as a dictionary of name: revision
169 """Return the bookmarks as a dictionary of name: revision
171
170
172 Bookmark names are to be UTF-8 strings.
171 Bookmark names are to be UTF-8 strings.
173 """
172 """
174 return {}
173 return {}
175
174
176 def checkrevformat(self, revstr):
175 def checkrevformat(self, revstr):
177 """revstr is a string that describes a revision in the given
176 """revstr is a string that describes a revision in the given
178 source control system. Return true if revstr has correct
177 source control system. Return true if revstr has correct
179 format.
178 format.
180 """
179 """
181 return True
180 return True
182
181
183 class converter_sink(object):
182 class converter_sink(object):
184 """Conversion sink (target) interface"""
183 """Conversion sink (target) interface"""
185
184
186 def __init__(self, ui, path):
185 def __init__(self, ui, path):
187 """Initialize conversion sink (or raise NoRepo("message")
186 """Initialize conversion sink (or raise NoRepo("message")
188 exception if path is not a valid repository)
187 exception if path is not a valid repository)
189
188
190 created is a list of paths to remove if a fatal error occurs
189 created is a list of paths to remove if a fatal error occurs
191 later"""
190 later"""
192 self.ui = ui
191 self.ui = ui
193 self.path = path
192 self.path = path
194 self.created = []
193 self.created = []
195
194
196 def getheads(self):
195 def getheads(self):
197 """Return a list of this repository's heads"""
196 """Return a list of this repository's heads"""
198 raise NotImplementedError
197 raise NotImplementedError
199
198
200 def revmapfile(self):
199 def revmapfile(self):
201 """Path to a file that will contain lines
200 """Path to a file that will contain lines
202 source_rev_id sink_rev_id
201 source_rev_id sink_rev_id
203 mapping equivalent revision identifiers for each system."""
202 mapping equivalent revision identifiers for each system."""
204 raise NotImplementedError
203 raise NotImplementedError
205
204
206 def authorfile(self):
205 def authorfile(self):
207 """Path to a file that will contain lines
206 """Path to a file that will contain lines
208 srcauthor=dstauthor
207 srcauthor=dstauthor
209 mapping equivalent authors identifiers for each system."""
208 mapping equivalent authors identifiers for each system."""
210 return None
209 return None
211
210
212 def putcommit(self, files, copies, parents, commit, source, revmap):
211 def putcommit(self, files, copies, parents, commit, source, revmap):
213 """Create a revision with all changed files listed in 'files'
212 """Create a revision with all changed files listed in 'files'
214 and having listed parents. 'commit' is a commit object
213 and having listed parents. 'commit' is a commit object
215 containing at a minimum the author, date, and message for this
214 containing at a minimum the author, date, and message for this
216 changeset. 'files' is a list of (path, version) tuples,
215 changeset. 'files' is a list of (path, version) tuples,
217 'copies' is a dictionary mapping destinations to sources,
216 'copies' is a dictionary mapping destinations to sources,
218 'source' is the source repository, and 'revmap' is a mapfile
217 'source' is the source repository, and 'revmap' is a mapfile
219 of source revisions to converted revisions. Only getfile() and
218 of source revisions to converted revisions. Only getfile() and
220 lookuprev() should be called on 'source'.
219 lookuprev() should be called on 'source'.
221
220
222 Note that the sink repository is not told to update itself to
221 Note that the sink repository is not told to update itself to
223 a particular revision (or even what that revision would be)
222 a particular revision (or even what that revision would be)
224 before it receives the file data.
223 before it receives the file data.
225 """
224 """
226 raise NotImplementedError
225 raise NotImplementedError
227
226
228 def puttags(self, tags):
227 def puttags(self, tags):
229 """Put tags into sink.
228 """Put tags into sink.
230
229
231 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
230 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
232 Return a pair (tag_revision, tag_parent_revision), or (None, None)
231 Return a pair (tag_revision, tag_parent_revision), or (None, None)
233 if nothing was changed.
232 if nothing was changed.
234 """
233 """
235 raise NotImplementedError
234 raise NotImplementedError
236
235
237 def setbranch(self, branch, pbranches):
236 def setbranch(self, branch, pbranches):
238 """Set the current branch name. Called before the first putcommit
237 """Set the current branch name. Called before the first putcommit
239 on the branch.
238 on the branch.
240 branch: branch name for subsequent commits
239 branch: branch name for subsequent commits
241 pbranches: (converted parent revision, parent branch) tuples"""
240 pbranches: (converted parent revision, parent branch) tuples"""
242 pass
241 pass
243
242
244 def setfilemapmode(self, active):
243 def setfilemapmode(self, active):
245 """Tell the destination that we're using a filemap
244 """Tell the destination that we're using a filemap
246
245
247 Some converter_sources (svn in particular) can claim that a file
246 Some converter_sources (svn in particular) can claim that a file
248 was changed in a revision, even if there was no change. This method
247 was changed in a revision, even if there was no change. This method
249 tells the destination that we're using a filemap and that it should
248 tells the destination that we're using a filemap and that it should
250 filter empty revisions.
249 filter empty revisions.
251 """
250 """
252 pass
251 pass
253
252
254 def before(self):
253 def before(self):
255 pass
254 pass
256
255
257 def after(self):
256 def after(self):
258 pass
257 pass
259
258
260 def putbookmarks(self, bookmarks):
259 def putbookmarks(self, bookmarks):
261 """Put bookmarks into sink.
260 """Put bookmarks into sink.
262
261
263 bookmarks: {bookmarkname: sink_rev_id, ...}
262 bookmarks: {bookmarkname: sink_rev_id, ...}
264 where bookmarkname is an UTF-8 string.
263 where bookmarkname is an UTF-8 string.
265 """
264 """
266 pass
265 pass
267
266
268 def hascommit(self, rev):
267 def hascommit(self, rev):
269 """Return True if the sink contains rev"""
268 """Return True if the sink contains rev"""
270 raise NotImplementedError
269 raise NotImplementedError
271
270
272 class commandline(object):
271 class commandline(object):
273 def __init__(self, ui, command):
272 def __init__(self, ui, command):
274 self.ui = ui
273 self.ui = ui
275 self.command = command
274 self.command = command
276
275
277 def prerun(self):
276 def prerun(self):
278 pass
277 pass
279
278
280 def postrun(self):
279 def postrun(self):
281 pass
280 pass
282
281
283 def _cmdline(self, cmd, *args, **kwargs):
282 def _cmdline(self, cmd, *args, **kwargs):
284 cmdline = [self.command, cmd] + list(args)
283 cmdline = [self.command, cmd] + list(args)
285 for k, v in kwargs.iteritems():
284 for k, v in kwargs.iteritems():
286 if len(k) == 1:
285 if len(k) == 1:
287 cmdline.append('-' + k)
286 cmdline.append('-' + k)
288 else:
287 else:
289 cmdline.append('--' + k.replace('_', '-'))
288 cmdline.append('--' + k.replace('_', '-'))
290 try:
289 try:
291 if len(k) == 1:
290 if len(k) == 1:
292 cmdline.append('' + v)
291 cmdline.append('' + v)
293 else:
292 else:
294 cmdline[-1] += '=' + v
293 cmdline[-1] += '=' + v
295 except TypeError:
294 except TypeError:
296 pass
295 pass
297 cmdline = [util.shellquote(arg) for arg in cmdline]
296 cmdline = [util.shellquote(arg) for arg in cmdline]
298 if not self.ui.debugflag:
297 if not self.ui.debugflag:
299 cmdline += ['2>', os.devnull]
298 cmdline += ['2>', os.devnull]
300 cmdline = ' '.join(cmdline)
299 cmdline = ' '.join(cmdline)
301 return cmdline
300 return cmdline
302
301
303 def _run(self, cmd, *args, **kwargs):
302 def _run(self, cmd, *args, **kwargs):
304 def popen(cmdline):
303 def popen(cmdline):
305 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
304 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
306 close_fds=util.closefds,
305 close_fds=util.closefds,
307 stdout=subprocess.PIPE)
306 stdout=subprocess.PIPE)
308 return p
307 return p
309 return self._dorun(popen, cmd, *args, **kwargs)
308 return self._dorun(popen, cmd, *args, **kwargs)
310
309
311 def _run2(self, cmd, *args, **kwargs):
310 def _run2(self, cmd, *args, **kwargs):
312 return self._dorun(util.popen2, cmd, *args, **kwargs)
311 return self._dorun(util.popen2, cmd, *args, **kwargs)
313
312
314 def _dorun(self, openfunc, cmd, *args, **kwargs):
313 def _dorun(self, openfunc, cmd, *args, **kwargs):
315 cmdline = self._cmdline(cmd, *args, **kwargs)
314 cmdline = self._cmdline(cmd, *args, **kwargs)
316 self.ui.debug('running: %s\n' % (cmdline,))
315 self.ui.debug('running: %s\n' % (cmdline,))
317 self.prerun()
316 self.prerun()
318 try:
317 try:
319 return openfunc(cmdline)
318 return openfunc(cmdline)
320 finally:
319 finally:
321 self.postrun()
320 self.postrun()
322
321
323 def run(self, cmd, *args, **kwargs):
322 def run(self, cmd, *args, **kwargs):
324 p = self._run(cmd, *args, **kwargs)
323 p = self._run(cmd, *args, **kwargs)
325 output = p.communicate()[0]
324 output = p.communicate()[0]
326 self.ui.debug(output)
325 self.ui.debug(output)
327 return output, p.returncode
326 return output, p.returncode
328
327
329 def runlines(self, cmd, *args, **kwargs):
328 def runlines(self, cmd, *args, **kwargs):
330 p = self._run(cmd, *args, **kwargs)
329 p = self._run(cmd, *args, **kwargs)
331 output = p.stdout.readlines()
330 output = p.stdout.readlines()
332 p.wait()
331 p.wait()
333 self.ui.debug(''.join(output))
332 self.ui.debug(''.join(output))
334 return output, p.returncode
333 return output, p.returncode
335
334
336 def checkexit(self, status, output=''):
335 def checkexit(self, status, output=''):
337 if status:
336 if status:
338 if output:
337 if output:
339 self.ui.warn(_('%s error:\n') % self.command)
338 self.ui.warn(_('%s error:\n') % self.command)
340 self.ui.warn(output)
339 self.ui.warn(output)
341 msg = util.explainexit(status)[0]
340 msg = util.explainexit(status)[0]
342 raise util.Abort('%s %s' % (self.command, msg))
341 raise util.Abort('%s %s' % (self.command, msg))
343
342
344 def run0(self, cmd, *args, **kwargs):
343 def run0(self, cmd, *args, **kwargs):
345 output, status = self.run(cmd, *args, **kwargs)
344 output, status = self.run(cmd, *args, **kwargs)
346 self.checkexit(status, output)
345 self.checkexit(status, output)
347 return output
346 return output
348
347
349 def runlines0(self, cmd, *args, **kwargs):
348 def runlines0(self, cmd, *args, **kwargs):
350 output, status = self.runlines(cmd, *args, **kwargs)
349 output, status = self.runlines(cmd, *args, **kwargs)
351 self.checkexit(status, ''.join(output))
350 self.checkexit(status, ''.join(output))
352 return output
351 return output
353
352
354 @propertycache
353 @propertycache
355 def argmax(self):
354 def argmax(self):
356 # POSIX requires at least 4096 bytes for ARG_MAX
355 # POSIX requires at least 4096 bytes for ARG_MAX
357 argmax = 4096
356 argmax = 4096
358 try:
357 try:
359 argmax = os.sysconf("SC_ARG_MAX")
358 argmax = os.sysconf("SC_ARG_MAX")
360 except (AttributeError, ValueError):
359 except (AttributeError, ValueError):
361 pass
360 pass
362
361
363 # Windows shells impose their own limits on command line length,
362 # Windows shells impose their own limits on command line length,
364 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
363 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
365 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
364 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
366 # details about cmd.exe limitations.
365 # details about cmd.exe limitations.
367
366
368 # Since ARG_MAX is for command line _and_ environment, lower our limit
367 # Since ARG_MAX is for command line _and_ environment, lower our limit
369 # (and make happy Windows shells while doing this).
368 # (and make happy Windows shells while doing this).
370 return argmax // 2 - 1
369 return argmax // 2 - 1
371
370
372 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
371 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
373 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
372 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
374 limit = self.argmax - cmdlen
373 limit = self.argmax - cmdlen
375 bytes = 0
374 bytes = 0
376 fl = []
375 fl = []
377 for fn in arglist:
376 for fn in arglist:
378 b = len(fn) + 3
377 b = len(fn) + 3
379 if bytes + b < limit or len(fl) == 0:
378 if bytes + b < limit or len(fl) == 0:
380 fl.append(fn)
379 fl.append(fn)
381 bytes += b
380 bytes += b
382 else:
381 else:
383 yield fl
382 yield fl
384 fl = [fn]
383 fl = [fn]
385 bytes = b
384 bytes = b
386 if fl:
385 if fl:
387 yield fl
386 yield fl
388
387
389 def xargs(self, arglist, cmd, *args, **kwargs):
388 def xargs(self, arglist, cmd, *args, **kwargs):
390 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
389 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
391 self.run0(cmd, *(list(args) + l), **kwargs)
390 self.run0(cmd, *(list(args) + l), **kwargs)
392
391
393 class mapfile(dict):
392 class mapfile(dict):
394 def __init__(self, ui, path):
393 def __init__(self, ui, path):
395 super(mapfile, self).__init__()
394 super(mapfile, self).__init__()
396 self.ui = ui
395 self.ui = ui
397 self.path = path
396 self.path = path
398 self.fp = None
397 self.fp = None
399 self.order = []
398 self.order = []
400 self._read()
399 self._read()
401
400
402 def _read(self):
401 def _read(self):
403 if not self.path:
402 if not self.path:
404 return
403 return
405 try:
404 try:
406 fp = open(self.path, 'r')
405 fp = open(self.path, 'r')
407 except IOError, err:
406 except IOError, err:
408 if err.errno != errno.ENOENT:
407 if err.errno != errno.ENOENT:
409 raise
408 raise
410 return
409 return
411 for i, line in enumerate(fp):
410 for i, line in enumerate(fp):
412 line = line.splitlines()[0].rstrip()
411 line = line.splitlines()[0].rstrip()
413 if not line:
412 if not line:
414 # Ignore blank lines
413 # Ignore blank lines
415 continue
414 continue
416 try:
415 try:
417 key, value = line.rsplit(' ', 1)
416 key, value = line.rsplit(' ', 1)
418 except ValueError:
417 except ValueError:
419 raise util.Abort(
418 raise util.Abort(
420 _('syntax error in %s(%d): key/value pair expected')
419 _('syntax error in %s(%d): key/value pair expected')
421 % (self.path, i + 1))
420 % (self.path, i + 1))
422 if key not in self:
421 if key not in self:
423 self.order.append(key)
422 self.order.append(key)
424 super(mapfile, self).__setitem__(key, value)
423 super(mapfile, self).__setitem__(key, value)
425 fp.close()
424 fp.close()
426
425
427 def __setitem__(self, key, value):
426 def __setitem__(self, key, value):
428 if self.fp is None:
427 if self.fp is None:
429 try:
428 try:
430 self.fp = open(self.path, 'a')
429 self.fp = open(self.path, 'a')
431 except IOError, err:
430 except IOError, err:
432 raise util.Abort(_('could not open map file %r: %s') %
431 raise util.Abort(_('could not open map file %r: %s') %
433 (self.path, err.strerror))
432 (self.path, err.strerror))
434 self.fp.write('%s %s\n' % (key, value))
433 self.fp.write('%s %s\n' % (key, value))
435 self.fp.flush()
434 self.fp.flush()
436 super(mapfile, self).__setitem__(key, value)
435 super(mapfile, self).__setitem__(key, value)
437
436
438 def close(self):
437 def close(self):
439 if self.fp:
438 if self.fp:
440 self.fp.close()
439 self.fp.close()
441 self.fp = None
440 self.fp = None
442
441
443 def makedatetimestamp(t):
442 def makedatetimestamp(t):
444 """Like util.makedate() but for time t instead of current time"""
443 """Like util.makedate() but for time t instead of current time"""
445 delta = (datetime.datetime.utcfromtimestamp(t) -
444 delta = (datetime.datetime.utcfromtimestamp(t) -
446 datetime.datetime.fromtimestamp(t))
445 datetime.datetime.fromtimestamp(t))
447 tz = delta.days * 86400 + delta.seconds
446 tz = delta.days * 86400 + delta.seconds
448 return t, tz
447 return t, tz
@@ -1,1254 +1,1262 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4
4
5 import os, re, sys, tempfile, urllib, urllib2, xml.dom.minidom
5 import os, re, sys, tempfile, urllib, urllib2, xml.dom.minidom
6 import cPickle as pickle
6 import cPickle as pickle
7
7
8 from mercurial import strutil, scmutil, util, encoding
8 from mercurial import strutil, scmutil, util, encoding
9 from mercurial.i18n import _
9 from mercurial.i18n import _
10
10
11 propertycache = util.propertycache
11 propertycache = util.propertycache
12
12
13 # Subversion stuff. Works best with very recent Python SVN bindings
13 # Subversion stuff. Works best with very recent Python SVN bindings
14 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
14 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
15 # these bindings.
15 # these bindings.
16
16
17 from cStringIO import StringIO
17 from cStringIO import StringIO
18
18
19 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
19 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
20 from common import commandline, converter_source, converter_sink, mapfile
20 from common import commandline, converter_source, converter_sink, mapfile
21 from common import makedatetimestamp
21 from common import makedatetimestamp
22
22
23 try:
23 try:
24 from svn.core import SubversionException, Pool
24 from svn.core import SubversionException, Pool
25 import svn
25 import svn
26 import svn.client
26 import svn.client
27 import svn.core
27 import svn.core
28 import svn.ra
28 import svn.ra
29 import svn.delta
29 import svn.delta
30 import transport
30 import transport
31 import warnings
31 import warnings
32 warnings.filterwarnings('ignore',
32 warnings.filterwarnings('ignore',
33 module='svn.core',
33 module='svn.core',
34 category=DeprecationWarning)
34 category=DeprecationWarning)
35
35
36 except ImportError:
36 except ImportError:
37 svn = None
37 svn = None
38
38
39 class SvnPathNotFound(Exception):
39 class SvnPathNotFound(Exception):
40 pass
40 pass
41
41
42 def revsplit(rev):
42 def revsplit(rev):
43 """Parse a revision string and return (uuid, path, revnum)."""
43 """Parse a revision string and return (uuid, path, revnum)."""
44 url, revnum = rev.rsplit('@', 1)
44 url, revnum = rev.rsplit('@', 1)
45 parts = url.split('/', 1)
45 parts = url.split('/', 1)
46 mod = ''
46 mod = ''
47 if len(parts) > 1:
47 if len(parts) > 1:
48 mod = '/' + parts[1]
48 mod = '/' + parts[1]
49 return parts[0][4:], mod, int(revnum)
49 return parts[0][4:], mod, int(revnum)
50
50
51 def quote(s):
51 def quote(s):
52 # As of svn 1.7, many svn calls expect "canonical" paths. In
52 # As of svn 1.7, many svn calls expect "canonical" paths. In
53 # theory, we should call svn.core.*canonicalize() on all paths
53 # theory, we should call svn.core.*canonicalize() on all paths
54 # before passing them to the API. Instead, we assume the base url
54 # before passing them to the API. Instead, we assume the base url
55 # is canonical and copy the behaviour of svn URL encoding function
55 # is canonical and copy the behaviour of svn URL encoding function
56 # so we can extend it safely with new components. The "safe"
56 # so we can extend it safely with new components. The "safe"
57 # characters were taken from the "svn_uri__char_validity" table in
57 # characters were taken from the "svn_uri__char_validity" table in
58 # libsvn_subr/path.c.
58 # libsvn_subr/path.c.
59 return urllib.quote(s, "!$&'()*+,-./:=@_~")
59 return urllib.quote(s, "!$&'()*+,-./:=@_~")
60
60
61 def geturl(path):
61 def geturl(path):
62 try:
62 try:
63 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
63 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
64 except SubversionException:
64 except SubversionException:
65 # svn.client.url_from_path() fails with local repositories
65 # svn.client.url_from_path() fails with local repositories
66 pass
66 pass
67 if os.path.isdir(path):
67 if os.path.isdir(path):
68 path = os.path.normpath(os.path.abspath(path))
68 path = os.path.normpath(os.path.abspath(path))
69 if os.name == 'nt':
69 if os.name == 'nt':
70 path = '/' + util.normpath(path)
70 path = '/' + util.normpath(path)
71 # Module URL is later compared with the repository URL returned
71 # Module URL is later compared with the repository URL returned
72 # by svn API, which is UTF-8.
72 # by svn API, which is UTF-8.
73 path = encoding.tolocal(path)
73 path = encoding.tolocal(path)
74 path = 'file://%s' % quote(path)
74 path = 'file://%s' % quote(path)
75 return svn.core.svn_path_canonicalize(path)
75 return svn.core.svn_path_canonicalize(path)
76
76
77 def optrev(number):
77 def optrev(number):
78 optrev = svn.core.svn_opt_revision_t()
78 optrev = svn.core.svn_opt_revision_t()
79 optrev.kind = svn.core.svn_opt_revision_number
79 optrev.kind = svn.core.svn_opt_revision_number
80 optrev.value.number = number
80 optrev.value.number = number
81 return optrev
81 return optrev
82
82
83 class changedpath(object):
83 class changedpath(object):
84 def __init__(self, p):
84 def __init__(self, p):
85 self.copyfrom_path = p.copyfrom_path
85 self.copyfrom_path = p.copyfrom_path
86 self.copyfrom_rev = p.copyfrom_rev
86 self.copyfrom_rev = p.copyfrom_rev
87 self.action = p.action
87 self.action = p.action
88
88
89 def get_log_child(fp, url, paths, start, end, limit=0,
89 def get_log_child(fp, url, paths, start, end, limit=0,
90 discover_changed_paths=True, strict_node_history=False):
90 discover_changed_paths=True, strict_node_history=False):
91 protocol = -1
91 protocol = -1
92 def receiver(orig_paths, revnum, author, date, message, pool):
92 def receiver(orig_paths, revnum, author, date, message, pool):
93 if orig_paths is not None:
93 if orig_paths is not None:
94 for k, v in orig_paths.iteritems():
94 for k, v in orig_paths.iteritems():
95 orig_paths[k] = changedpath(v)
95 orig_paths[k] = changedpath(v)
96 pickle.dump((orig_paths, revnum, author, date, message),
96 pickle.dump((orig_paths, revnum, author, date, message),
97 fp, protocol)
97 fp, protocol)
98
98
99 try:
99 try:
100 # Use an ra of our own so that our parent can consume
100 # Use an ra of our own so that our parent can consume
101 # our results without confusing the server.
101 # our results without confusing the server.
102 t = transport.SvnRaTransport(url=url)
102 t = transport.SvnRaTransport(url=url)
103 svn.ra.get_log(t.ra, paths, start, end, limit,
103 svn.ra.get_log(t.ra, paths, start, end, limit,
104 discover_changed_paths,
104 discover_changed_paths,
105 strict_node_history,
105 strict_node_history,
106 receiver)
106 receiver)
107 except IOError:
107 except IOError:
108 # Caller may interrupt the iteration
108 # Caller may interrupt the iteration
109 pickle.dump(None, fp, protocol)
109 pickle.dump(None, fp, protocol)
110 except Exception, inst:
110 except Exception, inst:
111 pickle.dump(str(inst), fp, protocol)
111 pickle.dump(str(inst), fp, protocol)
112 else:
112 else:
113 pickle.dump(None, fp, protocol)
113 pickle.dump(None, fp, protocol)
114 fp.close()
114 fp.close()
115 # With large history, cleanup process goes crazy and suddenly
115 # With large history, cleanup process goes crazy and suddenly
116 # consumes *huge* amount of memory. The output file being closed,
116 # consumes *huge* amount of memory. The output file being closed,
117 # there is no need for clean termination.
117 # there is no need for clean termination.
118 os._exit(0)
118 os._exit(0)
119
119
120 def debugsvnlog(ui, **opts):
120 def debugsvnlog(ui, **opts):
121 """Fetch SVN log in a subprocess and channel them back to parent to
121 """Fetch SVN log in a subprocess and channel them back to parent to
122 avoid memory collection issues.
122 avoid memory collection issues.
123 """
123 """
124 if svn is None:
124 if svn is None:
125 raise util.Abort(_('debugsvnlog could not load Subversion python '
125 raise util.Abort(_('debugsvnlog could not load Subversion python '
126 'bindings'))
126 'bindings'))
127
127
128 util.setbinary(sys.stdin)
128 util.setbinary(sys.stdin)
129 util.setbinary(sys.stdout)
129 util.setbinary(sys.stdout)
130 args = decodeargs(sys.stdin.read())
130 args = decodeargs(sys.stdin.read())
131 get_log_child(sys.stdout, *args)
131 get_log_child(sys.stdout, *args)
132
132
133 class logstream(object):
133 class logstream(object):
134 """Interruptible revision log iterator."""
134 """Interruptible revision log iterator."""
135 def __init__(self, stdout):
135 def __init__(self, stdout):
136 self._stdout = stdout
136 self._stdout = stdout
137
137
138 def __iter__(self):
138 def __iter__(self):
139 while True:
139 while True:
140 try:
140 try:
141 entry = pickle.load(self._stdout)
141 entry = pickle.load(self._stdout)
142 except EOFError:
142 except EOFError:
143 raise util.Abort(_('Mercurial failed to run itself, check'
143 raise util.Abort(_('Mercurial failed to run itself, check'
144 ' hg executable is in PATH'))
144 ' hg executable is in PATH'))
145 try:
145 try:
146 orig_paths, revnum, author, date, message = entry
146 orig_paths, revnum, author, date, message = entry
147 except (TypeError, ValueError):
147 except (TypeError, ValueError):
148 if entry is None:
148 if entry is None:
149 break
149 break
150 raise util.Abort(_("log stream exception '%s'") % entry)
150 raise util.Abort(_("log stream exception '%s'") % entry)
151 yield entry
151 yield entry
152
152
153 def close(self):
153 def close(self):
154 if self._stdout:
154 if self._stdout:
155 self._stdout.close()
155 self._stdout.close()
156 self._stdout = None
156 self._stdout = None
157
157
158
158
159 # Check to see if the given path is a local Subversion repo. Verify this by
159 # Check to see if the given path is a local Subversion repo. Verify this by
160 # looking for several svn-specific files and directories in the given
160 # looking for several svn-specific files and directories in the given
161 # directory.
161 # directory.
162 def filecheck(ui, path, proto):
162 def filecheck(ui, path, proto):
163 for x in ('locks', 'hooks', 'format', 'db'):
163 for x in ('locks', 'hooks', 'format', 'db'):
164 if not os.path.exists(os.path.join(path, x)):
164 if not os.path.exists(os.path.join(path, x)):
165 return False
165 return False
166 return True
166 return True
167
167
168 # Check to see if a given path is the root of an svn repo over http. We verify
168 # Check to see if a given path is the root of an svn repo over http. We verify
169 # this by requesting a version-controlled URL we know can't exist and looking
169 # this by requesting a version-controlled URL we know can't exist and looking
170 # for the svn-specific "not found" XML.
170 # for the svn-specific "not found" XML.
171 def httpcheck(ui, path, proto):
171 def httpcheck(ui, path, proto):
172 try:
172 try:
173 opener = urllib2.build_opener()
173 opener = urllib2.build_opener()
174 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
174 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
175 data = rsp.read()
175 data = rsp.read()
176 except urllib2.HTTPError, inst:
176 except urllib2.HTTPError, inst:
177 if inst.code != 404:
177 if inst.code != 404:
178 # Except for 404 we cannot know for sure this is not an svn repo
178 # Except for 404 we cannot know for sure this is not an svn repo
179 ui.warn(_('svn: cannot probe remote repository, assume it could '
179 ui.warn(_('svn: cannot probe remote repository, assume it could '
180 'be a subversion repository. Use --source-type if you '
180 'be a subversion repository. Use --source-type if you '
181 'know better.\n'))
181 'know better.\n'))
182 return True
182 return True
183 data = inst.fp.read()
183 data = inst.fp.read()
184 except Exception:
184 except Exception:
185 # Could be urllib2.URLError if the URL is invalid or anything else.
185 # Could be urllib2.URLError if the URL is invalid or anything else.
186 return False
186 return False
187 return '<m:human-readable errcode="160013">' in data
187 return '<m:human-readable errcode="160013">' in data
188
188
189 protomap = {'http': httpcheck,
189 protomap = {'http': httpcheck,
190 'https': httpcheck,
190 'https': httpcheck,
191 'file': filecheck,
191 'file': filecheck,
192 }
192 }
193 def issvnurl(ui, url):
193 def issvnurl(ui, url):
194 try:
194 try:
195 proto, path = url.split('://', 1)
195 proto, path = url.split('://', 1)
196 if proto == 'file':
196 if proto == 'file':
197 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
197 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
198 and path[2:6].lower() == '%3a/'):
198 and path[2:6].lower() == '%3a/'):
199 path = path[:2] + ':/' + path[6:]
199 path = path[:2] + ':/' + path[6:]
200 path = urllib.url2pathname(path)
200 path = urllib.url2pathname(path)
201 except ValueError:
201 except ValueError:
202 proto = 'file'
202 proto = 'file'
203 path = os.path.abspath(url)
203 path = os.path.abspath(url)
204 if proto == 'file':
204 if proto == 'file':
205 path = util.pconvert(path)
205 path = util.pconvert(path)
206 check = protomap.get(proto, lambda *args: False)
206 check = protomap.get(proto, lambda *args: False)
207 while '/' in path:
207 while '/' in path:
208 if check(ui, path, proto):
208 if check(ui, path, proto):
209 return True
209 return True
210 path = path.rsplit('/', 1)[0]
210 path = path.rsplit('/', 1)[0]
211 return False
211 return False
212
212
213 # SVN conversion code stolen from bzr-svn and tailor
213 # SVN conversion code stolen from bzr-svn and tailor
214 #
214 #
215 # Subversion looks like a versioned filesystem, branches structures
215 # Subversion looks like a versioned filesystem, branches structures
216 # are defined by conventions and not enforced by the tool. First,
216 # are defined by conventions and not enforced by the tool. First,
217 # we define the potential branches (modules) as "trunk" and "branches"
217 # we define the potential branches (modules) as "trunk" and "branches"
218 # children directories. Revisions are then identified by their
218 # children directories. Revisions are then identified by their
219 # module and revision number (and a repository identifier).
219 # module and revision number (and a repository identifier).
220 #
220 #
221 # The revision graph is really a tree (or a forest). By default, a
221 # The revision graph is really a tree (or a forest). By default, a
222 # revision parent is the previous revision in the same module. If the
222 # revision parent is the previous revision in the same module. If the
223 # module directory is copied/moved from another module then the
223 # module directory is copied/moved from another module then the
224 # revision is the module root and its parent the source revision in
224 # revision is the module root and its parent the source revision in
225 # the parent module. A revision has at most one parent.
225 # the parent module. A revision has at most one parent.
226 #
226 #
227 class svn_source(converter_source):
227 class svn_source(converter_source):
228 def __init__(self, ui, url, rev=None):
228 def __init__(self, ui, url, rev=None):
229 super(svn_source, self).__init__(ui, url, rev=rev)
229 super(svn_source, self).__init__(ui, url, rev=rev)
230
230
231 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
231 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
232 (os.path.exists(url) and
232 (os.path.exists(url) and
233 os.path.exists(os.path.join(url, '.svn'))) or
233 os.path.exists(os.path.join(url, '.svn'))) or
234 issvnurl(ui, url)):
234 issvnurl(ui, url)):
235 raise NoRepo(_("%s does not look like a Subversion repository")
235 raise NoRepo(_("%s does not look like a Subversion repository")
236 % url)
236 % url)
237 if svn is None:
237 if svn is None:
238 raise MissingTool(_('could not load Subversion python bindings'))
238 raise MissingTool(_('could not load Subversion python bindings'))
239
239
240 try:
240 try:
241 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
241 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
242 if version < (1, 4):
242 if version < (1, 4):
243 raise MissingTool(_('Subversion python bindings %d.%d found, '
243 raise MissingTool(_('Subversion python bindings %d.%d found, '
244 '1.4 or later required') % version)
244 '1.4 or later required') % version)
245 except AttributeError:
245 except AttributeError:
246 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
246 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
247 'or later required'))
247 'or later required'))
248
248
249 self.lastrevs = {}
249 self.lastrevs = {}
250
250
251 latest = None
251 latest = None
252 try:
252 try:
253 # Support file://path@rev syntax. Useful e.g. to convert
253 # Support file://path@rev syntax. Useful e.g. to convert
254 # deleted branches.
254 # deleted branches.
255 at = url.rfind('@')
255 at = url.rfind('@')
256 if at >= 0:
256 if at >= 0:
257 latest = int(url[at + 1:])
257 latest = int(url[at + 1:])
258 url = url[:at]
258 url = url[:at]
259 except ValueError:
259 except ValueError:
260 pass
260 pass
261 self.url = geturl(url)
261 self.url = geturl(url)
262 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
262 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
263 try:
263 try:
264 self.transport = transport.SvnRaTransport(url=self.url)
264 self.transport = transport.SvnRaTransport(url=self.url)
265 self.ra = self.transport.ra
265 self.ra = self.transport.ra
266 self.ctx = self.transport.client
266 self.ctx = self.transport.client
267 self.baseurl = svn.ra.get_repos_root(self.ra)
267 self.baseurl = svn.ra.get_repos_root(self.ra)
268 # Module is either empty or a repository path starting with
268 # Module is either empty or a repository path starting with
269 # a slash and not ending with a slash.
269 # a slash and not ending with a slash.
270 self.module = urllib.unquote(self.url[len(self.baseurl):])
270 self.module = urllib.unquote(self.url[len(self.baseurl):])
271 self.prevmodule = None
271 self.prevmodule = None
272 self.rootmodule = self.module
272 self.rootmodule = self.module
273 self.commits = {}
273 self.commits = {}
274 self.paths = {}
274 self.paths = {}
275 self.uuid = svn.ra.get_uuid(self.ra)
275 self.uuid = svn.ra.get_uuid(self.ra)
276 except SubversionException:
276 except SubversionException:
277 ui.traceback()
277 ui.traceback()
278 raise NoRepo(_("%s does not look like a Subversion repository")
278 raise NoRepo(_("%s does not look like a Subversion repository")
279 % self.url)
279 % self.url)
280
280
281 if rev:
281 if rev:
282 try:
282 try:
283 latest = int(rev)
283 latest = int(rev)
284 except ValueError:
284 except ValueError:
285 raise util.Abort(_('svn: revision %s is not an integer') % rev)
285 raise util.Abort(_('svn: revision %s is not an integer') % rev)
286
286
287 self.trunkname = self.ui.config('convert', 'svn.trunk',
287 self.trunkname = self.ui.config('convert', 'svn.trunk',
288 'trunk').strip('/')
288 'trunk').strip('/')
289 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
289 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
290 try:
290 try:
291 self.startrev = int(self.startrev)
291 self.startrev = int(self.startrev)
292 if self.startrev < 0:
292 if self.startrev < 0:
293 self.startrev = 0
293 self.startrev = 0
294 except ValueError:
294 except ValueError:
295 raise util.Abort(_('svn: start revision %s is not an integer')
295 raise util.Abort(_('svn: start revision %s is not an integer')
296 % self.startrev)
296 % self.startrev)
297
297
298 try:
298 try:
299 self.head = self.latest(self.module, latest)
299 self.head = self.latest(self.module, latest)
300 except SvnPathNotFound:
300 except SvnPathNotFound:
301 self.head = None
301 self.head = None
302 if not self.head:
302 if not self.head:
303 raise util.Abort(_('no revision found in module %s')
303 raise util.Abort(_('no revision found in module %s')
304 % self.module)
304 % self.module)
305 self.last_changed = self.revnum(self.head)
305 self.last_changed = self.revnum(self.head)
306
306
307 self._changescache = None
307 self._changescache = None
308
308
309 if os.path.exists(os.path.join(url, '.svn/entries')):
309 if os.path.exists(os.path.join(url, '.svn/entries')):
310 self.wc = url
310 self.wc = url
311 else:
311 else:
312 self.wc = None
312 self.wc = None
313 self.convertfp = None
313 self.convertfp = None
314
314
315 def setrevmap(self, revmap):
315 def setrevmap(self, revmap):
316 lastrevs = {}
316 lastrevs = {}
317 for revid in revmap.iterkeys():
317 for revid in revmap.iterkeys():
318 uuid, module, revnum = revsplit(revid)
318 uuid, module, revnum = revsplit(revid)
319 lastrevnum = lastrevs.setdefault(module, revnum)
319 lastrevnum = lastrevs.setdefault(module, revnum)
320 if revnum > lastrevnum:
320 if revnum > lastrevnum:
321 lastrevs[module] = revnum
321 lastrevs[module] = revnum
322 self.lastrevs = lastrevs
322 self.lastrevs = lastrevs
323
323
324 def exists(self, path, optrev):
324 def exists(self, path, optrev):
325 try:
325 try:
326 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
326 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
327 optrev, False, self.ctx)
327 optrev, False, self.ctx)
328 return True
328 return True
329 except SubversionException:
329 except SubversionException:
330 return False
330 return False
331
331
332 def getheads(self):
332 def getheads(self):
333
333
334 def isdir(path, revnum):
334 def isdir(path, revnum):
335 kind = self._checkpath(path, revnum)
335 kind = self._checkpath(path, revnum)
336 return kind == svn.core.svn_node_dir
336 return kind == svn.core.svn_node_dir
337
337
338 def getcfgpath(name, rev):
338 def getcfgpath(name, rev):
339 cfgpath = self.ui.config('convert', 'svn.' + name)
339 cfgpath = self.ui.config('convert', 'svn.' + name)
340 if cfgpath is not None and cfgpath.strip() == '':
340 if cfgpath is not None and cfgpath.strip() == '':
341 return None
341 return None
342 path = (cfgpath or name).strip('/')
342 path = (cfgpath or name).strip('/')
343 if not self.exists(path, rev):
343 if not self.exists(path, rev):
344 if self.module.endswith(path) and name == 'trunk':
344 if self.module.endswith(path) and name == 'trunk':
345 # we are converting from inside this directory
345 # we are converting from inside this directory
346 return None
346 return None
347 if cfgpath:
347 if cfgpath:
348 raise util.Abort(_('expected %s to be at %r, but not found')
348 raise util.Abort(_('expected %s to be at %r, but not found')
349 % (name, path))
349 % (name, path))
350 return None
350 return None
351 self.ui.note(_('found %s at %r\n') % (name, path))
351 self.ui.note(_('found %s at %r\n') % (name, path))
352 return path
352 return path
353
353
354 rev = optrev(self.last_changed)
354 rev = optrev(self.last_changed)
355 oldmodule = ''
355 oldmodule = ''
356 trunk = getcfgpath('trunk', rev)
356 trunk = getcfgpath('trunk', rev)
357 self.tags = getcfgpath('tags', rev)
357 self.tags = getcfgpath('tags', rev)
358 branches = getcfgpath('branches', rev)
358 branches = getcfgpath('branches', rev)
359
359
360 # If the project has a trunk or branches, we will extract heads
360 # If the project has a trunk or branches, we will extract heads
361 # from them. We keep the project root otherwise.
361 # from them. We keep the project root otherwise.
362 if trunk:
362 if trunk:
363 oldmodule = self.module or ''
363 oldmodule = self.module or ''
364 self.module += '/' + trunk
364 self.module += '/' + trunk
365 self.head = self.latest(self.module, self.last_changed)
365 self.head = self.latest(self.module, self.last_changed)
366 if not self.head:
366 if not self.head:
367 raise util.Abort(_('no revision found in module %s')
367 raise util.Abort(_('no revision found in module %s')
368 % self.module)
368 % self.module)
369
369
370 # First head in the list is the module's head
370 # First head in the list is the module's head
371 self.heads = [self.head]
371 self.heads = [self.head]
372 if self.tags is not None:
372 if self.tags is not None:
373 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
373 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
374
374
375 # Check if branches bring a few more heads to the list
375 # Check if branches bring a few more heads to the list
376 if branches:
376 if branches:
377 rpath = self.url.strip('/')
377 rpath = self.url.strip('/')
378 branchnames = svn.client.ls(rpath + '/' + quote(branches),
378 branchnames = svn.client.ls(rpath + '/' + quote(branches),
379 rev, False, self.ctx)
379 rev, False, self.ctx)
380 for branch in sorted(branchnames):
380 for branch in sorted(branchnames):
381 module = '%s/%s/%s' % (oldmodule, branches, branch)
381 module = '%s/%s/%s' % (oldmodule, branches, branch)
382 if not isdir(module, self.last_changed):
382 if not isdir(module, self.last_changed):
383 continue
383 continue
384 brevid = self.latest(module, self.last_changed)
384 brevid = self.latest(module, self.last_changed)
385 if not brevid:
385 if not brevid:
386 self.ui.note(_('ignoring empty branch %s\n') % branch)
386 self.ui.note(_('ignoring empty branch %s\n') % branch)
387 continue
387 continue
388 self.ui.note(_('found branch %s at %d\n') %
388 self.ui.note(_('found branch %s at %d\n') %
389 (branch, self.revnum(brevid)))
389 (branch, self.revnum(brevid)))
390 self.heads.append(brevid)
390 self.heads.append(brevid)
391
391
392 if self.startrev and self.heads:
392 if self.startrev and self.heads:
393 if len(self.heads) > 1:
393 if len(self.heads) > 1:
394 raise util.Abort(_('svn: start revision is not supported '
394 raise util.Abort(_('svn: start revision is not supported '
395 'with more than one branch'))
395 'with more than one branch'))
396 revnum = self.revnum(self.heads[0])
396 revnum = self.revnum(self.heads[0])
397 if revnum < self.startrev:
397 if revnum < self.startrev:
398 raise util.Abort(
398 raise util.Abort(
399 _('svn: no revision found after start revision %d')
399 _('svn: no revision found after start revision %d')
400 % self.startrev)
400 % self.startrev)
401
401
402 return self.heads
402 return self.heads
403
403
404 def getchanges(self, rev):
404 def getchanges(self, rev):
405 if self._changescache and self._changescache[0] == rev:
405 if self._changescache and self._changescache[0] == rev:
406 return self._changescache[1]
406 return self._changescache[1]
407 self._changescache = None
407 self._changescache = None
408 (paths, parents) = self.paths[rev]
408 (paths, parents) = self.paths[rev]
409 if parents:
409 if parents:
410 files, self.removed, copies = self.expandpaths(rev, paths, parents)
410 files, self.removed, copies = self.expandpaths(rev, paths, parents)
411 else:
411 else:
412 # Perform a full checkout on roots
412 # Perform a full checkout on roots
413 uuid, module, revnum = revsplit(rev)
413 uuid, module, revnum = revsplit(rev)
414 entries = svn.client.ls(self.baseurl + quote(module),
414 entries = svn.client.ls(self.baseurl + quote(module),
415 optrev(revnum), True, self.ctx)
415 optrev(revnum), True, self.ctx)
416 files = [n for n, e in entries.iteritems()
416 files = [n for n, e in entries.iteritems()
417 if e.kind == svn.core.svn_node_file]
417 if e.kind == svn.core.svn_node_file]
418 copies = {}
418 copies = {}
419 self.removed = set()
419 self.removed = set()
420
420
421 files.sort()
421 files.sort()
422 files = zip(files, [rev] * len(files))
422 files = zip(files, [rev] * len(files))
423
423
424 # caller caches the result, so free it here to release memory
424 # caller caches the result, so free it here to release memory
425 del self.paths[rev]
425 del self.paths[rev]
426 return (files, copies)
426 return (files, copies)
427
427
428 def getchangedfiles(self, rev, i):
428 def getchangedfiles(self, rev, i):
429 changes = self.getchanges(rev)
429 changes = self.getchanges(rev)
430 self._changescache = (rev, changes)
430 self._changescache = (rev, changes)
431 return [f[0] for f in changes[0]]
431 return [f[0] for f in changes[0]]
432
432
433 def getcommit(self, rev):
433 def getcommit(self, rev):
434 if rev not in self.commits:
434 if rev not in self.commits:
435 uuid, module, revnum = revsplit(rev)
435 uuid, module, revnum = revsplit(rev)
436 self.module = module
436 self.module = module
437 self.reparent(module)
437 self.reparent(module)
438 # We assume that:
438 # We assume that:
439 # - requests for revisions after "stop" come from the
439 # - requests for revisions after "stop" come from the
440 # revision graph backward traversal. Cache all of them
440 # revision graph backward traversal. Cache all of them
441 # down to stop, they will be used eventually.
441 # down to stop, they will be used eventually.
442 # - requests for revisions before "stop" come to get
442 # - requests for revisions before "stop" come to get
443 # isolated branches parents. Just fetch what is needed.
443 # isolated branches parents. Just fetch what is needed.
444 stop = self.lastrevs.get(module, 0)
444 stop = self.lastrevs.get(module, 0)
445 if revnum < stop:
445 if revnum < stop:
446 stop = revnum + 1
446 stop = revnum + 1
447 self._fetch_revisions(revnum, stop)
447 self._fetch_revisions(revnum, stop)
448 if rev not in self.commits:
448 if rev not in self.commits:
449 raise util.Abort(_('svn: revision %s not found') % revnum)
449 raise util.Abort(_('svn: revision %s not found') % revnum)
450 commit = self.commits[rev]
450 commit = self.commits[rev]
451 # caller caches the result, so free it here to release memory
451 # caller caches the result, so free it here to release memory
452 del self.commits[rev]
452 del self.commits[rev]
453 return commit
453 return commit
454
454
455 def checkrevformat(self, revstr):
456 """ fails if revision format does not match the correct format"""
457 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
458 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
459 '{12,12}(.*)\@[0-9]+$',revstr):
460 raise util.Abort(_('splicemap entry %s is not a valid revision'
461 ' identifier') % revstr)
462
455 def gettags(self):
463 def gettags(self):
456 tags = {}
464 tags = {}
457 if self.tags is None:
465 if self.tags is None:
458 return tags
466 return tags
459
467
460 # svn tags are just a convention, project branches left in a
468 # svn tags are just a convention, project branches left in a
461 # 'tags' directory. There is no other relationship than
469 # 'tags' directory. There is no other relationship than
462 # ancestry, which is expensive to discover and makes them hard
470 # ancestry, which is expensive to discover and makes them hard
463 # to update incrementally. Worse, past revisions may be
471 # to update incrementally. Worse, past revisions may be
464 # referenced by tags far away in the future, requiring a deep
472 # referenced by tags far away in the future, requiring a deep
465 # history traversal on every calculation. Current code
473 # history traversal on every calculation. Current code
466 # performs a single backward traversal, tracking moves within
474 # performs a single backward traversal, tracking moves within
467 # the tags directory (tag renaming) and recording a new tag
475 # the tags directory (tag renaming) and recording a new tag
468 # everytime a project is copied from outside the tags
476 # everytime a project is copied from outside the tags
469 # directory. It also lists deleted tags, this behaviour may
477 # directory. It also lists deleted tags, this behaviour may
470 # change in the future.
478 # change in the future.
471 pendings = []
479 pendings = []
472 tagspath = self.tags
480 tagspath = self.tags
473 start = svn.ra.get_latest_revnum(self.ra)
481 start = svn.ra.get_latest_revnum(self.ra)
474 stream = self._getlog([self.tags], start, self.startrev)
482 stream = self._getlog([self.tags], start, self.startrev)
475 try:
483 try:
476 for entry in stream:
484 for entry in stream:
477 origpaths, revnum, author, date, message = entry
485 origpaths, revnum, author, date, message = entry
478 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
486 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
479 in origpaths.iteritems() if e.copyfrom_path]
487 in origpaths.iteritems() if e.copyfrom_path]
480 # Apply moves/copies from more specific to general
488 # Apply moves/copies from more specific to general
481 copies.sort(reverse=True)
489 copies.sort(reverse=True)
482
490
483 srctagspath = tagspath
491 srctagspath = tagspath
484 if copies and copies[-1][2] == tagspath:
492 if copies and copies[-1][2] == tagspath:
485 # Track tags directory moves
493 # Track tags directory moves
486 srctagspath = copies.pop()[0]
494 srctagspath = copies.pop()[0]
487
495
488 for source, sourcerev, dest in copies:
496 for source, sourcerev, dest in copies:
489 if not dest.startswith(tagspath + '/'):
497 if not dest.startswith(tagspath + '/'):
490 continue
498 continue
491 for tag in pendings:
499 for tag in pendings:
492 if tag[0].startswith(dest):
500 if tag[0].startswith(dest):
493 tagpath = source + tag[0][len(dest):]
501 tagpath = source + tag[0][len(dest):]
494 tag[:2] = [tagpath, sourcerev]
502 tag[:2] = [tagpath, sourcerev]
495 break
503 break
496 else:
504 else:
497 pendings.append([source, sourcerev, dest])
505 pendings.append([source, sourcerev, dest])
498
506
499 # Filter out tags with children coming from different
507 # Filter out tags with children coming from different
500 # parts of the repository like:
508 # parts of the repository like:
501 # /tags/tag.1 (from /trunk:10)
509 # /tags/tag.1 (from /trunk:10)
502 # /tags/tag.1/foo (from /branches/foo:12)
510 # /tags/tag.1/foo (from /branches/foo:12)
503 # Here/tags/tag.1 discarded as well as its children.
511 # Here/tags/tag.1 discarded as well as its children.
504 # It happens with tools like cvs2svn. Such tags cannot
512 # It happens with tools like cvs2svn. Such tags cannot
505 # be represented in mercurial.
513 # be represented in mercurial.
506 addeds = dict((p, e.copyfrom_path) for p, e
514 addeds = dict((p, e.copyfrom_path) for p, e
507 in origpaths.iteritems()
515 in origpaths.iteritems()
508 if e.action == 'A' and e.copyfrom_path)
516 if e.action == 'A' and e.copyfrom_path)
509 badroots = set()
517 badroots = set()
510 for destroot in addeds:
518 for destroot in addeds:
511 for source, sourcerev, dest in pendings:
519 for source, sourcerev, dest in pendings:
512 if (not dest.startswith(destroot + '/')
520 if (not dest.startswith(destroot + '/')
513 or source.startswith(addeds[destroot] + '/')):
521 or source.startswith(addeds[destroot] + '/')):
514 continue
522 continue
515 badroots.add(destroot)
523 badroots.add(destroot)
516 break
524 break
517
525
518 for badroot in badroots:
526 for badroot in badroots:
519 pendings = [p for p in pendings if p[2] != badroot
527 pendings = [p for p in pendings if p[2] != badroot
520 and not p[2].startswith(badroot + '/')]
528 and not p[2].startswith(badroot + '/')]
521
529
522 # Tell tag renamings from tag creations
530 # Tell tag renamings from tag creations
523 renamings = []
531 renamings = []
524 for source, sourcerev, dest in pendings:
532 for source, sourcerev, dest in pendings:
525 tagname = dest.split('/')[-1]
533 tagname = dest.split('/')[-1]
526 if source.startswith(srctagspath):
534 if source.startswith(srctagspath):
527 renamings.append([source, sourcerev, tagname])
535 renamings.append([source, sourcerev, tagname])
528 continue
536 continue
529 if tagname in tags:
537 if tagname in tags:
530 # Keep the latest tag value
538 # Keep the latest tag value
531 continue
539 continue
532 # From revision may be fake, get one with changes
540 # From revision may be fake, get one with changes
533 try:
541 try:
534 tagid = self.latest(source, sourcerev)
542 tagid = self.latest(source, sourcerev)
535 if tagid and tagname not in tags:
543 if tagid and tagname not in tags:
536 tags[tagname] = tagid
544 tags[tagname] = tagid
537 except SvnPathNotFound:
545 except SvnPathNotFound:
538 # It happens when we are following directories
546 # It happens when we are following directories
539 # we assumed were copied with their parents
547 # we assumed were copied with their parents
540 # but were really created in the tag
548 # but were really created in the tag
541 # directory.
549 # directory.
542 pass
550 pass
543 pendings = renamings
551 pendings = renamings
544 tagspath = srctagspath
552 tagspath = srctagspath
545 finally:
553 finally:
546 stream.close()
554 stream.close()
547 return tags
555 return tags
548
556
549 def converted(self, rev, destrev):
557 def converted(self, rev, destrev):
550 if not self.wc:
558 if not self.wc:
551 return
559 return
552 if self.convertfp is None:
560 if self.convertfp is None:
553 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
561 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
554 'a')
562 'a')
555 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
563 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
556 self.convertfp.flush()
564 self.convertfp.flush()
557
565
558 def revid(self, revnum, module=None):
566 def revid(self, revnum, module=None):
559 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
567 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
560
568
561 def revnum(self, rev):
569 def revnum(self, rev):
562 return int(rev.split('@')[-1])
570 return int(rev.split('@')[-1])
563
571
564 def latest(self, path, stop=None):
572 def latest(self, path, stop=None):
565 """Find the latest revid affecting path, up to stop revision
573 """Find the latest revid affecting path, up to stop revision
566 number. If stop is None, default to repository latest
574 number. If stop is None, default to repository latest
567 revision. It may return a revision in a different module,
575 revision. It may return a revision in a different module,
568 since a branch may be moved without a change being
576 since a branch may be moved without a change being
569 reported. Return None if computed module does not belong to
577 reported. Return None if computed module does not belong to
570 rootmodule subtree.
578 rootmodule subtree.
571 """
579 """
572 def findchanges(path, start, stop=None):
580 def findchanges(path, start, stop=None):
573 stream = self._getlog([path], start, stop or 1)
581 stream = self._getlog([path], start, stop or 1)
574 try:
582 try:
575 for entry in stream:
583 for entry in stream:
576 paths, revnum, author, date, message = entry
584 paths, revnum, author, date, message = entry
577 if stop is None and paths:
585 if stop is None and paths:
578 # We do not know the latest changed revision,
586 # We do not know the latest changed revision,
579 # keep the first one with changed paths.
587 # keep the first one with changed paths.
580 break
588 break
581 if revnum <= stop:
589 if revnum <= stop:
582 break
590 break
583
591
584 for p in paths:
592 for p in paths:
585 if (not path.startswith(p) or
593 if (not path.startswith(p) or
586 not paths[p].copyfrom_path):
594 not paths[p].copyfrom_path):
587 continue
595 continue
588 newpath = paths[p].copyfrom_path + path[len(p):]
596 newpath = paths[p].copyfrom_path + path[len(p):]
589 self.ui.debug("branch renamed from %s to %s at %d\n" %
597 self.ui.debug("branch renamed from %s to %s at %d\n" %
590 (path, newpath, revnum))
598 (path, newpath, revnum))
591 path = newpath
599 path = newpath
592 break
600 break
593 if not paths:
601 if not paths:
594 revnum = None
602 revnum = None
595 return revnum, path
603 return revnum, path
596 finally:
604 finally:
597 stream.close()
605 stream.close()
598
606
599 if not path.startswith(self.rootmodule):
607 if not path.startswith(self.rootmodule):
600 # Requests on foreign branches may be forbidden at server level
608 # Requests on foreign branches may be forbidden at server level
601 self.ui.debug('ignoring foreign branch %r\n' % path)
609 self.ui.debug('ignoring foreign branch %r\n' % path)
602 return None
610 return None
603
611
604 if stop is None:
612 if stop is None:
605 stop = svn.ra.get_latest_revnum(self.ra)
613 stop = svn.ra.get_latest_revnum(self.ra)
606 try:
614 try:
607 prevmodule = self.reparent('')
615 prevmodule = self.reparent('')
608 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
616 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
609 self.reparent(prevmodule)
617 self.reparent(prevmodule)
610 except SubversionException:
618 except SubversionException:
611 dirent = None
619 dirent = None
612 if not dirent:
620 if not dirent:
613 raise SvnPathNotFound(_('%s not found up to revision %d')
621 raise SvnPathNotFound(_('%s not found up to revision %d')
614 % (path, stop))
622 % (path, stop))
615
623
616 # stat() gives us the previous revision on this line of
624 # stat() gives us the previous revision on this line of
617 # development, but it might be in *another module*. Fetch the
625 # development, but it might be in *another module*. Fetch the
618 # log and detect renames down to the latest revision.
626 # log and detect renames down to the latest revision.
619 revnum, realpath = findchanges(path, stop, dirent.created_rev)
627 revnum, realpath = findchanges(path, stop, dirent.created_rev)
620 if revnum is None:
628 if revnum is None:
621 # Tools like svnsync can create empty revision, when
629 # Tools like svnsync can create empty revision, when
622 # synchronizing only a subtree for instance. These empty
630 # synchronizing only a subtree for instance. These empty
623 # revisions created_rev still have their original values
631 # revisions created_rev still have their original values
624 # despite all changes having disappeared and can be
632 # despite all changes having disappeared and can be
625 # returned by ra.stat(), at least when stating the root
633 # returned by ra.stat(), at least when stating the root
626 # module. In that case, do not trust created_rev and scan
634 # module. In that case, do not trust created_rev and scan
627 # the whole history.
635 # the whole history.
628 revnum, realpath = findchanges(path, stop)
636 revnum, realpath = findchanges(path, stop)
629 if revnum is None:
637 if revnum is None:
630 self.ui.debug('ignoring empty branch %r\n' % realpath)
638 self.ui.debug('ignoring empty branch %r\n' % realpath)
631 return None
639 return None
632
640
633 if not realpath.startswith(self.rootmodule):
641 if not realpath.startswith(self.rootmodule):
634 self.ui.debug('ignoring foreign branch %r\n' % realpath)
642 self.ui.debug('ignoring foreign branch %r\n' % realpath)
635 return None
643 return None
636 return self.revid(revnum, realpath)
644 return self.revid(revnum, realpath)
637
645
638 def reparent(self, module):
646 def reparent(self, module):
639 """Reparent the svn transport and return the previous parent."""
647 """Reparent the svn transport and return the previous parent."""
640 if self.prevmodule == module:
648 if self.prevmodule == module:
641 return module
649 return module
642 svnurl = self.baseurl + quote(module)
650 svnurl = self.baseurl + quote(module)
643 prevmodule = self.prevmodule
651 prevmodule = self.prevmodule
644 if prevmodule is None:
652 if prevmodule is None:
645 prevmodule = ''
653 prevmodule = ''
646 self.ui.debug("reparent to %s\n" % svnurl)
654 self.ui.debug("reparent to %s\n" % svnurl)
647 svn.ra.reparent(self.ra, svnurl)
655 svn.ra.reparent(self.ra, svnurl)
648 self.prevmodule = module
656 self.prevmodule = module
649 return prevmodule
657 return prevmodule
650
658
651 def expandpaths(self, rev, paths, parents):
659 def expandpaths(self, rev, paths, parents):
652 changed, removed = set(), set()
660 changed, removed = set(), set()
653 copies = {}
661 copies = {}
654
662
655 new_module, revnum = revsplit(rev)[1:]
663 new_module, revnum = revsplit(rev)[1:]
656 if new_module != self.module:
664 if new_module != self.module:
657 self.module = new_module
665 self.module = new_module
658 self.reparent(self.module)
666 self.reparent(self.module)
659
667
660 for i, (path, ent) in enumerate(paths):
668 for i, (path, ent) in enumerate(paths):
661 self.ui.progress(_('scanning paths'), i, item=path,
669 self.ui.progress(_('scanning paths'), i, item=path,
662 total=len(paths))
670 total=len(paths))
663 entrypath = self.getrelpath(path)
671 entrypath = self.getrelpath(path)
664
672
665 kind = self._checkpath(entrypath, revnum)
673 kind = self._checkpath(entrypath, revnum)
666 if kind == svn.core.svn_node_file:
674 if kind == svn.core.svn_node_file:
667 changed.add(self.recode(entrypath))
675 changed.add(self.recode(entrypath))
668 if not ent.copyfrom_path or not parents:
676 if not ent.copyfrom_path or not parents:
669 continue
677 continue
670 # Copy sources not in parent revisions cannot be
678 # Copy sources not in parent revisions cannot be
671 # represented, ignore their origin for now
679 # represented, ignore their origin for now
672 pmodule, prevnum = revsplit(parents[0])[1:]
680 pmodule, prevnum = revsplit(parents[0])[1:]
673 if ent.copyfrom_rev < prevnum:
681 if ent.copyfrom_rev < prevnum:
674 continue
682 continue
675 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
683 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
676 if not copyfrom_path:
684 if not copyfrom_path:
677 continue
685 continue
678 self.ui.debug("copied to %s from %s@%s\n" %
686 self.ui.debug("copied to %s from %s@%s\n" %
679 (entrypath, copyfrom_path, ent.copyfrom_rev))
687 (entrypath, copyfrom_path, ent.copyfrom_rev))
680 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
688 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
681 elif kind == 0: # gone, but had better be a deleted *file*
689 elif kind == 0: # gone, but had better be a deleted *file*
682 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
690 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
683 pmodule, prevnum = revsplit(parents[0])[1:]
691 pmodule, prevnum = revsplit(parents[0])[1:]
684 parentpath = pmodule + "/" + entrypath
692 parentpath = pmodule + "/" + entrypath
685 fromkind = self._checkpath(entrypath, prevnum, pmodule)
693 fromkind = self._checkpath(entrypath, prevnum, pmodule)
686
694
687 if fromkind == svn.core.svn_node_file:
695 if fromkind == svn.core.svn_node_file:
688 removed.add(self.recode(entrypath))
696 removed.add(self.recode(entrypath))
689 elif fromkind == svn.core.svn_node_dir:
697 elif fromkind == svn.core.svn_node_dir:
690 oroot = parentpath.strip('/')
698 oroot = parentpath.strip('/')
691 nroot = path.strip('/')
699 nroot = path.strip('/')
692 children = self._iterfiles(oroot, prevnum)
700 children = self._iterfiles(oroot, prevnum)
693 for childpath in children:
701 for childpath in children:
694 childpath = childpath.replace(oroot, nroot)
702 childpath = childpath.replace(oroot, nroot)
695 childpath = self.getrelpath("/" + childpath, pmodule)
703 childpath = self.getrelpath("/" + childpath, pmodule)
696 if childpath:
704 if childpath:
697 removed.add(self.recode(childpath))
705 removed.add(self.recode(childpath))
698 else:
706 else:
699 self.ui.debug('unknown path in revision %d: %s\n' % \
707 self.ui.debug('unknown path in revision %d: %s\n' % \
700 (revnum, path))
708 (revnum, path))
701 elif kind == svn.core.svn_node_dir:
709 elif kind == svn.core.svn_node_dir:
702 if ent.action == 'M':
710 if ent.action == 'M':
703 # If the directory just had a prop change,
711 # If the directory just had a prop change,
704 # then we shouldn't need to look for its children.
712 # then we shouldn't need to look for its children.
705 continue
713 continue
706 if ent.action == 'R' and parents:
714 if ent.action == 'R' and parents:
707 # If a directory is replacing a file, mark the previous
715 # If a directory is replacing a file, mark the previous
708 # file as deleted
716 # file as deleted
709 pmodule, prevnum = revsplit(parents[0])[1:]
717 pmodule, prevnum = revsplit(parents[0])[1:]
710 pkind = self._checkpath(entrypath, prevnum, pmodule)
718 pkind = self._checkpath(entrypath, prevnum, pmodule)
711 if pkind == svn.core.svn_node_file:
719 if pkind == svn.core.svn_node_file:
712 removed.add(self.recode(entrypath))
720 removed.add(self.recode(entrypath))
713 elif pkind == svn.core.svn_node_dir:
721 elif pkind == svn.core.svn_node_dir:
714 # We do not know what files were kept or removed,
722 # We do not know what files were kept or removed,
715 # mark them all as changed.
723 # mark them all as changed.
716 for childpath in self._iterfiles(pmodule, prevnum):
724 for childpath in self._iterfiles(pmodule, prevnum):
717 childpath = self.getrelpath("/" + childpath)
725 childpath = self.getrelpath("/" + childpath)
718 if childpath:
726 if childpath:
719 changed.add(self.recode(childpath))
727 changed.add(self.recode(childpath))
720
728
721 for childpath in self._iterfiles(path, revnum):
729 for childpath in self._iterfiles(path, revnum):
722 childpath = self.getrelpath("/" + childpath)
730 childpath = self.getrelpath("/" + childpath)
723 if childpath:
731 if childpath:
724 changed.add(self.recode(childpath))
732 changed.add(self.recode(childpath))
725
733
726 # Handle directory copies
734 # Handle directory copies
727 if not ent.copyfrom_path or not parents:
735 if not ent.copyfrom_path or not parents:
728 continue
736 continue
729 # Copy sources not in parent revisions cannot be
737 # Copy sources not in parent revisions cannot be
730 # represented, ignore their origin for now
738 # represented, ignore their origin for now
731 pmodule, prevnum = revsplit(parents[0])[1:]
739 pmodule, prevnum = revsplit(parents[0])[1:]
732 if ent.copyfrom_rev < prevnum:
740 if ent.copyfrom_rev < prevnum:
733 continue
741 continue
734 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
742 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
735 if not copyfrompath:
743 if not copyfrompath:
736 continue
744 continue
737 self.ui.debug("mark %s came from %s:%d\n"
745 self.ui.debug("mark %s came from %s:%d\n"
738 % (path, copyfrompath, ent.copyfrom_rev))
746 % (path, copyfrompath, ent.copyfrom_rev))
739 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
747 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
740 for childpath in children:
748 for childpath in children:
741 childpath = self.getrelpath("/" + childpath, pmodule)
749 childpath = self.getrelpath("/" + childpath, pmodule)
742 if not childpath:
750 if not childpath:
743 continue
751 continue
744 copytopath = path + childpath[len(copyfrompath):]
752 copytopath = path + childpath[len(copyfrompath):]
745 copytopath = self.getrelpath(copytopath)
753 copytopath = self.getrelpath(copytopath)
746 copies[self.recode(copytopath)] = self.recode(childpath)
754 copies[self.recode(copytopath)] = self.recode(childpath)
747
755
748 self.ui.progress(_('scanning paths'), None)
756 self.ui.progress(_('scanning paths'), None)
749 changed.update(removed)
757 changed.update(removed)
750 return (list(changed), removed, copies)
758 return (list(changed), removed, copies)
751
759
752 def _fetch_revisions(self, from_revnum, to_revnum):
760 def _fetch_revisions(self, from_revnum, to_revnum):
753 if from_revnum < to_revnum:
761 if from_revnum < to_revnum:
754 from_revnum, to_revnum = to_revnum, from_revnum
762 from_revnum, to_revnum = to_revnum, from_revnum
755
763
756 self.child_cset = None
764 self.child_cset = None
757
765
758 def parselogentry(orig_paths, revnum, author, date, message):
766 def parselogentry(orig_paths, revnum, author, date, message):
759 """Return the parsed commit object or None, and True if
767 """Return the parsed commit object or None, and True if
760 the revision is a branch root.
768 the revision is a branch root.
761 """
769 """
762 self.ui.debug("parsing revision %d (%d changes)\n" %
770 self.ui.debug("parsing revision %d (%d changes)\n" %
763 (revnum, len(orig_paths)))
771 (revnum, len(orig_paths)))
764
772
765 branched = False
773 branched = False
766 rev = self.revid(revnum)
774 rev = self.revid(revnum)
767 # branch log might return entries for a parent we already have
775 # branch log might return entries for a parent we already have
768
776
769 if rev in self.commits or revnum < to_revnum:
777 if rev in self.commits or revnum < to_revnum:
770 return None, branched
778 return None, branched
771
779
772 parents = []
780 parents = []
773 # check whether this revision is the start of a branch or part
781 # check whether this revision is the start of a branch or part
774 # of a branch renaming
782 # of a branch renaming
775 orig_paths = sorted(orig_paths.iteritems())
783 orig_paths = sorted(orig_paths.iteritems())
776 root_paths = [(p, e) for p, e in orig_paths
784 root_paths = [(p, e) for p, e in orig_paths
777 if self.module.startswith(p)]
785 if self.module.startswith(p)]
778 if root_paths:
786 if root_paths:
779 path, ent = root_paths[-1]
787 path, ent = root_paths[-1]
780 if ent.copyfrom_path:
788 if ent.copyfrom_path:
781 branched = True
789 branched = True
782 newpath = ent.copyfrom_path + self.module[len(path):]
790 newpath = ent.copyfrom_path + self.module[len(path):]
783 # ent.copyfrom_rev may not be the actual last revision
791 # ent.copyfrom_rev may not be the actual last revision
784 previd = self.latest(newpath, ent.copyfrom_rev)
792 previd = self.latest(newpath, ent.copyfrom_rev)
785 if previd is not None:
793 if previd is not None:
786 prevmodule, prevnum = revsplit(previd)[1:]
794 prevmodule, prevnum = revsplit(previd)[1:]
787 if prevnum >= self.startrev:
795 if prevnum >= self.startrev:
788 parents = [previd]
796 parents = [previd]
789 self.ui.note(
797 self.ui.note(
790 _('found parent of branch %s at %d: %s\n') %
798 _('found parent of branch %s at %d: %s\n') %
791 (self.module, prevnum, prevmodule))
799 (self.module, prevnum, prevmodule))
792 else:
800 else:
793 self.ui.debug("no copyfrom path, don't know what to do.\n")
801 self.ui.debug("no copyfrom path, don't know what to do.\n")
794
802
795 paths = []
803 paths = []
796 # filter out unrelated paths
804 # filter out unrelated paths
797 for path, ent in orig_paths:
805 for path, ent in orig_paths:
798 if self.getrelpath(path) is None:
806 if self.getrelpath(path) is None:
799 continue
807 continue
800 paths.append((path, ent))
808 paths.append((path, ent))
801
809
802 # Example SVN datetime. Includes microseconds.
810 # Example SVN datetime. Includes microseconds.
803 # ISO-8601 conformant
811 # ISO-8601 conformant
804 # '2007-01-04T17:35:00.902377Z'
812 # '2007-01-04T17:35:00.902377Z'
805 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
813 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
806 if self.ui.configbool('convert', 'localtimezone'):
814 if self.ui.configbool('convert', 'localtimezone'):
807 date = makedatetimestamp(date[0])
815 date = makedatetimestamp(date[0])
808
816
809 log = message and self.recode(message) or ''
817 log = message and self.recode(message) or ''
810 author = author and self.recode(author) or ''
818 author = author and self.recode(author) or ''
811 try:
819 try:
812 branch = self.module.split("/")[-1]
820 branch = self.module.split("/")[-1]
813 if branch == self.trunkname:
821 if branch == self.trunkname:
814 branch = None
822 branch = None
815 except IndexError:
823 except IndexError:
816 branch = None
824 branch = None
817
825
818 cset = commit(author=author,
826 cset = commit(author=author,
819 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
827 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
820 desc=log,
828 desc=log,
821 parents=parents,
829 parents=parents,
822 branch=branch,
830 branch=branch,
823 rev=rev)
831 rev=rev)
824
832
825 self.commits[rev] = cset
833 self.commits[rev] = cset
826 # The parents list is *shared* among self.paths and the
834 # The parents list is *shared* among self.paths and the
827 # commit object. Both will be updated below.
835 # commit object. Both will be updated below.
828 self.paths[rev] = (paths, cset.parents)
836 self.paths[rev] = (paths, cset.parents)
829 if self.child_cset and not self.child_cset.parents:
837 if self.child_cset and not self.child_cset.parents:
830 self.child_cset.parents[:] = [rev]
838 self.child_cset.parents[:] = [rev]
831 self.child_cset = cset
839 self.child_cset = cset
832 return cset, branched
840 return cset, branched
833
841
834 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
842 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
835 (self.module, from_revnum, to_revnum))
843 (self.module, from_revnum, to_revnum))
836
844
837 try:
845 try:
838 firstcset = None
846 firstcset = None
839 lastonbranch = False
847 lastonbranch = False
840 stream = self._getlog([self.module], from_revnum, to_revnum)
848 stream = self._getlog([self.module], from_revnum, to_revnum)
841 try:
849 try:
842 for entry in stream:
850 for entry in stream:
843 paths, revnum, author, date, message = entry
851 paths, revnum, author, date, message = entry
844 if revnum < self.startrev:
852 if revnum < self.startrev:
845 lastonbranch = True
853 lastonbranch = True
846 break
854 break
847 if not paths:
855 if not paths:
848 self.ui.debug('revision %d has no entries\n' % revnum)
856 self.ui.debug('revision %d has no entries\n' % revnum)
849 # If we ever leave the loop on an empty
857 # If we ever leave the loop on an empty
850 # revision, do not try to get a parent branch
858 # revision, do not try to get a parent branch
851 lastonbranch = lastonbranch or revnum == 0
859 lastonbranch = lastonbranch or revnum == 0
852 continue
860 continue
853 cset, lastonbranch = parselogentry(paths, revnum, author,
861 cset, lastonbranch = parselogentry(paths, revnum, author,
854 date, message)
862 date, message)
855 if cset:
863 if cset:
856 firstcset = cset
864 firstcset = cset
857 if lastonbranch:
865 if lastonbranch:
858 break
866 break
859 finally:
867 finally:
860 stream.close()
868 stream.close()
861
869
862 if not lastonbranch and firstcset and not firstcset.parents:
870 if not lastonbranch and firstcset and not firstcset.parents:
863 # The first revision of the sequence (the last fetched one)
871 # The first revision of the sequence (the last fetched one)
864 # has invalid parents if not a branch root. Find the parent
872 # has invalid parents if not a branch root. Find the parent
865 # revision now, if any.
873 # revision now, if any.
866 try:
874 try:
867 firstrevnum = self.revnum(firstcset.rev)
875 firstrevnum = self.revnum(firstcset.rev)
868 if firstrevnum > 1:
876 if firstrevnum > 1:
869 latest = self.latest(self.module, firstrevnum - 1)
877 latest = self.latest(self.module, firstrevnum - 1)
870 if latest:
878 if latest:
871 firstcset.parents.append(latest)
879 firstcset.parents.append(latest)
872 except SvnPathNotFound:
880 except SvnPathNotFound:
873 pass
881 pass
874 except SubversionException, (inst, num):
882 except SubversionException, (inst, num):
875 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
883 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
876 raise util.Abort(_('svn: branch has no revision %s')
884 raise util.Abort(_('svn: branch has no revision %s')
877 % to_revnum)
885 % to_revnum)
878 raise
886 raise
879
887
880 def getfile(self, file, rev):
888 def getfile(self, file, rev):
881 # TODO: ra.get_file transmits the whole file instead of diffs.
889 # TODO: ra.get_file transmits the whole file instead of diffs.
882 if file in self.removed:
890 if file in self.removed:
883 raise IOError
891 raise IOError
884 mode = ''
892 mode = ''
885 try:
893 try:
886 new_module, revnum = revsplit(rev)[1:]
894 new_module, revnum = revsplit(rev)[1:]
887 if self.module != new_module:
895 if self.module != new_module:
888 self.module = new_module
896 self.module = new_module
889 self.reparent(self.module)
897 self.reparent(self.module)
890 io = StringIO()
898 io = StringIO()
891 info = svn.ra.get_file(self.ra, file, revnum, io)
899 info = svn.ra.get_file(self.ra, file, revnum, io)
892 data = io.getvalue()
900 data = io.getvalue()
893 # ra.get_file() seems to keep a reference on the input buffer
901 # ra.get_file() seems to keep a reference on the input buffer
894 # preventing collection. Release it explicitly.
902 # preventing collection. Release it explicitly.
895 io.close()
903 io.close()
896 if isinstance(info, list):
904 if isinstance(info, list):
897 info = info[-1]
905 info = info[-1]
898 mode = ("svn:executable" in info) and 'x' or ''
906 mode = ("svn:executable" in info) and 'x' or ''
899 mode = ("svn:special" in info) and 'l' or mode
907 mode = ("svn:special" in info) and 'l' or mode
900 except SubversionException, e:
908 except SubversionException, e:
901 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
909 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
902 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
910 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
903 if e.apr_err in notfound: # File not found
911 if e.apr_err in notfound: # File not found
904 raise IOError
912 raise IOError
905 raise
913 raise
906 if mode == 'l':
914 if mode == 'l':
907 link_prefix = "link "
915 link_prefix = "link "
908 if data.startswith(link_prefix):
916 if data.startswith(link_prefix):
909 data = data[len(link_prefix):]
917 data = data[len(link_prefix):]
910 return data, mode
918 return data, mode
911
919
912 def _iterfiles(self, path, revnum):
920 def _iterfiles(self, path, revnum):
913 """Enumerate all files in path at revnum, recursively."""
921 """Enumerate all files in path at revnum, recursively."""
914 path = path.strip('/')
922 path = path.strip('/')
915 pool = Pool()
923 pool = Pool()
916 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
924 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
917 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
925 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
918 if path:
926 if path:
919 path += '/'
927 path += '/'
920 return ((path + p) for p, e in entries.iteritems()
928 return ((path + p) for p, e in entries.iteritems()
921 if e.kind == svn.core.svn_node_file)
929 if e.kind == svn.core.svn_node_file)
922
930
923 def getrelpath(self, path, module=None):
931 def getrelpath(self, path, module=None):
924 if module is None:
932 if module is None:
925 module = self.module
933 module = self.module
926 # Given the repository url of this wc, say
934 # Given the repository url of this wc, say
927 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
935 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
928 # extract the "entry" portion (a relative path) from what
936 # extract the "entry" portion (a relative path) from what
929 # svn log --xml says, i.e.
937 # svn log --xml says, i.e.
930 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
938 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
931 # that is to say "tests/PloneTestCase.py"
939 # that is to say "tests/PloneTestCase.py"
932 if path.startswith(module):
940 if path.startswith(module):
933 relative = path.rstrip('/')[len(module):]
941 relative = path.rstrip('/')[len(module):]
934 if relative.startswith('/'):
942 if relative.startswith('/'):
935 return relative[1:]
943 return relative[1:]
936 elif relative == '':
944 elif relative == '':
937 return relative
945 return relative
938
946
939 # The path is outside our tracked tree...
947 # The path is outside our tracked tree...
940 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
948 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
941 return None
949 return None
942
950
943 def _checkpath(self, path, revnum, module=None):
951 def _checkpath(self, path, revnum, module=None):
944 if module is not None:
952 if module is not None:
945 prevmodule = self.reparent('')
953 prevmodule = self.reparent('')
946 path = module + '/' + path
954 path = module + '/' + path
947 try:
955 try:
948 # ra.check_path does not like leading slashes very much, it leads
956 # ra.check_path does not like leading slashes very much, it leads
949 # to PROPFIND subversion errors
957 # to PROPFIND subversion errors
950 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
958 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
951 finally:
959 finally:
952 if module is not None:
960 if module is not None:
953 self.reparent(prevmodule)
961 self.reparent(prevmodule)
954
962
955 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
963 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
956 strict_node_history=False):
964 strict_node_history=False):
957 # Normalize path names, svn >= 1.5 only wants paths relative to
965 # Normalize path names, svn >= 1.5 only wants paths relative to
958 # supplied URL
966 # supplied URL
959 relpaths = []
967 relpaths = []
960 for p in paths:
968 for p in paths:
961 if not p.startswith('/'):
969 if not p.startswith('/'):
962 p = self.module + '/' + p
970 p = self.module + '/' + p
963 relpaths.append(p.strip('/'))
971 relpaths.append(p.strip('/'))
964 args = [self.baseurl, relpaths, start, end, limit,
972 args = [self.baseurl, relpaths, start, end, limit,
965 discover_changed_paths, strict_node_history]
973 discover_changed_paths, strict_node_history]
966 arg = encodeargs(args)
974 arg = encodeargs(args)
967 hgexe = util.hgexecutable()
975 hgexe = util.hgexecutable()
968 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
976 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
969 stdin, stdout = util.popen2(util.quotecommand(cmd))
977 stdin, stdout = util.popen2(util.quotecommand(cmd))
970 stdin.write(arg)
978 stdin.write(arg)
971 try:
979 try:
972 stdin.close()
980 stdin.close()
973 except IOError:
981 except IOError:
974 raise util.Abort(_('Mercurial failed to run itself, check'
982 raise util.Abort(_('Mercurial failed to run itself, check'
975 ' hg executable is in PATH'))
983 ' hg executable is in PATH'))
976 return logstream(stdout)
984 return logstream(stdout)
977
985
978 pre_revprop_change = '''#!/bin/sh
986 pre_revprop_change = '''#!/bin/sh
979
987
980 REPOS="$1"
988 REPOS="$1"
981 REV="$2"
989 REV="$2"
982 USER="$3"
990 USER="$3"
983 PROPNAME="$4"
991 PROPNAME="$4"
984 ACTION="$5"
992 ACTION="$5"
985
993
986 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
994 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
987 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
995 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
988 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
996 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
989
997
990 echo "Changing prohibited revision property" >&2
998 echo "Changing prohibited revision property" >&2
991 exit 1
999 exit 1
992 '''
1000 '''
993
1001
994 class svn_sink(converter_sink, commandline):
1002 class svn_sink(converter_sink, commandline):
995 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1003 commit_re = re.compile(r'Committed revision (\d+).', re.M)
996 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1004 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
997
1005
998 def prerun(self):
1006 def prerun(self):
999 if self.wc:
1007 if self.wc:
1000 os.chdir(self.wc)
1008 os.chdir(self.wc)
1001
1009
1002 def postrun(self):
1010 def postrun(self):
1003 if self.wc:
1011 if self.wc:
1004 os.chdir(self.cwd)
1012 os.chdir(self.cwd)
1005
1013
1006 def join(self, name):
1014 def join(self, name):
1007 return os.path.join(self.wc, '.svn', name)
1015 return os.path.join(self.wc, '.svn', name)
1008
1016
1009 def revmapfile(self):
1017 def revmapfile(self):
1010 return self.join('hg-shamap')
1018 return self.join('hg-shamap')
1011
1019
1012 def authorfile(self):
1020 def authorfile(self):
1013 return self.join('hg-authormap')
1021 return self.join('hg-authormap')
1014
1022
1015 def __init__(self, ui, path):
1023 def __init__(self, ui, path):
1016
1024
1017 converter_sink.__init__(self, ui, path)
1025 converter_sink.__init__(self, ui, path)
1018 commandline.__init__(self, ui, 'svn')
1026 commandline.__init__(self, ui, 'svn')
1019 self.delete = []
1027 self.delete = []
1020 self.setexec = []
1028 self.setexec = []
1021 self.delexec = []
1029 self.delexec = []
1022 self.copies = []
1030 self.copies = []
1023 self.wc = None
1031 self.wc = None
1024 self.cwd = os.getcwd()
1032 self.cwd = os.getcwd()
1025
1033
1026 created = False
1034 created = False
1027 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1035 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1028 self.wc = os.path.realpath(path)
1036 self.wc = os.path.realpath(path)
1029 self.run0('update')
1037 self.run0('update')
1030 else:
1038 else:
1031 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1039 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1032 path = os.path.realpath(path)
1040 path = os.path.realpath(path)
1033 if os.path.isdir(os.path.dirname(path)):
1041 if os.path.isdir(os.path.dirname(path)):
1034 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1042 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1035 ui.status(_('initializing svn repository %r\n') %
1043 ui.status(_('initializing svn repository %r\n') %
1036 os.path.basename(path))
1044 os.path.basename(path))
1037 commandline(ui, 'svnadmin').run0('create', path)
1045 commandline(ui, 'svnadmin').run0('create', path)
1038 created = path
1046 created = path
1039 path = util.normpath(path)
1047 path = util.normpath(path)
1040 if not path.startswith('/'):
1048 if not path.startswith('/'):
1041 path = '/' + path
1049 path = '/' + path
1042 path = 'file://' + path
1050 path = 'file://' + path
1043
1051
1044 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1052 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1045 ui.status(_('initializing svn working copy %r\n')
1053 ui.status(_('initializing svn working copy %r\n')
1046 % os.path.basename(wcpath))
1054 % os.path.basename(wcpath))
1047 self.run0('checkout', path, wcpath)
1055 self.run0('checkout', path, wcpath)
1048
1056
1049 self.wc = wcpath
1057 self.wc = wcpath
1050 self.opener = scmutil.opener(self.wc)
1058 self.opener = scmutil.opener(self.wc)
1051 self.wopener = scmutil.opener(self.wc)
1059 self.wopener = scmutil.opener(self.wc)
1052 self.childmap = mapfile(ui, self.join('hg-childmap'))
1060 self.childmap = mapfile(ui, self.join('hg-childmap'))
1053 self.is_exec = util.checkexec(self.wc) and util.isexec or None
1061 self.is_exec = util.checkexec(self.wc) and util.isexec or None
1054
1062
1055 if created:
1063 if created:
1056 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1064 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1057 fp = open(hook, 'w')
1065 fp = open(hook, 'w')
1058 fp.write(pre_revprop_change)
1066 fp.write(pre_revprop_change)
1059 fp.close()
1067 fp.close()
1060 util.setflags(hook, False, True)
1068 util.setflags(hook, False, True)
1061
1069
1062 output = self.run0('info')
1070 output = self.run0('info')
1063 self.uuid = self.uuid_re.search(output).group(1).strip()
1071 self.uuid = self.uuid_re.search(output).group(1).strip()
1064
1072
1065 def wjoin(self, *names):
1073 def wjoin(self, *names):
1066 return os.path.join(self.wc, *names)
1074 return os.path.join(self.wc, *names)
1067
1075
1068 @propertycache
1076 @propertycache
1069 def manifest(self):
1077 def manifest(self):
1070 # As of svn 1.7, the "add" command fails when receiving
1078 # As of svn 1.7, the "add" command fails when receiving
1071 # already tracked entries, so we have to track and filter them
1079 # already tracked entries, so we have to track and filter them
1072 # ourselves.
1080 # ourselves.
1073 m = set()
1081 m = set()
1074 output = self.run0('ls', recursive=True, xml=True)
1082 output = self.run0('ls', recursive=True, xml=True)
1075 doc = xml.dom.minidom.parseString(output)
1083 doc = xml.dom.minidom.parseString(output)
1076 for e in doc.getElementsByTagName('entry'):
1084 for e in doc.getElementsByTagName('entry'):
1077 for n in e.childNodes:
1085 for n in e.childNodes:
1078 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1086 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1079 continue
1087 continue
1080 name = ''.join(c.data for c in n.childNodes
1088 name = ''.join(c.data for c in n.childNodes
1081 if c.nodeType == c.TEXT_NODE)
1089 if c.nodeType == c.TEXT_NODE)
1082 # Entries are compared with names coming from
1090 # Entries are compared with names coming from
1083 # mercurial, so bytes with undefined encoding. Our
1091 # mercurial, so bytes with undefined encoding. Our
1084 # best bet is to assume they are in local
1092 # best bet is to assume they are in local
1085 # encoding. They will be passed to command line calls
1093 # encoding. They will be passed to command line calls
1086 # later anyway, so they better be.
1094 # later anyway, so they better be.
1087 m.add(encoding.tolocal(name.encode('utf-8')))
1095 m.add(encoding.tolocal(name.encode('utf-8')))
1088 break
1096 break
1089 return m
1097 return m
1090
1098
1091 def putfile(self, filename, flags, data):
1099 def putfile(self, filename, flags, data):
1092 if 'l' in flags:
1100 if 'l' in flags:
1093 self.wopener.symlink(data, filename)
1101 self.wopener.symlink(data, filename)
1094 else:
1102 else:
1095 try:
1103 try:
1096 if os.path.islink(self.wjoin(filename)):
1104 if os.path.islink(self.wjoin(filename)):
1097 os.unlink(filename)
1105 os.unlink(filename)
1098 except OSError:
1106 except OSError:
1099 pass
1107 pass
1100 self.wopener.write(filename, data)
1108 self.wopener.write(filename, data)
1101
1109
1102 if self.is_exec:
1110 if self.is_exec:
1103 if self.is_exec(self.wjoin(filename)):
1111 if self.is_exec(self.wjoin(filename)):
1104 if 'x' not in flags:
1112 if 'x' not in flags:
1105 self.delexec.append(filename)
1113 self.delexec.append(filename)
1106 else:
1114 else:
1107 if 'x' in flags:
1115 if 'x' in flags:
1108 self.setexec.append(filename)
1116 self.setexec.append(filename)
1109 util.setflags(self.wjoin(filename), False, 'x' in flags)
1117 util.setflags(self.wjoin(filename), False, 'x' in flags)
1110
1118
1111 def _copyfile(self, source, dest):
1119 def _copyfile(self, source, dest):
1112 # SVN's copy command pukes if the destination file exists, but
1120 # SVN's copy command pukes if the destination file exists, but
1113 # our copyfile method expects to record a copy that has
1121 # our copyfile method expects to record a copy that has
1114 # already occurred. Cross the semantic gap.
1122 # already occurred. Cross the semantic gap.
1115 wdest = self.wjoin(dest)
1123 wdest = self.wjoin(dest)
1116 exists = os.path.lexists(wdest)
1124 exists = os.path.lexists(wdest)
1117 if exists:
1125 if exists:
1118 fd, tempname = tempfile.mkstemp(
1126 fd, tempname = tempfile.mkstemp(
1119 prefix='hg-copy-', dir=os.path.dirname(wdest))
1127 prefix='hg-copy-', dir=os.path.dirname(wdest))
1120 os.close(fd)
1128 os.close(fd)
1121 os.unlink(tempname)
1129 os.unlink(tempname)
1122 os.rename(wdest, tempname)
1130 os.rename(wdest, tempname)
1123 try:
1131 try:
1124 self.run0('copy', source, dest)
1132 self.run0('copy', source, dest)
1125 finally:
1133 finally:
1126 self.manifest.add(dest)
1134 self.manifest.add(dest)
1127 if exists:
1135 if exists:
1128 try:
1136 try:
1129 os.unlink(wdest)
1137 os.unlink(wdest)
1130 except OSError:
1138 except OSError:
1131 pass
1139 pass
1132 os.rename(tempname, wdest)
1140 os.rename(tempname, wdest)
1133
1141
1134 def dirs_of(self, files):
1142 def dirs_of(self, files):
1135 dirs = set()
1143 dirs = set()
1136 for f in files:
1144 for f in files:
1137 if os.path.isdir(self.wjoin(f)):
1145 if os.path.isdir(self.wjoin(f)):
1138 dirs.add(f)
1146 dirs.add(f)
1139 for i in strutil.rfindall(f, '/'):
1147 for i in strutil.rfindall(f, '/'):
1140 dirs.add(f[:i])
1148 dirs.add(f[:i])
1141 return dirs
1149 return dirs
1142
1150
1143 def add_dirs(self, files):
1151 def add_dirs(self, files):
1144 add_dirs = [d for d in sorted(self.dirs_of(files))
1152 add_dirs = [d for d in sorted(self.dirs_of(files))
1145 if d not in self.manifest]
1153 if d not in self.manifest]
1146 if add_dirs:
1154 if add_dirs:
1147 self.manifest.update(add_dirs)
1155 self.manifest.update(add_dirs)
1148 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1156 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1149 return add_dirs
1157 return add_dirs
1150
1158
1151 def add_files(self, files):
1159 def add_files(self, files):
1152 files = [f for f in files if f not in self.manifest]
1160 files = [f for f in files if f not in self.manifest]
1153 if files:
1161 if files:
1154 self.manifest.update(files)
1162 self.manifest.update(files)
1155 self.xargs(files, 'add', quiet=True)
1163 self.xargs(files, 'add', quiet=True)
1156 return files
1164 return files
1157
1165
1158 def tidy_dirs(self, names):
1166 def tidy_dirs(self, names):
1159 deleted = []
1167 deleted = []
1160 for d in sorted(self.dirs_of(names), reverse=True):
1168 for d in sorted(self.dirs_of(names), reverse=True):
1161 wd = self.wjoin(d)
1169 wd = self.wjoin(d)
1162 if os.listdir(wd) == '.svn':
1170 if os.listdir(wd) == '.svn':
1163 self.run0('delete', d)
1171 self.run0('delete', d)
1164 self.manifest.remove(d)
1172 self.manifest.remove(d)
1165 deleted.append(d)
1173 deleted.append(d)
1166 return deleted
1174 return deleted
1167
1175
1168 def addchild(self, parent, child):
1176 def addchild(self, parent, child):
1169 self.childmap[parent] = child
1177 self.childmap[parent] = child
1170
1178
1171 def revid(self, rev):
1179 def revid(self, rev):
1172 return u"svn:%s@%s" % (self.uuid, rev)
1180 return u"svn:%s@%s" % (self.uuid, rev)
1173
1181
1174 def putcommit(self, files, copies, parents, commit, source, revmap):
1182 def putcommit(self, files, copies, parents, commit, source, revmap):
1175 for parent in parents:
1183 for parent in parents:
1176 try:
1184 try:
1177 return self.revid(self.childmap[parent])
1185 return self.revid(self.childmap[parent])
1178 except KeyError:
1186 except KeyError:
1179 pass
1187 pass
1180
1188
1181 # Apply changes to working copy
1189 # Apply changes to working copy
1182 for f, v in files:
1190 for f, v in files:
1183 try:
1191 try:
1184 data, mode = source.getfile(f, v)
1192 data, mode = source.getfile(f, v)
1185 except IOError:
1193 except IOError:
1186 self.delete.append(f)
1194 self.delete.append(f)
1187 else:
1195 else:
1188 self.putfile(f, mode, data)
1196 self.putfile(f, mode, data)
1189 if f in copies:
1197 if f in copies:
1190 self.copies.append([copies[f], f])
1198 self.copies.append([copies[f], f])
1191 files = [f[0] for f in files]
1199 files = [f[0] for f in files]
1192
1200
1193 entries = set(self.delete)
1201 entries = set(self.delete)
1194 files = frozenset(files)
1202 files = frozenset(files)
1195 entries.update(self.add_dirs(files.difference(entries)))
1203 entries.update(self.add_dirs(files.difference(entries)))
1196 if self.copies:
1204 if self.copies:
1197 for s, d in self.copies:
1205 for s, d in self.copies:
1198 self._copyfile(s, d)
1206 self._copyfile(s, d)
1199 self.copies = []
1207 self.copies = []
1200 if self.delete:
1208 if self.delete:
1201 self.xargs(self.delete, 'delete')
1209 self.xargs(self.delete, 'delete')
1202 for f in self.delete:
1210 for f in self.delete:
1203 self.manifest.remove(f)
1211 self.manifest.remove(f)
1204 self.delete = []
1212 self.delete = []
1205 entries.update(self.add_files(files.difference(entries)))
1213 entries.update(self.add_files(files.difference(entries)))
1206 entries.update(self.tidy_dirs(entries))
1214 entries.update(self.tidy_dirs(entries))
1207 if self.delexec:
1215 if self.delexec:
1208 self.xargs(self.delexec, 'propdel', 'svn:executable')
1216 self.xargs(self.delexec, 'propdel', 'svn:executable')
1209 self.delexec = []
1217 self.delexec = []
1210 if self.setexec:
1218 if self.setexec:
1211 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1219 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1212 self.setexec = []
1220 self.setexec = []
1213
1221
1214 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1222 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1215 fp = os.fdopen(fd, 'w')
1223 fp = os.fdopen(fd, 'w')
1216 fp.write(commit.desc)
1224 fp.write(commit.desc)
1217 fp.close()
1225 fp.close()
1218 try:
1226 try:
1219 output = self.run0('commit',
1227 output = self.run0('commit',
1220 username=util.shortuser(commit.author),
1228 username=util.shortuser(commit.author),
1221 file=messagefile,
1229 file=messagefile,
1222 encoding='utf-8')
1230 encoding='utf-8')
1223 try:
1231 try:
1224 rev = self.commit_re.search(output).group(1)
1232 rev = self.commit_re.search(output).group(1)
1225 except AttributeError:
1233 except AttributeError:
1226 if not files:
1234 if not files:
1227 return parents[0]
1235 return parents[0]
1228 self.ui.warn(_('unexpected svn output:\n'))
1236 self.ui.warn(_('unexpected svn output:\n'))
1229 self.ui.warn(output)
1237 self.ui.warn(output)
1230 raise util.Abort(_('unable to cope with svn output'))
1238 raise util.Abort(_('unable to cope with svn output'))
1231 if commit.rev:
1239 if commit.rev:
1232 self.run('propset', 'hg:convert-rev', commit.rev,
1240 self.run('propset', 'hg:convert-rev', commit.rev,
1233 revprop=True, revision=rev)
1241 revprop=True, revision=rev)
1234 if commit.branch and commit.branch != 'default':
1242 if commit.branch and commit.branch != 'default':
1235 self.run('propset', 'hg:convert-branch', commit.branch,
1243 self.run('propset', 'hg:convert-branch', commit.branch,
1236 revprop=True, revision=rev)
1244 revprop=True, revision=rev)
1237 for parent in parents:
1245 for parent in parents:
1238 self.addchild(parent, rev)
1246 self.addchild(parent, rev)
1239 return self.revid(rev)
1247 return self.revid(rev)
1240 finally:
1248 finally:
1241 os.unlink(messagefile)
1249 os.unlink(messagefile)
1242
1250
1243 def puttags(self, tags):
1251 def puttags(self, tags):
1244 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1252 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1245 return None, None
1253 return None, None
1246
1254
1247 def hascommit(self, rev):
1255 def hascommit(self, rev):
1248 # This is not correct as one can convert to an existing subversion
1256 # This is not correct as one can convert to an existing subversion
1249 # repository and childmap would not list all revisions. Too bad.
1257 # repository and childmap would not list all revisions. Too bad.
1250 if rev in self.childmap:
1258 if rev in self.childmap:
1251 return True
1259 return True
1252 raise util.Abort(_('splice map revision %s not found in subversion '
1260 raise util.Abort(_('splice map revision %s not found in subversion '
1253 'child map (revision lookups are not implemented)')
1261 'child map (revision lookups are not implemented)')
1254 % rev)
1262 % rev)
@@ -1,210 +1,221 b''
1
1
2 $ "$TESTDIR/hghave" svn svn-bindings || exit 80
2 $ "$TESTDIR/hghave" svn svn-bindings || exit 80
3
3
4 $ cat >> $HGRCPATH <<EOF
4 $ cat >> $HGRCPATH <<EOF
5 > [extensions]
5 > [extensions]
6 > convert =
6 > convert =
7 > graphlog =
7 > graphlog =
8 > [convert]
8 > [convert]
9 > svn.trunk = mytrunk
9 > svn.trunk = mytrunk
10 > EOF
10 > EOF
11
11
12 $ svnadmin create svn-repo
12 $ svnadmin create svn-repo
13 $ SVNREPOPATH=`pwd`/svn-repo
13 $ SVNREPOPATH=`pwd`/svn-repo
14 #if windows
14 #if windows
15 $ SVNREPOURL=file:///`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
15 $ SVNREPOURL=file:///`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
16 #else
16 #else
17 $ SVNREPOURL=file://`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
17 $ SVNREPOURL=file://`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
18 #endif
18 #endif
19 $ INVALIDREVISIONID=svn:x2147622-4a9f-4db4-a8d3-13562ff547b2/proj%20B/mytrunk@1
20 $ VALIDREVISIONID=svn:a2147622-4a9f-4db4-a8d3-13562ff547b2/proj%20B/mytrunk/mytrunk@1
19
21
20 Now test that it works with trunk/tags layout, but no branches yet.
22 Now test that it works with trunk/tags layout, but no branches yet.
21
23
22 Initial svn import
24 Initial svn import
23
25
24 $ mkdir projB
26 $ mkdir projB
25 $ cd projB
27 $ cd projB
26 $ mkdir mytrunk
28 $ mkdir mytrunk
27 $ mkdir tags
29 $ mkdir tags
28 $ cd ..
30 $ cd ..
29
31
30 $ svn import -m "init projB" projB "$SVNREPOURL/proj%20B" | sort
32 $ svn import -m "init projB" projB "$SVNREPOURL/proj%20B" | sort
31
33
32 Adding projB/mytrunk (glob)
34 Adding projB/mytrunk (glob)
33 Adding projB/tags (glob)
35 Adding projB/tags (glob)
34 Committed revision 1.
36 Committed revision 1.
35
37
36 Update svn repository
38 Update svn repository
37
39
38 $ svn co "$SVNREPOURL/proj%20B/mytrunk" B
40 $ svn co "$SVNREPOURL/proj%20B/mytrunk" B
39 Checked out revision 1.
41 Checked out revision 1.
40 $ cd B
42 $ cd B
41 $ echo hello > 'letter .txt'
43 $ echo hello > 'letter .txt'
42 $ svn add 'letter .txt'
44 $ svn add 'letter .txt'
43 A letter .txt
45 A letter .txt
44 $ svn ci -m hello
46 $ svn ci -m hello
45 Adding letter .txt
47 Adding letter .txt
46 Transmitting file data .
48 Transmitting file data .
47 Committed revision 2.
49 Committed revision 2.
48
50
49 $ "$TESTDIR/svn-safe-append.py" world 'letter .txt'
51 $ "$TESTDIR/svn-safe-append.py" world 'letter .txt'
50 $ svn ci -m world
52 $ svn ci -m world
51 Sending letter .txt
53 Sending letter .txt
52 Transmitting file data .
54 Transmitting file data .
53 Committed revision 3.
55 Committed revision 3.
54
56
55 $ svn copy -m "tag v0.1" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.1"
57 $ svn copy -m "tag v0.1" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.1"
56
58
57 Committed revision 4.
59 Committed revision 4.
58
60
59 $ "$TESTDIR/svn-safe-append.py" 'nice day today!' 'letter .txt'
61 $ "$TESTDIR/svn-safe-append.py" 'nice day today!' 'letter .txt'
60 $ svn ci -m "nice day"
62 $ svn ci -m "nice day"
61 Sending letter .txt
63 Sending letter .txt
62 Transmitting file data .
64 Transmitting file data .
63 Committed revision 5.
65 Committed revision 5.
64 $ cd ..
66 $ cd ..
65
67
66 Convert to hg once and also test localtimezone option
68 Convert to hg once and also test localtimezone option
67
69
68 NOTE: This doesn't check all time zones -- it merely determines that
70 NOTE: This doesn't check all time zones -- it merely determines that
69 the configuration option is taking effect.
71 the configuration option is taking effect.
70
72
71 An arbitrary (U.S.) time zone is used here. TZ=US/Hawaii is selected
73 An arbitrary (U.S.) time zone is used here. TZ=US/Hawaii is selected
72 since it does not use DST (unlike other U.S. time zones) and is always
74 since it does not use DST (unlike other U.S. time zones) and is always
73 a fixed difference from UTC.
75 a fixed difference from UTC.
74
76
75 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True "$SVNREPOURL/proj%20B" B-hg
77 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True "$SVNREPOURL/proj%20B" B-hg
76 initializing destination B-hg repository
78 initializing destination B-hg repository
77 scanning source...
79 scanning source...
78 sorting...
80 sorting...
79 converting...
81 converting...
80 3 init projB
82 3 init projB
81 2 hello
83 2 hello
82 1 world
84 1 world
83 0 nice day
85 0 nice day
84 updating tags
86 updating tags
85
87
86 Update svn repository again
88 Update svn repository again
87
89
88 $ cd B
90 $ cd B
89 $ "$TESTDIR/svn-safe-append.py" "see second letter" 'letter .txt'
91 $ "$TESTDIR/svn-safe-append.py" "see second letter" 'letter .txt'
90 $ echo "nice to meet you" > letter2.txt
92 $ echo "nice to meet you" > letter2.txt
91 $ svn add letter2.txt
93 $ svn add letter2.txt
92 A letter2.txt
94 A letter2.txt
93 $ svn ci -m "second letter"
95 $ svn ci -m "second letter"
94 Sending letter .txt
96 Sending letter .txt
95 Adding letter2.txt
97 Adding letter2.txt
96 Transmitting file data ..
98 Transmitting file data ..
97 Committed revision 6.
99 Committed revision 6.
98
100
99 $ svn copy -m "tag v0.2" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.2"
101 $ svn copy -m "tag v0.2" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.2"
100
102
101 Committed revision 7.
103 Committed revision 7.
102
104
103 $ "$TESTDIR/svn-safe-append.py" "blah-blah-blah" letter2.txt
105 $ "$TESTDIR/svn-safe-append.py" "blah-blah-blah" letter2.txt
104 $ svn ci -m "work in progress"
106 $ svn ci -m "work in progress"
105 Sending letter2.txt
107 Sending letter2.txt
106 Transmitting file data .
108 Transmitting file data .
107 Committed revision 8.
109 Committed revision 8.
108 $ cd ..
110 $ cd ..
109
111
110 $ hg convert -s svn "$SVNREPOURL/proj%20B/non-existent-path" dest
112 $ hg convert -s svn "$SVNREPOURL/proj%20B/non-existent-path" dest
111 initializing destination dest repository
113 initializing destination dest repository
112 abort: no revision found in module /proj B/non-existent-path
114 abort: no revision found in module /proj B/non-existent-path
113 [255]
115 [255]
114
116
115 ########################################
117 ########################################
116
118
117 Test incremental conversion
119 Test incremental conversion
118
120
119 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True "$SVNREPOURL/proj%20B" B-hg
121 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True "$SVNREPOURL/proj%20B" B-hg
120 scanning source...
122 scanning source...
121 sorting...
123 sorting...
122 converting...
124 converting...
123 1 second letter
125 1 second letter
124 0 work in progress
126 0 work in progress
125 updating tags
127 updating tags
126
128
127 $ cd B-hg
129 $ cd B-hg
128 $ hg glog --template '{rev} {desc|firstline} date: {date|date} files: {files}\n'
130 $ hg glog --template '{rev} {desc|firstline} date: {date|date} files: {files}\n'
129 o 7 update tags date: * +0000 files: .hgtags (glob)
131 o 7 update tags date: * +0000 files: .hgtags (glob)
130 |
132 |
131 o 6 work in progress date: * -1000 files: letter2.txt (glob)
133 o 6 work in progress date: * -1000 files: letter2.txt (glob)
132 |
134 |
133 o 5 second letter date: * -1000 files: letter .txt letter2.txt (glob)
135 o 5 second letter date: * -1000 files: letter .txt letter2.txt (glob)
134 |
136 |
135 o 4 update tags date: * +0000 files: .hgtags (glob)
137 o 4 update tags date: * +0000 files: .hgtags (glob)
136 |
138 |
137 o 3 nice day date: * -1000 files: letter .txt (glob)
139 o 3 nice day date: * -1000 files: letter .txt (glob)
138 |
140 |
139 o 2 world date: * -1000 files: letter .txt (glob)
141 o 2 world date: * -1000 files: letter .txt (glob)
140 |
142 |
141 o 1 hello date: * -1000 files: letter .txt (glob)
143 o 1 hello date: * -1000 files: letter .txt (glob)
142 |
144 |
143 o 0 init projB date: * -1000 files: (glob)
145 o 0 init projB date: * -1000 files: (glob)
144
146
145 $ hg tags -q
147 $ hg tags -q
146 tip
148 tip
147 v0.2
149 v0.2
148 v0.1
150 v0.1
149 $ cd ..
151 $ cd ..
150
152
151 Test filemap
153 Test filemap
152 $ echo 'include letter2.txt' > filemap
154 $ echo 'include letter2.txt' > filemap
153 $ hg convert --filemap filemap "$SVNREPOURL/proj%20B/mytrunk" fmap
155 $ hg convert --filemap filemap "$SVNREPOURL/proj%20B/mytrunk" fmap
154 initializing destination fmap repository
156 initializing destination fmap repository
155 scanning source...
157 scanning source...
156 sorting...
158 sorting...
157 converting...
159 converting...
158 5 init projB
160 5 init projB
159 4 hello
161 4 hello
160 3 world
162 3 world
161 2 nice day
163 2 nice day
162 1 second letter
164 1 second letter
163 0 work in progress
165 0 work in progress
164 $ hg -R fmap branch -q
166 $ hg -R fmap branch -q
165 default
167 default
166 $ hg glog -R fmap --template '{rev} {desc|firstline} files: {files}\n'
168 $ hg glog -R fmap --template '{rev} {desc|firstline} files: {files}\n'
167 o 1 work in progress files: letter2.txt
169 o 1 work in progress files: letter2.txt
168 |
170 |
169 o 0 second letter files: letter2.txt
171 o 0 second letter files: letter2.txt
170
172
173 test invalid splicemap1
174
175 $ cat > splicemap <<EOF
176 > $INVALIDREVISIONID $VALIDREVISIONID
177 > EOF
178 $ hg convert --splicemap splicemap "$SVNREPOURL/proj%20B/mytrunk" smap
179 initializing destination smap repository
180 abort: splicemap entry svn:x2147622-4a9f-4db4-a8d3-13562ff547b2/proj%20B/mytrunk@1 is not a valid revision identifier
181 [255]
171
182
172 Test stop revision
183 Test stop revision
173 $ hg convert --rev 1 "$SVNREPOURL/proj%20B/mytrunk" stoprev
184 $ hg convert --rev 1 "$SVNREPOURL/proj%20B/mytrunk" stoprev
174 initializing destination stoprev repository
185 initializing destination stoprev repository
175 scanning source...
186 scanning source...
176 sorting...
187 sorting...
177 converting...
188 converting...
178 0 init projB
189 0 init projB
179 $ hg -R stoprev branch -q
190 $ hg -R stoprev branch -q
180 default
191 default
181
192
182 Check convert_revision extra-records.
193 Check convert_revision extra-records.
183 This is also the only place testing more than one extra field in a revision.
194 This is also the only place testing more than one extra field in a revision.
184
195
185 $ cd stoprev
196 $ cd stoprev
186 $ hg tip --debug | grep extra
197 $ hg tip --debug | grep extra
187 extra: branch=default
198 extra: branch=default
188 extra: convert_revision=svn:........-....-....-....-............/proj B/mytrunk@1 (re)
199 extra: convert_revision=svn:........-....-....-....-............/proj B/mytrunk@1 (re)
189 $ cd ..
200 $ cd ..
190
201
191 Test converting empty heads (issue3347)
202 Test converting empty heads (issue3347)
192
203
193 $ svnadmin create svn-empty
204 $ svnadmin create svn-empty
194 $ svnadmin load -q svn-empty < "$TESTDIR/svn/empty.svndump"
205 $ svnadmin load -q svn-empty < "$TESTDIR/svn/empty.svndump"
195 $ hg --config convert.svn.trunk= convert svn-empty
206 $ hg --config convert.svn.trunk= convert svn-empty
196 assuming destination svn-empty-hg
207 assuming destination svn-empty-hg
197 initializing destination svn-empty-hg repository
208 initializing destination svn-empty-hg repository
198 scanning source...
209 scanning source...
199 sorting...
210 sorting...
200 converting...
211 converting...
201 1 init projA
212 1 init projA
202 0 adddir
213 0 adddir
203 $ hg --config convert.svn.trunk= convert "$SVNREPOURL/../svn-empty/trunk"
214 $ hg --config convert.svn.trunk= convert "$SVNREPOURL/../svn-empty/trunk"
204 assuming destination trunk-hg
215 assuming destination trunk-hg
205 initializing destination trunk-hg repository
216 initializing destination trunk-hg repository
206 scanning source...
217 scanning source...
207 sorting...
218 sorting...
208 converting...
219 converting...
209 1 init projA
220 1 init projA
210 0 adddir
221 0 adddir
General Comments 0
You need to be logged in to leave comments. Login now