##// END OF EJS Templates
convert: stringify `shlex` class argument...
Matt Harbison -
r52579:39033e7a default
parent child Browse files
Show More
@@ -1,612 +1,615
1 1 # common.py - common code for the convert extension
2 2 #
3 3 # Copyright 2005-2009 Olivia Mackall <olivia@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
9 9 import os
10 10 import pickle
11 11 import re
12 12 import shlex
13 13 import subprocess
14 14 import typing
15 15
16 16 from typing import (
17 17 Any,
18 18 AnyStr,
19 19 Optional,
20 20 )
21 21
22 22 from mercurial.i18n import _
23 23 from mercurial.pycompat import open
24 24 from mercurial import (
25 25 encoding,
26 26 error,
27 27 phases,
28 28 pycompat,
29 29 util,
30 30 )
31 31 from mercurial.utils import (
32 32 dateutil,
33 33 procutil,
34 34 )
35 35
36 36 if typing.TYPE_CHECKING:
37 37 from typing import (
38 38 overload,
39 39 )
40 40 from mercurial import (
41 41 ui as uimod,
42 42 )
43 43
44 44 propertycache = util.propertycache
45 45
46 46
47 47 if typing.TYPE_CHECKING:
48 48
49 49 @overload
50 50 def _encodeornone(d: str) -> bytes:
51 51 pass
52 52
53 53 @overload
54 54 def _encodeornone(d: None) -> None:
55 55 pass
56 56
57 57
58 58 def _encodeornone(d):
59 59 if d is None:
60 60 return
61 61 return d.encode('latin1')
62 62
63 63
64 64 class _shlexpy3proxy:
65 65 def __init__(self, l: shlex.shlex) -> None:
66 66 self._l = l
67 67
68 68 def __iter__(self):
69 69 return (_encodeornone(v) for v in self._l)
70 70
71 71 def get_token(self):
72 72 return _encodeornone(self._l.get_token())
73 73
74 74 @property
75 def infile(self):
76 return self._l.infile or b'<unknown>'
75 def infile(self) -> bytes:
76 if self._l.infile is not None:
77 return encoding.strtolocal(self._l.infile)
78 return b'<unknown>'
77 79
78 80 @property
79 81 def lineno(self) -> int:
80 82 return self._l.lineno
81 83
82 84
83 85 def shlexer(
84 86 data=None,
85 filepath: Optional[str] = None,
87 filepath: Optional[bytes] = None,
86 88 wordchars: Optional[bytes] = None,
87 89 whitespace: Optional[bytes] = None,
88 90 ):
89 91 if data is None:
90 92 data = open(filepath, b'r', encoding='latin1')
91 93 else:
92 94 if filepath is not None:
93 95 raise error.ProgrammingError(
94 96 b'shlexer only accepts data or filepath, not both'
95 97 )
96 98 data = data.decode('latin1')
97 l = shlex.shlex(data, infile=filepath, posix=True)
99 infile = encoding.strfromlocal(filepath) if filepath is not None else None
100 l = shlex.shlex(data, infile=infile, posix=True)
98 101 if whitespace is not None:
99 102 l.whitespace_split = True
100 103 l.whitespace += whitespace.decode('latin1')
101 104 if wordchars is not None:
102 105 l.wordchars += wordchars.decode('latin1')
103 106 return _shlexpy3proxy(l)
104 107
105 108
106 109 def encodeargs(args: Any) -> bytes:
107 110 def encodearg(s: bytes) -> bytes:
108 111 lines = base64.encodebytes(s)
109 112 lines = [l.splitlines()[0] for l in pycompat.iterbytestr(lines)]
110 113 return b''.join(lines)
111 114
112 115 s = pickle.dumps(args)
113 116 return encodearg(s)
114 117
115 118
116 119 def decodeargs(s: bytes) -> Any:
117 120 s = base64.decodebytes(s)
118 121 return pickle.loads(s)
119 122
120 123
121 124 class MissingTool(Exception):
122 125 pass
123 126
124 127
125 128 def checktool(
126 129 exe: bytes, name: Optional[bytes] = None, abort: bool = True
127 130 ) -> None:
128 131 name = name or exe
129 132 if not procutil.findexe(exe):
130 133 if abort:
131 134 exc = error.Abort
132 135 else:
133 136 exc = MissingTool
134 137 raise exc(_(b'cannot find required "%s" tool') % name)
135 138
136 139
137 140 class NoRepo(Exception):
138 141 pass
139 142
140 143
141 144 SKIPREV: bytes = b'SKIP'
142 145
143 146
144 147 class commit:
145 148 def __init__(
146 149 self,
147 150 author: bytes,
148 151 date: bytes,
149 152 desc: bytes,
150 153 parents,
151 154 branch: Optional[bytes] = None,
152 155 rev=None,
153 156 extra=None,
154 157 sortkey=None,
155 158 saverev=True,
156 159 phase: int = phases.draft,
157 160 optparents=None,
158 161 ctx=None,
159 162 ) -> None:
160 163 self.author = author or b'unknown'
161 164 self.date = date or b'0 0'
162 165 self.desc = desc
163 166 self.parents = parents # will be converted and used as parents
164 167 self.optparents = optparents or [] # will be used if already converted
165 168 self.branch = branch
166 169 self.rev = rev
167 170 self.extra = extra or {}
168 171 self.sortkey = sortkey
169 172 self.saverev = saverev
170 173 self.phase = phase
171 174 self.ctx = ctx # for hg to hg conversions
172 175
173 176
174 177 class converter_source:
175 178 """Conversion source interface"""
176 179
177 180 def __init__(
178 181 self,
179 182 ui: "uimod.ui",
180 183 repotype: bytes,
181 184 path: Optional[bytes] = None,
182 185 revs=None,
183 186 ) -> None:
184 187 """Initialize conversion source (or raise NoRepo("message")
185 188 exception if path is not a valid repository)"""
186 189 self.ui = ui
187 190 self.path = path
188 191 self.revs = revs
189 192 self.repotype = repotype
190 193
191 194 self.encoding = b'utf-8'
192 195
193 196 def checkhexformat(
194 197 self, revstr: bytes, mapname: bytes = b'splicemap'
195 198 ) -> None:
196 199 """fails if revstr is not a 40 byte hex. mercurial and git both uses
197 200 such format for their revision numbering
198 201 """
199 202 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr):
200 203 raise error.Abort(
201 204 _(b'%s entry %s is not a valid revision identifier')
202 205 % (mapname, revstr)
203 206 )
204 207
205 208 def before(self) -> None:
206 209 pass
207 210
208 211 def after(self) -> None:
209 212 pass
210 213
211 214 def targetfilebelongstosource(self, targetfilename):
212 215 """Returns true if the given targetfile belongs to the source repo. This
213 216 is useful when only a subdirectory of the target belongs to the source
214 217 repo."""
215 218 # For normal full repo converts, this is always True.
216 219 return True
217 220
218 221 def setrevmap(self, revmap):
219 222 """set the map of already-converted revisions"""
220 223
221 224 def getheads(self):
222 225 """Return a list of this repository's heads"""
223 226 raise NotImplementedError
224 227
225 228 def getfile(self, name, rev):
226 229 """Return a pair (data, mode) where data is the file content
227 230 as a string and mode one of '', 'x' or 'l'. rev is the
228 231 identifier returned by a previous call to getchanges().
229 232 Data is None if file is missing/deleted in rev.
230 233 """
231 234 raise NotImplementedError
232 235
233 236 def getchanges(self, version, full):
234 237 """Returns a tuple of (files, copies, cleanp2).
235 238
236 239 files is a sorted list of (filename, id) tuples for all files
237 240 changed between version and its first parent returned by
238 241 getcommit(). If full, all files in that revision is returned.
239 242 id is the source revision id of the file.
240 243
241 244 copies is a dictionary of dest: source
242 245
243 246 cleanp2 is the set of files filenames that are clean against p2.
244 247 (Files that are clean against p1 are already not in files (unless
245 248 full). This makes it possible to handle p2 clean files similarly.)
246 249 """
247 250 raise NotImplementedError
248 251
249 252 def getcommit(self, version):
250 253 """Return the commit object for version"""
251 254 raise NotImplementedError
252 255
253 256 def numcommits(self):
254 257 """Return the number of commits in this source.
255 258
256 259 If unknown, return None.
257 260 """
258 261 return None
259 262
260 263 def gettags(self):
261 264 """Return the tags as a dictionary of name: revision
262 265
263 266 Tag names must be UTF-8 strings.
264 267 """
265 268 raise NotImplementedError
266 269
267 270 def recode(self, s: AnyStr, encoding: Optional[bytes] = None) -> bytes:
268 271 if not encoding:
269 272 encoding = self.encoding or b'utf-8'
270 273
271 274 if isinstance(s, str):
272 275 return s.encode("utf-8")
273 276 try:
274 277 return s.decode(pycompat.sysstr(encoding)).encode("utf-8")
275 278 except UnicodeError:
276 279 try:
277 280 return s.decode("latin-1").encode("utf-8")
278 281 except UnicodeError:
279 282 return s.decode(pycompat.sysstr(encoding), "replace").encode(
280 283 "utf-8"
281 284 )
282 285
283 286 def getchangedfiles(self, rev, i):
284 287 """Return the files changed by rev compared to parent[i].
285 288
286 289 i is an index selecting one of the parents of rev. The return
287 290 value should be the list of files that are different in rev and
288 291 this parent.
289 292
290 293 If rev has no parents, i is None.
291 294
292 295 This function is only needed to support --filemap
293 296 """
294 297 raise NotImplementedError
295 298
296 299 def converted(self, rev, sinkrev) -> None:
297 300 '''Notify the source that a revision has been converted.'''
298 301
299 302 def hasnativeorder(self) -> bool:
300 303 """Return true if this source has a meaningful, native revision
301 304 order. For instance, Mercurial revisions are store sequentially
302 305 while there is no such global ordering with Darcs.
303 306 """
304 307 return False
305 308
306 309 def hasnativeclose(self) -> bool:
307 310 """Return true if this source has ability to close branch."""
308 311 return False
309 312
310 313 def lookuprev(self, rev):
311 314 """If rev is a meaningful revision reference in source, return
312 315 the referenced identifier in the same format used by getcommit().
313 316 return None otherwise.
314 317 """
315 318 return None
316 319
317 320 def getbookmarks(self):
318 321 """Return the bookmarks as a dictionary of name: revision
319 322
320 323 Bookmark names are to be UTF-8 strings.
321 324 """
322 325 return {}
323 326
324 327 def checkrevformat(self, revstr, mapname: bytes = b'splicemap') -> bool:
325 328 """revstr is a string that describes a revision in the given
326 329 source control system. Return true if revstr has correct
327 330 format.
328 331 """
329 332 return True
330 333
331 334
332 335 class converter_sink:
333 336 """Conversion sink (target) interface"""
334 337
335 338 def __init__(self, ui: "uimod.ui", repotype: bytes, path: bytes) -> None:
336 339 """Initialize conversion sink (or raise NoRepo("message")
337 340 exception if path is not a valid repository)
338 341
339 342 created is a list of paths to remove if a fatal error occurs
340 343 later"""
341 344 self.ui = ui
342 345 self.path = path
343 346 self.created = []
344 347 self.repotype = repotype
345 348
346 349 def revmapfile(self):
347 350 """Path to a file that will contain lines
348 351 source_rev_id sink_rev_id
349 352 mapping equivalent revision identifiers for each system."""
350 353 raise NotImplementedError
351 354
352 355 def authorfile(self):
353 356 """Path to a file that will contain lines
354 357 srcauthor=dstauthor
355 358 mapping equivalent authors identifiers for each system."""
356 359 return None
357 360
358 361 def putcommit(
359 362 self, files, copies, parents, commit, source, revmap, full, cleanp2
360 363 ):
361 364 """Create a revision with all changed files listed in 'files'
362 365 and having listed parents. 'commit' is a commit object
363 366 containing at a minimum the author, date, and message for this
364 367 changeset. 'files' is a list of (path, version) tuples,
365 368 'copies' is a dictionary mapping destinations to sources,
366 369 'source' is the source repository, and 'revmap' is a mapfile
367 370 of source revisions to converted revisions. Only getfile() and
368 371 lookuprev() should be called on 'source'. 'full' means that 'files'
369 372 is complete and all other files should be removed.
370 373 'cleanp2' is a set of the filenames that are unchanged from p2
371 374 (only in the common merge case where there two parents).
372 375
373 376 Note that the sink repository is not told to update itself to
374 377 a particular revision (or even what that revision would be)
375 378 before it receives the file data.
376 379 """
377 380 raise NotImplementedError
378 381
379 382 def puttags(self, tags):
380 383 """Put tags into sink.
381 384
382 385 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
383 386 Return a pair (tag_revision, tag_parent_revision), or (None, None)
384 387 if nothing was changed.
385 388 """
386 389 raise NotImplementedError
387 390
388 391 def setbranch(self, branch, pbranches):
389 392 """Set the current branch name. Called before the first putcommit
390 393 on the branch.
391 394 branch: branch name for subsequent commits
392 395 pbranches: (converted parent revision, parent branch) tuples"""
393 396
394 397 def setfilemapmode(self, active):
395 398 """Tell the destination that we're using a filemap
396 399
397 400 Some converter_sources (svn in particular) can claim that a file
398 401 was changed in a revision, even if there was no change. This method
399 402 tells the destination that we're using a filemap and that it should
400 403 filter empty revisions.
401 404 """
402 405
403 406 def before(self) -> None:
404 407 pass
405 408
406 409 def after(self) -> None:
407 410 pass
408 411
409 412 def putbookmarks(self, bookmarks):
410 413 """Put bookmarks into sink.
411 414
412 415 bookmarks: {bookmarkname: sink_rev_id, ...}
413 416 where bookmarkname is an UTF-8 string.
414 417 """
415 418
416 419 def hascommitfrommap(self, rev):
417 420 """Return False if a rev mentioned in a filemap is known to not be
418 421 present."""
419 422 raise NotImplementedError
420 423
421 424 def hascommitforsplicemap(self, rev):
422 425 """This method is for the special needs for splicemap handling and not
423 426 for general use. Returns True if the sink contains rev, aborts on some
424 427 special cases."""
425 428 raise NotImplementedError
426 429
427 430
428 431 class commandline:
429 432 def __init__(self, ui: "uimod.ui", command: bytes) -> None:
430 433 self.ui = ui
431 434 self.command = command
432 435
433 436 def prerun(self) -> None:
434 437 pass
435 438
436 439 def postrun(self) -> None:
437 440 pass
438 441
439 442 def _cmdline(self, cmd: bytes, *args: bytes, **kwargs) -> bytes:
440 443 kwargs = pycompat.byteskwargs(kwargs)
441 444 cmdline = [self.command, cmd] + list(args)
442 445 for k, v in kwargs.items():
443 446 if len(k) == 1:
444 447 cmdline.append(b'-' + k)
445 448 else:
446 449 cmdline.append(b'--' + k.replace(b'_', b'-'))
447 450 try:
448 451 if len(k) == 1:
449 452 cmdline.append(b'' + v)
450 453 else:
451 454 cmdline[-1] += b'=' + v
452 455 except TypeError:
453 456 pass
454 457 cmdline = [procutil.shellquote(arg) for arg in cmdline]
455 458 if not self.ui.debugflag:
456 459 cmdline += [b'2>', pycompat.bytestr(os.devnull)]
457 460 cmdline = b' '.join(cmdline)
458 461 return cmdline
459 462
460 463 def _run(self, cmd: bytes, *args: bytes, **kwargs):
461 464 def popen(cmdline):
462 465 p = subprocess.Popen(
463 466 procutil.tonativestr(cmdline),
464 467 shell=True,
465 468 bufsize=-1,
466 469 close_fds=procutil.closefds,
467 470 stdout=subprocess.PIPE,
468 471 )
469 472 return p
470 473
471 474 return self._dorun(popen, cmd, *args, **kwargs)
472 475
473 476 def _run2(self, cmd: bytes, *args: bytes, **kwargs):
474 477 return self._dorun(procutil.popen2, cmd, *args, **kwargs)
475 478
476 479 def _run3(self, cmd: bytes, *args: bytes, **kwargs):
477 480 return self._dorun(procutil.popen3, cmd, *args, **kwargs)
478 481
479 482 def _dorun(self, openfunc, cmd: bytes, *args: bytes, **kwargs):
480 483 cmdline = self._cmdline(cmd, *args, **kwargs)
481 484 self.ui.debug(b'running: %s\n' % (cmdline,))
482 485 self.prerun()
483 486 try:
484 487 return openfunc(cmdline)
485 488 finally:
486 489 self.postrun()
487 490
488 491 def run(self, cmd: bytes, *args: bytes, **kwargs):
489 492 p = self._run(cmd, *args, **kwargs)
490 493 output = p.communicate()[0]
491 494 self.ui.debug(output)
492 495 return output, p.returncode
493 496
494 497 def runlines(self, cmd: bytes, *args: bytes, **kwargs):
495 498 p = self._run(cmd, *args, **kwargs)
496 499 output = p.stdout.readlines()
497 500 p.wait()
498 501 self.ui.debug(b''.join(output))
499 502 return output, p.returncode
500 503
501 504 def checkexit(self, status, output: bytes = b'') -> None:
502 505 if status:
503 506 if output:
504 507 self.ui.warn(_(b'%s error:\n') % self.command)
505 508 self.ui.warn(output)
506 509 msg = procutil.explainexit(status)
507 510 raise error.Abort(b'%s %s' % (self.command, msg))
508 511
509 512 def run0(self, cmd: bytes, *args: bytes, **kwargs):
510 513 output, status = self.run(cmd, *args, **kwargs)
511 514 self.checkexit(status, output)
512 515 return output
513 516
514 517 def runlines0(self, cmd: bytes, *args: bytes, **kwargs):
515 518 output, status = self.runlines(cmd, *args, **kwargs)
516 519 self.checkexit(status, b''.join(output))
517 520 return output
518 521
519 522 @propertycache
520 523 def argmax(self):
521 524 # POSIX requires at least 4096 bytes for ARG_MAX
522 525 argmax = 4096
523 526 try:
524 527 argmax = os.sysconf("SC_ARG_MAX")
525 528 except (AttributeError, ValueError):
526 529 pass
527 530
528 531 # Windows shells impose their own limits on command line length,
529 532 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
530 533 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
531 534 # details about cmd.exe limitations.
532 535
533 536 # Since ARG_MAX is for command line _and_ environment, lower our limit
534 537 # (and make happy Windows shells while doing this).
535 538 return argmax // 2 - 1
536 539
537 540 def _limit_arglist(self, arglist, cmd: bytes, *args: bytes, **kwargs):
538 541 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
539 542 limit = self.argmax - cmdlen
540 543 numbytes = 0
541 544 fl = []
542 545 for fn in arglist:
543 546 b = len(fn) + 3
544 547 if numbytes + b < limit or len(fl) == 0:
545 548 fl.append(fn)
546 549 numbytes += b
547 550 else:
548 551 yield fl
549 552 fl = [fn]
550 553 numbytes = b
551 554 if fl:
552 555 yield fl
553 556
554 557 def xargs(self, arglist, cmd: bytes, *args: bytes, **kwargs):
555 558 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
556 559 self.run0(cmd, *(list(args) + l), **kwargs)
557 560
558 561
559 562 class mapfile(dict):
560 563 def __init__(self, ui: "uimod.ui", path: bytes) -> None:
561 564 super(mapfile, self).__init__()
562 565 self.ui = ui
563 566 self.path = path
564 567 self.fp = None
565 568 self.order = []
566 569 self._read()
567 570
568 571 def _read(self) -> None:
569 572 if not self.path:
570 573 return
571 574 try:
572 575 fp = open(self.path, b'rb')
573 576 except FileNotFoundError:
574 577 return
575 578 for i, line in enumerate(fp):
576 579 line = line.splitlines()[0].rstrip()
577 580 if not line:
578 581 # Ignore blank lines
579 582 continue
580 583 try:
581 584 key, value = line.rsplit(b' ', 1)
582 585 except ValueError:
583 586 raise error.Abort(
584 587 _(b'syntax error in %s(%d): key/value pair expected')
585 588 % (self.path, i + 1)
586 589 )
587 590 if key not in self:
588 591 self.order.append(key)
589 592 super(mapfile, self).__setitem__(key, value)
590 593 fp.close()
591 594
592 595 def __setitem__(self, key, value) -> None:
593 596 if self.fp is None:
594 597 try:
595 598 self.fp = open(self.path, b'ab')
596 599 except IOError as err:
597 600 raise error.Abort(
598 601 _(b'could not open map file %r: %s')
599 602 % (self.path, encoding.strtolocal(err.strerror))
600 603 )
601 604 self.fp.write(util.tonativeeol(b'%s %s\n' % (key, value)))
602 605 self.fp.flush()
603 606 super(mapfile, self).__setitem__(key, value)
604 607
605 608 def close(self) -> None:
606 609 if self.fp:
607 610 self.fp.close()
608 611 self.fp = None
609 612
610 613
611 614 def makedatetimestamp(t: float) -> dateutil.hgdate:
612 615 return dateutil.makedate(t)
@@ -1,530 +1,531
1 1 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
2 2 # Copyright 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7
8 8 import posixpath
9 9 import typing
10 10
11 11 from typing import (
12 12 Iterator,
13 13 Mapping,
14 14 MutableMapping,
15 15 Optional,
16 16 Set,
17 17 Tuple,
18 18 overload,
19 19 )
20 20
21 21 from mercurial.i18n import _
22 22 from mercurial import (
23 23 error,
24 24 pycompat,
25 25 )
26 26 from . import common
27 27
28 28 if typing.TYPE_CHECKING:
29 29 from mercurial import (
30 30 ui as uimod,
31 31 )
32 32
33 33 SKIPREV = common.SKIPREV
34 34
35 35
36 36 def rpairs(path: bytes) -> Iterator[Tuple[bytes, bytes]]:
37 37 """Yield tuples with path split at '/', starting with the full path.
38 38 No leading, trailing or double '/', please.
39 39 >>> for x in rpairs(b'foo/bar/baz'): print(x)
40 40 ('foo/bar/baz', '')
41 41 ('foo/bar', 'baz')
42 42 ('foo', 'bar/baz')
43 43 ('.', 'foo/bar/baz')
44 44 """
45 45 i = len(path)
46 46 while i != -1:
47 47 yield path[:i], path[i + 1 :]
48 48 i = path.rfind(b'/', 0, i)
49 49 yield b'.', path
50 50
51 51
52 52 if typing.TYPE_CHECKING:
53 53
54 54 @overload
55 55 def normalize(path: bytes) -> bytes:
56 56 pass
57 57
58 58 @overload
59 59 def normalize(path: None) -> None:
60 60 pass
61 61
62 62
63 63 def normalize(path):
64 64 """We use posixpath.normpath to support cross-platform path format.
65 65 However, it doesn't handle None input. So we wrap it up."""
66 66 if path is None:
67 67 return None
68 68 return posixpath.normpath(path)
69 69
70 70
71 71 class filemapper:
72 72 """Map and filter filenames when importing.
73 73 A name can be mapped to itself, a new name, or None (omit from new
74 74 repository)."""
75 75
76 76 rename: MutableMapping[bytes, bytes]
77 77 targetprefixes: Optional[Set[bytes]]
78 78
79 def __init__(self, ui: "uimod.ui", path=None) -> None:
79 def __init__(self, ui: "uimod.ui", path: Optional[bytes] = None) -> None:
80 80 self.ui = ui
81 81 self.include = {}
82 82 self.exclude = {}
83 83 self.rename = {}
84 84 self.targetprefixes = None
85 85 if path:
86 86 if self.parse(path):
87 87 raise error.Abort(_(b'errors in filemap'))
88 88
89 # TODO: cmd==b'source' case breaks if ``path``is str
90 def parse(self, path) -> int:
89 def parse(self, path: Optional[bytes]) -> int:
91 90 errs = 0
92 91
93 92 def check(name: bytes, mapping, listname: bytes):
94 93 if not name:
95 94 self.ui.warn(
96 95 _(b'%s:%d: path to %s is missing\n')
97 96 % (lex.infile, lex.lineno, listname)
98 97 )
99 98 return 1
100 99 if name in mapping:
101 100 self.ui.warn(
102 101 _(b'%s:%d: %r already in %s list\n')
103 102 % (lex.infile, lex.lineno, name, listname)
104 103 )
105 104 return 1
106 105 if name.startswith(b'/') or name.endswith(b'/') or b'//' in name:
107 106 self.ui.warn(
108 107 _(b'%s:%d: superfluous / in %s %r\n')
109 108 % (lex.infile, lex.lineno, listname, pycompat.bytestr(name))
110 109 )
111 110 return 1
112 111 return 0
113 112
114 113 lex = common.shlexer(
115 114 filepath=path, wordchars=b'!@#$%^&*()-=+[]{}|;:,./<>?'
116 115 )
117 116 cmd = lex.get_token()
118 117 while cmd:
119 118 if cmd == b'include':
120 119 name = normalize(lex.get_token())
121 120 errs += check(name, self.exclude, b'exclude')
122 121 self.include[name] = name
123 122 elif cmd == b'exclude':
124 123 name = normalize(lex.get_token())
125 124 errs += check(name, self.include, b'include')
126 125 errs += check(name, self.rename, b'rename')
127 126 self.exclude[name] = name
128 127 elif cmd == b'rename':
129 128 src = normalize(lex.get_token())
130 129 dest = normalize(lex.get_token())
131 130 errs += check(src, self.exclude, b'exclude')
132 131 self.rename[src] = dest
133 132 elif cmd == b'source':
134 133 errs += self.parse(normalize(lex.get_token()))
135 134 else:
136 135 self.ui.warn(
137 136 _(b'%s:%d: unknown directive %r\n')
138 137 % (lex.infile, lex.lineno, pycompat.bytestr(cmd))
139 138 )
140 139 errs += 1
141 140 cmd = lex.get_token()
142 141 return errs
143 142
144 143 def lookup(
145 144 self, name: bytes, mapping: Mapping[bytes, bytes]
146 145 ) -> Tuple[bytes, bytes, bytes]:
147 146 name = normalize(name)
148 147 for pre, suf in rpairs(name):
149 148 try:
150 149 return mapping[pre], pre, suf
151 150 except KeyError:
152 151 pass
153 152 return b'', name, b''
154 153
155 154 def istargetfile(self, filename: bytes) -> bool:
156 155 """Return true if the given target filename is covered as a destination
157 156 of the filemap. This is useful for identifying what parts of the target
158 157 repo belong to the source repo and what parts don't."""
159 158 if self.targetprefixes is None:
160 159 self.targetprefixes = set()
161 160 for before, after in self.rename.items():
162 161 self.targetprefixes.add(after)
163 162
164 163 # If "." is a target, then all target files are considered from the
165 164 # source.
166 165 if not self.targetprefixes or b'.' in self.targetprefixes:
167 166 return True
168 167
169 168 filename = normalize(filename)
170 169 for pre, suf in rpairs(filename):
171 170 # This check is imperfect since it doesn't account for the
172 171 # include/exclude list, but it should work in filemaps that don't
173 172 # apply include/exclude to the same source directories they are
174 173 # renaming.
175 174 if pre in self.targetprefixes:
176 175 return True
177 176 return False
178 177
179 178 def __call__(self, name: bytes) -> Optional[bytes]:
180 179 if self.include:
181 180 inc = self.lookup(name, self.include)[0]
182 181 else:
183 182 inc = name
184 183 if self.exclude:
185 184 exc = self.lookup(name, self.exclude)[0]
186 185 else:
187 186 exc = b''
188 187 if (not self.include and exc) or (len(inc) <= len(exc)):
189 188 return None
190 189 newpre, pre, suf = self.lookup(name, self.rename)
191 190 if newpre:
192 191 if newpre == b'.':
193 192 return suf
194 193 if suf:
195 194 if newpre.endswith(b'/'):
196 195 return newpre + suf
197 196 return newpre + b'/' + suf
198 197 return newpre
199 198 return name
200 199
201 200 def active(self) -> bool:
202 201 return bool(self.include or self.exclude or self.rename)
203 202
204 203
205 204 # This class does two additional things compared to a regular source:
206 205 #
207 206 # - Filter and rename files. This is mostly wrapped by the filemapper
208 207 # class above. We hide the original filename in the revision that is
209 208 # returned by getchanges to be able to find things later in getfile.
210 209 #
211 210 # - Return only revisions that matter for the files we're interested in.
212 211 # This involves rewriting the parents of the original revision to
213 212 # create a graph that is restricted to those revisions.
214 213 #
215 214 # This set of revisions includes not only revisions that directly
216 215 # touch files we're interested in, but also merges that merge two
217 216 # or more interesting revisions.
218 217
219 218
220 219 class filemap_source(common.converter_source):
221 def __init__(self, ui: "uimod.ui", baseconverter, filemap) -> None:
220 def __init__(
221 self, ui: "uimod.ui", baseconverter, filemap: Optional[bytes]
222 ) -> None:
222 223 super(filemap_source, self).__init__(ui, baseconverter.repotype)
223 224 self.base = baseconverter
224 225 self.filemapper = filemapper(ui, filemap)
225 226 self.commits = {}
226 227 # if a revision rev has parent p in the original revision graph, then
227 228 # rev will have parent self.parentmap[p] in the restricted graph.
228 229 self.parentmap = {}
229 230 # self.wantedancestors[rev] is the set of all ancestors of rev that
230 231 # are in the restricted graph.
231 232 self.wantedancestors = {}
232 233 self.convertedorder = None
233 234 self._rebuilt = False
234 235 self.origparents = {}
235 236 self.children = {}
236 237 self.seenchildren = {}
237 238 # experimental config: convert.ignoreancestorcheck
238 239 self.ignoreancestorcheck = self.ui.configbool(
239 240 b'convert', b'ignoreancestorcheck'
240 241 )
241 242
242 243 def before(self) -> None:
243 244 self.base.before()
244 245
245 246 def after(self) -> None:
246 247 self.base.after()
247 248
248 249 def setrevmap(self, revmap):
249 250 # rebuild our state to make things restartable
250 251 #
251 252 # To avoid calling getcommit for every revision that has already
252 253 # been converted, we rebuild only the parentmap, delaying the
253 254 # rebuild of wantedancestors until we need it (i.e. until a
254 255 # merge).
255 256 #
256 257 # We assume the order argument lists the revisions in
257 258 # topological order, so that we can infer which revisions were
258 259 # wanted by previous runs.
259 260 self._rebuilt = not revmap
260 261 seen = {SKIPREV: SKIPREV}
261 262 dummyset = set()
262 263 converted = []
263 264 for rev in revmap.order:
264 265 mapped = revmap[rev]
265 266 wanted = mapped not in seen
266 267 if wanted:
267 268 seen[mapped] = rev
268 269 self.parentmap[rev] = rev
269 270 else:
270 271 self.parentmap[rev] = seen[mapped]
271 272 self.wantedancestors[rev] = dummyset
272 273 arg = seen[mapped]
273 274 if arg == SKIPREV:
274 275 arg = None
275 276 converted.append((rev, wanted, arg))
276 277 self.convertedorder = converted
277 278 return self.base.setrevmap(revmap)
278 279
279 280 def rebuild(self) -> bool:
280 281 if self._rebuilt:
281 282 return True
282 283 self._rebuilt = True
283 284 self.parentmap.clear()
284 285 self.wantedancestors.clear()
285 286 self.seenchildren.clear()
286 287 for rev, wanted, arg in self.convertedorder:
287 288 if rev not in self.origparents:
288 289 try:
289 290 self.origparents[rev] = self.getcommit(rev).parents
290 291 except error.RepoLookupError:
291 292 self.ui.debug(b"unknown revmap source: %s\n" % rev)
292 293 continue
293 294 if arg is not None:
294 295 self.children[arg] = self.children.get(arg, 0) + 1
295 296
296 297 for rev, wanted, arg in self.convertedorder:
297 298 try:
298 299 parents = self.origparents[rev]
299 300 except KeyError:
300 301 continue # unknown revmap source
301 302 if wanted:
302 303 self.mark_wanted(rev, parents)
303 304 else:
304 305 self.mark_not_wanted(rev, arg)
305 306 self._discard(arg, *parents)
306 307
307 308 return True
308 309
309 310 def getheads(self):
310 311 return self.base.getheads()
311 312
312 313 def getcommit(self, rev: bytes):
313 314 # We want to save a reference to the commit objects to be able
314 315 # to rewrite their parents later on.
315 316 c = self.commits[rev] = self.base.getcommit(rev)
316 317 for p in c.parents:
317 318 self.children[p] = self.children.get(p, 0) + 1
318 319 return c
319 320
320 321 def numcommits(self):
321 322 return self.base.numcommits()
322 323
323 324 def _cachedcommit(self, rev):
324 325 if rev in self.commits:
325 326 return self.commits[rev]
326 327 return self.base.getcommit(rev)
327 328
328 329 def _discard(self, *revs) -> None:
329 330 for r in revs:
330 331 if r is None:
331 332 continue
332 333 self.seenchildren[r] = self.seenchildren.get(r, 0) + 1
333 334 if self.seenchildren[r] == self.children[r]:
334 335 self.wantedancestors.pop(r, None)
335 336 self.parentmap.pop(r, None)
336 337 del self.seenchildren[r]
337 338 if self._rebuilt:
338 339 del self.children[r]
339 340
340 341 def wanted(self, rev, i) -> bool:
341 342 # Return True if we're directly interested in rev.
342 343 #
343 344 # i is an index selecting one of the parents of rev (if rev
344 345 # has no parents, i is None). getchangedfiles will give us
345 346 # the list of files that are different in rev and in the parent
346 347 # indicated by i. If we're interested in any of these files,
347 348 # we're interested in rev.
348 349 try:
349 350 files = self.base.getchangedfiles(rev, i)
350 351 except NotImplementedError:
351 352 raise error.Abort(_(b"source repository doesn't support --filemap"))
352 353 for f in files:
353 354 if self.filemapper(f):
354 355 return True
355 356
356 357 # The include directive is documented to include nothing else (though
357 358 # valid branch closes are included).
358 359 if self.filemapper.include:
359 360 return False
360 361
361 362 # Allow empty commits in the source revision through. The getchanges()
362 363 # method doesn't even bother calling this if it determines that the
363 364 # close marker is significant (i.e. all of the branch ancestors weren't
364 365 # eliminated). Therefore if there *is* a close marker, getchanges()
365 366 # doesn't consider it significant, and this revision should be dropped.
366 367 return not files and b'close' not in self.commits[rev].extra
367 368
368 369 def mark_not_wanted(self, rev, p) -> None:
369 370 # Mark rev as not interesting and update data structures.
370 371
371 372 if p is None:
372 373 # A root revision. Use SKIPREV to indicate that it doesn't
373 374 # map to any revision in the restricted graph. Put SKIPREV
374 375 # in the set of wanted ancestors to simplify code elsewhere
375 376 self.parentmap[rev] = SKIPREV
376 377 self.wantedancestors[rev] = {SKIPREV}
377 378 return
378 379
379 380 # Reuse the data from our parent.
380 381 self.parentmap[rev] = self.parentmap[p]
381 382 self.wantedancestors[rev] = self.wantedancestors[p]
382 383
383 384 def mark_wanted(self, rev, parents) -> None:
384 385 # Mark rev ss wanted and update data structures.
385 386
386 387 # rev will be in the restricted graph, so children of rev in
387 388 # the original graph should still have rev as a parent in the
388 389 # restricted graph.
389 390 self.parentmap[rev] = rev
390 391
391 392 # The set of wanted ancestors of rev is the union of the sets
392 393 # of wanted ancestors of its parents. Plus rev itself.
393 394 wrev = set()
394 395 for p in parents:
395 396 if p in self.wantedancestors:
396 397 wrev.update(self.wantedancestors[p])
397 398 else:
398 399 self.ui.warn(
399 400 _(b'warning: %s parent %s is missing\n') % (rev, p)
400 401 )
401 402 wrev.add(rev)
402 403 self.wantedancestors[rev] = wrev
403 404
404 405 def getchanges(self, rev, full):
405 406 parents = self.commits[rev].parents
406 407 if len(parents) > 1 and not self.ignoreancestorcheck:
407 408 self.rebuild()
408 409
409 410 # To decide whether we're interested in rev we:
410 411 #
411 412 # - calculate what parents rev will have if it turns out we're
412 413 # interested in it. If it's going to have more than 1 parent,
413 414 # we're interested in it.
414 415 #
415 416 # - otherwise, we'll compare it with the single parent we found.
416 417 # If any of the files we're interested in is different in the
417 418 # the two revisions, we're interested in rev.
418 419
419 420 # A parent p is interesting if its mapped version (self.parentmap[p]):
420 421 # - is not SKIPREV
421 422 # - is still not in the list of parents (we don't want duplicates)
422 423 # - is not an ancestor of the mapped versions of the other parents or
423 424 # there is no parent in the same branch than the current revision.
424 425 mparents = []
425 426 knownparents = set()
426 427 branch = self.commits[rev].branch
427 428 hasbranchparent = False
428 429 for i, p1 in enumerate(parents):
429 430 mp1 = self.parentmap[p1]
430 431 if mp1 == SKIPREV or mp1 in knownparents:
431 432 continue
432 433
433 434 isancestor = not self.ignoreancestorcheck and any(
434 435 p2
435 436 for p2 in parents
436 437 if p1 != p2
437 438 and mp1 != self.parentmap[p2]
438 439 and mp1 in self.wantedancestors[p2]
439 440 )
440 441 if not isancestor and not hasbranchparent and len(parents) > 1:
441 442 # This could be expensive, avoid unnecessary calls.
442 443 if self._cachedcommit(p1).branch == branch:
443 444 hasbranchparent = True
444 445 mparents.append((p1, mp1, i, isancestor))
445 446 knownparents.add(mp1)
446 447 # Discard parents ancestors of other parents if there is a
447 448 # non-ancestor one on the same branch than current revision.
448 449 if hasbranchparent:
449 450 mparents = [p for p in mparents if not p[3]]
450 451 wp = None
451 452 if mparents:
452 453 wp = max(p[2] for p in mparents)
453 454 mparents = [p[1] for p in mparents]
454 455 elif parents:
455 456 wp = 0
456 457
457 458 self.origparents[rev] = parents
458 459
459 460 closed = False
460 461 if b'close' in self.commits[rev].extra:
461 462 # A branch closing revision is only useful if one of its
462 463 # parents belong to the branch being closed
463 464 pbranches = [self._cachedcommit(p).branch for p in mparents]
464 465 if branch in pbranches:
465 466 closed = True
466 467
467 468 if len(mparents) < 2 and not closed and not self.wanted(rev, wp):
468 469 # We don't want this revision.
469 470 # Update our state and tell the convert process to map this
470 471 # revision to the same revision its parent as mapped to.
471 472 p = None
472 473 if parents:
473 474 p = parents[wp]
474 475 self.mark_not_wanted(rev, p)
475 476 self.convertedorder.append((rev, False, p))
476 477 self._discard(*parents)
477 478 return self.parentmap[rev]
478 479
479 480 # We want this revision.
480 481 # Rewrite the parents of the commit object
481 482 self.commits[rev].parents = mparents
482 483 self.mark_wanted(rev, parents)
483 484 self.convertedorder.append((rev, True, None))
484 485 self._discard(*parents)
485 486
486 487 # Get the real changes and do the filtering/mapping. To be
487 488 # able to get the files later on in getfile, we hide the
488 489 # original filename in the rev part of the return value.
489 490 changes, copies, cleanp2 = self.base.getchanges(rev, full)
490 491 files = {}
491 492 ncleanp2 = set(cleanp2)
492 493 for f, r in changes:
493 494 newf = self.filemapper(f)
494 495 if newf and (newf != f or newf not in files):
495 496 files[newf] = (f, r)
496 497 if newf != f:
497 498 ncleanp2.discard(f)
498 499 files = sorted(files.items())
499 500
500 501 ncopies = {}
501 502 for c in copies:
502 503 newc = self.filemapper(c)
503 504 if newc:
504 505 newsource = self.filemapper(copies[c])
505 506 if newsource:
506 507 ncopies[newc] = newsource
507 508
508 509 return files, ncopies, ncleanp2
509 510
510 511 def targetfilebelongstosource(self, targetfilename: bytes) -> bool:
511 512 return self.filemapper.istargetfile(targetfilename)
512 513
513 514 def getfile(self, name, rev):
514 515 realname, realrev = rev
515 516 return self.base.getfile(realname, realrev)
516 517
517 518 def gettags(self):
518 519 return self.base.gettags()
519 520
520 521 def hasnativeorder(self) -> bool:
521 522 return self.base.hasnativeorder()
522 523
523 524 def lookuprev(self, rev):
524 525 return self.base.lookuprev(rev)
525 526
526 527 def getbookmarks(self):
527 528 return self.base.getbookmarks()
528 529
529 530 def converted(self, rev, sinkrev):
530 531 self.base.converted(rev, sinkrev)
General Comments 0
You need to be logged in to leave comments. Login now