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