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