##// END OF EJS Templates
convert: add support to common commandline to access stdin of the process
Daniel Atallah -
r13759:49b818fd default
parent child Browse files
Show More
@@ -1,403 +1,411 b''
1 # common.py - common code for the convert extension
1 # common.py - common code for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import base64, errno
8 import base64, errno
9 import os
9 import os
10 import cPickle as pickle
10 import cPickle as pickle
11 from mercurial import util
11 from mercurial import util
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13
13
14 def encodeargs(args):
14 def encodeargs(args):
15 def encodearg(s):
15 def encodearg(s):
16 lines = base64.encodestring(s)
16 lines = base64.encodestring(s)
17 lines = [l.splitlines()[0] for l in lines]
17 lines = [l.splitlines()[0] for l in lines]
18 return ''.join(lines)
18 return ''.join(lines)
19
19
20 s = pickle.dumps(args)
20 s = pickle.dumps(args)
21 return encodearg(s)
21 return encodearg(s)
22
22
23 def decodeargs(s):
23 def decodeargs(s):
24 s = base64.decodestring(s)
24 s = base64.decodestring(s)
25 return pickle.loads(s)
25 return pickle.loads(s)
26
26
27 class MissingTool(Exception):
27 class MissingTool(Exception):
28 pass
28 pass
29
29
30 def checktool(exe, name=None, abort=True):
30 def checktool(exe, name=None, abort=True):
31 name = name or exe
31 name = name or exe
32 if not util.find_exe(exe):
32 if not util.find_exe(exe):
33 exc = abort and util.Abort or MissingTool
33 exc = abort and util.Abort or MissingTool
34 raise exc(_('cannot find required "%s" tool') % name)
34 raise exc(_('cannot find required "%s" tool') % name)
35
35
36 class NoRepo(Exception):
36 class NoRepo(Exception):
37 pass
37 pass
38
38
39 SKIPREV = 'SKIP'
39 SKIPREV = 'SKIP'
40
40
41 class commit(object):
41 class commit(object):
42 def __init__(self, author, date, desc, parents, branch=None, rev=None,
42 def __init__(self, author, date, desc, parents, branch=None, rev=None,
43 extra={}, sortkey=None):
43 extra={}, sortkey=None):
44 self.author = author or 'unknown'
44 self.author = author or 'unknown'
45 self.date = date or '0 0'
45 self.date = date or '0 0'
46 self.desc = desc
46 self.desc = desc
47 self.parents = parents
47 self.parents = parents
48 self.branch = branch
48 self.branch = branch
49 self.rev = rev
49 self.rev = rev
50 self.extra = extra
50 self.extra = extra
51 self.sortkey = sortkey
51 self.sortkey = sortkey
52
52
53 class converter_source(object):
53 class converter_source(object):
54 """Conversion source interface"""
54 """Conversion source interface"""
55
55
56 def __init__(self, ui, path=None, rev=None):
56 def __init__(self, ui, path=None, rev=None):
57 """Initialize conversion source (or raise NoRepo("message")
57 """Initialize conversion source (or raise NoRepo("message")
58 exception if path is not a valid repository)"""
58 exception if path is not a valid repository)"""
59 self.ui = ui
59 self.ui = ui
60 self.path = path
60 self.path = path
61 self.rev = rev
61 self.rev = rev
62
62
63 self.encoding = 'utf-8'
63 self.encoding = 'utf-8'
64
64
65 def before(self):
65 def before(self):
66 pass
66 pass
67
67
68 def after(self):
68 def after(self):
69 pass
69 pass
70
70
71 def setrevmap(self, revmap):
71 def setrevmap(self, revmap):
72 """set the map of already-converted revisions"""
72 """set the map of already-converted revisions"""
73 pass
73 pass
74
74
75 def getheads(self):
75 def getheads(self):
76 """Return a list of this repository's heads"""
76 """Return a list of this repository's heads"""
77 raise NotImplementedError()
77 raise NotImplementedError()
78
78
79 def getfile(self, name, rev):
79 def getfile(self, name, rev):
80 """Return a pair (data, mode) where data is the file content
80 """Return a pair (data, mode) where data is the file content
81 as a string and mode one of '', 'x' or 'l'. rev is the
81 as a string and mode one of '', 'x' or 'l'. rev is the
82 identifier returned by a previous call to getchanges(). Raise
82 identifier returned by a previous call to getchanges(). Raise
83 IOError to indicate that name was deleted in rev.
83 IOError to indicate that name was deleted in rev.
84 """
84 """
85 raise NotImplementedError()
85 raise NotImplementedError()
86
86
87 def getchanges(self, version):
87 def getchanges(self, version):
88 """Returns a tuple of (files, copies).
88 """Returns a tuple of (files, copies).
89
89
90 files is a sorted list of (filename, id) tuples for all files
90 files is a sorted list of (filename, id) tuples for all files
91 changed between version and its first parent returned by
91 changed between version and its first parent returned by
92 getcommit(). id is the source revision id of the file.
92 getcommit(). id is the source revision id of the file.
93
93
94 copies is a dictionary of dest: source
94 copies is a dictionary of dest: source
95 """
95 """
96 raise NotImplementedError()
96 raise NotImplementedError()
97
97
98 def getcommit(self, version):
98 def getcommit(self, version):
99 """Return the commit object for version"""
99 """Return the commit object for version"""
100 raise NotImplementedError()
100 raise NotImplementedError()
101
101
102 def gettags(self):
102 def gettags(self):
103 """Return the tags as a dictionary of name: revision
103 """Return the tags as a dictionary of name: revision
104
104
105 Tag names must be UTF-8 strings.
105 Tag names must be UTF-8 strings.
106 """
106 """
107 raise NotImplementedError()
107 raise NotImplementedError()
108
108
109 def recode(self, s, encoding=None):
109 def recode(self, s, encoding=None):
110 if not encoding:
110 if not encoding:
111 encoding = self.encoding or 'utf-8'
111 encoding = self.encoding or 'utf-8'
112
112
113 if isinstance(s, unicode):
113 if isinstance(s, unicode):
114 return s.encode("utf-8")
114 return s.encode("utf-8")
115 try:
115 try:
116 return s.decode(encoding).encode("utf-8")
116 return s.decode(encoding).encode("utf-8")
117 except:
117 except:
118 try:
118 try:
119 return s.decode("latin-1").encode("utf-8")
119 return s.decode("latin-1").encode("utf-8")
120 except:
120 except:
121 return s.decode(encoding, "replace").encode("utf-8")
121 return s.decode(encoding, "replace").encode("utf-8")
122
122
123 def getchangedfiles(self, rev, i):
123 def getchangedfiles(self, rev, i):
124 """Return the files changed by rev compared to parent[i].
124 """Return the files changed by rev compared to parent[i].
125
125
126 i is an index selecting one of the parents of rev. The return
126 i is an index selecting one of the parents of rev. The return
127 value should be the list of files that are different in rev and
127 value should be the list of files that are different in rev and
128 this parent.
128 this parent.
129
129
130 If rev has no parents, i is None.
130 If rev has no parents, i is None.
131
131
132 This function is only needed to support --filemap
132 This function is only needed to support --filemap
133 """
133 """
134 raise NotImplementedError()
134 raise NotImplementedError()
135
135
136 def converted(self, rev, sinkrev):
136 def converted(self, rev, sinkrev):
137 '''Notify the source that a revision has been converted.'''
137 '''Notify the source that a revision has been converted.'''
138 pass
138 pass
139
139
140 def hasnativeorder(self):
140 def hasnativeorder(self):
141 """Return true if this source has a meaningful, native revision
141 """Return true if this source has a meaningful, native revision
142 order. For instance, Mercurial revisions are store sequentially
142 order. For instance, Mercurial revisions are store sequentially
143 while there is no such global ordering with Darcs.
143 while there is no such global ordering with Darcs.
144 """
144 """
145 return False
145 return False
146
146
147 def lookuprev(self, rev):
147 def lookuprev(self, rev):
148 """If rev is a meaningful revision reference in source, return
148 """If rev is a meaningful revision reference in source, return
149 the referenced identifier in the same format used by getcommit().
149 the referenced identifier in the same format used by getcommit().
150 return None otherwise.
150 return None otherwise.
151 """
151 """
152 return None
152 return None
153
153
154 def getbookmarks(self):
154 def getbookmarks(self):
155 """Return the bookmarks as a dictionary of name: revision
155 """Return the bookmarks as a dictionary of name: revision
156
156
157 Bookmark names are to be UTF-8 strings.
157 Bookmark names are to be UTF-8 strings.
158 """
158 """
159 return {}
159 return {}
160
160
161 class converter_sink(object):
161 class converter_sink(object):
162 """Conversion sink (target) interface"""
162 """Conversion sink (target) interface"""
163
163
164 def __init__(self, ui, path):
164 def __init__(self, ui, path):
165 """Initialize conversion sink (or raise NoRepo("message")
165 """Initialize conversion sink (or raise NoRepo("message")
166 exception if path is not a valid repository)
166 exception if path is not a valid repository)
167
167
168 created is a list of paths to remove if a fatal error occurs
168 created is a list of paths to remove if a fatal error occurs
169 later"""
169 later"""
170 self.ui = ui
170 self.ui = ui
171 self.path = path
171 self.path = path
172 self.created = []
172 self.created = []
173
173
174 def getheads(self):
174 def getheads(self):
175 """Return a list of this repository's heads"""
175 """Return a list of this repository's heads"""
176 raise NotImplementedError()
176 raise NotImplementedError()
177
177
178 def revmapfile(self):
178 def revmapfile(self):
179 """Path to a file that will contain lines
179 """Path to a file that will contain lines
180 source_rev_id sink_rev_id
180 source_rev_id sink_rev_id
181 mapping equivalent revision identifiers for each system."""
181 mapping equivalent revision identifiers for each system."""
182 raise NotImplementedError()
182 raise NotImplementedError()
183
183
184 def authorfile(self):
184 def authorfile(self):
185 """Path to a file that will contain lines
185 """Path to a file that will contain lines
186 srcauthor=dstauthor
186 srcauthor=dstauthor
187 mapping equivalent authors identifiers for each system."""
187 mapping equivalent authors identifiers for each system."""
188 return None
188 return None
189
189
190 def putcommit(self, files, copies, parents, commit, source, revmap):
190 def putcommit(self, files, copies, parents, commit, source, revmap):
191 """Create a revision with all changed files listed in 'files'
191 """Create a revision with all changed files listed in 'files'
192 and having listed parents. 'commit' is a commit object
192 and having listed parents. 'commit' is a commit object
193 containing at a minimum the author, date, and message for this
193 containing at a minimum the author, date, and message for this
194 changeset. 'files' is a list of (path, version) tuples,
194 changeset. 'files' is a list of (path, version) tuples,
195 'copies' is a dictionary mapping destinations to sources,
195 'copies' is a dictionary mapping destinations to sources,
196 'source' is the source repository, and 'revmap' is a mapfile
196 'source' is the source repository, and 'revmap' is a mapfile
197 of source revisions to converted revisions. Only getfile() and
197 of source revisions to converted revisions. Only getfile() and
198 lookuprev() should be called on 'source'.
198 lookuprev() should be called on 'source'.
199
199
200 Note that the sink repository is not told to update itself to
200 Note that the sink repository is not told to update itself to
201 a particular revision (or even what that revision would be)
201 a particular revision (or even what that revision would be)
202 before it receives the file data.
202 before it receives the file data.
203 """
203 """
204 raise NotImplementedError()
204 raise NotImplementedError()
205
205
206 def puttags(self, tags):
206 def puttags(self, tags):
207 """Put tags into sink.
207 """Put tags into sink.
208
208
209 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
209 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
210 Return a pair (tag_revision, tag_parent_revision), or (None, None)
210 Return a pair (tag_revision, tag_parent_revision), or (None, None)
211 if nothing was changed.
211 if nothing was changed.
212 """
212 """
213 raise NotImplementedError()
213 raise NotImplementedError()
214
214
215 def setbranch(self, branch, pbranches):
215 def setbranch(self, branch, pbranches):
216 """Set the current branch name. Called before the first putcommit
216 """Set the current branch name. Called before the first putcommit
217 on the branch.
217 on the branch.
218 branch: branch name for subsequent commits
218 branch: branch name for subsequent commits
219 pbranches: (converted parent revision, parent branch) tuples"""
219 pbranches: (converted parent revision, parent branch) tuples"""
220 pass
220 pass
221
221
222 def setfilemapmode(self, active):
222 def setfilemapmode(self, active):
223 """Tell the destination that we're using a filemap
223 """Tell the destination that we're using a filemap
224
224
225 Some converter_sources (svn in particular) can claim that a file
225 Some converter_sources (svn in particular) can claim that a file
226 was changed in a revision, even if there was no change. This method
226 was changed in a revision, even if there was no change. This method
227 tells the destination that we're using a filemap and that it should
227 tells the destination that we're using a filemap and that it should
228 filter empty revisions.
228 filter empty revisions.
229 """
229 """
230 pass
230 pass
231
231
232 def before(self):
232 def before(self):
233 pass
233 pass
234
234
235 def after(self):
235 def after(self):
236 pass
236 pass
237
237
238 def putbookmarks(self, bookmarks):
238 def putbookmarks(self, bookmarks):
239 """Put bookmarks into sink.
239 """Put bookmarks into sink.
240
240
241 bookmarks: {bookmarkname: sink_rev_id, ...}
241 bookmarks: {bookmarkname: sink_rev_id, ...}
242 where bookmarkname is an UTF-8 string.
242 where bookmarkname is an UTF-8 string.
243 """
243 """
244 pass
244 pass
245
245
246 class commandline(object):
246 class commandline(object):
247 def __init__(self, ui, command):
247 def __init__(self, ui, command):
248 self.ui = ui
248 self.ui = ui
249 self.command = command
249 self.command = command
250
250
251 def prerun(self):
251 def prerun(self):
252 pass
252 pass
253
253
254 def postrun(self):
254 def postrun(self):
255 pass
255 pass
256
256
257 def _cmdline(self, cmd, *args, **kwargs):
257 def _cmdline(self, cmd, closestdin, *args, **kwargs):
258 cmdline = [self.command, cmd] + list(args)
258 cmdline = [self.command, cmd] + list(args)
259 for k, v in kwargs.iteritems():
259 for k, v in kwargs.iteritems():
260 if len(k) == 1:
260 if len(k) == 1:
261 cmdline.append('-' + k)
261 cmdline.append('-' + k)
262 else:
262 else:
263 cmdline.append('--' + k.replace('_', '-'))
263 cmdline.append('--' + k.replace('_', '-'))
264 try:
264 try:
265 if len(k) == 1:
265 if len(k) == 1:
266 cmdline.append('' + v)
266 cmdline.append('' + v)
267 else:
267 else:
268 cmdline[-1] += '=' + v
268 cmdline[-1] += '=' + v
269 except TypeError:
269 except TypeError:
270 pass
270 pass
271 cmdline = [util.shellquote(arg) for arg in cmdline]
271 cmdline = [util.shellquote(arg) for arg in cmdline]
272 if not self.ui.debugflag:
272 if not self.ui.debugflag:
273 cmdline += ['2>', util.nulldev]
273 cmdline += ['2>', util.nulldev]
274 cmdline += ['<', util.nulldev]
274 if closestdin:
275 cmdline += ['<', util.nulldev]
275 cmdline = ' '.join(cmdline)
276 cmdline = ' '.join(cmdline)
276 return cmdline
277 return cmdline
277
278
278 def _run(self, cmd, *args, **kwargs):
279 def _run(self, cmd, *args, **kwargs):
279 cmdline = self._cmdline(cmd, *args, **kwargs)
280 return self._dorun(util.popen, cmd, True, *args, **kwargs)
281
282 def _run2(self, cmd, *args, **kwargs):
283 return self._dorun(util.popen2, cmd, False, *args, **kwargs)
284
285 def _dorun(self, openfunc, cmd, closestdin, *args, **kwargs):
286 cmdline = self._cmdline(cmd, closestdin, *args, **kwargs)
280 self.ui.debug('running: %s\n' % (cmdline,))
287 self.ui.debug('running: %s\n' % (cmdline,))
281 self.prerun()
288 self.prerun()
282 try:
289 try:
283 return util.popen(cmdline)
290 return openfunc(cmdline)
284 finally:
291 finally:
285 self.postrun()
292 self.postrun()
286
293
287 def run(self, cmd, *args, **kwargs):
294 def run(self, cmd, *args, **kwargs):
288 fp = self._run(cmd, *args, **kwargs)
295 fp = self._run(cmd, *args, **kwargs)
289 output = fp.read()
296 output = fp.read()
290 self.ui.debug(output)
297 self.ui.debug(output)
291 return output, fp.close()
298 return output, fp.close()
292
299
293 def runlines(self, cmd, *args, **kwargs):
300 def runlines(self, cmd, *args, **kwargs):
294 fp = self._run(cmd, *args, **kwargs)
301 fp = self._run(cmd, *args, **kwargs)
295 output = fp.readlines()
302 output = fp.readlines()
296 self.ui.debug(''.join(output))
303 self.ui.debug(''.join(output))
297 return output, fp.close()
304 return output, fp.close()
298
305
299 def checkexit(self, status, output=''):
306 def checkexit(self, status, output=''):
300 if status:
307 if status:
301 if output:
308 if output:
302 self.ui.warn(_('%s error:\n') % self.command)
309 self.ui.warn(_('%s error:\n') % self.command)
303 self.ui.warn(output)
310 self.ui.warn(output)
304 msg = util.explain_exit(status)[0]
311 msg = util.explain_exit(status)[0]
305 raise util.Abort('%s %s' % (self.command, msg))
312 raise util.Abort('%s %s' % (self.command, msg))
306
313
307 def run0(self, cmd, *args, **kwargs):
314 def run0(self, cmd, *args, **kwargs):
308 output, status = self.run(cmd, *args, **kwargs)
315 output, status = self.run(cmd, *args, **kwargs)
309 self.checkexit(status, output)
316 self.checkexit(status, output)
310 return output
317 return output
311
318
312 def runlines0(self, cmd, *args, **kwargs):
319 def runlines0(self, cmd, *args, **kwargs):
313 output, status = self.runlines(cmd, *args, **kwargs)
320 output, status = self.runlines(cmd, *args, **kwargs)
314 self.checkexit(status, ''.join(output))
321 self.checkexit(status, ''.join(output))
315 return output
322 return output
316
323
317 def getargmax(self):
324 def getargmax(self):
318 if '_argmax' in self.__dict__:
325 if '_argmax' in self.__dict__:
319 return self._argmax
326 return self._argmax
320
327
321 # POSIX requires at least 4096 bytes for ARG_MAX
328 # POSIX requires at least 4096 bytes for ARG_MAX
322 self._argmax = 4096
329 self._argmax = 4096
323 try:
330 try:
324 self._argmax = os.sysconf("SC_ARG_MAX")
331 self._argmax = os.sysconf("SC_ARG_MAX")
325 except:
332 except:
326 pass
333 pass
327
334
328 # Windows shells impose their own limits on command line length,
335 # Windows shells impose their own limits on command line length,
329 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
336 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
330 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
337 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
331 # details about cmd.exe limitations.
338 # details about cmd.exe limitations.
332
339
333 # Since ARG_MAX is for command line _and_ environment, lower our limit
340 # Since ARG_MAX is for command line _and_ environment, lower our limit
334 # (and make happy Windows shells while doing this).
341 # (and make happy Windows shells while doing this).
335
342
336 self._argmax = self._argmax / 2 - 1
343 self._argmax = self._argmax / 2 - 1
337 return self._argmax
344 return self._argmax
338
345
339 def limit_arglist(self, arglist, cmd, *args, **kwargs):
346 def limit_arglist(self, arglist, cmd, closestdin, *args, **kwargs):
340 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
347 cmdlen = len(self._cmdline(cmd, closestdin, *args, **kwargs))
348 limit = self.getargmax() - cmdlen
341 bytes = 0
349 bytes = 0
342 fl = []
350 fl = []
343 for fn in arglist:
351 for fn in arglist:
344 b = len(fn) + 3
352 b = len(fn) + 3
345 if bytes + b < limit or len(fl) == 0:
353 if bytes + b < limit or len(fl) == 0:
346 fl.append(fn)
354 fl.append(fn)
347 bytes += b
355 bytes += b
348 else:
356 else:
349 yield fl
357 yield fl
350 fl = [fn]
358 fl = [fn]
351 bytes = b
359 bytes = b
352 if fl:
360 if fl:
353 yield fl
361 yield fl
354
362
355 def xargs(self, arglist, cmd, *args, **kwargs):
363 def xargs(self, arglist, cmd, *args, **kwargs):
356 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
364 for l in self.limit_arglist(arglist, cmd, True, *args, **kwargs):
357 self.run0(cmd, *(list(args) + l), **kwargs)
365 self.run0(cmd, *(list(args) + l), **kwargs)
358
366
359 class mapfile(dict):
367 class mapfile(dict):
360 def __init__(self, ui, path):
368 def __init__(self, ui, path):
361 super(mapfile, self).__init__()
369 super(mapfile, self).__init__()
362 self.ui = ui
370 self.ui = ui
363 self.path = path
371 self.path = path
364 self.fp = None
372 self.fp = None
365 self.order = []
373 self.order = []
366 self._read()
374 self._read()
367
375
368 def _read(self):
376 def _read(self):
369 if not self.path:
377 if not self.path:
370 return
378 return
371 try:
379 try:
372 fp = open(self.path, 'r')
380 fp = open(self.path, 'r')
373 except IOError, err:
381 except IOError, err:
374 if err.errno != errno.ENOENT:
382 if err.errno != errno.ENOENT:
375 raise
383 raise
376 return
384 return
377 for i, line in enumerate(fp):
385 for i, line in enumerate(fp):
378 try:
386 try:
379 key, value = line.splitlines()[0].rsplit(' ', 1)
387 key, value = line.splitlines()[0].rsplit(' ', 1)
380 except ValueError:
388 except ValueError:
381 raise util.Abort(
389 raise util.Abort(
382 _('syntax error in %s(%d): key/value pair expected')
390 _('syntax error in %s(%d): key/value pair expected')
383 % (self.path, i + 1))
391 % (self.path, i + 1))
384 if key not in self:
392 if key not in self:
385 self.order.append(key)
393 self.order.append(key)
386 super(mapfile, self).__setitem__(key, value)
394 super(mapfile, self).__setitem__(key, value)
387 fp.close()
395 fp.close()
388
396
389 def __setitem__(self, key, value):
397 def __setitem__(self, key, value):
390 if self.fp is None:
398 if self.fp is None:
391 try:
399 try:
392 self.fp = open(self.path, 'a')
400 self.fp = open(self.path, 'a')
393 except IOError, err:
401 except IOError, err:
394 raise util.Abort(_('could not open map file %r: %s') %
402 raise util.Abort(_('could not open map file %r: %s') %
395 (self.path, err.strerror))
403 (self.path, err.strerror))
396 self.fp.write('%s %s\n' % (key, value))
404 self.fp.write('%s %s\n' % (key, value))
397 self.fp.flush()
405 self.fp.flush()
398 super(mapfile, self).__setitem__(key, value)
406 super(mapfile, self).__setitem__(key, value)
399
407
400 def close(self):
408 def close(self):
401 if self.fp:
409 if self.fp:
402 self.fp.close()
410 self.fp.close()
403 self.fp = None
411 self.fp = None
General Comments 0
You need to be logged in to leave comments. Login now