##// END OF EJS Templates
convert: reintegrate file retrieval code in sinks...
Patrick Mezard -
r6716:c9b8d256 default
parent child Browse files
Show More
@@ -1,357 +1,349 b''
1 # common code for the convert extension
1 # common code for the convert extension
2 import base64, errno
2 import base64, errno
3 import os
3 import os
4 import cPickle as pickle
4 import cPickle as pickle
5 from mercurial import util
5 from mercurial import util
6 from mercurial.i18n import _
6 from mercurial.i18n import _
7
7
8 def encodeargs(args):
8 def encodeargs(args):
9 def encodearg(s):
9 def encodearg(s):
10 lines = base64.encodestring(s)
10 lines = base64.encodestring(s)
11 lines = [l.splitlines()[0] for l in lines]
11 lines = [l.splitlines()[0] for l in lines]
12 return ''.join(lines)
12 return ''.join(lines)
13
13
14 s = pickle.dumps(args)
14 s = pickle.dumps(args)
15 return encodearg(s)
15 return encodearg(s)
16
16
17 def decodeargs(s):
17 def decodeargs(s):
18 s = base64.decodestring(s)
18 s = base64.decodestring(s)
19 return pickle.loads(s)
19 return pickle.loads(s)
20
20
21 class MissingTool(Exception): pass
21 class MissingTool(Exception): pass
22
22
23 def checktool(exe, name=None, abort=True):
23 def checktool(exe, name=None, abort=True):
24 name = name or exe
24 name = name or exe
25 if not util.find_exe(exe):
25 if not util.find_exe(exe):
26 exc = abort and util.Abort or MissingTool
26 exc = abort and util.Abort or MissingTool
27 raise exc(_('cannot find required "%s" tool') % name)
27 raise exc(_('cannot find required "%s" tool') % name)
28
28
29 class NoRepo(Exception): pass
29 class NoRepo(Exception): pass
30
30
31 SKIPREV = 'SKIP'
31 SKIPREV = 'SKIP'
32
32
33 class commit(object):
33 class commit(object):
34 def __init__(self, author, date, desc, parents, branch=None, rev=None,
34 def __init__(self, author, date, desc, parents, branch=None, rev=None,
35 extra={}):
35 extra={}):
36 self.author = author or 'unknown'
36 self.author = author or 'unknown'
37 self.date = date or '0 0'
37 self.date = date or '0 0'
38 self.desc = desc
38 self.desc = desc
39 self.parents = parents
39 self.parents = parents
40 self.branch = branch
40 self.branch = branch
41 self.rev = rev
41 self.rev = rev
42 self.extra = extra
42 self.extra = extra
43
43
44 class converter_source(object):
44 class converter_source(object):
45 """Conversion source interface"""
45 """Conversion source interface"""
46
46
47 def __init__(self, ui, path=None, rev=None):
47 def __init__(self, ui, path=None, rev=None):
48 """Initialize conversion source (or raise NoRepo("message")
48 """Initialize conversion source (or raise NoRepo("message")
49 exception if path is not a valid repository)"""
49 exception if path is not a valid repository)"""
50 self.ui = ui
50 self.ui = ui
51 self.path = path
51 self.path = path
52 self.rev = rev
52 self.rev = rev
53
53
54 self.encoding = 'utf-8'
54 self.encoding = 'utf-8'
55
55
56 def before(self):
56 def before(self):
57 pass
57 pass
58
58
59 def after(self):
59 def after(self):
60 pass
60 pass
61
61
62 def setrevmap(self, revmap):
62 def setrevmap(self, revmap):
63 """set the map of already-converted revisions"""
63 """set the map of already-converted revisions"""
64 pass
64 pass
65
65
66 def getheads(self):
66 def getheads(self):
67 """Return a list of this repository's heads"""
67 """Return a list of this repository's heads"""
68 raise NotImplementedError()
68 raise NotImplementedError()
69
69
70 def getfile(self, name, rev):
70 def getfile(self, name, rev):
71 """Return file contents as a string"""
71 """Return file contents as a string"""
72 raise NotImplementedError()
72 raise NotImplementedError()
73
73
74 def getmode(self, name, rev):
74 def getmode(self, name, rev):
75 """Return file mode, eg. '', 'x', or 'l'"""
75 """Return file mode, eg. '', 'x', or 'l'"""
76 raise NotImplementedError()
76 raise NotImplementedError()
77
77
78 def getchanges(self, version):
78 def getchanges(self, version):
79 """Returns a tuple of (files, copies)
79 """Returns a tuple of (files, copies)
80 Files is a sorted list of (filename, id) tuples for all files changed
80 Files is a sorted list of (filename, id) tuples for all files changed
81 in version, where id is the source revision id of the file.
81 in version, where id is the source revision id of the file.
82
82
83 copies is a dictionary of dest: source
83 copies is a dictionary of dest: source
84 """
84 """
85 raise NotImplementedError()
85 raise NotImplementedError()
86
86
87 def getcommit(self, version):
87 def getcommit(self, version):
88 """Return the commit object for version"""
88 """Return the commit object for version"""
89 raise NotImplementedError()
89 raise NotImplementedError()
90
90
91 def gettags(self):
91 def gettags(self):
92 """Return the tags as a dictionary of name: revision"""
92 """Return the tags as a dictionary of name: revision"""
93 raise NotImplementedError()
93 raise NotImplementedError()
94
94
95 def recode(self, s, encoding=None):
95 def recode(self, s, encoding=None):
96 if not encoding:
96 if not encoding:
97 encoding = self.encoding or 'utf-8'
97 encoding = self.encoding or 'utf-8'
98
98
99 if isinstance(s, unicode):
99 if isinstance(s, unicode):
100 return s.encode("utf-8")
100 return s.encode("utf-8")
101 try:
101 try:
102 return s.decode(encoding).encode("utf-8")
102 return s.decode(encoding).encode("utf-8")
103 except:
103 except:
104 try:
104 try:
105 return s.decode("latin-1").encode("utf-8")
105 return s.decode("latin-1").encode("utf-8")
106 except:
106 except:
107 return s.decode(encoding, "replace").encode("utf-8")
107 return s.decode(encoding, "replace").encode("utf-8")
108
108
109 def getchangedfiles(self, rev, i):
109 def getchangedfiles(self, rev, i):
110 """Return the files changed by rev compared to parent[i].
110 """Return the files changed by rev compared to parent[i].
111
111
112 i is an index selecting one of the parents of rev. The return
112 i is an index selecting one of the parents of rev. The return
113 value should be the list of files that are different in rev and
113 value should be the list of files that are different in rev and
114 this parent.
114 this parent.
115
115
116 If rev has no parents, i is None.
116 If rev has no parents, i is None.
117
117
118 This function is only needed to support --filemap
118 This function is only needed to support --filemap
119 """
119 """
120 raise NotImplementedError()
120 raise NotImplementedError()
121
121
122 def converted(self, rev, sinkrev):
122 def converted(self, rev, sinkrev):
123 '''Notify the source that a revision has been converted.'''
123 '''Notify the source that a revision has been converted.'''
124 pass
124 pass
125
125
126
126
127 class converter_sink(object):
127 class converter_sink(object):
128 """Conversion sink (target) interface"""
128 """Conversion sink (target) interface"""
129
129
130 def __init__(self, ui, path):
130 def __init__(self, ui, path):
131 """Initialize conversion sink (or raise NoRepo("message")
131 """Initialize conversion sink (or raise NoRepo("message")
132 exception if path is not a valid repository)
132 exception if path is not a valid repository)
133
133
134 created is a list of paths to remove if a fatal error occurs
134 created is a list of paths to remove if a fatal error occurs
135 later"""
135 later"""
136 self.ui = ui
136 self.ui = ui
137 self.path = path
137 self.path = path
138 self.created = []
138 self.created = []
139
139
140 def getheads(self):
140 def getheads(self):
141 """Return a list of this repository's heads"""
141 """Return a list of this repository's heads"""
142 raise NotImplementedError()
142 raise NotImplementedError()
143
143
144 def revmapfile(self):
144 def revmapfile(self):
145 """Path to a file that will contain lines
145 """Path to a file that will contain lines
146 source_rev_id sink_rev_id
146 source_rev_id sink_rev_id
147 mapping equivalent revision identifiers for each system."""
147 mapping equivalent revision identifiers for each system."""
148 raise NotImplementedError()
148 raise NotImplementedError()
149
149
150 def authorfile(self):
150 def authorfile(self):
151 """Path to a file that will contain lines
151 """Path to a file that will contain lines
152 srcauthor=dstauthor
152 srcauthor=dstauthor
153 mapping equivalent authors identifiers for each system."""
153 mapping equivalent authors identifiers for each system."""
154 return None
154 return None
155
155
156 def putfile(self, f, e, data):
156 def putcommit(self, files, copies, parents, commit, source):
157 """Put file for next putcommit().
158 f: path to file
159 e: '', 'x', or 'l' (regular file, executable, or symlink)
160 data: file contents"""
161 raise NotImplementedError()
162
163 def delfile(self, f):
164 """Delete file for next putcommit().
165 f: path to file"""
166 raise NotImplementedError()
167
168 def putcommit(self, files, parents, commit):
169 """Create a revision with all changed files listed in 'files'
157 """Create a revision with all changed files listed in 'files'
170 and having listed parents. 'commit' is a commit object containing
158 and having listed parents. 'commit' is a commit object containing
171 at a minimum the author, date, and message for this changeset.
159 at a minimum the author, date, and message for this changeset.
172 Called after putfile() and delfile() calls. Note that the sink
160 'files' is a list of (path, version) tuples, 'copies'is a dictionary
173 repository is not told to update itself to a particular revision
161 mapping destinations to sources, and 'source' is the source repository.
174 (or even what that revision would be) before it receives the
162 Only getfile() and getmode() should be called on 'source'.
175 file data."""
163
164 Note that the sink repository is not told to update itself to
165 a particular revision (or even what that revision would be)
166 before it receives the file data.
167 """
176 raise NotImplementedError()
168 raise NotImplementedError()
177
169
178 def puttags(self, tags):
170 def puttags(self, tags):
179 """Put tags into sink.
171 """Put tags into sink.
180 tags: {tagname: sink_rev_id, ...}"""
172 tags: {tagname: sink_rev_id, ...}"""
181 raise NotImplementedError()
173 raise NotImplementedError()
182
174
183 def setbranch(self, branch, pbranches):
175 def setbranch(self, branch, pbranches):
184 """Set the current branch name. Called before the first putfile
176 """Set the current branch name. Called before the first putcommit
185 on the branch.
177 on the branch.
186 branch: branch name for subsequent commits
178 branch: branch name for subsequent commits
187 pbranches: (converted parent revision, parent branch) tuples"""
179 pbranches: (converted parent revision, parent branch) tuples"""
188 pass
180 pass
189
181
190 def setfilemapmode(self, active):
182 def setfilemapmode(self, active):
191 """Tell the destination that we're using a filemap
183 """Tell the destination that we're using a filemap
192
184
193 Some converter_sources (svn in particular) can claim that a file
185 Some converter_sources (svn in particular) can claim that a file
194 was changed in a revision, even if there was no change. This method
186 was changed in a revision, even if there was no change. This method
195 tells the destination that we're using a filemap and that it should
187 tells the destination that we're using a filemap and that it should
196 filter empty revisions.
188 filter empty revisions.
197 """
189 """
198 pass
190 pass
199
191
200 def before(self):
192 def before(self):
201 pass
193 pass
202
194
203 def after(self):
195 def after(self):
204 pass
196 pass
205
197
206
198
207 class commandline(object):
199 class commandline(object):
208 def __init__(self, ui, command):
200 def __init__(self, ui, command):
209 self.ui = ui
201 self.ui = ui
210 self.command = command
202 self.command = command
211
203
212 def prerun(self):
204 def prerun(self):
213 pass
205 pass
214
206
215 def postrun(self):
207 def postrun(self):
216 pass
208 pass
217
209
218 def _cmdline(self, cmd, *args, **kwargs):
210 def _cmdline(self, cmd, *args, **kwargs):
219 cmdline = [self.command, cmd] + list(args)
211 cmdline = [self.command, cmd] + list(args)
220 for k, v in kwargs.iteritems():
212 for k, v in kwargs.iteritems():
221 if len(k) == 1:
213 if len(k) == 1:
222 cmdline.append('-' + k)
214 cmdline.append('-' + k)
223 else:
215 else:
224 cmdline.append('--' + k.replace('_', '-'))
216 cmdline.append('--' + k.replace('_', '-'))
225 try:
217 try:
226 if len(k) == 1:
218 if len(k) == 1:
227 cmdline.append('' + v)
219 cmdline.append('' + v)
228 else:
220 else:
229 cmdline[-1] += '=' + v
221 cmdline[-1] += '=' + v
230 except TypeError:
222 except TypeError:
231 pass
223 pass
232 cmdline = [util.shellquote(arg) for arg in cmdline]
224 cmdline = [util.shellquote(arg) for arg in cmdline]
233 cmdline += ['2>', util.nulldev, '<', util.nulldev]
225 cmdline += ['2>', util.nulldev, '<', util.nulldev]
234 cmdline = ' '.join(cmdline)
226 cmdline = ' '.join(cmdline)
235 self.ui.debug(cmdline, '\n')
227 self.ui.debug(cmdline, '\n')
236 return cmdline
228 return cmdline
237
229
238 def _run(self, cmd, *args, **kwargs):
230 def _run(self, cmd, *args, **kwargs):
239 cmdline = self._cmdline(cmd, *args, **kwargs)
231 cmdline = self._cmdline(cmd, *args, **kwargs)
240 self.prerun()
232 self.prerun()
241 try:
233 try:
242 return util.popen(cmdline)
234 return util.popen(cmdline)
243 finally:
235 finally:
244 self.postrun()
236 self.postrun()
245
237
246 def run(self, cmd, *args, **kwargs):
238 def run(self, cmd, *args, **kwargs):
247 fp = self._run(cmd, *args, **kwargs)
239 fp = self._run(cmd, *args, **kwargs)
248 output = fp.read()
240 output = fp.read()
249 self.ui.debug(output)
241 self.ui.debug(output)
250 return output, fp.close()
242 return output, fp.close()
251
243
252 def runlines(self, cmd, *args, **kwargs):
244 def runlines(self, cmd, *args, **kwargs):
253 fp = self._run(cmd, *args, **kwargs)
245 fp = self._run(cmd, *args, **kwargs)
254 output = fp.readlines()
246 output = fp.readlines()
255 self.ui.debug(''.join(output))
247 self.ui.debug(''.join(output))
256 return output, fp.close()
248 return output, fp.close()
257
249
258 def checkexit(self, status, output=''):
250 def checkexit(self, status, output=''):
259 if status:
251 if status:
260 if output:
252 if output:
261 self.ui.warn(_('%s error:\n') % self.command)
253 self.ui.warn(_('%s error:\n') % self.command)
262 self.ui.warn(output)
254 self.ui.warn(output)
263 msg = util.explain_exit(status)[0]
255 msg = util.explain_exit(status)[0]
264 raise util.Abort(_('%s %s') % (self.command, msg))
256 raise util.Abort(_('%s %s') % (self.command, msg))
265
257
266 def run0(self, cmd, *args, **kwargs):
258 def run0(self, cmd, *args, **kwargs):
267 output, status = self.run(cmd, *args, **kwargs)
259 output, status = self.run(cmd, *args, **kwargs)
268 self.checkexit(status, output)
260 self.checkexit(status, output)
269 return output
261 return output
270
262
271 def runlines0(self, cmd, *args, **kwargs):
263 def runlines0(self, cmd, *args, **kwargs):
272 output, status = self.runlines(cmd, *args, **kwargs)
264 output, status = self.runlines(cmd, *args, **kwargs)
273 self.checkexit(status, ''.join(output))
265 self.checkexit(status, ''.join(output))
274 return output
266 return output
275
267
276 def getargmax(self):
268 def getargmax(self):
277 if '_argmax' in self.__dict__:
269 if '_argmax' in self.__dict__:
278 return self._argmax
270 return self._argmax
279
271
280 # POSIX requires at least 4096 bytes for ARG_MAX
272 # POSIX requires at least 4096 bytes for ARG_MAX
281 self._argmax = 4096
273 self._argmax = 4096
282 try:
274 try:
283 self._argmax = os.sysconf("SC_ARG_MAX")
275 self._argmax = os.sysconf("SC_ARG_MAX")
284 except:
276 except:
285 pass
277 pass
286
278
287 # Windows shells impose their own limits on command line length,
279 # Windows shells impose their own limits on command line length,
288 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
280 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
289 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
281 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
290 # details about cmd.exe limitations.
282 # details about cmd.exe limitations.
291
283
292 # Since ARG_MAX is for command line _and_ environment, lower our limit
284 # Since ARG_MAX is for command line _and_ environment, lower our limit
293 # (and make happy Windows shells while doing this).
285 # (and make happy Windows shells while doing this).
294
286
295 self._argmax = self._argmax/2 - 1
287 self._argmax = self._argmax/2 - 1
296 return self._argmax
288 return self._argmax
297
289
298 def limit_arglist(self, arglist, cmd, *args, **kwargs):
290 def limit_arglist(self, arglist, cmd, *args, **kwargs):
299 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
291 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
300 bytes = 0
292 bytes = 0
301 fl = []
293 fl = []
302 for fn in arglist:
294 for fn in arglist:
303 b = len(fn) + 3
295 b = len(fn) + 3
304 if bytes + b < limit or len(fl) == 0:
296 if bytes + b < limit or len(fl) == 0:
305 fl.append(fn)
297 fl.append(fn)
306 bytes += b
298 bytes += b
307 else:
299 else:
308 yield fl
300 yield fl
309 fl = [fn]
301 fl = [fn]
310 bytes = b
302 bytes = b
311 if fl:
303 if fl:
312 yield fl
304 yield fl
313
305
314 def xargs(self, arglist, cmd, *args, **kwargs):
306 def xargs(self, arglist, cmd, *args, **kwargs):
315 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
307 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
316 self.run0(cmd, *(list(args) + l), **kwargs)
308 self.run0(cmd, *(list(args) + l), **kwargs)
317
309
318 class mapfile(dict):
310 class mapfile(dict):
319 def __init__(self, ui, path):
311 def __init__(self, ui, path):
320 super(mapfile, self).__init__()
312 super(mapfile, self).__init__()
321 self.ui = ui
313 self.ui = ui
322 self.path = path
314 self.path = path
323 self.fp = None
315 self.fp = None
324 self.order = []
316 self.order = []
325 self._read()
317 self._read()
326
318
327 def _read(self):
319 def _read(self):
328 if self.path is None:
320 if self.path is None:
329 return
321 return
330 try:
322 try:
331 fp = open(self.path, 'r')
323 fp = open(self.path, 'r')
332 except IOError, err:
324 except IOError, err:
333 if err.errno != errno.ENOENT:
325 if err.errno != errno.ENOENT:
334 raise
326 raise
335 return
327 return
336 for line in fp:
328 for line in fp:
337 key, value = line[:-1].split(' ', 1)
329 key, value = line[:-1].split(' ', 1)
338 if key not in self:
330 if key not in self:
339 self.order.append(key)
331 self.order.append(key)
340 super(mapfile, self).__setitem__(key, value)
332 super(mapfile, self).__setitem__(key, value)
341 fp.close()
333 fp.close()
342
334
343 def __setitem__(self, key, value):
335 def __setitem__(self, key, value):
344 if self.fp is None:
336 if self.fp is None:
345 try:
337 try:
346 self.fp = open(self.path, 'a')
338 self.fp = open(self.path, 'a')
347 except IOError, err:
339 except IOError, err:
348 raise util.Abort(_('could not open map file %r: %s') %
340 raise util.Abort(_('could not open map file %r: %s') %
349 (self.path, err.strerror))
341 (self.path, err.strerror))
350 self.fp.write('%s %s\n' % (key, value))
342 self.fp.write('%s %s\n' % (key, value))
351 self.fp.flush()
343 self.fp.flush()
352 super(mapfile, self).__setitem__(key, value)
344 super(mapfile, self).__setitem__(key, value)
353
345
354 def close(self):
346 def close(self):
355 if self.fp:
347 if self.fp:
356 self.fp.close()
348 self.fp.close()
357 self.fp = None
349 self.fp = None
@@ -1,354 +1,337 b''
1 # convcmd - convert extension commands definition
1 # convcmd - convert extension commands definition
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
9 from cvs import convert_cvs
9 from cvs import convert_cvs
10 from darcs import darcs_source
10 from darcs import darcs_source
11 from git import convert_git
11 from git import convert_git
12 from hg import mercurial_source, mercurial_sink
12 from hg import mercurial_source, mercurial_sink
13 from subversion import debugsvnlog, svn_source, svn_sink
13 from subversion import debugsvnlog, svn_source, svn_sink
14 from monotone import monotone_source
14 from monotone import monotone_source
15 from gnuarch import gnuarch_source
15 from gnuarch import gnuarch_source
16 import filemap
16 import filemap
17
17
18 import os, shutil
18 import os, shutil
19 from mercurial import hg, util
19 from mercurial import hg, util
20 from mercurial.i18n import _
20 from mercurial.i18n import _
21
21
22 orig_encoding = 'ascii'
22 orig_encoding = 'ascii'
23
23
24 def recode(s):
24 def recode(s):
25 if isinstance(s, unicode):
25 if isinstance(s, unicode):
26 return s.encode(orig_encoding, 'replace')
26 return s.encode(orig_encoding, 'replace')
27 else:
27 else:
28 return s.decode('utf-8').encode(orig_encoding, 'replace')
28 return s.decode('utf-8').encode(orig_encoding, 'replace')
29
29
30 source_converters = [
30 source_converters = [
31 ('cvs', convert_cvs),
31 ('cvs', convert_cvs),
32 ('git', convert_git),
32 ('git', convert_git),
33 ('svn', svn_source),
33 ('svn', svn_source),
34 ('hg', mercurial_source),
34 ('hg', mercurial_source),
35 ('darcs', darcs_source),
35 ('darcs', darcs_source),
36 ('mtn', monotone_source),
36 ('mtn', monotone_source),
37 ('gnuarch', gnuarch_source),
37 ('gnuarch', gnuarch_source),
38 ]
38 ]
39
39
40 sink_converters = [
40 sink_converters = [
41 ('hg', mercurial_sink),
41 ('hg', mercurial_sink),
42 ('svn', svn_sink),
42 ('svn', svn_sink),
43 ]
43 ]
44
44
45 def convertsource(ui, path, type, rev):
45 def convertsource(ui, path, type, rev):
46 exceptions = []
46 exceptions = []
47 for name, source in source_converters:
47 for name, source in source_converters:
48 try:
48 try:
49 if not type or name == type:
49 if not type or name == type:
50 return source(ui, path, rev)
50 return source(ui, path, rev)
51 except (NoRepo, MissingTool), inst:
51 except (NoRepo, MissingTool), inst:
52 exceptions.append(inst)
52 exceptions.append(inst)
53 if not ui.quiet:
53 if not ui.quiet:
54 for inst in exceptions:
54 for inst in exceptions:
55 ui.write(_("%s\n") % inst)
55 ui.write(_("%s\n") % inst)
56 raise util.Abort('%s: unknown repository type' % path)
56 raise util.Abort('%s: unknown repository type' % path)
57
57
58 def convertsink(ui, path, type):
58 def convertsink(ui, path, type):
59 for name, sink in sink_converters:
59 for name, sink in sink_converters:
60 try:
60 try:
61 if not type or name == type:
61 if not type or name == type:
62 return sink(ui, path)
62 return sink(ui, path)
63 except NoRepo, inst:
63 except NoRepo, inst:
64 ui.note(_("convert: %s\n") % inst)
64 ui.note(_("convert: %s\n") % inst)
65 raise util.Abort('%s: unknown repository type' % path)
65 raise util.Abort('%s: unknown repository type' % path)
66
66
67 class converter(object):
67 class converter(object):
68 def __init__(self, ui, source, dest, revmapfile, opts):
68 def __init__(self, ui, source, dest, revmapfile, opts):
69
69
70 self.source = source
70 self.source = source
71 self.dest = dest
71 self.dest = dest
72 self.ui = ui
72 self.ui = ui
73 self.opts = opts
73 self.opts = opts
74 self.commitcache = {}
74 self.commitcache = {}
75 self.authors = {}
75 self.authors = {}
76 self.authorfile = None
76 self.authorfile = None
77
77
78 self.map = mapfile(ui, revmapfile)
78 self.map = mapfile(ui, revmapfile)
79
79
80 # Read first the dst author map if any
80 # Read first the dst author map if any
81 authorfile = self.dest.authorfile()
81 authorfile = self.dest.authorfile()
82 if authorfile and os.path.exists(authorfile):
82 if authorfile and os.path.exists(authorfile):
83 self.readauthormap(authorfile)
83 self.readauthormap(authorfile)
84 # Extend/Override with new author map if necessary
84 # Extend/Override with new author map if necessary
85 if opts.get('authors'):
85 if opts.get('authors'):
86 self.readauthormap(opts.get('authors'))
86 self.readauthormap(opts.get('authors'))
87 self.authorfile = self.dest.authorfile()
87 self.authorfile = self.dest.authorfile()
88
88
89 self.splicemap = mapfile(ui, opts.get('splicemap'))
89 self.splicemap = mapfile(ui, opts.get('splicemap'))
90
90
91 def walktree(self, heads):
91 def walktree(self, heads):
92 '''Return a mapping that identifies the uncommitted parents of every
92 '''Return a mapping that identifies the uncommitted parents of every
93 uncommitted changeset.'''
93 uncommitted changeset.'''
94 visit = heads
94 visit = heads
95 known = {}
95 known = {}
96 parents = {}
96 parents = {}
97 while visit:
97 while visit:
98 n = visit.pop(0)
98 n = visit.pop(0)
99 if n in known or n in self.map: continue
99 if n in known or n in self.map: continue
100 known[n] = 1
100 known[n] = 1
101 commit = self.cachecommit(n)
101 commit = self.cachecommit(n)
102 parents[n] = []
102 parents[n] = []
103 for p in commit.parents:
103 for p in commit.parents:
104 parents[n].append(p)
104 parents[n].append(p)
105 visit.append(p)
105 visit.append(p)
106
106
107 return parents
107 return parents
108
108
109 def toposort(self, parents):
109 def toposort(self, parents):
110 '''Return an ordering such that every uncommitted changeset is
110 '''Return an ordering such that every uncommitted changeset is
111 preceeded by all its uncommitted ancestors.'''
111 preceeded by all its uncommitted ancestors.'''
112 visit = parents.keys()
112 visit = parents.keys()
113 seen = {}
113 seen = {}
114 children = {}
114 children = {}
115 actives = []
115 actives = []
116
116
117 while visit:
117 while visit:
118 n = visit.pop(0)
118 n = visit.pop(0)
119 if n in seen: continue
119 if n in seen: continue
120 seen[n] = 1
120 seen[n] = 1
121 # Ensure that nodes without parents are present in the 'children'
121 # Ensure that nodes without parents are present in the 'children'
122 # mapping.
122 # mapping.
123 children.setdefault(n, [])
123 children.setdefault(n, [])
124 hasparent = False
124 hasparent = False
125 for p in parents[n]:
125 for p in parents[n]:
126 if not p in self.map:
126 if not p in self.map:
127 visit.append(p)
127 visit.append(p)
128 hasparent = True
128 hasparent = True
129 children.setdefault(p, []).append(n)
129 children.setdefault(p, []).append(n)
130 if not hasparent:
130 if not hasparent:
131 actives.append(n)
131 actives.append(n)
132
132
133 del seen
133 del seen
134 del visit
134 del visit
135
135
136 if self.opts.get('datesort'):
136 if self.opts.get('datesort'):
137 dates = {}
137 dates = {}
138 def getdate(n):
138 def getdate(n):
139 if n not in dates:
139 if n not in dates:
140 dates[n] = util.parsedate(self.commitcache[n].date)
140 dates[n] = util.parsedate(self.commitcache[n].date)
141 return dates[n]
141 return dates[n]
142
142
143 def picknext(nodes):
143 def picknext(nodes):
144 return min([(getdate(n), n) for n in nodes])[1]
144 return min([(getdate(n), n) for n in nodes])[1]
145 else:
145 else:
146 prev = [None]
146 prev = [None]
147 def picknext(nodes):
147 def picknext(nodes):
148 # Return the first eligible child of the previously converted
148 # Return the first eligible child of the previously converted
149 # revision, or any of them.
149 # revision, or any of them.
150 next = nodes[0]
150 next = nodes[0]
151 for n in nodes:
151 for n in nodes:
152 if prev[0] in parents[n]:
152 if prev[0] in parents[n]:
153 next = n
153 next = n
154 break
154 break
155 prev[0] = next
155 prev[0] = next
156 return next
156 return next
157
157
158 s = []
158 s = []
159 pendings = {}
159 pendings = {}
160 while actives:
160 while actives:
161 n = picknext(actives)
161 n = picknext(actives)
162 actives.remove(n)
162 actives.remove(n)
163 s.append(n)
163 s.append(n)
164
164
165 # Update dependents list
165 # Update dependents list
166 for c in children.get(n, []):
166 for c in children.get(n, []):
167 if c not in pendings:
167 if c not in pendings:
168 pendings[c] = [p for p in parents[c] if p not in self.map]
168 pendings[c] = [p for p in parents[c] if p not in self.map]
169 try:
169 try:
170 pendings[c].remove(n)
170 pendings[c].remove(n)
171 except ValueError:
171 except ValueError:
172 raise util.Abort(_('cycle detected between %s and %s')
172 raise util.Abort(_('cycle detected between %s and %s')
173 % (recode(c), recode(n)))
173 % (recode(c), recode(n)))
174 if not pendings[c]:
174 if not pendings[c]:
175 # Parents are converted, node is eligible
175 # Parents are converted, node is eligible
176 actives.insert(0, c)
176 actives.insert(0, c)
177 pendings[c] = None
177 pendings[c] = None
178
178
179 if len(s) != len(parents):
179 if len(s) != len(parents):
180 raise util.Abort(_("not all revisions were sorted"))
180 raise util.Abort(_("not all revisions were sorted"))
181
181
182 return s
182 return s
183
183
184 def writeauthormap(self):
184 def writeauthormap(self):
185 authorfile = self.authorfile
185 authorfile = self.authorfile
186 if authorfile:
186 if authorfile:
187 self.ui.status('Writing author map file %s\n' % authorfile)
187 self.ui.status('Writing author map file %s\n' % authorfile)
188 ofile = open(authorfile, 'w+')
188 ofile = open(authorfile, 'w+')
189 for author in self.authors:
189 for author in self.authors:
190 ofile.write("%s=%s\n" % (author, self.authors[author]))
190 ofile.write("%s=%s\n" % (author, self.authors[author]))
191 ofile.close()
191 ofile.close()
192
192
193 def readauthormap(self, authorfile):
193 def readauthormap(self, authorfile):
194 afile = open(authorfile, 'r')
194 afile = open(authorfile, 'r')
195 for line in afile:
195 for line in afile:
196 if line.strip() == '':
196 if line.strip() == '':
197 continue
197 continue
198 try:
198 try:
199 srcauthor, dstauthor = line.split('=', 1)
199 srcauthor, dstauthor = line.split('=', 1)
200 srcauthor = srcauthor.strip()
200 srcauthor = srcauthor.strip()
201 dstauthor = dstauthor.strip()
201 dstauthor = dstauthor.strip()
202 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
202 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
203 self.ui.status(
203 self.ui.status(
204 'Overriding mapping for author %s, was %s, will be %s\n'
204 'Overriding mapping for author %s, was %s, will be %s\n'
205 % (srcauthor, self.authors[srcauthor], dstauthor))
205 % (srcauthor, self.authors[srcauthor], dstauthor))
206 else:
206 else:
207 self.ui.debug('Mapping author %s to %s\n'
207 self.ui.debug('Mapping author %s to %s\n'
208 % (srcauthor, dstauthor))
208 % (srcauthor, dstauthor))
209 self.authors[srcauthor] = dstauthor
209 self.authors[srcauthor] = dstauthor
210 except IndexError:
210 except IndexError:
211 self.ui.warn(
211 self.ui.warn(
212 'Ignoring bad line in author map file %s: %s\n'
212 'Ignoring bad line in author map file %s: %s\n'
213 % (authorfile, line.rstrip()))
213 % (authorfile, line.rstrip()))
214 afile.close()
214 afile.close()
215
215
216 def cachecommit(self, rev):
216 def cachecommit(self, rev):
217 commit = self.source.getcommit(rev)
217 commit = self.source.getcommit(rev)
218 commit.author = self.authors.get(commit.author, commit.author)
218 commit.author = self.authors.get(commit.author, commit.author)
219 self.commitcache[rev] = commit
219 self.commitcache[rev] = commit
220 return commit
220 return commit
221
221
222 def copy(self, rev):
222 def copy(self, rev):
223 commit = self.commitcache[rev]
223 commit = self.commitcache[rev]
224 do_copies = hasattr(self.dest, 'copyfile')
225 filenames = []
226
224
227 changes = self.source.getchanges(rev)
225 changes = self.source.getchanges(rev)
228 if isinstance(changes, basestring):
226 if isinstance(changes, basestring):
229 if changes == SKIPREV:
227 if changes == SKIPREV:
230 dest = SKIPREV
228 dest = SKIPREV
231 else:
229 else:
232 dest = self.map[changes]
230 dest = self.map[changes]
233 self.map[rev] = dest
231 self.map[rev] = dest
234 return
232 return
235 files, copies = changes
233 files, copies = changes
236 pbranches = []
234 pbranches = []
237 if commit.parents:
235 if commit.parents:
238 for prev in commit.parents:
236 for prev in commit.parents:
239 if prev not in self.commitcache:
237 if prev not in self.commitcache:
240 self.cachecommit(prev)
238 self.cachecommit(prev)
241 pbranches.append((self.map[prev],
239 pbranches.append((self.map[prev],
242 self.commitcache[prev].branch))
240 self.commitcache[prev].branch))
243 self.dest.setbranch(commit.branch, pbranches)
241 self.dest.setbranch(commit.branch, pbranches)
244 for f, v in files:
245 filenames.append(f)
246 try:
247 data = self.source.getfile(f, v)
248 except IOError, inst:
249 self.dest.delfile(f)
250 else:
251 e = self.source.getmode(f, v)
252 self.dest.putfile(f, e, data)
253 if do_copies:
254 if f in copies:
255 copyf = copies[f]
256 # Merely marks that a copy happened.
257 self.dest.copyfile(copyf, f)
258
259 try:
242 try:
260 parents = self.splicemap[rev].replace(',', ' ').split()
243 parents = self.splicemap[rev].replace(',', ' ').split()
261 self.ui.status('spliced in %s as parents of %s\n' %
244 self.ui.status('spliced in %s as parents of %s\n' %
262 (parents, rev))
245 (parents, rev))
263 parents = [self.map.get(p, p) for p in parents]
246 parents = [self.map.get(p, p) for p in parents]
264 except KeyError:
247 except KeyError:
265 parents = [b[0] for b in pbranches]
248 parents = [b[0] for b in pbranches]
266 newnode = self.dest.putcommit(filenames, parents, commit)
249 newnode = self.dest.putcommit(files, copies, parents, commit, self.source)
267 self.source.converted(rev, newnode)
250 self.source.converted(rev, newnode)
268 self.map[rev] = newnode
251 self.map[rev] = newnode
269
252
270 def convert(self):
253 def convert(self):
271
254
272 try:
255 try:
273 self.source.before()
256 self.source.before()
274 self.dest.before()
257 self.dest.before()
275 self.source.setrevmap(self.map)
258 self.source.setrevmap(self.map)
276 self.ui.status("scanning source...\n")
259 self.ui.status("scanning source...\n")
277 heads = self.source.getheads()
260 heads = self.source.getheads()
278 parents = self.walktree(heads)
261 parents = self.walktree(heads)
279 self.ui.status("sorting...\n")
262 self.ui.status("sorting...\n")
280 t = self.toposort(parents)
263 t = self.toposort(parents)
281 num = len(t)
264 num = len(t)
282 c = None
265 c = None
283
266
284 self.ui.status("converting...\n")
267 self.ui.status("converting...\n")
285 for c in t:
268 for c in t:
286 num -= 1
269 num -= 1
287 desc = self.commitcache[c].desc
270 desc = self.commitcache[c].desc
288 if "\n" in desc:
271 if "\n" in desc:
289 desc = desc.splitlines()[0]
272 desc = desc.splitlines()[0]
290 # convert log message to local encoding without using
273 # convert log message to local encoding without using
291 # tolocal() because util._encoding conver() use it as
274 # tolocal() because util._encoding conver() use it as
292 # 'utf-8'
275 # 'utf-8'
293 self.ui.status("%d %s\n" % (num, recode(desc)))
276 self.ui.status("%d %s\n" % (num, recode(desc)))
294 self.ui.note(_("source: %s\n" % recode(c)))
277 self.ui.note(_("source: %s\n" % recode(c)))
295 self.copy(c)
278 self.copy(c)
296
279
297 tags = self.source.gettags()
280 tags = self.source.gettags()
298 ctags = {}
281 ctags = {}
299 for k in tags:
282 for k in tags:
300 v = tags[k]
283 v = tags[k]
301 if self.map.get(v, SKIPREV) != SKIPREV:
284 if self.map.get(v, SKIPREV) != SKIPREV:
302 ctags[k] = self.map[v]
285 ctags[k] = self.map[v]
303
286
304 if c and ctags:
287 if c and ctags:
305 nrev = self.dest.puttags(ctags)
288 nrev = self.dest.puttags(ctags)
306 # write another hash correspondence to override the previous
289 # write another hash correspondence to override the previous
307 # one so we don't end up with extra tag heads
290 # one so we don't end up with extra tag heads
308 if nrev:
291 if nrev:
309 self.map[c] = nrev
292 self.map[c] = nrev
310
293
311 self.writeauthormap()
294 self.writeauthormap()
312 finally:
295 finally:
313 self.cleanup()
296 self.cleanup()
314
297
315 def cleanup(self):
298 def cleanup(self):
316 try:
299 try:
317 self.dest.after()
300 self.dest.after()
318 finally:
301 finally:
319 self.source.after()
302 self.source.after()
320 self.map.close()
303 self.map.close()
321
304
322 def convert(ui, src, dest=None, revmapfile=None, **opts):
305 def convert(ui, src, dest=None, revmapfile=None, **opts):
323 global orig_encoding
306 global orig_encoding
324 orig_encoding = util._encoding
307 orig_encoding = util._encoding
325 util._encoding = 'UTF-8'
308 util._encoding = 'UTF-8'
326
309
327 if not dest:
310 if not dest:
328 dest = hg.defaultdest(src) + "-hg"
311 dest = hg.defaultdest(src) + "-hg"
329 ui.status("assuming destination %s\n" % dest)
312 ui.status("assuming destination %s\n" % dest)
330
313
331 destc = convertsink(ui, dest, opts.get('dest_type'))
314 destc = convertsink(ui, dest, opts.get('dest_type'))
332
315
333 try:
316 try:
334 srcc = convertsource(ui, src, opts.get('source_type'),
317 srcc = convertsource(ui, src, opts.get('source_type'),
335 opts.get('rev'))
318 opts.get('rev'))
336 except Exception:
319 except Exception:
337 for path in destc.created:
320 for path in destc.created:
338 shutil.rmtree(path, True)
321 shutil.rmtree(path, True)
339 raise
322 raise
340
323
341 fmap = opts.get('filemap')
324 fmap = opts.get('filemap')
342 if fmap:
325 if fmap:
343 srcc = filemap.filemap_source(ui, srcc, fmap)
326 srcc = filemap.filemap_source(ui, srcc, fmap)
344 destc.setfilemapmode(True)
327 destc.setfilemapmode(True)
345
328
346 if not revmapfile:
329 if not revmapfile:
347 try:
330 try:
348 revmapfile = destc.revmapfile()
331 revmapfile = destc.revmapfile()
349 except:
332 except:
350 revmapfile = os.path.join(destc, "map")
333 revmapfile = os.path.join(destc, "map")
351
334
352 c = converter(ui, srcc, destc, revmapfile, opts)
335 c = converter(ui, srcc, destc, revmapfile, opts)
353 c.convert()
336 c.convert()
354
337
@@ -1,302 +1,305 b''
1 # hg backend for convert extension
1 # hg backend for convert extension
2
2
3 # Notes for hg->hg conversion:
3 # Notes for hg->hg conversion:
4 #
4 #
5 # * Old versions of Mercurial didn't trim the whitespace from the ends
5 # * Old versions of Mercurial didn't trim the whitespace from the ends
6 # of commit messages, but new versions do. Changesets created by
6 # of commit messages, but new versions do. Changesets created by
7 # those older versions, then converted, may thus have different
7 # those older versions, then converted, may thus have different
8 # hashes for changesets that are otherwise identical.
8 # hashes for changesets that are otherwise identical.
9 #
9 #
10 # * By default, the source revision is stored in the converted
10 # * By default, the source revision is stored in the converted
11 # revision. This will cause the converted revision to have a
11 # revision. This will cause the converted revision to have a
12 # different identity than the source. To avoid this, use the
12 # different identity than the source. To avoid this, use the
13 # following option: "--config convert.hg.saverev=false"
13 # following option: "--config convert.hg.saverev=false"
14
14
15
15
16 import os, time
16 import os, time
17 from mercurial.i18n import _
17 from mercurial.i18n import _
18 from mercurial.repo import RepoError
18 from mercurial.repo import RepoError
19 from mercurial.node import bin, hex, nullid
19 from mercurial.node import bin, hex, nullid
20 from mercurial import hg, revlog, util
20 from mercurial import hg, revlog, util
21
21
22 from common import NoRepo, commit, converter_source, converter_sink
22 from common import NoRepo, commit, converter_source, converter_sink
23
23
24 class mercurial_sink(converter_sink):
24 class mercurial_sink(converter_sink):
25 def __init__(self, ui, path):
25 def __init__(self, ui, path):
26 converter_sink.__init__(self, ui, path)
26 converter_sink.__init__(self, ui, path)
27 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
27 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
28 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
28 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
29 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
29 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
30 self.lastbranch = None
30 self.lastbranch = None
31 if os.path.isdir(path) and len(os.listdir(path)) > 0:
31 if os.path.isdir(path) and len(os.listdir(path)) > 0:
32 try:
32 try:
33 self.repo = hg.repository(self.ui, path)
33 self.repo = hg.repository(self.ui, path)
34 if not self.repo.local():
34 if not self.repo.local():
35 raise NoRepo(_('%s is not a local Mercurial repo') % path)
35 raise NoRepo(_('%s is not a local Mercurial repo') % path)
36 except RepoError, err:
36 except RepoError, err:
37 ui.print_exc()
37 ui.print_exc()
38 raise NoRepo(err.args[0])
38 raise NoRepo(err.args[0])
39 else:
39 else:
40 try:
40 try:
41 ui.status(_('initializing destination %s repository\n') % path)
41 ui.status(_('initializing destination %s repository\n') % path)
42 self.repo = hg.repository(self.ui, path, create=True)
42 self.repo = hg.repository(self.ui, path, create=True)
43 if not self.repo.local():
43 if not self.repo.local():
44 raise NoRepo(_('%s is not a local Mercurial repo') % path)
44 raise NoRepo(_('%s is not a local Mercurial repo') % path)
45 self.created.append(path)
45 self.created.append(path)
46 except RepoError, err:
46 except RepoError, err:
47 ui.print_exc()
47 ui.print_exc()
48 raise NoRepo("could not create hg repo %s as sink" % path)
48 raise NoRepo("could not create hg repo %s as sink" % path)
49 self.lock = None
49 self.lock = None
50 self.wlock = None
50 self.wlock = None
51 self.filemapmode = False
51 self.filemapmode = False
52
52
53 def before(self):
53 def before(self):
54 self.ui.debug(_('run hg sink pre-conversion action\n'))
54 self.ui.debug(_('run hg sink pre-conversion action\n'))
55 self.wlock = self.repo.wlock()
55 self.wlock = self.repo.wlock()
56 self.lock = self.repo.lock()
56 self.lock = self.repo.lock()
57 self.repo.dirstate.clear()
57 self.repo.dirstate.clear()
58
58
59 def after(self):
59 def after(self):
60 self.ui.debug(_('run hg sink post-conversion action\n'))
60 self.ui.debug(_('run hg sink post-conversion action\n'))
61 self.repo.dirstate.invalidate()
61 self.repo.dirstate.invalidate()
62 self.lock = None
62 self.lock = None
63 self.wlock = None
63 self.wlock = None
64
64
65 def revmapfile(self):
65 def revmapfile(self):
66 return os.path.join(self.path, ".hg", "shamap")
66 return os.path.join(self.path, ".hg", "shamap")
67
67
68 def authorfile(self):
68 def authorfile(self):
69 return os.path.join(self.path, ".hg", "authormap")
69 return os.path.join(self.path, ".hg", "authormap")
70
70
71 def getheads(self):
71 def getheads(self):
72 h = self.repo.changelog.heads()
72 h = self.repo.changelog.heads()
73 return [ hex(x) for x in h ]
73 return [ hex(x) for x in h ]
74
74
75 def putfile(self, f, e, data):
76 self.repo.wwrite(f, data, e)
77 if f not in self.repo.dirstate:
78 self.repo.dirstate.normallookup(f)
79
80 def copyfile(self, source, dest):
81 self.repo.copy(source, dest)
82
83 def delfile(self, f):
84 try:
85 util.unlink(self.repo.wjoin(f))
86 #self.repo.remove([f])
87 except OSError:
88 pass
89
90 def setbranch(self, branch, pbranches):
75 def setbranch(self, branch, pbranches):
91 if not self.clonebranches:
76 if not self.clonebranches:
92 return
77 return
93
78
94 setbranch = (branch != self.lastbranch)
79 setbranch = (branch != self.lastbranch)
95 self.lastbranch = branch
80 self.lastbranch = branch
96 if not branch:
81 if not branch:
97 branch = 'default'
82 branch = 'default'
98 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
83 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
99 pbranch = pbranches and pbranches[0][1] or 'default'
84 pbranch = pbranches and pbranches[0][1] or 'default'
100
85
101 branchpath = os.path.join(self.path, branch)
86 branchpath = os.path.join(self.path, branch)
102 if setbranch:
87 if setbranch:
103 self.after()
88 self.after()
104 try:
89 try:
105 self.repo = hg.repository(self.ui, branchpath)
90 self.repo = hg.repository(self.ui, branchpath)
106 except:
91 except:
107 self.repo = hg.repository(self.ui, branchpath, create=True)
92 self.repo = hg.repository(self.ui, branchpath, create=True)
108 self.before()
93 self.before()
109
94
110 # pbranches may bring revisions from other branches (merge parents)
95 # pbranches may bring revisions from other branches (merge parents)
111 # Make sure we have them, or pull them.
96 # Make sure we have them, or pull them.
112 missings = {}
97 missings = {}
113 for b in pbranches:
98 for b in pbranches:
114 try:
99 try:
115 self.repo.lookup(b[0])
100 self.repo.lookup(b[0])
116 except:
101 except:
117 missings.setdefault(b[1], []).append(b[0])
102 missings.setdefault(b[1], []).append(b[0])
118
103
119 if missings:
104 if missings:
120 self.after()
105 self.after()
121 for pbranch, heads in missings.iteritems():
106 for pbranch, heads in missings.iteritems():
122 pbranchpath = os.path.join(self.path, pbranch)
107 pbranchpath = os.path.join(self.path, pbranch)
123 prepo = hg.repository(self.ui, pbranchpath)
108 prepo = hg.repository(self.ui, pbranchpath)
124 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
109 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
125 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
110 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
126 self.before()
111 self.before()
127
112
128 def putcommit(self, files, parents, commit):
113 def putcommit(self, files, copies, parents, commit, source):
114 # Apply changes to working copy
115 for f, v in files:
116 try:
117 data = source.getfile(f, v)
118 except IOError, inst:
119 try:
120 util.unlink(self.repo.wjoin(f))
121 except OSError:
122 pass
123 else:
124 e = source.getmode(f, v)
125 self.repo.wwrite(f, data, e)
126 if f not in self.repo.dirstate:
127 self.repo.dirstate.normallookup(f)
128 if f in copies:
129 self.repo.copy(copies[f], f)
130 files = [f[0] for f in files]
131
129 seen = {}
132 seen = {}
130 pl = []
133 pl = []
131 for p in parents:
134 for p in parents:
132 if p not in seen:
135 if p not in seen:
133 pl.append(p)
136 pl.append(p)
134 seen[p] = 1
137 seen[p] = 1
135 parents = pl
138 parents = pl
136 nparents = len(parents)
139 nparents = len(parents)
137 if self.filemapmode and nparents == 1:
140 if self.filemapmode and nparents == 1:
138 m1node = self.repo.changelog.read(bin(parents[0]))[0]
141 m1node = self.repo.changelog.read(bin(parents[0]))[0]
139 parent = parents[0]
142 parent = parents[0]
140
143
141 if len(parents) < 2: parents.append("0" * 40)
144 if len(parents) < 2: parents.append("0" * 40)
142 if len(parents) < 2: parents.append("0" * 40)
145 if len(parents) < 2: parents.append("0" * 40)
143 p2 = parents.pop(0)
146 p2 = parents.pop(0)
144
147
145 text = commit.desc
148 text = commit.desc
146 extra = commit.extra.copy()
149 extra = commit.extra.copy()
147 if self.branchnames and commit.branch:
150 if self.branchnames and commit.branch:
148 extra['branch'] = commit.branch
151 extra['branch'] = commit.branch
149 if commit.rev:
152 if commit.rev:
150 extra['convert_revision'] = commit.rev
153 extra['convert_revision'] = commit.rev
151
154
152 while parents:
155 while parents:
153 p1 = p2
156 p1 = p2
154 p2 = parents.pop(0)
157 p2 = parents.pop(0)
155 a = self.repo.rawcommit(files, text, commit.author, commit.date,
158 a = self.repo.rawcommit(files, text, commit.author, commit.date,
156 bin(p1), bin(p2), extra=extra)
159 bin(p1), bin(p2), extra=extra)
157 self.repo.dirstate.clear()
160 self.repo.dirstate.clear()
158 text = "(octopus merge fixup)\n"
161 text = "(octopus merge fixup)\n"
159 p2 = hex(self.repo.changelog.tip())
162 p2 = hex(self.repo.changelog.tip())
160
163
161 if self.filemapmode and nparents == 1:
164 if self.filemapmode and nparents == 1:
162 man = self.repo.manifest
165 man = self.repo.manifest
163 mnode = self.repo.changelog.read(bin(p2))[0]
166 mnode = self.repo.changelog.read(bin(p2))[0]
164 if not man.cmp(m1node, man.revision(mnode)):
167 if not man.cmp(m1node, man.revision(mnode)):
165 self.repo.rollback()
168 self.repo.rollback()
166 self.repo.dirstate.clear()
169 self.repo.dirstate.clear()
167 return parent
170 return parent
168 return p2
171 return p2
169
172
170 def puttags(self, tags):
173 def puttags(self, tags):
171 try:
174 try:
172 old = self.repo.wfile(".hgtags").read()
175 old = self.repo.wfile(".hgtags").read()
173 oldlines = old.splitlines(1)
176 oldlines = old.splitlines(1)
174 oldlines.sort()
177 oldlines.sort()
175 except:
178 except:
176 oldlines = []
179 oldlines = []
177
180
178 k = tags.keys()
181 k = tags.keys()
179 k.sort()
182 k.sort()
180 newlines = []
183 newlines = []
181 for tag in k:
184 for tag in k:
182 newlines.append("%s %s\n" % (tags[tag], tag))
185 newlines.append("%s %s\n" % (tags[tag], tag))
183
186
184 newlines.sort()
187 newlines.sort()
185
188
186 if newlines != oldlines:
189 if newlines != oldlines:
187 self.ui.status("updating tags\n")
190 self.ui.status("updating tags\n")
188 f = self.repo.wfile(".hgtags", "w")
191 f = self.repo.wfile(".hgtags", "w")
189 f.write("".join(newlines))
192 f.write("".join(newlines))
190 f.close()
193 f.close()
191 if not oldlines: self.repo.add([".hgtags"])
194 if not oldlines: self.repo.add([".hgtags"])
192 date = "%s 0" % int(time.mktime(time.gmtime()))
195 date = "%s 0" % int(time.mktime(time.gmtime()))
193 extra = {}
196 extra = {}
194 if self.tagsbranch != 'default':
197 if self.tagsbranch != 'default':
195 extra['branch'] = self.tagsbranch
198 extra['branch'] = self.tagsbranch
196 try:
199 try:
197 tagparent = self.repo.changectx(self.tagsbranch).node()
200 tagparent = self.repo.changectx(self.tagsbranch).node()
198 except RepoError, inst:
201 except RepoError, inst:
199 tagparent = nullid
202 tagparent = nullid
200 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
203 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
201 date, tagparent, nullid, extra=extra)
204 date, tagparent, nullid, extra=extra)
202 return hex(self.repo.changelog.tip())
205 return hex(self.repo.changelog.tip())
203
206
204 def setfilemapmode(self, active):
207 def setfilemapmode(self, active):
205 self.filemapmode = active
208 self.filemapmode = active
206
209
207 class mercurial_source(converter_source):
210 class mercurial_source(converter_source):
208 def __init__(self, ui, path, rev=None):
211 def __init__(self, ui, path, rev=None):
209 converter_source.__init__(self, ui, path, rev)
212 converter_source.__init__(self, ui, path, rev)
210 self.saverev = ui.configbool('convert', 'hg.saverev', True)
213 self.saverev = ui.configbool('convert', 'hg.saverev', True)
211 try:
214 try:
212 self.repo = hg.repository(self.ui, path)
215 self.repo = hg.repository(self.ui, path)
213 # try to provoke an exception if this isn't really a hg
216 # try to provoke an exception if this isn't really a hg
214 # repo, but some other bogus compatible-looking url
217 # repo, but some other bogus compatible-looking url
215 if not self.repo.local():
218 if not self.repo.local():
216 raise RepoError()
219 raise RepoError()
217 except RepoError:
220 except RepoError:
218 ui.print_exc()
221 ui.print_exc()
219 raise NoRepo("%s is not a local Mercurial repo" % path)
222 raise NoRepo("%s is not a local Mercurial repo" % path)
220 self.lastrev = None
223 self.lastrev = None
221 self.lastctx = None
224 self.lastctx = None
222 self._changescache = None
225 self._changescache = None
223 self.convertfp = None
226 self.convertfp = None
224
227
225 def changectx(self, rev):
228 def changectx(self, rev):
226 if self.lastrev != rev:
229 if self.lastrev != rev:
227 self.lastctx = self.repo.changectx(rev)
230 self.lastctx = self.repo.changectx(rev)
228 self.lastrev = rev
231 self.lastrev = rev
229 return self.lastctx
232 return self.lastctx
230
233
231 def getheads(self):
234 def getheads(self):
232 if self.rev:
235 if self.rev:
233 return [hex(self.repo.changectx(self.rev).node())]
236 return [hex(self.repo.changectx(self.rev).node())]
234 else:
237 else:
235 return [hex(node) for node in self.repo.heads()]
238 return [hex(node) for node in self.repo.heads()]
236
239
237 def getfile(self, name, rev):
240 def getfile(self, name, rev):
238 try:
241 try:
239 return self.changectx(rev).filectx(name).data()
242 return self.changectx(rev).filectx(name).data()
240 except revlog.LookupError, err:
243 except revlog.LookupError, err:
241 raise IOError(err)
244 raise IOError(err)
242
245
243 def getmode(self, name, rev):
246 def getmode(self, name, rev):
244 m = self.changectx(rev).manifest()
247 m = self.changectx(rev).manifest()
245 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
248 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
246
249
247 def getchanges(self, rev):
250 def getchanges(self, rev):
248 ctx = self.changectx(rev)
251 ctx = self.changectx(rev)
249 if self._changescache and self._changescache[0] == rev:
252 if self._changescache and self._changescache[0] == rev:
250 m, a, r = self._changescache[1]
253 m, a, r = self._changescache[1]
251 else:
254 else:
252 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
255 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
253 changes = [(name, rev) for name in m + a + r]
256 changes = [(name, rev) for name in m + a + r]
254 changes.sort()
257 changes.sort()
255 return (changes, self.getcopies(ctx, m + a))
258 return (changes, self.getcopies(ctx, m + a))
256
259
257 def getcopies(self, ctx, files):
260 def getcopies(self, ctx, files):
258 copies = {}
261 copies = {}
259 for name in files:
262 for name in files:
260 try:
263 try:
261 copies[name] = ctx.filectx(name).renamed()[0]
264 copies[name] = ctx.filectx(name).renamed()[0]
262 except TypeError:
265 except TypeError:
263 pass
266 pass
264 return copies
267 return copies
265
268
266 def getcommit(self, rev):
269 def getcommit(self, rev):
267 ctx = self.changectx(rev)
270 ctx = self.changectx(rev)
268 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
271 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
269 if self.saverev:
272 if self.saverev:
270 crev = rev
273 crev = rev
271 else:
274 else:
272 crev = None
275 crev = None
273 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
276 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
274 desc=ctx.description(), rev=crev, parents=parents,
277 desc=ctx.description(), rev=crev, parents=parents,
275 branch=ctx.branch(), extra=ctx.extra())
278 branch=ctx.branch(), extra=ctx.extra())
276
279
277 def gettags(self):
280 def gettags(self):
278 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
281 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
279 return dict([(name, hex(node)) for name, node in tags])
282 return dict([(name, hex(node)) for name, node in tags])
280
283
281 def getchangedfiles(self, rev, i):
284 def getchangedfiles(self, rev, i):
282 ctx = self.changectx(rev)
285 ctx = self.changectx(rev)
283 i = i or 0
286 i = i or 0
284 changes = self.repo.status(ctx.parents()[i].node(), ctx.node())[:3]
287 changes = self.repo.status(ctx.parents()[i].node(), ctx.node())[:3]
285
288
286 if i == 0:
289 if i == 0:
287 self._changescache = (rev, changes)
290 self._changescache = (rev, changes)
288
291
289 return changes[0] + changes[1] + changes[2]
292 return changes[0] + changes[1] + changes[2]
290
293
291 def converted(self, rev, destrev):
294 def converted(self, rev, destrev):
292 if self.convertfp is None:
295 if self.convertfp is None:
293 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
296 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
294 'a')
297 'a')
295 self.convertfp.write('%s %s\n' % (destrev, rev))
298 self.convertfp.write('%s %s\n' % (destrev, rev))
296 self.convertfp.flush()
299 self.convertfp.flush()
297
300
298 def before(self):
301 def before(self):
299 self.ui.debug(_('run hg source pre-conversion action\n'))
302 self.ui.debug(_('run hg source pre-conversion action\n'))
300
303
301 def after(self):
304 def after(self):
302 self.ui.debug(_('run hg source post-conversion action\n'))
305 self.ui.debug(_('run hg source post-conversion action\n'))
@@ -1,1137 +1,1144 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 # Configuration options:
5 # Configuration options:
6 #
6 #
7 # convert.svn.trunk
7 # convert.svn.trunk
8 # Relative path to the trunk (default: "trunk")
8 # Relative path to the trunk (default: "trunk")
9 # convert.svn.branches
9 # convert.svn.branches
10 # Relative path to tree of branches (default: "branches")
10 # Relative path to tree of branches (default: "branches")
11 # convert.svn.tags
11 # convert.svn.tags
12 # Relative path to tree of tags (default: "tags")
12 # Relative path to tree of tags (default: "tags")
13 #
13 #
14 # Set these in a hgrc, or on the command line as follows:
14 # Set these in a hgrc, or on the command line as follows:
15 #
15 #
16 # hg convert --config convert.svn.trunk=wackoname [...]
16 # hg convert --config convert.svn.trunk=wackoname [...]
17
17
18 import locale
18 import locale
19 import os
19 import os
20 import re
20 import re
21 import sys
21 import sys
22 import cPickle as pickle
22 import cPickle as pickle
23 import tempfile
23 import tempfile
24
24
25 from mercurial import strutil, util
25 from mercurial import strutil, util
26 from mercurial.i18n import _
26 from mercurial.i18n import _
27
27
28 # Subversion stuff. Works best with very recent Python SVN bindings
28 # Subversion stuff. Works best with very recent Python SVN bindings
29 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
29 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
30 # these bindings.
30 # these bindings.
31
31
32 from cStringIO import StringIO
32 from cStringIO import StringIO
33
33
34 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
34 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
35 from common import commandline, converter_sink, mapfile
35 from common import commandline, converter_sink, mapfile
36
36
37 try:
37 try:
38 from svn.core import SubversionException, Pool
38 from svn.core import SubversionException, Pool
39 import svn
39 import svn
40 import svn.client
40 import svn.client
41 import svn.core
41 import svn.core
42 import svn.ra
42 import svn.ra
43 import svn.delta
43 import svn.delta
44 import transport
44 import transport
45 except ImportError:
45 except ImportError:
46 pass
46 pass
47
47
48 def geturl(path):
48 def geturl(path):
49 try:
49 try:
50 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
50 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
51 except SubversionException:
51 except SubversionException:
52 pass
52 pass
53 if os.path.isdir(path):
53 if os.path.isdir(path):
54 path = os.path.normpath(os.path.abspath(path))
54 path = os.path.normpath(os.path.abspath(path))
55 if os.name == 'nt':
55 if os.name == 'nt':
56 path = '/' + util.normpath(path)
56 path = '/' + util.normpath(path)
57 return 'file://%s' % path
57 return 'file://%s' % path
58 return path
58 return path
59
59
60 def optrev(number):
60 def optrev(number):
61 optrev = svn.core.svn_opt_revision_t()
61 optrev = svn.core.svn_opt_revision_t()
62 optrev.kind = svn.core.svn_opt_revision_number
62 optrev.kind = svn.core.svn_opt_revision_number
63 optrev.value.number = number
63 optrev.value.number = number
64 return optrev
64 return optrev
65
65
66 class changedpath(object):
66 class changedpath(object):
67 def __init__(self, p):
67 def __init__(self, p):
68 self.copyfrom_path = p.copyfrom_path
68 self.copyfrom_path = p.copyfrom_path
69 self.copyfrom_rev = p.copyfrom_rev
69 self.copyfrom_rev = p.copyfrom_rev
70 self.action = p.action
70 self.action = p.action
71
71
72 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
72 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
73 strict_node_history=False):
73 strict_node_history=False):
74 protocol = -1
74 protocol = -1
75 def receiver(orig_paths, revnum, author, date, message, pool):
75 def receiver(orig_paths, revnum, author, date, message, pool):
76 if orig_paths is not None:
76 if orig_paths is not None:
77 for k, v in orig_paths.iteritems():
77 for k, v in orig_paths.iteritems():
78 orig_paths[k] = changedpath(v)
78 orig_paths[k] = changedpath(v)
79 pickle.dump((orig_paths, revnum, author, date, message),
79 pickle.dump((orig_paths, revnum, author, date, message),
80 fp, protocol)
80 fp, protocol)
81
81
82 try:
82 try:
83 # Use an ra of our own so that our parent can consume
83 # Use an ra of our own so that our parent can consume
84 # our results without confusing the server.
84 # our results without confusing the server.
85 t = transport.SvnRaTransport(url=url)
85 t = transport.SvnRaTransport(url=url)
86 svn.ra.get_log(t.ra, paths, start, end, limit,
86 svn.ra.get_log(t.ra, paths, start, end, limit,
87 discover_changed_paths,
87 discover_changed_paths,
88 strict_node_history,
88 strict_node_history,
89 receiver)
89 receiver)
90 except SubversionException, (inst, num):
90 except SubversionException, (inst, num):
91 pickle.dump(num, fp, protocol)
91 pickle.dump(num, fp, protocol)
92 except IOError:
92 except IOError:
93 # Caller may interrupt the iteration
93 # Caller may interrupt the iteration
94 pickle.dump(None, fp, protocol)
94 pickle.dump(None, fp, protocol)
95 else:
95 else:
96 pickle.dump(None, fp, protocol)
96 pickle.dump(None, fp, protocol)
97 fp.close()
97 fp.close()
98 # With large history, cleanup process goes crazy and suddenly
98 # With large history, cleanup process goes crazy and suddenly
99 # consumes *huge* amount of memory. The output file being closed,
99 # consumes *huge* amount of memory. The output file being closed,
100 # there is no need for clean termination.
100 # there is no need for clean termination.
101 os._exit(0)
101 os._exit(0)
102
102
103 def debugsvnlog(ui, **opts):
103 def debugsvnlog(ui, **opts):
104 """Fetch SVN log in a subprocess and channel them back to parent to
104 """Fetch SVN log in a subprocess and channel them back to parent to
105 avoid memory collection issues.
105 avoid memory collection issues.
106 """
106 """
107 util.set_binary(sys.stdin)
107 util.set_binary(sys.stdin)
108 util.set_binary(sys.stdout)
108 util.set_binary(sys.stdout)
109 args = decodeargs(sys.stdin.read())
109 args = decodeargs(sys.stdin.read())
110 get_log_child(sys.stdout, *args)
110 get_log_child(sys.stdout, *args)
111
111
112 class logstream:
112 class logstream:
113 """Interruptible revision log iterator."""
113 """Interruptible revision log iterator."""
114 def __init__(self, stdout):
114 def __init__(self, stdout):
115 self._stdout = stdout
115 self._stdout = stdout
116
116
117 def __iter__(self):
117 def __iter__(self):
118 while True:
118 while True:
119 entry = pickle.load(self._stdout)
119 entry = pickle.load(self._stdout)
120 try:
120 try:
121 orig_paths, revnum, author, date, message = entry
121 orig_paths, revnum, author, date, message = entry
122 except:
122 except:
123 if entry is None:
123 if entry is None:
124 break
124 break
125 raise SubversionException("child raised exception", entry)
125 raise SubversionException("child raised exception", entry)
126 yield entry
126 yield entry
127
127
128 def close(self):
128 def close(self):
129 if self._stdout:
129 if self._stdout:
130 self._stdout.close()
130 self._stdout.close()
131 self._stdout = None
131 self._stdout = None
132
132
133 def get_log(url, paths, start, end, limit=0, discover_changed_paths=True,
133 def get_log(url, paths, start, end, limit=0, discover_changed_paths=True,
134 strict_node_history=False):
134 strict_node_history=False):
135 args = [url, paths, start, end, limit, discover_changed_paths,
135 args = [url, paths, start, end, limit, discover_changed_paths,
136 strict_node_history]
136 strict_node_history]
137 arg = encodeargs(args)
137 arg = encodeargs(args)
138 hgexe = util.hgexecutable()
138 hgexe = util.hgexecutable()
139 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
139 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
140 stdin, stdout = os.popen2(cmd, 'b')
140 stdin, stdout = os.popen2(cmd, 'b')
141 stdin.write(arg)
141 stdin.write(arg)
142 stdin.close()
142 stdin.close()
143 return logstream(stdout)
143 return logstream(stdout)
144
144
145 # SVN conversion code stolen from bzr-svn and tailor
145 # SVN conversion code stolen from bzr-svn and tailor
146 #
146 #
147 # Subversion looks like a versioned filesystem, branches structures
147 # Subversion looks like a versioned filesystem, branches structures
148 # are defined by conventions and not enforced by the tool. First,
148 # are defined by conventions and not enforced by the tool. First,
149 # we define the potential branches (modules) as "trunk" and "branches"
149 # we define the potential branches (modules) as "trunk" and "branches"
150 # children directories. Revisions are then identified by their
150 # children directories. Revisions are then identified by their
151 # module and revision number (and a repository identifier).
151 # module and revision number (and a repository identifier).
152 #
152 #
153 # The revision graph is really a tree (or a forest). By default, a
153 # The revision graph is really a tree (or a forest). By default, a
154 # revision parent is the previous revision in the same module. If the
154 # revision parent is the previous revision in the same module. If the
155 # module directory is copied/moved from another module then the
155 # module directory is copied/moved from another module then the
156 # revision is the module root and its parent the source revision in
156 # revision is the module root and its parent the source revision in
157 # the parent module. A revision has at most one parent.
157 # the parent module. A revision has at most one parent.
158 #
158 #
159 class svn_source(converter_source):
159 class svn_source(converter_source):
160 def __init__(self, ui, url, rev=None):
160 def __init__(self, ui, url, rev=None):
161 super(svn_source, self).__init__(ui, url, rev=rev)
161 super(svn_source, self).__init__(ui, url, rev=rev)
162
162
163 try:
163 try:
164 SubversionException
164 SubversionException
165 except NameError:
165 except NameError:
166 raise NoRepo('Subversion python bindings could not be loaded')
166 raise NoRepo('Subversion python bindings could not be loaded')
167
167
168 self.encoding = locale.getpreferredencoding()
168 self.encoding = locale.getpreferredencoding()
169 self.lastrevs = {}
169 self.lastrevs = {}
170
170
171 latest = None
171 latest = None
172 try:
172 try:
173 # Support file://path@rev syntax. Useful e.g. to convert
173 # Support file://path@rev syntax. Useful e.g. to convert
174 # deleted branches.
174 # deleted branches.
175 at = url.rfind('@')
175 at = url.rfind('@')
176 if at >= 0:
176 if at >= 0:
177 latest = int(url[at+1:])
177 latest = int(url[at+1:])
178 url = url[:at]
178 url = url[:at]
179 except ValueError, e:
179 except ValueError, e:
180 pass
180 pass
181 self.url = geturl(url)
181 self.url = geturl(url)
182 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
182 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
183 try:
183 try:
184 self.transport = transport.SvnRaTransport(url=self.url)
184 self.transport = transport.SvnRaTransport(url=self.url)
185 self.ra = self.transport.ra
185 self.ra = self.transport.ra
186 self.ctx = self.transport.client
186 self.ctx = self.transport.client
187 self.base = svn.ra.get_repos_root(self.ra)
187 self.base = svn.ra.get_repos_root(self.ra)
188 # Module is either empty or a repository path starting with
188 # Module is either empty or a repository path starting with
189 # a slash and not ending with a slash.
189 # a slash and not ending with a slash.
190 self.module = self.url[len(self.base):]
190 self.module = self.url[len(self.base):]
191 self.rootmodule = self.module
191 self.rootmodule = self.module
192 self.commits = {}
192 self.commits = {}
193 self.paths = {}
193 self.paths = {}
194 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
194 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
195 except SubversionException, e:
195 except SubversionException, e:
196 ui.print_exc()
196 ui.print_exc()
197 raise NoRepo("%s does not look like a Subversion repo" % self.url)
197 raise NoRepo("%s does not look like a Subversion repo" % self.url)
198
198
199 if rev:
199 if rev:
200 try:
200 try:
201 latest = int(rev)
201 latest = int(rev)
202 except ValueError:
202 except ValueError:
203 raise util.Abort('svn: revision %s is not an integer' % rev)
203 raise util.Abort('svn: revision %s is not an integer' % rev)
204
204
205 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
205 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
206 try:
206 try:
207 self.startrev = int(self.startrev)
207 self.startrev = int(self.startrev)
208 if self.startrev < 0:
208 if self.startrev < 0:
209 self.startrev = 0
209 self.startrev = 0
210 except ValueError:
210 except ValueError:
211 raise util.Abort(_('svn: start revision %s is not an integer')
211 raise util.Abort(_('svn: start revision %s is not an integer')
212 % self.startrev)
212 % self.startrev)
213
213
214 try:
214 try:
215 self.get_blacklist()
215 self.get_blacklist()
216 except IOError, e:
216 except IOError, e:
217 pass
217 pass
218
218
219 self.head = self.latest(self.module, latest)
219 self.head = self.latest(self.module, latest)
220 if not self.head:
220 if not self.head:
221 raise util.Abort(_('no revision found in module %s') %
221 raise util.Abort(_('no revision found in module %s') %
222 self.module.encode(self.encoding))
222 self.module.encode(self.encoding))
223 self.last_changed = self.revnum(self.head)
223 self.last_changed = self.revnum(self.head)
224
224
225 self._changescache = None
225 self._changescache = None
226
226
227 if os.path.exists(os.path.join(url, '.svn/entries')):
227 if os.path.exists(os.path.join(url, '.svn/entries')):
228 self.wc = url
228 self.wc = url
229 else:
229 else:
230 self.wc = None
230 self.wc = None
231 self.convertfp = None
231 self.convertfp = None
232
232
233 def setrevmap(self, revmap):
233 def setrevmap(self, revmap):
234 lastrevs = {}
234 lastrevs = {}
235 for revid in revmap.iterkeys():
235 for revid in revmap.iterkeys():
236 uuid, module, revnum = self.revsplit(revid)
236 uuid, module, revnum = self.revsplit(revid)
237 lastrevnum = lastrevs.setdefault(module, revnum)
237 lastrevnum = lastrevs.setdefault(module, revnum)
238 if revnum > lastrevnum:
238 if revnum > lastrevnum:
239 lastrevs[module] = revnum
239 lastrevs[module] = revnum
240 self.lastrevs = lastrevs
240 self.lastrevs = lastrevs
241
241
242 def exists(self, path, optrev):
242 def exists(self, path, optrev):
243 try:
243 try:
244 svn.client.ls(self.url.rstrip('/') + '/' + path,
244 svn.client.ls(self.url.rstrip('/') + '/' + path,
245 optrev, False, self.ctx)
245 optrev, False, self.ctx)
246 return True
246 return True
247 except SubversionException, err:
247 except SubversionException, err:
248 return False
248 return False
249
249
250 def getheads(self):
250 def getheads(self):
251
251
252 def isdir(path, revnum):
252 def isdir(path, revnum):
253 kind = svn.ra.check_path(self.ra, path, revnum)
253 kind = svn.ra.check_path(self.ra, path, revnum)
254 return kind == svn.core.svn_node_dir
254 return kind == svn.core.svn_node_dir
255
255
256 def getcfgpath(name, rev):
256 def getcfgpath(name, rev):
257 cfgpath = self.ui.config('convert', 'svn.' + name)
257 cfgpath = self.ui.config('convert', 'svn.' + name)
258 if cfgpath is not None and cfgpath.strip() == '':
258 if cfgpath is not None and cfgpath.strip() == '':
259 return None
259 return None
260 path = (cfgpath or name).strip('/')
260 path = (cfgpath or name).strip('/')
261 if not self.exists(path, rev):
261 if not self.exists(path, rev):
262 if cfgpath:
262 if cfgpath:
263 raise util.Abort(_('expected %s to be at %r, but not found')
263 raise util.Abort(_('expected %s to be at %r, but not found')
264 % (name, path))
264 % (name, path))
265 return None
265 return None
266 self.ui.note(_('found %s at %r\n') % (name, path))
266 self.ui.note(_('found %s at %r\n') % (name, path))
267 return path
267 return path
268
268
269 rev = optrev(self.last_changed)
269 rev = optrev(self.last_changed)
270 oldmodule = ''
270 oldmodule = ''
271 trunk = getcfgpath('trunk', rev)
271 trunk = getcfgpath('trunk', rev)
272 self.tags = getcfgpath('tags', rev)
272 self.tags = getcfgpath('tags', rev)
273 branches = getcfgpath('branches', rev)
273 branches = getcfgpath('branches', rev)
274
274
275 # If the project has a trunk or branches, we will extract heads
275 # If the project has a trunk or branches, we will extract heads
276 # from them. We keep the project root otherwise.
276 # from them. We keep the project root otherwise.
277 if trunk:
277 if trunk:
278 oldmodule = self.module or ''
278 oldmodule = self.module or ''
279 self.module += '/' + trunk
279 self.module += '/' + trunk
280 self.head = self.latest(self.module, self.last_changed)
280 self.head = self.latest(self.module, self.last_changed)
281 if not self.head:
281 if not self.head:
282 raise util.Abort(_('no revision found in module %s') %
282 raise util.Abort(_('no revision found in module %s') %
283 self.module.encode(self.encoding))
283 self.module.encode(self.encoding))
284
284
285 # First head in the list is the module's head
285 # First head in the list is the module's head
286 self.heads = [self.head]
286 self.heads = [self.head]
287 if self.tags is not None:
287 if self.tags is not None:
288 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
288 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
289
289
290 # Check if branches bring a few more heads to the list
290 # Check if branches bring a few more heads to the list
291 if branches:
291 if branches:
292 rpath = self.url.strip('/')
292 rpath = self.url.strip('/')
293 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
293 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
294 self.ctx)
294 self.ctx)
295 for branch in branchnames.keys():
295 for branch in branchnames.keys():
296 module = '%s/%s/%s' % (oldmodule, branches, branch)
296 module = '%s/%s/%s' % (oldmodule, branches, branch)
297 if not isdir(module, self.last_changed):
297 if not isdir(module, self.last_changed):
298 continue
298 continue
299 brevid = self.latest(module, self.last_changed)
299 brevid = self.latest(module, self.last_changed)
300 if not brevid:
300 if not brevid:
301 self.ui.note(_('ignoring empty branch %s\n') %
301 self.ui.note(_('ignoring empty branch %s\n') %
302 branch.encode(self.encoding))
302 branch.encode(self.encoding))
303 continue
303 continue
304 self.ui.note('found branch %s at %d\n' %
304 self.ui.note('found branch %s at %d\n' %
305 (branch, self.revnum(brevid)))
305 (branch, self.revnum(brevid)))
306 self.heads.append(brevid)
306 self.heads.append(brevid)
307
307
308 if self.startrev and self.heads:
308 if self.startrev and self.heads:
309 if len(self.heads) > 1:
309 if len(self.heads) > 1:
310 raise util.Abort(_('svn: start revision is not supported with '
310 raise util.Abort(_('svn: start revision is not supported with '
311 'with more than one branch'))
311 'with more than one branch'))
312 revnum = self.revnum(self.heads[0])
312 revnum = self.revnum(self.heads[0])
313 if revnum < self.startrev:
313 if revnum < self.startrev:
314 raise util.Abort(_('svn: no revision found after start revision %d')
314 raise util.Abort(_('svn: no revision found after start revision %d')
315 % self.startrev)
315 % self.startrev)
316
316
317 return self.heads
317 return self.heads
318
318
319 def getfile(self, file, rev):
319 def getfile(self, file, rev):
320 data, mode = self._getfile(file, rev)
320 data, mode = self._getfile(file, rev)
321 self.modecache[(file, rev)] = mode
321 self.modecache[(file, rev)] = mode
322 return data
322 return data
323
323
324 def getmode(self, file, rev):
324 def getmode(self, file, rev):
325 return self.modecache[(file, rev)]
325 return self.modecache[(file, rev)]
326
326
327 def getchanges(self, rev):
327 def getchanges(self, rev):
328 if self._changescache and self._changescache[0] == rev:
328 if self._changescache and self._changescache[0] == rev:
329 return self._changescache[1]
329 return self._changescache[1]
330 self._changescache = None
330 self._changescache = None
331 self.modecache = {}
331 self.modecache = {}
332 (paths, parents) = self.paths[rev]
332 (paths, parents) = self.paths[rev]
333 if parents:
333 if parents:
334 files, copies = self.expandpaths(rev, paths, parents)
334 files, copies = self.expandpaths(rev, paths, parents)
335 else:
335 else:
336 # Perform a full checkout on roots
336 # Perform a full checkout on roots
337 uuid, module, revnum = self.revsplit(rev)
337 uuid, module, revnum = self.revsplit(rev)
338 entries = svn.client.ls(self.base + module, optrev(revnum),
338 entries = svn.client.ls(self.base + module, optrev(revnum),
339 True, self.ctx)
339 True, self.ctx)
340 files = [n for n,e in entries.iteritems()
340 files = [n for n,e in entries.iteritems()
341 if e.kind == svn.core.svn_node_file]
341 if e.kind == svn.core.svn_node_file]
342 copies = {}
342 copies = {}
343
343
344 files.sort()
344 files.sort()
345 files = zip(files, [rev] * len(files))
345 files = zip(files, [rev] * len(files))
346
346
347 # caller caches the result, so free it here to release memory
347 # caller caches the result, so free it here to release memory
348 del self.paths[rev]
348 del self.paths[rev]
349 return (files, copies)
349 return (files, copies)
350
350
351 def getchangedfiles(self, rev, i):
351 def getchangedfiles(self, rev, i):
352 changes = self.getchanges(rev)
352 changes = self.getchanges(rev)
353 self._changescache = (rev, changes)
353 self._changescache = (rev, changes)
354 return [f[0] for f in changes[0]]
354 return [f[0] for f in changes[0]]
355
355
356 def getcommit(self, rev):
356 def getcommit(self, rev):
357 if rev not in self.commits:
357 if rev not in self.commits:
358 uuid, module, revnum = self.revsplit(rev)
358 uuid, module, revnum = self.revsplit(rev)
359 self.module = module
359 self.module = module
360 self.reparent(module)
360 self.reparent(module)
361 # We assume that:
361 # We assume that:
362 # - requests for revisions after "stop" come from the
362 # - requests for revisions after "stop" come from the
363 # revision graph backward traversal. Cache all of them
363 # revision graph backward traversal. Cache all of them
364 # down to stop, they will be used eventually.
364 # down to stop, they will be used eventually.
365 # - requests for revisions before "stop" come to get
365 # - requests for revisions before "stop" come to get
366 # isolated branches parents. Just fetch what is needed.
366 # isolated branches parents. Just fetch what is needed.
367 stop = self.lastrevs.get(module, 0)
367 stop = self.lastrevs.get(module, 0)
368 if revnum < stop:
368 if revnum < stop:
369 stop = revnum + 1
369 stop = revnum + 1
370 self._fetch_revisions(revnum, stop)
370 self._fetch_revisions(revnum, stop)
371 commit = self.commits[rev]
371 commit = self.commits[rev]
372 # caller caches the result, so free it here to release memory
372 # caller caches the result, so free it here to release memory
373 del self.commits[rev]
373 del self.commits[rev]
374 return commit
374 return commit
375
375
376 def gettags(self):
376 def gettags(self):
377 tags = {}
377 tags = {}
378 if self.tags is None:
378 if self.tags is None:
379 return tags
379 return tags
380
380
381 # svn tags are just a convention, project branches left in a
381 # svn tags are just a convention, project branches left in a
382 # 'tags' directory. There is no other relationship than
382 # 'tags' directory. There is no other relationship than
383 # ancestry, which is expensive to discover and makes them hard
383 # ancestry, which is expensive to discover and makes them hard
384 # to update incrementally. Worse, past revisions may be
384 # to update incrementally. Worse, past revisions may be
385 # referenced by tags far away in the future, requiring a deep
385 # referenced by tags far away in the future, requiring a deep
386 # history traversal on every calculation. Current code
386 # history traversal on every calculation. Current code
387 # performs a single backward traversal, tracking moves within
387 # performs a single backward traversal, tracking moves within
388 # the tags directory (tag renaming) and recording a new tag
388 # the tags directory (tag renaming) and recording a new tag
389 # everytime a project is copied from outside the tags
389 # everytime a project is copied from outside the tags
390 # directory. It also lists deleted tags, this behaviour may
390 # directory. It also lists deleted tags, this behaviour may
391 # change in the future.
391 # change in the future.
392 pendings = []
392 pendings = []
393 tagspath = self.tags
393 tagspath = self.tags
394 start = svn.ra.get_latest_revnum(self.ra)
394 start = svn.ra.get_latest_revnum(self.ra)
395 try:
395 try:
396 for entry in get_log(self.url, [self.tags], start, self.startrev):
396 for entry in get_log(self.url, [self.tags], start, self.startrev):
397 origpaths, revnum, author, date, message = entry
397 origpaths, revnum, author, date, message = entry
398 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
398 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
399 in origpaths.iteritems() if e.copyfrom_path]
399 in origpaths.iteritems() if e.copyfrom_path]
400 copies.sort()
400 copies.sort()
401 # Apply moves/copies from more specific to general
401 # Apply moves/copies from more specific to general
402 copies.reverse()
402 copies.reverse()
403
403
404 srctagspath = tagspath
404 srctagspath = tagspath
405 if copies and copies[-1][2] == tagspath:
405 if copies and copies[-1][2] == tagspath:
406 # Track tags directory moves
406 # Track tags directory moves
407 srctagspath = copies.pop()[0]
407 srctagspath = copies.pop()[0]
408
408
409 for source, sourcerev, dest in copies:
409 for source, sourcerev, dest in copies:
410 if not dest.startswith(tagspath + '/'):
410 if not dest.startswith(tagspath + '/'):
411 continue
411 continue
412 for tag in pendings:
412 for tag in pendings:
413 if tag[0].startswith(dest):
413 if tag[0].startswith(dest):
414 tagpath = source + tag[0][len(dest):]
414 tagpath = source + tag[0][len(dest):]
415 tag[:2] = [tagpath, sourcerev]
415 tag[:2] = [tagpath, sourcerev]
416 break
416 break
417 else:
417 else:
418 pendings.append([source, sourcerev, dest.split('/')[-1]])
418 pendings.append([source, sourcerev, dest.split('/')[-1]])
419
419
420 # Tell tag renamings from tag creations
420 # Tell tag renamings from tag creations
421 remainings = []
421 remainings = []
422 for source, sourcerev, tagname in pendings:
422 for source, sourcerev, tagname in pendings:
423 if source.startswith(srctagspath):
423 if source.startswith(srctagspath):
424 remainings.append([source, sourcerev, tagname])
424 remainings.append([source, sourcerev, tagname])
425 continue
425 continue
426 # From revision may be fake, get one with changes
426 # From revision may be fake, get one with changes
427 tagid = self.latest(source, sourcerev)
427 tagid = self.latest(source, sourcerev)
428 if tagid:
428 if tagid:
429 tags[tagname] = tagid
429 tags[tagname] = tagid
430 pendings = remainings
430 pendings = remainings
431 tagspath = srctagspath
431 tagspath = srctagspath
432
432
433 except SubversionException, (inst, num):
433 except SubversionException, (inst, num):
434 self.ui.note('no tags found at revision %d\n' % start)
434 self.ui.note('no tags found at revision %d\n' % start)
435 return tags
435 return tags
436
436
437 def converted(self, rev, destrev):
437 def converted(self, rev, destrev):
438 if not self.wc:
438 if not self.wc:
439 return
439 return
440 if self.convertfp is None:
440 if self.convertfp is None:
441 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
441 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
442 'a')
442 'a')
443 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
443 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
444 self.convertfp.flush()
444 self.convertfp.flush()
445
445
446 # -- helper functions --
446 # -- helper functions --
447
447
448 def revid(self, revnum, module=None):
448 def revid(self, revnum, module=None):
449 if not module:
449 if not module:
450 module = self.module
450 module = self.module
451 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
451 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
452 revnum)
452 revnum)
453
453
454 def revnum(self, rev):
454 def revnum(self, rev):
455 return int(rev.split('@')[-1])
455 return int(rev.split('@')[-1])
456
456
457 def revsplit(self, rev):
457 def revsplit(self, rev):
458 url, revnum = rev.encode(self.encoding).split('@', 1)
458 url, revnum = rev.encode(self.encoding).split('@', 1)
459 revnum = int(revnum)
459 revnum = int(revnum)
460 parts = url.split('/', 1)
460 parts = url.split('/', 1)
461 uuid = parts.pop(0)[4:]
461 uuid = parts.pop(0)[4:]
462 mod = ''
462 mod = ''
463 if parts:
463 if parts:
464 mod = '/' + parts[0]
464 mod = '/' + parts[0]
465 return uuid, mod, revnum
465 return uuid, mod, revnum
466
466
467 def latest(self, path, stop=0):
467 def latest(self, path, stop=0):
468 """Find the latest revid affecting path, up to stop. It may return
468 """Find the latest revid affecting path, up to stop. It may return
469 a revision in a different module, since a branch may be moved without
469 a revision in a different module, since a branch may be moved without
470 a change being reported. Return None if computed module does not
470 a change being reported. Return None if computed module does not
471 belong to rootmodule subtree.
471 belong to rootmodule subtree.
472 """
472 """
473 if not path.startswith(self.rootmodule):
473 if not path.startswith(self.rootmodule):
474 # Requests on foreign branches may be forbidden at server level
474 # Requests on foreign branches may be forbidden at server level
475 self.ui.debug(_('ignoring foreign branch %r\n') % path)
475 self.ui.debug(_('ignoring foreign branch %r\n') % path)
476 return None
476 return None
477
477
478 if not stop:
478 if not stop:
479 stop = svn.ra.get_latest_revnum(self.ra)
479 stop = svn.ra.get_latest_revnum(self.ra)
480 try:
480 try:
481 self.reparent('')
481 self.reparent('')
482 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
482 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
483 self.reparent(self.module)
483 self.reparent(self.module)
484 except SubversionException:
484 except SubversionException:
485 dirent = None
485 dirent = None
486 if not dirent:
486 if not dirent:
487 raise util.Abort('%s not found up to revision %d' % (path, stop))
487 raise util.Abort('%s not found up to revision %d' % (path, stop))
488
488
489 # stat() gives us the previous revision on this line of development, but
489 # stat() gives us the previous revision on this line of development, but
490 # it might be in *another module*. Fetch the log and detect renames down
490 # it might be in *another module*. Fetch the log and detect renames down
491 # to the latest revision.
491 # to the latest revision.
492 stream = get_log(self.url, [path], stop, dirent.created_rev)
492 stream = get_log(self.url, [path], stop, dirent.created_rev)
493 try:
493 try:
494 for entry in stream:
494 for entry in stream:
495 paths, revnum, author, date, message = entry
495 paths, revnum, author, date, message = entry
496 if revnum <= dirent.created_rev:
496 if revnum <= dirent.created_rev:
497 break
497 break
498
498
499 for p in paths:
499 for p in paths:
500 if not path.startswith(p) or not paths[p].copyfrom_path:
500 if not path.startswith(p) or not paths[p].copyfrom_path:
501 continue
501 continue
502 newpath = paths[p].copyfrom_path + path[len(p):]
502 newpath = paths[p].copyfrom_path + path[len(p):]
503 self.ui.debug("branch renamed from %s to %s at %d\n" %
503 self.ui.debug("branch renamed from %s to %s at %d\n" %
504 (path, newpath, revnum))
504 (path, newpath, revnum))
505 path = newpath
505 path = newpath
506 break
506 break
507 finally:
507 finally:
508 stream.close()
508 stream.close()
509
509
510 if not path.startswith(self.rootmodule):
510 if not path.startswith(self.rootmodule):
511 self.ui.debug(_('ignoring foreign branch %r\n') % path)
511 self.ui.debug(_('ignoring foreign branch %r\n') % path)
512 return None
512 return None
513 return self.revid(dirent.created_rev, path)
513 return self.revid(dirent.created_rev, path)
514
514
515 def get_blacklist(self):
515 def get_blacklist(self):
516 """Avoid certain revision numbers.
516 """Avoid certain revision numbers.
517 It is not uncommon for two nearby revisions to cancel each other
517 It is not uncommon for two nearby revisions to cancel each other
518 out, e.g. 'I copied trunk into a subdirectory of itself instead
518 out, e.g. 'I copied trunk into a subdirectory of itself instead
519 of making a branch'. The converted repository is significantly
519 of making a branch'. The converted repository is significantly
520 smaller if we ignore such revisions."""
520 smaller if we ignore such revisions."""
521 self.blacklist = util.set()
521 self.blacklist = util.set()
522 blacklist = self.blacklist
522 blacklist = self.blacklist
523 for line in file("blacklist.txt", "r"):
523 for line in file("blacklist.txt", "r"):
524 if not line.startswith("#"):
524 if not line.startswith("#"):
525 try:
525 try:
526 svn_rev = int(line.strip())
526 svn_rev = int(line.strip())
527 blacklist.add(svn_rev)
527 blacklist.add(svn_rev)
528 except ValueError, e:
528 except ValueError, e:
529 pass # not an integer or a comment
529 pass # not an integer or a comment
530
530
531 def is_blacklisted(self, svn_rev):
531 def is_blacklisted(self, svn_rev):
532 return svn_rev in self.blacklist
532 return svn_rev in self.blacklist
533
533
534 def reparent(self, module):
534 def reparent(self, module):
535 svn_url = self.base + module
535 svn_url = self.base + module
536 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
536 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
537 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
537 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
538
538
539 def expandpaths(self, rev, paths, parents):
539 def expandpaths(self, rev, paths, parents):
540 entries = []
540 entries = []
541 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
541 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
542 copies = {}
542 copies = {}
543
543
544 new_module, revnum = self.revsplit(rev)[1:]
544 new_module, revnum = self.revsplit(rev)[1:]
545 if new_module != self.module:
545 if new_module != self.module:
546 self.module = new_module
546 self.module = new_module
547 self.reparent(self.module)
547 self.reparent(self.module)
548
548
549 for path, ent in paths:
549 for path, ent in paths:
550 entrypath = self.getrelpath(path)
550 entrypath = self.getrelpath(path)
551 entry = entrypath.decode(self.encoding)
551 entry = entrypath.decode(self.encoding)
552
552
553 kind = svn.ra.check_path(self.ra, entrypath, revnum)
553 kind = svn.ra.check_path(self.ra, entrypath, revnum)
554 if kind == svn.core.svn_node_file:
554 if kind == svn.core.svn_node_file:
555 entries.append(self.recode(entry))
555 entries.append(self.recode(entry))
556 if not ent.copyfrom_path or not parents:
556 if not ent.copyfrom_path or not parents:
557 continue
557 continue
558 # Copy sources not in parent revisions cannot be represented,
558 # Copy sources not in parent revisions cannot be represented,
559 # ignore their origin for now
559 # ignore their origin for now
560 pmodule, prevnum = self.revsplit(parents[0])[1:]
560 pmodule, prevnum = self.revsplit(parents[0])[1:]
561 if ent.copyfrom_rev < prevnum:
561 if ent.copyfrom_rev < prevnum:
562 continue
562 continue
563 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
563 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
564 if not copyfrom_path:
564 if not copyfrom_path:
565 continue
565 continue
566 self.ui.debug("copied to %s from %s@%s\n" %
566 self.ui.debug("copied to %s from %s@%s\n" %
567 (entrypath, copyfrom_path, ent.copyfrom_rev))
567 (entrypath, copyfrom_path, ent.copyfrom_rev))
568 copies[self.recode(entry)] = self.recode(copyfrom_path)
568 copies[self.recode(entry)] = self.recode(copyfrom_path)
569 elif kind == 0: # gone, but had better be a deleted *file*
569 elif kind == 0: # gone, but had better be a deleted *file*
570 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
570 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
571
571
572 # if a branch is created but entries are removed in the same
572 # if a branch is created but entries are removed in the same
573 # changeset, get the right fromrev
573 # changeset, get the right fromrev
574 # parents cannot be empty here, you cannot remove things from
574 # parents cannot be empty here, you cannot remove things from
575 # a root revision.
575 # a root revision.
576 uuid, old_module, fromrev = self.revsplit(parents[0])
576 uuid, old_module, fromrev = self.revsplit(parents[0])
577
577
578 basepath = old_module + "/" + self.getrelpath(path)
578 basepath = old_module + "/" + self.getrelpath(path)
579 entrypath = basepath
579 entrypath = basepath
580
580
581 def lookup_parts(p):
581 def lookup_parts(p):
582 rc = None
582 rc = None
583 parts = p.split("/")
583 parts = p.split("/")
584 for i in range(len(parts)):
584 for i in range(len(parts)):
585 part = "/".join(parts[:i])
585 part = "/".join(parts[:i])
586 info = part, copyfrom.get(part, None)
586 info = part, copyfrom.get(part, None)
587 if info[1] is not None:
587 if info[1] is not None:
588 self.ui.debug("Found parent directory %s\n" % info[1])
588 self.ui.debug("Found parent directory %s\n" % info[1])
589 rc = info
589 rc = info
590 return rc
590 return rc
591
591
592 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
592 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
593
593
594 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
594 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
595
595
596 # need to remove fragment from lookup_parts and replace with copyfrom_path
596 # need to remove fragment from lookup_parts and replace with copyfrom_path
597 if frompath is not None:
597 if frompath is not None:
598 self.ui.debug("munge-o-matic\n")
598 self.ui.debug("munge-o-matic\n")
599 self.ui.debug(entrypath + '\n')
599 self.ui.debug(entrypath + '\n')
600 self.ui.debug(entrypath[len(frompath):] + '\n')
600 self.ui.debug(entrypath[len(frompath):] + '\n')
601 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
601 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
602 fromrev = froment.copyfrom_rev
602 fromrev = froment.copyfrom_rev
603 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
603 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
604
604
605 # We can avoid the reparent calls if the module has not changed
605 # We can avoid the reparent calls if the module has not changed
606 # but it probably does not worth the pain.
606 # but it probably does not worth the pain.
607 self.reparent('')
607 self.reparent('')
608 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
608 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
609 self.reparent(self.module)
609 self.reparent(self.module)
610
610
611 if fromkind == svn.core.svn_node_file: # a deleted file
611 if fromkind == svn.core.svn_node_file: # a deleted file
612 entries.append(self.recode(entry))
612 entries.append(self.recode(entry))
613 elif fromkind == svn.core.svn_node_dir:
613 elif fromkind == svn.core.svn_node_dir:
614 # print "Deleted/moved non-file:", revnum, path, ent
614 # print "Deleted/moved non-file:", revnum, path, ent
615 # children = self._find_children(path, revnum - 1)
615 # children = self._find_children(path, revnum - 1)
616 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
616 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
617 # Sometimes this is tricky. For example: in
617 # Sometimes this is tricky. For example: in
618 # The Subversion Repository revision 6940 a dir
618 # The Subversion Repository revision 6940 a dir
619 # was copied and one of its files was deleted
619 # was copied and one of its files was deleted
620 # from the new location in the same commit. This
620 # from the new location in the same commit. This
621 # code can't deal with that yet.
621 # code can't deal with that yet.
622 if ent.action == 'C':
622 if ent.action == 'C':
623 children = self._find_children(path, fromrev)
623 children = self._find_children(path, fromrev)
624 else:
624 else:
625 oroot = entrypath.strip('/')
625 oroot = entrypath.strip('/')
626 nroot = path.strip('/')
626 nroot = path.strip('/')
627 children = self._find_children(oroot, fromrev)
627 children = self._find_children(oroot, fromrev)
628 children = [s.replace(oroot,nroot) for s in children]
628 children = [s.replace(oroot,nroot) for s in children]
629 # Mark all [files, not directories] as deleted.
629 # Mark all [files, not directories] as deleted.
630 for child in children:
630 for child in children:
631 # Can we move a child directory and its
631 # Can we move a child directory and its
632 # parent in the same commit? (probably can). Could
632 # parent in the same commit? (probably can). Could
633 # cause problems if instead of revnum -1,
633 # cause problems if instead of revnum -1,
634 # we have to look in (copyfrom_path, revnum - 1)
634 # we have to look in (copyfrom_path, revnum - 1)
635 entrypath = self.getrelpath("/" + child, module=old_module)
635 entrypath = self.getrelpath("/" + child, module=old_module)
636 if entrypath:
636 if entrypath:
637 entry = self.recode(entrypath.decode(self.encoding))
637 entry = self.recode(entrypath.decode(self.encoding))
638 if entry in copies:
638 if entry in copies:
639 # deleted file within a copy
639 # deleted file within a copy
640 del copies[entry]
640 del copies[entry]
641 else:
641 else:
642 entries.append(entry)
642 entries.append(entry)
643 else:
643 else:
644 self.ui.debug('unknown path in revision %d: %s\n' % \
644 self.ui.debug('unknown path in revision %d: %s\n' % \
645 (revnum, path))
645 (revnum, path))
646 elif kind == svn.core.svn_node_dir:
646 elif kind == svn.core.svn_node_dir:
647 # Should probably synthesize normal file entries
647 # Should probably synthesize normal file entries
648 # and handle as above to clean up copy/rename handling.
648 # and handle as above to clean up copy/rename handling.
649
649
650 # If the directory just had a prop change,
650 # If the directory just had a prop change,
651 # then we shouldn't need to look for its children.
651 # then we shouldn't need to look for its children.
652 if ent.action == 'M':
652 if ent.action == 'M':
653 continue
653 continue
654
654
655 # Also this could create duplicate entries. Not sure
655 # Also this could create duplicate entries. Not sure
656 # whether this will matter. Maybe should make entries a set.
656 # whether this will matter. Maybe should make entries a set.
657 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
657 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
658 # This will fail if a directory was copied
658 # This will fail if a directory was copied
659 # from another branch and then some of its files
659 # from another branch and then some of its files
660 # were deleted in the same transaction.
660 # were deleted in the same transaction.
661 children = self._find_children(path, revnum)
661 children = self._find_children(path, revnum)
662 children.sort()
662 children.sort()
663 for child in children:
663 for child in children:
664 # Can we move a child directory and its
664 # Can we move a child directory and its
665 # parent in the same commit? (probably can). Could
665 # parent in the same commit? (probably can). Could
666 # cause problems if instead of revnum -1,
666 # cause problems if instead of revnum -1,
667 # we have to look in (copyfrom_path, revnum - 1)
667 # we have to look in (copyfrom_path, revnum - 1)
668 entrypath = self.getrelpath("/" + child)
668 entrypath = self.getrelpath("/" + child)
669 # print child, self.module, entrypath
669 # print child, self.module, entrypath
670 if entrypath:
670 if entrypath:
671 # Need to filter out directories here...
671 # Need to filter out directories here...
672 kind = svn.ra.check_path(self.ra, entrypath, revnum)
672 kind = svn.ra.check_path(self.ra, entrypath, revnum)
673 if kind != svn.core.svn_node_dir:
673 if kind != svn.core.svn_node_dir:
674 entries.append(self.recode(entrypath))
674 entries.append(self.recode(entrypath))
675
675
676 # Copies here (must copy all from source)
676 # Copies here (must copy all from source)
677 # Probably not a real problem for us if
677 # Probably not a real problem for us if
678 # source does not exist
678 # source does not exist
679 if not ent.copyfrom_path or not parents:
679 if not ent.copyfrom_path or not parents:
680 continue
680 continue
681 # Copy sources not in parent revisions cannot be represented,
681 # Copy sources not in parent revisions cannot be represented,
682 # ignore their origin for now
682 # ignore their origin for now
683 pmodule, prevnum = self.revsplit(parents[0])[1:]
683 pmodule, prevnum = self.revsplit(parents[0])[1:]
684 if ent.copyfrom_rev < prevnum:
684 if ent.copyfrom_rev < prevnum:
685 continue
685 continue
686 copyfrompath = ent.copyfrom_path.decode(self.encoding)
686 copyfrompath = ent.copyfrom_path.decode(self.encoding)
687 copyfrompath = self.getrelpath(copyfrompath, pmodule)
687 copyfrompath = self.getrelpath(copyfrompath, pmodule)
688 if not copyfrompath:
688 if not copyfrompath:
689 continue
689 continue
690 copyfrom[path] = ent
690 copyfrom[path] = ent
691 self.ui.debug("mark %s came from %s:%d\n"
691 self.ui.debug("mark %s came from %s:%d\n"
692 % (path, copyfrompath, ent.copyfrom_rev))
692 % (path, copyfrompath, ent.copyfrom_rev))
693 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
693 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
694 children.sort()
694 children.sort()
695 for child in children:
695 for child in children:
696 entrypath = self.getrelpath("/" + child, pmodule)
696 entrypath = self.getrelpath("/" + child, pmodule)
697 if not entrypath:
697 if not entrypath:
698 continue
698 continue
699 entry = entrypath.decode(self.encoding)
699 entry = entrypath.decode(self.encoding)
700 copytopath = path + entry[len(copyfrompath):]
700 copytopath = path + entry[len(copyfrompath):]
701 copytopath = self.getrelpath(copytopath)
701 copytopath = self.getrelpath(copytopath)
702 copies[self.recode(copytopath)] = self.recode(entry, pmodule)
702 copies[self.recode(copytopath)] = self.recode(entry, pmodule)
703
703
704 return (util.unique(entries), copies)
704 return (util.unique(entries), copies)
705
705
706 def _fetch_revisions(self, from_revnum, to_revnum):
706 def _fetch_revisions(self, from_revnum, to_revnum):
707 if from_revnum < to_revnum:
707 if from_revnum < to_revnum:
708 from_revnum, to_revnum = to_revnum, from_revnum
708 from_revnum, to_revnum = to_revnum, from_revnum
709
709
710 self.child_cset = None
710 self.child_cset = None
711
711
712 def isdescendantof(parent, child):
712 def isdescendantof(parent, child):
713 if not child or not parent or not child.startswith(parent):
713 if not child or not parent or not child.startswith(parent):
714 return False
714 return False
715 subpath = child[len(parent):]
715 subpath = child[len(parent):]
716 return len(subpath) > 1 and subpath[0] == '/'
716 return len(subpath) > 1 and subpath[0] == '/'
717
717
718 def parselogentry(orig_paths, revnum, author, date, message):
718 def parselogentry(orig_paths, revnum, author, date, message):
719 """Return the parsed commit object or None, and True if
719 """Return the parsed commit object or None, and True if
720 the revision is a branch root.
720 the revision is a branch root.
721 """
721 """
722 self.ui.debug("parsing revision %d (%d changes)\n" %
722 self.ui.debug("parsing revision %d (%d changes)\n" %
723 (revnum, len(orig_paths)))
723 (revnum, len(orig_paths)))
724
724
725 branched = False
725 branched = False
726 rev = self.revid(revnum)
726 rev = self.revid(revnum)
727 # branch log might return entries for a parent we already have
727 # branch log might return entries for a parent we already have
728
728
729 if (rev in self.commits or revnum < to_revnum):
729 if (rev in self.commits or revnum < to_revnum):
730 return None, branched
730 return None, branched
731
731
732 parents = []
732 parents = []
733 # check whether this revision is the start of a branch or part
733 # check whether this revision is the start of a branch or part
734 # of a branch renaming
734 # of a branch renaming
735 orig_paths = orig_paths.items()
735 orig_paths = orig_paths.items()
736 orig_paths.sort()
736 orig_paths.sort()
737 root_paths = [(p,e) for p,e in orig_paths if self.module.startswith(p)]
737 root_paths = [(p,e) for p,e in orig_paths if self.module.startswith(p)]
738 if root_paths:
738 if root_paths:
739 path, ent = root_paths[-1]
739 path, ent = root_paths[-1]
740 if ent.copyfrom_path:
740 if ent.copyfrom_path:
741 # If dir was moved while one of its file was removed
741 # If dir was moved while one of its file was removed
742 # the log may look like:
742 # the log may look like:
743 # A /dir (from /dir:x)
743 # A /dir (from /dir:x)
744 # A /dir/a (from /dir/a:y)
744 # A /dir/a (from /dir/a:y)
745 # A /dir/b (from /dir/b:z)
745 # A /dir/b (from /dir/b:z)
746 # ...
746 # ...
747 # for all remaining children.
747 # for all remaining children.
748 # Let's take the highest child element from rev as source.
748 # Let's take the highest child element from rev as source.
749 copies = [(p,e) for p,e in orig_paths[:-1]
749 copies = [(p,e) for p,e in orig_paths[:-1]
750 if isdescendantof(ent.copyfrom_path, e.copyfrom_path)]
750 if isdescendantof(ent.copyfrom_path, e.copyfrom_path)]
751 fromrev = max([e.copyfrom_rev for p,e in copies] + [ent.copyfrom_rev])
751 fromrev = max([e.copyfrom_rev for p,e in copies] + [ent.copyfrom_rev])
752 branched = True
752 branched = True
753 newpath = ent.copyfrom_path + self.module[len(path):]
753 newpath = ent.copyfrom_path + self.module[len(path):]
754 # ent.copyfrom_rev may not be the actual last revision
754 # ent.copyfrom_rev may not be the actual last revision
755 previd = self.latest(newpath, fromrev)
755 previd = self.latest(newpath, fromrev)
756 if previd is not None:
756 if previd is not None:
757 prevmodule, prevnum = self.revsplit(previd)[1:]
757 prevmodule, prevnum = self.revsplit(previd)[1:]
758 if prevnum >= self.startrev:
758 if prevnum >= self.startrev:
759 parents = [previd]
759 parents = [previd]
760 self.ui.note('found parent of branch %s at %d: %s\n' %
760 self.ui.note('found parent of branch %s at %d: %s\n' %
761 (self.module, prevnum, prevmodule))
761 (self.module, prevnum, prevmodule))
762 else:
762 else:
763 self.ui.debug("No copyfrom path, don't know what to do.\n")
763 self.ui.debug("No copyfrom path, don't know what to do.\n")
764
764
765 paths = []
765 paths = []
766 # filter out unrelated paths
766 # filter out unrelated paths
767 for path, ent in orig_paths:
767 for path, ent in orig_paths:
768 if self.getrelpath(path) is None:
768 if self.getrelpath(path) is None:
769 continue
769 continue
770 paths.append((path, ent))
770 paths.append((path, ent))
771
771
772 # Example SVN datetime. Includes microseconds.
772 # Example SVN datetime. Includes microseconds.
773 # ISO-8601 conformant
773 # ISO-8601 conformant
774 # '2007-01-04T17:35:00.902377Z'
774 # '2007-01-04T17:35:00.902377Z'
775 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
775 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
776
776
777 log = message and self.recode(message) or ''
777 log = message and self.recode(message) or ''
778 author = author and self.recode(author) or ''
778 author = author and self.recode(author) or ''
779 try:
779 try:
780 branch = self.module.split("/")[-1]
780 branch = self.module.split("/")[-1]
781 if branch == 'trunk':
781 if branch == 'trunk':
782 branch = ''
782 branch = ''
783 except IndexError:
783 except IndexError:
784 branch = None
784 branch = None
785
785
786 cset = commit(author=author,
786 cset = commit(author=author,
787 date=util.datestr(date),
787 date=util.datestr(date),
788 desc=log,
788 desc=log,
789 parents=parents,
789 parents=parents,
790 branch=branch,
790 branch=branch,
791 rev=rev.encode('utf-8'))
791 rev=rev.encode('utf-8'))
792
792
793 self.commits[rev] = cset
793 self.commits[rev] = cset
794 # The parents list is *shared* among self.paths and the
794 # The parents list is *shared* among self.paths and the
795 # commit object. Both will be updated below.
795 # commit object. Both will be updated below.
796 self.paths[rev] = (paths, cset.parents)
796 self.paths[rev] = (paths, cset.parents)
797 if self.child_cset and not self.child_cset.parents:
797 if self.child_cset and not self.child_cset.parents:
798 self.child_cset.parents[:] = [rev]
798 self.child_cset.parents[:] = [rev]
799 self.child_cset = cset
799 self.child_cset = cset
800 return cset, branched
800 return cset, branched
801
801
802 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
802 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
803 (self.module, from_revnum, to_revnum))
803 (self.module, from_revnum, to_revnum))
804
804
805 try:
805 try:
806 firstcset = None
806 firstcset = None
807 lastonbranch = False
807 lastonbranch = False
808 stream = get_log(self.url, [self.module], from_revnum, to_revnum)
808 stream = get_log(self.url, [self.module], from_revnum, to_revnum)
809 try:
809 try:
810 for entry in stream:
810 for entry in stream:
811 paths, revnum, author, date, message = entry
811 paths, revnum, author, date, message = entry
812 if revnum < self.startrev:
812 if revnum < self.startrev:
813 lastonbranch = True
813 lastonbranch = True
814 break
814 break
815 if self.is_blacklisted(revnum):
815 if self.is_blacklisted(revnum):
816 self.ui.note('skipping blacklisted revision %d\n'
816 self.ui.note('skipping blacklisted revision %d\n'
817 % revnum)
817 % revnum)
818 continue
818 continue
819 if paths is None:
819 if paths is None:
820 self.ui.debug('revision %d has no entries\n' % revnum)
820 self.ui.debug('revision %d has no entries\n' % revnum)
821 continue
821 continue
822 cset, lastonbranch = parselogentry(paths, revnum, author,
822 cset, lastonbranch = parselogentry(paths, revnum, author,
823 date, message)
823 date, message)
824 if cset:
824 if cset:
825 firstcset = cset
825 firstcset = cset
826 if lastonbranch:
826 if lastonbranch:
827 break
827 break
828 finally:
828 finally:
829 stream.close()
829 stream.close()
830
830
831 if not lastonbranch and firstcset and not firstcset.parents:
831 if not lastonbranch and firstcset and not firstcset.parents:
832 # The first revision of the sequence (the last fetched one)
832 # The first revision of the sequence (the last fetched one)
833 # has invalid parents if not a branch root. Find the parent
833 # has invalid parents if not a branch root. Find the parent
834 # revision now, if any.
834 # revision now, if any.
835 try:
835 try:
836 firstrevnum = self.revnum(firstcset.rev)
836 firstrevnum = self.revnum(firstcset.rev)
837 if firstrevnum > 1:
837 if firstrevnum > 1:
838 latest = self.latest(self.module, firstrevnum - 1)
838 latest = self.latest(self.module, firstrevnum - 1)
839 if latest:
839 if latest:
840 firstcset.parents.append(latest)
840 firstcset.parents.append(latest)
841 except util.Abort:
841 except util.Abort:
842 pass
842 pass
843 except SubversionException, (inst, num):
843 except SubversionException, (inst, num):
844 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
844 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
845 raise util.Abort('svn: branch has no revision %s' % to_revnum)
845 raise util.Abort('svn: branch has no revision %s' % to_revnum)
846 raise
846 raise
847
847
848 def _getfile(self, file, rev):
848 def _getfile(self, file, rev):
849 io = StringIO()
849 io = StringIO()
850 # TODO: ra.get_file transmits the whole file instead of diffs.
850 # TODO: ra.get_file transmits the whole file instead of diffs.
851 mode = ''
851 mode = ''
852 try:
852 try:
853 new_module, revnum = self.revsplit(rev)[1:]
853 new_module, revnum = self.revsplit(rev)[1:]
854 if self.module != new_module:
854 if self.module != new_module:
855 self.module = new_module
855 self.module = new_module
856 self.reparent(self.module)
856 self.reparent(self.module)
857 info = svn.ra.get_file(self.ra, file, revnum, io)
857 info = svn.ra.get_file(self.ra, file, revnum, io)
858 if isinstance(info, list):
858 if isinstance(info, list):
859 info = info[-1]
859 info = info[-1]
860 mode = ("svn:executable" in info) and 'x' or ''
860 mode = ("svn:executable" in info) and 'x' or ''
861 mode = ("svn:special" in info) and 'l' or mode
861 mode = ("svn:special" in info) and 'l' or mode
862 except SubversionException, e:
862 except SubversionException, e:
863 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
863 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
864 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
864 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
865 if e.apr_err in notfound: # File not found
865 if e.apr_err in notfound: # File not found
866 raise IOError()
866 raise IOError()
867 raise
867 raise
868 data = io.getvalue()
868 data = io.getvalue()
869 if mode == 'l':
869 if mode == 'l':
870 link_prefix = "link "
870 link_prefix = "link "
871 if data.startswith(link_prefix):
871 if data.startswith(link_prefix):
872 data = data[len(link_prefix):]
872 data = data[len(link_prefix):]
873 return data, mode
873 return data, mode
874
874
875 def _find_children(self, path, revnum):
875 def _find_children(self, path, revnum):
876 path = path.strip('/')
876 path = path.strip('/')
877 pool = Pool()
877 pool = Pool()
878 rpath = '/'.join([self.base, path]).strip('/')
878 rpath = '/'.join([self.base, path]).strip('/')
879 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
879 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
880
880
881 def getrelpath(self, path, module=None):
881 def getrelpath(self, path, module=None):
882 if module is None:
882 if module is None:
883 module = self.module
883 module = self.module
884 # Given the repository url of this wc, say
884 # Given the repository url of this wc, say
885 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
885 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
886 # extract the "entry" portion (a relative path) from what
886 # extract the "entry" portion (a relative path) from what
887 # svn log --xml says, ie
887 # svn log --xml says, ie
888 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
888 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
889 # that is to say "tests/PloneTestCase.py"
889 # that is to say "tests/PloneTestCase.py"
890 if path.startswith(module):
890 if path.startswith(module):
891 relative = path.rstrip('/')[len(module):]
891 relative = path.rstrip('/')[len(module):]
892 if relative.startswith('/'):
892 if relative.startswith('/'):
893 return relative[1:]
893 return relative[1:]
894 elif relative == '':
894 elif relative == '':
895 return relative
895 return relative
896
896
897 # The path is outside our tracked tree...
897 # The path is outside our tracked tree...
898 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
898 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
899 return None
899 return None
900
900
901 pre_revprop_change = '''#!/bin/sh
901 pre_revprop_change = '''#!/bin/sh
902
902
903 REPOS="$1"
903 REPOS="$1"
904 REV="$2"
904 REV="$2"
905 USER="$3"
905 USER="$3"
906 PROPNAME="$4"
906 PROPNAME="$4"
907 ACTION="$5"
907 ACTION="$5"
908
908
909 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
909 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
910 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
910 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
911 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
911 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
912
912
913 echo "Changing prohibited revision property" >&2
913 echo "Changing prohibited revision property" >&2
914 exit 1
914 exit 1
915 '''
915 '''
916
916
917 class svn_sink(converter_sink, commandline):
917 class svn_sink(converter_sink, commandline):
918 commit_re = re.compile(r'Committed revision (\d+).', re.M)
918 commit_re = re.compile(r'Committed revision (\d+).', re.M)
919
919
920 def prerun(self):
920 def prerun(self):
921 if self.wc:
921 if self.wc:
922 os.chdir(self.wc)
922 os.chdir(self.wc)
923
923
924 def postrun(self):
924 def postrun(self):
925 if self.wc:
925 if self.wc:
926 os.chdir(self.cwd)
926 os.chdir(self.cwd)
927
927
928 def join(self, name):
928 def join(self, name):
929 return os.path.join(self.wc, '.svn', name)
929 return os.path.join(self.wc, '.svn', name)
930
930
931 def revmapfile(self):
931 def revmapfile(self):
932 return self.join('hg-shamap')
932 return self.join('hg-shamap')
933
933
934 def authorfile(self):
934 def authorfile(self):
935 return self.join('hg-authormap')
935 return self.join('hg-authormap')
936
936
937 def __init__(self, ui, path):
937 def __init__(self, ui, path):
938 converter_sink.__init__(self, ui, path)
938 converter_sink.__init__(self, ui, path)
939 commandline.__init__(self, ui, 'svn')
939 commandline.__init__(self, ui, 'svn')
940 self.delete = []
940 self.delete = []
941 self.setexec = []
941 self.setexec = []
942 self.delexec = []
942 self.delexec = []
943 self.copies = []
943 self.copies = []
944 self.wc = None
944 self.wc = None
945 self.cwd = os.getcwd()
945 self.cwd = os.getcwd()
946
946
947 path = os.path.realpath(path)
947 path = os.path.realpath(path)
948
948
949 created = False
949 created = False
950 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
950 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
951 self.wc = path
951 self.wc = path
952 self.run0('update')
952 self.run0('update')
953 else:
953 else:
954 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
954 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
955
955
956 if os.path.isdir(os.path.dirname(path)):
956 if os.path.isdir(os.path.dirname(path)):
957 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
957 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
958 ui.status(_('initializing svn repo %r\n') %
958 ui.status(_('initializing svn repo %r\n') %
959 os.path.basename(path))
959 os.path.basename(path))
960 commandline(ui, 'svnadmin').run0('create', path)
960 commandline(ui, 'svnadmin').run0('create', path)
961 created = path
961 created = path
962 path = util.normpath(path)
962 path = util.normpath(path)
963 if not path.startswith('/'):
963 if not path.startswith('/'):
964 path = '/' + path
964 path = '/' + path
965 path = 'file://' + path
965 path = 'file://' + path
966
966
967 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
967 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
968 self.run0('checkout', path, wcpath)
968 self.run0('checkout', path, wcpath)
969
969
970 self.wc = wcpath
970 self.wc = wcpath
971 self.opener = util.opener(self.wc)
971 self.opener = util.opener(self.wc)
972 self.wopener = util.opener(self.wc)
972 self.wopener = util.opener(self.wc)
973 self.childmap = mapfile(ui, self.join('hg-childmap'))
973 self.childmap = mapfile(ui, self.join('hg-childmap'))
974 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
974 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
975
975
976 if created:
976 if created:
977 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
977 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
978 fp = open(hook, 'w')
978 fp = open(hook, 'w')
979 fp.write(pre_revprop_change)
979 fp.write(pre_revprop_change)
980 fp.close()
980 fp.close()
981 util.set_flags(hook, "x")
981 util.set_flags(hook, "x")
982
982
983 xport = transport.SvnRaTransport(url=geturl(path))
983 xport = transport.SvnRaTransport(url=geturl(path))
984 self.uuid = svn.ra.get_uuid(xport.ra)
984 self.uuid = svn.ra.get_uuid(xport.ra)
985
985
986 def wjoin(self, *names):
986 def wjoin(self, *names):
987 return os.path.join(self.wc, *names)
987 return os.path.join(self.wc, *names)
988
988
989 def putfile(self, filename, flags, data):
989 def putfile(self, filename, flags, data):
990 if 'l' in flags:
990 if 'l' in flags:
991 self.wopener.symlink(data, filename)
991 self.wopener.symlink(data, filename)
992 else:
992 else:
993 try:
993 try:
994 if os.path.islink(self.wjoin(filename)):
994 if os.path.islink(self.wjoin(filename)):
995 os.unlink(filename)
995 os.unlink(filename)
996 except OSError:
996 except OSError:
997 pass
997 pass
998 self.wopener(filename, 'w').write(data)
998 self.wopener(filename, 'w').write(data)
999
999
1000 if self.is_exec:
1000 if self.is_exec:
1001 was_exec = self.is_exec(self.wjoin(filename))
1001 was_exec = self.is_exec(self.wjoin(filename))
1002 else:
1002 else:
1003 # On filesystems not supporting execute-bit, there is no way
1003 # On filesystems not supporting execute-bit, there is no way
1004 # to know if it is set but asking subversion. Setting it
1004 # to know if it is set but asking subversion. Setting it
1005 # systematically is just as expensive and much simpler.
1005 # systematically is just as expensive and much simpler.
1006 was_exec = 'x' not in flags
1006 was_exec = 'x' not in flags
1007
1007
1008 util.set_flags(self.wjoin(filename), flags)
1008 util.set_flags(self.wjoin(filename), flags)
1009 if was_exec:
1009 if was_exec:
1010 if 'x' not in flags:
1010 if 'x' not in flags:
1011 self.delexec.append(filename)
1011 self.delexec.append(filename)
1012 else:
1012 else:
1013 if 'x' in flags:
1013 if 'x' in flags:
1014 self.setexec.append(filename)
1014 self.setexec.append(filename)
1015
1015
1016 def delfile(self, name):
1017 self.delete.append(name)
1018
1019 def copyfile(self, source, dest):
1020 self.copies.append([source, dest])
1021
1022 def _copyfile(self, source, dest):
1016 def _copyfile(self, source, dest):
1023 # SVN's copy command pukes if the destination file exists, but
1017 # SVN's copy command pukes if the destination file exists, but
1024 # our copyfile method expects to record a copy that has
1018 # our copyfile method expects to record a copy that has
1025 # already occurred. Cross the semantic gap.
1019 # already occurred. Cross the semantic gap.
1026 wdest = self.wjoin(dest)
1020 wdest = self.wjoin(dest)
1027 exists = os.path.exists(wdest)
1021 exists = os.path.exists(wdest)
1028 if exists:
1022 if exists:
1029 fd, tempname = tempfile.mkstemp(
1023 fd, tempname = tempfile.mkstemp(
1030 prefix='hg-copy-', dir=os.path.dirname(wdest))
1024 prefix='hg-copy-', dir=os.path.dirname(wdest))
1031 os.close(fd)
1025 os.close(fd)
1032 os.unlink(tempname)
1026 os.unlink(tempname)
1033 os.rename(wdest, tempname)
1027 os.rename(wdest, tempname)
1034 try:
1028 try:
1035 self.run0('copy', source, dest)
1029 self.run0('copy', source, dest)
1036 finally:
1030 finally:
1037 if exists:
1031 if exists:
1038 try:
1032 try:
1039 os.unlink(wdest)
1033 os.unlink(wdest)
1040 except OSError:
1034 except OSError:
1041 pass
1035 pass
1042 os.rename(tempname, wdest)
1036 os.rename(tempname, wdest)
1043
1037
1044 def dirs_of(self, files):
1038 def dirs_of(self, files):
1045 dirs = util.set()
1039 dirs = util.set()
1046 for f in files:
1040 for f in files:
1047 if os.path.isdir(self.wjoin(f)):
1041 if os.path.isdir(self.wjoin(f)):
1048 dirs.add(f)
1042 dirs.add(f)
1049 for i in strutil.rfindall(f, '/'):
1043 for i in strutil.rfindall(f, '/'):
1050 dirs.add(f[:i])
1044 dirs.add(f[:i])
1051 return dirs
1045 return dirs
1052
1046
1053 def add_dirs(self, files):
1047 def add_dirs(self, files):
1054 add_dirs = [d for d in self.dirs_of(files)
1048 add_dirs = [d for d in self.dirs_of(files)
1055 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1049 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1056 if add_dirs:
1050 if add_dirs:
1057 add_dirs.sort()
1051 add_dirs.sort()
1058 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1052 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1059 return add_dirs
1053 return add_dirs
1060
1054
1061 def add_files(self, files):
1055 def add_files(self, files):
1062 if files:
1056 if files:
1063 self.xargs(files, 'add', quiet=True)
1057 self.xargs(files, 'add', quiet=True)
1064 return files
1058 return files
1065
1059
1066 def tidy_dirs(self, names):
1060 def tidy_dirs(self, names):
1067 dirs = list(self.dirs_of(names))
1061 dirs = list(self.dirs_of(names))
1068 dirs.sort()
1062 dirs.sort()
1069 dirs.reverse()
1063 dirs.reverse()
1070 deleted = []
1064 deleted = []
1071 for d in dirs:
1065 for d in dirs:
1072 wd = self.wjoin(d)
1066 wd = self.wjoin(d)
1073 if os.listdir(wd) == '.svn':
1067 if os.listdir(wd) == '.svn':
1074 self.run0('delete', d)
1068 self.run0('delete', d)
1075 deleted.append(d)
1069 deleted.append(d)
1076 return deleted
1070 return deleted
1077
1071
1078 def addchild(self, parent, child):
1072 def addchild(self, parent, child):
1079 self.childmap[parent] = child
1073 self.childmap[parent] = child
1080
1074
1081 def revid(self, rev):
1075 def revid(self, rev):
1082 return u"svn:%s@%s" % (self.uuid, rev)
1076 return u"svn:%s@%s" % (self.uuid, rev)
1083
1077
1084 def putcommit(self, files, parents, commit):
1078 def putcommit(self, files, copies, parents, commit, source):
1079 # Apply changes to working copy
1080 for f, v in files:
1081 try:
1082 data = source.getfile(f, v)
1083 except IOError, inst:
1084 self.delete.append(f)
1085 else:
1086 e = source.getmode(f, v)
1087 self.putfile(f, e, data)
1088 if f in copies:
1089 self.copies.append([copies[f], f])
1090 files = [f[0] for f in files]
1091
1085 for parent in parents:
1092 for parent in parents:
1086 try:
1093 try:
1087 return self.revid(self.childmap[parent])
1094 return self.revid(self.childmap[parent])
1088 except KeyError:
1095 except KeyError:
1089 pass
1096 pass
1090 entries = util.set(self.delete)
1097 entries = util.set(self.delete)
1091 files = util.frozenset(files)
1098 files = util.frozenset(files)
1092 entries.update(self.add_dirs(files.difference(entries)))
1099 entries.update(self.add_dirs(files.difference(entries)))
1093 if self.copies:
1100 if self.copies:
1094 for s, d in self.copies:
1101 for s, d in self.copies:
1095 self._copyfile(s, d)
1102 self._copyfile(s, d)
1096 self.copies = []
1103 self.copies = []
1097 if self.delete:
1104 if self.delete:
1098 self.xargs(self.delete, 'delete')
1105 self.xargs(self.delete, 'delete')
1099 self.delete = []
1106 self.delete = []
1100 entries.update(self.add_files(files.difference(entries)))
1107 entries.update(self.add_files(files.difference(entries)))
1101 entries.update(self.tidy_dirs(entries))
1108 entries.update(self.tidy_dirs(entries))
1102 if self.delexec:
1109 if self.delexec:
1103 self.xargs(self.delexec, 'propdel', 'svn:executable')
1110 self.xargs(self.delexec, 'propdel', 'svn:executable')
1104 self.delexec = []
1111 self.delexec = []
1105 if self.setexec:
1112 if self.setexec:
1106 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1113 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1107 self.setexec = []
1114 self.setexec = []
1108
1115
1109 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1116 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1110 fp = os.fdopen(fd, 'w')
1117 fp = os.fdopen(fd, 'w')
1111 fp.write(commit.desc)
1118 fp.write(commit.desc)
1112 fp.close()
1119 fp.close()
1113 try:
1120 try:
1114 output = self.run0('commit',
1121 output = self.run0('commit',
1115 username=util.shortuser(commit.author),
1122 username=util.shortuser(commit.author),
1116 file=messagefile,
1123 file=messagefile,
1117 encoding='utf-8')
1124 encoding='utf-8')
1118 try:
1125 try:
1119 rev = self.commit_re.search(output).group(1)
1126 rev = self.commit_re.search(output).group(1)
1120 except AttributeError:
1127 except AttributeError:
1121 self.ui.warn(_('unexpected svn output:\n'))
1128 self.ui.warn(_('unexpected svn output:\n'))
1122 self.ui.warn(output)
1129 self.ui.warn(output)
1123 raise util.Abort(_('unable to cope with svn output'))
1130 raise util.Abort(_('unable to cope with svn output'))
1124 if commit.rev:
1131 if commit.rev:
1125 self.run('propset', 'hg:convert-rev', commit.rev,
1132 self.run('propset', 'hg:convert-rev', commit.rev,
1126 revprop=True, revision=rev)
1133 revprop=True, revision=rev)
1127 if commit.branch and commit.branch != 'default':
1134 if commit.branch and commit.branch != 'default':
1128 self.run('propset', 'hg:convert-branch', commit.branch,
1135 self.run('propset', 'hg:convert-branch', commit.branch,
1129 revprop=True, revision=rev)
1136 revprop=True, revision=rev)
1130 for parent in parents:
1137 for parent in parents:
1131 self.addchild(parent, rev)
1138 self.addchild(parent, rev)
1132 return self.revid(rev)
1139 return self.revid(rev)
1133 finally:
1140 finally:
1134 os.unlink(messagefile)
1141 os.unlink(messagefile)
1135
1142
1136 def puttags(self, tags):
1143 def puttags(self, tags):
1137 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
1144 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now