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