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