##// END OF EJS Templates
convert: add commandline.xargs(), use it in svn_sink class...
Maxim Dounin -
r5832:2192ed18 default
parent child Browse files
Show More
@@ -1,297 +1,342
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 cPickle as pickle
4 import cPickle as pickle
4 from mercurial import util
5 from mercurial import util
5 from mercurial.i18n import _
6 from mercurial.i18n import _
6
7
7 def encodeargs(args):
8 def encodeargs(args):
8 def encodearg(s):
9 def encodearg(s):
9 lines = base64.encodestring(s)
10 lines = base64.encodestring(s)
10 lines = [l.splitlines()[0] for l in lines]
11 lines = [l.splitlines()[0] for l in lines]
11 return ''.join(lines)
12 return ''.join(lines)
12
13
13 s = pickle.dumps(args)
14 s = pickle.dumps(args)
14 return encodearg(s)
15 return encodearg(s)
15
16
16 def decodeargs(s):
17 def decodeargs(s):
17 s = base64.decodestring(s)
18 s = base64.decodestring(s)
18 return pickle.loads(s)
19 return pickle.loads(s)
19
20
20 def checktool(exe, name=None):
21 def checktool(exe, name=None):
21 name = name or exe
22 name = name or exe
22 if not util.find_exe(exe):
23 if not util.find_exe(exe):
23 raise util.Abort('cannot find required "%s" tool' % name)
24 raise util.Abort('cannot find required "%s" tool' % name)
24
25
25 class NoRepo(Exception): pass
26 class NoRepo(Exception): pass
26
27
27 SKIPREV = 'SKIP'
28 SKIPREV = 'SKIP'
28
29
29 class commit(object):
30 class commit(object):
30 def __init__(self, author, date, desc, parents, branch=None, rev=None,
31 def __init__(self, author, date, desc, parents, branch=None, rev=None,
31 extra={}):
32 extra={}):
32 self.author = author
33 self.author = author
33 self.date = date
34 self.date = date
34 self.desc = desc
35 self.desc = desc
35 self.parents = parents
36 self.parents = parents
36 self.branch = branch
37 self.branch = branch
37 self.rev = rev
38 self.rev = rev
38 self.extra = extra
39 self.extra = extra
39
40
40 class converter_source(object):
41 class converter_source(object):
41 """Conversion source interface"""
42 """Conversion source interface"""
42
43
43 def __init__(self, ui, path=None, rev=None):
44 def __init__(self, ui, path=None, rev=None):
44 """Initialize conversion source (or raise NoRepo("message")
45 """Initialize conversion source (or raise NoRepo("message")
45 exception if path is not a valid repository)"""
46 exception if path is not a valid repository)"""
46 self.ui = ui
47 self.ui = ui
47 self.path = path
48 self.path = path
48 self.rev = rev
49 self.rev = rev
49
50
50 self.encoding = 'utf-8'
51 self.encoding = 'utf-8'
51
52
52 def before(self):
53 def before(self):
53 pass
54 pass
54
55
55 def after(self):
56 def after(self):
56 pass
57 pass
57
58
58 def setrevmap(self, revmap):
59 def setrevmap(self, revmap):
59 """set the map of already-converted revisions"""
60 """set the map of already-converted revisions"""
60 pass
61 pass
61
62
62 def getheads(self):
63 def getheads(self):
63 """Return a list of this repository's heads"""
64 """Return a list of this repository's heads"""
64 raise NotImplementedError()
65 raise NotImplementedError()
65
66
66 def getfile(self, name, rev):
67 def getfile(self, name, rev):
67 """Return file contents as a string"""
68 """Return file contents as a string"""
68 raise NotImplementedError()
69 raise NotImplementedError()
69
70
70 def getmode(self, name, rev):
71 def getmode(self, name, rev):
71 """Return file mode, eg. '', 'x', or 'l'"""
72 """Return file mode, eg. '', 'x', or 'l'"""
72 raise NotImplementedError()
73 raise NotImplementedError()
73
74
74 def getchanges(self, version):
75 def getchanges(self, version):
75 """Returns a tuple of (files, copies)
76 """Returns a tuple of (files, copies)
76 Files is a sorted list of (filename, id) tuples for all files changed
77 Files is a sorted list of (filename, id) tuples for all files changed
77 in version, where id is the source revision id of the file.
78 in version, where id is the source revision id of the file.
78
79
79 copies is a dictionary of dest: source
80 copies is a dictionary of dest: source
80 """
81 """
81 raise NotImplementedError()
82 raise NotImplementedError()
82
83
83 def getcommit(self, version):
84 def getcommit(self, version):
84 """Return the commit object for version"""
85 """Return the commit object for version"""
85 raise NotImplementedError()
86 raise NotImplementedError()
86
87
87 def gettags(self):
88 def gettags(self):
88 """Return the tags as a dictionary of name: revision"""
89 """Return the tags as a dictionary of name: revision"""
89 raise NotImplementedError()
90 raise NotImplementedError()
90
91
91 def recode(self, s, encoding=None):
92 def recode(self, s, encoding=None):
92 if not encoding:
93 if not encoding:
93 encoding = self.encoding or 'utf-8'
94 encoding = self.encoding or 'utf-8'
94
95
95 if isinstance(s, unicode):
96 if isinstance(s, unicode):
96 return s.encode("utf-8")
97 return s.encode("utf-8")
97 try:
98 try:
98 return s.decode(encoding).encode("utf-8")
99 return s.decode(encoding).encode("utf-8")
99 except:
100 except:
100 try:
101 try:
101 return s.decode("latin-1").encode("utf-8")
102 return s.decode("latin-1").encode("utf-8")
102 except:
103 except:
103 return s.decode(encoding, "replace").encode("utf-8")
104 return s.decode(encoding, "replace").encode("utf-8")
104
105
105 def getchangedfiles(self, rev, i):
106 def getchangedfiles(self, rev, i):
106 """Return the files changed by rev compared to parent[i].
107 """Return the files changed by rev compared to parent[i].
107
108
108 i is an index selecting one of the parents of rev. The return
109 i is an index selecting one of the parents of rev. The return
109 value should be the list of files that are different in rev and
110 value should be the list of files that are different in rev and
110 this parent.
111 this parent.
111
112
112 If rev has no parents, i is None.
113 If rev has no parents, i is None.
113
114
114 This function is only needed to support --filemap
115 This function is only needed to support --filemap
115 """
116 """
116 raise NotImplementedError()
117 raise NotImplementedError()
117
118
118 def converted(self, rev, sinkrev):
119 def converted(self, rev, sinkrev):
119 '''Notify the source that a revision has been converted.'''
120 '''Notify the source that a revision has been converted.'''
120 pass
121 pass
121
122
122
123
123 class converter_sink(object):
124 class converter_sink(object):
124 """Conversion sink (target) interface"""
125 """Conversion sink (target) interface"""
125
126
126 def __init__(self, ui, path):
127 def __init__(self, ui, path):
127 """Initialize conversion sink (or raise NoRepo("message")
128 """Initialize conversion sink (or raise NoRepo("message")
128 exception if path is not a valid repository)
129 exception if path is not a valid repository)
129
130
130 created is a list of paths to remove if a fatal error occurs
131 created is a list of paths to remove if a fatal error occurs
131 later"""
132 later"""
132 self.ui = ui
133 self.ui = ui
133 self.path = path
134 self.path = path
134 self.created = []
135 self.created = []
135
136
136 def getheads(self):
137 def getheads(self):
137 """Return a list of this repository's heads"""
138 """Return a list of this repository's heads"""
138 raise NotImplementedError()
139 raise NotImplementedError()
139
140
140 def revmapfile(self):
141 def revmapfile(self):
141 """Path to a file that will contain lines
142 """Path to a file that will contain lines
142 source_rev_id sink_rev_id
143 source_rev_id sink_rev_id
143 mapping equivalent revision identifiers for each system."""
144 mapping equivalent revision identifiers for each system."""
144 raise NotImplementedError()
145 raise NotImplementedError()
145
146
146 def authorfile(self):
147 def authorfile(self):
147 """Path to a file that will contain lines
148 """Path to a file that will contain lines
148 srcauthor=dstauthor
149 srcauthor=dstauthor
149 mapping equivalent authors identifiers for each system."""
150 mapping equivalent authors identifiers for each system."""
150 return None
151 return None
151
152
152 def putfile(self, f, e, data):
153 def putfile(self, f, e, data):
153 """Put file for next putcommit().
154 """Put file for next putcommit().
154 f: path to file
155 f: path to file
155 e: '', 'x', or 'l' (regular file, executable, or symlink)
156 e: '', 'x', or 'l' (regular file, executable, or symlink)
156 data: file contents"""
157 data: file contents"""
157 raise NotImplementedError()
158 raise NotImplementedError()
158
159
159 def delfile(self, f):
160 def delfile(self, f):
160 """Delete file for next putcommit().
161 """Delete file for next putcommit().
161 f: path to file"""
162 f: path to file"""
162 raise NotImplementedError()
163 raise NotImplementedError()
163
164
164 def putcommit(self, files, parents, commit):
165 def putcommit(self, files, parents, commit):
165 """Create a revision with all changed files listed in 'files'
166 """Create a revision with all changed files listed in 'files'
166 and having listed parents. 'commit' is a commit object containing
167 and having listed parents. 'commit' is a commit object containing
167 at a minimum the author, date, and message for this changeset.
168 at a minimum the author, date, and message for this changeset.
168 Called after putfile() and delfile() calls. Note that the sink
169 Called after putfile() and delfile() calls. Note that the sink
169 repository is not told to update itself to a particular revision
170 repository is not told to update itself to a particular revision
170 (or even what that revision would be) before it receives the
171 (or even what that revision would be) before it receives the
171 file data."""
172 file data."""
172 raise NotImplementedError()
173 raise NotImplementedError()
173
174
174 def puttags(self, tags):
175 def puttags(self, tags):
175 """Put tags into sink.
176 """Put tags into sink.
176 tags: {tagname: sink_rev_id, ...}"""
177 tags: {tagname: sink_rev_id, ...}"""
177 raise NotImplementedError()
178 raise NotImplementedError()
178
179
179 def setbranch(self, branch, pbranch, parents):
180 def setbranch(self, branch, pbranch, parents):
180 """Set the current branch name. Called before the first putfile
181 """Set the current branch name. Called before the first putfile
181 on the branch.
182 on the branch.
182 branch: branch name for subsequent commits
183 branch: branch name for subsequent commits
183 pbranch: branch name of parent commit
184 pbranch: branch name of parent commit
184 parents: destination revisions of parent"""
185 parents: destination revisions of parent"""
185 pass
186 pass
186
187
187 def setfilemapmode(self, active):
188 def setfilemapmode(self, active):
188 """Tell the destination that we're using a filemap
189 """Tell the destination that we're using a filemap
189
190
190 Some converter_sources (svn in particular) can claim that a file
191 Some converter_sources (svn in particular) can claim that a file
191 was changed in a revision, even if there was no change. This method
192 was changed in a revision, even if there was no change. This method
192 tells the destination that we're using a filemap and that it should
193 tells the destination that we're using a filemap and that it should
193 filter empty revisions.
194 filter empty revisions.
194 """
195 """
195 pass
196 pass
196
197
197 def before(self):
198 def before(self):
198 pass
199 pass
199
200
200 def after(self):
201 def after(self):
201 pass
202 pass
202
203
203
204
204 class commandline(object):
205 class commandline(object):
205 def __init__(self, ui, command):
206 def __init__(self, ui, command):
206 self.ui = ui
207 self.ui = ui
207 self.command = command
208 self.command = command
208
209
209 def prerun(self):
210 def prerun(self):
210 pass
211 pass
211
212
212 def postrun(self):
213 def postrun(self):
213 pass
214 pass
214
215
215 def _run(self, cmd, *args, **kwargs):
216 def _cmdline(self, cmd, *args, **kwargs):
216 cmdline = [self.command, cmd] + list(args)
217 cmdline = [self.command, cmd] + list(args)
217 for k, v in kwargs.iteritems():
218 for k, v in kwargs.iteritems():
218 if len(k) == 1:
219 if len(k) == 1:
219 cmdline.append('-' + k)
220 cmdline.append('-' + k)
220 else:
221 else:
221 cmdline.append('--' + k.replace('_', '-'))
222 cmdline.append('--' + k.replace('_', '-'))
222 try:
223 try:
223 if len(k) == 1:
224 if len(k) == 1:
224 cmdline.append('' + v)
225 cmdline.append('' + v)
225 else:
226 else:
226 cmdline[-1] += '=' + v
227 cmdline[-1] += '=' + v
227 except TypeError:
228 except TypeError:
228 pass
229 pass
229 cmdline = [util.shellquote(arg) for arg in cmdline]
230 cmdline = [util.shellquote(arg) for arg in cmdline]
230 cmdline += ['<', util.nulldev]
231 cmdline += ['<', util.nulldev]
231 cmdline = ' '.join(cmdline)
232 cmdline = ' '.join(cmdline)
232 self.ui.debug(cmdline, '\n')
233 self.ui.debug(cmdline, '\n')
234 return cmdline
233
235
236 def _run(self, cmd, *args, **kwargs):
237 cmdline = self._cmdline(cmd, *args, **kwargs)
234 self.prerun()
238 self.prerun()
235 try:
239 try:
236 return util.popen(cmdline)
240 return util.popen(cmdline)
237 finally:
241 finally:
238 self.postrun()
242 self.postrun()
239
243
240 def run(self, cmd, *args, **kwargs):
244 def run(self, cmd, *args, **kwargs):
241 fp = self._run(cmd, *args, **kwargs)
245 fp = self._run(cmd, *args, **kwargs)
242 output = fp.read()
246 output = fp.read()
243 self.ui.debug(output)
247 self.ui.debug(output)
244 return output, fp.close()
248 return output, fp.close()
245
249
246 def checkexit(self, status, output=''):
250 def checkexit(self, status, output=''):
247 if status:
251 if status:
248 if output:
252 if output:
249 self.ui.warn(_('%s error:\n') % self.command)
253 self.ui.warn(_('%s error:\n') % self.command)
250 self.ui.warn(output)
254 self.ui.warn(output)
251 msg = util.explain_exit(status)[0]
255 msg = util.explain_exit(status)[0]
252 raise util.Abort(_('%s %s') % (self.command, msg))
256 raise util.Abort(_('%s %s') % (self.command, msg))
253
257
254 def run0(self, cmd, *args, **kwargs):
258 def run0(self, cmd, *args, **kwargs):
255 output, status = self.run(cmd, *args, **kwargs)
259 output, status = self.run(cmd, *args, **kwargs)
256 self.checkexit(status, output)
260 self.checkexit(status, output)
257 return output
261 return output
258
262
263 def getargmax(self):
264 if '_argmax' in self.__dict__:
265 return self._argmax
266
267 # POSIX requires at least 4096 bytes for ARG_MAX
268 self._argmax = 4096
269 try:
270 self._argmax = os.sysconf("SC_ARG_MAX")
271 except:
272 pass
273
274 # Windows shells impose their own limits on command line length,
275 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
276 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
277 # details about cmd.exe limitations.
278
279 # Since ARG_MAX is for command line _and_ environment, lower our limit
280 # (and make happy Windows shells while doing this).
281
282 self._argmax = self._argmax/2 - 1
283 return self._argmax
284
285 def limit_arglist(self, arglist, cmd, *args, **kwargs):
286 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
287 bytes = 0
288 fl = []
289 for fn in arglist:
290 b = len(fn) + 3
291 if bytes + b < limit or len(fl) == 0:
292 fl.append(fn)
293 bytes += b
294 else:
295 yield fl
296 fl = [fn]
297 bytes = b
298 if fl:
299 yield fl
300
301 def xargs(self, arglist, cmd, *args, **kwargs):
302 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
303 self.run0(cmd, *(list(args) + l), **kwargs)
259
304
260 class mapfile(dict):
305 class mapfile(dict):
261 def __init__(self, ui, path):
306 def __init__(self, ui, path):
262 super(mapfile, self).__init__()
307 super(mapfile, self).__init__()
263 self.ui = ui
308 self.ui = ui
264 self.path = path
309 self.path = path
265 self.fp = None
310 self.fp = None
266 self.order = []
311 self.order = []
267 self._read()
312 self._read()
268
313
269 def _read(self):
314 def _read(self):
270 try:
315 try:
271 fp = open(self.path, 'r')
316 fp = open(self.path, 'r')
272 except IOError, err:
317 except IOError, err:
273 if err.errno != errno.ENOENT:
318 if err.errno != errno.ENOENT:
274 raise
319 raise
275 return
320 return
276 for line in fp:
321 for line in fp:
277 key, value = line[:-1].split(' ', 1)
322 key, value = line[:-1].split(' ', 1)
278 if key not in self:
323 if key not in self:
279 self.order.append(key)
324 self.order.append(key)
280 super(mapfile, self).__setitem__(key, value)
325 super(mapfile, self).__setitem__(key, value)
281 fp.close()
326 fp.close()
282
327
283 def __setitem__(self, key, value):
328 def __setitem__(self, key, value):
284 if self.fp is None:
329 if self.fp is None:
285 try:
330 try:
286 self.fp = open(self.path, 'a')
331 self.fp = open(self.path, 'a')
287 except IOError, err:
332 except IOError, err:
288 raise util.Abort(_('could not open map file %r: %s') %
333 raise util.Abort(_('could not open map file %r: %s') %
289 (self.path, err.strerror))
334 (self.path, err.strerror))
290 self.fp.write('%s %s\n' % (key, value))
335 self.fp.write('%s %s\n' % (key, value))
291 self.fp.flush()
336 self.fp.flush()
292 super(mapfile, self).__setitem__(key, value)
337 super(mapfile, self).__setitem__(key, value)
293
338
294 def close(self):
339 def close(self):
295 if self.fp:
340 if self.fp:
296 self.fp.close()
341 self.fp.close()
297 self.fp = None
342 self.fp = None
@@ -1,952 +1,926
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 = '/' + path.replace('\\', '/')
56 path = '/' + path.replace('\\', '/')
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 else:
92 else:
93 pickle.dump(None, fp, protocol)
93 pickle.dump(None, fp, protocol)
94 fp.close()
94 fp.close()
95
95
96 def debugsvnlog(ui, **opts):
96 def debugsvnlog(ui, **opts):
97 """Fetch SVN log in a subprocess and channel them back to parent to
97 """Fetch SVN log in a subprocess and channel them back to parent to
98 avoid memory collection issues.
98 avoid memory collection issues.
99 """
99 """
100 util.set_binary(sys.stdin)
100 util.set_binary(sys.stdin)
101 util.set_binary(sys.stdout)
101 util.set_binary(sys.stdout)
102 args = decodeargs(sys.stdin.read())
102 args = decodeargs(sys.stdin.read())
103 get_log_child(sys.stdout, *args)
103 get_log_child(sys.stdout, *args)
104
104
105 # SVN conversion code stolen from bzr-svn and tailor
105 # SVN conversion code stolen from bzr-svn and tailor
106 class svn_source(converter_source):
106 class svn_source(converter_source):
107 def __init__(self, ui, url, rev=None):
107 def __init__(self, ui, url, rev=None):
108 super(svn_source, self).__init__(ui, url, rev=rev)
108 super(svn_source, self).__init__(ui, url, rev=rev)
109
109
110 try:
110 try:
111 SubversionException
111 SubversionException
112 except NameError:
112 except NameError:
113 raise NoRepo('Subversion python bindings could not be loaded')
113 raise NoRepo('Subversion python bindings could not be loaded')
114
114
115 self.encoding = locale.getpreferredencoding()
115 self.encoding = locale.getpreferredencoding()
116 self.lastrevs = {}
116 self.lastrevs = {}
117
117
118 latest = None
118 latest = None
119 try:
119 try:
120 # Support file://path@rev syntax. Useful e.g. to convert
120 # Support file://path@rev syntax. Useful e.g. to convert
121 # deleted branches.
121 # deleted branches.
122 at = url.rfind('@')
122 at = url.rfind('@')
123 if at >= 0:
123 if at >= 0:
124 latest = int(url[at+1:])
124 latest = int(url[at+1:])
125 url = url[:at]
125 url = url[:at]
126 except ValueError, e:
126 except ValueError, e:
127 pass
127 pass
128 self.url = geturl(url)
128 self.url = geturl(url)
129 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
129 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
130 try:
130 try:
131 self.transport = transport.SvnRaTransport(url=self.url)
131 self.transport = transport.SvnRaTransport(url=self.url)
132 self.ra = self.transport.ra
132 self.ra = self.transport.ra
133 self.ctx = self.transport.client
133 self.ctx = self.transport.client
134 self.base = svn.ra.get_repos_root(self.ra)
134 self.base = svn.ra.get_repos_root(self.ra)
135 self.module = self.url[len(self.base):]
135 self.module = self.url[len(self.base):]
136 self.modulemap = {} # revision, module
136 self.modulemap = {} # revision, module
137 self.commits = {}
137 self.commits = {}
138 self.paths = {}
138 self.paths = {}
139 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
139 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
140 except SubversionException, e:
140 except SubversionException, e:
141 ui.print_exc()
141 ui.print_exc()
142 raise NoRepo("%s does not look like a Subversion repo" % self.url)
142 raise NoRepo("%s does not look like a Subversion repo" % self.url)
143
143
144 if rev:
144 if rev:
145 try:
145 try:
146 latest = int(rev)
146 latest = int(rev)
147 except ValueError:
147 except ValueError:
148 raise util.Abort('svn: revision %s is not an integer' % rev)
148 raise util.Abort('svn: revision %s is not an integer' % rev)
149
149
150 try:
150 try:
151 self.get_blacklist()
151 self.get_blacklist()
152 except IOError, e:
152 except IOError, e:
153 pass
153 pass
154
154
155 self.last_changed = self.latest(self.module, latest)
155 self.last_changed = self.latest(self.module, latest)
156
156
157 self.head = self.revid(self.last_changed)
157 self.head = self.revid(self.last_changed)
158 self._changescache = None
158 self._changescache = None
159
159
160 if os.path.exists(os.path.join(url, '.svn/entries')):
160 if os.path.exists(os.path.join(url, '.svn/entries')):
161 self.wc = url
161 self.wc = url
162 else:
162 else:
163 self.wc = None
163 self.wc = None
164 self.convertfp = None
164 self.convertfp = None
165
165
166 def setrevmap(self, revmap):
166 def setrevmap(self, revmap):
167 lastrevs = {}
167 lastrevs = {}
168 for revid in revmap.iterkeys():
168 for revid in revmap.iterkeys():
169 uuid, module, revnum = self.revsplit(revid)
169 uuid, module, revnum = self.revsplit(revid)
170 lastrevnum = lastrevs.setdefault(module, revnum)
170 lastrevnum = lastrevs.setdefault(module, revnum)
171 if revnum > lastrevnum:
171 if revnum > lastrevnum:
172 lastrevs[module] = revnum
172 lastrevs[module] = revnum
173 self.lastrevs = lastrevs
173 self.lastrevs = lastrevs
174
174
175 def exists(self, path, optrev):
175 def exists(self, path, optrev):
176 try:
176 try:
177 svn.client.ls(self.url.rstrip('/') + '/' + path,
177 svn.client.ls(self.url.rstrip('/') + '/' + path,
178 optrev, False, self.ctx)
178 optrev, False, self.ctx)
179 return True
179 return True
180 except SubversionException, err:
180 except SubversionException, err:
181 return False
181 return False
182
182
183 def getheads(self):
183 def getheads(self):
184 # detect standard /branches, /tags, /trunk layout
184 # detect standard /branches, /tags, /trunk layout
185 rev = optrev(self.last_changed)
185 rev = optrev(self.last_changed)
186 rpath = self.url.strip('/')
186 rpath = self.url.strip('/')
187 cfgtrunk = self.ui.config('convert', 'svn.trunk')
187 cfgtrunk = self.ui.config('convert', 'svn.trunk')
188 cfgbranches = self.ui.config('convert', 'svn.branches')
188 cfgbranches = self.ui.config('convert', 'svn.branches')
189 cfgtags = self.ui.config('convert', 'svn.tags')
189 cfgtags = self.ui.config('convert', 'svn.tags')
190 trunk = (cfgtrunk or 'trunk').strip('/')
190 trunk = (cfgtrunk or 'trunk').strip('/')
191 branches = (cfgbranches or 'branches').strip('/')
191 branches = (cfgbranches or 'branches').strip('/')
192 tags = (cfgtags or 'tags').strip('/')
192 tags = (cfgtags or 'tags').strip('/')
193 if self.exists(trunk, rev) and self.exists(branches, rev) and self.exists(tags, rev):
193 if self.exists(trunk, rev) and self.exists(branches, rev) and self.exists(tags, rev):
194 self.ui.note('found trunk at %r, branches at %r and tags at %r\n' %
194 self.ui.note('found trunk at %r, branches at %r and tags at %r\n' %
195 (trunk, branches, tags))
195 (trunk, branches, tags))
196 oldmodule = self.module
196 oldmodule = self.module
197 self.module += '/' + trunk
197 self.module += '/' + trunk
198 lt = self.latest(self.module, self.last_changed)
198 lt = self.latest(self.module, self.last_changed)
199 self.head = self.revid(lt)
199 self.head = self.revid(lt)
200 self.heads = [self.head]
200 self.heads = [self.head]
201 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
201 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
202 self.ctx)
202 self.ctx)
203 for branch in branchnames.keys():
203 for branch in branchnames.keys():
204 if oldmodule:
204 if oldmodule:
205 module = oldmodule + '/' + branches + '/' + branch
205 module = oldmodule + '/' + branches + '/' + branch
206 else:
206 else:
207 module = '/' + branches + '/' + branch
207 module = '/' + branches + '/' + branch
208 brevnum = self.latest(module, self.last_changed)
208 brevnum = self.latest(module, self.last_changed)
209 brev = self.revid(brevnum, module)
209 brev = self.revid(brevnum, module)
210 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
210 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
211 self.heads.append(brev)
211 self.heads.append(brev)
212
212
213 if oldmodule:
213 if oldmodule:
214 self.tags = '%s/%s' % (oldmodule, tags)
214 self.tags = '%s/%s' % (oldmodule, tags)
215 else:
215 else:
216 self.tags = '/%s' % tags
216 self.tags = '/%s' % tags
217
217
218 elif cfgtrunk or cfgbranches or cfgtags:
218 elif cfgtrunk or cfgbranches or cfgtags:
219 raise util.Abort('trunk/branch/tags layout expected, but not found')
219 raise util.Abort('trunk/branch/tags layout expected, but not found')
220 else:
220 else:
221 self.ui.note('working with one branch\n')
221 self.ui.note('working with one branch\n')
222 self.heads = [self.head]
222 self.heads = [self.head]
223 self.tags = tags
223 self.tags = tags
224 return self.heads
224 return self.heads
225
225
226 def getfile(self, file, rev):
226 def getfile(self, file, rev):
227 data, mode = self._getfile(file, rev)
227 data, mode = self._getfile(file, rev)
228 self.modecache[(file, rev)] = mode
228 self.modecache[(file, rev)] = mode
229 return data
229 return data
230
230
231 def getmode(self, file, rev):
231 def getmode(self, file, rev):
232 return self.modecache[(file, rev)]
232 return self.modecache[(file, rev)]
233
233
234 def getchanges(self, rev):
234 def getchanges(self, rev):
235 if self._changescache and self._changescache[0] == rev:
235 if self._changescache and self._changescache[0] == rev:
236 return self._changescache[1]
236 return self._changescache[1]
237 self._changescache = None
237 self._changescache = None
238 self.modecache = {}
238 self.modecache = {}
239 (paths, parents) = self.paths[rev]
239 (paths, parents) = self.paths[rev]
240 files, copies = self.expandpaths(rev, paths, parents)
240 files, copies = self.expandpaths(rev, paths, parents)
241 files.sort()
241 files.sort()
242 files = zip(files, [rev] * len(files))
242 files = zip(files, [rev] * len(files))
243
243
244 # caller caches the result, so free it here to release memory
244 # caller caches the result, so free it here to release memory
245 del self.paths[rev]
245 del self.paths[rev]
246 return (files, copies)
246 return (files, copies)
247
247
248 def getchangedfiles(self, rev, i):
248 def getchangedfiles(self, rev, i):
249 changes = self.getchanges(rev)
249 changes = self.getchanges(rev)
250 self._changescache = (rev, changes)
250 self._changescache = (rev, changes)
251 return [f[0] for f in changes[0]]
251 return [f[0] for f in changes[0]]
252
252
253 def getcommit(self, rev):
253 def getcommit(self, rev):
254 if rev not in self.commits:
254 if rev not in self.commits:
255 uuid, module, revnum = self.revsplit(rev)
255 uuid, module, revnum = self.revsplit(rev)
256 self.module = module
256 self.module = module
257 self.reparent(module)
257 self.reparent(module)
258 stop = self.lastrevs.get(module, 0)
258 stop = self.lastrevs.get(module, 0)
259 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
259 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
260 commit = self.commits[rev]
260 commit = self.commits[rev]
261 # caller caches the result, so free it here to release memory
261 # caller caches the result, so free it here to release memory
262 del self.commits[rev]
262 del self.commits[rev]
263 return commit
263 return commit
264
264
265 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
265 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
266 strict_node_history=False):
266 strict_node_history=False):
267
267
268 def parent(fp):
268 def parent(fp):
269 while True:
269 while True:
270 entry = pickle.load(fp)
270 entry = pickle.load(fp)
271 try:
271 try:
272 orig_paths, revnum, author, date, message = entry
272 orig_paths, revnum, author, date, message = entry
273 except:
273 except:
274 if entry is None:
274 if entry is None:
275 break
275 break
276 raise SubversionException("child raised exception", entry)
276 raise SubversionException("child raised exception", entry)
277 yield entry
277 yield entry
278
278
279 args = [self.url, paths, start, end, limit, discover_changed_paths,
279 args = [self.url, paths, start, end, limit, discover_changed_paths,
280 strict_node_history]
280 strict_node_history]
281 arg = encodeargs(args)
281 arg = encodeargs(args)
282 hgexe = util.hgexecutable()
282 hgexe = util.hgexecutable()
283 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
283 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
284 stdin, stdout = os.popen2(cmd, 'b')
284 stdin, stdout = os.popen2(cmd, 'b')
285
285
286 stdin.write(arg)
286 stdin.write(arg)
287 stdin.close()
287 stdin.close()
288
288
289 for p in parent(stdout):
289 for p in parent(stdout):
290 yield p
290 yield p
291
291
292 def gettags(self):
292 def gettags(self):
293 tags = {}
293 tags = {}
294 start = self.revnum(self.head)
294 start = self.revnum(self.head)
295 try:
295 try:
296 for entry in self.get_log([self.tags], 0, start):
296 for entry in self.get_log([self.tags], 0, start):
297 orig_paths, revnum, author, date, message = entry
297 orig_paths, revnum, author, date, message = entry
298 for path in orig_paths:
298 for path in orig_paths:
299 if not path.startswith(self.tags+'/'):
299 if not path.startswith(self.tags+'/'):
300 continue
300 continue
301 ent = orig_paths[path]
301 ent = orig_paths[path]
302 source = ent.copyfrom_path
302 source = ent.copyfrom_path
303 rev = ent.copyfrom_rev
303 rev = ent.copyfrom_rev
304 tag = path.split('/')[-1]
304 tag = path.split('/')[-1]
305 tags[tag] = self.revid(rev, module=source)
305 tags[tag] = self.revid(rev, module=source)
306 except SubversionException, (inst, num):
306 except SubversionException, (inst, num):
307 self.ui.note('no tags found at revision %d\n' % start)
307 self.ui.note('no tags found at revision %d\n' % start)
308 return tags
308 return tags
309
309
310 def converted(self, rev, destrev):
310 def converted(self, rev, destrev):
311 if not self.wc:
311 if not self.wc:
312 return
312 return
313 if self.convertfp is None:
313 if self.convertfp is None:
314 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
314 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
315 'a')
315 'a')
316 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
316 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
317 self.convertfp.flush()
317 self.convertfp.flush()
318
318
319 # -- helper functions --
319 # -- helper functions --
320
320
321 def revid(self, revnum, module=None):
321 def revid(self, revnum, module=None):
322 if not module:
322 if not module:
323 module = self.module
323 module = self.module
324 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
324 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
325 revnum)
325 revnum)
326
326
327 def revnum(self, rev):
327 def revnum(self, rev):
328 return int(rev.split('@')[-1])
328 return int(rev.split('@')[-1])
329
329
330 def revsplit(self, rev):
330 def revsplit(self, rev):
331 url, revnum = rev.encode(self.encoding).split('@', 1)
331 url, revnum = rev.encode(self.encoding).split('@', 1)
332 revnum = int(revnum)
332 revnum = int(revnum)
333 parts = url.split('/', 1)
333 parts = url.split('/', 1)
334 uuid = parts.pop(0)[4:]
334 uuid = parts.pop(0)[4:]
335 mod = ''
335 mod = ''
336 if parts:
336 if parts:
337 mod = '/' + parts[0]
337 mod = '/' + parts[0]
338 return uuid, mod, revnum
338 return uuid, mod, revnum
339
339
340 def latest(self, path, stop=0):
340 def latest(self, path, stop=0):
341 'find the latest revision affecting path, up to stop'
341 'find the latest revision affecting path, up to stop'
342 if not stop:
342 if not stop:
343 stop = svn.ra.get_latest_revnum(self.ra)
343 stop = svn.ra.get_latest_revnum(self.ra)
344 try:
344 try:
345 self.reparent('')
345 self.reparent('')
346 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
346 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
347 self.reparent(self.module)
347 self.reparent(self.module)
348 except SubversionException:
348 except SubversionException:
349 dirent = None
349 dirent = None
350 if not dirent:
350 if not dirent:
351 raise util.Abort('%s not found up to revision %d' % (path, stop))
351 raise util.Abort('%s not found up to revision %d' % (path, stop))
352
352
353 return dirent.created_rev
353 return dirent.created_rev
354
354
355 def get_blacklist(self):
355 def get_blacklist(self):
356 """Avoid certain revision numbers.
356 """Avoid certain revision numbers.
357 It is not uncommon for two nearby revisions to cancel each other
357 It is not uncommon for two nearby revisions to cancel each other
358 out, e.g. 'I copied trunk into a subdirectory of itself instead
358 out, e.g. 'I copied trunk into a subdirectory of itself instead
359 of making a branch'. The converted repository is significantly
359 of making a branch'. The converted repository is significantly
360 smaller if we ignore such revisions."""
360 smaller if we ignore such revisions."""
361 self.blacklist = util.set()
361 self.blacklist = util.set()
362 blacklist = self.blacklist
362 blacklist = self.blacklist
363 for line in file("blacklist.txt", "r"):
363 for line in file("blacklist.txt", "r"):
364 if not line.startswith("#"):
364 if not line.startswith("#"):
365 try:
365 try:
366 svn_rev = int(line.strip())
366 svn_rev = int(line.strip())
367 blacklist.add(svn_rev)
367 blacklist.add(svn_rev)
368 except ValueError, e:
368 except ValueError, e:
369 pass # not an integer or a comment
369 pass # not an integer or a comment
370
370
371 def is_blacklisted(self, svn_rev):
371 def is_blacklisted(self, svn_rev):
372 return svn_rev in self.blacklist
372 return svn_rev in self.blacklist
373
373
374 def reparent(self, module):
374 def reparent(self, module):
375 svn_url = self.base + module
375 svn_url = self.base + module
376 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
376 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
377 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
377 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
378
378
379 def expandpaths(self, rev, paths, parents):
379 def expandpaths(self, rev, paths, parents):
380 def get_entry_from_path(path, module=self.module):
380 def get_entry_from_path(path, module=self.module):
381 # Given the repository url of this wc, say
381 # Given the repository url of this wc, say
382 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
382 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
383 # extract the "entry" portion (a relative path) from what
383 # extract the "entry" portion (a relative path) from what
384 # svn log --xml says, ie
384 # svn log --xml says, ie
385 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
385 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
386 # that is to say "tests/PloneTestCase.py"
386 # that is to say "tests/PloneTestCase.py"
387 if path.startswith(module):
387 if path.startswith(module):
388 relative = path[len(module):]
388 relative = path[len(module):]
389 if relative.startswith('/'):
389 if relative.startswith('/'):
390 return relative[1:]
390 return relative[1:]
391 else:
391 else:
392 return relative
392 return relative
393
393
394 # The path is outside our tracked tree...
394 # The path is outside our tracked tree...
395 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
395 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
396 return None
396 return None
397
397
398 entries = []
398 entries = []
399 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
399 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
400 copies = {}
400 copies = {}
401 revnum = self.revnum(rev)
401 revnum = self.revnum(rev)
402
402
403 if revnum in self.modulemap:
403 if revnum in self.modulemap:
404 new_module = self.modulemap[revnum]
404 new_module = self.modulemap[revnum]
405 if new_module != self.module:
405 if new_module != self.module:
406 self.module = new_module
406 self.module = new_module
407 self.reparent(self.module)
407 self.reparent(self.module)
408
408
409 for path, ent in paths:
409 for path, ent in paths:
410 entrypath = get_entry_from_path(path, module=self.module)
410 entrypath = get_entry_from_path(path, module=self.module)
411 entry = entrypath.decode(self.encoding)
411 entry = entrypath.decode(self.encoding)
412
412
413 kind = svn.ra.check_path(self.ra, entrypath, revnum)
413 kind = svn.ra.check_path(self.ra, entrypath, revnum)
414 if kind == svn.core.svn_node_file:
414 if kind == svn.core.svn_node_file:
415 if ent.copyfrom_path:
415 if ent.copyfrom_path:
416 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
416 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
417 if copyfrom_path:
417 if copyfrom_path:
418 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
418 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
419 # It's probably important for hg that the source
419 # It's probably important for hg that the source
420 # exists in the revision's parent, not just the
420 # exists in the revision's parent, not just the
421 # ent.copyfrom_rev
421 # ent.copyfrom_rev
422 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
422 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
423 if fromkind != 0:
423 if fromkind != 0:
424 copies[self.recode(entry)] = self.recode(copyfrom_path)
424 copies[self.recode(entry)] = self.recode(copyfrom_path)
425 entries.append(self.recode(entry))
425 entries.append(self.recode(entry))
426 elif kind == 0: # gone, but had better be a deleted *file*
426 elif kind == 0: # gone, but had better be a deleted *file*
427 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
427 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
428
428
429 # if a branch is created but entries are removed in the same
429 # if a branch is created but entries are removed in the same
430 # changeset, get the right fromrev
430 # changeset, get the right fromrev
431 if parents:
431 if parents:
432 uuid, old_module, fromrev = self.revsplit(parents[0])
432 uuid, old_module, fromrev = self.revsplit(parents[0])
433 else:
433 else:
434 fromrev = revnum - 1
434 fromrev = revnum - 1
435 # might always need to be revnum - 1 in these 3 lines?
435 # might always need to be revnum - 1 in these 3 lines?
436 old_module = self.modulemap.get(fromrev, self.module)
436 old_module = self.modulemap.get(fromrev, self.module)
437
437
438 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
438 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
439 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
439 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
440
440
441 def lookup_parts(p):
441 def lookup_parts(p):
442 rc = None
442 rc = None
443 parts = p.split("/")
443 parts = p.split("/")
444 for i in range(len(parts)):
444 for i in range(len(parts)):
445 part = "/".join(parts[:i])
445 part = "/".join(parts[:i])
446 info = part, copyfrom.get(part, None)
446 info = part, copyfrom.get(part, None)
447 if info[1] is not None:
447 if info[1] is not None:
448 self.ui.debug("Found parent directory %s\n" % info[1])
448 self.ui.debug("Found parent directory %s\n" % info[1])
449 rc = info
449 rc = info
450 return rc
450 return rc
451
451
452 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
452 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
453
453
454 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
454 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
455
455
456 # need to remove fragment from lookup_parts and replace with copyfrom_path
456 # need to remove fragment from lookup_parts and replace with copyfrom_path
457 if frompath is not None:
457 if frompath is not None:
458 self.ui.debug("munge-o-matic\n")
458 self.ui.debug("munge-o-matic\n")
459 self.ui.debug(entrypath + '\n')
459 self.ui.debug(entrypath + '\n')
460 self.ui.debug(entrypath[len(frompath):] + '\n')
460 self.ui.debug(entrypath[len(frompath):] + '\n')
461 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
461 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
462 fromrev = froment.copyfrom_rev
462 fromrev = froment.copyfrom_rev
463 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
463 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
464
464
465 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
465 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
466 if fromkind == svn.core.svn_node_file: # a deleted file
466 if fromkind == svn.core.svn_node_file: # a deleted file
467 entries.append(self.recode(entry))
467 entries.append(self.recode(entry))
468 elif fromkind == svn.core.svn_node_dir:
468 elif fromkind == svn.core.svn_node_dir:
469 # print "Deleted/moved non-file:", revnum, path, ent
469 # print "Deleted/moved non-file:", revnum, path, ent
470 # children = self._find_children(path, revnum - 1)
470 # children = self._find_children(path, revnum - 1)
471 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
471 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
472 # Sometimes this is tricky. For example: in
472 # Sometimes this is tricky. For example: in
473 # The Subversion Repository revision 6940 a dir
473 # The Subversion Repository revision 6940 a dir
474 # was copied and one of its files was deleted
474 # was copied and one of its files was deleted
475 # from the new location in the same commit. This
475 # from the new location in the same commit. This
476 # code can't deal with that yet.
476 # code can't deal with that yet.
477 if ent.action == 'C':
477 if ent.action == 'C':
478 children = self._find_children(path, fromrev)
478 children = self._find_children(path, fromrev)
479 else:
479 else:
480 oroot = entrypath.strip('/')
480 oroot = entrypath.strip('/')
481 nroot = path.strip('/')
481 nroot = path.strip('/')
482 children = self._find_children(oroot, fromrev)
482 children = self._find_children(oroot, fromrev)
483 children = [s.replace(oroot,nroot) for s in children]
483 children = [s.replace(oroot,nroot) for s in children]
484 # Mark all [files, not directories] as deleted.
484 # Mark all [files, not directories] as deleted.
485 for child in children:
485 for child in children:
486 # Can we move a child directory and its
486 # Can we move a child directory and its
487 # parent in the same commit? (probably can). Could
487 # parent in the same commit? (probably can). Could
488 # cause problems if instead of revnum -1,
488 # cause problems if instead of revnum -1,
489 # we have to look in (copyfrom_path, revnum - 1)
489 # we have to look in (copyfrom_path, revnum - 1)
490 entrypath = get_entry_from_path("/" + child, module=old_module)
490 entrypath = get_entry_from_path("/" + child, module=old_module)
491 if entrypath:
491 if entrypath:
492 entry = self.recode(entrypath.decode(self.encoding))
492 entry = self.recode(entrypath.decode(self.encoding))
493 if entry in copies:
493 if entry in copies:
494 # deleted file within a copy
494 # deleted file within a copy
495 del copies[entry]
495 del copies[entry]
496 else:
496 else:
497 entries.append(entry)
497 entries.append(entry)
498 else:
498 else:
499 self.ui.debug('unknown path in revision %d: %s\n' % \
499 self.ui.debug('unknown path in revision %d: %s\n' % \
500 (revnum, path))
500 (revnum, path))
501 elif kind == svn.core.svn_node_dir:
501 elif kind == svn.core.svn_node_dir:
502 # Should probably synthesize normal file entries
502 # Should probably synthesize normal file entries
503 # and handle as above to clean up copy/rename handling.
503 # and handle as above to clean up copy/rename handling.
504
504
505 # If the directory just had a prop change,
505 # If the directory just had a prop change,
506 # then we shouldn't need to look for its children.
506 # then we shouldn't need to look for its children.
507 # Also this could create duplicate entries. Not sure
507 # Also this could create duplicate entries. Not sure
508 # whether this will matter. Maybe should make entries a set.
508 # whether this will matter. Maybe should make entries a set.
509 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
509 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
510 # This will fail if a directory was copied
510 # This will fail if a directory was copied
511 # from another branch and then some of its files
511 # from another branch and then some of its files
512 # were deleted in the same transaction.
512 # were deleted in the same transaction.
513 children = self._find_children(path, revnum)
513 children = self._find_children(path, revnum)
514 children.sort()
514 children.sort()
515 for child in children:
515 for child in children:
516 # Can we move a child directory and its
516 # Can we move a child directory and its
517 # parent in the same commit? (probably can). Could
517 # parent in the same commit? (probably can). Could
518 # cause problems if instead of revnum -1,
518 # cause problems if instead of revnum -1,
519 # we have to look in (copyfrom_path, revnum - 1)
519 # we have to look in (copyfrom_path, revnum - 1)
520 entrypath = get_entry_from_path("/" + child, module=self.module)
520 entrypath = get_entry_from_path("/" + child, module=self.module)
521 # print child, self.module, entrypath
521 # print child, self.module, entrypath
522 if entrypath:
522 if entrypath:
523 # Need to filter out directories here...
523 # Need to filter out directories here...
524 kind = svn.ra.check_path(self.ra, entrypath, revnum)
524 kind = svn.ra.check_path(self.ra, entrypath, revnum)
525 if kind != svn.core.svn_node_dir:
525 if kind != svn.core.svn_node_dir:
526 entries.append(self.recode(entrypath))
526 entries.append(self.recode(entrypath))
527
527
528 # Copies here (must copy all from source)
528 # Copies here (must copy all from source)
529 # Probably not a real problem for us if
529 # Probably not a real problem for us if
530 # source does not exist
530 # source does not exist
531
531
532 # Can do this with the copy command "hg copy"
532 # Can do this with the copy command "hg copy"
533 # if ent.copyfrom_path:
533 # if ent.copyfrom_path:
534 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
534 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
535 # module=self.module)
535 # module=self.module)
536 # copyto_entry = entrypath
536 # copyto_entry = entrypath
537 #
537 #
538 # print "copy directory", copyfrom_entry, 'to', copyto_entry
538 # print "copy directory", copyfrom_entry, 'to', copyto_entry
539 #
539 #
540 # copies.append((copyfrom_entry, copyto_entry))
540 # copies.append((copyfrom_entry, copyto_entry))
541
541
542 if ent.copyfrom_path:
542 if ent.copyfrom_path:
543 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
543 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
544 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
544 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
545 if copyfrom_entry:
545 if copyfrom_entry:
546 copyfrom[path] = ent
546 copyfrom[path] = ent
547 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
547 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
548
548
549 # Good, /probably/ a regular copy. Really should check
549 # Good, /probably/ a regular copy. Really should check
550 # to see whether the parent revision actually contains
550 # to see whether the parent revision actually contains
551 # the directory in question.
551 # the directory in question.
552 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
552 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
553 children.sort()
553 children.sort()
554 for child in children:
554 for child in children:
555 entrypath = get_entry_from_path("/" + child, module=self.module)
555 entrypath = get_entry_from_path("/" + child, module=self.module)
556 if entrypath:
556 if entrypath:
557 entry = entrypath.decode(self.encoding)
557 entry = entrypath.decode(self.encoding)
558 # print "COPY COPY From", copyfrom_entry, entry
558 # print "COPY COPY From", copyfrom_entry, entry
559 copyto_path = path + entry[len(copyfrom_entry):]
559 copyto_path = path + entry[len(copyfrom_entry):]
560 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
560 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
561 # print "COPY", entry, "COPY To", copyto_entry
561 # print "COPY", entry, "COPY To", copyto_entry
562 copies[self.recode(copyto_entry)] = self.recode(entry)
562 copies[self.recode(copyto_entry)] = self.recode(entry)
563 # copy from quux splort/quuxfile
563 # copy from quux splort/quuxfile
564
564
565 return (entries, copies)
565 return (entries, copies)
566
566
567 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
567 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
568 self.child_cset = None
568 self.child_cset = None
569 def parselogentry(orig_paths, revnum, author, date, message):
569 def parselogentry(orig_paths, revnum, author, date, message):
570 self.ui.debug("parsing revision %d (%d changes)\n" %
570 self.ui.debug("parsing revision %d (%d changes)\n" %
571 (revnum, len(orig_paths)))
571 (revnum, len(orig_paths)))
572
572
573 if revnum in self.modulemap:
573 if revnum in self.modulemap:
574 new_module = self.modulemap[revnum]
574 new_module = self.modulemap[revnum]
575 if new_module != self.module:
575 if new_module != self.module:
576 self.module = new_module
576 self.module = new_module
577 self.reparent(self.module)
577 self.reparent(self.module)
578
578
579 rev = self.revid(revnum)
579 rev = self.revid(revnum)
580 # branch log might return entries for a parent we already have
580 # branch log might return entries for a parent we already have
581 if (rev in self.commits or
581 if (rev in self.commits or
582 (revnum < self.lastrevs.get(self.module, 0))):
582 (revnum < self.lastrevs.get(self.module, 0))):
583 return
583 return
584
584
585 parents = []
585 parents = []
586 # check whether this revision is the start of a branch
586 # check whether this revision is the start of a branch
587 if self.module in orig_paths:
587 if self.module in orig_paths:
588 ent = orig_paths[self.module]
588 ent = orig_paths[self.module]
589 if ent.copyfrom_path:
589 if ent.copyfrom_path:
590 # ent.copyfrom_rev may not be the actual last revision
590 # ent.copyfrom_rev may not be the actual last revision
591 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
591 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
592 self.modulemap[prev] = ent.copyfrom_path
592 self.modulemap[prev] = ent.copyfrom_path
593 parents = [self.revid(prev, ent.copyfrom_path)]
593 parents = [self.revid(prev, ent.copyfrom_path)]
594 self.ui.note('found parent of branch %s at %d: %s\n' % \
594 self.ui.note('found parent of branch %s at %d: %s\n' % \
595 (self.module, prev, ent.copyfrom_path))
595 (self.module, prev, ent.copyfrom_path))
596 else:
596 else:
597 self.ui.debug("No copyfrom path, don't know what to do.\n")
597 self.ui.debug("No copyfrom path, don't know what to do.\n")
598
598
599 self.modulemap[revnum] = self.module # track backwards in time
599 self.modulemap[revnum] = self.module # track backwards in time
600
600
601 orig_paths = orig_paths.items()
601 orig_paths = orig_paths.items()
602 orig_paths.sort()
602 orig_paths.sort()
603 paths = []
603 paths = []
604 # filter out unrelated paths
604 # filter out unrelated paths
605 for path, ent in orig_paths:
605 for path, ent in orig_paths:
606 if not path.startswith(self.module):
606 if not path.startswith(self.module):
607 self.ui.debug("boring@%s: %s\n" % (revnum, path))
607 self.ui.debug("boring@%s: %s\n" % (revnum, path))
608 continue
608 continue
609 paths.append((path, ent))
609 paths.append((path, ent))
610
610
611 self.paths[rev] = (paths, parents)
611 self.paths[rev] = (paths, parents)
612
612
613 # Example SVN datetime. Includes microseconds.
613 # Example SVN datetime. Includes microseconds.
614 # ISO-8601 conformant
614 # ISO-8601 conformant
615 # '2007-01-04T17:35:00.902377Z'
615 # '2007-01-04T17:35:00.902377Z'
616 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
616 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
617
617
618 log = message and self.recode(message)
618 log = message and self.recode(message)
619 author = author and self.recode(author) or ''
619 author = author and self.recode(author) or ''
620 try:
620 try:
621 branch = self.module.split("/")[-1]
621 branch = self.module.split("/")[-1]
622 if branch == 'trunk':
622 if branch == 'trunk':
623 branch = ''
623 branch = ''
624 except IndexError:
624 except IndexError:
625 branch = None
625 branch = None
626
626
627 cset = commit(author=author,
627 cset = commit(author=author,
628 date=util.datestr(date),
628 date=util.datestr(date),
629 desc=log,
629 desc=log,
630 parents=parents,
630 parents=parents,
631 branch=branch,
631 branch=branch,
632 rev=rev.encode('utf-8'))
632 rev=rev.encode('utf-8'))
633
633
634 self.commits[rev] = cset
634 self.commits[rev] = cset
635 if self.child_cset and not self.child_cset.parents:
635 if self.child_cset and not self.child_cset.parents:
636 self.child_cset.parents = [rev]
636 self.child_cset.parents = [rev]
637 self.child_cset = cset
637 self.child_cset = cset
638
638
639 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
639 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
640 (self.module, from_revnum, to_revnum))
640 (self.module, from_revnum, to_revnum))
641
641
642 try:
642 try:
643 for entry in self.get_log([self.module], from_revnum, to_revnum):
643 for entry in self.get_log([self.module], from_revnum, to_revnum):
644 orig_paths, revnum, author, date, message = entry
644 orig_paths, revnum, author, date, message = entry
645 if self.is_blacklisted(revnum):
645 if self.is_blacklisted(revnum):
646 self.ui.note('skipping blacklisted revision %d\n' % revnum)
646 self.ui.note('skipping blacklisted revision %d\n' % revnum)
647 continue
647 continue
648 if orig_paths is None:
648 if orig_paths is None:
649 self.ui.debug('revision %d has no entries\n' % revnum)
649 self.ui.debug('revision %d has no entries\n' % revnum)
650 continue
650 continue
651 parselogentry(orig_paths, revnum, author, date, message)
651 parselogentry(orig_paths, revnum, author, date, message)
652 except SubversionException, (inst, num):
652 except SubversionException, (inst, num):
653 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
653 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
654 raise NoSuchRevision(branch=self,
654 raise NoSuchRevision(branch=self,
655 revision="Revision number %d" % to_revnum)
655 revision="Revision number %d" % to_revnum)
656 raise
656 raise
657
657
658 def _getfile(self, file, rev):
658 def _getfile(self, file, rev):
659 io = StringIO()
659 io = StringIO()
660 # TODO: ra.get_file transmits the whole file instead of diffs.
660 # TODO: ra.get_file transmits the whole file instead of diffs.
661 mode = ''
661 mode = ''
662 try:
662 try:
663 revnum = self.revnum(rev)
663 revnum = self.revnum(rev)
664 if self.module != self.modulemap[revnum]:
664 if self.module != self.modulemap[revnum]:
665 self.module = self.modulemap[revnum]
665 self.module = self.modulemap[revnum]
666 self.reparent(self.module)
666 self.reparent(self.module)
667 info = svn.ra.get_file(self.ra, file, revnum, io)
667 info = svn.ra.get_file(self.ra, file, revnum, io)
668 if isinstance(info, list):
668 if isinstance(info, list):
669 info = info[-1]
669 info = info[-1]
670 mode = ("svn:executable" in info) and 'x' or ''
670 mode = ("svn:executable" in info) and 'x' or ''
671 mode = ("svn:special" in info) and 'l' or mode
671 mode = ("svn:special" in info) and 'l' or mode
672 except SubversionException, e:
672 except SubversionException, e:
673 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
673 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
674 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
674 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
675 if e.apr_err in notfound: # File not found
675 if e.apr_err in notfound: # File not found
676 raise IOError()
676 raise IOError()
677 raise
677 raise
678 data = io.getvalue()
678 data = io.getvalue()
679 if mode == 'l':
679 if mode == 'l':
680 link_prefix = "link "
680 link_prefix = "link "
681 if data.startswith(link_prefix):
681 if data.startswith(link_prefix):
682 data = data[len(link_prefix):]
682 data = data[len(link_prefix):]
683 return data, mode
683 return data, mode
684
684
685 def _find_children(self, path, revnum):
685 def _find_children(self, path, revnum):
686 path = path.strip('/')
686 path = path.strip('/')
687 pool = Pool()
687 pool = Pool()
688 rpath = '/'.join([self.base, path]).strip('/')
688 rpath = '/'.join([self.base, path]).strip('/')
689 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
689 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
690
690
691 pre_revprop_change = '''#!/bin/sh
691 pre_revprop_change = '''#!/bin/sh
692
692
693 REPOS="$1"
693 REPOS="$1"
694 REV="$2"
694 REV="$2"
695 USER="$3"
695 USER="$3"
696 PROPNAME="$4"
696 PROPNAME="$4"
697 ACTION="$5"
697 ACTION="$5"
698
698
699 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
699 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
700 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
700 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
701 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
701 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
702
702
703 echo "Changing prohibited revision property" >&2
703 echo "Changing prohibited revision property" >&2
704 exit 1
704 exit 1
705 '''
705 '''
706
706
707 class svn_sink(converter_sink, commandline):
707 class svn_sink(converter_sink, commandline):
708 commit_re = re.compile(r'Committed revision (\d+).', re.M)
708 commit_re = re.compile(r'Committed revision (\d+).', re.M)
709
709
710 # iterates sublist of given list for concatenated length is within limit
711 def limit_arglist(self, files):
712 if os.name != 'nt':
713 yield files
714 return
715 # When I tested on WinXP, limit = 2500 is NG, 2400 is OK
716 limit = 2000
717 bytes = 0
718 fl = []
719 for fn in files:
720 b = len(fn) + 1
721 if bytes + b < limit:
722 fl.append(fn)
723 bytes += b
724 else:
725 yield fl
726 fl = [fn]
727 bytes = b
728 if fl:
729 yield fl
730
731 def prerun(self):
710 def prerun(self):
732 if self.wc:
711 if self.wc:
733 os.chdir(self.wc)
712 os.chdir(self.wc)
734
713
735 def postrun(self):
714 def postrun(self):
736 if self.wc:
715 if self.wc:
737 os.chdir(self.cwd)
716 os.chdir(self.cwd)
738
717
739 def join(self, name):
718 def join(self, name):
740 return os.path.join(self.wc, '.svn', name)
719 return os.path.join(self.wc, '.svn', name)
741
720
742 def revmapfile(self):
721 def revmapfile(self):
743 return self.join('hg-shamap')
722 return self.join('hg-shamap')
744
723
745 def authorfile(self):
724 def authorfile(self):
746 return self.join('hg-authormap')
725 return self.join('hg-authormap')
747
726
748 def __init__(self, ui, path):
727 def __init__(self, ui, path):
749 converter_sink.__init__(self, ui, path)
728 converter_sink.__init__(self, ui, path)
750 commandline.__init__(self, ui, 'svn')
729 commandline.__init__(self, ui, 'svn')
751 self.delete = []
730 self.delete = []
752 self.setexec = []
731 self.setexec = []
753 self.delexec = []
732 self.delexec = []
754 self.copies = []
733 self.copies = []
755 self.wc = None
734 self.wc = None
756 self.cwd = os.getcwd()
735 self.cwd = os.getcwd()
757
736
758 path = os.path.realpath(path)
737 path = os.path.realpath(path)
759
738
760 created = False
739 created = False
761 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
740 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
762 self.wc = path
741 self.wc = path
763 self.run0('update')
742 self.run0('update')
764 else:
743 else:
765 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
744 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
766
745
767 if os.path.isdir(os.path.dirname(path)):
746 if os.path.isdir(os.path.dirname(path)):
768 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
747 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
769 ui.status(_('initializing svn repo %r\n') %
748 ui.status(_('initializing svn repo %r\n') %
770 os.path.basename(path))
749 os.path.basename(path))
771 commandline(ui, 'svnadmin').run0('create', path)
750 commandline(ui, 'svnadmin').run0('create', path)
772 created = path
751 created = path
773 path = path.replace('\\', '/')
752 path = path.replace('\\', '/')
774 if not path.startswith('/'):
753 if not path.startswith('/'):
775 path = '/' + path
754 path = '/' + path
776 path = 'file://' + path
755 path = 'file://' + path
777
756
778 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
757 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
779 self.run0('checkout', path, wcpath)
758 self.run0('checkout', path, wcpath)
780
759
781 self.wc = wcpath
760 self.wc = wcpath
782 self.opener = util.opener(self.wc)
761 self.opener = util.opener(self.wc)
783 self.wopener = util.opener(self.wc)
762 self.wopener = util.opener(self.wc)
784 self.childmap = mapfile(ui, self.join('hg-childmap'))
763 self.childmap = mapfile(ui, self.join('hg-childmap'))
785 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
764 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
786
765
787 if created:
766 if created:
788 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
767 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
789 fp = open(hook, 'w')
768 fp = open(hook, 'w')
790 fp.write(pre_revprop_change)
769 fp.write(pre_revprop_change)
791 fp.close()
770 fp.close()
792 util.set_flags(hook, "x")
771 util.set_flags(hook, "x")
793
772
794 xport = transport.SvnRaTransport(url=geturl(path))
773 xport = transport.SvnRaTransport(url=geturl(path))
795 self.uuid = svn.ra.get_uuid(xport.ra)
774 self.uuid = svn.ra.get_uuid(xport.ra)
796
775
797 def wjoin(self, *names):
776 def wjoin(self, *names):
798 return os.path.join(self.wc, *names)
777 return os.path.join(self.wc, *names)
799
778
800 def putfile(self, filename, flags, data):
779 def putfile(self, filename, flags, data):
801 if 'l' in flags:
780 if 'l' in flags:
802 self.wopener.symlink(data, filename)
781 self.wopener.symlink(data, filename)
803 else:
782 else:
804 try:
783 try:
805 if os.path.islink(self.wjoin(filename)):
784 if os.path.islink(self.wjoin(filename)):
806 os.unlink(filename)
785 os.unlink(filename)
807 except OSError:
786 except OSError:
808 pass
787 pass
809 self.wopener(filename, 'w').write(data)
788 self.wopener(filename, 'w').write(data)
810
789
811 if self.is_exec:
790 if self.is_exec:
812 was_exec = self.is_exec(self.wjoin(filename))
791 was_exec = self.is_exec(self.wjoin(filename))
813 else:
792 else:
814 # On filesystems not supporting execute-bit, there is no way
793 # On filesystems not supporting execute-bit, there is no way
815 # to know if it is set but asking subversion. Setting it
794 # to know if it is set but asking subversion. Setting it
816 # systematically is just as expensive and much simpler.
795 # systematically is just as expensive and much simpler.
817 was_exec = 'x' not in flags
796 was_exec = 'x' not in flags
818
797
819 util.set_flags(self.wjoin(filename), flags)
798 util.set_flags(self.wjoin(filename), flags)
820 if was_exec:
799 if was_exec:
821 if 'x' not in flags:
800 if 'x' not in flags:
822 self.delexec.append(filename)
801 self.delexec.append(filename)
823 else:
802 else:
824 if 'x' in flags:
803 if 'x' in flags:
825 self.setexec.append(filename)
804 self.setexec.append(filename)
826
805
827 def delfile(self, name):
806 def delfile(self, name):
828 self.delete.append(name)
807 self.delete.append(name)
829
808
830 def copyfile(self, source, dest):
809 def copyfile(self, source, dest):
831 self.copies.append([source, dest])
810 self.copies.append([source, dest])
832
811
833 def _copyfile(self, source, dest):
812 def _copyfile(self, source, dest):
834 # SVN's copy command pukes if the destination file exists, but
813 # SVN's copy command pukes if the destination file exists, but
835 # our copyfile method expects to record a copy that has
814 # our copyfile method expects to record a copy that has
836 # already occurred. Cross the semantic gap.
815 # already occurred. Cross the semantic gap.
837 wdest = self.wjoin(dest)
816 wdest = self.wjoin(dest)
838 exists = os.path.exists(wdest)
817 exists = os.path.exists(wdest)
839 if exists:
818 if exists:
840 fd, tempname = tempfile.mkstemp(
819 fd, tempname = tempfile.mkstemp(
841 prefix='hg-copy-', dir=os.path.dirname(wdest))
820 prefix='hg-copy-', dir=os.path.dirname(wdest))
842 os.close(fd)
821 os.close(fd)
843 os.unlink(tempname)
822 os.unlink(tempname)
844 os.rename(wdest, tempname)
823 os.rename(wdest, tempname)
845 try:
824 try:
846 self.run0('copy', source, dest)
825 self.run0('copy', source, dest)
847 finally:
826 finally:
848 if exists:
827 if exists:
849 try:
828 try:
850 os.unlink(wdest)
829 os.unlink(wdest)
851 except OSError:
830 except OSError:
852 pass
831 pass
853 os.rename(tempname, wdest)
832 os.rename(tempname, wdest)
854
833
855 def dirs_of(self, files):
834 def dirs_of(self, files):
856 dirs = set()
835 dirs = set()
857 for f in files:
836 for f in files:
858 if os.path.isdir(self.wjoin(f)):
837 if os.path.isdir(self.wjoin(f)):
859 dirs.add(f)
838 dirs.add(f)
860 for i in strutil.rfindall(f, '/'):
839 for i in strutil.rfindall(f, '/'):
861 dirs.add(f[:i])
840 dirs.add(f[:i])
862 return dirs
841 return dirs
863
842
864 def add_dirs(self, files):
843 def add_dirs(self, files):
865 add_dirs = [d for d in self.dirs_of(files)
844 add_dirs = [d for d in self.dirs_of(files)
866 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
845 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
867 if add_dirs:
846 if add_dirs:
868 add_dirs.sort()
847 add_dirs.sort()
869 for fl in self.limit_arglist(add_dirs):
848 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
870 self.run('add', non_recursive=True, quiet=True, *fl)
871 return add_dirs
849 return add_dirs
872
850
873 def add_files(self, files):
851 def add_files(self, files):
874 if files:
852 if files:
875 for fl in self.limit_arglist(files):
853 self.xargs(files, 'add', quiet=True)
876 self.run('add', quiet=True, *fl)
877 return files
854 return files
878
855
879 def tidy_dirs(self, names):
856 def tidy_dirs(self, names):
880 dirs = list(self.dirs_of(names))
857 dirs = list(self.dirs_of(names))
881 dirs.sort(reverse=True)
858 dirs.sort(reverse=True)
882 deleted = []
859 deleted = []
883 for d in dirs:
860 for d in dirs:
884 wd = self.wjoin(d)
861 wd = self.wjoin(d)
885 if os.listdir(wd) == '.svn':
862 if os.listdir(wd) == '.svn':
886 self.run0('delete', d)
863 self.run0('delete', d)
887 deleted.append(d)
864 deleted.append(d)
888 return deleted
865 return deleted
889
866
890 def addchild(self, parent, child):
867 def addchild(self, parent, child):
891 self.childmap[parent] = child
868 self.childmap[parent] = child
892
869
893 def revid(self, rev):
870 def revid(self, rev):
894 return u"svn:%s@%s" % (self.uuid, rev)
871 return u"svn:%s@%s" % (self.uuid, rev)
895
872
896 def putcommit(self, files, parents, commit):
873 def putcommit(self, files, parents, commit):
897 for parent in parents:
874 for parent in parents:
898 try:
875 try:
899 return self.revid(self.childmap[parent])
876 return self.revid(self.childmap[parent])
900 except KeyError:
877 except KeyError:
901 pass
878 pass
902 entries = set(self.delete)
879 entries = set(self.delete)
903 files = util.frozenset(files)
880 files = util.frozenset(files)
904 entries.update(self.add_dirs(files.difference(entries)))
881 entries.update(self.add_dirs(files.difference(entries)))
905 if self.copies:
882 if self.copies:
906 for s, d in self.copies:
883 for s, d in self.copies:
907 self._copyfile(s, d)
884 self._copyfile(s, d)
908 self.copies = []
885 self.copies = []
909 if self.delete:
886 if self.delete:
910 for fl in self.limit_arglist(self.delete):
887 self.xargs(self.delete, 'delete')
911 self.run0('delete', *fl)
912 self.delete = []
888 self.delete = []
913 entries.update(self.add_files(files.difference(entries)))
889 entries.update(self.add_files(files.difference(entries)))
914 entries.update(self.tidy_dirs(entries))
890 entries.update(self.tidy_dirs(entries))
915 if self.delexec:
891 if self.delexec:
916 for fl in self.limit_arglist(self.delexec):
892 self.xargs(self.delexec, 'propdel', 'svn:executable')
917 self.run0('propdel', 'svn:executable', *fl)
918 self.delexec = []
893 self.delexec = []
919 if self.setexec:
894 if self.setexec:
920 for fl in self.limit_arglist(self.setexec):
895 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
921 self.run0('propset', 'svn:executable', '*', *fl)
922 self.setexec = []
896 self.setexec = []
923
897
924 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
898 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
925 fp = os.fdopen(fd, 'w')
899 fp = os.fdopen(fd, 'w')
926 fp.write(commit.desc)
900 fp.write(commit.desc)
927 fp.close()
901 fp.close()
928 try:
902 try:
929 output = self.run0('commit',
903 output = self.run0('commit',
930 username=util.shortuser(commit.author),
904 username=util.shortuser(commit.author),
931 file=messagefile,
905 file=messagefile,
932 encoding='utf-8')
906 encoding='utf-8')
933 try:
907 try:
934 rev = self.commit_re.search(output).group(1)
908 rev = self.commit_re.search(output).group(1)
935 except AttributeError:
909 except AttributeError:
936 self.ui.warn(_('unexpected svn output:\n'))
910 self.ui.warn(_('unexpected svn output:\n'))
937 self.ui.warn(output)
911 self.ui.warn(output)
938 raise util.Abort(_('unable to cope with svn output'))
912 raise util.Abort(_('unable to cope with svn output'))
939 if commit.rev:
913 if commit.rev:
940 self.run('propset', 'hg:convert-rev', commit.rev,
914 self.run('propset', 'hg:convert-rev', commit.rev,
941 revprop=True, revision=rev)
915 revprop=True, revision=rev)
942 if commit.branch and commit.branch != 'default':
916 if commit.branch and commit.branch != 'default':
943 self.run('propset', 'hg:convert-branch', commit.branch,
917 self.run('propset', 'hg:convert-branch', commit.branch,
944 revprop=True, revision=rev)
918 revprop=True, revision=rev)
945 for parent in parents:
919 for parent in parents:
946 self.addchild(parent, rev)
920 self.addchild(parent, rev)
947 return self.revid(rev)
921 return self.revid(rev)
948 finally:
922 finally:
949 os.unlink(messagefile)
923 os.unlink(messagefile)
950
924
951 def puttags(self, tags):
925 def puttags(self, tags):
952 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
926 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now