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