##// END OF EJS Templates
errors: add config that lets user get more detailed exit codes...
Martin von Zweigbergk -
r46430:21733e8c default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,1609 +1,1612 b''
1 1 # configitems.py - centralized declaration of configuration option
2 2 #
3 3 # Copyright 2017 Pierre-Yves David <pierre-yves.david@octobus.net>
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 from __future__ import absolute_import
9 9
10 10 import functools
11 11 import re
12 12
13 13 from . import (
14 14 encoding,
15 15 error,
16 16 )
17 17
18 18
19 19 def loadconfigtable(ui, extname, configtable):
20 20 """update config item known to the ui with the extension ones"""
21 21 for section, items in sorted(configtable.items()):
22 22 knownitems = ui._knownconfig.setdefault(section, itemregister())
23 23 knownkeys = set(knownitems)
24 24 newkeys = set(items)
25 25 for key in sorted(knownkeys & newkeys):
26 26 msg = b"extension '%s' overwrite config item '%s.%s'"
27 27 msg %= (extname, section, key)
28 28 ui.develwarn(msg, config=b'warn-config')
29 29
30 30 knownitems.update(items)
31 31
32 32
33 33 class configitem(object):
34 34 """represent a known config item
35 35
36 36 :section: the official config section where to find this item,
37 37 :name: the official name within the section,
38 38 :default: default value for this item,
39 39 :alias: optional list of tuples as alternatives,
40 40 :generic: this is a generic definition, match name using regular expression.
41 41 """
42 42
43 43 def __init__(
44 44 self,
45 45 section,
46 46 name,
47 47 default=None,
48 48 alias=(),
49 49 generic=False,
50 50 priority=0,
51 51 experimental=False,
52 52 ):
53 53 self.section = section
54 54 self.name = name
55 55 self.default = default
56 56 self.alias = list(alias)
57 57 self.generic = generic
58 58 self.priority = priority
59 59 self.experimental = experimental
60 60 self._re = None
61 61 if generic:
62 62 self._re = re.compile(self.name)
63 63
64 64
65 65 class itemregister(dict):
66 66 """A specialized dictionary that can handle wild-card selection"""
67 67
68 68 def __init__(self):
69 69 super(itemregister, self).__init__()
70 70 self._generics = set()
71 71
72 72 def update(self, other):
73 73 super(itemregister, self).update(other)
74 74 self._generics.update(other._generics)
75 75
76 76 def __setitem__(self, key, item):
77 77 super(itemregister, self).__setitem__(key, item)
78 78 if item.generic:
79 79 self._generics.add(item)
80 80
81 81 def get(self, key):
82 82 baseitem = super(itemregister, self).get(key)
83 83 if baseitem is not None and not baseitem.generic:
84 84 return baseitem
85 85
86 86 # search for a matching generic item
87 87 generics = sorted(self._generics, key=(lambda x: (x.priority, x.name)))
88 88 for item in generics:
89 89 # we use 'match' instead of 'search' to make the matching simpler
90 90 # for people unfamiliar with regular expression. Having the match
91 91 # rooted to the start of the string will produce less surprising
92 92 # result for user writing simple regex for sub-attribute.
93 93 #
94 94 # For example using "color\..*" match produces an unsurprising
95 95 # result, while using search could suddenly match apparently
96 96 # unrelated configuration that happens to contains "color."
97 97 # anywhere. This is a tradeoff where we favor requiring ".*" on
98 98 # some match to avoid the need to prefix most pattern with "^".
99 99 # The "^" seems more error prone.
100 100 if item._re.match(key):
101 101 return item
102 102
103 103 return None
104 104
105 105
106 106 coreitems = {}
107 107
108 108
109 109 def _register(configtable, *args, **kwargs):
110 110 item = configitem(*args, **kwargs)
111 111 section = configtable.setdefault(item.section, itemregister())
112 112 if item.name in section:
113 113 msg = b"duplicated config item registration for '%s.%s'"
114 114 raise error.ProgrammingError(msg % (item.section, item.name))
115 115 section[item.name] = item
116 116
117 117
118 118 # special value for case where the default is derived from other values
119 119 dynamicdefault = object()
120 120
121 121 # Registering actual config items
122 122
123 123
124 124 def getitemregister(configtable):
125 125 f = functools.partial(_register, configtable)
126 126 # export pseudo enum as configitem.*
127 127 f.dynamicdefault = dynamicdefault
128 128 return f
129 129
130 130
131 131 coreconfigitem = getitemregister(coreitems)
132 132
133 133
134 134 def _registerdiffopts(section, configprefix=b''):
135 135 coreconfigitem(
136 136 section, configprefix + b'nodates', default=False,
137 137 )
138 138 coreconfigitem(
139 139 section, configprefix + b'showfunc', default=False,
140 140 )
141 141 coreconfigitem(
142 142 section, configprefix + b'unified', default=None,
143 143 )
144 144 coreconfigitem(
145 145 section, configprefix + b'git', default=False,
146 146 )
147 147 coreconfigitem(
148 148 section, configprefix + b'ignorews', default=False,
149 149 )
150 150 coreconfigitem(
151 151 section, configprefix + b'ignorewsamount', default=False,
152 152 )
153 153 coreconfigitem(
154 154 section, configprefix + b'ignoreblanklines', default=False,
155 155 )
156 156 coreconfigitem(
157 157 section, configprefix + b'ignorewseol', default=False,
158 158 )
159 159 coreconfigitem(
160 160 section, configprefix + b'nobinary', default=False,
161 161 )
162 162 coreconfigitem(
163 163 section, configprefix + b'noprefix', default=False,
164 164 )
165 165 coreconfigitem(
166 166 section, configprefix + b'word-diff', default=False,
167 167 )
168 168
169 169
170 170 coreconfigitem(
171 171 b'alias', b'.*', default=dynamicdefault, generic=True,
172 172 )
173 173 coreconfigitem(
174 174 b'auth', b'cookiefile', default=None,
175 175 )
176 176 _registerdiffopts(section=b'annotate')
177 177 # bookmarks.pushing: internal hack for discovery
178 178 coreconfigitem(
179 179 b'bookmarks', b'pushing', default=list,
180 180 )
181 181 # bundle.mainreporoot: internal hack for bundlerepo
182 182 coreconfigitem(
183 183 b'bundle', b'mainreporoot', default=b'',
184 184 )
185 185 coreconfigitem(
186 186 b'censor', b'policy', default=b'abort', experimental=True,
187 187 )
188 188 coreconfigitem(
189 189 b'chgserver', b'idletimeout', default=3600,
190 190 )
191 191 coreconfigitem(
192 192 b'chgserver', b'skiphash', default=False,
193 193 )
194 194 coreconfigitem(
195 195 b'cmdserver', b'log', default=None,
196 196 )
197 197 coreconfigitem(
198 198 b'cmdserver', b'max-log-files', default=7,
199 199 )
200 200 coreconfigitem(
201 201 b'cmdserver', b'max-log-size', default=b'1 MB',
202 202 )
203 203 coreconfigitem(
204 204 b'cmdserver', b'max-repo-cache', default=0, experimental=True,
205 205 )
206 206 coreconfigitem(
207 207 b'cmdserver', b'message-encodings', default=list,
208 208 )
209 209 coreconfigitem(
210 210 b'cmdserver',
211 211 b'track-log',
212 212 default=lambda: [b'chgserver', b'cmdserver', b'repocache'],
213 213 )
214 214 coreconfigitem(
215 215 b'cmdserver', b'shutdown-on-interrupt', default=True,
216 216 )
217 217 coreconfigitem(
218 218 b'color', b'.*', default=None, generic=True,
219 219 )
220 220 coreconfigitem(
221 221 b'color', b'mode', default=b'auto',
222 222 )
223 223 coreconfigitem(
224 224 b'color', b'pagermode', default=dynamicdefault,
225 225 )
226 226 coreconfigitem(
227 227 b'command-templates',
228 228 b'graphnode',
229 229 default=None,
230 230 alias=[(b'ui', b'graphnodetemplate')],
231 231 )
232 232 coreconfigitem(
233 233 b'command-templates', b'log', default=None, alias=[(b'ui', b'logtemplate')],
234 234 )
235 235 coreconfigitem(
236 236 b'command-templates',
237 237 b'mergemarker',
238 238 default=(
239 239 b'{node|short} '
240 240 b'{ifeq(tags, "tip", "", '
241 241 b'ifeq(tags, "", "", "{tags} "))}'
242 242 b'{if(bookmarks, "{bookmarks} ")}'
243 243 b'{ifeq(branch, "default", "", "{branch} ")}'
244 244 b'- {author|user}: {desc|firstline}'
245 245 ),
246 246 alias=[(b'ui', b'mergemarkertemplate')],
247 247 )
248 248 coreconfigitem(
249 249 b'command-templates',
250 250 b'pre-merge-tool-output',
251 251 default=None,
252 252 alias=[(b'ui', b'pre-merge-tool-output-template')],
253 253 )
254 254 coreconfigitem(
255 255 b'command-templates', b'oneline-summary', default=None,
256 256 )
257 257 coreconfigitem(
258 258 b'command-templates',
259 259 b'oneline-summary.*',
260 260 default=dynamicdefault,
261 261 generic=True,
262 262 )
263 263 _registerdiffopts(section=b'commands', configprefix=b'commit.interactive.')
264 264 coreconfigitem(
265 265 b'commands', b'commit.post-status', default=False,
266 266 )
267 267 coreconfigitem(
268 268 b'commands', b'grep.all-files', default=False, experimental=True,
269 269 )
270 270 coreconfigitem(
271 271 b'commands', b'merge.require-rev', default=False,
272 272 )
273 273 coreconfigitem(
274 274 b'commands', b'push.require-revs', default=False,
275 275 )
276 276 coreconfigitem(
277 277 b'commands', b'resolve.confirm', default=False,
278 278 )
279 279 coreconfigitem(
280 280 b'commands', b'resolve.explicit-re-merge', default=False,
281 281 )
282 282 coreconfigitem(
283 283 b'commands', b'resolve.mark-check', default=b'none',
284 284 )
285 285 _registerdiffopts(section=b'commands', configprefix=b'revert.interactive.')
286 286 coreconfigitem(
287 287 b'commands', b'show.aliasprefix', default=list,
288 288 )
289 289 coreconfigitem(
290 290 b'commands', b'status.relative', default=False,
291 291 )
292 292 coreconfigitem(
293 293 b'commands', b'status.skipstates', default=[], experimental=True,
294 294 )
295 295 coreconfigitem(
296 296 b'commands', b'status.terse', default=b'',
297 297 )
298 298 coreconfigitem(
299 299 b'commands', b'status.verbose', default=False,
300 300 )
301 301 coreconfigitem(
302 302 b'commands', b'update.check', default=None,
303 303 )
304 304 coreconfigitem(
305 305 b'commands', b'update.requiredest', default=False,
306 306 )
307 307 coreconfigitem(
308 308 b'committemplate', b'.*', default=None, generic=True,
309 309 )
310 310 coreconfigitem(
311 311 b'convert', b'bzr.saverev', default=True,
312 312 )
313 313 coreconfigitem(
314 314 b'convert', b'cvsps.cache', default=True,
315 315 )
316 316 coreconfigitem(
317 317 b'convert', b'cvsps.fuzz', default=60,
318 318 )
319 319 coreconfigitem(
320 320 b'convert', b'cvsps.logencoding', default=None,
321 321 )
322 322 coreconfigitem(
323 323 b'convert', b'cvsps.mergefrom', default=None,
324 324 )
325 325 coreconfigitem(
326 326 b'convert', b'cvsps.mergeto', default=None,
327 327 )
328 328 coreconfigitem(
329 329 b'convert', b'git.committeractions', default=lambda: [b'messagedifferent'],
330 330 )
331 331 coreconfigitem(
332 332 b'convert', b'git.extrakeys', default=list,
333 333 )
334 334 coreconfigitem(
335 335 b'convert', b'git.findcopiesharder', default=False,
336 336 )
337 337 coreconfigitem(
338 338 b'convert', b'git.remoteprefix', default=b'remote',
339 339 )
340 340 coreconfigitem(
341 341 b'convert', b'git.renamelimit', default=400,
342 342 )
343 343 coreconfigitem(
344 344 b'convert', b'git.saverev', default=True,
345 345 )
346 346 coreconfigitem(
347 347 b'convert', b'git.similarity', default=50,
348 348 )
349 349 coreconfigitem(
350 350 b'convert', b'git.skipsubmodules', default=False,
351 351 )
352 352 coreconfigitem(
353 353 b'convert', b'hg.clonebranches', default=False,
354 354 )
355 355 coreconfigitem(
356 356 b'convert', b'hg.ignoreerrors', default=False,
357 357 )
358 358 coreconfigitem(
359 359 b'convert', b'hg.preserve-hash', default=False,
360 360 )
361 361 coreconfigitem(
362 362 b'convert', b'hg.revs', default=None,
363 363 )
364 364 coreconfigitem(
365 365 b'convert', b'hg.saverev', default=False,
366 366 )
367 367 coreconfigitem(
368 368 b'convert', b'hg.sourcename', default=None,
369 369 )
370 370 coreconfigitem(
371 371 b'convert', b'hg.startrev', default=None,
372 372 )
373 373 coreconfigitem(
374 374 b'convert', b'hg.tagsbranch', default=b'default',
375 375 )
376 376 coreconfigitem(
377 377 b'convert', b'hg.usebranchnames', default=True,
378 378 )
379 379 coreconfigitem(
380 380 b'convert', b'ignoreancestorcheck', default=False, experimental=True,
381 381 )
382 382 coreconfigitem(
383 383 b'convert', b'localtimezone', default=False,
384 384 )
385 385 coreconfigitem(
386 386 b'convert', b'p4.encoding', default=dynamicdefault,
387 387 )
388 388 coreconfigitem(
389 389 b'convert', b'p4.startrev', default=0,
390 390 )
391 391 coreconfigitem(
392 392 b'convert', b'skiptags', default=False,
393 393 )
394 394 coreconfigitem(
395 395 b'convert', b'svn.debugsvnlog', default=True,
396 396 )
397 397 coreconfigitem(
398 398 b'convert', b'svn.trunk', default=None,
399 399 )
400 400 coreconfigitem(
401 401 b'convert', b'svn.tags', default=None,
402 402 )
403 403 coreconfigitem(
404 404 b'convert', b'svn.branches', default=None,
405 405 )
406 406 coreconfigitem(
407 407 b'convert', b'svn.startrev', default=0,
408 408 )
409 409 coreconfigitem(
410 410 b'debug', b'dirstate.delaywrite', default=0,
411 411 )
412 412 coreconfigitem(
413 413 b'defaults', b'.*', default=None, generic=True,
414 414 )
415 415 coreconfigitem(
416 416 b'devel', b'all-warnings', default=False,
417 417 )
418 418 coreconfigitem(
419 419 b'devel', b'bundle2.debug', default=False,
420 420 )
421 421 coreconfigitem(
422 422 b'devel', b'bundle.delta', default=b'',
423 423 )
424 424 coreconfigitem(
425 425 b'devel', b'cache-vfs', default=None,
426 426 )
427 427 coreconfigitem(
428 428 b'devel', b'check-locks', default=False,
429 429 )
430 430 coreconfigitem(
431 431 b'devel', b'check-relroot', default=False,
432 432 )
433 433 coreconfigitem(
434 434 b'devel', b'default-date', default=None,
435 435 )
436 436 coreconfigitem(
437 437 b'devel', b'deprec-warn', default=False,
438 438 )
439 439 coreconfigitem(
440 440 b'devel', b'disableloaddefaultcerts', default=False,
441 441 )
442 442 coreconfigitem(
443 443 b'devel', b'warn-empty-changegroup', default=False,
444 444 )
445 445 coreconfigitem(
446 446 b'devel', b'legacy.exchange', default=list,
447 447 )
448 448 coreconfigitem(
449 449 b'devel', b'persistent-nodemap', default=False,
450 450 )
451 451 coreconfigitem(
452 452 b'devel', b'servercafile', default=b'',
453 453 )
454 454 coreconfigitem(
455 455 b'devel', b'serverexactprotocol', default=b'',
456 456 )
457 457 coreconfigitem(
458 458 b'devel', b'serverrequirecert', default=False,
459 459 )
460 460 coreconfigitem(
461 461 b'devel', b'strip-obsmarkers', default=True,
462 462 )
463 463 coreconfigitem(
464 464 b'devel', b'warn-config', default=None,
465 465 )
466 466 coreconfigitem(
467 467 b'devel', b'warn-config-default', default=None,
468 468 )
469 469 coreconfigitem(
470 470 b'devel', b'user.obsmarker', default=None,
471 471 )
472 472 coreconfigitem(
473 473 b'devel', b'warn-config-unknown', default=None,
474 474 )
475 475 coreconfigitem(
476 476 b'devel', b'debug.copies', default=False,
477 477 )
478 478 coreconfigitem(
479 479 b'devel', b'debug.extensions', default=False,
480 480 )
481 481 coreconfigitem(
482 482 b'devel', b'debug.repo-filters', default=False,
483 483 )
484 484 coreconfigitem(
485 485 b'devel', b'debug.peer-request', default=False,
486 486 )
487 487 coreconfigitem(
488 488 b'devel', b'discovery.randomize', default=True,
489 489 )
490 490 _registerdiffopts(section=b'diff')
491 491 coreconfigitem(
492 492 b'email', b'bcc', default=None,
493 493 )
494 494 coreconfigitem(
495 495 b'email', b'cc', default=None,
496 496 )
497 497 coreconfigitem(
498 498 b'email', b'charsets', default=list,
499 499 )
500 500 coreconfigitem(
501 501 b'email', b'from', default=None,
502 502 )
503 503 coreconfigitem(
504 504 b'email', b'method', default=b'smtp',
505 505 )
506 506 coreconfigitem(
507 507 b'email', b'reply-to', default=None,
508 508 )
509 509 coreconfigitem(
510 510 b'email', b'to', default=None,
511 511 )
512 512 coreconfigitem(
513 513 b'experimental', b'archivemetatemplate', default=dynamicdefault,
514 514 )
515 515 coreconfigitem(
516 516 b'experimental', b'auto-publish', default=b'publish',
517 517 )
518 518 coreconfigitem(
519 519 b'experimental', b'bundle-phases', default=False,
520 520 )
521 521 coreconfigitem(
522 522 b'experimental', b'bundle2-advertise', default=True,
523 523 )
524 524 coreconfigitem(
525 525 b'experimental', b'bundle2-output-capture', default=False,
526 526 )
527 527 coreconfigitem(
528 528 b'experimental', b'bundle2.pushback', default=False,
529 529 )
530 530 coreconfigitem(
531 531 b'experimental', b'bundle2lazylocking', default=False,
532 532 )
533 533 coreconfigitem(
534 534 b'experimental', b'bundlecomplevel', default=None,
535 535 )
536 536 coreconfigitem(
537 537 b'experimental', b'bundlecomplevel.bzip2', default=None,
538 538 )
539 539 coreconfigitem(
540 540 b'experimental', b'bundlecomplevel.gzip', default=None,
541 541 )
542 542 coreconfigitem(
543 543 b'experimental', b'bundlecomplevel.none', default=None,
544 544 )
545 545 coreconfigitem(
546 546 b'experimental', b'bundlecomplevel.zstd', default=None,
547 547 )
548 548 coreconfigitem(
549 549 b'experimental', b'changegroup3', default=False,
550 550 )
551 551 coreconfigitem(
552 552 b'experimental', b'cleanup-as-archived', default=False,
553 553 )
554 554 coreconfigitem(
555 555 b'experimental', b'clientcompressionengines', default=list,
556 556 )
557 557 coreconfigitem(
558 558 b'experimental', b'copytrace', default=b'on',
559 559 )
560 560 coreconfigitem(
561 561 b'experimental', b'copytrace.movecandidateslimit', default=100,
562 562 )
563 563 coreconfigitem(
564 564 b'experimental', b'copytrace.sourcecommitlimit', default=100,
565 565 )
566 566 coreconfigitem(
567 567 b'experimental', b'copies.read-from', default=b"filelog-only",
568 568 )
569 569 coreconfigitem(
570 570 b'experimental', b'copies.write-to', default=b'filelog-only',
571 571 )
572 572 coreconfigitem(
573 573 b'experimental', b'crecordtest', default=None,
574 574 )
575 575 coreconfigitem(
576 576 b'experimental', b'directaccess', default=False,
577 577 )
578 578 coreconfigitem(
579 579 b'experimental', b'directaccess.revnums', default=False,
580 580 )
581 581 coreconfigitem(
582 582 b'experimental', b'editortmpinhg', default=False,
583 583 )
584 584 coreconfigitem(
585 585 b'experimental', b'evolution', default=list,
586 586 )
587 587 coreconfigitem(
588 588 b'experimental',
589 589 b'evolution.allowdivergence',
590 590 default=False,
591 591 alias=[(b'experimental', b'allowdivergence')],
592 592 )
593 593 coreconfigitem(
594 594 b'experimental', b'evolution.allowunstable', default=None,
595 595 )
596 596 coreconfigitem(
597 597 b'experimental', b'evolution.createmarkers', default=None,
598 598 )
599 599 coreconfigitem(
600 600 b'experimental',
601 601 b'evolution.effect-flags',
602 602 default=True,
603 603 alias=[(b'experimental', b'effect-flags')],
604 604 )
605 605 coreconfigitem(
606 606 b'experimental', b'evolution.exchange', default=None,
607 607 )
608 608 coreconfigitem(
609 609 b'experimental', b'evolution.bundle-obsmarker', default=False,
610 610 )
611 611 coreconfigitem(
612 612 b'experimental', b'log.topo', default=False,
613 613 )
614 614 coreconfigitem(
615 615 b'experimental', b'evolution.report-instabilities', default=True,
616 616 )
617 617 coreconfigitem(
618 618 b'experimental', b'evolution.track-operation', default=True,
619 619 )
620 620 # repo-level config to exclude a revset visibility
621 621 #
622 622 # The target use case is to use `share` to expose different subset of the same
623 623 # repository, especially server side. See also `server.view`.
624 624 coreconfigitem(
625 625 b'experimental', b'extra-filter-revs', default=None,
626 626 )
627 627 coreconfigitem(
628 628 b'experimental', b'maxdeltachainspan', default=-1,
629 629 )
630 630 # tracks files which were undeleted (merge might delete them but we explicitly
631 631 # kept/undeleted them) and creates new filenodes for them
632 632 coreconfigitem(
633 633 b'experimental', b'merge-track-salvaged', default=False,
634 634 )
635 635 coreconfigitem(
636 636 b'experimental', b'mergetempdirprefix', default=None,
637 637 )
638 638 coreconfigitem(
639 639 b'experimental', b'mmapindexthreshold', default=None,
640 640 )
641 641 coreconfigitem(
642 642 b'experimental', b'narrow', default=False,
643 643 )
644 644 coreconfigitem(
645 645 b'experimental', b'nonnormalparanoidcheck', default=False,
646 646 )
647 647 coreconfigitem(
648 648 b'experimental', b'exportableenviron', default=list,
649 649 )
650 650 coreconfigitem(
651 651 b'experimental', b'extendedheader.index', default=None,
652 652 )
653 653 coreconfigitem(
654 654 b'experimental', b'extendedheader.similarity', default=False,
655 655 )
656 656 coreconfigitem(
657 657 b'experimental', b'graphshorten', default=False,
658 658 )
659 659 coreconfigitem(
660 660 b'experimental', b'graphstyle.parent', default=dynamicdefault,
661 661 )
662 662 coreconfigitem(
663 663 b'experimental', b'graphstyle.missing', default=dynamicdefault,
664 664 )
665 665 coreconfigitem(
666 666 b'experimental', b'graphstyle.grandparent', default=dynamicdefault,
667 667 )
668 668 coreconfigitem(
669 669 b'experimental', b'hook-track-tags', default=False,
670 670 )
671 671 coreconfigitem(
672 672 b'experimental', b'httppeer.advertise-v2', default=False,
673 673 )
674 674 coreconfigitem(
675 675 b'experimental', b'httppeer.v2-encoder-order', default=None,
676 676 )
677 677 coreconfigitem(
678 678 b'experimental', b'httppostargs', default=False,
679 679 )
680 680 coreconfigitem(b'experimental', b'nointerrupt', default=False)
681 681 coreconfigitem(b'experimental', b'nointerrupt-interactiveonly', default=True)
682 682
683 683 coreconfigitem(
684 684 b'experimental', b'obsmarkers-exchange-debug', default=False,
685 685 )
686 686 coreconfigitem(
687 687 b'experimental', b'remotenames', default=False,
688 688 )
689 689 coreconfigitem(
690 690 b'experimental', b'removeemptydirs', default=True,
691 691 )
692 692 coreconfigitem(
693 693 b'experimental', b'revert.interactive.select-to-keep', default=False,
694 694 )
695 695 coreconfigitem(
696 696 b'experimental', b'revisions.prefixhexnode', default=False,
697 697 )
698 698 coreconfigitem(
699 699 b'experimental', b'revlogv2', default=None,
700 700 )
701 701 coreconfigitem(
702 702 b'experimental', b'revisions.disambiguatewithin', default=None,
703 703 )
704 704 coreconfigitem(
705 705 b'experimental', b'rust.index', default=False,
706 706 )
707 707 coreconfigitem(
708 708 b'experimental', b'server.filesdata.recommended-batch-size', default=50000,
709 709 )
710 710 coreconfigitem(
711 711 b'experimental',
712 712 b'server.manifestdata.recommended-batch-size',
713 713 default=100000,
714 714 )
715 715 coreconfigitem(
716 716 b'experimental', b'server.stream-narrow-clones', default=False,
717 717 )
718 718 coreconfigitem(
719 719 b'experimental', b'single-head-per-branch', default=False,
720 720 )
721 721 coreconfigitem(
722 722 b'experimental',
723 723 b'single-head-per-branch:account-closed-heads',
724 724 default=False,
725 725 )
726 726 coreconfigitem(
727 727 b'experimental', b'sshserver.support-v2', default=False,
728 728 )
729 729 coreconfigitem(
730 730 b'experimental', b'sparse-read', default=False,
731 731 )
732 732 coreconfigitem(
733 733 b'experimental', b'sparse-read.density-threshold', default=0.50,
734 734 )
735 735 coreconfigitem(
736 736 b'experimental', b'sparse-read.min-gap-size', default=b'65K',
737 737 )
738 738 coreconfigitem(
739 739 b'experimental', b'treemanifest', default=False,
740 740 )
741 741 coreconfigitem(
742 742 b'experimental', b'update.atomic-file', default=False,
743 743 )
744 744 coreconfigitem(
745 745 b'experimental', b'sshpeer.advertise-v2', default=False,
746 746 )
747 747 coreconfigitem(
748 748 b'experimental', b'web.apiserver', default=False,
749 749 )
750 750 coreconfigitem(
751 751 b'experimental', b'web.api.http-v2', default=False,
752 752 )
753 753 coreconfigitem(
754 754 b'experimental', b'web.api.debugreflect', default=False,
755 755 )
756 756 coreconfigitem(
757 757 b'experimental', b'worker.wdir-get-thread-safe', default=False,
758 758 )
759 759 coreconfigitem(
760 760 b'experimental', b'worker.repository-upgrade', default=False,
761 761 )
762 762 coreconfigitem(
763 763 b'experimental', b'xdiff', default=False,
764 764 )
765 765 coreconfigitem(
766 766 b'extensions', b'.*', default=None, generic=True,
767 767 )
768 768 coreconfigitem(
769 769 b'extdata', b'.*', default=None, generic=True,
770 770 )
771 771 coreconfigitem(
772 772 b'format', b'bookmarks-in-store', default=False,
773 773 )
774 774 coreconfigitem(
775 775 b'format', b'chunkcachesize', default=None, experimental=True,
776 776 )
777 777 coreconfigitem(
778 778 b'format', b'dotencode', default=True,
779 779 )
780 780 coreconfigitem(
781 781 b'format', b'generaldelta', default=False, experimental=True,
782 782 )
783 783 coreconfigitem(
784 784 b'format', b'manifestcachesize', default=None, experimental=True,
785 785 )
786 786 coreconfigitem(
787 787 b'format', b'maxchainlen', default=dynamicdefault, experimental=True,
788 788 )
789 789 coreconfigitem(
790 790 b'format', b'obsstore-version', default=None,
791 791 )
792 792 coreconfigitem(
793 793 b'format', b'sparse-revlog', default=True,
794 794 )
795 795 coreconfigitem(
796 796 b'format',
797 797 b'revlog-compression',
798 798 default=lambda: [b'zlib'],
799 799 alias=[(b'experimental', b'format.compression')],
800 800 )
801 801 coreconfigitem(
802 802 b'format', b'usefncache', default=True,
803 803 )
804 804 coreconfigitem(
805 805 b'format', b'usegeneraldelta', default=True,
806 806 )
807 807 coreconfigitem(
808 808 b'format', b'usestore', default=True,
809 809 )
810 810 # Right now, the only efficient implement of the nodemap logic is in Rust, so
811 811 # the persistent nodemap feature needs to stay experimental as long as the Rust
812 812 # extensions are an experimental feature.
813 813 coreconfigitem(
814 814 b'format', b'use-persistent-nodemap', default=False, experimental=True
815 815 )
816 816 coreconfigitem(
817 817 b'format',
818 818 b'exp-use-copies-side-data-changeset',
819 819 default=False,
820 820 experimental=True,
821 821 )
822 822 coreconfigitem(
823 823 b'format', b'exp-use-side-data', default=False, experimental=True,
824 824 )
825 825 coreconfigitem(
826 826 b'format', b'exp-share-safe', default=False, experimental=True,
827 827 )
828 828 coreconfigitem(
829 829 b'format', b'internal-phase', default=False, experimental=True,
830 830 )
831 831 coreconfigitem(
832 832 b'fsmonitor', b'warn_when_unused', default=True,
833 833 )
834 834 coreconfigitem(
835 835 b'fsmonitor', b'warn_update_file_count', default=50000,
836 836 )
837 837 coreconfigitem(
838 838 b'fsmonitor', b'warn_update_file_count_rust', default=400000,
839 839 )
840 840 coreconfigitem(
841 841 b'help', br'hidden-command\..*', default=False, generic=True,
842 842 )
843 843 coreconfigitem(
844 844 b'help', br'hidden-topic\..*', default=False, generic=True,
845 845 )
846 846 coreconfigitem(
847 847 b'hooks', b'.*', default=dynamicdefault, generic=True,
848 848 )
849 849 coreconfigitem(
850 850 b'hgweb-paths', b'.*', default=list, generic=True,
851 851 )
852 852 coreconfigitem(
853 853 b'hostfingerprints', b'.*', default=list, generic=True,
854 854 )
855 855 coreconfigitem(
856 856 b'hostsecurity', b'ciphers', default=None,
857 857 )
858 858 coreconfigitem(
859 859 b'hostsecurity', b'minimumprotocol', default=dynamicdefault,
860 860 )
861 861 coreconfigitem(
862 862 b'hostsecurity',
863 863 b'.*:minimumprotocol$',
864 864 default=dynamicdefault,
865 865 generic=True,
866 866 )
867 867 coreconfigitem(
868 868 b'hostsecurity', b'.*:ciphers$', default=dynamicdefault, generic=True,
869 869 )
870 870 coreconfigitem(
871 871 b'hostsecurity', b'.*:fingerprints$', default=list, generic=True,
872 872 )
873 873 coreconfigitem(
874 874 b'hostsecurity', b'.*:verifycertsfile$', default=None, generic=True,
875 875 )
876 876
877 877 coreconfigitem(
878 878 b'http_proxy', b'always', default=False,
879 879 )
880 880 coreconfigitem(
881 881 b'http_proxy', b'host', default=None,
882 882 )
883 883 coreconfigitem(
884 884 b'http_proxy', b'no', default=list,
885 885 )
886 886 coreconfigitem(
887 887 b'http_proxy', b'passwd', default=None,
888 888 )
889 889 coreconfigitem(
890 890 b'http_proxy', b'user', default=None,
891 891 )
892 892
893 893 coreconfigitem(
894 894 b'http', b'timeout', default=None,
895 895 )
896 896
897 897 coreconfigitem(
898 898 b'logtoprocess', b'commandexception', default=None,
899 899 )
900 900 coreconfigitem(
901 901 b'logtoprocess', b'commandfinish', default=None,
902 902 )
903 903 coreconfigitem(
904 904 b'logtoprocess', b'command', default=None,
905 905 )
906 906 coreconfigitem(
907 907 b'logtoprocess', b'develwarn', default=None,
908 908 )
909 909 coreconfigitem(
910 910 b'logtoprocess', b'uiblocked', default=None,
911 911 )
912 912 coreconfigitem(
913 913 b'merge', b'checkunknown', default=b'abort',
914 914 )
915 915 coreconfigitem(
916 916 b'merge', b'checkignored', default=b'abort',
917 917 )
918 918 coreconfigitem(
919 919 b'experimental', b'merge.checkpathconflicts', default=False,
920 920 )
921 921 coreconfigitem(
922 922 b'merge', b'followcopies', default=True,
923 923 )
924 924 coreconfigitem(
925 925 b'merge', b'on-failure', default=b'continue',
926 926 )
927 927 coreconfigitem(
928 928 b'merge', b'preferancestor', default=lambda: [b'*'], experimental=True,
929 929 )
930 930 coreconfigitem(
931 931 b'merge', b'strict-capability-check', default=False,
932 932 )
933 933 coreconfigitem(
934 934 b'merge-tools', b'.*', default=None, generic=True,
935 935 )
936 936 coreconfigitem(
937 937 b'merge-tools',
938 938 br'.*\.args$',
939 939 default=b"$local $base $other",
940 940 generic=True,
941 941 priority=-1,
942 942 )
943 943 coreconfigitem(
944 944 b'merge-tools', br'.*\.binary$', default=False, generic=True, priority=-1,
945 945 )
946 946 coreconfigitem(
947 947 b'merge-tools', br'.*\.check$', default=list, generic=True, priority=-1,
948 948 )
949 949 coreconfigitem(
950 950 b'merge-tools',
951 951 br'.*\.checkchanged$',
952 952 default=False,
953 953 generic=True,
954 954 priority=-1,
955 955 )
956 956 coreconfigitem(
957 957 b'merge-tools',
958 958 br'.*\.executable$',
959 959 default=dynamicdefault,
960 960 generic=True,
961 961 priority=-1,
962 962 )
963 963 coreconfigitem(
964 964 b'merge-tools', br'.*\.fixeol$', default=False, generic=True, priority=-1,
965 965 )
966 966 coreconfigitem(
967 967 b'merge-tools', br'.*\.gui$', default=False, generic=True, priority=-1,
968 968 )
969 969 coreconfigitem(
970 970 b'merge-tools',
971 971 br'.*\.mergemarkers$',
972 972 default=b'basic',
973 973 generic=True,
974 974 priority=-1,
975 975 )
976 976 coreconfigitem(
977 977 b'merge-tools',
978 978 br'.*\.mergemarkertemplate$',
979 979 default=dynamicdefault, # take from command-templates.mergemarker
980 980 generic=True,
981 981 priority=-1,
982 982 )
983 983 coreconfigitem(
984 984 b'merge-tools', br'.*\.priority$', default=0, generic=True, priority=-1,
985 985 )
986 986 coreconfigitem(
987 987 b'merge-tools',
988 988 br'.*\.premerge$',
989 989 default=dynamicdefault,
990 990 generic=True,
991 991 priority=-1,
992 992 )
993 993 coreconfigitem(
994 994 b'merge-tools', br'.*\.symlink$', default=False, generic=True, priority=-1,
995 995 )
996 996 coreconfigitem(
997 997 b'pager', b'attend-.*', default=dynamicdefault, generic=True,
998 998 )
999 999 coreconfigitem(
1000 1000 b'pager', b'ignore', default=list,
1001 1001 )
1002 1002 coreconfigitem(
1003 1003 b'pager', b'pager', default=dynamicdefault,
1004 1004 )
1005 1005 coreconfigitem(
1006 1006 b'patch', b'eol', default=b'strict',
1007 1007 )
1008 1008 coreconfigitem(
1009 1009 b'patch', b'fuzz', default=2,
1010 1010 )
1011 1011 coreconfigitem(
1012 1012 b'paths', b'default', default=None,
1013 1013 )
1014 1014 coreconfigitem(
1015 1015 b'paths', b'default-push', default=None,
1016 1016 )
1017 1017 coreconfigitem(
1018 1018 b'paths', b'.*', default=None, generic=True,
1019 1019 )
1020 1020 coreconfigitem(
1021 1021 b'phases', b'checksubrepos', default=b'follow',
1022 1022 )
1023 1023 coreconfigitem(
1024 1024 b'phases', b'new-commit', default=b'draft',
1025 1025 )
1026 1026 coreconfigitem(
1027 1027 b'phases', b'publish', default=True,
1028 1028 )
1029 1029 coreconfigitem(
1030 1030 b'profiling', b'enabled', default=False,
1031 1031 )
1032 1032 coreconfigitem(
1033 1033 b'profiling', b'format', default=b'text',
1034 1034 )
1035 1035 coreconfigitem(
1036 1036 b'profiling', b'freq', default=1000,
1037 1037 )
1038 1038 coreconfigitem(
1039 1039 b'profiling', b'limit', default=30,
1040 1040 )
1041 1041 coreconfigitem(
1042 1042 b'profiling', b'nested', default=0,
1043 1043 )
1044 1044 coreconfigitem(
1045 1045 b'profiling', b'output', default=None,
1046 1046 )
1047 1047 coreconfigitem(
1048 1048 b'profiling', b'showmax', default=0.999,
1049 1049 )
1050 1050 coreconfigitem(
1051 1051 b'profiling', b'showmin', default=dynamicdefault,
1052 1052 )
1053 1053 coreconfigitem(
1054 1054 b'profiling', b'showtime', default=True,
1055 1055 )
1056 1056 coreconfigitem(
1057 1057 b'profiling', b'sort', default=b'inlinetime',
1058 1058 )
1059 1059 coreconfigitem(
1060 1060 b'profiling', b'statformat', default=b'hotpath',
1061 1061 )
1062 1062 coreconfigitem(
1063 1063 b'profiling', b'time-track', default=dynamicdefault,
1064 1064 )
1065 1065 coreconfigitem(
1066 1066 b'profiling', b'type', default=b'stat',
1067 1067 )
1068 1068 coreconfigitem(
1069 1069 b'progress', b'assume-tty', default=False,
1070 1070 )
1071 1071 coreconfigitem(
1072 1072 b'progress', b'changedelay', default=1,
1073 1073 )
1074 1074 coreconfigitem(
1075 1075 b'progress', b'clear-complete', default=True,
1076 1076 )
1077 1077 coreconfigitem(
1078 1078 b'progress', b'debug', default=False,
1079 1079 )
1080 1080 coreconfigitem(
1081 1081 b'progress', b'delay', default=3,
1082 1082 )
1083 1083 coreconfigitem(
1084 1084 b'progress', b'disable', default=False,
1085 1085 )
1086 1086 coreconfigitem(
1087 1087 b'progress', b'estimateinterval', default=60.0,
1088 1088 )
1089 1089 coreconfigitem(
1090 1090 b'progress',
1091 1091 b'format',
1092 1092 default=lambda: [b'topic', b'bar', b'number', b'estimate'],
1093 1093 )
1094 1094 coreconfigitem(
1095 1095 b'progress', b'refresh', default=0.1,
1096 1096 )
1097 1097 coreconfigitem(
1098 1098 b'progress', b'width', default=dynamicdefault,
1099 1099 )
1100 1100 coreconfigitem(
1101 1101 b'pull', b'confirm', default=False,
1102 1102 )
1103 1103 coreconfigitem(
1104 1104 b'push', b'pushvars.server', default=False,
1105 1105 )
1106 1106 coreconfigitem(
1107 1107 b'rewrite',
1108 1108 b'backup-bundle',
1109 1109 default=True,
1110 1110 alias=[(b'ui', b'history-editing-backup')],
1111 1111 )
1112 1112 coreconfigitem(
1113 1113 b'rewrite', b'update-timestamp', default=False,
1114 1114 )
1115 1115 coreconfigitem(
1116 1116 b'rewrite', b'empty-successor', default=b'skip', experimental=True,
1117 1117 )
1118 1118 coreconfigitem(
1119 1119 b'storage', b'new-repo-backend', default=b'revlogv1', experimental=True,
1120 1120 )
1121 1121 coreconfigitem(
1122 1122 b'storage',
1123 1123 b'revlog.optimize-delta-parent-choice',
1124 1124 default=True,
1125 1125 alias=[(b'format', b'aggressivemergedeltas')],
1126 1126 )
1127 1127 # experimental as long as rust is experimental (or a C version is implemented)
1128 1128 coreconfigitem(
1129 1129 b'storage', b'revlog.nodemap.mmap', default=True, experimental=True
1130 1130 )
1131 1131 # experimental as long as format.use-persistent-nodemap is.
1132 1132 coreconfigitem(
1133 1133 b'storage', b'revlog.nodemap.mode', default=b'compat', experimental=True
1134 1134 )
1135 1135 coreconfigitem(
1136 1136 b'storage', b'revlog.reuse-external-delta', default=True,
1137 1137 )
1138 1138 coreconfigitem(
1139 1139 b'storage', b'revlog.reuse-external-delta-parent', default=None,
1140 1140 )
1141 1141 coreconfigitem(
1142 1142 b'storage', b'revlog.zlib.level', default=None,
1143 1143 )
1144 1144 coreconfigitem(
1145 1145 b'storage', b'revlog.zstd.level', default=None,
1146 1146 )
1147 1147 coreconfigitem(
1148 1148 b'server', b'bookmarks-pushkey-compat', default=True,
1149 1149 )
1150 1150 coreconfigitem(
1151 1151 b'server', b'bundle1', default=True,
1152 1152 )
1153 1153 coreconfigitem(
1154 1154 b'server', b'bundle1gd', default=None,
1155 1155 )
1156 1156 coreconfigitem(
1157 1157 b'server', b'bundle1.pull', default=None,
1158 1158 )
1159 1159 coreconfigitem(
1160 1160 b'server', b'bundle1gd.pull', default=None,
1161 1161 )
1162 1162 coreconfigitem(
1163 1163 b'server', b'bundle1.push', default=None,
1164 1164 )
1165 1165 coreconfigitem(
1166 1166 b'server', b'bundle1gd.push', default=None,
1167 1167 )
1168 1168 coreconfigitem(
1169 1169 b'server',
1170 1170 b'bundle2.stream',
1171 1171 default=True,
1172 1172 alias=[(b'experimental', b'bundle2.stream')],
1173 1173 )
1174 1174 coreconfigitem(
1175 1175 b'server', b'compressionengines', default=list,
1176 1176 )
1177 1177 coreconfigitem(
1178 1178 b'server', b'concurrent-push-mode', default=b'check-related',
1179 1179 )
1180 1180 coreconfigitem(
1181 1181 b'server', b'disablefullbundle', default=False,
1182 1182 )
1183 1183 coreconfigitem(
1184 1184 b'server', b'maxhttpheaderlen', default=1024,
1185 1185 )
1186 1186 coreconfigitem(
1187 1187 b'server', b'pullbundle', default=False,
1188 1188 )
1189 1189 coreconfigitem(
1190 1190 b'server', b'preferuncompressed', default=False,
1191 1191 )
1192 1192 coreconfigitem(
1193 1193 b'server', b'streamunbundle', default=False,
1194 1194 )
1195 1195 coreconfigitem(
1196 1196 b'server', b'uncompressed', default=True,
1197 1197 )
1198 1198 coreconfigitem(
1199 1199 b'server', b'uncompressedallowsecret', default=False,
1200 1200 )
1201 1201 coreconfigitem(
1202 1202 b'server', b'view', default=b'served',
1203 1203 )
1204 1204 coreconfigitem(
1205 1205 b'server', b'validate', default=False,
1206 1206 )
1207 1207 coreconfigitem(
1208 1208 b'server', b'zliblevel', default=-1,
1209 1209 )
1210 1210 coreconfigitem(
1211 1211 b'server', b'zstdlevel', default=3,
1212 1212 )
1213 1213 coreconfigitem(
1214 1214 b'share', b'pool', default=None,
1215 1215 )
1216 1216 coreconfigitem(
1217 1217 b'share', b'poolnaming', default=b'identity',
1218 1218 )
1219 1219 coreconfigitem(
1220 1220 b'shelve', b'maxbackups', default=10,
1221 1221 )
1222 1222 coreconfigitem(
1223 1223 b'smtp', b'host', default=None,
1224 1224 )
1225 1225 coreconfigitem(
1226 1226 b'smtp', b'local_hostname', default=None,
1227 1227 )
1228 1228 coreconfigitem(
1229 1229 b'smtp', b'password', default=None,
1230 1230 )
1231 1231 coreconfigitem(
1232 1232 b'smtp', b'port', default=dynamicdefault,
1233 1233 )
1234 1234 coreconfigitem(
1235 1235 b'smtp', b'tls', default=b'none',
1236 1236 )
1237 1237 coreconfigitem(
1238 1238 b'smtp', b'username', default=None,
1239 1239 )
1240 1240 coreconfigitem(
1241 1241 b'sparse', b'missingwarning', default=True, experimental=True,
1242 1242 )
1243 1243 coreconfigitem(
1244 1244 b'subrepos',
1245 1245 b'allowed',
1246 1246 default=dynamicdefault, # to make backporting simpler
1247 1247 )
1248 1248 coreconfigitem(
1249 1249 b'subrepos', b'hg:allowed', default=dynamicdefault,
1250 1250 )
1251 1251 coreconfigitem(
1252 1252 b'subrepos', b'git:allowed', default=dynamicdefault,
1253 1253 )
1254 1254 coreconfigitem(
1255 1255 b'subrepos', b'svn:allowed', default=dynamicdefault,
1256 1256 )
1257 1257 coreconfigitem(
1258 1258 b'templates', b'.*', default=None, generic=True,
1259 1259 )
1260 1260 coreconfigitem(
1261 1261 b'templateconfig', b'.*', default=dynamicdefault, generic=True,
1262 1262 )
1263 1263 coreconfigitem(
1264 1264 b'trusted', b'groups', default=list,
1265 1265 )
1266 1266 coreconfigitem(
1267 1267 b'trusted', b'users', default=list,
1268 1268 )
1269 1269 coreconfigitem(
1270 1270 b'ui', b'_usedassubrepo', default=False,
1271 1271 )
1272 1272 coreconfigitem(
1273 1273 b'ui', b'allowemptycommit', default=False,
1274 1274 )
1275 1275 coreconfigitem(
1276 1276 b'ui', b'archivemeta', default=True,
1277 1277 )
1278 1278 coreconfigitem(
1279 1279 b'ui', b'askusername', default=False,
1280 1280 )
1281 1281 coreconfigitem(
1282 1282 b'ui', b'available-memory', default=None,
1283 1283 )
1284 1284
1285 1285 coreconfigitem(
1286 1286 b'ui', b'clonebundlefallback', default=False,
1287 1287 )
1288 1288 coreconfigitem(
1289 1289 b'ui', b'clonebundleprefers', default=list,
1290 1290 )
1291 1291 coreconfigitem(
1292 1292 b'ui', b'clonebundles', default=True,
1293 1293 )
1294 1294 coreconfigitem(
1295 1295 b'ui', b'color', default=b'auto',
1296 1296 )
1297 1297 coreconfigitem(
1298 1298 b'ui', b'commitsubrepos', default=False,
1299 1299 )
1300 1300 coreconfigitem(
1301 1301 b'ui', b'debug', default=False,
1302 1302 )
1303 1303 coreconfigitem(
1304 1304 b'ui', b'debugger', default=None,
1305 1305 )
1306 1306 coreconfigitem(
1307 1307 b'ui', b'editor', default=dynamicdefault,
1308 1308 )
1309 1309 coreconfigitem(
1310 b'ui', b'detailed-exit-code', default=False, experimental=True,
1311 )
1312 coreconfigitem(
1310 1313 b'ui', b'fallbackencoding', default=None,
1311 1314 )
1312 1315 coreconfigitem(
1313 1316 b'ui', b'forcecwd', default=None,
1314 1317 )
1315 1318 coreconfigitem(
1316 1319 b'ui', b'forcemerge', default=None,
1317 1320 )
1318 1321 coreconfigitem(
1319 1322 b'ui', b'formatdebug', default=False,
1320 1323 )
1321 1324 coreconfigitem(
1322 1325 b'ui', b'formatjson', default=False,
1323 1326 )
1324 1327 coreconfigitem(
1325 1328 b'ui', b'formatted', default=None,
1326 1329 )
1327 1330 coreconfigitem(
1328 1331 b'ui', b'interactive', default=None,
1329 1332 )
1330 1333 coreconfigitem(
1331 1334 b'ui', b'interface', default=None,
1332 1335 )
1333 1336 coreconfigitem(
1334 1337 b'ui', b'interface.chunkselector', default=None,
1335 1338 )
1336 1339 coreconfigitem(
1337 1340 b'ui', b'large-file-limit', default=10000000,
1338 1341 )
1339 1342 coreconfigitem(
1340 1343 b'ui', b'logblockedtimes', default=False,
1341 1344 )
1342 1345 coreconfigitem(
1343 1346 b'ui', b'merge', default=None,
1344 1347 )
1345 1348 coreconfigitem(
1346 1349 b'ui', b'mergemarkers', default=b'basic',
1347 1350 )
1348 1351 coreconfigitem(
1349 1352 b'ui', b'message-output', default=b'stdio',
1350 1353 )
1351 1354 coreconfigitem(
1352 1355 b'ui', b'nontty', default=False,
1353 1356 )
1354 1357 coreconfigitem(
1355 1358 b'ui', b'origbackuppath', default=None,
1356 1359 )
1357 1360 coreconfigitem(
1358 1361 b'ui', b'paginate', default=True,
1359 1362 )
1360 1363 coreconfigitem(
1361 1364 b'ui', b'patch', default=None,
1362 1365 )
1363 1366 coreconfigitem(
1364 1367 b'ui', b'portablefilenames', default=b'warn',
1365 1368 )
1366 1369 coreconfigitem(
1367 1370 b'ui', b'promptecho', default=False,
1368 1371 )
1369 1372 coreconfigitem(
1370 1373 b'ui', b'quiet', default=False,
1371 1374 )
1372 1375 coreconfigitem(
1373 1376 b'ui', b'quietbookmarkmove', default=False,
1374 1377 )
1375 1378 coreconfigitem(
1376 1379 b'ui', b'relative-paths', default=b'legacy',
1377 1380 )
1378 1381 coreconfigitem(
1379 1382 b'ui', b'remotecmd', default=b'hg',
1380 1383 )
1381 1384 coreconfigitem(
1382 1385 b'ui', b'report_untrusted', default=True,
1383 1386 )
1384 1387 coreconfigitem(
1385 1388 b'ui', b'rollback', default=True,
1386 1389 )
1387 1390 coreconfigitem(
1388 1391 b'ui', b'signal-safe-lock', default=True,
1389 1392 )
1390 1393 coreconfigitem(
1391 1394 b'ui', b'slash', default=False,
1392 1395 )
1393 1396 coreconfigitem(
1394 1397 b'ui', b'ssh', default=b'ssh',
1395 1398 )
1396 1399 coreconfigitem(
1397 1400 b'ui', b'ssherrorhint', default=None,
1398 1401 )
1399 1402 coreconfigitem(
1400 1403 b'ui', b'statuscopies', default=False,
1401 1404 )
1402 1405 coreconfigitem(
1403 1406 b'ui', b'strict', default=False,
1404 1407 )
1405 1408 coreconfigitem(
1406 1409 b'ui', b'style', default=b'',
1407 1410 )
1408 1411 coreconfigitem(
1409 1412 b'ui', b'supportcontact', default=None,
1410 1413 )
1411 1414 coreconfigitem(
1412 1415 b'ui', b'textwidth', default=78,
1413 1416 )
1414 1417 coreconfigitem(
1415 1418 b'ui', b'timeout', default=b'600',
1416 1419 )
1417 1420 coreconfigitem(
1418 1421 b'ui', b'timeout.warn', default=0,
1419 1422 )
1420 1423 coreconfigitem(
1421 1424 b'ui', b'timestamp-output', default=False,
1422 1425 )
1423 1426 coreconfigitem(
1424 1427 b'ui', b'traceback', default=False,
1425 1428 )
1426 1429 coreconfigitem(
1427 1430 b'ui', b'tweakdefaults', default=False,
1428 1431 )
1429 1432 coreconfigitem(b'ui', b'username', alias=[(b'ui', b'user')])
1430 1433 coreconfigitem(
1431 1434 b'ui', b'verbose', default=False,
1432 1435 )
1433 1436 coreconfigitem(
1434 1437 b'verify', b'skipflags', default=None,
1435 1438 )
1436 1439 coreconfigitem(
1437 1440 b'web', b'allowbz2', default=False,
1438 1441 )
1439 1442 coreconfigitem(
1440 1443 b'web', b'allowgz', default=False,
1441 1444 )
1442 1445 coreconfigitem(
1443 1446 b'web', b'allow-pull', alias=[(b'web', b'allowpull')], default=True,
1444 1447 )
1445 1448 coreconfigitem(
1446 1449 b'web', b'allow-push', alias=[(b'web', b'allow_push')], default=list,
1447 1450 )
1448 1451 coreconfigitem(
1449 1452 b'web', b'allowzip', default=False,
1450 1453 )
1451 1454 coreconfigitem(
1452 1455 b'web', b'archivesubrepos', default=False,
1453 1456 )
1454 1457 coreconfigitem(
1455 1458 b'web', b'cache', default=True,
1456 1459 )
1457 1460 coreconfigitem(
1458 1461 b'web', b'comparisoncontext', default=5,
1459 1462 )
1460 1463 coreconfigitem(
1461 1464 b'web', b'contact', default=None,
1462 1465 )
1463 1466 coreconfigitem(
1464 1467 b'web', b'deny_push', default=list,
1465 1468 )
1466 1469 coreconfigitem(
1467 1470 b'web', b'guessmime', default=False,
1468 1471 )
1469 1472 coreconfigitem(
1470 1473 b'web', b'hidden', default=False,
1471 1474 )
1472 1475 coreconfigitem(
1473 1476 b'web', b'labels', default=list,
1474 1477 )
1475 1478 coreconfigitem(
1476 1479 b'web', b'logoimg', default=b'hglogo.png',
1477 1480 )
1478 1481 coreconfigitem(
1479 1482 b'web', b'logourl', default=b'https://mercurial-scm.org/',
1480 1483 )
1481 1484 coreconfigitem(
1482 1485 b'web', b'accesslog', default=b'-',
1483 1486 )
1484 1487 coreconfigitem(
1485 1488 b'web', b'address', default=b'',
1486 1489 )
1487 1490 coreconfigitem(
1488 1491 b'web', b'allow-archive', alias=[(b'web', b'allow_archive')], default=list,
1489 1492 )
1490 1493 coreconfigitem(
1491 1494 b'web', b'allow_read', default=list,
1492 1495 )
1493 1496 coreconfigitem(
1494 1497 b'web', b'baseurl', default=None,
1495 1498 )
1496 1499 coreconfigitem(
1497 1500 b'web', b'cacerts', default=None,
1498 1501 )
1499 1502 coreconfigitem(
1500 1503 b'web', b'certificate', default=None,
1501 1504 )
1502 1505 coreconfigitem(
1503 1506 b'web', b'collapse', default=False,
1504 1507 )
1505 1508 coreconfigitem(
1506 1509 b'web', b'csp', default=None,
1507 1510 )
1508 1511 coreconfigitem(
1509 1512 b'web', b'deny_read', default=list,
1510 1513 )
1511 1514 coreconfigitem(
1512 1515 b'web', b'descend', default=True,
1513 1516 )
1514 1517 coreconfigitem(
1515 1518 b'web', b'description', default=b"",
1516 1519 )
1517 1520 coreconfigitem(
1518 1521 b'web', b'encoding', default=lambda: encoding.encoding,
1519 1522 )
1520 1523 coreconfigitem(
1521 1524 b'web', b'errorlog', default=b'-',
1522 1525 )
1523 1526 coreconfigitem(
1524 1527 b'web', b'ipv6', default=False,
1525 1528 )
1526 1529 coreconfigitem(
1527 1530 b'web', b'maxchanges', default=10,
1528 1531 )
1529 1532 coreconfigitem(
1530 1533 b'web', b'maxfiles', default=10,
1531 1534 )
1532 1535 coreconfigitem(
1533 1536 b'web', b'maxshortchanges', default=60,
1534 1537 )
1535 1538 coreconfigitem(
1536 1539 b'web', b'motd', default=b'',
1537 1540 )
1538 1541 coreconfigitem(
1539 1542 b'web', b'name', default=dynamicdefault,
1540 1543 )
1541 1544 coreconfigitem(
1542 1545 b'web', b'port', default=8000,
1543 1546 )
1544 1547 coreconfigitem(
1545 1548 b'web', b'prefix', default=b'',
1546 1549 )
1547 1550 coreconfigitem(
1548 1551 b'web', b'push_ssl', default=True,
1549 1552 )
1550 1553 coreconfigitem(
1551 1554 b'web', b'refreshinterval', default=20,
1552 1555 )
1553 1556 coreconfigitem(
1554 1557 b'web', b'server-header', default=None,
1555 1558 )
1556 1559 coreconfigitem(
1557 1560 b'web', b'static', default=None,
1558 1561 )
1559 1562 coreconfigitem(
1560 1563 b'web', b'staticurl', default=None,
1561 1564 )
1562 1565 coreconfigitem(
1563 1566 b'web', b'stripes', default=1,
1564 1567 )
1565 1568 coreconfigitem(
1566 1569 b'web', b'style', default=b'paper',
1567 1570 )
1568 1571 coreconfigitem(
1569 1572 b'web', b'templates', default=None,
1570 1573 )
1571 1574 coreconfigitem(
1572 1575 b'web', b'view', default=b'served', experimental=True,
1573 1576 )
1574 1577 coreconfigitem(
1575 1578 b'worker', b'backgroundclose', default=dynamicdefault,
1576 1579 )
1577 1580 # Windows defaults to a limit of 512 open files. A buffer of 128
1578 1581 # should give us enough headway.
1579 1582 coreconfigitem(
1580 1583 b'worker', b'backgroundclosemaxqueue', default=384,
1581 1584 )
1582 1585 coreconfigitem(
1583 1586 b'worker', b'backgroundcloseminfilecount', default=2048,
1584 1587 )
1585 1588 coreconfigitem(
1586 1589 b'worker', b'backgroundclosethreadcount', default=4,
1587 1590 )
1588 1591 coreconfigitem(
1589 1592 b'worker', b'enabled', default=True,
1590 1593 )
1591 1594 coreconfigitem(
1592 1595 b'worker', b'numcpus', default=None,
1593 1596 )
1594 1597
1595 1598 # Rebase related configuration moved to core because other extension are doing
1596 1599 # strange things. For example, shelve import the extensions to reuse some bit
1597 1600 # without formally loading it.
1598 1601 coreconfigitem(
1599 1602 b'commands', b'rebase.requiredest', default=False,
1600 1603 )
1601 1604 coreconfigitem(
1602 1605 b'experimental', b'rebaseskipobsolete', default=True,
1603 1606 )
1604 1607 coreconfigitem(
1605 1608 b'rebase', b'singletransaction', default=False,
1606 1609 )
1607 1610 coreconfigitem(
1608 1611 b'rebase', b'experimental.inmemory', default=False,
1609 1612 )
@@ -1,2290 +1,2297 b''
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com>
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 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import glob
12 12 import os
13 13 import posixpath
14 14 import re
15 15 import subprocess
16 16 import weakref
17 17
18 18 from .i18n import _
19 19 from .node import (
20 20 bin,
21 21 hex,
22 22 nullid,
23 23 nullrev,
24 24 short,
25 25 wdirid,
26 26 wdirrev,
27 27 )
28 28 from .pycompat import getattr
29 29 from .thirdparty import attr
30 30 from . import (
31 31 copies as copiesmod,
32 32 encoding,
33 33 error,
34 34 match as matchmod,
35 35 obsolete,
36 36 obsutil,
37 37 pathutil,
38 38 phases,
39 39 policy,
40 40 pycompat,
41 41 requirements as requirementsmod,
42 42 revsetlang,
43 43 similar,
44 44 smartset,
45 45 url,
46 46 util,
47 47 vfs,
48 48 )
49 49
50 50 from .utils import (
51 51 hashutil,
52 52 procutil,
53 53 stringutil,
54 54 )
55 55
56 56 if pycompat.iswindows:
57 57 from . import scmwindows as scmplatform
58 58 else:
59 59 from . import scmposix as scmplatform
60 60
61 61 parsers = policy.importmod('parsers')
62 62 rustrevlog = policy.importrust('revlog')
63 63
64 64 termsize = scmplatform.termsize
65 65
66 66
67 67 @attr.s(slots=True, repr=False)
68 68 class status(object):
69 69 '''Struct with a list of files per status.
70 70
71 71 The 'deleted', 'unknown' and 'ignored' properties are only
72 72 relevant to the working copy.
73 73 '''
74 74
75 75 modified = attr.ib(default=attr.Factory(list))
76 76 added = attr.ib(default=attr.Factory(list))
77 77 removed = attr.ib(default=attr.Factory(list))
78 78 deleted = attr.ib(default=attr.Factory(list))
79 79 unknown = attr.ib(default=attr.Factory(list))
80 80 ignored = attr.ib(default=attr.Factory(list))
81 81 clean = attr.ib(default=attr.Factory(list))
82 82
83 83 def __iter__(self):
84 84 yield self.modified
85 85 yield self.added
86 86 yield self.removed
87 87 yield self.deleted
88 88 yield self.unknown
89 89 yield self.ignored
90 90 yield self.clean
91 91
92 92 def __repr__(self):
93 93 return (
94 94 r'<status modified=%s, added=%s, removed=%s, deleted=%s, '
95 95 r'unknown=%s, ignored=%s, clean=%s>'
96 96 ) % tuple(pycompat.sysstr(stringutil.pprint(v)) for v in self)
97 97
98 98
99 99 def itersubrepos(ctx1, ctx2):
100 100 """find subrepos in ctx1 or ctx2"""
101 101 # Create a (subpath, ctx) mapping where we prefer subpaths from
102 102 # ctx1. The subpaths from ctx2 are important when the .hgsub file
103 103 # has been modified (in ctx2) but not yet committed (in ctx1).
104 104 subpaths = dict.fromkeys(ctx2.substate, ctx2)
105 105 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
106 106
107 107 missing = set()
108 108
109 109 for subpath in ctx2.substate:
110 110 if subpath not in ctx1.substate:
111 111 del subpaths[subpath]
112 112 missing.add(subpath)
113 113
114 114 for subpath, ctx in sorted(pycompat.iteritems(subpaths)):
115 115 yield subpath, ctx.sub(subpath)
116 116
117 117 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
118 118 # status and diff will have an accurate result when it does
119 119 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
120 120 # against itself.
121 121 for subpath in missing:
122 122 yield subpath, ctx2.nullsub(subpath, ctx1)
123 123
124 124
125 125 def nochangesfound(ui, repo, excluded=None):
126 126 '''Report no changes for push/pull, excluded is None or a list of
127 127 nodes excluded from the push/pull.
128 128 '''
129 129 secretlist = []
130 130 if excluded:
131 131 for n in excluded:
132 132 ctx = repo[n]
133 133 if ctx.phase() >= phases.secret and not ctx.extinct():
134 134 secretlist.append(n)
135 135
136 136 if secretlist:
137 137 ui.status(
138 138 _(b"no changes found (ignored %d secret changesets)\n")
139 139 % len(secretlist)
140 140 )
141 141 else:
142 142 ui.status(_(b"no changes found\n"))
143 143
144 144
145 145 def callcatch(ui, func):
146 146 """call func() with global exception handling
147 147
148 148 return func() if no exception happens. otherwise do some error handling
149 149 and return an exit code accordingly. does not handle all exceptions.
150 150 """
151 coarse_exit_code = -1
152 detailed_exit_code = -1
151 153 try:
152 154 try:
153 155 return func()
154 156 except: # re-raises
155 157 ui.traceback()
156 158 raise
157 159 # Global exception handling, alphabetically
158 160 # Mercurial-specific first, followed by built-in and library exceptions
159 161 except error.LockHeld as inst:
160 162 if inst.errno == errno.ETIMEDOUT:
161 163 reason = _(b'timed out waiting for lock held by %r') % (
162 164 pycompat.bytestr(inst.locker)
163 165 )
164 166 else:
165 167 reason = _(b'lock held by %r') % inst.locker
166 168 ui.error(
167 169 _(b"abort: %s: %s\n")
168 170 % (inst.desc or stringutil.forcebytestr(inst.filename), reason)
169 171 )
170 172 if not inst.locker:
171 173 ui.error(_(b"(lock might be very busy)\n"))
172 174 except error.LockUnavailable as inst:
173 175 ui.error(
174 176 _(b"abort: could not lock %s: %s\n")
175 177 % (
176 178 inst.desc or stringutil.forcebytestr(inst.filename),
177 179 encoding.strtolocal(inst.strerror),
178 180 )
179 181 )
180 182 except error.OutOfBandError as inst:
181 183 if inst.args:
182 184 msg = _(b"abort: remote error:\n")
183 185 else:
184 186 msg = _(b"abort: remote error\n")
185 187 ui.error(msg)
186 188 if inst.args:
187 189 ui.error(b''.join(inst.args))
188 190 if inst.hint:
189 191 ui.error(b'(%s)\n' % inst.hint)
190 192 except error.RepoError as inst:
191 193 ui.error(_(b"abort: %s!\n") % inst)
192 194 if inst.hint:
193 195 ui.error(_(b"(%s)\n") % inst.hint)
194 196 except error.ResponseError as inst:
195 197 ui.error(_(b"abort: %s") % inst.args[0])
196 198 msg = inst.args[1]
197 199 if isinstance(msg, type(u'')):
198 200 msg = pycompat.sysbytes(msg)
199 201 if not isinstance(msg, bytes):
200 202 ui.error(b" %r\n" % (msg,))
201 203 elif not msg:
202 204 ui.error(_(b" empty string\n"))
203 205 else:
204 206 ui.error(b"\n%r\n" % pycompat.bytestr(stringutil.ellipsis(msg)))
205 207 except error.CensoredNodeError as inst:
206 208 ui.error(_(b"abort: file censored %s!\n") % inst)
207 209 except error.StorageError as inst:
208 210 ui.error(_(b"abort: %s!\n") % inst)
209 211 if inst.hint:
210 212 ui.error(_(b"(%s)\n") % inst.hint)
211 213 except error.InterventionRequired as inst:
212 214 ui.error(b"%s\n" % inst)
213 215 if inst.hint:
214 216 ui.error(_(b"(%s)\n") % inst.hint)
215 return 1
217 detailed_exit_code = 240
218 coarse_exit_code = 1
216 219 except error.WdirUnsupported:
217 220 ui.error(_(b"abort: working directory revision cannot be specified\n"))
218 221 except error.Abort as inst:
219 222 ui.error(_(b"abort: %s\n") % inst.message)
220 223 if inst.hint:
221 224 ui.error(_(b"(%s)\n") % inst.hint)
222 225 except error.WorkerError as inst:
223 226 # Don't print a message -- the worker already should have
224 227 return inst.status_code
225 228 except ImportError as inst:
226 229 ui.error(_(b"abort: %s!\n") % stringutil.forcebytestr(inst))
227 230 m = stringutil.forcebytestr(inst).split()[-1]
228 231 if m in b"mpatch bdiff".split():
229 232 ui.error(_(b"(did you forget to compile extensions?)\n"))
230 233 elif m in b"zlib".split():
231 234 ui.error(_(b"(is your Python install correct?)\n"))
232 235 except (IOError, OSError) as inst:
233 236 if util.safehasattr(inst, b"code"): # HTTPError
234 237 ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
235 238 elif util.safehasattr(inst, b"reason"): # URLError or SSLError
236 239 try: # usually it is in the form (errno, strerror)
237 240 reason = inst.reason.args[1]
238 241 except (AttributeError, IndexError):
239 242 # it might be anything, for example a string
240 243 reason = inst.reason
241 244 if isinstance(reason, pycompat.unicode):
242 245 # SSLError of Python 2.7.9 contains a unicode
243 246 reason = encoding.unitolocal(reason)
244 247 ui.error(_(b"abort: error: %s\n") % stringutil.forcebytestr(reason))
245 248 elif (
246 249 util.safehasattr(inst, b"args")
247 250 and inst.args
248 251 and inst.args[0] == errno.EPIPE
249 252 ):
250 253 pass
251 254 elif getattr(inst, "strerror", None): # common IOError or OSError
252 255 if getattr(inst, "filename", None) is not None:
253 256 ui.error(
254 257 _(b"abort: %s: '%s'\n")
255 258 % (
256 259 encoding.strtolocal(inst.strerror),
257 260 stringutil.forcebytestr(inst.filename),
258 261 )
259 262 )
260 263 else:
261 264 ui.error(_(b"abort: %s\n") % encoding.strtolocal(inst.strerror))
262 265 else: # suspicious IOError
263 266 raise
264 267 except MemoryError:
265 268 ui.error(_(b"abort: out of memory\n"))
266 269 except SystemExit as inst:
267 270 # Commands shouldn't sys.exit directly, but give a return code.
268 271 # Just in case catch this and and pass exit code to caller.
269 return inst.code
272 detailed_exit_code = 254
273 coarse_exit_code = inst.code
270 274
271 return -1
275 if ui.configbool(b'ui', b'detailed-exit-code'):
276 return detailed_exit_code
277 else:
278 return coarse_exit_code
272 279
273 280
274 281 def checknewlabel(repo, lbl, kind):
275 282 # Do not use the "kind" parameter in ui output.
276 283 # It makes strings difficult to translate.
277 284 if lbl in [b'tip', b'.', b'null']:
278 285 raise error.Abort(_(b"the name '%s' is reserved") % lbl)
279 286 for c in (b':', b'\0', b'\n', b'\r'):
280 287 if c in lbl:
281 288 raise error.Abort(
282 289 _(b"%r cannot be used in a name") % pycompat.bytestr(c)
283 290 )
284 291 try:
285 292 int(lbl)
286 293 raise error.Abort(_(b"cannot use an integer as a name"))
287 294 except ValueError:
288 295 pass
289 296 if lbl.strip() != lbl:
290 297 raise error.Abort(_(b"leading or trailing whitespace in name %r") % lbl)
291 298
292 299
293 300 def checkfilename(f):
294 301 '''Check that the filename f is an acceptable filename for a tracked file'''
295 302 if b'\r' in f or b'\n' in f:
296 303 raise error.Abort(
297 304 _(b"'\\n' and '\\r' disallowed in filenames: %r")
298 305 % pycompat.bytestr(f)
299 306 )
300 307
301 308
302 309 def checkportable(ui, f):
303 310 '''Check if filename f is portable and warn or abort depending on config'''
304 311 checkfilename(f)
305 312 abort, warn = checkportabilityalert(ui)
306 313 if abort or warn:
307 314 msg = util.checkwinfilename(f)
308 315 if msg:
309 316 msg = b"%s: %s" % (msg, procutil.shellquote(f))
310 317 if abort:
311 318 raise error.Abort(msg)
312 319 ui.warn(_(b"warning: %s\n") % msg)
313 320
314 321
315 322 def checkportabilityalert(ui):
316 323 '''check if the user's config requests nothing, a warning, or abort for
317 324 non-portable filenames'''
318 325 val = ui.config(b'ui', b'portablefilenames')
319 326 lval = val.lower()
320 327 bval = stringutil.parsebool(val)
321 328 abort = pycompat.iswindows or lval == b'abort'
322 329 warn = bval or lval == b'warn'
323 330 if bval is None and not (warn or abort or lval == b'ignore'):
324 331 raise error.ConfigError(
325 332 _(b"ui.portablefilenames value is invalid ('%s')") % val
326 333 )
327 334 return abort, warn
328 335
329 336
330 337 class casecollisionauditor(object):
331 338 def __init__(self, ui, abort, dirstate):
332 339 self._ui = ui
333 340 self._abort = abort
334 341 allfiles = b'\0'.join(dirstate)
335 342 self._loweredfiles = set(encoding.lower(allfiles).split(b'\0'))
336 343 self._dirstate = dirstate
337 344 # The purpose of _newfiles is so that we don't complain about
338 345 # case collisions if someone were to call this object with the
339 346 # same filename twice.
340 347 self._newfiles = set()
341 348
342 349 def __call__(self, f):
343 350 if f in self._newfiles:
344 351 return
345 352 fl = encoding.lower(f)
346 353 if fl in self._loweredfiles and f not in self._dirstate:
347 354 msg = _(b'possible case-folding collision for %s') % f
348 355 if self._abort:
349 356 raise error.Abort(msg)
350 357 self._ui.warn(_(b"warning: %s\n") % msg)
351 358 self._loweredfiles.add(fl)
352 359 self._newfiles.add(f)
353 360
354 361
355 362 def filteredhash(repo, maxrev):
356 363 """build hash of filtered revisions in the current repoview.
357 364
358 365 Multiple caches perform up-to-date validation by checking that the
359 366 tiprev and tipnode stored in the cache file match the current repository.
360 367 However, this is not sufficient for validating repoviews because the set
361 368 of revisions in the view may change without the repository tiprev and
362 369 tipnode changing.
363 370
364 371 This function hashes all the revs filtered from the view and returns
365 372 that SHA-1 digest.
366 373 """
367 374 cl = repo.changelog
368 375 if not cl.filteredrevs:
369 376 return None
370 377 key = cl._filteredrevs_hashcache.get(maxrev)
371 378 if not key:
372 379 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
373 380 if revs:
374 381 s = hashutil.sha1()
375 382 for rev in revs:
376 383 s.update(b'%d;' % rev)
377 384 key = s.digest()
378 385 cl._filteredrevs_hashcache[maxrev] = key
379 386 return key
380 387
381 388
382 389 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
383 390 '''yield every hg repository under path, always recursively.
384 391 The recurse flag will only control recursion into repo working dirs'''
385 392
386 393 def errhandler(err):
387 394 if err.filename == path:
388 395 raise err
389 396
390 397 samestat = getattr(os.path, 'samestat', None)
391 398 if followsym and samestat is not None:
392 399
393 400 def adddir(dirlst, dirname):
394 401 dirstat = os.stat(dirname)
395 402 match = any(samestat(dirstat, lstdirstat) for lstdirstat in dirlst)
396 403 if not match:
397 404 dirlst.append(dirstat)
398 405 return not match
399 406
400 407 else:
401 408 followsym = False
402 409
403 410 if (seen_dirs is None) and followsym:
404 411 seen_dirs = []
405 412 adddir(seen_dirs, path)
406 413 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
407 414 dirs.sort()
408 415 if b'.hg' in dirs:
409 416 yield root # found a repository
410 417 qroot = os.path.join(root, b'.hg', b'patches')
411 418 if os.path.isdir(os.path.join(qroot, b'.hg')):
412 419 yield qroot # we have a patch queue repo here
413 420 if recurse:
414 421 # avoid recursing inside the .hg directory
415 422 dirs.remove(b'.hg')
416 423 else:
417 424 dirs[:] = [] # don't descend further
418 425 elif followsym:
419 426 newdirs = []
420 427 for d in dirs:
421 428 fname = os.path.join(root, d)
422 429 if adddir(seen_dirs, fname):
423 430 if os.path.islink(fname):
424 431 for hgname in walkrepos(fname, True, seen_dirs):
425 432 yield hgname
426 433 else:
427 434 newdirs.append(d)
428 435 dirs[:] = newdirs
429 436
430 437
431 438 def binnode(ctx):
432 439 """Return binary node id for a given basectx"""
433 440 node = ctx.node()
434 441 if node is None:
435 442 return wdirid
436 443 return node
437 444
438 445
439 446 def intrev(ctx):
440 447 """Return integer for a given basectx that can be used in comparison or
441 448 arithmetic operation"""
442 449 rev = ctx.rev()
443 450 if rev is None:
444 451 return wdirrev
445 452 return rev
446 453
447 454
448 455 def formatchangeid(ctx):
449 456 """Format changectx as '{rev}:{node|formatnode}', which is the default
450 457 template provided by logcmdutil.changesettemplater"""
451 458 repo = ctx.repo()
452 459 return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))
453 460
454 461
455 462 def formatrevnode(ui, rev, node):
456 463 """Format given revision and node depending on the current verbosity"""
457 464 if ui.debugflag:
458 465 hexfunc = hex
459 466 else:
460 467 hexfunc = short
461 468 return b'%d:%s' % (rev, hexfunc(node))
462 469
463 470
464 471 def resolvehexnodeidprefix(repo, prefix):
465 472 if prefix.startswith(b'x'):
466 473 prefix = prefix[1:]
467 474 try:
468 475 # Uses unfiltered repo because it's faster when prefix is ambiguous/
469 476 # This matches the shortesthexnodeidprefix() function below.
470 477 node = repo.unfiltered().changelog._partialmatch(prefix)
471 478 except error.AmbiguousPrefixLookupError:
472 479 revset = repo.ui.config(
473 480 b'experimental', b'revisions.disambiguatewithin'
474 481 )
475 482 if revset:
476 483 # Clear config to avoid infinite recursion
477 484 configoverrides = {
478 485 (b'experimental', b'revisions.disambiguatewithin'): None
479 486 }
480 487 with repo.ui.configoverride(configoverrides):
481 488 revs = repo.anyrevs([revset], user=True)
482 489 matches = []
483 490 for rev in revs:
484 491 node = repo.changelog.node(rev)
485 492 if hex(node).startswith(prefix):
486 493 matches.append(node)
487 494 if len(matches) == 1:
488 495 return matches[0]
489 496 raise
490 497 if node is None:
491 498 return
492 499 repo.changelog.rev(node) # make sure node isn't filtered
493 500 return node
494 501
495 502
496 503 def mayberevnum(repo, prefix):
497 504 """Checks if the given prefix may be mistaken for a revision number"""
498 505 try:
499 506 i = int(prefix)
500 507 # if we are a pure int, then starting with zero will not be
501 508 # confused as a rev; or, obviously, if the int is larger
502 509 # than the value of the tip rev. We still need to disambiguate if
503 510 # prefix == '0', since that *is* a valid revnum.
504 511 if (prefix != b'0' and prefix[0:1] == b'0') or i >= len(repo):
505 512 return False
506 513 return True
507 514 except ValueError:
508 515 return False
509 516
510 517
511 518 def shortesthexnodeidprefix(repo, node, minlength=1, cache=None):
512 519 """Find the shortest unambiguous prefix that matches hexnode.
513 520
514 521 If "cache" is not None, it must be a dictionary that can be used for
515 522 caching between calls to this method.
516 523 """
517 524 # _partialmatch() of filtered changelog could take O(len(repo)) time,
518 525 # which would be unacceptably slow. so we look for hash collision in
519 526 # unfiltered space, which means some hashes may be slightly longer.
520 527
521 528 minlength = max(minlength, 1)
522 529
523 530 def disambiguate(prefix):
524 531 """Disambiguate against revnums."""
525 532 if repo.ui.configbool(b'experimental', b'revisions.prefixhexnode'):
526 533 if mayberevnum(repo, prefix):
527 534 return b'x' + prefix
528 535 else:
529 536 return prefix
530 537
531 538 hexnode = hex(node)
532 539 for length in range(len(prefix), len(hexnode) + 1):
533 540 prefix = hexnode[:length]
534 541 if not mayberevnum(repo, prefix):
535 542 return prefix
536 543
537 544 cl = repo.unfiltered().changelog
538 545 revset = repo.ui.config(b'experimental', b'revisions.disambiguatewithin')
539 546 if revset:
540 547 revs = None
541 548 if cache is not None:
542 549 revs = cache.get(b'disambiguationrevset')
543 550 if revs is None:
544 551 revs = repo.anyrevs([revset], user=True)
545 552 if cache is not None:
546 553 cache[b'disambiguationrevset'] = revs
547 554 if cl.rev(node) in revs:
548 555 hexnode = hex(node)
549 556 nodetree = None
550 557 if cache is not None:
551 558 nodetree = cache.get(b'disambiguationnodetree')
552 559 if not nodetree:
553 560 if util.safehasattr(parsers, 'nodetree'):
554 561 # The CExt is the only implementation to provide a nodetree
555 562 # class so far.
556 563 index = cl.index
557 564 if util.safehasattr(index, 'get_cindex'):
558 565 # the rust wrapped need to give access to its internal index
559 566 index = index.get_cindex()
560 567 nodetree = parsers.nodetree(index, len(revs))
561 568 for r in revs:
562 569 nodetree.insert(r)
563 570 if cache is not None:
564 571 cache[b'disambiguationnodetree'] = nodetree
565 572 if nodetree is not None:
566 573 length = max(nodetree.shortest(node), minlength)
567 574 prefix = hexnode[:length]
568 575 return disambiguate(prefix)
569 576 for length in range(minlength, len(hexnode) + 1):
570 577 matches = []
571 578 prefix = hexnode[:length]
572 579 for rev in revs:
573 580 otherhexnode = repo[rev].hex()
574 581 if prefix == otherhexnode[:length]:
575 582 matches.append(otherhexnode)
576 583 if len(matches) == 1:
577 584 return disambiguate(prefix)
578 585
579 586 try:
580 587 return disambiguate(cl.shortest(node, minlength))
581 588 except error.LookupError:
582 589 raise error.RepoLookupError()
583 590
584 591
585 592 def isrevsymbol(repo, symbol):
586 593 """Checks if a symbol exists in the repo.
587 594
588 595 See revsymbol() for details. Raises error.AmbiguousPrefixLookupError if the
589 596 symbol is an ambiguous nodeid prefix.
590 597 """
591 598 try:
592 599 revsymbol(repo, symbol)
593 600 return True
594 601 except error.RepoLookupError:
595 602 return False
596 603
597 604
598 605 def revsymbol(repo, symbol):
599 606 """Returns a context given a single revision symbol (as string).
600 607
601 608 This is similar to revsingle(), but accepts only a single revision symbol,
602 609 i.e. things like ".", "tip", "1234", "deadbeef", "my-bookmark" work, but
603 610 not "max(public())".
604 611 """
605 612 if not isinstance(symbol, bytes):
606 613 msg = (
607 614 b"symbol (%s of type %s) was not a string, did you mean "
608 615 b"repo[symbol]?" % (symbol, type(symbol))
609 616 )
610 617 raise error.ProgrammingError(msg)
611 618 try:
612 619 if symbol in (b'.', b'tip', b'null'):
613 620 return repo[symbol]
614 621
615 622 try:
616 623 r = int(symbol)
617 624 if b'%d' % r != symbol:
618 625 raise ValueError
619 626 l = len(repo.changelog)
620 627 if r < 0:
621 628 r += l
622 629 if r < 0 or r >= l and r != wdirrev:
623 630 raise ValueError
624 631 return repo[r]
625 632 except error.FilteredIndexError:
626 633 raise
627 634 except (ValueError, OverflowError, IndexError):
628 635 pass
629 636
630 637 if len(symbol) == 40:
631 638 try:
632 639 node = bin(symbol)
633 640 rev = repo.changelog.rev(node)
634 641 return repo[rev]
635 642 except error.FilteredLookupError:
636 643 raise
637 644 except (TypeError, LookupError):
638 645 pass
639 646
640 647 # look up bookmarks through the name interface
641 648 try:
642 649 node = repo.names.singlenode(repo, symbol)
643 650 rev = repo.changelog.rev(node)
644 651 return repo[rev]
645 652 except KeyError:
646 653 pass
647 654
648 655 node = resolvehexnodeidprefix(repo, symbol)
649 656 if node is not None:
650 657 rev = repo.changelog.rev(node)
651 658 return repo[rev]
652 659
653 660 raise error.RepoLookupError(_(b"unknown revision '%s'") % symbol)
654 661
655 662 except error.WdirUnsupported:
656 663 return repo[None]
657 664 except (
658 665 error.FilteredIndexError,
659 666 error.FilteredLookupError,
660 667 error.FilteredRepoLookupError,
661 668 ):
662 669 raise _filterederror(repo, symbol)
663 670
664 671
665 672 def _filterederror(repo, changeid):
666 673 """build an exception to be raised about a filtered changeid
667 674
668 675 This is extracted in a function to help extensions (eg: evolve) to
669 676 experiment with various message variants."""
670 677 if repo.filtername.startswith(b'visible'):
671 678
672 679 # Check if the changeset is obsolete
673 680 unfilteredrepo = repo.unfiltered()
674 681 ctx = revsymbol(unfilteredrepo, changeid)
675 682
676 683 # If the changeset is obsolete, enrich the message with the reason
677 684 # that made this changeset not visible
678 685 if ctx.obsolete():
679 686 msg = obsutil._getfilteredreason(repo, changeid, ctx)
680 687 else:
681 688 msg = _(b"hidden revision '%s'") % changeid
682 689
683 690 hint = _(b'use --hidden to access hidden revisions')
684 691
685 692 return error.FilteredRepoLookupError(msg, hint=hint)
686 693 msg = _(b"filtered revision '%s' (not in '%s' subset)")
687 694 msg %= (changeid, repo.filtername)
688 695 return error.FilteredRepoLookupError(msg)
689 696
690 697
691 698 def revsingle(repo, revspec, default=b'.', localalias=None):
692 699 if not revspec and revspec != 0:
693 700 return repo[default]
694 701
695 702 l = revrange(repo, [revspec], localalias=localalias)
696 703 if not l:
697 704 raise error.Abort(_(b'empty revision set'))
698 705 return repo[l.last()]
699 706
700 707
701 708 def _pairspec(revspec):
702 709 tree = revsetlang.parse(revspec)
703 710 return tree and tree[0] in (
704 711 b'range',
705 712 b'rangepre',
706 713 b'rangepost',
707 714 b'rangeall',
708 715 )
709 716
710 717
711 718 def revpair(repo, revs):
712 719 if not revs:
713 720 return repo[b'.'], repo[None]
714 721
715 722 l = revrange(repo, revs)
716 723
717 724 if not l:
718 725 raise error.Abort(_(b'empty revision range'))
719 726
720 727 first = l.first()
721 728 second = l.last()
722 729
723 730 if (
724 731 first == second
725 732 and len(revs) >= 2
726 733 and not all(revrange(repo, [r]) for r in revs)
727 734 ):
728 735 raise error.Abort(_(b'empty revision on one side of range'))
729 736
730 737 # if top-level is range expression, the result must always be a pair
731 738 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
732 739 return repo[first], repo[None]
733 740
734 741 return repo[first], repo[second]
735 742
736 743
737 744 def revrange(repo, specs, localalias=None):
738 745 """Execute 1 to many revsets and return the union.
739 746
740 747 This is the preferred mechanism for executing revsets using user-specified
741 748 config options, such as revset aliases.
742 749
743 750 The revsets specified by ``specs`` will be executed via a chained ``OR``
744 751 expression. If ``specs`` is empty, an empty result is returned.
745 752
746 753 ``specs`` can contain integers, in which case they are assumed to be
747 754 revision numbers.
748 755
749 756 It is assumed the revsets are already formatted. If you have arguments
750 757 that need to be expanded in the revset, call ``revsetlang.formatspec()``
751 758 and pass the result as an element of ``specs``.
752 759
753 760 Specifying a single revset is allowed.
754 761
755 762 Returns a ``smartset.abstractsmartset`` which is a list-like interface over
756 763 integer revisions.
757 764 """
758 765 allspecs = []
759 766 for spec in specs:
760 767 if isinstance(spec, int):
761 768 spec = revsetlang.formatspec(b'%d', spec)
762 769 allspecs.append(spec)
763 770 return repo.anyrevs(allspecs, user=True, localalias=localalias)
764 771
765 772
766 773 def increasingwindows(windowsize=8, sizelimit=512):
767 774 while True:
768 775 yield windowsize
769 776 if windowsize < sizelimit:
770 777 windowsize *= 2
771 778
772 779
773 780 def walkchangerevs(repo, revs, makefilematcher, prepare):
774 781 '''Iterate over files and the revs in a "windowed" way.
775 782
776 783 Callers most commonly need to iterate backwards over the history
777 784 in which they are interested. Doing so has awful (quadratic-looking)
778 785 performance, so we use iterators in a "windowed" way.
779 786
780 787 We walk a window of revisions in the desired order. Within the
781 788 window, we first walk forwards to gather data, then in the desired
782 789 order (usually backwards) to display it.
783 790
784 791 This function returns an iterator yielding contexts. Before
785 792 yielding each context, the iterator will first call the prepare
786 793 function on each context in the window in forward order.'''
787 794
788 795 if not revs:
789 796 return []
790 797 change = repo.__getitem__
791 798
792 799 def iterate():
793 800 it = iter(revs)
794 801 stopiteration = False
795 802 for windowsize in increasingwindows():
796 803 nrevs = []
797 804 for i in pycompat.xrange(windowsize):
798 805 rev = next(it, None)
799 806 if rev is None:
800 807 stopiteration = True
801 808 break
802 809 nrevs.append(rev)
803 810 for rev in sorted(nrevs):
804 811 ctx = change(rev)
805 812 prepare(ctx, makefilematcher(ctx))
806 813 for rev in nrevs:
807 814 yield change(rev)
808 815
809 816 if stopiteration:
810 817 break
811 818
812 819 return iterate()
813 820
814 821
815 822 def meaningfulparents(repo, ctx):
816 823 """Return list of meaningful (or all if debug) parentrevs for rev.
817 824
818 825 For merges (two non-nullrev revisions) both parents are meaningful.
819 826 Otherwise the first parent revision is considered meaningful if it
820 827 is not the preceding revision.
821 828 """
822 829 parents = ctx.parents()
823 830 if len(parents) > 1:
824 831 return parents
825 832 if repo.ui.debugflag:
826 833 return [parents[0], repo[nullrev]]
827 834 if parents[0].rev() >= intrev(ctx) - 1:
828 835 return []
829 836 return parents
830 837
831 838
832 839 def getuipathfn(repo, legacyrelativevalue=False, forcerelativevalue=None):
833 840 """Return a function that produced paths for presenting to the user.
834 841
835 842 The returned function takes a repo-relative path and produces a path
836 843 that can be presented in the UI.
837 844
838 845 Depending on the value of ui.relative-paths, either a repo-relative or
839 846 cwd-relative path will be produced.
840 847
841 848 legacyrelativevalue is the value to use if ui.relative-paths=legacy
842 849
843 850 If forcerelativevalue is not None, then that value will be used regardless
844 851 of what ui.relative-paths is set to.
845 852 """
846 853 if forcerelativevalue is not None:
847 854 relative = forcerelativevalue
848 855 else:
849 856 config = repo.ui.config(b'ui', b'relative-paths')
850 857 if config == b'legacy':
851 858 relative = legacyrelativevalue
852 859 else:
853 860 relative = stringutil.parsebool(config)
854 861 if relative is None:
855 862 raise error.ConfigError(
856 863 _(b"ui.relative-paths is not a boolean ('%s')") % config
857 864 )
858 865
859 866 if relative:
860 867 cwd = repo.getcwd()
861 868 if cwd != b'':
862 869 # this branch would work even if cwd == b'' (ie cwd = repo
863 870 # root), but its generality makes the returned function slower
864 871 pathto = repo.pathto
865 872 return lambda f: pathto(f, cwd)
866 873 if repo.ui.configbool(b'ui', b'slash'):
867 874 return lambda f: f
868 875 else:
869 876 return util.localpath
870 877
871 878
872 879 def subdiruipathfn(subpath, uipathfn):
873 880 '''Create a new uipathfn that treats the file as relative to subpath.'''
874 881 return lambda f: uipathfn(posixpath.join(subpath, f))
875 882
876 883
877 884 def anypats(pats, opts):
878 885 '''Checks if any patterns, including --include and --exclude were given.
879 886
880 887 Some commands (e.g. addremove) use this condition for deciding whether to
881 888 print absolute or relative paths.
882 889 '''
883 890 return bool(pats or opts.get(b'include') or opts.get(b'exclude'))
884 891
885 892
886 893 def expandpats(pats):
887 894 '''Expand bare globs when running on windows.
888 895 On posix we assume it already has already been done by sh.'''
889 896 if not util.expandglobs:
890 897 return list(pats)
891 898 ret = []
892 899 for kindpat in pats:
893 900 kind, pat = matchmod._patsplit(kindpat, None)
894 901 if kind is None:
895 902 try:
896 903 globbed = glob.glob(pat)
897 904 except re.error:
898 905 globbed = [pat]
899 906 if globbed:
900 907 ret.extend(globbed)
901 908 continue
902 909 ret.append(kindpat)
903 910 return ret
904 911
905 912
906 913 def matchandpats(
907 914 ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
908 915 ):
909 916 '''Return a matcher and the patterns that were used.
910 917 The matcher will warn about bad matches, unless an alternate badfn callback
911 918 is provided.'''
912 919 if opts is None:
913 920 opts = {}
914 921 if not globbed and default == b'relpath':
915 922 pats = expandpats(pats or [])
916 923
917 924 uipathfn = getuipathfn(ctx.repo(), legacyrelativevalue=True)
918 925
919 926 def bad(f, msg):
920 927 ctx.repo().ui.warn(b"%s: %s\n" % (uipathfn(f), msg))
921 928
922 929 if badfn is None:
923 930 badfn = bad
924 931
925 932 m = ctx.match(
926 933 pats,
927 934 opts.get(b'include'),
928 935 opts.get(b'exclude'),
929 936 default,
930 937 listsubrepos=opts.get(b'subrepos'),
931 938 badfn=badfn,
932 939 )
933 940
934 941 if m.always():
935 942 pats = []
936 943 return m, pats
937 944
938 945
939 946 def match(
940 947 ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
941 948 ):
942 949 '''Return a matcher that will warn about bad matches.'''
943 950 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
944 951
945 952
946 953 def matchall(repo):
947 954 '''Return a matcher that will efficiently match everything.'''
948 955 return matchmod.always()
949 956
950 957
951 958 def matchfiles(repo, files, badfn=None):
952 959 '''Return a matcher that will efficiently match exactly these files.'''
953 960 return matchmod.exact(files, badfn=badfn)
954 961
955 962
956 963 def parsefollowlinespattern(repo, rev, pat, msg):
957 964 """Return a file name from `pat` pattern suitable for usage in followlines
958 965 logic.
959 966 """
960 967 if not matchmod.patkind(pat):
961 968 return pathutil.canonpath(repo.root, repo.getcwd(), pat)
962 969 else:
963 970 ctx = repo[rev]
964 971 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=ctx)
965 972 files = [f for f in ctx if m(f)]
966 973 if len(files) != 1:
967 974 raise error.ParseError(msg)
968 975 return files[0]
969 976
970 977
971 978 def getorigvfs(ui, repo):
972 979 """return a vfs suitable to save 'orig' file
973 980
974 981 return None if no special directory is configured"""
975 982 origbackuppath = ui.config(b'ui', b'origbackuppath')
976 983 if not origbackuppath:
977 984 return None
978 985 return vfs.vfs(repo.wvfs.join(origbackuppath))
979 986
980 987
981 988 def backuppath(ui, repo, filepath):
982 989 '''customize where working copy backup files (.orig files) are created
983 990
984 991 Fetch user defined path from config file: [ui] origbackuppath = <path>
985 992 Fall back to default (filepath with .orig suffix) if not specified
986 993
987 994 filepath is repo-relative
988 995
989 996 Returns an absolute path
990 997 '''
991 998 origvfs = getorigvfs(ui, repo)
992 999 if origvfs is None:
993 1000 return repo.wjoin(filepath + b".orig")
994 1001
995 1002 origbackupdir = origvfs.dirname(filepath)
996 1003 if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir):
997 1004 ui.note(_(b'creating directory: %s\n') % origvfs.join(origbackupdir))
998 1005
999 1006 # Remove any files that conflict with the backup file's path
1000 1007 for f in reversed(list(pathutil.finddirs(filepath))):
1001 1008 if origvfs.isfileorlink(f):
1002 1009 ui.note(_(b'removing conflicting file: %s\n') % origvfs.join(f))
1003 1010 origvfs.unlink(f)
1004 1011 break
1005 1012
1006 1013 origvfs.makedirs(origbackupdir)
1007 1014
1008 1015 if origvfs.isdir(filepath) and not origvfs.islink(filepath):
1009 1016 ui.note(
1010 1017 _(b'removing conflicting directory: %s\n') % origvfs.join(filepath)
1011 1018 )
1012 1019 origvfs.rmtree(filepath, forcibly=True)
1013 1020
1014 1021 return origvfs.join(filepath)
1015 1022
1016 1023
1017 1024 class _containsnode(object):
1018 1025 """proxy __contains__(node) to container.__contains__ which accepts revs"""
1019 1026
1020 1027 def __init__(self, repo, revcontainer):
1021 1028 self._torev = repo.changelog.rev
1022 1029 self._revcontains = revcontainer.__contains__
1023 1030
1024 1031 def __contains__(self, node):
1025 1032 return self._revcontains(self._torev(node))
1026 1033
1027 1034
1028 1035 def cleanupnodes(
1029 1036 repo,
1030 1037 replacements,
1031 1038 operation,
1032 1039 moves=None,
1033 1040 metadata=None,
1034 1041 fixphase=False,
1035 1042 targetphase=None,
1036 1043 backup=True,
1037 1044 ):
1038 1045 """do common cleanups when old nodes are replaced by new nodes
1039 1046
1040 1047 That includes writing obsmarkers or stripping nodes, and moving bookmarks.
1041 1048 (we might also want to move working directory parent in the future)
1042 1049
1043 1050 By default, bookmark moves are calculated automatically from 'replacements',
1044 1051 but 'moves' can be used to override that. Also, 'moves' may include
1045 1052 additional bookmark moves that should not have associated obsmarkers.
1046 1053
1047 1054 replacements is {oldnode: [newnode]} or a iterable of nodes if they do not
1048 1055 have replacements. operation is a string, like "rebase".
1049 1056
1050 1057 metadata is dictionary containing metadata to be stored in obsmarker if
1051 1058 obsolescence is enabled.
1052 1059 """
1053 1060 assert fixphase or targetphase is None
1054 1061 if not replacements and not moves:
1055 1062 return
1056 1063
1057 1064 # translate mapping's other forms
1058 1065 if not util.safehasattr(replacements, b'items'):
1059 1066 replacements = {(n,): () for n in replacements}
1060 1067 else:
1061 1068 # upgrading non tuple "source" to tuple ones for BC
1062 1069 repls = {}
1063 1070 for key, value in replacements.items():
1064 1071 if not isinstance(key, tuple):
1065 1072 key = (key,)
1066 1073 repls[key] = value
1067 1074 replacements = repls
1068 1075
1069 1076 # Unfiltered repo is needed since nodes in replacements might be hidden.
1070 1077 unfi = repo.unfiltered()
1071 1078
1072 1079 # Calculate bookmark movements
1073 1080 if moves is None:
1074 1081 moves = {}
1075 1082 for oldnodes, newnodes in replacements.items():
1076 1083 for oldnode in oldnodes:
1077 1084 if oldnode in moves:
1078 1085 continue
1079 1086 if len(newnodes) > 1:
1080 1087 # usually a split, take the one with biggest rev number
1081 1088 newnode = next(unfi.set(b'max(%ln)', newnodes)).node()
1082 1089 elif len(newnodes) == 0:
1083 1090 # move bookmark backwards
1084 1091 allreplaced = []
1085 1092 for rep in replacements:
1086 1093 allreplaced.extend(rep)
1087 1094 roots = list(
1088 1095 unfi.set(b'max((::%n) - %ln)', oldnode, allreplaced)
1089 1096 )
1090 1097 if roots:
1091 1098 newnode = roots[0].node()
1092 1099 else:
1093 1100 newnode = nullid
1094 1101 else:
1095 1102 newnode = newnodes[0]
1096 1103 moves[oldnode] = newnode
1097 1104
1098 1105 allnewnodes = [n for ns in replacements.values() for n in ns]
1099 1106 toretract = {}
1100 1107 toadvance = {}
1101 1108 if fixphase:
1102 1109 precursors = {}
1103 1110 for oldnodes, newnodes in replacements.items():
1104 1111 for oldnode in oldnodes:
1105 1112 for newnode in newnodes:
1106 1113 precursors.setdefault(newnode, []).append(oldnode)
1107 1114
1108 1115 allnewnodes.sort(key=lambda n: unfi[n].rev())
1109 1116 newphases = {}
1110 1117
1111 1118 def phase(ctx):
1112 1119 return newphases.get(ctx.node(), ctx.phase())
1113 1120
1114 1121 for newnode in allnewnodes:
1115 1122 ctx = unfi[newnode]
1116 1123 parentphase = max(phase(p) for p in ctx.parents())
1117 1124 if targetphase is None:
1118 1125 oldphase = max(
1119 1126 unfi[oldnode].phase() for oldnode in precursors[newnode]
1120 1127 )
1121 1128 newphase = max(oldphase, parentphase)
1122 1129 else:
1123 1130 newphase = max(targetphase, parentphase)
1124 1131 newphases[newnode] = newphase
1125 1132 if newphase > ctx.phase():
1126 1133 toretract.setdefault(newphase, []).append(newnode)
1127 1134 elif newphase < ctx.phase():
1128 1135 toadvance.setdefault(newphase, []).append(newnode)
1129 1136
1130 1137 with repo.transaction(b'cleanup') as tr:
1131 1138 # Move bookmarks
1132 1139 bmarks = repo._bookmarks
1133 1140 bmarkchanges = []
1134 1141 for oldnode, newnode in moves.items():
1135 1142 oldbmarks = repo.nodebookmarks(oldnode)
1136 1143 if not oldbmarks:
1137 1144 continue
1138 1145 from . import bookmarks # avoid import cycle
1139 1146
1140 1147 repo.ui.debug(
1141 1148 b'moving bookmarks %r from %s to %s\n'
1142 1149 % (
1143 1150 pycompat.rapply(pycompat.maybebytestr, oldbmarks),
1144 1151 hex(oldnode),
1145 1152 hex(newnode),
1146 1153 )
1147 1154 )
1148 1155 # Delete divergent bookmarks being parents of related newnodes
1149 1156 deleterevs = repo.revs(
1150 1157 b'parents(roots(%ln & (::%n))) - parents(%n)',
1151 1158 allnewnodes,
1152 1159 newnode,
1153 1160 oldnode,
1154 1161 )
1155 1162 deletenodes = _containsnode(repo, deleterevs)
1156 1163 for name in oldbmarks:
1157 1164 bmarkchanges.append((name, newnode))
1158 1165 for b in bookmarks.divergent2delete(repo, deletenodes, name):
1159 1166 bmarkchanges.append((b, None))
1160 1167
1161 1168 if bmarkchanges:
1162 1169 bmarks.applychanges(repo, tr, bmarkchanges)
1163 1170
1164 1171 for phase, nodes in toretract.items():
1165 1172 phases.retractboundary(repo, tr, phase, nodes)
1166 1173 for phase, nodes in toadvance.items():
1167 1174 phases.advanceboundary(repo, tr, phase, nodes)
1168 1175
1169 1176 mayusearchived = repo.ui.config(b'experimental', b'cleanup-as-archived')
1170 1177 # Obsolete or strip nodes
1171 1178 if obsolete.isenabled(repo, obsolete.createmarkersopt):
1172 1179 # If a node is already obsoleted, and we want to obsolete it
1173 1180 # without a successor, skip that obssolete request since it's
1174 1181 # unnecessary. That's the "if s or not isobs(n)" check below.
1175 1182 # Also sort the node in topology order, that might be useful for
1176 1183 # some obsstore logic.
1177 1184 # NOTE: the sorting might belong to createmarkers.
1178 1185 torev = unfi.changelog.rev
1179 1186 sortfunc = lambda ns: torev(ns[0][0])
1180 1187 rels = []
1181 1188 for ns, s in sorted(replacements.items(), key=sortfunc):
1182 1189 rel = (tuple(unfi[n] for n in ns), tuple(unfi[m] for m in s))
1183 1190 rels.append(rel)
1184 1191 if rels:
1185 1192 obsolete.createmarkers(
1186 1193 repo, rels, operation=operation, metadata=metadata
1187 1194 )
1188 1195 elif phases.supportinternal(repo) and mayusearchived:
1189 1196 # this assume we do not have "unstable" nodes above the cleaned ones
1190 1197 allreplaced = set()
1191 1198 for ns in replacements.keys():
1192 1199 allreplaced.update(ns)
1193 1200 if backup:
1194 1201 from . import repair # avoid import cycle
1195 1202
1196 1203 node = min(allreplaced, key=repo.changelog.rev)
1197 1204 repair.backupbundle(
1198 1205 repo, allreplaced, allreplaced, node, operation
1199 1206 )
1200 1207 phases.retractboundary(repo, tr, phases.archived, allreplaced)
1201 1208 else:
1202 1209 from . import repair # avoid import cycle
1203 1210
1204 1211 tostrip = list(n for ns in replacements for n in ns)
1205 1212 if tostrip:
1206 1213 repair.delayedstrip(
1207 1214 repo.ui, repo, tostrip, operation, backup=backup
1208 1215 )
1209 1216
1210 1217
1211 1218 def addremove(repo, matcher, prefix, uipathfn, opts=None):
1212 1219 if opts is None:
1213 1220 opts = {}
1214 1221 m = matcher
1215 1222 dry_run = opts.get(b'dry_run')
1216 1223 try:
1217 1224 similarity = float(opts.get(b'similarity') or 0)
1218 1225 except ValueError:
1219 1226 raise error.Abort(_(b'similarity must be a number'))
1220 1227 if similarity < 0 or similarity > 100:
1221 1228 raise error.Abort(_(b'similarity must be between 0 and 100'))
1222 1229 similarity /= 100.0
1223 1230
1224 1231 ret = 0
1225 1232
1226 1233 wctx = repo[None]
1227 1234 for subpath in sorted(wctx.substate):
1228 1235 submatch = matchmod.subdirmatcher(subpath, m)
1229 1236 if opts.get(b'subrepos') or m.exact(subpath) or any(submatch.files()):
1230 1237 sub = wctx.sub(subpath)
1231 1238 subprefix = repo.wvfs.reljoin(prefix, subpath)
1232 1239 subuipathfn = subdiruipathfn(subpath, uipathfn)
1233 1240 try:
1234 1241 if sub.addremove(submatch, subprefix, subuipathfn, opts):
1235 1242 ret = 1
1236 1243 except error.LookupError:
1237 1244 repo.ui.status(
1238 1245 _(b"skipping missing subrepository: %s\n")
1239 1246 % uipathfn(subpath)
1240 1247 )
1241 1248
1242 1249 rejected = []
1243 1250
1244 1251 def badfn(f, msg):
1245 1252 if f in m.files():
1246 1253 m.bad(f, msg)
1247 1254 rejected.append(f)
1248 1255
1249 1256 badmatch = matchmod.badmatch(m, badfn)
1250 1257 added, unknown, deleted, removed, forgotten = _interestingfiles(
1251 1258 repo, badmatch
1252 1259 )
1253 1260
1254 1261 unknownset = set(unknown + forgotten)
1255 1262 toprint = unknownset.copy()
1256 1263 toprint.update(deleted)
1257 1264 for abs in sorted(toprint):
1258 1265 if repo.ui.verbose or not m.exact(abs):
1259 1266 if abs in unknownset:
1260 1267 status = _(b'adding %s\n') % uipathfn(abs)
1261 1268 label = b'ui.addremove.added'
1262 1269 else:
1263 1270 status = _(b'removing %s\n') % uipathfn(abs)
1264 1271 label = b'ui.addremove.removed'
1265 1272 repo.ui.status(status, label=label)
1266 1273
1267 1274 renames = _findrenames(
1268 1275 repo, m, added + unknown, removed + deleted, similarity, uipathfn
1269 1276 )
1270 1277
1271 1278 if not dry_run:
1272 1279 _markchanges(repo, unknown + forgotten, deleted, renames)
1273 1280
1274 1281 for f in rejected:
1275 1282 if f in m.files():
1276 1283 return 1
1277 1284 return ret
1278 1285
1279 1286
1280 1287 def marktouched(repo, files, similarity=0.0):
1281 1288 '''Assert that files have somehow been operated upon. files are relative to
1282 1289 the repo root.'''
1283 1290 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
1284 1291 rejected = []
1285 1292
1286 1293 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
1287 1294
1288 1295 if repo.ui.verbose:
1289 1296 unknownset = set(unknown + forgotten)
1290 1297 toprint = unknownset.copy()
1291 1298 toprint.update(deleted)
1292 1299 for abs in sorted(toprint):
1293 1300 if abs in unknownset:
1294 1301 status = _(b'adding %s\n') % abs
1295 1302 else:
1296 1303 status = _(b'removing %s\n') % abs
1297 1304 repo.ui.status(status)
1298 1305
1299 1306 # TODO: We should probably have the caller pass in uipathfn and apply it to
1300 1307 # the messages above too. legacyrelativevalue=True is consistent with how
1301 1308 # it used to work.
1302 1309 uipathfn = getuipathfn(repo, legacyrelativevalue=True)
1303 1310 renames = _findrenames(
1304 1311 repo, m, added + unknown, removed + deleted, similarity, uipathfn
1305 1312 )
1306 1313
1307 1314 _markchanges(repo, unknown + forgotten, deleted, renames)
1308 1315
1309 1316 for f in rejected:
1310 1317 if f in m.files():
1311 1318 return 1
1312 1319 return 0
1313 1320
1314 1321
1315 1322 def _interestingfiles(repo, matcher):
1316 1323 '''Walk dirstate with matcher, looking for files that addremove would care
1317 1324 about.
1318 1325
1319 1326 This is different from dirstate.status because it doesn't care about
1320 1327 whether files are modified or clean.'''
1321 1328 added, unknown, deleted, removed, forgotten = [], [], [], [], []
1322 1329 audit_path = pathutil.pathauditor(repo.root, cached=True)
1323 1330
1324 1331 ctx = repo[None]
1325 1332 dirstate = repo.dirstate
1326 1333 matcher = repo.narrowmatch(matcher, includeexact=True)
1327 1334 walkresults = dirstate.walk(
1328 1335 matcher,
1329 1336 subrepos=sorted(ctx.substate),
1330 1337 unknown=True,
1331 1338 ignored=False,
1332 1339 full=False,
1333 1340 )
1334 1341 for abs, st in pycompat.iteritems(walkresults):
1335 1342 dstate = dirstate[abs]
1336 1343 if dstate == b'?' and audit_path.check(abs):
1337 1344 unknown.append(abs)
1338 1345 elif dstate != b'r' and not st:
1339 1346 deleted.append(abs)
1340 1347 elif dstate == b'r' and st:
1341 1348 forgotten.append(abs)
1342 1349 # for finding renames
1343 1350 elif dstate == b'r' and not st:
1344 1351 removed.append(abs)
1345 1352 elif dstate == b'a':
1346 1353 added.append(abs)
1347 1354
1348 1355 return added, unknown, deleted, removed, forgotten
1349 1356
1350 1357
1351 1358 def _findrenames(repo, matcher, added, removed, similarity, uipathfn):
1352 1359 '''Find renames from removed files to added ones.'''
1353 1360 renames = {}
1354 1361 if similarity > 0:
1355 1362 for old, new, score in similar.findrenames(
1356 1363 repo, added, removed, similarity
1357 1364 ):
1358 1365 if (
1359 1366 repo.ui.verbose
1360 1367 or not matcher.exact(old)
1361 1368 or not matcher.exact(new)
1362 1369 ):
1363 1370 repo.ui.status(
1364 1371 _(
1365 1372 b'recording removal of %s as rename to %s '
1366 1373 b'(%d%% similar)\n'
1367 1374 )
1368 1375 % (uipathfn(old), uipathfn(new), score * 100)
1369 1376 )
1370 1377 renames[new] = old
1371 1378 return renames
1372 1379
1373 1380
1374 1381 def _markchanges(repo, unknown, deleted, renames):
1375 1382 '''Marks the files in unknown as added, the files in deleted as removed,
1376 1383 and the files in renames as copied.'''
1377 1384 wctx = repo[None]
1378 1385 with repo.wlock():
1379 1386 wctx.forget(deleted)
1380 1387 wctx.add(unknown)
1381 1388 for new, old in pycompat.iteritems(renames):
1382 1389 wctx.copy(old, new)
1383 1390
1384 1391
1385 1392 def getrenamedfn(repo, endrev=None):
1386 1393 if copiesmod.usechangesetcentricalgo(repo):
1387 1394
1388 1395 def getrenamed(fn, rev):
1389 1396 ctx = repo[rev]
1390 1397 p1copies = ctx.p1copies()
1391 1398 if fn in p1copies:
1392 1399 return p1copies[fn]
1393 1400 p2copies = ctx.p2copies()
1394 1401 if fn in p2copies:
1395 1402 return p2copies[fn]
1396 1403 return None
1397 1404
1398 1405 return getrenamed
1399 1406
1400 1407 rcache = {}
1401 1408 if endrev is None:
1402 1409 endrev = len(repo)
1403 1410
1404 1411 def getrenamed(fn, rev):
1405 1412 '''looks up all renames for a file (up to endrev) the first
1406 1413 time the file is given. It indexes on the changerev and only
1407 1414 parses the manifest if linkrev != changerev.
1408 1415 Returns rename info for fn at changerev rev.'''
1409 1416 if fn not in rcache:
1410 1417 rcache[fn] = {}
1411 1418 fl = repo.file(fn)
1412 1419 for i in fl:
1413 1420 lr = fl.linkrev(i)
1414 1421 renamed = fl.renamed(fl.node(i))
1415 1422 rcache[fn][lr] = renamed and renamed[0]
1416 1423 if lr >= endrev:
1417 1424 break
1418 1425 if rev in rcache[fn]:
1419 1426 return rcache[fn][rev]
1420 1427
1421 1428 # If linkrev != rev (i.e. rev not found in rcache) fallback to
1422 1429 # filectx logic.
1423 1430 try:
1424 1431 return repo[rev][fn].copysource()
1425 1432 except error.LookupError:
1426 1433 return None
1427 1434
1428 1435 return getrenamed
1429 1436
1430 1437
1431 1438 def getcopiesfn(repo, endrev=None):
1432 1439 if copiesmod.usechangesetcentricalgo(repo):
1433 1440
1434 1441 def copiesfn(ctx):
1435 1442 if ctx.p2copies():
1436 1443 allcopies = ctx.p1copies().copy()
1437 1444 # There should be no overlap
1438 1445 allcopies.update(ctx.p2copies())
1439 1446 return sorted(allcopies.items())
1440 1447 else:
1441 1448 return sorted(ctx.p1copies().items())
1442 1449
1443 1450 else:
1444 1451 getrenamed = getrenamedfn(repo, endrev)
1445 1452
1446 1453 def copiesfn(ctx):
1447 1454 copies = []
1448 1455 for fn in ctx.files():
1449 1456 rename = getrenamed(fn, ctx.rev())
1450 1457 if rename:
1451 1458 copies.append((fn, rename))
1452 1459 return copies
1453 1460
1454 1461 return copiesfn
1455 1462
1456 1463
1457 1464 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
1458 1465 """Update the dirstate to reflect the intent of copying src to dst. For
1459 1466 different reasons it might not end with dst being marked as copied from src.
1460 1467 """
1461 1468 origsrc = repo.dirstate.copied(src) or src
1462 1469 if dst == origsrc: # copying back a copy?
1463 1470 if repo.dirstate[dst] not in b'mn' and not dryrun:
1464 1471 repo.dirstate.normallookup(dst)
1465 1472 else:
1466 1473 if repo.dirstate[origsrc] == b'a' and origsrc == src:
1467 1474 if not ui.quiet:
1468 1475 ui.warn(
1469 1476 _(
1470 1477 b"%s has not been committed yet, so no copy "
1471 1478 b"data will be stored for %s.\n"
1472 1479 )
1473 1480 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd))
1474 1481 )
1475 1482 if repo.dirstate[dst] in b'?r' and not dryrun:
1476 1483 wctx.add([dst])
1477 1484 elif not dryrun:
1478 1485 wctx.copy(origsrc, dst)
1479 1486
1480 1487
1481 1488 def movedirstate(repo, newctx, match=None):
1482 1489 """Move the dirstate to newctx and adjust it as necessary.
1483 1490
1484 1491 A matcher can be provided as an optimization. It is probably a bug to pass
1485 1492 a matcher that doesn't match all the differences between the parent of the
1486 1493 working copy and newctx.
1487 1494 """
1488 1495 oldctx = repo[b'.']
1489 1496 ds = repo.dirstate
1490 1497 copies = dict(ds.copies())
1491 1498 ds.setparents(newctx.node(), nullid)
1492 1499 s = newctx.status(oldctx, match=match)
1493 1500 for f in s.modified:
1494 1501 if ds[f] == b'r':
1495 1502 # modified + removed -> removed
1496 1503 continue
1497 1504 ds.normallookup(f)
1498 1505
1499 1506 for f in s.added:
1500 1507 if ds[f] == b'r':
1501 1508 # added + removed -> unknown
1502 1509 ds.drop(f)
1503 1510 elif ds[f] != b'a':
1504 1511 ds.add(f)
1505 1512
1506 1513 for f in s.removed:
1507 1514 if ds[f] == b'a':
1508 1515 # removed + added -> normal
1509 1516 ds.normallookup(f)
1510 1517 elif ds[f] != b'r':
1511 1518 ds.remove(f)
1512 1519
1513 1520 # Merge old parent and old working dir copies
1514 1521 oldcopies = copiesmod.pathcopies(newctx, oldctx, match)
1515 1522 oldcopies.update(copies)
1516 1523 copies = {
1517 1524 dst: oldcopies.get(src, src)
1518 1525 for dst, src in pycompat.iteritems(oldcopies)
1519 1526 }
1520 1527 # Adjust the dirstate copies
1521 1528 for dst, src in pycompat.iteritems(copies):
1522 1529 if src not in newctx or dst in newctx or ds[dst] != b'a':
1523 1530 src = None
1524 1531 ds.copy(src, dst)
1525 1532 repo._quick_access_changeid_invalidate()
1526 1533
1527 1534
1528 1535 def filterrequirements(requirements):
1529 1536 """ filters the requirements into two sets:
1530 1537
1531 1538 wcreq: requirements which should be written in .hg/requires
1532 1539 storereq: which should be written in .hg/store/requires
1533 1540
1534 1541 Returns (wcreq, storereq)
1535 1542 """
1536 1543 if requirementsmod.SHARESAFE_REQUIREMENT in requirements:
1537 1544 wc, store = set(), set()
1538 1545 for r in requirements:
1539 1546 if r in requirementsmod.WORKING_DIR_REQUIREMENTS:
1540 1547 wc.add(r)
1541 1548 else:
1542 1549 store.add(r)
1543 1550 return wc, store
1544 1551 return requirements, None
1545 1552
1546 1553
1547 1554 def istreemanifest(repo):
1548 1555 """ returns whether the repository is using treemanifest or not """
1549 1556 return requirementsmod.TREEMANIFEST_REQUIREMENT in repo.requirements
1550 1557
1551 1558
1552 1559 def writereporequirements(repo, requirements=None):
1553 1560 """ writes requirements for the repo to .hg/requires """
1554 1561 if requirements:
1555 1562 repo.requirements = requirements
1556 1563 wcreq, storereq = filterrequirements(repo.requirements)
1557 1564 if wcreq is not None:
1558 1565 writerequires(repo.vfs, wcreq)
1559 1566 if storereq is not None:
1560 1567 writerequires(repo.svfs, storereq)
1561 1568
1562 1569
1563 1570 def writerequires(opener, requirements):
1564 1571 with opener(b'requires', b'w', atomictemp=True) as fp:
1565 1572 for r in sorted(requirements):
1566 1573 fp.write(b"%s\n" % r)
1567 1574
1568 1575
1569 1576 class filecachesubentry(object):
1570 1577 def __init__(self, path, stat):
1571 1578 self.path = path
1572 1579 self.cachestat = None
1573 1580 self._cacheable = None
1574 1581
1575 1582 if stat:
1576 1583 self.cachestat = filecachesubentry.stat(self.path)
1577 1584
1578 1585 if self.cachestat:
1579 1586 self._cacheable = self.cachestat.cacheable()
1580 1587 else:
1581 1588 # None means we don't know yet
1582 1589 self._cacheable = None
1583 1590
1584 1591 def refresh(self):
1585 1592 if self.cacheable():
1586 1593 self.cachestat = filecachesubentry.stat(self.path)
1587 1594
1588 1595 def cacheable(self):
1589 1596 if self._cacheable is not None:
1590 1597 return self._cacheable
1591 1598
1592 1599 # we don't know yet, assume it is for now
1593 1600 return True
1594 1601
1595 1602 def changed(self):
1596 1603 # no point in going further if we can't cache it
1597 1604 if not self.cacheable():
1598 1605 return True
1599 1606
1600 1607 newstat = filecachesubentry.stat(self.path)
1601 1608
1602 1609 # we may not know if it's cacheable yet, check again now
1603 1610 if newstat and self._cacheable is None:
1604 1611 self._cacheable = newstat.cacheable()
1605 1612
1606 1613 # check again
1607 1614 if not self._cacheable:
1608 1615 return True
1609 1616
1610 1617 if self.cachestat != newstat:
1611 1618 self.cachestat = newstat
1612 1619 return True
1613 1620 else:
1614 1621 return False
1615 1622
1616 1623 @staticmethod
1617 1624 def stat(path):
1618 1625 try:
1619 1626 return util.cachestat(path)
1620 1627 except OSError as e:
1621 1628 if e.errno != errno.ENOENT:
1622 1629 raise
1623 1630
1624 1631
1625 1632 class filecacheentry(object):
1626 1633 def __init__(self, paths, stat=True):
1627 1634 self._entries = []
1628 1635 for path in paths:
1629 1636 self._entries.append(filecachesubentry(path, stat))
1630 1637
1631 1638 def changed(self):
1632 1639 '''true if any entry has changed'''
1633 1640 for entry in self._entries:
1634 1641 if entry.changed():
1635 1642 return True
1636 1643 return False
1637 1644
1638 1645 def refresh(self):
1639 1646 for entry in self._entries:
1640 1647 entry.refresh()
1641 1648
1642 1649
1643 1650 class filecache(object):
1644 1651 """A property like decorator that tracks files under .hg/ for updates.
1645 1652
1646 1653 On first access, the files defined as arguments are stat()ed and the
1647 1654 results cached. The decorated function is called. The results are stashed
1648 1655 away in a ``_filecache`` dict on the object whose method is decorated.
1649 1656
1650 1657 On subsequent access, the cached result is used as it is set to the
1651 1658 instance dictionary.
1652 1659
1653 1660 On external property set/delete operations, the caller must update the
1654 1661 corresponding _filecache entry appropriately. Use __class__.<attr>.set()
1655 1662 instead of directly setting <attr>.
1656 1663
1657 1664 When using the property API, the cached data is always used if available.
1658 1665 No stat() is performed to check if the file has changed.
1659 1666
1660 1667 Others can muck about with the state of the ``_filecache`` dict. e.g. they
1661 1668 can populate an entry before the property's getter is called. In this case,
1662 1669 entries in ``_filecache`` will be used during property operations,
1663 1670 if available. If the underlying file changes, it is up to external callers
1664 1671 to reflect this by e.g. calling ``delattr(obj, attr)`` to remove the cached
1665 1672 method result as well as possibly calling ``del obj._filecache[attr]`` to
1666 1673 remove the ``filecacheentry``.
1667 1674 """
1668 1675
1669 1676 def __init__(self, *paths):
1670 1677 self.paths = paths
1671 1678
1672 1679 def join(self, obj, fname):
1673 1680 """Used to compute the runtime path of a cached file.
1674 1681
1675 1682 Users should subclass filecache and provide their own version of this
1676 1683 function to call the appropriate join function on 'obj' (an instance
1677 1684 of the class that its member function was decorated).
1678 1685 """
1679 1686 raise NotImplementedError
1680 1687
1681 1688 def __call__(self, func):
1682 1689 self.func = func
1683 1690 self.sname = func.__name__
1684 1691 self.name = pycompat.sysbytes(self.sname)
1685 1692 return self
1686 1693
1687 1694 def __get__(self, obj, type=None):
1688 1695 # if accessed on the class, return the descriptor itself.
1689 1696 if obj is None:
1690 1697 return self
1691 1698
1692 1699 assert self.sname not in obj.__dict__
1693 1700
1694 1701 entry = obj._filecache.get(self.name)
1695 1702
1696 1703 if entry:
1697 1704 if entry.changed():
1698 1705 entry.obj = self.func(obj)
1699 1706 else:
1700 1707 paths = [self.join(obj, path) for path in self.paths]
1701 1708
1702 1709 # We stat -before- creating the object so our cache doesn't lie if
1703 1710 # a writer modified between the time we read and stat
1704 1711 entry = filecacheentry(paths, True)
1705 1712 entry.obj = self.func(obj)
1706 1713
1707 1714 obj._filecache[self.name] = entry
1708 1715
1709 1716 obj.__dict__[self.sname] = entry.obj
1710 1717 return entry.obj
1711 1718
1712 1719 # don't implement __set__(), which would make __dict__ lookup as slow as
1713 1720 # function call.
1714 1721
1715 1722 def set(self, obj, value):
1716 1723 if self.name not in obj._filecache:
1717 1724 # we add an entry for the missing value because X in __dict__
1718 1725 # implies X in _filecache
1719 1726 paths = [self.join(obj, path) for path in self.paths]
1720 1727 ce = filecacheentry(paths, False)
1721 1728 obj._filecache[self.name] = ce
1722 1729 else:
1723 1730 ce = obj._filecache[self.name]
1724 1731
1725 1732 ce.obj = value # update cached copy
1726 1733 obj.__dict__[self.sname] = value # update copy returned by obj.x
1727 1734
1728 1735
1729 1736 def extdatasource(repo, source):
1730 1737 """Gather a map of rev -> value dict from the specified source
1731 1738
1732 1739 A source spec is treated as a URL, with a special case shell: type
1733 1740 for parsing the output from a shell command.
1734 1741
1735 1742 The data is parsed as a series of newline-separated records where
1736 1743 each record is a revision specifier optionally followed by a space
1737 1744 and a freeform string value. If the revision is known locally, it
1738 1745 is converted to a rev, otherwise the record is skipped.
1739 1746
1740 1747 Note that both key and value are treated as UTF-8 and converted to
1741 1748 the local encoding. This allows uniformity between local and
1742 1749 remote data sources.
1743 1750 """
1744 1751
1745 1752 spec = repo.ui.config(b"extdata", source)
1746 1753 if not spec:
1747 1754 raise error.Abort(_(b"unknown extdata source '%s'") % source)
1748 1755
1749 1756 data = {}
1750 1757 src = proc = None
1751 1758 try:
1752 1759 if spec.startswith(b"shell:"):
1753 1760 # external commands should be run relative to the repo root
1754 1761 cmd = spec[6:]
1755 1762 proc = subprocess.Popen(
1756 1763 procutil.tonativestr(cmd),
1757 1764 shell=True,
1758 1765 bufsize=-1,
1759 1766 close_fds=procutil.closefds,
1760 1767 stdout=subprocess.PIPE,
1761 1768 cwd=procutil.tonativestr(repo.root),
1762 1769 )
1763 1770 src = proc.stdout
1764 1771 else:
1765 1772 # treat as a URL or file
1766 1773 src = url.open(repo.ui, spec)
1767 1774 for l in src:
1768 1775 if b" " in l:
1769 1776 k, v = l.strip().split(b" ", 1)
1770 1777 else:
1771 1778 k, v = l.strip(), b""
1772 1779
1773 1780 k = encoding.tolocal(k)
1774 1781 try:
1775 1782 data[revsingle(repo, k).rev()] = encoding.tolocal(v)
1776 1783 except (error.LookupError, error.RepoLookupError):
1777 1784 pass # we ignore data for nodes that don't exist locally
1778 1785 finally:
1779 1786 if proc:
1780 1787 try:
1781 1788 proc.communicate()
1782 1789 except ValueError:
1783 1790 # This happens if we started iterating src and then
1784 1791 # get a parse error on a line. It should be safe to ignore.
1785 1792 pass
1786 1793 if src:
1787 1794 src.close()
1788 1795 if proc and proc.returncode != 0:
1789 1796 raise error.Abort(
1790 1797 _(b"extdata command '%s' failed: %s")
1791 1798 % (cmd, procutil.explainexit(proc.returncode))
1792 1799 )
1793 1800
1794 1801 return data
1795 1802
1796 1803
1797 1804 class progress(object):
1798 1805 def __init__(self, ui, updatebar, topic, unit=b"", total=None):
1799 1806 self.ui = ui
1800 1807 self.pos = 0
1801 1808 self.topic = topic
1802 1809 self.unit = unit
1803 1810 self.total = total
1804 1811 self.debug = ui.configbool(b'progress', b'debug')
1805 1812 self._updatebar = updatebar
1806 1813
1807 1814 def __enter__(self):
1808 1815 return self
1809 1816
1810 1817 def __exit__(self, exc_type, exc_value, exc_tb):
1811 1818 self.complete()
1812 1819
1813 1820 def update(self, pos, item=b"", total=None):
1814 1821 assert pos is not None
1815 1822 if total:
1816 1823 self.total = total
1817 1824 self.pos = pos
1818 1825 self._updatebar(self.topic, self.pos, item, self.unit, self.total)
1819 1826 if self.debug:
1820 1827 self._printdebug(item)
1821 1828
1822 1829 def increment(self, step=1, item=b"", total=None):
1823 1830 self.update(self.pos + step, item, total)
1824 1831
1825 1832 def complete(self):
1826 1833 self.pos = None
1827 1834 self.unit = b""
1828 1835 self.total = None
1829 1836 self._updatebar(self.topic, self.pos, b"", self.unit, self.total)
1830 1837
1831 1838 def _printdebug(self, item):
1832 1839 unit = b''
1833 1840 if self.unit:
1834 1841 unit = b' ' + self.unit
1835 1842 if item:
1836 1843 item = b' ' + item
1837 1844
1838 1845 if self.total:
1839 1846 pct = 100.0 * self.pos / self.total
1840 1847 self.ui.debug(
1841 1848 b'%s:%s %d/%d%s (%4.2f%%)\n'
1842 1849 % (self.topic, item, self.pos, self.total, unit, pct)
1843 1850 )
1844 1851 else:
1845 1852 self.ui.debug(b'%s:%s %d%s\n' % (self.topic, item, self.pos, unit))
1846 1853
1847 1854
1848 1855 def gdinitconfig(ui):
1849 1856 """helper function to know if a repo should be created as general delta
1850 1857 """
1851 1858 # experimental config: format.generaldelta
1852 1859 return ui.configbool(b'format', b'generaldelta') or ui.configbool(
1853 1860 b'format', b'usegeneraldelta'
1854 1861 )
1855 1862
1856 1863
1857 1864 def gddeltaconfig(ui):
1858 1865 """helper function to know if incoming delta should be optimised
1859 1866 """
1860 1867 # experimental config: format.generaldelta
1861 1868 return ui.configbool(b'format', b'generaldelta')
1862 1869
1863 1870
1864 1871 class simplekeyvaluefile(object):
1865 1872 """A simple file with key=value lines
1866 1873
1867 1874 Keys must be alphanumerics and start with a letter, values must not
1868 1875 contain '\n' characters"""
1869 1876
1870 1877 firstlinekey = b'__firstline'
1871 1878
1872 1879 def __init__(self, vfs, path, keys=None):
1873 1880 self.vfs = vfs
1874 1881 self.path = path
1875 1882
1876 1883 def read(self, firstlinenonkeyval=False):
1877 1884 """Read the contents of a simple key-value file
1878 1885
1879 1886 'firstlinenonkeyval' indicates whether the first line of file should
1880 1887 be treated as a key-value pair or reuturned fully under the
1881 1888 __firstline key."""
1882 1889 lines = self.vfs.readlines(self.path)
1883 1890 d = {}
1884 1891 if firstlinenonkeyval:
1885 1892 if not lines:
1886 1893 e = _(b"empty simplekeyvalue file")
1887 1894 raise error.CorruptedState(e)
1888 1895 # we don't want to include '\n' in the __firstline
1889 1896 d[self.firstlinekey] = lines[0][:-1]
1890 1897 del lines[0]
1891 1898
1892 1899 try:
1893 1900 # the 'if line.strip()' part prevents us from failing on empty
1894 1901 # lines which only contain '\n' therefore are not skipped
1895 1902 # by 'if line'
1896 1903 updatedict = dict(
1897 1904 line[:-1].split(b'=', 1) for line in lines if line.strip()
1898 1905 )
1899 1906 if self.firstlinekey in updatedict:
1900 1907 e = _(b"%r can't be used as a key")
1901 1908 raise error.CorruptedState(e % self.firstlinekey)
1902 1909 d.update(updatedict)
1903 1910 except ValueError as e:
1904 1911 raise error.CorruptedState(stringutil.forcebytestr(e))
1905 1912 return d
1906 1913
1907 1914 def write(self, data, firstline=None):
1908 1915 """Write key=>value mapping to a file
1909 1916 data is a dict. Keys must be alphanumerical and start with a letter.
1910 1917 Values must not contain newline characters.
1911 1918
1912 1919 If 'firstline' is not None, it is written to file before
1913 1920 everything else, as it is, not in a key=value form"""
1914 1921 lines = []
1915 1922 if firstline is not None:
1916 1923 lines.append(b'%s\n' % firstline)
1917 1924
1918 1925 for k, v in data.items():
1919 1926 if k == self.firstlinekey:
1920 1927 e = b"key name '%s' is reserved" % self.firstlinekey
1921 1928 raise error.ProgrammingError(e)
1922 1929 if not k[0:1].isalpha():
1923 1930 e = b"keys must start with a letter in a key-value file"
1924 1931 raise error.ProgrammingError(e)
1925 1932 if not k.isalnum():
1926 1933 e = b"invalid key name in a simple key-value file"
1927 1934 raise error.ProgrammingError(e)
1928 1935 if b'\n' in v:
1929 1936 e = b"invalid value in a simple key-value file"
1930 1937 raise error.ProgrammingError(e)
1931 1938 lines.append(b"%s=%s\n" % (k, v))
1932 1939 with self.vfs(self.path, mode=b'wb', atomictemp=True) as fp:
1933 1940 fp.write(b''.join(lines))
1934 1941
1935 1942
1936 1943 _reportobsoletedsource = [
1937 1944 b'debugobsolete',
1938 1945 b'pull',
1939 1946 b'push',
1940 1947 b'serve',
1941 1948 b'unbundle',
1942 1949 ]
1943 1950
1944 1951 _reportnewcssource = [
1945 1952 b'pull',
1946 1953 b'unbundle',
1947 1954 ]
1948 1955
1949 1956
1950 1957 def prefetchfiles(repo, revmatches):
1951 1958 """Invokes the registered file prefetch functions, allowing extensions to
1952 1959 ensure the corresponding files are available locally, before the command
1953 1960 uses them.
1954 1961
1955 1962 Args:
1956 1963 revmatches: a list of (revision, match) tuples to indicate the files to
1957 1964 fetch at each revision. If any of the match elements is None, it matches
1958 1965 all files.
1959 1966 """
1960 1967
1961 1968 def _matcher(m):
1962 1969 if m:
1963 1970 assert isinstance(m, matchmod.basematcher)
1964 1971 # The command itself will complain about files that don't exist, so
1965 1972 # don't duplicate the message.
1966 1973 return matchmod.badmatch(m, lambda fn, msg: None)
1967 1974 else:
1968 1975 return matchall(repo)
1969 1976
1970 1977 revbadmatches = [(rev, _matcher(match)) for (rev, match) in revmatches]
1971 1978
1972 1979 fileprefetchhooks(repo, revbadmatches)
1973 1980
1974 1981
1975 1982 # a list of (repo, revs, match) prefetch functions
1976 1983 fileprefetchhooks = util.hooks()
1977 1984
1978 1985 # A marker that tells the evolve extension to suppress its own reporting
1979 1986 _reportstroubledchangesets = True
1980 1987
1981 1988
1982 1989 def registersummarycallback(repo, otr, txnname=b'', as_validator=False):
1983 1990 """register a callback to issue a summary after the transaction is closed
1984 1991
1985 1992 If as_validator is true, then the callbacks are registered as transaction
1986 1993 validators instead
1987 1994 """
1988 1995
1989 1996 def txmatch(sources):
1990 1997 return any(txnname.startswith(source) for source in sources)
1991 1998
1992 1999 categories = []
1993 2000
1994 2001 def reportsummary(func):
1995 2002 """decorator for report callbacks."""
1996 2003 # The repoview life cycle is shorter than the one of the actual
1997 2004 # underlying repository. So the filtered object can die before the
1998 2005 # weakref is used leading to troubles. We keep a reference to the
1999 2006 # unfiltered object and restore the filtering when retrieving the
2000 2007 # repository through the weakref.
2001 2008 filtername = repo.filtername
2002 2009 reporef = weakref.ref(repo.unfiltered())
2003 2010
2004 2011 def wrapped(tr):
2005 2012 repo = reporef()
2006 2013 if filtername:
2007 2014 assert repo is not None # help pytype
2008 2015 repo = repo.filtered(filtername)
2009 2016 func(repo, tr)
2010 2017
2011 2018 newcat = b'%02i-txnreport' % len(categories)
2012 2019 if as_validator:
2013 2020 otr.addvalidator(newcat, wrapped)
2014 2021 else:
2015 2022 otr.addpostclose(newcat, wrapped)
2016 2023 categories.append(newcat)
2017 2024 return wrapped
2018 2025
2019 2026 @reportsummary
2020 2027 def reportchangegroup(repo, tr):
2021 2028 cgchangesets = tr.changes.get(b'changegroup-count-changesets', 0)
2022 2029 cgrevisions = tr.changes.get(b'changegroup-count-revisions', 0)
2023 2030 cgfiles = tr.changes.get(b'changegroup-count-files', 0)
2024 2031 cgheads = tr.changes.get(b'changegroup-count-heads', 0)
2025 2032 if cgchangesets or cgrevisions or cgfiles:
2026 2033 htext = b""
2027 2034 if cgheads:
2028 2035 htext = _(b" (%+d heads)") % cgheads
2029 2036 msg = _(b"added %d changesets with %d changes to %d files%s\n")
2030 2037 if as_validator:
2031 2038 msg = _(b"adding %d changesets with %d changes to %d files%s\n")
2032 2039 assert repo is not None # help pytype
2033 2040 repo.ui.status(msg % (cgchangesets, cgrevisions, cgfiles, htext))
2034 2041
2035 2042 if txmatch(_reportobsoletedsource):
2036 2043
2037 2044 @reportsummary
2038 2045 def reportobsoleted(repo, tr):
2039 2046 obsoleted = obsutil.getobsoleted(repo, tr)
2040 2047 newmarkers = len(tr.changes.get(b'obsmarkers', ()))
2041 2048 if newmarkers:
2042 2049 repo.ui.status(_(b'%i new obsolescence markers\n') % newmarkers)
2043 2050 if obsoleted:
2044 2051 msg = _(b'obsoleted %i changesets\n')
2045 2052 if as_validator:
2046 2053 msg = _(b'obsoleting %i changesets\n')
2047 2054 repo.ui.status(msg % len(obsoleted))
2048 2055
2049 2056 if obsolete.isenabled(
2050 2057 repo, obsolete.createmarkersopt
2051 2058 ) and repo.ui.configbool(
2052 2059 b'experimental', b'evolution.report-instabilities'
2053 2060 ):
2054 2061 instabilitytypes = [
2055 2062 (b'orphan', b'orphan'),
2056 2063 (b'phase-divergent', b'phasedivergent'),
2057 2064 (b'content-divergent', b'contentdivergent'),
2058 2065 ]
2059 2066
2060 2067 def getinstabilitycounts(repo):
2061 2068 filtered = repo.changelog.filteredrevs
2062 2069 counts = {}
2063 2070 for instability, revset in instabilitytypes:
2064 2071 counts[instability] = len(
2065 2072 set(obsolete.getrevs(repo, revset)) - filtered
2066 2073 )
2067 2074 return counts
2068 2075
2069 2076 oldinstabilitycounts = getinstabilitycounts(repo)
2070 2077
2071 2078 @reportsummary
2072 2079 def reportnewinstabilities(repo, tr):
2073 2080 newinstabilitycounts = getinstabilitycounts(repo)
2074 2081 for instability, revset in instabilitytypes:
2075 2082 delta = (
2076 2083 newinstabilitycounts[instability]
2077 2084 - oldinstabilitycounts[instability]
2078 2085 )
2079 2086 msg = getinstabilitymessage(delta, instability)
2080 2087 if msg:
2081 2088 repo.ui.warn(msg)
2082 2089
2083 2090 if txmatch(_reportnewcssource):
2084 2091
2085 2092 @reportsummary
2086 2093 def reportnewcs(repo, tr):
2087 2094 """Report the range of new revisions pulled/unbundled."""
2088 2095 origrepolen = tr.changes.get(b'origrepolen', len(repo))
2089 2096 unfi = repo.unfiltered()
2090 2097 if origrepolen >= len(unfi):
2091 2098 return
2092 2099
2093 2100 # Compute the bounds of new visible revisions' range.
2094 2101 revs = smartset.spanset(repo, start=origrepolen)
2095 2102 if revs:
2096 2103 minrev, maxrev = repo[revs.min()], repo[revs.max()]
2097 2104
2098 2105 if minrev == maxrev:
2099 2106 revrange = minrev
2100 2107 else:
2101 2108 revrange = b'%s:%s' % (minrev, maxrev)
2102 2109 draft = len(repo.revs(b'%ld and draft()', revs))
2103 2110 secret = len(repo.revs(b'%ld and secret()', revs))
2104 2111 if not (draft or secret):
2105 2112 msg = _(b'new changesets %s\n') % revrange
2106 2113 elif draft and secret:
2107 2114 msg = _(b'new changesets %s (%d drafts, %d secrets)\n')
2108 2115 msg %= (revrange, draft, secret)
2109 2116 elif draft:
2110 2117 msg = _(b'new changesets %s (%d drafts)\n')
2111 2118 msg %= (revrange, draft)
2112 2119 elif secret:
2113 2120 msg = _(b'new changesets %s (%d secrets)\n')
2114 2121 msg %= (revrange, secret)
2115 2122 else:
2116 2123 errormsg = b'entered unreachable condition'
2117 2124 raise error.ProgrammingError(errormsg)
2118 2125 repo.ui.status(msg)
2119 2126
2120 2127 # search new changesets directly pulled as obsolete
2121 2128 duplicates = tr.changes.get(b'revduplicates', ())
2122 2129 obsadded = unfi.revs(
2123 2130 b'(%d: + %ld) and obsolete()', origrepolen, duplicates
2124 2131 )
2125 2132 cl = repo.changelog
2126 2133 extinctadded = [r for r in obsadded if r not in cl]
2127 2134 if extinctadded:
2128 2135 # They are not just obsolete, but obsolete and invisible
2129 2136 # we call them "extinct" internally but the terms have not been
2130 2137 # exposed to users.
2131 2138 msg = b'(%d other changesets obsolete on arrival)\n'
2132 2139 repo.ui.status(msg % len(extinctadded))
2133 2140
2134 2141 @reportsummary
2135 2142 def reportphasechanges(repo, tr):
2136 2143 """Report statistics of phase changes for changesets pre-existing
2137 2144 pull/unbundle.
2138 2145 """
2139 2146 origrepolen = tr.changes.get(b'origrepolen', len(repo))
2140 2147 published = []
2141 2148 for revs, (old, new) in tr.changes.get(b'phases', []):
2142 2149 if new != phases.public:
2143 2150 continue
2144 2151 published.extend(rev for rev in revs if rev < origrepolen)
2145 2152 if not published:
2146 2153 return
2147 2154 msg = _(b'%d local changesets published\n')
2148 2155 if as_validator:
2149 2156 msg = _(b'%d local changesets will be published\n')
2150 2157 repo.ui.status(msg % len(published))
2151 2158
2152 2159
2153 2160 def getinstabilitymessage(delta, instability):
2154 2161 """function to return the message to show warning about new instabilities
2155 2162
2156 2163 exists as a separate function so that extension can wrap to show more
2157 2164 information like how to fix instabilities"""
2158 2165 if delta > 0:
2159 2166 return _(b'%i new %s changesets\n') % (delta, instability)
2160 2167
2161 2168
2162 2169 def nodesummaries(repo, nodes, maxnumnodes=4):
2163 2170 if len(nodes) <= maxnumnodes or repo.ui.verbose:
2164 2171 return b' '.join(short(h) for h in nodes)
2165 2172 first = b' '.join(short(h) for h in nodes[:maxnumnodes])
2166 2173 return _(b"%s and %d others") % (first, len(nodes) - maxnumnodes)
2167 2174
2168 2175
2169 2176 def enforcesinglehead(repo, tr, desc, accountclosed=False):
2170 2177 """check that no named branch has multiple heads"""
2171 2178 if desc in (b'strip', b'repair'):
2172 2179 # skip the logic during strip
2173 2180 return
2174 2181 visible = repo.filtered(b'visible')
2175 2182 # possible improvement: we could restrict the check to affected branch
2176 2183 bm = visible.branchmap()
2177 2184 for name in bm:
2178 2185 heads = bm.branchheads(name, closed=accountclosed)
2179 2186 if len(heads) > 1:
2180 2187 msg = _(b'rejecting multiple heads on branch "%s"')
2181 2188 msg %= name
2182 2189 hint = _(b'%d heads: %s')
2183 2190 hint %= (len(heads), nodesummaries(repo, heads))
2184 2191 raise error.Abort(msg, hint=hint)
2185 2192
2186 2193
2187 2194 def wrapconvertsink(sink):
2188 2195 """Allow extensions to wrap the sink returned by convcmd.convertsink()
2189 2196 before it is used, whether or not the convert extension was formally loaded.
2190 2197 """
2191 2198 return sink
2192 2199
2193 2200
2194 2201 def unhidehashlikerevs(repo, specs, hiddentype):
2195 2202 """parse the user specs and unhide changesets whose hash or revision number
2196 2203 is passed.
2197 2204
2198 2205 hiddentype can be: 1) 'warn': warn while unhiding changesets
2199 2206 2) 'nowarn': don't warn while unhiding changesets
2200 2207
2201 2208 returns a repo object with the required changesets unhidden
2202 2209 """
2203 2210 if not repo.filtername or not repo.ui.configbool(
2204 2211 b'experimental', b'directaccess'
2205 2212 ):
2206 2213 return repo
2207 2214
2208 2215 if repo.filtername not in (b'visible', b'visible-hidden'):
2209 2216 return repo
2210 2217
2211 2218 symbols = set()
2212 2219 for spec in specs:
2213 2220 try:
2214 2221 tree = revsetlang.parse(spec)
2215 2222 except error.ParseError: # will be reported by scmutil.revrange()
2216 2223 continue
2217 2224
2218 2225 symbols.update(revsetlang.gethashlikesymbols(tree))
2219 2226
2220 2227 if not symbols:
2221 2228 return repo
2222 2229
2223 2230 revs = _getrevsfromsymbols(repo, symbols)
2224 2231
2225 2232 if not revs:
2226 2233 return repo
2227 2234
2228 2235 if hiddentype == b'warn':
2229 2236 unfi = repo.unfiltered()
2230 2237 revstr = b", ".join([pycompat.bytestr(unfi[l]) for l in revs])
2231 2238 repo.ui.warn(
2232 2239 _(
2233 2240 b"warning: accessing hidden changesets for write "
2234 2241 b"operation: %s\n"
2235 2242 )
2236 2243 % revstr
2237 2244 )
2238 2245
2239 2246 # we have to use new filtername to separate branch/tags cache until we can
2240 2247 # disbale these cache when revisions are dynamically pinned.
2241 2248 return repo.filtered(b'visible-hidden', revs)
2242 2249
2243 2250
2244 2251 def _getrevsfromsymbols(repo, symbols):
2245 2252 """parse the list of symbols and returns a set of revision numbers of hidden
2246 2253 changesets present in symbols"""
2247 2254 revs = set()
2248 2255 unfi = repo.unfiltered()
2249 2256 unficl = unfi.changelog
2250 2257 cl = repo.changelog
2251 2258 tiprev = len(unficl)
2252 2259 allowrevnums = repo.ui.configbool(b'experimental', b'directaccess.revnums')
2253 2260 for s in symbols:
2254 2261 try:
2255 2262 n = int(s)
2256 2263 if n <= tiprev:
2257 2264 if not allowrevnums:
2258 2265 continue
2259 2266 else:
2260 2267 if n not in cl:
2261 2268 revs.add(n)
2262 2269 continue
2263 2270 except ValueError:
2264 2271 pass
2265 2272
2266 2273 try:
2267 2274 s = resolvehexnodeidprefix(unfi, s)
2268 2275 except (error.LookupError, error.WdirUnsupported):
2269 2276 s = None
2270 2277
2271 2278 if s is not None:
2272 2279 rev = unficl.rev(s)
2273 2280 if rev not in cl:
2274 2281 revs.add(rev)
2275 2282
2276 2283 return revs
2277 2284
2278 2285
2279 2286 def bookmarkrevs(repo, mark):
2280 2287 """
2281 2288 Select revisions reachable by a given bookmark
2282 2289 """
2283 2290 return repo.revs(
2284 2291 b"ancestors(bookmark(%s)) - "
2285 2292 b"ancestors(head() and not bookmark(%s)) - "
2286 2293 b"ancestors(bookmark() and not bookmark(%s))",
2287 2294 mark,
2288 2295 mark,
2289 2296 mark,
2290 2297 )
@@ -1,2372 +1,2374 b''
1 1 # ui.py - user interface bits for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
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 from __future__ import absolute_import
9 9
10 10 import collections
11 11 import contextlib
12 12 import datetime
13 13 import errno
14 14 import getpass
15 15 import inspect
16 16 import os
17 17 import re
18 18 import signal
19 19 import socket
20 20 import subprocess
21 21 import sys
22 22 import traceback
23 23
24 24 from .i18n import _
25 25 from .node import hex
26 26 from .pycompat import (
27 27 getattr,
28 28 open,
29 29 setattr,
30 30 )
31 31
32 32 from . import (
33 33 color,
34 34 config,
35 35 configitems,
36 36 encoding,
37 37 error,
38 38 formatter,
39 39 loggingutil,
40 40 progress,
41 41 pycompat,
42 42 rcutil,
43 43 scmutil,
44 44 util,
45 45 )
46 46 from .utils import (
47 47 dateutil,
48 48 procutil,
49 49 resourceutil,
50 50 stringutil,
51 51 )
52 52
53 53 urlreq = util.urlreq
54 54
55 55 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
56 56 _keepalnum = b''.join(
57 57 c for c in map(pycompat.bytechr, range(256)) if not c.isalnum()
58 58 )
59 59
60 60 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
61 61 tweakrc = b"""
62 62 [ui]
63 # Gives detailed exit codes for input/user errors, config errors, etc.
64 detailed-exit-code = True
63 65 # The rollback command is dangerous. As a rule, don't use it.
64 66 rollback = False
65 67 # Make `hg status` report copy information
66 68 statuscopies = yes
67 69 # Prefer curses UIs when available. Revert to plain-text with `text`.
68 70 interface = curses
69 71 # Make compatible commands emit cwd-relative paths by default.
70 72 relative-paths = yes
71 73
72 74 [commands]
73 75 # Grep working directory by default.
74 76 grep.all-files = True
75 77 # Refuse to perform an `hg update` that would cause a file content merge
76 78 update.check = noconflict
77 79 # Show conflicts information in `hg status`
78 80 status.verbose = True
79 81 # Make `hg resolve` with no action (like `-m`) fail instead of re-merging.
80 82 resolve.explicit-re-merge = True
81 83
82 84 [diff]
83 85 git = 1
84 86 showfunc = 1
85 87 word-diff = 1
86 88 """
87 89
88 90 samplehgrcs = {
89 91 b'user': b"""# example user config (see 'hg help config' for more info)
90 92 [ui]
91 93 # name and email, e.g.
92 94 # username = Jane Doe <jdoe@example.com>
93 95 username =
94 96
95 97 # We recommend enabling tweakdefaults to get slight improvements to
96 98 # the UI over time. Make sure to set HGPLAIN in the environment when
97 99 # writing scripts!
98 100 # tweakdefaults = True
99 101
100 102 # uncomment to disable color in command output
101 103 # (see 'hg help color' for details)
102 104 # color = never
103 105
104 106 # uncomment to disable command output pagination
105 107 # (see 'hg help pager' for details)
106 108 # paginate = never
107 109
108 110 [extensions]
109 111 # uncomment the lines below to enable some popular extensions
110 112 # (see 'hg help extensions' for more info)
111 113 #
112 114 # histedit =
113 115 # rebase =
114 116 # uncommit =
115 117 """,
116 118 b'cloned': b"""# example repository config (see 'hg help config' for more info)
117 119 [paths]
118 120 default = %s
119 121
120 122 # path aliases to other clones of this repo in URLs or filesystem paths
121 123 # (see 'hg help config.paths' for more info)
122 124 #
123 125 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
124 126 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
125 127 # my-clone = /home/jdoe/jdoes-clone
126 128
127 129 [ui]
128 130 # name and email (local to this repository, optional), e.g.
129 131 # username = Jane Doe <jdoe@example.com>
130 132 """,
131 133 b'local': b"""# example repository config (see 'hg help config' for more info)
132 134 [paths]
133 135 # path aliases to other clones of this repo in URLs or filesystem paths
134 136 # (see 'hg help config.paths' for more info)
135 137 #
136 138 # default = http://example.com/hg/example-repo
137 139 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
138 140 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
139 141 # my-clone = /home/jdoe/jdoes-clone
140 142
141 143 [ui]
142 144 # name and email (local to this repository, optional), e.g.
143 145 # username = Jane Doe <jdoe@example.com>
144 146 """,
145 147 b'global': b"""# example system-wide hg config (see 'hg help config' for more info)
146 148
147 149 [ui]
148 150 # uncomment to disable color in command output
149 151 # (see 'hg help color' for details)
150 152 # color = never
151 153
152 154 # uncomment to disable command output pagination
153 155 # (see 'hg help pager' for details)
154 156 # paginate = never
155 157
156 158 [extensions]
157 159 # uncomment the lines below to enable some popular extensions
158 160 # (see 'hg help extensions' for more info)
159 161 #
160 162 # blackbox =
161 163 # churn =
162 164 """,
163 165 }
164 166
165 167
166 168 def _maybestrurl(maybebytes):
167 169 return pycompat.rapply(pycompat.strurl, maybebytes)
168 170
169 171
170 172 def _maybebytesurl(maybestr):
171 173 return pycompat.rapply(pycompat.bytesurl, maybestr)
172 174
173 175
174 176 class httppasswordmgrdbproxy(object):
175 177 """Delays loading urllib2 until it's needed."""
176 178
177 179 def __init__(self):
178 180 self._mgr = None
179 181
180 182 def _get_mgr(self):
181 183 if self._mgr is None:
182 184 self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
183 185 return self._mgr
184 186
185 187 def add_password(self, realm, uris, user, passwd):
186 188 return self._get_mgr().add_password(
187 189 _maybestrurl(realm),
188 190 _maybestrurl(uris),
189 191 _maybestrurl(user),
190 192 _maybestrurl(passwd),
191 193 )
192 194
193 195 def find_user_password(self, realm, uri):
194 196 mgr = self._get_mgr()
195 197 return _maybebytesurl(
196 198 mgr.find_user_password(_maybestrurl(realm), _maybestrurl(uri))
197 199 )
198 200
199 201
200 202 def _catchterm(*args):
201 203 raise error.SignalInterrupt
202 204
203 205
204 206 # unique object used to detect no default value has been provided when
205 207 # retrieving configuration value.
206 208 _unset = object()
207 209
208 210 # _reqexithandlers: callbacks run at the end of a request
209 211 _reqexithandlers = []
210 212
211 213
212 214 class ui(object):
213 215 def __init__(self, src=None):
214 216 """Create a fresh new ui object if no src given
215 217
216 218 Use uimod.ui.load() to create a ui which knows global and user configs.
217 219 In most cases, you should use ui.copy() to create a copy of an existing
218 220 ui object.
219 221 """
220 222 # _buffers: used for temporary capture of output
221 223 self._buffers = []
222 224 # 3-tuple describing how each buffer in the stack behaves.
223 225 # Values are (capture stderr, capture subprocesses, apply labels).
224 226 self._bufferstates = []
225 227 # When a buffer is active, defines whether we are expanding labels.
226 228 # This exists to prevent an extra list lookup.
227 229 self._bufferapplylabels = None
228 230 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
229 231 self._reportuntrusted = True
230 232 self._knownconfig = configitems.coreitems
231 233 self._ocfg = config.config() # overlay
232 234 self._tcfg = config.config() # trusted
233 235 self._ucfg = config.config() # untrusted
234 236 self._trustusers = set()
235 237 self._trustgroups = set()
236 238 self.callhooks = True
237 239 # Insecure server connections requested.
238 240 self.insecureconnections = False
239 241 # Blocked time
240 242 self.logblockedtimes = False
241 243 # color mode: see mercurial/color.py for possible value
242 244 self._colormode = None
243 245 self._terminfoparams = {}
244 246 self._styles = {}
245 247 self._uninterruptible = False
246 248 self.showtimestamp = False
247 249
248 250 if src:
249 251 self._fout = src._fout
250 252 self._ferr = src._ferr
251 253 self._fin = src._fin
252 254 self._fmsg = src._fmsg
253 255 self._fmsgout = src._fmsgout
254 256 self._fmsgerr = src._fmsgerr
255 257 self._finoutredirected = src._finoutredirected
256 258 self._loggers = src._loggers.copy()
257 259 self.pageractive = src.pageractive
258 260 self._disablepager = src._disablepager
259 261 self._tweaked = src._tweaked
260 262
261 263 self._tcfg = src._tcfg.copy()
262 264 self._ucfg = src._ucfg.copy()
263 265 self._ocfg = src._ocfg.copy()
264 266 self._trustusers = src._trustusers.copy()
265 267 self._trustgroups = src._trustgroups.copy()
266 268 self.environ = src.environ
267 269 self.callhooks = src.callhooks
268 270 self.insecureconnections = src.insecureconnections
269 271 self._colormode = src._colormode
270 272 self._terminfoparams = src._terminfoparams.copy()
271 273 self._styles = src._styles.copy()
272 274
273 275 self.fixconfig()
274 276
275 277 self.httppasswordmgrdb = src.httppasswordmgrdb
276 278 self._blockedtimes = src._blockedtimes
277 279 else:
278 280 self._fout = procutil.stdout
279 281 self._ferr = procutil.stderr
280 282 self._fin = procutil.stdin
281 283 self._fmsg = None
282 284 self._fmsgout = self.fout # configurable
283 285 self._fmsgerr = self.ferr # configurable
284 286 self._finoutredirected = False
285 287 self._loggers = {}
286 288 self.pageractive = False
287 289 self._disablepager = False
288 290 self._tweaked = False
289 291
290 292 # shared read-only environment
291 293 self.environ = encoding.environ
292 294
293 295 self.httppasswordmgrdb = httppasswordmgrdbproxy()
294 296 self._blockedtimes = collections.defaultdict(int)
295 297
296 298 allowed = self.configlist(b'experimental', b'exportableenviron')
297 299 if b'*' in allowed:
298 300 self._exportableenviron = self.environ
299 301 else:
300 302 self._exportableenviron = {}
301 303 for k in allowed:
302 304 if k in self.environ:
303 305 self._exportableenviron[k] = self.environ[k]
304 306
305 307 @classmethod
306 308 def load(cls):
307 309 """Create a ui and load global and user configs"""
308 310 u = cls()
309 311 # we always trust global config files and environment variables
310 312 for t, f in rcutil.rccomponents():
311 313 if t == b'path':
312 314 u.readconfig(f, trust=True)
313 315 elif t == b'resource':
314 316 u.read_resource_config(f, trust=True)
315 317 elif t == b'items':
316 318 sections = set()
317 319 for section, name, value, source in f:
318 320 # do not set u._ocfg
319 321 # XXX clean this up once immutable config object is a thing
320 322 u._tcfg.set(section, name, value, source)
321 323 u._ucfg.set(section, name, value, source)
322 324 sections.add(section)
323 325 for section in sections:
324 326 u.fixconfig(section=section)
325 327 else:
326 328 raise error.ProgrammingError(b'unknown rctype: %s' % t)
327 329 u._maybetweakdefaults()
328 330 return u
329 331
330 332 def _maybetweakdefaults(self):
331 333 if not self.configbool(b'ui', b'tweakdefaults'):
332 334 return
333 335 if self._tweaked or self.plain(b'tweakdefaults'):
334 336 return
335 337
336 338 # Note: it is SUPER IMPORTANT that you set self._tweaked to
337 339 # True *before* any calls to setconfig(), otherwise you'll get
338 340 # infinite recursion between setconfig and this method.
339 341 #
340 342 # TODO: We should extract an inner method in setconfig() to
341 343 # avoid this weirdness.
342 344 self._tweaked = True
343 345 tmpcfg = config.config()
344 346 tmpcfg.parse(b'<tweakdefaults>', tweakrc)
345 347 for section in tmpcfg:
346 348 for name, value in tmpcfg.items(section):
347 349 if not self.hasconfig(section, name):
348 350 self.setconfig(section, name, value, b"<tweakdefaults>")
349 351
350 352 def copy(self):
351 353 return self.__class__(self)
352 354
353 355 def resetstate(self):
354 356 """Clear internal state that shouldn't persist across commands"""
355 357 if self._progbar:
356 358 self._progbar.resetstate() # reset last-print time of progress bar
357 359 self.httppasswordmgrdb = httppasswordmgrdbproxy()
358 360
359 361 @contextlib.contextmanager
360 362 def timeblockedsection(self, key):
361 363 # this is open-coded below - search for timeblockedsection to find them
362 364 starttime = util.timer()
363 365 try:
364 366 yield
365 367 finally:
366 368 self._blockedtimes[key + b'_blocked'] += (
367 369 util.timer() - starttime
368 370 ) * 1000
369 371
370 372 @contextlib.contextmanager
371 373 def uninterruptible(self):
372 374 """Mark an operation as unsafe.
373 375
374 376 Most operations on a repository are safe to interrupt, but a
375 377 few are risky (for example repair.strip). This context manager
376 378 lets you advise Mercurial that something risky is happening so
377 379 that control-C etc can be blocked if desired.
378 380 """
379 381 enabled = self.configbool(b'experimental', b'nointerrupt')
380 382 if enabled and self.configbool(
381 383 b'experimental', b'nointerrupt-interactiveonly'
382 384 ):
383 385 enabled = self.interactive()
384 386 if self._uninterruptible or not enabled:
385 387 # if nointerrupt support is turned off, the process isn't
386 388 # interactive, or we're already in an uninterruptible
387 389 # block, do nothing.
388 390 yield
389 391 return
390 392
391 393 def warn():
392 394 self.warn(_(b"shutting down cleanly\n"))
393 395 self.warn(
394 396 _(b"press ^C again to terminate immediately (dangerous)\n")
395 397 )
396 398 return True
397 399
398 400 with procutil.uninterruptible(warn):
399 401 try:
400 402 self._uninterruptible = True
401 403 yield
402 404 finally:
403 405 self._uninterruptible = False
404 406
405 407 def formatter(self, topic, opts):
406 408 return formatter.formatter(self, self, topic, opts)
407 409
408 410 def _trusted(self, fp, f):
409 411 st = util.fstat(fp)
410 412 if util.isowner(st):
411 413 return True
412 414
413 415 tusers, tgroups = self._trustusers, self._trustgroups
414 416 if b'*' in tusers or b'*' in tgroups:
415 417 return True
416 418
417 419 user = util.username(st.st_uid)
418 420 group = util.groupname(st.st_gid)
419 421 if user in tusers or group in tgroups or user == util.username():
420 422 return True
421 423
422 424 if self._reportuntrusted:
423 425 self.warn(
424 426 _(
425 427 b'not trusting file %s from untrusted '
426 428 b'user %s, group %s\n'
427 429 )
428 430 % (f, user, group)
429 431 )
430 432 return False
431 433
432 434 def read_resource_config(
433 435 self, name, root=None, trust=False, sections=None, remap=None
434 436 ):
435 437 try:
436 438 fp = resourceutil.open_resource(name[0], name[1])
437 439 except IOError:
438 440 if not sections: # ignore unless we were looking for something
439 441 return
440 442 raise
441 443
442 444 self._readconfig(
443 445 b'resource:%s.%s' % name, fp, root, trust, sections, remap
444 446 )
445 447
446 448 def readconfig(
447 449 self, filename, root=None, trust=False, sections=None, remap=None
448 450 ):
449 451 try:
450 452 fp = open(filename, 'rb')
451 453 except IOError:
452 454 if not sections: # ignore unless we were looking for something
453 455 return
454 456 raise
455 457
456 458 self._readconfig(filename, fp, root, trust, sections, remap)
457 459
458 460 def _readconfig(
459 461 self, filename, fp, root=None, trust=False, sections=None, remap=None
460 462 ):
461 463 with fp:
462 464 cfg = config.config()
463 465 trusted = sections or trust or self._trusted(fp, filename)
464 466
465 467 try:
466 468 cfg.read(filename, fp, sections=sections, remap=remap)
467 469 except error.ParseError as inst:
468 470 if trusted:
469 471 raise
470 472 self.warn(_(b'ignored: %s\n') % stringutil.forcebytestr(inst))
471 473
472 474 self._applyconfig(cfg, trusted, root)
473 475
474 476 def applyconfig(self, configitems, source=b"", root=None):
475 477 """Add configitems from a non-file source. Unlike with ``setconfig()``,
476 478 they can be overridden by subsequent config file reads. The items are
477 479 in the same format as ``configoverride()``, namely a dict of the
478 480 following structures: {(section, name) : value}
479 481
480 482 Typically this is used by extensions that inject themselves into the
481 483 config file load procedure by monkeypatching ``localrepo.loadhgrc()``.
482 484 """
483 485 cfg = config.config()
484 486
485 487 for (section, name), value in configitems.items():
486 488 cfg.set(section, name, value, source)
487 489
488 490 self._applyconfig(cfg, True, root)
489 491
490 492 def _applyconfig(self, cfg, trusted, root):
491 493 if self.plain():
492 494 for k in (
493 495 b'debug',
494 496 b'fallbackencoding',
495 497 b'quiet',
496 498 b'slash',
497 499 b'logtemplate',
498 500 b'message-output',
499 501 b'statuscopies',
500 502 b'style',
501 503 b'traceback',
502 504 b'verbose',
503 505 ):
504 506 if k in cfg[b'ui']:
505 507 del cfg[b'ui'][k]
506 508 for k, v in cfg.items(b'defaults'):
507 509 del cfg[b'defaults'][k]
508 510 for k, v in cfg.items(b'commands'):
509 511 del cfg[b'commands'][k]
510 512 for k, v in cfg.items(b'command-templates'):
511 513 del cfg[b'command-templates'][k]
512 514 # Don't remove aliases from the configuration if in the exceptionlist
513 515 if self.plain(b'alias'):
514 516 for k, v in cfg.items(b'alias'):
515 517 del cfg[b'alias'][k]
516 518 if self.plain(b'revsetalias'):
517 519 for k, v in cfg.items(b'revsetalias'):
518 520 del cfg[b'revsetalias'][k]
519 521 if self.plain(b'templatealias'):
520 522 for k, v in cfg.items(b'templatealias'):
521 523 del cfg[b'templatealias'][k]
522 524
523 525 if trusted:
524 526 self._tcfg.update(cfg)
525 527 self._tcfg.update(self._ocfg)
526 528 self._ucfg.update(cfg)
527 529 self._ucfg.update(self._ocfg)
528 530
529 531 if root is None:
530 532 root = os.path.expanduser(b'~')
531 533 self.fixconfig(root=root)
532 534
533 535 def fixconfig(self, root=None, section=None):
534 536 if section in (None, b'paths'):
535 537 # expand vars and ~
536 538 # translate paths relative to root (or home) into absolute paths
537 539 root = root or encoding.getcwd()
538 540 for c in self._tcfg, self._ucfg, self._ocfg:
539 541 for n, p in c.items(b'paths'):
540 542 # Ignore sub-options.
541 543 if b':' in n:
542 544 continue
543 545 if not p:
544 546 continue
545 547 if b'%%' in p:
546 548 s = self.configsource(b'paths', n) or b'none'
547 549 self.warn(
548 550 _(b"(deprecated '%%' in path %s=%s from %s)\n")
549 551 % (n, p, s)
550 552 )
551 553 p = p.replace(b'%%', b'%')
552 554 p = util.expandpath(p)
553 555 if not util.hasscheme(p) and not os.path.isabs(p):
554 556 p = os.path.normpath(os.path.join(root, p))
555 557 c.set(b"paths", n, p)
556 558
557 559 if section in (None, b'ui'):
558 560 # update ui options
559 561 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
560 562 self.debugflag = self.configbool(b'ui', b'debug')
561 563 self.verbose = self.debugflag or self.configbool(b'ui', b'verbose')
562 564 self.quiet = not self.debugflag and self.configbool(b'ui', b'quiet')
563 565 if self.verbose and self.quiet:
564 566 self.quiet = self.verbose = False
565 567 self._reportuntrusted = self.debugflag or self.configbool(
566 568 b"ui", b"report_untrusted"
567 569 )
568 570 self.showtimestamp = self.configbool(b'ui', b'timestamp-output')
569 571 self.tracebackflag = self.configbool(b'ui', b'traceback')
570 572 self.logblockedtimes = self.configbool(b'ui', b'logblockedtimes')
571 573
572 574 if section in (None, b'trusted'):
573 575 # update trust information
574 576 self._trustusers.update(self.configlist(b'trusted', b'users'))
575 577 self._trustgroups.update(self.configlist(b'trusted', b'groups'))
576 578
577 579 if section in (None, b'devel', b'ui') and self.debugflag:
578 580 tracked = set()
579 581 if self.configbool(b'devel', b'debug.extensions'):
580 582 tracked.add(b'extension')
581 583 if tracked:
582 584 logger = loggingutil.fileobjectlogger(self._ferr, tracked)
583 585 self.setlogger(b'debug', logger)
584 586
585 587 def backupconfig(self, section, item):
586 588 return (
587 589 self._ocfg.backup(section, item),
588 590 self._tcfg.backup(section, item),
589 591 self._ucfg.backup(section, item),
590 592 )
591 593
592 594 def restoreconfig(self, data):
593 595 self._ocfg.restore(data[0])
594 596 self._tcfg.restore(data[1])
595 597 self._ucfg.restore(data[2])
596 598
597 599 def setconfig(self, section, name, value, source=b''):
598 600 for cfg in (self._ocfg, self._tcfg, self._ucfg):
599 601 cfg.set(section, name, value, source)
600 602 self.fixconfig(section=section)
601 603 self._maybetweakdefaults()
602 604
603 605 def _data(self, untrusted):
604 606 return untrusted and self._ucfg or self._tcfg
605 607
606 608 def configsource(self, section, name, untrusted=False):
607 609 return self._data(untrusted).source(section, name)
608 610
609 611 def config(self, section, name, default=_unset, untrusted=False):
610 612 """return the plain string version of a config"""
611 613 value = self._config(
612 614 section, name, default=default, untrusted=untrusted
613 615 )
614 616 if value is _unset:
615 617 return None
616 618 return value
617 619
618 620 def _config(self, section, name, default=_unset, untrusted=False):
619 621 value = itemdefault = default
620 622 item = self._knownconfig.get(section, {}).get(name)
621 623 alternates = [(section, name)]
622 624
623 625 if item is not None:
624 626 alternates.extend(item.alias)
625 627 if callable(item.default):
626 628 itemdefault = item.default()
627 629 else:
628 630 itemdefault = item.default
629 631 else:
630 632 msg = b"accessing unregistered config item: '%s.%s'"
631 633 msg %= (section, name)
632 634 self.develwarn(msg, 2, b'warn-config-unknown')
633 635
634 636 if default is _unset:
635 637 if item is None:
636 638 value = default
637 639 elif item.default is configitems.dynamicdefault:
638 640 value = None
639 641 msg = b"config item requires an explicit default value: '%s.%s'"
640 642 msg %= (section, name)
641 643 self.develwarn(msg, 2, b'warn-config-default')
642 644 else:
643 645 value = itemdefault
644 646 elif (
645 647 item is not None
646 648 and item.default is not configitems.dynamicdefault
647 649 and default != itemdefault
648 650 ):
649 651 msg = (
650 652 b"specifying a mismatched default value for a registered "
651 653 b"config item: '%s.%s' '%s'"
652 654 )
653 655 msg %= (section, name, pycompat.bytestr(default))
654 656 self.develwarn(msg, 2, b'warn-config-default')
655 657
656 658 for s, n in alternates:
657 659 candidate = self._data(untrusted).get(s, n, None)
658 660 if candidate is not None:
659 661 value = candidate
660 662 break
661 663
662 664 if self.debugflag and not untrusted and self._reportuntrusted:
663 665 for s, n in alternates:
664 666 uvalue = self._ucfg.get(s, n)
665 667 if uvalue is not None and uvalue != value:
666 668 self.debug(
667 669 b"ignoring untrusted configuration option "
668 670 b"%s.%s = %s\n" % (s, n, uvalue)
669 671 )
670 672 return value
671 673
672 674 def configsuboptions(self, section, name, default=_unset, untrusted=False):
673 675 """Get a config option and all sub-options.
674 676
675 677 Some config options have sub-options that are declared with the
676 678 format "key:opt = value". This method is used to return the main
677 679 option and all its declared sub-options.
678 680
679 681 Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
680 682 is a dict of defined sub-options where keys and values are strings.
681 683 """
682 684 main = self.config(section, name, default, untrusted=untrusted)
683 685 data = self._data(untrusted)
684 686 sub = {}
685 687 prefix = b'%s:' % name
686 688 for k, v in data.items(section):
687 689 if k.startswith(prefix):
688 690 sub[k[len(prefix) :]] = v
689 691
690 692 if self.debugflag and not untrusted and self._reportuntrusted:
691 693 for k, v in sub.items():
692 694 uvalue = self._ucfg.get(section, b'%s:%s' % (name, k))
693 695 if uvalue is not None and uvalue != v:
694 696 self.debug(
695 697 b'ignoring untrusted configuration option '
696 698 b'%s:%s.%s = %s\n' % (section, name, k, uvalue)
697 699 )
698 700
699 701 return main, sub
700 702
701 703 def configpath(self, section, name, default=_unset, untrusted=False):
702 704 """get a path config item, expanded relative to repo root or config
703 705 file"""
704 706 v = self.config(section, name, default, untrusted)
705 707 if v is None:
706 708 return None
707 709 if not os.path.isabs(v) or b"://" not in v:
708 710 src = self.configsource(section, name, untrusted)
709 711 if b':' in src:
710 712 base = os.path.dirname(src.rsplit(b':')[0])
711 713 v = os.path.join(base, os.path.expanduser(v))
712 714 return v
713 715
714 716 def configbool(self, section, name, default=_unset, untrusted=False):
715 717 """parse a configuration element as a boolean
716 718
717 719 >>> u = ui(); s = b'foo'
718 720 >>> u.setconfig(s, b'true', b'yes')
719 721 >>> u.configbool(s, b'true')
720 722 True
721 723 >>> u.setconfig(s, b'false', b'no')
722 724 >>> u.configbool(s, b'false')
723 725 False
724 726 >>> u.configbool(s, b'unknown')
725 727 False
726 728 >>> u.configbool(s, b'unknown', True)
727 729 True
728 730 >>> u.setconfig(s, b'invalid', b'somevalue')
729 731 >>> u.configbool(s, b'invalid')
730 732 Traceback (most recent call last):
731 733 ...
732 734 ConfigError: foo.invalid is not a boolean ('somevalue')
733 735 """
734 736
735 737 v = self._config(section, name, default, untrusted=untrusted)
736 738 if v is None:
737 739 return v
738 740 if v is _unset:
739 741 if default is _unset:
740 742 return False
741 743 return default
742 744 if isinstance(v, bool):
743 745 return v
744 746 b = stringutil.parsebool(v)
745 747 if b is None:
746 748 raise error.ConfigError(
747 749 _(b"%s.%s is not a boolean ('%s')") % (section, name, v)
748 750 )
749 751 return b
750 752
751 753 def configwith(
752 754 self, convert, section, name, default=_unset, desc=None, untrusted=False
753 755 ):
754 756 """parse a configuration element with a conversion function
755 757
756 758 >>> u = ui(); s = b'foo'
757 759 >>> u.setconfig(s, b'float1', b'42')
758 760 >>> u.configwith(float, s, b'float1')
759 761 42.0
760 762 >>> u.setconfig(s, b'float2', b'-4.25')
761 763 >>> u.configwith(float, s, b'float2')
762 764 -4.25
763 765 >>> u.configwith(float, s, b'unknown', 7)
764 766 7.0
765 767 >>> u.setconfig(s, b'invalid', b'somevalue')
766 768 >>> u.configwith(float, s, b'invalid')
767 769 Traceback (most recent call last):
768 770 ...
769 771 ConfigError: foo.invalid is not a valid float ('somevalue')
770 772 >>> u.configwith(float, s, b'invalid', desc=b'womble')
771 773 Traceback (most recent call last):
772 774 ...
773 775 ConfigError: foo.invalid is not a valid womble ('somevalue')
774 776 """
775 777
776 778 v = self.config(section, name, default, untrusted)
777 779 if v is None:
778 780 return v # do not attempt to convert None
779 781 try:
780 782 return convert(v)
781 783 except (ValueError, error.ParseError):
782 784 if desc is None:
783 785 desc = pycompat.sysbytes(convert.__name__)
784 786 raise error.ConfigError(
785 787 _(b"%s.%s is not a valid %s ('%s')") % (section, name, desc, v)
786 788 )
787 789
788 790 def configint(self, section, name, default=_unset, untrusted=False):
789 791 """parse a configuration element as an integer
790 792
791 793 >>> u = ui(); s = b'foo'
792 794 >>> u.setconfig(s, b'int1', b'42')
793 795 >>> u.configint(s, b'int1')
794 796 42
795 797 >>> u.setconfig(s, b'int2', b'-42')
796 798 >>> u.configint(s, b'int2')
797 799 -42
798 800 >>> u.configint(s, b'unknown', 7)
799 801 7
800 802 >>> u.setconfig(s, b'invalid', b'somevalue')
801 803 >>> u.configint(s, b'invalid')
802 804 Traceback (most recent call last):
803 805 ...
804 806 ConfigError: foo.invalid is not a valid integer ('somevalue')
805 807 """
806 808
807 809 return self.configwith(
808 810 int, section, name, default, b'integer', untrusted
809 811 )
810 812
811 813 def configbytes(self, section, name, default=_unset, untrusted=False):
812 814 """parse a configuration element as a quantity in bytes
813 815
814 816 Units can be specified as b (bytes), k or kb (kilobytes), m or
815 817 mb (megabytes), g or gb (gigabytes).
816 818
817 819 >>> u = ui(); s = b'foo'
818 820 >>> u.setconfig(s, b'val1', b'42')
819 821 >>> u.configbytes(s, b'val1')
820 822 42
821 823 >>> u.setconfig(s, b'val2', b'42.5 kb')
822 824 >>> u.configbytes(s, b'val2')
823 825 43520
824 826 >>> u.configbytes(s, b'unknown', b'7 MB')
825 827 7340032
826 828 >>> u.setconfig(s, b'invalid', b'somevalue')
827 829 >>> u.configbytes(s, b'invalid')
828 830 Traceback (most recent call last):
829 831 ...
830 832 ConfigError: foo.invalid is not a byte quantity ('somevalue')
831 833 """
832 834
833 835 value = self._config(section, name, default, untrusted)
834 836 if value is _unset:
835 837 if default is _unset:
836 838 default = 0
837 839 value = default
838 840 if not isinstance(value, bytes):
839 841 return value
840 842 try:
841 843 return util.sizetoint(value)
842 844 except error.ParseError:
843 845 raise error.ConfigError(
844 846 _(b"%s.%s is not a byte quantity ('%s')")
845 847 % (section, name, value)
846 848 )
847 849
848 850 def configlist(self, section, name, default=_unset, untrusted=False):
849 851 """parse a configuration element as a list of comma/space separated
850 852 strings
851 853
852 854 >>> u = ui(); s = b'foo'
853 855 >>> u.setconfig(s, b'list1', b'this,is "a small" ,test')
854 856 >>> u.configlist(s, b'list1')
855 857 ['this', 'is', 'a small', 'test']
856 858 >>> u.setconfig(s, b'list2', b'this, is "a small" , test ')
857 859 >>> u.configlist(s, b'list2')
858 860 ['this', 'is', 'a small', 'test']
859 861 """
860 862 # default is not always a list
861 863 v = self.configwith(
862 864 config.parselist, section, name, default, b'list', untrusted
863 865 )
864 866 if isinstance(v, bytes):
865 867 return config.parselist(v)
866 868 elif v is None:
867 869 return []
868 870 return v
869 871
870 872 def configdate(self, section, name, default=_unset, untrusted=False):
871 873 """parse a configuration element as a tuple of ints
872 874
873 875 >>> u = ui(); s = b'foo'
874 876 >>> u.setconfig(s, b'date', b'0 0')
875 877 >>> u.configdate(s, b'date')
876 878 (0, 0)
877 879 """
878 880 if self.config(section, name, default, untrusted):
879 881 return self.configwith(
880 882 dateutil.parsedate, section, name, default, b'date', untrusted
881 883 )
882 884 if default is _unset:
883 885 return None
884 886 return default
885 887
886 888 def configdefault(self, section, name):
887 889 """returns the default value of the config item"""
888 890 item = self._knownconfig.get(section, {}).get(name)
889 891 itemdefault = None
890 892 if item is not None:
891 893 if callable(item.default):
892 894 itemdefault = item.default()
893 895 else:
894 896 itemdefault = item.default
895 897 return itemdefault
896 898
897 899 def hasconfig(self, section, name, untrusted=False):
898 900 return self._data(untrusted).hasitem(section, name)
899 901
900 902 def has_section(self, section, untrusted=False):
901 903 '''tell whether section exists in config.'''
902 904 return section in self._data(untrusted)
903 905
904 906 def configitems(self, section, untrusted=False, ignoresub=False):
905 907 items = self._data(untrusted).items(section)
906 908 if ignoresub:
907 909 items = [i for i in items if b':' not in i[0]]
908 910 if self.debugflag and not untrusted and self._reportuntrusted:
909 911 for k, v in self._ucfg.items(section):
910 912 if self._tcfg.get(section, k) != v:
911 913 self.debug(
912 914 b"ignoring untrusted configuration option "
913 915 b"%s.%s = %s\n" % (section, k, v)
914 916 )
915 917 return items
916 918
917 919 def walkconfig(self, untrusted=False):
918 920 cfg = self._data(untrusted)
919 921 for section in cfg.sections():
920 922 for name, value in self.configitems(section, untrusted):
921 923 yield section, name, value
922 924
923 925 def plain(self, feature=None):
924 926 '''is plain mode active?
925 927
926 928 Plain mode means that all configuration variables which affect
927 929 the behavior and output of Mercurial should be
928 930 ignored. Additionally, the output should be stable,
929 931 reproducible and suitable for use in scripts or applications.
930 932
931 933 The only way to trigger plain mode is by setting either the
932 934 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
933 935
934 936 The return value can either be
935 937 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
936 938 - False if feature is disabled by default and not included in HGPLAIN
937 939 - True otherwise
938 940 '''
939 941 if (
940 942 b'HGPLAIN' not in encoding.environ
941 943 and b'HGPLAINEXCEPT' not in encoding.environ
942 944 ):
943 945 return False
944 946 exceptions = (
945 947 encoding.environ.get(b'HGPLAINEXCEPT', b'').strip().split(b',')
946 948 )
947 949 # TODO: add support for HGPLAIN=+feature,-feature syntax
948 950 if b'+strictflags' not in encoding.environ.get(b'HGPLAIN', b'').split(
949 951 b','
950 952 ):
951 953 exceptions.append(b'strictflags')
952 954 if feature and exceptions:
953 955 return feature not in exceptions
954 956 return True
955 957
956 958 def username(self, acceptempty=False):
957 959 """Return default username to be used in commits.
958 960
959 961 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
960 962 and stop searching if one of these is set.
961 963 If not found and acceptempty is True, returns None.
962 964 If not found and ui.askusername is True, ask the user, else use
963 965 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
964 966 If no username could be found, raise an Abort error.
965 967 """
966 968 user = encoding.environ.get(b"HGUSER")
967 969 if user is None:
968 970 user = self.config(b"ui", b"username")
969 971 if user is not None:
970 972 user = os.path.expandvars(user)
971 973 if user is None:
972 974 user = encoding.environ.get(b"EMAIL")
973 975 if user is None and acceptempty:
974 976 return user
975 977 if user is None and self.configbool(b"ui", b"askusername"):
976 978 user = self.prompt(_(b"enter a commit username:"), default=None)
977 979 if user is None and not self.interactive():
978 980 try:
979 981 user = b'%s@%s' % (
980 982 procutil.getuser(),
981 983 encoding.strtolocal(socket.getfqdn()),
982 984 )
983 985 self.warn(_(b"no username found, using '%s' instead\n") % user)
984 986 except KeyError:
985 987 pass
986 988 if not user:
987 989 raise error.Abort(
988 990 _(b'no username supplied'),
989 991 hint=_(b"use 'hg config --edit' " b'to set your username'),
990 992 )
991 993 if b"\n" in user:
992 994 raise error.Abort(
993 995 _(b"username %r contains a newline\n") % pycompat.bytestr(user)
994 996 )
995 997 return user
996 998
997 999 def shortuser(self, user):
998 1000 """Return a short representation of a user name or email address."""
999 1001 if not self.verbose:
1000 1002 user = stringutil.shortuser(user)
1001 1003 return user
1002 1004
1003 1005 def expandpath(self, loc, default=None):
1004 1006 """Return repository location relative to cwd or from [paths]"""
1005 1007 try:
1006 1008 p = self.paths.getpath(loc)
1007 1009 if p:
1008 1010 return p.rawloc
1009 1011 except error.RepoError:
1010 1012 pass
1011 1013
1012 1014 if default:
1013 1015 try:
1014 1016 p = self.paths.getpath(default)
1015 1017 if p:
1016 1018 return p.rawloc
1017 1019 except error.RepoError:
1018 1020 pass
1019 1021
1020 1022 return loc
1021 1023
1022 1024 @util.propertycache
1023 1025 def paths(self):
1024 1026 return paths(self)
1025 1027
1026 1028 @property
1027 1029 def fout(self):
1028 1030 return self._fout
1029 1031
1030 1032 @fout.setter
1031 1033 def fout(self, f):
1032 1034 self._fout = f
1033 1035 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
1034 1036
1035 1037 @property
1036 1038 def ferr(self):
1037 1039 return self._ferr
1038 1040
1039 1041 @ferr.setter
1040 1042 def ferr(self, f):
1041 1043 self._ferr = f
1042 1044 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
1043 1045
1044 1046 @property
1045 1047 def fin(self):
1046 1048 return self._fin
1047 1049
1048 1050 @fin.setter
1049 1051 def fin(self, f):
1050 1052 self._fin = f
1051 1053
1052 1054 @property
1053 1055 def fmsg(self):
1054 1056 """Stream dedicated for status/error messages; may be None if
1055 1057 fout/ferr are used"""
1056 1058 return self._fmsg
1057 1059
1058 1060 @fmsg.setter
1059 1061 def fmsg(self, f):
1060 1062 self._fmsg = f
1061 1063 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
1062 1064
1063 1065 def pushbuffer(self, error=False, subproc=False, labeled=False):
1064 1066 """install a buffer to capture standard output of the ui object
1065 1067
1066 1068 If error is True, the error output will be captured too.
1067 1069
1068 1070 If subproc is True, output from subprocesses (typically hooks) will be
1069 1071 captured too.
1070 1072
1071 1073 If labeled is True, any labels associated with buffered
1072 1074 output will be handled. By default, this has no effect
1073 1075 on the output returned, but extensions and GUI tools may
1074 1076 handle this argument and returned styled output. If output
1075 1077 is being buffered so it can be captured and parsed or
1076 1078 processed, labeled should not be set to True.
1077 1079 """
1078 1080 self._buffers.append([])
1079 1081 self._bufferstates.append((error, subproc, labeled))
1080 1082 self._bufferapplylabels = labeled
1081 1083
1082 1084 def popbuffer(self):
1083 1085 '''pop the last buffer and return the buffered output'''
1084 1086 self._bufferstates.pop()
1085 1087 if self._bufferstates:
1086 1088 self._bufferapplylabels = self._bufferstates[-1][2]
1087 1089 else:
1088 1090 self._bufferapplylabels = None
1089 1091
1090 1092 return b"".join(self._buffers.pop())
1091 1093
1092 1094 def _isbuffered(self, dest):
1093 1095 if dest is self._fout:
1094 1096 return bool(self._buffers)
1095 1097 if dest is self._ferr:
1096 1098 return bool(self._bufferstates and self._bufferstates[-1][0])
1097 1099 return False
1098 1100
1099 1101 def canwritewithoutlabels(self):
1100 1102 '''check if write skips the label'''
1101 1103 if self._buffers and not self._bufferapplylabels:
1102 1104 return True
1103 1105 return self._colormode is None
1104 1106
1105 1107 def canbatchlabeledwrites(self):
1106 1108 '''check if write calls with labels are batchable'''
1107 1109 # Windows color printing is special, see ``write``.
1108 1110 return self._colormode != b'win32'
1109 1111
1110 1112 def write(self, *args, **opts):
1111 1113 '''write args to output
1112 1114
1113 1115 By default, this method simply writes to the buffer or stdout.
1114 1116 Color mode can be set on the UI class to have the output decorated
1115 1117 with color modifier before being written to stdout.
1116 1118
1117 1119 The color used is controlled by an optional keyword argument, "label".
1118 1120 This should be a string containing label names separated by space.
1119 1121 Label names take the form of "topic.type". For example, ui.debug()
1120 1122 issues a label of "ui.debug".
1121 1123
1122 1124 Progress reports via stderr are normally cleared before writing as
1123 1125 stdout and stderr go to the same terminal. This can be skipped with
1124 1126 the optional keyword argument "keepprogressbar". The progress bar
1125 1127 will continue to occupy a partial line on stderr in that case.
1126 1128 This functionality is intended when Mercurial acts as data source
1127 1129 in a pipe.
1128 1130
1129 1131 When labeling output for a specific command, a label of
1130 1132 "cmdname.type" is recommended. For example, status issues
1131 1133 a label of "status.modified" for modified files.
1132 1134 '''
1133 1135 dest = self._fout
1134 1136
1135 1137 # inlined _write() for speed
1136 1138 if self._buffers:
1137 1139 label = opts.get('label', b'')
1138 1140 if label and self._bufferapplylabels:
1139 1141 self._buffers[-1].extend(self.label(a, label) for a in args)
1140 1142 else:
1141 1143 self._buffers[-1].extend(args)
1142 1144 return
1143 1145
1144 1146 # inlined _writenobuf() for speed
1145 1147 if not opts.get('keepprogressbar', False):
1146 1148 self._progclear()
1147 1149 msg = b''.join(args)
1148 1150
1149 1151 # opencode timeblockedsection because this is a critical path
1150 1152 starttime = util.timer()
1151 1153 try:
1152 1154 if self._colormode == b'win32':
1153 1155 # windows color printing is its own can of crab, defer to
1154 1156 # the color module and that is it.
1155 1157 color.win32print(self, dest.write, msg, **opts)
1156 1158 else:
1157 1159 if self._colormode is not None:
1158 1160 label = opts.get('label', b'')
1159 1161 msg = self.label(msg, label)
1160 1162 dest.write(msg)
1161 1163 except IOError as err:
1162 1164 raise error.StdioError(err)
1163 1165 finally:
1164 1166 self._blockedtimes[b'stdio_blocked'] += (
1165 1167 util.timer() - starttime
1166 1168 ) * 1000
1167 1169
1168 1170 def write_err(self, *args, **opts):
1169 1171 self._write(self._ferr, *args, **opts)
1170 1172
1171 1173 def _write(self, dest, *args, **opts):
1172 1174 # update write() as well if you touch this code
1173 1175 if self._isbuffered(dest):
1174 1176 label = opts.get('label', b'')
1175 1177 if label and self._bufferapplylabels:
1176 1178 self._buffers[-1].extend(self.label(a, label) for a in args)
1177 1179 else:
1178 1180 self._buffers[-1].extend(args)
1179 1181 else:
1180 1182 self._writenobuf(dest, *args, **opts)
1181 1183
1182 1184 def _writenobuf(self, dest, *args, **opts):
1183 1185 # update write() as well if you touch this code
1184 1186 if not opts.get('keepprogressbar', False):
1185 1187 self._progclear()
1186 1188 msg = b''.join(args)
1187 1189
1188 1190 # opencode timeblockedsection because this is a critical path
1189 1191 starttime = util.timer()
1190 1192 try:
1191 1193 if dest is self._ferr and not getattr(self._fout, 'closed', False):
1192 1194 self._fout.flush()
1193 1195 if getattr(dest, 'structured', False):
1194 1196 # channel for machine-readable output with metadata, where
1195 1197 # no extra colorization is necessary.
1196 1198 dest.write(msg, **opts)
1197 1199 elif self._colormode == b'win32':
1198 1200 # windows color printing is its own can of crab, defer to
1199 1201 # the color module and that is it.
1200 1202 color.win32print(self, dest.write, msg, **opts)
1201 1203 else:
1202 1204 if self._colormode is not None:
1203 1205 label = opts.get('label', b'')
1204 1206 msg = self.label(msg, label)
1205 1207 dest.write(msg)
1206 1208 # stderr may be buffered under win32 when redirected to files,
1207 1209 # including stdout.
1208 1210 if dest is self._ferr and not getattr(dest, 'closed', False):
1209 1211 dest.flush()
1210 1212 except IOError as err:
1211 1213 if dest is self._ferr and err.errno in (
1212 1214 errno.EPIPE,
1213 1215 errno.EIO,
1214 1216 errno.EBADF,
1215 1217 ):
1216 1218 # no way to report the error, so ignore it
1217 1219 return
1218 1220 raise error.StdioError(err)
1219 1221 finally:
1220 1222 self._blockedtimes[b'stdio_blocked'] += (
1221 1223 util.timer() - starttime
1222 1224 ) * 1000
1223 1225
1224 1226 def _writemsg(self, dest, *args, **opts):
1225 1227 timestamp = self.showtimestamp and opts.get('type') in {
1226 1228 b'debug',
1227 1229 b'error',
1228 1230 b'note',
1229 1231 b'status',
1230 1232 b'warning',
1231 1233 }
1232 1234 if timestamp:
1233 1235 args = (
1234 1236 b'[%s] '
1235 1237 % pycompat.bytestr(datetime.datetime.now().isoformat()),
1236 1238 ) + args
1237 1239 _writemsgwith(self._write, dest, *args, **opts)
1238 1240 if timestamp:
1239 1241 dest.flush()
1240 1242
1241 1243 def _writemsgnobuf(self, dest, *args, **opts):
1242 1244 _writemsgwith(self._writenobuf, dest, *args, **opts)
1243 1245
1244 1246 def flush(self):
1245 1247 # opencode timeblockedsection because this is a critical path
1246 1248 starttime = util.timer()
1247 1249 try:
1248 1250 try:
1249 1251 self._fout.flush()
1250 1252 except IOError as err:
1251 1253 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
1252 1254 raise error.StdioError(err)
1253 1255 finally:
1254 1256 try:
1255 1257 self._ferr.flush()
1256 1258 except IOError as err:
1257 1259 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
1258 1260 raise error.StdioError(err)
1259 1261 finally:
1260 1262 self._blockedtimes[b'stdio_blocked'] += (
1261 1263 util.timer() - starttime
1262 1264 ) * 1000
1263 1265
1264 1266 def _isatty(self, fh):
1265 1267 if self.configbool(b'ui', b'nontty'):
1266 1268 return False
1267 1269 return procutil.isatty(fh)
1268 1270
1269 1271 def protectfinout(self):
1270 1272 """Duplicate ui streams and redirect original if they are stdio
1271 1273
1272 1274 Returns (fin, fout) which point to the original ui fds, but may be
1273 1275 copy of them. The returned streams can be considered "owned" in that
1274 1276 print(), exec(), etc. never reach to them.
1275 1277 """
1276 1278 if self._finoutredirected:
1277 1279 # if already redirected, protectstdio() would just create another
1278 1280 # nullfd pair, which is equivalent to returning self._fin/_fout.
1279 1281 return self._fin, self._fout
1280 1282 fin, fout = procutil.protectstdio(self._fin, self._fout)
1281 1283 self._finoutredirected = (fin, fout) != (self._fin, self._fout)
1282 1284 return fin, fout
1283 1285
1284 1286 def restorefinout(self, fin, fout):
1285 1287 """Restore ui streams from possibly duplicated (fin, fout)"""
1286 1288 if (fin, fout) == (self._fin, self._fout):
1287 1289 return
1288 1290 procutil.restorestdio(self._fin, self._fout, fin, fout)
1289 1291 # protectfinout() won't create more than one duplicated streams,
1290 1292 # so we can just turn the redirection flag off.
1291 1293 self._finoutredirected = False
1292 1294
1293 1295 @contextlib.contextmanager
1294 1296 def protectedfinout(self):
1295 1297 """Run code block with protected standard streams"""
1296 1298 fin, fout = self.protectfinout()
1297 1299 try:
1298 1300 yield fin, fout
1299 1301 finally:
1300 1302 self.restorefinout(fin, fout)
1301 1303
1302 1304 def disablepager(self):
1303 1305 self._disablepager = True
1304 1306
1305 1307 def pager(self, command):
1306 1308 """Start a pager for subsequent command output.
1307 1309
1308 1310 Commands which produce a long stream of output should call
1309 1311 this function to activate the user's preferred pagination
1310 1312 mechanism (which may be no pager). Calling this function
1311 1313 precludes any future use of interactive functionality, such as
1312 1314 prompting the user or activating curses.
1313 1315
1314 1316 Args:
1315 1317 command: The full, non-aliased name of the command. That is, "log"
1316 1318 not "history, "summary" not "summ", etc.
1317 1319 """
1318 1320 if self._disablepager or self.pageractive:
1319 1321 # how pager should do is already determined
1320 1322 return
1321 1323
1322 1324 if not command.startswith(b'internal-always-') and (
1323 1325 # explicit --pager=on (= 'internal-always-' prefix) should
1324 1326 # take precedence over disabling factors below
1325 1327 command in self.configlist(b'pager', b'ignore')
1326 1328 or not self.configbool(b'ui', b'paginate')
1327 1329 or not self.configbool(b'pager', b'attend-' + command, True)
1328 1330 or encoding.environ.get(b'TERM') == b'dumb'
1329 1331 # TODO: if we want to allow HGPLAINEXCEPT=pager,
1330 1332 # formatted() will need some adjustment.
1331 1333 or not self.formatted()
1332 1334 or self.plain()
1333 1335 or self._buffers
1334 1336 # TODO: expose debugger-enabled on the UI object
1335 1337 or b'--debugger' in pycompat.sysargv
1336 1338 ):
1337 1339 # We only want to paginate if the ui appears to be
1338 1340 # interactive, the user didn't say HGPLAIN or
1339 1341 # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
1340 1342 return
1341 1343
1342 1344 pagercmd = self.config(b'pager', b'pager', rcutil.fallbackpager)
1343 1345 if not pagercmd:
1344 1346 return
1345 1347
1346 1348 pagerenv = {}
1347 1349 for name, value in rcutil.defaultpagerenv().items():
1348 1350 if name not in encoding.environ:
1349 1351 pagerenv[name] = value
1350 1352
1351 1353 self.debug(
1352 1354 b'starting pager for command %s\n' % stringutil.pprint(command)
1353 1355 )
1354 1356 self.flush()
1355 1357
1356 1358 wasformatted = self.formatted()
1357 1359 if util.safehasattr(signal, b"SIGPIPE"):
1358 1360 signal.signal(signal.SIGPIPE, _catchterm)
1359 1361 if self._runpager(pagercmd, pagerenv):
1360 1362 self.pageractive = True
1361 1363 # Preserve the formatted-ness of the UI. This is important
1362 1364 # because we mess with stdout, which might confuse
1363 1365 # auto-detection of things being formatted.
1364 1366 self.setconfig(b'ui', b'formatted', wasformatted, b'pager')
1365 1367 self.setconfig(b'ui', b'interactive', False, b'pager')
1366 1368
1367 1369 # If pagermode differs from color.mode, reconfigure color now that
1368 1370 # pageractive is set.
1369 1371 cm = self._colormode
1370 1372 if cm != self.config(b'color', b'pagermode', cm):
1371 1373 color.setup(self)
1372 1374 else:
1373 1375 # If the pager can't be spawned in dispatch when --pager=on is
1374 1376 # given, don't try again when the command runs, to avoid a duplicate
1375 1377 # warning about a missing pager command.
1376 1378 self.disablepager()
1377 1379
1378 1380 def _runpager(self, command, env=None):
1379 1381 """Actually start the pager and set up file descriptors.
1380 1382
1381 1383 This is separate in part so that extensions (like chg) can
1382 1384 override how a pager is invoked.
1383 1385 """
1384 1386 if command == b'cat':
1385 1387 # Save ourselves some work.
1386 1388 return False
1387 1389 # If the command doesn't contain any of these characters, we
1388 1390 # assume it's a binary and exec it directly. This means for
1389 1391 # simple pager command configurations, we can degrade
1390 1392 # gracefully and tell the user about their broken pager.
1391 1393 shell = any(c in command for c in b"|&;<>()$`\\\"' \t\n*?[#~=%")
1392 1394
1393 1395 if pycompat.iswindows and not shell:
1394 1396 # Window's built-in `more` cannot be invoked with shell=False, but
1395 1397 # its `more.com` can. Hide this implementation detail from the
1396 1398 # user so we can also get sane bad PAGER behavior. MSYS has
1397 1399 # `more.exe`, so do a cmd.exe style resolution of the executable to
1398 1400 # determine which one to use.
1399 1401 fullcmd = procutil.findexe(command)
1400 1402 if not fullcmd:
1401 1403 self.warn(
1402 1404 _(b"missing pager command '%s', skipping pager\n") % command
1403 1405 )
1404 1406 return False
1405 1407
1406 1408 command = fullcmd
1407 1409
1408 1410 try:
1409 1411 pager = subprocess.Popen(
1410 1412 procutil.tonativestr(command),
1411 1413 shell=shell,
1412 1414 bufsize=-1,
1413 1415 close_fds=procutil.closefds,
1414 1416 stdin=subprocess.PIPE,
1415 1417 stdout=procutil.stdout,
1416 1418 stderr=procutil.stderr,
1417 1419 env=procutil.tonativeenv(procutil.shellenviron(env)),
1418 1420 )
1419 1421 except OSError as e:
1420 1422 if e.errno == errno.ENOENT and not shell:
1421 1423 self.warn(
1422 1424 _(b"missing pager command '%s', skipping pager\n") % command
1423 1425 )
1424 1426 return False
1425 1427 raise
1426 1428
1427 1429 # back up original file descriptors
1428 1430 stdoutfd = os.dup(procutil.stdout.fileno())
1429 1431 stderrfd = os.dup(procutil.stderr.fileno())
1430 1432
1431 1433 os.dup2(pager.stdin.fileno(), procutil.stdout.fileno())
1432 1434 if self._isatty(procutil.stderr):
1433 1435 os.dup2(pager.stdin.fileno(), procutil.stderr.fileno())
1434 1436
1435 1437 @self.atexit
1436 1438 def killpager():
1437 1439 if util.safehasattr(signal, b"SIGINT"):
1438 1440 signal.signal(signal.SIGINT, signal.SIG_IGN)
1439 1441 # restore original fds, closing pager.stdin copies in the process
1440 1442 os.dup2(stdoutfd, procutil.stdout.fileno())
1441 1443 os.dup2(stderrfd, procutil.stderr.fileno())
1442 1444 pager.stdin.close()
1443 1445 pager.wait()
1444 1446
1445 1447 return True
1446 1448
1447 1449 @property
1448 1450 def _exithandlers(self):
1449 1451 return _reqexithandlers
1450 1452
1451 1453 def atexit(self, func, *args, **kwargs):
1452 1454 '''register a function to run after dispatching a request
1453 1455
1454 1456 Handlers do not stay registered across request boundaries.'''
1455 1457 self._exithandlers.append((func, args, kwargs))
1456 1458 return func
1457 1459
1458 1460 def interface(self, feature):
1459 1461 """what interface to use for interactive console features?
1460 1462
1461 1463 The interface is controlled by the value of `ui.interface` but also by
1462 1464 the value of feature-specific configuration. For example:
1463 1465
1464 1466 ui.interface.histedit = text
1465 1467 ui.interface.chunkselector = curses
1466 1468
1467 1469 Here the features are "histedit" and "chunkselector".
1468 1470
1469 1471 The configuration above means that the default interfaces for commands
1470 1472 is curses, the interface for histedit is text and the interface for
1471 1473 selecting chunk is crecord (the best curses interface available).
1472 1474
1473 1475 Consider the following example:
1474 1476 ui.interface = curses
1475 1477 ui.interface.histedit = text
1476 1478
1477 1479 Then histedit will use the text interface and chunkselector will use
1478 1480 the default curses interface (crecord at the moment).
1479 1481 """
1480 1482 alldefaults = frozenset([b"text", b"curses"])
1481 1483
1482 1484 featureinterfaces = {
1483 1485 b"chunkselector": [b"text", b"curses",],
1484 1486 b"histedit": [b"text", b"curses",],
1485 1487 }
1486 1488
1487 1489 # Feature-specific interface
1488 1490 if feature not in featureinterfaces.keys():
1489 1491 # Programming error, not user error
1490 1492 raise ValueError(b"Unknown feature requested %s" % feature)
1491 1493
1492 1494 availableinterfaces = frozenset(featureinterfaces[feature])
1493 1495 if alldefaults > availableinterfaces:
1494 1496 # Programming error, not user error. We need a use case to
1495 1497 # define the right thing to do here.
1496 1498 raise ValueError(
1497 1499 b"Feature %s does not handle all default interfaces" % feature
1498 1500 )
1499 1501
1500 1502 if self.plain() or encoding.environ.get(b'TERM') == b'dumb':
1501 1503 return b"text"
1502 1504
1503 1505 # Default interface for all the features
1504 1506 defaultinterface = b"text"
1505 1507 i = self.config(b"ui", b"interface")
1506 1508 if i in alldefaults:
1507 1509 defaultinterface = i
1508 1510
1509 1511 choseninterface = defaultinterface
1510 1512 f = self.config(b"ui", b"interface.%s" % feature)
1511 1513 if f in availableinterfaces:
1512 1514 choseninterface = f
1513 1515
1514 1516 if i is not None and defaultinterface != i:
1515 1517 if f is not None:
1516 1518 self.warn(_(b"invalid value for ui.interface: %s\n") % (i,))
1517 1519 else:
1518 1520 self.warn(
1519 1521 _(b"invalid value for ui.interface: %s (using %s)\n")
1520 1522 % (i, choseninterface)
1521 1523 )
1522 1524 if f is not None and choseninterface != f:
1523 1525 self.warn(
1524 1526 _(b"invalid value for ui.interface.%s: %s (using %s)\n")
1525 1527 % (feature, f, choseninterface)
1526 1528 )
1527 1529
1528 1530 return choseninterface
1529 1531
1530 1532 def interactive(self):
1531 1533 '''is interactive input allowed?
1532 1534
1533 1535 An interactive session is a session where input can be reasonably read
1534 1536 from `sys.stdin'. If this function returns false, any attempt to read
1535 1537 from stdin should fail with an error, unless a sensible default has been
1536 1538 specified.
1537 1539
1538 1540 Interactiveness is triggered by the value of the `ui.interactive'
1539 1541 configuration variable or - if it is unset - when `sys.stdin' points
1540 1542 to a terminal device.
1541 1543
1542 1544 This function refers to input only; for output, see `ui.formatted()'.
1543 1545 '''
1544 1546 i = self.configbool(b"ui", b"interactive")
1545 1547 if i is None:
1546 1548 # some environments replace stdin without implementing isatty
1547 1549 # usually those are non-interactive
1548 1550 return self._isatty(self._fin)
1549 1551
1550 1552 return i
1551 1553
1552 1554 def termwidth(self):
1553 1555 '''how wide is the terminal in columns?
1554 1556 '''
1555 1557 if b'COLUMNS' in encoding.environ:
1556 1558 try:
1557 1559 return int(encoding.environ[b'COLUMNS'])
1558 1560 except ValueError:
1559 1561 pass
1560 1562 return scmutil.termsize(self)[0]
1561 1563
1562 1564 def formatted(self):
1563 1565 '''should formatted output be used?
1564 1566
1565 1567 It is often desirable to format the output to suite the output medium.
1566 1568 Examples of this are truncating long lines or colorizing messages.
1567 1569 However, this is not often not desirable when piping output into other
1568 1570 utilities, e.g. `grep'.
1569 1571
1570 1572 Formatted output is triggered by the value of the `ui.formatted'
1571 1573 configuration variable or - if it is unset - when `sys.stdout' points
1572 1574 to a terminal device. Please note that `ui.formatted' should be
1573 1575 considered an implementation detail; it is not intended for use outside
1574 1576 Mercurial or its extensions.
1575 1577
1576 1578 This function refers to output only; for input, see `ui.interactive()'.
1577 1579 This function always returns false when in plain mode, see `ui.plain()'.
1578 1580 '''
1579 1581 if self.plain():
1580 1582 return False
1581 1583
1582 1584 i = self.configbool(b"ui", b"formatted")
1583 1585 if i is None:
1584 1586 # some environments replace stdout without implementing isatty
1585 1587 # usually those are non-interactive
1586 1588 return self._isatty(self._fout)
1587 1589
1588 1590 return i
1589 1591
1590 1592 def _readline(self, prompt=b' ', promptopts=None):
1591 1593 # Replacing stdin/stdout temporarily is a hard problem on Python 3
1592 1594 # because they have to be text streams with *no buffering*. Instead,
1593 1595 # we use rawinput() only if call_readline() will be invoked by
1594 1596 # PyOS_Readline(), so no I/O will be made at Python layer.
1595 1597 usereadline = (
1596 1598 self._isatty(self._fin)
1597 1599 and self._isatty(self._fout)
1598 1600 and procutil.isstdin(self._fin)
1599 1601 and procutil.isstdout(self._fout)
1600 1602 )
1601 1603 if usereadline:
1602 1604 try:
1603 1605 # magically add command line editing support, where
1604 1606 # available
1605 1607 import readline
1606 1608
1607 1609 # force demandimport to really load the module
1608 1610 readline.read_history_file
1609 1611 # windows sometimes raises something other than ImportError
1610 1612 except Exception:
1611 1613 usereadline = False
1612 1614
1613 1615 if self._colormode == b'win32' or not usereadline:
1614 1616 if not promptopts:
1615 1617 promptopts = {}
1616 1618 self._writemsgnobuf(
1617 1619 self._fmsgout, prompt, type=b'prompt', **promptopts
1618 1620 )
1619 1621 self.flush()
1620 1622 prompt = b' '
1621 1623 else:
1622 1624 prompt = self.label(prompt, b'ui.prompt') + b' '
1623 1625
1624 1626 # prompt ' ' must exist; otherwise readline may delete entire line
1625 1627 # - http://bugs.python.org/issue12833
1626 1628 with self.timeblockedsection(b'stdio'):
1627 1629 if usereadline:
1628 1630 self.flush()
1629 1631 prompt = encoding.strfromlocal(prompt)
1630 1632 line = encoding.strtolocal(pycompat.rawinput(prompt))
1631 1633 # When stdin is in binary mode on Windows, it can cause
1632 1634 # raw_input() to emit an extra trailing carriage return
1633 1635 if pycompat.oslinesep == b'\r\n' and line.endswith(b'\r'):
1634 1636 line = line[:-1]
1635 1637 else:
1636 1638 self._fout.write(pycompat.bytestr(prompt))
1637 1639 self._fout.flush()
1638 1640 line = self._fin.readline()
1639 1641 if not line:
1640 1642 raise EOFError
1641 1643 line = line.rstrip(pycompat.oslinesep)
1642 1644
1643 1645 return line
1644 1646
1645 1647 def prompt(self, msg, default=b"y"):
1646 1648 """Prompt user with msg, read response.
1647 1649 If ui is not interactive, the default is returned.
1648 1650 """
1649 1651 return self._prompt(msg, default=default)
1650 1652
1651 1653 def _prompt(self, msg, **opts):
1652 1654 default = opts['default']
1653 1655 if not self.interactive():
1654 1656 self._writemsg(self._fmsgout, msg, b' ', type=b'prompt', **opts)
1655 1657 self._writemsg(
1656 1658 self._fmsgout, default or b'', b"\n", type=b'promptecho'
1657 1659 )
1658 1660 return default
1659 1661 try:
1660 1662 r = self._readline(prompt=msg, promptopts=opts)
1661 1663 if not r:
1662 1664 r = default
1663 1665 if self.configbool(b'ui', b'promptecho'):
1664 1666 self._writemsg(
1665 1667 self._fmsgout, r or b'', b"\n", type=b'promptecho'
1666 1668 )
1667 1669 return r
1668 1670 except EOFError:
1669 1671 raise error.ResponseExpected()
1670 1672
1671 1673 @staticmethod
1672 1674 def extractchoices(prompt):
1673 1675 """Extract prompt message and list of choices from specified prompt.
1674 1676
1675 1677 This returns tuple "(message, choices)", and "choices" is the
1676 1678 list of tuple "(response character, text without &)".
1677 1679
1678 1680 >>> ui.extractchoices(b"awake? $$ &Yes $$ &No")
1679 1681 ('awake? ', [('y', 'Yes'), ('n', 'No')])
1680 1682 >>> ui.extractchoices(b"line\\nbreak? $$ &Yes $$ &No")
1681 1683 ('line\\nbreak? ', [('y', 'Yes'), ('n', 'No')])
1682 1684 >>> ui.extractchoices(b"want lots of $$money$$?$$Ye&s$$N&o")
1683 1685 ('want lots of $$money$$?', [('s', 'Yes'), ('o', 'No')])
1684 1686 """
1685 1687
1686 1688 # Sadly, the prompt string may have been built with a filename
1687 1689 # containing "$$" so let's try to find the first valid-looking
1688 1690 # prompt to start parsing. Sadly, we also can't rely on
1689 1691 # choices containing spaces, ASCII, or basically anything
1690 1692 # except an ampersand followed by a character.
1691 1693 m = re.match(br'(?s)(.+?)\$\$([^$]*&[^ $].*)', prompt)
1692 1694 msg = m.group(1)
1693 1695 choices = [p.strip(b' ') for p in m.group(2).split(b'$$')]
1694 1696
1695 1697 def choicetuple(s):
1696 1698 ampidx = s.index(b'&')
1697 1699 return s[ampidx + 1 : ampidx + 2].lower(), s.replace(b'&', b'', 1)
1698 1700
1699 1701 return (msg, [choicetuple(s) for s in choices])
1700 1702
1701 1703 def promptchoice(self, prompt, default=0):
1702 1704 """Prompt user with a message, read response, and ensure it matches
1703 1705 one of the provided choices. The prompt is formatted as follows:
1704 1706
1705 1707 "would you like fries with that (Yn)? $$ &Yes $$ &No"
1706 1708
1707 1709 The index of the choice is returned. Responses are case
1708 1710 insensitive. If ui is not interactive, the default is
1709 1711 returned.
1710 1712 """
1711 1713
1712 1714 msg, choices = self.extractchoices(prompt)
1713 1715 resps = [r for r, t in choices]
1714 1716 while True:
1715 1717 r = self._prompt(msg, default=resps[default], choices=choices)
1716 1718 if r.lower() in resps:
1717 1719 return resps.index(r.lower())
1718 1720 # TODO: shouldn't it be a warning?
1719 1721 self._writemsg(self._fmsgout, _(b"unrecognized response\n"))
1720 1722
1721 1723 def getpass(self, prompt=None, default=None):
1722 1724 if not self.interactive():
1723 1725 return default
1724 1726 try:
1725 1727 self._writemsg(
1726 1728 self._fmsgerr,
1727 1729 prompt or _(b'password: '),
1728 1730 type=b'prompt',
1729 1731 password=True,
1730 1732 )
1731 1733 # disable getpass() only if explicitly specified. it's still valid
1732 1734 # to interact with tty even if fin is not a tty.
1733 1735 with self.timeblockedsection(b'stdio'):
1734 1736 if self.configbool(b'ui', b'nontty'):
1735 1737 l = self._fin.readline()
1736 1738 if not l:
1737 1739 raise EOFError
1738 1740 return l.rstrip(b'\n')
1739 1741 else:
1740 1742 return getpass.getpass('')
1741 1743 except EOFError:
1742 1744 raise error.ResponseExpected()
1743 1745
1744 1746 def status(self, *msg, **opts):
1745 1747 '''write status message to output (if ui.quiet is False)
1746 1748
1747 1749 This adds an output label of "ui.status".
1748 1750 '''
1749 1751 if not self.quiet:
1750 1752 self._writemsg(self._fmsgout, type=b'status', *msg, **opts)
1751 1753
1752 1754 def warn(self, *msg, **opts):
1753 1755 '''write warning message to output (stderr)
1754 1756
1755 1757 This adds an output label of "ui.warning".
1756 1758 '''
1757 1759 self._writemsg(self._fmsgerr, type=b'warning', *msg, **opts)
1758 1760
1759 1761 def error(self, *msg, **opts):
1760 1762 '''write error message to output (stderr)
1761 1763
1762 1764 This adds an output label of "ui.error".
1763 1765 '''
1764 1766 self._writemsg(self._fmsgerr, type=b'error', *msg, **opts)
1765 1767
1766 1768 def note(self, *msg, **opts):
1767 1769 '''write note to output (if ui.verbose is True)
1768 1770
1769 1771 This adds an output label of "ui.note".
1770 1772 '''
1771 1773 if self.verbose:
1772 1774 self._writemsg(self._fmsgout, type=b'note', *msg, **opts)
1773 1775
1774 1776 def debug(self, *msg, **opts):
1775 1777 '''write debug message to output (if ui.debugflag is True)
1776 1778
1777 1779 This adds an output label of "ui.debug".
1778 1780 '''
1779 1781 if self.debugflag:
1780 1782 self._writemsg(self._fmsgout, type=b'debug', *msg, **opts)
1781 1783 self.log(b'debug', b'%s', b''.join(msg))
1782 1784
1783 1785 # Aliases to defeat check-code.
1784 1786 statusnoi18n = status
1785 1787 notenoi18n = note
1786 1788 warnnoi18n = warn
1787 1789 writenoi18n = write
1788 1790
1789 1791 def edit(
1790 1792 self,
1791 1793 text,
1792 1794 user,
1793 1795 extra=None,
1794 1796 editform=None,
1795 1797 pending=None,
1796 1798 repopath=None,
1797 1799 action=None,
1798 1800 ):
1799 1801 if action is None:
1800 1802 self.develwarn(
1801 1803 b'action is None but will soon be a required '
1802 1804 b'parameter to ui.edit()'
1803 1805 )
1804 1806 extra_defaults = {
1805 1807 b'prefix': b'editor',
1806 1808 b'suffix': b'.txt',
1807 1809 }
1808 1810 if extra is not None:
1809 1811 if extra.get(b'suffix') is not None:
1810 1812 self.develwarn(
1811 1813 b'extra.suffix is not None but will soon be '
1812 1814 b'ignored by ui.edit()'
1813 1815 )
1814 1816 extra_defaults.update(extra)
1815 1817 extra = extra_defaults
1816 1818
1817 1819 if action == b'diff':
1818 1820 suffix = b'.diff'
1819 1821 elif action:
1820 1822 suffix = b'.%s.hg.txt' % action
1821 1823 else:
1822 1824 suffix = extra[b'suffix']
1823 1825
1824 1826 rdir = None
1825 1827 if self.configbool(b'experimental', b'editortmpinhg'):
1826 1828 rdir = repopath
1827 1829 (fd, name) = pycompat.mkstemp(
1828 1830 prefix=b'hg-' + extra[b'prefix'] + b'-', suffix=suffix, dir=rdir
1829 1831 )
1830 1832 try:
1831 1833 with os.fdopen(fd, 'wb') as f:
1832 1834 f.write(util.tonativeeol(text))
1833 1835
1834 1836 environ = {b'HGUSER': user}
1835 1837 if b'transplant_source' in extra:
1836 1838 environ.update(
1837 1839 {b'HGREVISION': hex(extra[b'transplant_source'])}
1838 1840 )
1839 1841 for label in (b'intermediate-source', b'source', b'rebase_source'):
1840 1842 if label in extra:
1841 1843 environ.update({b'HGREVISION': extra[label]})
1842 1844 break
1843 1845 if editform:
1844 1846 environ.update({b'HGEDITFORM': editform})
1845 1847 if pending:
1846 1848 environ.update({b'HG_PENDING': pending})
1847 1849
1848 1850 editor = self.geteditor()
1849 1851
1850 1852 self.system(
1851 1853 b"%s \"%s\"" % (editor, name),
1852 1854 environ=environ,
1853 1855 onerr=error.Abort,
1854 1856 errprefix=_(b"edit failed"),
1855 1857 blockedtag=b'editor',
1856 1858 )
1857 1859
1858 1860 with open(name, 'rb') as f:
1859 1861 t = util.fromnativeeol(f.read())
1860 1862 finally:
1861 1863 os.unlink(name)
1862 1864
1863 1865 return t
1864 1866
1865 1867 def system(
1866 1868 self,
1867 1869 cmd,
1868 1870 environ=None,
1869 1871 cwd=None,
1870 1872 onerr=None,
1871 1873 errprefix=None,
1872 1874 blockedtag=None,
1873 1875 ):
1874 1876 '''execute shell command with appropriate output stream. command
1875 1877 output will be redirected if fout is not stdout.
1876 1878
1877 1879 if command fails and onerr is None, return status, else raise onerr
1878 1880 object as exception.
1879 1881 '''
1880 1882 if blockedtag is None:
1881 1883 # Long cmds tend to be because of an absolute path on cmd. Keep
1882 1884 # the tail end instead
1883 1885 cmdsuffix = cmd.translate(None, _keepalnum)[-85:]
1884 1886 blockedtag = b'unknown_system_' + cmdsuffix
1885 1887 out = self._fout
1886 1888 if any(s[1] for s in self._bufferstates):
1887 1889 out = self
1888 1890 with self.timeblockedsection(blockedtag):
1889 1891 rc = self._runsystem(cmd, environ=environ, cwd=cwd, out=out)
1890 1892 if rc and onerr:
1891 1893 errmsg = b'%s %s' % (
1892 1894 procutil.shellsplit(cmd)[0],
1893 1895 procutil.explainexit(rc),
1894 1896 )
1895 1897 if errprefix:
1896 1898 errmsg = b'%s: %s' % (errprefix, errmsg)
1897 1899 raise onerr(errmsg)
1898 1900 return rc
1899 1901
1900 1902 def _runsystem(self, cmd, environ, cwd, out):
1901 1903 """actually execute the given shell command (can be overridden by
1902 1904 extensions like chg)"""
1903 1905 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
1904 1906
1905 1907 def traceback(self, exc=None, force=False):
1906 1908 '''print exception traceback if traceback printing enabled or forced.
1907 1909 only to call in exception handler. returns true if traceback
1908 1910 printed.'''
1909 1911 if self.tracebackflag or force:
1910 1912 if exc is None:
1911 1913 exc = sys.exc_info()
1912 1914 cause = getattr(exc[1], 'cause', None)
1913 1915
1914 1916 if cause is not None:
1915 1917 causetb = traceback.format_tb(cause[2])
1916 1918 exctb = traceback.format_tb(exc[2])
1917 1919 exconly = traceback.format_exception_only(cause[0], cause[1])
1918 1920
1919 1921 # exclude frame where 'exc' was chained and rethrown from exctb
1920 1922 self.write_err(
1921 1923 b'Traceback (most recent call last):\n',
1922 1924 encoding.strtolocal(''.join(exctb[:-1])),
1923 1925 encoding.strtolocal(''.join(causetb)),
1924 1926 encoding.strtolocal(''.join(exconly)),
1925 1927 )
1926 1928 else:
1927 1929 output = traceback.format_exception(exc[0], exc[1], exc[2])
1928 1930 self.write_err(encoding.strtolocal(''.join(output)))
1929 1931 return self.tracebackflag or force
1930 1932
1931 1933 def geteditor(self):
1932 1934 '''return editor to use'''
1933 1935 if pycompat.sysplatform == b'plan9':
1934 1936 # vi is the MIPS instruction simulator on Plan 9. We
1935 1937 # instead default to E to plumb commit messages to
1936 1938 # avoid confusion.
1937 1939 editor = b'E'
1938 1940 elif pycompat.isdarwin:
1939 1941 # vi on darwin is POSIX compatible to a fault, and that includes
1940 1942 # exiting non-zero if you make any mistake when running an ex
1941 1943 # command. Proof: `vi -c ':unknown' -c ':qa'; echo $?` produces 1,
1942 1944 # while s/vi/vim/ doesn't.
1943 1945 editor = b'vim'
1944 1946 else:
1945 1947 editor = b'vi'
1946 1948 return encoding.environ.get(b"HGEDITOR") or self.config(
1947 1949 b"ui", b"editor", editor
1948 1950 )
1949 1951
1950 1952 @util.propertycache
1951 1953 def _progbar(self):
1952 1954 """setup the progbar singleton to the ui object"""
1953 1955 if (
1954 1956 self.quiet
1955 1957 or self.debugflag
1956 1958 or self.configbool(b'progress', b'disable')
1957 1959 or not progress.shouldprint(self)
1958 1960 ):
1959 1961 return None
1960 1962 return getprogbar(self)
1961 1963
1962 1964 def _progclear(self):
1963 1965 """clear progress bar output if any. use it before any output"""
1964 1966 if not haveprogbar(): # nothing loaded yet
1965 1967 return
1966 1968 if self._progbar is not None and self._progbar.printed:
1967 1969 self._progbar.clear()
1968 1970
1969 1971 def makeprogress(self, topic, unit=b"", total=None):
1970 1972 """Create a progress helper for the specified topic"""
1971 1973 if getattr(self._fmsgerr, 'structured', False):
1972 1974 # channel for machine-readable output with metadata, just send
1973 1975 # raw information
1974 1976 # TODO: consider porting some useful information (e.g. estimated
1975 1977 # time) from progbar. we might want to support update delay to
1976 1978 # reduce the cost of transferring progress messages.
1977 1979 def updatebar(topic, pos, item, unit, total):
1978 1980 self._fmsgerr.write(
1979 1981 None,
1980 1982 type=b'progress',
1981 1983 topic=topic,
1982 1984 pos=pos,
1983 1985 item=item,
1984 1986 unit=unit,
1985 1987 total=total,
1986 1988 )
1987 1989
1988 1990 elif self._progbar is not None:
1989 1991 updatebar = self._progbar.progress
1990 1992 else:
1991 1993
1992 1994 def updatebar(topic, pos, item, unit, total):
1993 1995 pass
1994 1996
1995 1997 return scmutil.progress(self, updatebar, topic, unit, total)
1996 1998
1997 1999 def getlogger(self, name):
1998 2000 """Returns a logger of the given name; or None if not registered"""
1999 2001 return self._loggers.get(name)
2000 2002
2001 2003 def setlogger(self, name, logger):
2002 2004 """Install logger which can be identified later by the given name
2003 2005
2004 2006 More than one loggers can be registered. Use extension or module
2005 2007 name to uniquely identify the logger instance.
2006 2008 """
2007 2009 self._loggers[name] = logger
2008 2010
2009 2011 def log(self, event, msgfmt, *msgargs, **opts):
2010 2012 '''hook for logging facility extensions
2011 2013
2012 2014 event should be a readily-identifiable subsystem, which will
2013 2015 allow filtering.
2014 2016
2015 2017 msgfmt should be a newline-terminated format string to log, and
2016 2018 *msgargs are %-formatted into it.
2017 2019
2018 2020 **opts currently has no defined meanings.
2019 2021 '''
2020 2022 if not self._loggers:
2021 2023 return
2022 2024 activeloggers = [
2023 2025 l for l in pycompat.itervalues(self._loggers) if l.tracked(event)
2024 2026 ]
2025 2027 if not activeloggers:
2026 2028 return
2027 2029 msg = msgfmt % msgargs
2028 2030 opts = pycompat.byteskwargs(opts)
2029 2031 # guard against recursion from e.g. ui.debug()
2030 2032 registeredloggers = self._loggers
2031 2033 self._loggers = {}
2032 2034 try:
2033 2035 for logger in activeloggers:
2034 2036 logger.log(self, event, msg, opts)
2035 2037 finally:
2036 2038 self._loggers = registeredloggers
2037 2039
2038 2040 def label(self, msg, label):
2039 2041 '''style msg based on supplied label
2040 2042
2041 2043 If some color mode is enabled, this will add the necessary control
2042 2044 characters to apply such color. In addition, 'debug' color mode adds
2043 2045 markup showing which label affects a piece of text.
2044 2046
2045 2047 ui.write(s, 'label') is equivalent to
2046 2048 ui.write(ui.label(s, 'label')).
2047 2049 '''
2048 2050 if self._colormode is not None:
2049 2051 return color.colorlabel(self, msg, label)
2050 2052 return msg
2051 2053
2052 2054 def develwarn(self, msg, stacklevel=1, config=None):
2053 2055 """issue a developer warning message
2054 2056
2055 2057 Use 'stacklevel' to report the offender some layers further up in the
2056 2058 stack.
2057 2059 """
2058 2060 if not self.configbool(b'devel', b'all-warnings'):
2059 2061 if config is None or not self.configbool(b'devel', config):
2060 2062 return
2061 2063 msg = b'devel-warn: ' + msg
2062 2064 stacklevel += 1 # get in develwarn
2063 2065 if self.tracebackflag:
2064 2066 util.debugstacktrace(msg, stacklevel, self._ferr, self._fout)
2065 2067 self.log(
2066 2068 b'develwarn',
2067 2069 b'%s at:\n%s'
2068 2070 % (msg, b''.join(util.getstackframes(stacklevel))),
2069 2071 )
2070 2072 else:
2071 2073 curframe = inspect.currentframe()
2072 2074 calframe = inspect.getouterframes(curframe, 2)
2073 2075 fname, lineno, fmsg = calframe[stacklevel][1:4]
2074 2076 fname, fmsg = pycompat.sysbytes(fname), pycompat.sysbytes(fmsg)
2075 2077 self.write_err(b'%s at: %s:%d (%s)\n' % (msg, fname, lineno, fmsg))
2076 2078 self.log(
2077 2079 b'develwarn', b'%s at: %s:%d (%s)\n', msg, fname, lineno, fmsg
2078 2080 )
2079 2081
2080 2082 # avoid cycles
2081 2083 del curframe
2082 2084 del calframe
2083 2085
2084 2086 def deprecwarn(self, msg, version, stacklevel=2):
2085 2087 """issue a deprecation warning
2086 2088
2087 2089 - msg: message explaining what is deprecated and how to upgrade,
2088 2090 - version: last version where the API will be supported,
2089 2091 """
2090 2092 if not (
2091 2093 self.configbool(b'devel', b'all-warnings')
2092 2094 or self.configbool(b'devel', b'deprec-warn')
2093 2095 ):
2094 2096 return
2095 2097 msg += (
2096 2098 b"\n(compatibility will be dropped after Mercurial-%s,"
2097 2099 b" update your code.)"
2098 2100 ) % version
2099 2101 self.develwarn(msg, stacklevel=stacklevel, config=b'deprec-warn')
2100 2102
2101 2103 def exportableenviron(self):
2102 2104 """The environment variables that are safe to export, e.g. through
2103 2105 hgweb.
2104 2106 """
2105 2107 return self._exportableenviron
2106 2108
2107 2109 @contextlib.contextmanager
2108 2110 def configoverride(self, overrides, source=b""):
2109 2111 """Context manager for temporary config overrides
2110 2112 `overrides` must be a dict of the following structure:
2111 2113 {(section, name) : value}"""
2112 2114 backups = {}
2113 2115 try:
2114 2116 for (section, name), value in overrides.items():
2115 2117 backups[(section, name)] = self.backupconfig(section, name)
2116 2118 self.setconfig(section, name, value, source)
2117 2119 yield
2118 2120 finally:
2119 2121 for __, backup in backups.items():
2120 2122 self.restoreconfig(backup)
2121 2123 # just restoring ui.quiet config to the previous value is not enough
2122 2124 # as it does not update ui.quiet class member
2123 2125 if (b'ui', b'quiet') in overrides:
2124 2126 self.fixconfig(section=b'ui')
2125 2127
2126 2128 def estimatememory(self):
2127 2129 """Provide an estimate for the available system memory in Bytes.
2128 2130
2129 2131 This can be overriden via ui.available-memory. It returns None, if
2130 2132 no estimate can be computed.
2131 2133 """
2132 2134 value = self.config(b'ui', b'available-memory')
2133 2135 if value is not None:
2134 2136 try:
2135 2137 return util.sizetoint(value)
2136 2138 except error.ParseError:
2137 2139 raise error.ConfigError(
2138 2140 _(b"ui.available-memory value is invalid ('%s')") % value
2139 2141 )
2140 2142 return util._estimatememory()
2141 2143
2142 2144
2143 2145 class paths(dict):
2144 2146 """Represents a collection of paths and their configs.
2145 2147
2146 2148 Data is initially derived from ui instances and the config files they have
2147 2149 loaded.
2148 2150 """
2149 2151
2150 2152 def __init__(self, ui):
2151 2153 dict.__init__(self)
2152 2154
2153 2155 for name, loc in ui.configitems(b'paths', ignoresub=True):
2154 2156 # No location is the same as not existing.
2155 2157 if not loc:
2156 2158 continue
2157 2159 loc, sub = ui.configsuboptions(b'paths', name)
2158 2160 self[name] = path(ui, name, rawloc=loc, suboptions=sub)
2159 2161
2160 2162 def getpath(self, name, default=None):
2161 2163 """Return a ``path`` from a string, falling back to default.
2162 2164
2163 2165 ``name`` can be a named path or locations. Locations are filesystem
2164 2166 paths or URIs.
2165 2167
2166 2168 Returns None if ``name`` is not a registered path, a URI, or a local
2167 2169 path to a repo.
2168 2170 """
2169 2171 # Only fall back to default if no path was requested.
2170 2172 if name is None:
2171 2173 if not default:
2172 2174 default = ()
2173 2175 elif not isinstance(default, (tuple, list)):
2174 2176 default = (default,)
2175 2177 for k in default:
2176 2178 try:
2177 2179 return self[k]
2178 2180 except KeyError:
2179 2181 continue
2180 2182 return None
2181 2183
2182 2184 # Most likely empty string.
2183 2185 # This may need to raise in the future.
2184 2186 if not name:
2185 2187 return None
2186 2188
2187 2189 try:
2188 2190 return self[name]
2189 2191 except KeyError:
2190 2192 # Try to resolve as a local path or URI.
2191 2193 try:
2192 2194 # We don't pass sub-options in, so no need to pass ui instance.
2193 2195 return path(None, None, rawloc=name)
2194 2196 except ValueError:
2195 2197 raise error.RepoError(_(b'repository %s does not exist') % name)
2196 2198
2197 2199
2198 2200 _pathsuboptions = {}
2199 2201
2200 2202
2201 2203 def pathsuboption(option, attr):
2202 2204 """Decorator used to declare a path sub-option.
2203 2205
2204 2206 Arguments are the sub-option name and the attribute it should set on
2205 2207 ``path`` instances.
2206 2208
2207 2209 The decorated function will receive as arguments a ``ui`` instance,
2208 2210 ``path`` instance, and the string value of this option from the config.
2209 2211 The function should return the value that will be set on the ``path``
2210 2212 instance.
2211 2213
2212 2214 This decorator can be used to perform additional verification of
2213 2215 sub-options and to change the type of sub-options.
2214 2216 """
2215 2217
2216 2218 def register(func):
2217 2219 _pathsuboptions[option] = (attr, func)
2218 2220 return func
2219 2221
2220 2222 return register
2221 2223
2222 2224
2223 2225 @pathsuboption(b'pushurl', b'pushloc')
2224 2226 def pushurlpathoption(ui, path, value):
2225 2227 u = util.url(value)
2226 2228 # Actually require a URL.
2227 2229 if not u.scheme:
2228 2230 ui.warn(_(b'(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
2229 2231 return None
2230 2232
2231 2233 # Don't support the #foo syntax in the push URL to declare branch to
2232 2234 # push.
2233 2235 if u.fragment:
2234 2236 ui.warn(
2235 2237 _(
2236 2238 b'("#fragment" in paths.%s:pushurl not supported; '
2237 2239 b'ignoring)\n'
2238 2240 )
2239 2241 % path.name
2240 2242 )
2241 2243 u.fragment = None
2242 2244
2243 2245 return bytes(u)
2244 2246
2245 2247
2246 2248 @pathsuboption(b'pushrev', b'pushrev')
2247 2249 def pushrevpathoption(ui, path, value):
2248 2250 return value
2249 2251
2250 2252
2251 2253 class path(object):
2252 2254 """Represents an individual path and its configuration."""
2253 2255
2254 2256 def __init__(self, ui, name, rawloc=None, suboptions=None):
2255 2257 """Construct a path from its config options.
2256 2258
2257 2259 ``ui`` is the ``ui`` instance the path is coming from.
2258 2260 ``name`` is the symbolic name of the path.
2259 2261 ``rawloc`` is the raw location, as defined in the config.
2260 2262 ``pushloc`` is the raw locations pushes should be made to.
2261 2263
2262 2264 If ``name`` is not defined, we require that the location be a) a local
2263 2265 filesystem path with a .hg directory or b) a URL. If not,
2264 2266 ``ValueError`` is raised.
2265 2267 """
2266 2268 if not rawloc:
2267 2269 raise ValueError(b'rawloc must be defined')
2268 2270
2269 2271 # Locations may define branches via syntax <base>#<branch>.
2270 2272 u = util.url(rawloc)
2271 2273 branch = None
2272 2274 if u.fragment:
2273 2275 branch = u.fragment
2274 2276 u.fragment = None
2275 2277
2276 2278 self.url = u
2277 2279 self.branch = branch
2278 2280
2279 2281 self.name = name
2280 2282 self.rawloc = rawloc
2281 2283 self.loc = b'%s' % u
2282 2284
2283 2285 # When given a raw location but not a symbolic name, validate the
2284 2286 # location is valid.
2285 2287 if not name and not u.scheme and not self._isvalidlocalpath(self.loc):
2286 2288 raise ValueError(
2287 2289 b'location is not a URL or path to a local '
2288 2290 b'repo: %s' % rawloc
2289 2291 )
2290 2292
2291 2293 suboptions = suboptions or {}
2292 2294
2293 2295 # Now process the sub-options. If a sub-option is registered, its
2294 2296 # attribute will always be present. The value will be None if there
2295 2297 # was no valid sub-option.
2296 2298 for suboption, (attr, func) in pycompat.iteritems(_pathsuboptions):
2297 2299 if suboption not in suboptions:
2298 2300 setattr(self, attr, None)
2299 2301 continue
2300 2302
2301 2303 value = func(ui, self, suboptions[suboption])
2302 2304 setattr(self, attr, value)
2303 2305
2304 2306 def _isvalidlocalpath(self, path):
2305 2307 """Returns True if the given path is a potentially valid repository.
2306 2308 This is its own function so that extensions can change the definition of
2307 2309 'valid' in this case (like when pulling from a git repo into a hg
2308 2310 one)."""
2309 2311 try:
2310 2312 return os.path.isdir(os.path.join(path, b'.hg'))
2311 2313 # Python 2 may return TypeError. Python 3, ValueError.
2312 2314 except (TypeError, ValueError):
2313 2315 return False
2314 2316
2315 2317 @property
2316 2318 def suboptions(self):
2317 2319 """Return sub-options and their values for this path.
2318 2320
2319 2321 This is intended to be used for presentation purposes.
2320 2322 """
2321 2323 d = {}
2322 2324 for subopt, (attr, _func) in pycompat.iteritems(_pathsuboptions):
2323 2325 value = getattr(self, attr)
2324 2326 if value is not None:
2325 2327 d[subopt] = value
2326 2328 return d
2327 2329
2328 2330
2329 2331 # we instantiate one globally shared progress bar to avoid
2330 2332 # competing progress bars when multiple UI objects get created
2331 2333 _progresssingleton = None
2332 2334
2333 2335
2334 2336 def getprogbar(ui):
2335 2337 global _progresssingleton
2336 2338 if _progresssingleton is None:
2337 2339 # passing 'ui' object to the singleton is fishy,
2338 2340 # this is how the extension used to work but feel free to rework it.
2339 2341 _progresssingleton = progress.progbar(ui)
2340 2342 return _progresssingleton
2341 2343
2342 2344
2343 2345 def haveprogbar():
2344 2346 return _progresssingleton is not None
2345 2347
2346 2348
2347 2349 def _selectmsgdests(ui):
2348 2350 name = ui.config(b'ui', b'message-output')
2349 2351 if name == b'channel':
2350 2352 if ui.fmsg:
2351 2353 return ui.fmsg, ui.fmsg
2352 2354 else:
2353 2355 # fall back to ferr if channel isn't ready so that status/error
2354 2356 # messages can be printed
2355 2357 return ui.ferr, ui.ferr
2356 2358 if name == b'stdio':
2357 2359 return ui.fout, ui.ferr
2358 2360 if name == b'stderr':
2359 2361 return ui.ferr, ui.ferr
2360 2362 raise error.Abort(b'invalid ui.message-output destination: %s' % name)
2361 2363
2362 2364
2363 2365 def _writemsgwith(write, dest, *args, **opts):
2364 2366 """Write ui message with the given ui._write*() function
2365 2367
2366 2368 The specified message type is translated to 'ui.<type>' label if the dest
2367 2369 isn't a structured channel, so that the message will be colorized.
2368 2370 """
2369 2371 # TODO: maybe change 'type' to a mandatory option
2370 2372 if 'type' in opts and not getattr(dest, 'structured', False):
2371 2373 opts['label'] = opts.get('label', b'') + b' ui.%s' % opts.pop('type')
2372 2374 write(dest, *args, **opts)
@@ -1,3780 +1,3781 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # run-tests.py - Run a set of tests on Mercurial
4 4 #
5 5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 # Modifying this script is tricky because it has many modes:
11 11 # - serial (default) vs parallel (-jN, N > 1)
12 12 # - no coverage (default) vs coverage (-c, -C, -s)
13 13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 14 # - tests are a mix of shell scripts and Python scripts
15 15 #
16 16 # If you change this script, it is recommended that you ensure you
17 17 # haven't broken it by running it in various modes with a representative
18 18 # sample of test scripts. For example:
19 19 #
20 20 # 1) serial, no coverage, temp install:
21 21 # ./run-tests.py test-s*
22 22 # 2) serial, no coverage, local hg:
23 23 # ./run-tests.py --local test-s*
24 24 # 3) serial, coverage, temp install:
25 25 # ./run-tests.py -c test-s*
26 26 # 4) serial, coverage, local hg:
27 27 # ./run-tests.py -c --local test-s* # unsupported
28 28 # 5) parallel, no coverage, temp install:
29 29 # ./run-tests.py -j2 test-s*
30 30 # 6) parallel, no coverage, local hg:
31 31 # ./run-tests.py -j2 --local test-s*
32 32 # 7) parallel, coverage, temp install:
33 33 # ./run-tests.py -j2 -c test-s* # currently broken
34 34 # 8) parallel, coverage, local install:
35 35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 36 # 9) parallel, custom tmp dir:
37 37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 38 # 10) parallel, pure, tests that call run-tests:
39 39 # ./run-tests.py --pure `grep -l run-tests.py *.t`
40 40 #
41 41 # (You could use any subset of the tests: test-s* happens to match
42 42 # enough that it's worth doing parallel runs, few enough that it
43 43 # completes fairly quickly, includes both shell and Python scripts, and
44 44 # includes some scripts that run daemon processes.)
45 45
46 46 from __future__ import absolute_import, print_function
47 47
48 48 import argparse
49 49 import collections
50 50 import difflib
51 51 import distutils.version as version
52 52 import errno
53 53 import json
54 54 import multiprocessing
55 55 import os
56 56 import platform
57 57 import random
58 58 import re
59 59 import shutil
60 60 import signal
61 61 import socket
62 62 import subprocess
63 63 import sys
64 64 import sysconfig
65 65 import tempfile
66 66 import threading
67 67 import time
68 68 import unittest
69 69 import uuid
70 70 import xml.dom.minidom as minidom
71 71
72 72 try:
73 73 import Queue as queue
74 74 except ImportError:
75 75 import queue
76 76
77 77 try:
78 78 import shlex
79 79
80 80 shellquote = shlex.quote
81 81 except (ImportError, AttributeError):
82 82 import pipes
83 83
84 84 shellquote = pipes.quote
85 85
86 86 processlock = threading.Lock()
87 87
88 88 pygmentspresent = False
89 89 # ANSI color is unsupported prior to Windows 10
90 90 if os.name != 'nt':
91 91 try: # is pygments installed
92 92 import pygments
93 93 import pygments.lexers as lexers
94 94 import pygments.lexer as lexer
95 95 import pygments.formatters as formatters
96 96 import pygments.token as token
97 97 import pygments.style as style
98 98
99 99 pygmentspresent = True
100 100 difflexer = lexers.DiffLexer()
101 101 terminal256formatter = formatters.Terminal256Formatter()
102 102 except ImportError:
103 103 pass
104 104
105 105 if pygmentspresent:
106 106
107 107 class TestRunnerStyle(style.Style):
108 108 default_style = ""
109 109 skipped = token.string_to_tokentype("Token.Generic.Skipped")
110 110 failed = token.string_to_tokentype("Token.Generic.Failed")
111 111 skippedname = token.string_to_tokentype("Token.Generic.SName")
112 112 failedname = token.string_to_tokentype("Token.Generic.FName")
113 113 styles = {
114 114 skipped: '#e5e5e5',
115 115 skippedname: '#00ffff',
116 116 failed: '#7f0000',
117 117 failedname: '#ff0000',
118 118 }
119 119
120 120 class TestRunnerLexer(lexer.RegexLexer):
121 121 testpattern = r'[\w-]+\.(t|py)(#[a-zA-Z0-9_\-\.]+)?'
122 122 tokens = {
123 123 'root': [
124 124 (r'^Skipped', token.Generic.Skipped, 'skipped'),
125 125 (r'^Failed ', token.Generic.Failed, 'failed'),
126 126 (r'^ERROR: ', token.Generic.Failed, 'failed'),
127 127 ],
128 128 'skipped': [
129 129 (testpattern, token.Generic.SName),
130 130 (r':.*', token.Generic.Skipped),
131 131 ],
132 132 'failed': [
133 133 (testpattern, token.Generic.FName),
134 134 (r'(:| ).*', token.Generic.Failed),
135 135 ],
136 136 }
137 137
138 138 runnerformatter = formatters.Terminal256Formatter(style=TestRunnerStyle)
139 139 runnerlexer = TestRunnerLexer()
140 140
141 141 origenviron = os.environ.copy()
142 142
143 143 if sys.version_info > (3, 5, 0):
144 144 PYTHON3 = True
145 145 xrange = range # we use xrange in one place, and we'd rather not use range
146 146
147 147 def _sys2bytes(p):
148 148 if p is None:
149 149 return p
150 150 return p.encode('utf-8')
151 151
152 152 def _bytes2sys(p):
153 153 if p is None:
154 154 return p
155 155 return p.decode('utf-8')
156 156
157 157 osenvironb = getattr(os, 'environb', None)
158 158 if osenvironb is None:
159 159 # Windows lacks os.environb, for instance. A proxy over the real thing
160 160 # instead of a copy allows the environment to be updated via bytes on
161 161 # all platforms.
162 162 class environbytes(object):
163 163 def __init__(self, strenv):
164 164 self.__len__ = strenv.__len__
165 165 self.clear = strenv.clear
166 166 self._strenv = strenv
167 167
168 168 def __getitem__(self, k):
169 169 v = self._strenv.__getitem__(_bytes2sys(k))
170 170 return _sys2bytes(v)
171 171
172 172 def __setitem__(self, k, v):
173 173 self._strenv.__setitem__(_bytes2sys(k), _bytes2sys(v))
174 174
175 175 def __delitem__(self, k):
176 176 self._strenv.__delitem__(_bytes2sys(k))
177 177
178 178 def __contains__(self, k):
179 179 return self._strenv.__contains__(_bytes2sys(k))
180 180
181 181 def __iter__(self):
182 182 return iter([_sys2bytes(k) for k in iter(self._strenv)])
183 183
184 184 def get(self, k, default=None):
185 185 v = self._strenv.get(_bytes2sys(k), _bytes2sys(default))
186 186 return _sys2bytes(v)
187 187
188 188 def pop(self, k, default=None):
189 189 v = self._strenv.pop(_bytes2sys(k), _bytes2sys(default))
190 190 return _sys2bytes(v)
191 191
192 192 osenvironb = environbytes(os.environ)
193 193
194 194 getcwdb = getattr(os, 'getcwdb')
195 195 if not getcwdb or os.name == 'nt':
196 196 getcwdb = lambda: _sys2bytes(os.getcwd())
197 197
198 198 elif sys.version_info >= (3, 0, 0):
199 199 print(
200 200 '%s is only supported on Python 3.5+ and 2.7, not %s'
201 201 % (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3]))
202 202 )
203 203 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
204 204 else:
205 205 PYTHON3 = False
206 206
207 207 # In python 2.x, path operations are generally done using
208 208 # bytestrings by default, so we don't have to do any extra
209 209 # fiddling there. We define the wrapper functions anyway just to
210 210 # help keep code consistent between platforms.
211 211 def _sys2bytes(p):
212 212 return p
213 213
214 214 _bytes2sys = _sys2bytes
215 215 osenvironb = os.environ
216 216 getcwdb = os.getcwd
217 217
218 218 # For Windows support
219 219 wifexited = getattr(os, "WIFEXITED", lambda x: False)
220 220
221 221 # Whether to use IPv6
222 222 def checksocketfamily(name, port=20058):
223 223 """return true if we can listen on localhost using family=name
224 224
225 225 name should be either 'AF_INET', or 'AF_INET6'.
226 226 port being used is okay - EADDRINUSE is considered as successful.
227 227 """
228 228 family = getattr(socket, name, None)
229 229 if family is None:
230 230 return False
231 231 try:
232 232 s = socket.socket(family, socket.SOCK_STREAM)
233 233 s.bind(('localhost', port))
234 234 s.close()
235 235 return True
236 236 except socket.error as exc:
237 237 if exc.errno == errno.EADDRINUSE:
238 238 return True
239 239 elif exc.errno in (errno.EADDRNOTAVAIL, errno.EPROTONOSUPPORT):
240 240 return False
241 241 else:
242 242 raise
243 243 else:
244 244 return False
245 245
246 246
247 247 # useipv6 will be set by parseargs
248 248 useipv6 = None
249 249
250 250
251 251 def checkportisavailable(port):
252 252 """return true if a port seems free to bind on localhost"""
253 253 if useipv6:
254 254 family = socket.AF_INET6
255 255 else:
256 256 family = socket.AF_INET
257 257 try:
258 258 s = socket.socket(family, socket.SOCK_STREAM)
259 259 s.bind(('localhost', port))
260 260 s.close()
261 261 return True
262 262 except socket.error as exc:
263 263 if exc.errno not in (
264 264 errno.EADDRINUSE,
265 265 errno.EADDRNOTAVAIL,
266 266 errno.EPROTONOSUPPORT,
267 267 ):
268 268 raise
269 269 return False
270 270
271 271
272 272 closefds = os.name == 'posix'
273 273
274 274
275 275 def Popen4(cmd, wd, timeout, env=None):
276 276 processlock.acquire()
277 277 p = subprocess.Popen(
278 278 _bytes2sys(cmd),
279 279 shell=True,
280 280 bufsize=-1,
281 281 cwd=_bytes2sys(wd),
282 282 env=env,
283 283 close_fds=closefds,
284 284 stdin=subprocess.PIPE,
285 285 stdout=subprocess.PIPE,
286 286 stderr=subprocess.STDOUT,
287 287 )
288 288 processlock.release()
289 289
290 290 p.fromchild = p.stdout
291 291 p.tochild = p.stdin
292 292 p.childerr = p.stderr
293 293
294 294 p.timeout = False
295 295 if timeout:
296 296
297 297 def t():
298 298 start = time.time()
299 299 while time.time() - start < timeout and p.returncode is None:
300 300 time.sleep(0.1)
301 301 p.timeout = True
302 302 if p.returncode is None:
303 303 terminate(p)
304 304
305 305 threading.Thread(target=t).start()
306 306
307 307 return p
308 308
309 309
310 310 if sys.executable:
311 311 sysexecutable = sys.executable
312 312 elif os.environ.get('PYTHONEXECUTABLE'):
313 313 sysexecutable = os.environ['PYTHONEXECUTABLE']
314 314 elif os.environ.get('PYTHON'):
315 315 sysexecutable = os.environ['PYTHON']
316 316 else:
317 317 raise AssertionError('Could not find Python interpreter')
318 318
319 319 PYTHON = _sys2bytes(sysexecutable.replace('\\', '/'))
320 320 IMPL_PATH = b'PYTHONPATH'
321 321 if 'java' in sys.platform:
322 322 IMPL_PATH = b'JYTHONPATH'
323 323
324 324 default_defaults = {
325 325 'jobs': ('HGTEST_JOBS', multiprocessing.cpu_count()),
326 326 'timeout': ('HGTEST_TIMEOUT', 180),
327 327 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 1500),
328 328 'port': ('HGTEST_PORT', 20059),
329 329 'shell': ('HGTEST_SHELL', 'sh'),
330 330 }
331 331
332 332 defaults = default_defaults.copy()
333 333
334 334
335 335 def canonpath(path):
336 336 return os.path.realpath(os.path.expanduser(path))
337 337
338 338
339 339 def parselistfiles(files, listtype, warn=True):
340 340 entries = dict()
341 341 for filename in files:
342 342 try:
343 343 path = os.path.expanduser(os.path.expandvars(filename))
344 344 f = open(path, "rb")
345 345 except IOError as err:
346 346 if err.errno != errno.ENOENT:
347 347 raise
348 348 if warn:
349 349 print("warning: no such %s file: %s" % (listtype, filename))
350 350 continue
351 351
352 352 for line in f.readlines():
353 353 line = line.split(b'#', 1)[0].strip()
354 354 if line:
355 355 entries[line] = filename
356 356
357 357 f.close()
358 358 return entries
359 359
360 360
361 361 def parsettestcases(path):
362 362 """read a .t test file, return a set of test case names
363 363
364 364 If path does not exist, return an empty set.
365 365 """
366 366 cases = []
367 367 try:
368 368 with open(path, 'rb') as f:
369 369 for l in f:
370 370 if l.startswith(b'#testcases '):
371 371 cases.append(sorted(l[11:].split()))
372 372 except IOError as ex:
373 373 if ex.errno != errno.ENOENT:
374 374 raise
375 375 return cases
376 376
377 377
378 378 def getparser():
379 379 """Obtain the OptionParser used by the CLI."""
380 380 parser = argparse.ArgumentParser(usage='%(prog)s [options] [tests]')
381 381
382 382 selection = parser.add_argument_group('Test Selection')
383 383 selection.add_argument(
384 384 '--allow-slow-tests',
385 385 action='store_true',
386 386 help='allow extremely slow tests',
387 387 )
388 388 selection.add_argument(
389 389 "--blacklist",
390 390 action="append",
391 391 help="skip tests listed in the specified blacklist file",
392 392 )
393 393 selection.add_argument(
394 394 "--changed",
395 395 help="run tests that are changed in parent rev or working directory",
396 396 )
397 397 selection.add_argument(
398 398 "-k", "--keywords", help="run tests matching keywords"
399 399 )
400 400 selection.add_argument(
401 401 "-r", "--retest", action="store_true", help="retest failed tests"
402 402 )
403 403 selection.add_argument(
404 404 "--test-list",
405 405 action="append",
406 406 help="read tests to run from the specified file",
407 407 )
408 408 selection.add_argument(
409 409 "--whitelist",
410 410 action="append",
411 411 help="always run tests listed in the specified whitelist file",
412 412 )
413 413 selection.add_argument(
414 414 'tests', metavar='TESTS', nargs='*', help='Tests to run'
415 415 )
416 416
417 417 harness = parser.add_argument_group('Test Harness Behavior')
418 418 harness.add_argument(
419 419 '--bisect-repo',
420 420 metavar='bisect_repo',
421 421 help=(
422 422 "Path of a repo to bisect. Use together with " "--known-good-rev"
423 423 ),
424 424 )
425 425 harness.add_argument(
426 426 "-d",
427 427 "--debug",
428 428 action="store_true",
429 429 help="debug mode: write output of test scripts to console"
430 430 " rather than capturing and diffing it (disables timeout)",
431 431 )
432 432 harness.add_argument(
433 433 "-f",
434 434 "--first",
435 435 action="store_true",
436 436 help="exit on the first test failure",
437 437 )
438 438 harness.add_argument(
439 439 "-i",
440 440 "--interactive",
441 441 action="store_true",
442 442 help="prompt to accept changed output",
443 443 )
444 444 harness.add_argument(
445 445 "-j",
446 446 "--jobs",
447 447 type=int,
448 448 help="number of jobs to run in parallel"
449 449 " (default: $%s or %d)" % defaults['jobs'],
450 450 )
451 451 harness.add_argument(
452 452 "--keep-tmpdir",
453 453 action="store_true",
454 454 help="keep temporary directory after running tests",
455 455 )
456 456 harness.add_argument(
457 457 '--known-good-rev',
458 458 metavar="known_good_rev",
459 459 help=(
460 460 "Automatically bisect any failures using this "
461 461 "revision as a known-good revision."
462 462 ),
463 463 )
464 464 harness.add_argument(
465 465 "--list-tests",
466 466 action="store_true",
467 467 help="list tests instead of running them",
468 468 )
469 469 harness.add_argument(
470 470 "--loop", action="store_true", help="loop tests repeatedly"
471 471 )
472 472 harness.add_argument(
473 473 '--random', action="store_true", help='run tests in random order'
474 474 )
475 475 harness.add_argument(
476 476 '--order-by-runtime',
477 477 action="store_true",
478 478 help='run slowest tests first, according to .testtimes',
479 479 )
480 480 harness.add_argument(
481 481 "-p",
482 482 "--port",
483 483 type=int,
484 484 help="port on which servers should listen"
485 485 " (default: $%s or %d)" % defaults['port'],
486 486 )
487 487 harness.add_argument(
488 488 '--profile-runner',
489 489 action='store_true',
490 490 help='run statprof on run-tests',
491 491 )
492 492 harness.add_argument(
493 493 "-R", "--restart", action="store_true", help="restart at last error"
494 494 )
495 495 harness.add_argument(
496 496 "--runs-per-test",
497 497 type=int,
498 498 dest="runs_per_test",
499 499 help="run each test N times (default=1)",
500 500 default=1,
501 501 )
502 502 harness.add_argument(
503 503 "--shell", help="shell to use (default: $%s or %s)" % defaults['shell']
504 504 )
505 505 harness.add_argument(
506 506 '--showchannels', action='store_true', help='show scheduling channels'
507 507 )
508 508 harness.add_argument(
509 509 "--slowtimeout",
510 510 type=int,
511 511 help="kill errant slow tests after SLOWTIMEOUT seconds"
512 512 " (default: $%s or %d)" % defaults['slowtimeout'],
513 513 )
514 514 harness.add_argument(
515 515 "-t",
516 516 "--timeout",
517 517 type=int,
518 518 help="kill errant tests after TIMEOUT seconds"
519 519 " (default: $%s or %d)" % defaults['timeout'],
520 520 )
521 521 harness.add_argument(
522 522 "--tmpdir",
523 523 help="run tests in the given temporary directory"
524 524 " (implies --keep-tmpdir)",
525 525 )
526 526 harness.add_argument(
527 527 "-v", "--verbose", action="store_true", help="output verbose messages"
528 528 )
529 529
530 530 hgconf = parser.add_argument_group('Mercurial Configuration')
531 531 hgconf.add_argument(
532 532 "--chg",
533 533 action="store_true",
534 534 help="install and use chg wrapper in place of hg",
535 535 )
536 536 hgconf.add_argument(
537 537 "--chg-debug", action="store_true", help="show chg debug logs",
538 538 )
539 539 hgconf.add_argument("--compiler", help="compiler to build with")
540 540 hgconf.add_argument(
541 541 '--extra-config-opt',
542 542 action="append",
543 543 default=[],
544 544 help='set the given config opt in the test hgrc',
545 545 )
546 546 hgconf.add_argument(
547 547 "-l",
548 548 "--local",
549 549 action="store_true",
550 550 help="shortcut for --with-hg=<testdir>/../hg, "
551 551 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set",
552 552 )
553 553 hgconf.add_argument(
554 554 "--ipv6",
555 555 action="store_true",
556 556 help="prefer IPv6 to IPv4 for network related tests",
557 557 )
558 558 hgconf.add_argument(
559 559 "--pure",
560 560 action="store_true",
561 561 help="use pure Python code instead of C extensions",
562 562 )
563 563 hgconf.add_argument(
564 564 "--rust",
565 565 action="store_true",
566 566 help="use Rust code alongside C extensions",
567 567 )
568 568 hgconf.add_argument(
569 569 "--no-rust",
570 570 action="store_true",
571 571 help="do not use Rust code even if compiled",
572 572 )
573 573 hgconf.add_argument(
574 574 "--with-chg",
575 575 metavar="CHG",
576 576 help="use specified chg wrapper in place of hg",
577 577 )
578 578 hgconf.add_argument(
579 579 "--with-hg",
580 580 metavar="HG",
581 581 help="test using specified hg script rather than a "
582 582 "temporary installation",
583 583 )
584 584
585 585 reporting = parser.add_argument_group('Results Reporting')
586 586 reporting.add_argument(
587 587 "-C",
588 588 "--annotate",
589 589 action="store_true",
590 590 help="output files annotated with coverage",
591 591 )
592 592 reporting.add_argument(
593 593 "--color",
594 594 choices=["always", "auto", "never"],
595 595 default=os.environ.get('HGRUNTESTSCOLOR', 'auto'),
596 596 help="colorisation: always|auto|never (default: auto)",
597 597 )
598 598 reporting.add_argument(
599 599 "-c",
600 600 "--cover",
601 601 action="store_true",
602 602 help="print a test coverage report",
603 603 )
604 604 reporting.add_argument(
605 605 '--exceptions',
606 606 action='store_true',
607 607 help='log all exceptions and generate an exception report',
608 608 )
609 609 reporting.add_argument(
610 610 "-H",
611 611 "--htmlcov",
612 612 action="store_true",
613 613 help="create an HTML report of the coverage of the files",
614 614 )
615 615 reporting.add_argument(
616 616 "--json",
617 617 action="store_true",
618 618 help="store test result data in 'report.json' file",
619 619 )
620 620 reporting.add_argument(
621 621 "--outputdir",
622 622 help="directory to write error logs to (default=test directory)",
623 623 )
624 624 reporting.add_argument(
625 625 "-n", "--nodiff", action="store_true", help="skip showing test changes"
626 626 )
627 627 reporting.add_argument(
628 628 "-S",
629 629 "--noskips",
630 630 action="store_true",
631 631 help="don't report skip tests verbosely",
632 632 )
633 633 reporting.add_argument(
634 634 "--time", action="store_true", help="time how long each test takes"
635 635 )
636 636 reporting.add_argument("--view", help="external diff viewer")
637 637 reporting.add_argument(
638 638 "--xunit", help="record xunit results at specified path"
639 639 )
640 640
641 641 for option, (envvar, default) in defaults.items():
642 642 defaults[option] = type(default)(os.environ.get(envvar, default))
643 643 parser.set_defaults(**defaults)
644 644
645 645 return parser
646 646
647 647
648 648 def parseargs(args, parser):
649 649 """Parse arguments with our OptionParser and validate results."""
650 650 options = parser.parse_args(args)
651 651
652 652 # jython is always pure
653 653 if 'java' in sys.platform or '__pypy__' in sys.modules:
654 654 options.pure = True
655 655
656 656 if platform.python_implementation() != 'CPython' and options.rust:
657 657 parser.error('Rust extensions are only available with CPython')
658 658
659 659 if options.pure and options.rust:
660 660 parser.error('--rust cannot be used with --pure')
661 661
662 662 if options.rust and options.no_rust:
663 663 parser.error('--rust cannot be used with --no-rust')
664 664
665 665 if options.local:
666 666 if options.with_hg or options.with_chg:
667 667 parser.error('--local cannot be used with --with-hg or --with-chg')
668 668 testdir = os.path.dirname(_sys2bytes(canonpath(sys.argv[0])))
669 669 reporootdir = os.path.dirname(testdir)
670 670 pathandattrs = [(b'hg', 'with_hg')]
671 671 if options.chg:
672 672 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
673 673 for relpath, attr in pathandattrs:
674 674 binpath = os.path.join(reporootdir, relpath)
675 675 if os.name != 'nt' and not os.access(binpath, os.X_OK):
676 676 parser.error(
677 677 '--local specified, but %r not found or '
678 678 'not executable' % binpath
679 679 )
680 680 setattr(options, attr, _bytes2sys(binpath))
681 681
682 682 if options.with_hg:
683 683 options.with_hg = canonpath(_sys2bytes(options.with_hg))
684 684 if not (
685 685 os.path.isfile(options.with_hg)
686 686 and os.access(options.with_hg, os.X_OK)
687 687 ):
688 688 parser.error('--with-hg must specify an executable hg script')
689 689 if os.path.basename(options.with_hg) not in [b'hg', b'hg.exe']:
690 690 sys.stderr.write('warning: --with-hg should specify an hg script\n')
691 691 sys.stderr.flush()
692 692
693 693 if (options.chg or options.with_chg) and os.name == 'nt':
694 694 parser.error('chg does not work on %s' % os.name)
695 695 if options.with_chg:
696 696 options.chg = False # no installation to temporary location
697 697 options.with_chg = canonpath(_sys2bytes(options.with_chg))
698 698 if not (
699 699 os.path.isfile(options.with_chg)
700 700 and os.access(options.with_chg, os.X_OK)
701 701 ):
702 702 parser.error('--with-chg must specify a chg executable')
703 703 if options.chg and options.with_hg:
704 704 # chg shares installation location with hg
705 705 parser.error(
706 706 '--chg does not work when --with-hg is specified '
707 707 '(use --with-chg instead)'
708 708 )
709 709
710 710 if options.color == 'always' and not pygmentspresent:
711 711 sys.stderr.write(
712 712 'warning: --color=always ignored because '
713 713 'pygments is not installed\n'
714 714 )
715 715
716 716 if options.bisect_repo and not options.known_good_rev:
717 717 parser.error("--bisect-repo cannot be used without --known-good-rev")
718 718
719 719 global useipv6
720 720 if options.ipv6:
721 721 useipv6 = checksocketfamily('AF_INET6')
722 722 else:
723 723 # only use IPv6 if IPv4 is unavailable and IPv6 is available
724 724 useipv6 = (not checksocketfamily('AF_INET')) and checksocketfamily(
725 725 'AF_INET6'
726 726 )
727 727
728 728 options.anycoverage = options.cover or options.annotate or options.htmlcov
729 729 if options.anycoverage:
730 730 try:
731 731 import coverage
732 732
733 733 covver = version.StrictVersion(coverage.__version__).version
734 734 if covver < (3, 3):
735 735 parser.error('coverage options require coverage 3.3 or later')
736 736 except ImportError:
737 737 parser.error('coverage options now require the coverage package')
738 738
739 739 if options.anycoverage and options.local:
740 740 # this needs some path mangling somewhere, I guess
741 741 parser.error(
742 742 "sorry, coverage options do not work when --local " "is specified"
743 743 )
744 744
745 745 if options.anycoverage and options.with_hg:
746 746 parser.error(
747 747 "sorry, coverage options do not work when --with-hg " "is specified"
748 748 )
749 749
750 750 global verbose
751 751 if options.verbose:
752 752 verbose = ''
753 753
754 754 if options.tmpdir:
755 755 options.tmpdir = canonpath(options.tmpdir)
756 756
757 757 if options.jobs < 1:
758 758 parser.error('--jobs must be positive')
759 759 if options.interactive and options.debug:
760 760 parser.error("-i/--interactive and -d/--debug are incompatible")
761 761 if options.debug:
762 762 if options.timeout != defaults['timeout']:
763 763 sys.stderr.write('warning: --timeout option ignored with --debug\n')
764 764 if options.slowtimeout != defaults['slowtimeout']:
765 765 sys.stderr.write(
766 766 'warning: --slowtimeout option ignored with --debug\n'
767 767 )
768 768 options.timeout = 0
769 769 options.slowtimeout = 0
770 770
771 771 if options.blacklist:
772 772 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
773 773 if options.whitelist:
774 774 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
775 775 else:
776 776 options.whitelisted = {}
777 777
778 778 if options.showchannels:
779 779 options.nodiff = True
780 780
781 781 return options
782 782
783 783
784 784 def rename(src, dst):
785 785 """Like os.rename(), trade atomicity and opened files friendliness
786 786 for existing destination support.
787 787 """
788 788 shutil.copy(src, dst)
789 789 os.remove(src)
790 790
791 791
792 792 def makecleanable(path):
793 793 """Try to fix directory permission recursively so that the entire tree
794 794 can be deleted"""
795 795 for dirpath, dirnames, _filenames in os.walk(path, topdown=True):
796 796 for d in dirnames:
797 797 p = os.path.join(dirpath, d)
798 798 try:
799 799 os.chmod(p, os.stat(p).st_mode & 0o777 | 0o700) # chmod u+rwx
800 800 except OSError:
801 801 pass
802 802
803 803
804 804 _unified_diff = difflib.unified_diff
805 805 if PYTHON3:
806 806 import functools
807 807
808 808 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
809 809
810 810
811 811 def getdiff(expected, output, ref, err):
812 812 servefail = False
813 813 lines = []
814 814 for line in _unified_diff(expected, output, ref, err):
815 815 if line.startswith(b'+++') or line.startswith(b'---'):
816 816 line = line.replace(b'\\', b'/')
817 817 if line.endswith(b' \n'):
818 818 line = line[:-2] + b'\n'
819 819 lines.append(line)
820 820 if not servefail and line.startswith(
821 821 b'+ abort: child process failed to start'
822 822 ):
823 823 servefail = True
824 824
825 825 return servefail, lines
826 826
827 827
828 828 verbose = False
829 829
830 830
831 831 def vlog(*msg):
832 832 """Log only when in verbose mode."""
833 833 if verbose is False:
834 834 return
835 835
836 836 return log(*msg)
837 837
838 838
839 839 # Bytes that break XML even in a CDATA block: control characters 0-31
840 840 # sans \t, \n and \r
841 841 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
842 842
843 843 # Match feature conditionalized output lines in the form, capturing the feature
844 844 # list in group 2, and the preceeding line output in group 1:
845 845 #
846 846 # output..output (feature !)\n
847 847 optline = re.compile(br'(.*) \((.+?) !\)\n$')
848 848
849 849
850 850 def cdatasafe(data):
851 851 """Make a string safe to include in a CDATA block.
852 852
853 853 Certain control characters are illegal in a CDATA block, and
854 854 there's no way to include a ]]> in a CDATA either. This function
855 855 replaces illegal bytes with ? and adds a space between the ]] so
856 856 that it won't break the CDATA block.
857 857 """
858 858 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
859 859
860 860
861 861 def log(*msg):
862 862 """Log something to stdout.
863 863
864 864 Arguments are strings to print.
865 865 """
866 866 with iolock:
867 867 if verbose:
868 868 print(verbose, end=' ')
869 869 for m in msg:
870 870 print(m, end=' ')
871 871 print()
872 872 sys.stdout.flush()
873 873
874 874
875 875 def highlightdiff(line, color):
876 876 if not color:
877 877 return line
878 878 assert pygmentspresent
879 879 return pygments.highlight(
880 880 line.decode('latin1'), difflexer, terminal256formatter
881 881 ).encode('latin1')
882 882
883 883
884 884 def highlightmsg(msg, color):
885 885 if not color:
886 886 return msg
887 887 assert pygmentspresent
888 888 return pygments.highlight(msg, runnerlexer, runnerformatter)
889 889
890 890
891 891 def terminate(proc):
892 892 """Terminate subprocess"""
893 893 vlog('# Terminating process %d' % proc.pid)
894 894 try:
895 895 proc.terminate()
896 896 except OSError:
897 897 pass
898 898
899 899
900 900 def killdaemons(pidfile):
901 901 import killdaemons as killmod
902 902
903 903 return killmod.killdaemons(pidfile, tryhard=False, remove=True, logfn=vlog)
904 904
905 905
906 906 class Test(unittest.TestCase):
907 907 """Encapsulates a single, runnable test.
908 908
909 909 While this class conforms to the unittest.TestCase API, it differs in that
910 910 instances need to be instantiated manually. (Typically, unittest.TestCase
911 911 classes are instantiated automatically by scanning modules.)
912 912 """
913 913
914 914 # Status code reserved for skipped tests (used by hghave).
915 915 SKIPPED_STATUS = 80
916 916
917 917 def __init__(
918 918 self,
919 919 path,
920 920 outputdir,
921 921 tmpdir,
922 922 keeptmpdir=False,
923 923 debug=False,
924 924 first=False,
925 925 timeout=None,
926 926 startport=None,
927 927 extraconfigopts=None,
928 928 shell=None,
929 929 hgcommand=None,
930 930 slowtimeout=None,
931 931 usechg=False,
932 932 chgdebug=False,
933 933 useipv6=False,
934 934 ):
935 935 """Create a test from parameters.
936 936
937 937 path is the full path to the file defining the test.
938 938
939 939 tmpdir is the main temporary directory to use for this test.
940 940
941 941 keeptmpdir determines whether to keep the test's temporary directory
942 942 after execution. It defaults to removal (False).
943 943
944 944 debug mode will make the test execute verbosely, with unfiltered
945 945 output.
946 946
947 947 timeout controls the maximum run time of the test. It is ignored when
948 948 debug is True. See slowtimeout for tests with #require slow.
949 949
950 950 slowtimeout overrides timeout if the test has #require slow.
951 951
952 952 startport controls the starting port number to use for this test. Each
953 953 test will reserve 3 port numbers for execution. It is the caller's
954 954 responsibility to allocate a non-overlapping port range to Test
955 955 instances.
956 956
957 957 extraconfigopts is an iterable of extra hgrc config options. Values
958 958 must have the form "key=value" (something understood by hgrc). Values
959 959 of the form "foo.key=value" will result in "[foo] key=value".
960 960
961 961 shell is the shell to execute tests in.
962 962 """
963 963 if timeout is None:
964 964 timeout = defaults['timeout']
965 965 if startport is None:
966 966 startport = defaults['port']
967 967 if slowtimeout is None:
968 968 slowtimeout = defaults['slowtimeout']
969 969 self.path = path
970 970 self.relpath = os.path.relpath(path)
971 971 self.bname = os.path.basename(path)
972 972 self.name = _bytes2sys(self.bname)
973 973 self._testdir = os.path.dirname(path)
974 974 self._outputdir = outputdir
975 975 self._tmpname = os.path.basename(path)
976 976 self.errpath = os.path.join(self._outputdir, b'%s.err' % self.bname)
977 977
978 978 self._threadtmp = tmpdir
979 979 self._keeptmpdir = keeptmpdir
980 980 self._debug = debug
981 981 self._first = first
982 982 self._timeout = timeout
983 983 self._slowtimeout = slowtimeout
984 984 self._startport = startport
985 985 self._extraconfigopts = extraconfigopts or []
986 986 self._shell = _sys2bytes(shell)
987 987 self._hgcommand = hgcommand or b'hg'
988 988 self._usechg = usechg
989 989 self._chgdebug = chgdebug
990 990 self._useipv6 = useipv6
991 991
992 992 self._aborted = False
993 993 self._daemonpids = []
994 994 self._finished = None
995 995 self._ret = None
996 996 self._out = None
997 997 self._skipped = None
998 998 self._testtmp = None
999 999 self._chgsockdir = None
1000 1000
1001 1001 self._refout = self.readrefout()
1002 1002
1003 1003 def readrefout(self):
1004 1004 """read reference output"""
1005 1005 # If we're not in --debug mode and reference output file exists,
1006 1006 # check test output against it.
1007 1007 if self._debug:
1008 1008 return None # to match "out is None"
1009 1009 elif os.path.exists(self.refpath):
1010 1010 with open(self.refpath, 'rb') as f:
1011 1011 return f.read().splitlines(True)
1012 1012 else:
1013 1013 return []
1014 1014
1015 1015 # needed to get base class __repr__ running
1016 1016 @property
1017 1017 def _testMethodName(self):
1018 1018 return self.name
1019 1019
1020 1020 def __str__(self):
1021 1021 return self.name
1022 1022
1023 1023 def shortDescription(self):
1024 1024 return self.name
1025 1025
1026 1026 def setUp(self):
1027 1027 """Tasks to perform before run()."""
1028 1028 self._finished = False
1029 1029 self._ret = None
1030 1030 self._out = None
1031 1031 self._skipped = None
1032 1032
1033 1033 try:
1034 1034 os.mkdir(self._threadtmp)
1035 1035 except OSError as e:
1036 1036 if e.errno != errno.EEXIST:
1037 1037 raise
1038 1038
1039 1039 name = self._tmpname
1040 1040 self._testtmp = os.path.join(self._threadtmp, name)
1041 1041 os.mkdir(self._testtmp)
1042 1042
1043 1043 # Remove any previous output files.
1044 1044 if os.path.exists(self.errpath):
1045 1045 try:
1046 1046 os.remove(self.errpath)
1047 1047 except OSError as e:
1048 1048 # We might have raced another test to clean up a .err
1049 1049 # file, so ignore ENOENT when removing a previous .err
1050 1050 # file.
1051 1051 if e.errno != errno.ENOENT:
1052 1052 raise
1053 1053
1054 1054 if self._usechg:
1055 1055 self._chgsockdir = os.path.join(
1056 1056 self._threadtmp, b'%s.chgsock' % name
1057 1057 )
1058 1058 os.mkdir(self._chgsockdir)
1059 1059
1060 1060 def run(self, result):
1061 1061 """Run this test and report results against a TestResult instance."""
1062 1062 # This function is extremely similar to unittest.TestCase.run(). Once
1063 1063 # we require Python 2.7 (or at least its version of unittest), this
1064 1064 # function can largely go away.
1065 1065 self._result = result
1066 1066 result.startTest(self)
1067 1067 try:
1068 1068 try:
1069 1069 self.setUp()
1070 1070 except (KeyboardInterrupt, SystemExit):
1071 1071 self._aborted = True
1072 1072 raise
1073 1073 except Exception:
1074 1074 result.addError(self, sys.exc_info())
1075 1075 return
1076 1076
1077 1077 success = False
1078 1078 try:
1079 1079 self.runTest()
1080 1080 except KeyboardInterrupt:
1081 1081 self._aborted = True
1082 1082 raise
1083 1083 except unittest.SkipTest as e:
1084 1084 result.addSkip(self, str(e))
1085 1085 # The base class will have already counted this as a
1086 1086 # test we "ran", but we want to exclude skipped tests
1087 1087 # from those we count towards those run.
1088 1088 result.testsRun -= 1
1089 1089 except self.failureException as e:
1090 1090 # This differs from unittest in that we don't capture
1091 1091 # the stack trace. This is for historical reasons and
1092 1092 # this decision could be revisited in the future,
1093 1093 # especially for PythonTest instances.
1094 1094 if result.addFailure(self, str(e)):
1095 1095 success = True
1096 1096 except Exception:
1097 1097 result.addError(self, sys.exc_info())
1098 1098 else:
1099 1099 success = True
1100 1100
1101 1101 try:
1102 1102 self.tearDown()
1103 1103 except (KeyboardInterrupt, SystemExit):
1104 1104 self._aborted = True
1105 1105 raise
1106 1106 except Exception:
1107 1107 result.addError(self, sys.exc_info())
1108 1108 success = False
1109 1109
1110 1110 if success:
1111 1111 result.addSuccess(self)
1112 1112 finally:
1113 1113 result.stopTest(self, interrupted=self._aborted)
1114 1114
1115 1115 def runTest(self):
1116 1116 """Run this test instance.
1117 1117
1118 1118 This will return a tuple describing the result of the test.
1119 1119 """
1120 1120 env = self._getenv()
1121 1121 self._genrestoreenv(env)
1122 1122 self._daemonpids.append(env['DAEMON_PIDS'])
1123 1123 self._createhgrc(env['HGRCPATH'])
1124 1124
1125 1125 vlog('# Test', self.name)
1126 1126
1127 1127 ret, out = self._run(env)
1128 1128 self._finished = True
1129 1129 self._ret = ret
1130 1130 self._out = out
1131 1131
1132 1132 def describe(ret):
1133 1133 if ret < 0:
1134 1134 return 'killed by signal: %d' % -ret
1135 1135 return 'returned error code %d' % ret
1136 1136
1137 1137 self._skipped = False
1138 1138
1139 1139 if ret == self.SKIPPED_STATUS:
1140 1140 if out is None: # Debug mode, nothing to parse.
1141 1141 missing = ['unknown']
1142 1142 failed = None
1143 1143 else:
1144 1144 missing, failed = TTest.parsehghaveoutput(out)
1145 1145
1146 1146 if not missing:
1147 1147 missing = ['skipped']
1148 1148
1149 1149 if failed:
1150 1150 self.fail('hg have failed checking for %s' % failed[-1])
1151 1151 else:
1152 1152 self._skipped = True
1153 1153 raise unittest.SkipTest(missing[-1])
1154 1154 elif ret == 'timeout':
1155 1155 self.fail('timed out')
1156 1156 elif ret is False:
1157 1157 self.fail('no result code from test')
1158 1158 elif out != self._refout:
1159 1159 # Diff generation may rely on written .err file.
1160 1160 if (
1161 1161 (ret != 0 or out != self._refout)
1162 1162 and not self._skipped
1163 1163 and not self._debug
1164 1164 ):
1165 1165 with open(self.errpath, 'wb') as f:
1166 1166 for line in out:
1167 1167 f.write(line)
1168 1168
1169 1169 # The result object handles diff calculation for us.
1170 1170 with firstlock:
1171 1171 if self._result.addOutputMismatch(self, ret, out, self._refout):
1172 1172 # change was accepted, skip failing
1173 1173 return
1174 1174 if self._first:
1175 1175 global firsterror
1176 1176 firsterror = True
1177 1177
1178 1178 if ret:
1179 1179 msg = 'output changed and ' + describe(ret)
1180 1180 else:
1181 1181 msg = 'output changed'
1182 1182
1183 1183 self.fail(msg)
1184 1184 elif ret:
1185 1185 self.fail(describe(ret))
1186 1186
1187 1187 def tearDown(self):
1188 1188 """Tasks to perform after run()."""
1189 1189 for entry in self._daemonpids:
1190 1190 killdaemons(entry)
1191 1191 self._daemonpids = []
1192 1192
1193 1193 if self._keeptmpdir:
1194 1194 log(
1195 1195 '\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s'
1196 1196 % (_bytes2sys(self._testtmp), _bytes2sys(self._threadtmp),)
1197 1197 )
1198 1198 else:
1199 1199 try:
1200 1200 shutil.rmtree(self._testtmp)
1201 1201 except OSError:
1202 1202 # unreadable directory may be left in $TESTTMP; fix permission
1203 1203 # and try again
1204 1204 makecleanable(self._testtmp)
1205 1205 shutil.rmtree(self._testtmp, True)
1206 1206 shutil.rmtree(self._threadtmp, True)
1207 1207
1208 1208 if self._usechg:
1209 1209 # chgservers will stop automatically after they find the socket
1210 1210 # files are deleted
1211 1211 shutil.rmtree(self._chgsockdir, True)
1212 1212
1213 1213 if (
1214 1214 (self._ret != 0 or self._out != self._refout)
1215 1215 and not self._skipped
1216 1216 and not self._debug
1217 1217 and self._out
1218 1218 ):
1219 1219 with open(self.errpath, 'wb') as f:
1220 1220 for line in self._out:
1221 1221 f.write(line)
1222 1222
1223 1223 vlog("# Ret was:", self._ret, '(%s)' % self.name)
1224 1224
1225 1225 def _run(self, env):
1226 1226 # This should be implemented in child classes to run tests.
1227 1227 raise unittest.SkipTest('unknown test type')
1228 1228
1229 1229 def abort(self):
1230 1230 """Terminate execution of this test."""
1231 1231 self._aborted = True
1232 1232
1233 1233 def _portmap(self, i):
1234 1234 offset = b'' if i == 0 else b'%d' % i
1235 1235 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
1236 1236
1237 1237 def _getreplacements(self):
1238 1238 """Obtain a mapping of text replacements to apply to test output.
1239 1239
1240 1240 Test output needs to be normalized so it can be compared to expected
1241 1241 output. This function defines how some of that normalization will
1242 1242 occur.
1243 1243 """
1244 1244 r = [
1245 1245 # This list should be parallel to defineport in _getenv
1246 1246 self._portmap(0),
1247 1247 self._portmap(1),
1248 1248 self._portmap(2),
1249 1249 (br'([^0-9])%s' % re.escape(self._localip()), br'\1$LOCALIP'),
1250 1250 (br'\bHG_TXNID=TXN:[a-f0-9]{40}\b', br'HG_TXNID=TXN:$ID$'),
1251 1251 ]
1252 1252 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
1253 1253
1254 1254 replacementfile = os.path.join(self._testdir, b'common-pattern.py')
1255 1255
1256 1256 if os.path.exists(replacementfile):
1257 1257 data = {}
1258 1258 with open(replacementfile, mode='rb') as source:
1259 1259 # the intermediate 'compile' step help with debugging
1260 1260 code = compile(source.read(), replacementfile, 'exec')
1261 1261 exec(code, data)
1262 1262 for value in data.get('substitutions', ()):
1263 1263 if len(value) != 2:
1264 1264 msg = 'malformatted substitution in %s: %r'
1265 1265 msg %= (replacementfile, value)
1266 1266 raise ValueError(msg)
1267 1267 r.append(value)
1268 1268 return r
1269 1269
1270 1270 def _escapepath(self, p):
1271 1271 if os.name == 'nt':
1272 1272 return b''.join(
1273 1273 c.isalpha()
1274 1274 and b'[%s%s]' % (c.lower(), c.upper())
1275 1275 or c in b'/\\'
1276 1276 and br'[/\\]'
1277 1277 or c.isdigit()
1278 1278 and c
1279 1279 or b'\\' + c
1280 1280 for c in [p[i : i + 1] for i in range(len(p))]
1281 1281 )
1282 1282 else:
1283 1283 return re.escape(p)
1284 1284
1285 1285 def _localip(self):
1286 1286 if self._useipv6:
1287 1287 return b'::1'
1288 1288 else:
1289 1289 return b'127.0.0.1'
1290 1290
1291 1291 def _genrestoreenv(self, testenv):
1292 1292 """Generate a script that can be used by tests to restore the original
1293 1293 environment."""
1294 1294 # Put the restoreenv script inside self._threadtmp
1295 1295 scriptpath = os.path.join(self._threadtmp, b'restoreenv.sh')
1296 1296 testenv['HGTEST_RESTOREENV'] = _bytes2sys(scriptpath)
1297 1297
1298 1298 # Only restore environment variable names that the shell allows
1299 1299 # us to export.
1300 1300 name_regex = re.compile('^[a-zA-Z][a-zA-Z0-9_]*$')
1301 1301
1302 1302 # Do not restore these variables; otherwise tests would fail.
1303 1303 reqnames = {'PYTHON', 'TESTDIR', 'TESTTMP'}
1304 1304
1305 1305 with open(scriptpath, 'w') as envf:
1306 1306 for name, value in origenviron.items():
1307 1307 if not name_regex.match(name):
1308 1308 # Skip environment variables with unusual names not
1309 1309 # allowed by most shells.
1310 1310 continue
1311 1311 if name in reqnames:
1312 1312 continue
1313 1313 envf.write('%s=%s\n' % (name, shellquote(value)))
1314 1314
1315 1315 for name in testenv:
1316 1316 if name in origenviron or name in reqnames:
1317 1317 continue
1318 1318 envf.write('unset %s\n' % (name,))
1319 1319
1320 1320 def _getenv(self):
1321 1321 """Obtain environment variables to use during test execution."""
1322 1322
1323 1323 def defineport(i):
1324 1324 offset = '' if i == 0 else '%s' % i
1325 1325 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
1326 1326
1327 1327 env = os.environ.copy()
1328 1328 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase') or ''
1329 1329 env['HGEMITWARNINGS'] = '1'
1330 1330 env['TESTTMP'] = _bytes2sys(self._testtmp)
1331 1331 env['TESTNAME'] = self.name
1332 1332 env['HOME'] = _bytes2sys(self._testtmp)
1333 1333 formated_timeout = _bytes2sys(b"%d" % default_defaults['timeout'][1])
1334 1334 env['HGTEST_TIMEOUT_DEFAULT'] = formated_timeout
1335 1335 env['HGTEST_TIMEOUT'] = _bytes2sys(b"%d" % self._timeout)
1336 1336 # This number should match portneeded in _getport
1337 1337 for port in xrange(3):
1338 1338 # This list should be parallel to _portmap in _getreplacements
1339 1339 defineport(port)
1340 1340 env["HGRCPATH"] = _bytes2sys(os.path.join(self._threadtmp, b'.hgrc'))
1341 1341 env["DAEMON_PIDS"] = _bytes2sys(
1342 1342 os.path.join(self._threadtmp, b'daemon.pids')
1343 1343 )
1344 1344 env["HGEDITOR"] = (
1345 1345 '"' + sysexecutable + '"' + ' -c "import sys; sys.exit(0)"'
1346 1346 )
1347 1347 env["HGUSER"] = "test"
1348 1348 env["HGENCODING"] = "ascii"
1349 1349 env["HGENCODINGMODE"] = "strict"
1350 1350 env["HGHOSTNAME"] = "test-hostname"
1351 1351 env['HGIPV6'] = str(int(self._useipv6))
1352 1352 # See contrib/catapipe.py for how to use this functionality.
1353 1353 if 'HGTESTCATAPULTSERVERPIPE' not in env:
1354 1354 # If we don't have HGTESTCATAPULTSERVERPIPE explicitly set, pull the
1355 1355 # non-test one in as a default, otherwise set to devnull
1356 1356 env['HGTESTCATAPULTSERVERPIPE'] = env.get(
1357 1357 'HGCATAPULTSERVERPIPE', os.devnull
1358 1358 )
1359 1359
1360 1360 extraextensions = []
1361 1361 for opt in self._extraconfigopts:
1362 1362 section, key = _sys2bytes(opt).split(b'.', 1)
1363 1363 if section != 'extensions':
1364 1364 continue
1365 1365 name = key.split(b'=', 1)[0]
1366 1366 extraextensions.append(name)
1367 1367
1368 1368 if extraextensions:
1369 1369 env['HGTESTEXTRAEXTENSIONS'] = b' '.join(extraextensions)
1370 1370
1371 1371 # LOCALIP could be ::1 or 127.0.0.1. Useful for tests that require raw
1372 1372 # IP addresses.
1373 1373 env['LOCALIP'] = _bytes2sys(self._localip())
1374 1374
1375 1375 # This has the same effect as Py_LegacyWindowsStdioFlag in exewrapper.c,
1376 1376 # but this is needed for testing python instances like dummyssh,
1377 1377 # dummysmtpd.py, and dumbhttp.py.
1378 1378 if PYTHON3 and os.name == 'nt':
1379 1379 env['PYTHONLEGACYWINDOWSSTDIO'] = '1'
1380 1380
1381 1381 # Modified HOME in test environment can confuse Rust tools. So set
1382 1382 # CARGO_HOME and RUSTUP_HOME automatically if a Rust toolchain is
1383 1383 # present and these variables aren't already defined.
1384 1384 cargo_home_path = os.path.expanduser('~/.cargo')
1385 1385 rustup_home_path = os.path.expanduser('~/.rustup')
1386 1386
1387 1387 if os.path.exists(cargo_home_path) and b'CARGO_HOME' not in osenvironb:
1388 1388 env['CARGO_HOME'] = cargo_home_path
1389 1389 if (
1390 1390 os.path.exists(rustup_home_path)
1391 1391 and b'RUSTUP_HOME' not in osenvironb
1392 1392 ):
1393 1393 env['RUSTUP_HOME'] = rustup_home_path
1394 1394
1395 1395 # Reset some environment variables to well-known values so that
1396 1396 # the tests produce repeatable output.
1397 1397 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
1398 1398 env['TZ'] = 'GMT'
1399 1399 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1400 1400 env['COLUMNS'] = '80'
1401 1401 env['TERM'] = 'xterm'
1402 1402
1403 1403 dropped = [
1404 1404 'CDPATH',
1405 1405 'CHGDEBUG',
1406 1406 'EDITOR',
1407 1407 'GREP_OPTIONS',
1408 1408 'HG',
1409 1409 'HGMERGE',
1410 1410 'HGPLAIN',
1411 1411 'HGPLAINEXCEPT',
1412 1412 'HGPROF',
1413 1413 'http_proxy',
1414 1414 'no_proxy',
1415 1415 'NO_PROXY',
1416 1416 'PAGER',
1417 1417 'VISUAL',
1418 1418 ]
1419 1419
1420 1420 for k in dropped:
1421 1421 if k in env:
1422 1422 del env[k]
1423 1423
1424 1424 # unset env related to hooks
1425 1425 for k in list(env):
1426 1426 if k.startswith('HG_'):
1427 1427 del env[k]
1428 1428
1429 1429 if self._usechg:
1430 1430 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
1431 1431 if self._chgdebug:
1432 1432 env['CHGDEBUG'] = 'true'
1433 1433
1434 1434 return env
1435 1435
1436 1436 def _createhgrc(self, path):
1437 1437 """Create an hgrc file for this test."""
1438 1438 with open(path, 'wb') as hgrc:
1439 1439 hgrc.write(b'[ui]\n')
1440 1440 hgrc.write(b'slash = True\n')
1441 1441 hgrc.write(b'interactive = False\n')
1442 hgrc.write(b'detailed-exit-code = True\n')
1442 1443 hgrc.write(b'merge = internal:merge\n')
1443 1444 hgrc.write(b'mergemarkers = detailed\n')
1444 1445 hgrc.write(b'promptecho = True\n')
1445 1446 hgrc.write(b'[defaults]\n')
1446 1447 hgrc.write(b'[devel]\n')
1447 1448 hgrc.write(b'all-warnings = true\n')
1448 1449 hgrc.write(b'default-date = 0 0\n')
1449 1450 hgrc.write(b'[largefiles]\n')
1450 1451 hgrc.write(
1451 1452 b'usercache = %s\n'
1452 1453 % (os.path.join(self._testtmp, b'.cache/largefiles'))
1453 1454 )
1454 1455 hgrc.write(b'[lfs]\n')
1455 1456 hgrc.write(
1456 1457 b'usercache = %s\n'
1457 1458 % (os.path.join(self._testtmp, b'.cache/lfs'))
1458 1459 )
1459 1460 hgrc.write(b'[web]\n')
1460 1461 hgrc.write(b'address = localhost\n')
1461 1462 hgrc.write(b'ipv6 = %r\n' % self._useipv6)
1462 1463 hgrc.write(b'server-header = testing stub value\n')
1463 1464
1464 1465 for opt in self._extraconfigopts:
1465 1466 section, key = _sys2bytes(opt).split(b'.', 1)
1466 1467 assert b'=' in key, (
1467 1468 'extra config opt %s must ' 'have an = for assignment' % opt
1468 1469 )
1469 1470 hgrc.write(b'[%s]\n%s\n' % (section, key))
1470 1471
1471 1472 def fail(self, msg):
1472 1473 # unittest differentiates between errored and failed.
1473 1474 # Failed is denoted by AssertionError (by default at least).
1474 1475 raise AssertionError(msg)
1475 1476
1476 1477 def _runcommand(self, cmd, env, normalizenewlines=False):
1477 1478 """Run command in a sub-process, capturing the output (stdout and
1478 1479 stderr).
1479 1480
1480 1481 Return a tuple (exitcode, output). output is None in debug mode.
1481 1482 """
1482 1483 if self._debug:
1483 1484 proc = subprocess.Popen(
1484 1485 _bytes2sys(cmd),
1485 1486 shell=True,
1486 1487 cwd=_bytes2sys(self._testtmp),
1487 1488 env=env,
1488 1489 )
1489 1490 ret = proc.wait()
1490 1491 return (ret, None)
1491 1492
1492 1493 proc = Popen4(cmd, self._testtmp, self._timeout, env)
1493 1494
1494 1495 def cleanup():
1495 1496 terminate(proc)
1496 1497 ret = proc.wait()
1497 1498 if ret == 0:
1498 1499 ret = signal.SIGTERM << 8
1499 1500 killdaemons(env['DAEMON_PIDS'])
1500 1501 return ret
1501 1502
1502 1503 proc.tochild.close()
1503 1504
1504 1505 try:
1505 1506 output = proc.fromchild.read()
1506 1507 except KeyboardInterrupt:
1507 1508 vlog('# Handling keyboard interrupt')
1508 1509 cleanup()
1509 1510 raise
1510 1511
1511 1512 ret = proc.wait()
1512 1513 if wifexited(ret):
1513 1514 ret = os.WEXITSTATUS(ret)
1514 1515
1515 1516 if proc.timeout:
1516 1517 ret = 'timeout'
1517 1518
1518 1519 if ret:
1519 1520 killdaemons(env['DAEMON_PIDS'])
1520 1521
1521 1522 for s, r in self._getreplacements():
1522 1523 output = re.sub(s, r, output)
1523 1524
1524 1525 if normalizenewlines:
1525 1526 output = output.replace(b'\r\n', b'\n')
1526 1527
1527 1528 return ret, output.splitlines(True)
1528 1529
1529 1530
1530 1531 class PythonTest(Test):
1531 1532 """A Python-based test."""
1532 1533
1533 1534 @property
1534 1535 def refpath(self):
1535 1536 return os.path.join(self._testdir, b'%s.out' % self.bname)
1536 1537
1537 1538 def _run(self, env):
1538 1539 # Quote the python(3) executable for Windows
1539 1540 cmd = b'"%s" "%s"' % (PYTHON, self.path)
1540 1541 vlog("# Running", cmd.decode("utf-8"))
1541 1542 normalizenewlines = os.name == 'nt'
1542 1543 result = self._runcommand(cmd, env, normalizenewlines=normalizenewlines)
1543 1544 if self._aborted:
1544 1545 raise KeyboardInterrupt()
1545 1546
1546 1547 return result
1547 1548
1548 1549
1549 1550 # Some glob patterns apply only in some circumstances, so the script
1550 1551 # might want to remove (glob) annotations that otherwise should be
1551 1552 # retained.
1552 1553 checkcodeglobpats = [
1553 1554 # On Windows it looks like \ doesn't require a (glob), but we know
1554 1555 # better.
1555 1556 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1556 1557 re.compile(br'^moving \S+/.*[^)]$'),
1557 1558 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1558 1559 # Not all platforms have 127.0.0.1 as loopback (though most do),
1559 1560 # so we always glob that too.
1560 1561 re.compile(br'.*\$LOCALIP.*$'),
1561 1562 ]
1562 1563
1563 1564 bchr = chr
1564 1565 if PYTHON3:
1565 1566 bchr = lambda x: bytes([x])
1566 1567
1567 1568 WARN_UNDEFINED = 1
1568 1569 WARN_YES = 2
1569 1570 WARN_NO = 3
1570 1571
1571 1572 MARK_OPTIONAL = b" (?)\n"
1572 1573
1573 1574
1574 1575 def isoptional(line):
1575 1576 return line.endswith(MARK_OPTIONAL)
1576 1577
1577 1578
1578 1579 class TTest(Test):
1579 1580 """A "t test" is a test backed by a .t file."""
1580 1581
1581 1582 SKIPPED_PREFIX = b'skipped: '
1582 1583 FAILED_PREFIX = b'hghave check failed: '
1583 1584 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1584 1585
1585 1586 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1586 1587 ESCAPEMAP = {bchr(i): br'\x%02x' % i for i in range(256)}
1587 1588 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1588 1589
1589 1590 def __init__(self, path, *args, **kwds):
1590 1591 # accept an extra "case" parameter
1591 1592 case = kwds.pop('case', [])
1592 1593 self._case = case
1593 1594 self._allcases = {x for y in parsettestcases(path) for x in y}
1594 1595 super(TTest, self).__init__(path, *args, **kwds)
1595 1596 if case:
1596 1597 casepath = b'#'.join(case)
1597 1598 self.name = '%s#%s' % (self.name, _bytes2sys(casepath))
1598 1599 self.errpath = b'%s#%s.err' % (self.errpath[:-4], casepath)
1599 1600 self._tmpname += b'-%s' % casepath.replace(b'#', b'-')
1600 1601 self._have = {}
1601 1602
1602 1603 @property
1603 1604 def refpath(self):
1604 1605 return os.path.join(self._testdir, self.bname)
1605 1606
1606 1607 def _run(self, env):
1607 1608 with open(self.path, 'rb') as f:
1608 1609 lines = f.readlines()
1609 1610
1610 1611 # .t file is both reference output and the test input, keep reference
1611 1612 # output updated with the the test input. This avoids some race
1612 1613 # conditions where the reference output does not match the actual test.
1613 1614 if self._refout is not None:
1614 1615 self._refout = lines
1615 1616
1616 1617 salt, script, after, expected = self._parsetest(lines)
1617 1618
1618 1619 # Write out the generated script.
1619 1620 fname = b'%s.sh' % self._testtmp
1620 1621 with open(fname, 'wb') as f:
1621 1622 for l in script:
1622 1623 f.write(l)
1623 1624
1624 1625 cmd = b'%s "%s"' % (self._shell, fname)
1625 1626 vlog("# Running", cmd.decode("utf-8"))
1626 1627
1627 1628 exitcode, output = self._runcommand(cmd, env)
1628 1629
1629 1630 if self._aborted:
1630 1631 raise KeyboardInterrupt()
1631 1632
1632 1633 # Do not merge output if skipped. Return hghave message instead.
1633 1634 # Similarly, with --debug, output is None.
1634 1635 if exitcode == self.SKIPPED_STATUS or output is None:
1635 1636 return exitcode, output
1636 1637
1637 1638 return self._processoutput(exitcode, output, salt, after, expected)
1638 1639
1639 1640 def _hghave(self, reqs):
1640 1641 allreqs = b' '.join(reqs)
1641 1642
1642 1643 self._detectslow(reqs)
1643 1644
1644 1645 if allreqs in self._have:
1645 1646 return self._have.get(allreqs)
1646 1647
1647 1648 # TODO do something smarter when all other uses of hghave are gone.
1648 1649 runtestdir = osenvironb[b'RUNTESTDIR']
1649 1650 tdir = runtestdir.replace(b'\\', b'/')
1650 1651 proc = Popen4(
1651 1652 b'%s -c "%s/hghave %s"' % (self._shell, tdir, allreqs),
1652 1653 self._testtmp,
1653 1654 0,
1654 1655 self._getenv(),
1655 1656 )
1656 1657 stdout, stderr = proc.communicate()
1657 1658 ret = proc.wait()
1658 1659 if wifexited(ret):
1659 1660 ret = os.WEXITSTATUS(ret)
1660 1661 if ret == 2:
1661 1662 print(stdout.decode('utf-8'))
1662 1663 sys.exit(1)
1663 1664
1664 1665 if ret != 0:
1665 1666 self._have[allreqs] = (False, stdout)
1666 1667 return False, stdout
1667 1668
1668 1669 self._have[allreqs] = (True, None)
1669 1670 return True, None
1670 1671
1671 1672 def _detectslow(self, reqs):
1672 1673 """update the timeout of slow test when appropriate"""
1673 1674 if b'slow' in reqs:
1674 1675 self._timeout = self._slowtimeout
1675 1676
1676 1677 def _iftest(self, args):
1677 1678 # implements "#if"
1678 1679 reqs = []
1679 1680 for arg in args:
1680 1681 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1681 1682 if arg[3:] in self._case:
1682 1683 return False
1683 1684 elif arg in self._allcases:
1684 1685 if arg not in self._case:
1685 1686 return False
1686 1687 else:
1687 1688 reqs.append(arg)
1688 1689 self._detectslow(reqs)
1689 1690 return self._hghave(reqs)[0]
1690 1691
1691 1692 def _parsetest(self, lines):
1692 1693 # We generate a shell script which outputs unique markers to line
1693 1694 # up script results with our source. These markers include input
1694 1695 # line number and the last return code.
1695 1696 salt = b"SALT%d" % time.time()
1696 1697
1697 1698 def addsalt(line, inpython):
1698 1699 if inpython:
1699 1700 script.append(b'%s %d 0\n' % (salt, line))
1700 1701 else:
1701 1702 script.append(b'echo %s %d $?\n' % (salt, line))
1702 1703
1703 1704 activetrace = []
1704 1705 session = str(uuid.uuid4())
1705 1706 if PYTHON3:
1706 1707 session = session.encode('ascii')
1707 1708 hgcatapult = os.getenv('HGTESTCATAPULTSERVERPIPE') or os.getenv(
1708 1709 'HGCATAPULTSERVERPIPE'
1709 1710 )
1710 1711
1711 1712 def toggletrace(cmd=None):
1712 1713 if not hgcatapult or hgcatapult == os.devnull:
1713 1714 return
1714 1715
1715 1716 if activetrace:
1716 1717 script.append(
1717 1718 b'echo END %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1718 1719 % (session, activetrace[0])
1719 1720 )
1720 1721 if cmd is None:
1721 1722 return
1722 1723
1723 1724 if isinstance(cmd, str):
1724 1725 quoted = shellquote(cmd.strip())
1725 1726 else:
1726 1727 quoted = shellquote(cmd.strip().decode('utf8')).encode('utf8')
1727 1728 quoted = quoted.replace(b'\\', b'\\\\')
1728 1729 script.append(
1729 1730 b'echo START %s %s >> "$HGTESTCATAPULTSERVERPIPE"\n'
1730 1731 % (session, quoted)
1731 1732 )
1732 1733 activetrace[0:] = [quoted]
1733 1734
1734 1735 script = []
1735 1736
1736 1737 # After we run the shell script, we re-unify the script output
1737 1738 # with non-active parts of the source, with synchronization by our
1738 1739 # SALT line number markers. The after table contains the non-active
1739 1740 # components, ordered by line number.
1740 1741 after = {}
1741 1742
1742 1743 # Expected shell script output.
1743 1744 expected = {}
1744 1745
1745 1746 pos = prepos = -1
1746 1747
1747 1748 # True or False when in a true or false conditional section
1748 1749 skipping = None
1749 1750
1750 1751 # We keep track of whether or not we're in a Python block so we
1751 1752 # can generate the surrounding doctest magic.
1752 1753 inpython = False
1753 1754
1754 1755 if self._debug:
1755 1756 script.append(b'set -x\n')
1756 1757 if self._hgcommand != b'hg':
1757 1758 script.append(b'alias hg="%s"\n' % self._hgcommand)
1758 1759 if os.getenv('MSYSTEM'):
1759 1760 script.append(b'alias pwd="pwd -W"\n')
1760 1761
1761 1762 if hgcatapult and hgcatapult != os.devnull:
1762 1763 if PYTHON3:
1763 1764 hgcatapult = hgcatapult.encode('utf8')
1764 1765 cataname = self.name.encode('utf8')
1765 1766 else:
1766 1767 cataname = self.name
1767 1768
1768 1769 # Kludge: use a while loop to keep the pipe from getting
1769 1770 # closed by our echo commands. The still-running file gets
1770 1771 # reaped at the end of the script, which causes the while
1771 1772 # loop to exit and closes the pipe. Sigh.
1772 1773 script.append(
1773 1774 b'rtendtracing() {\n'
1774 1775 b' echo END %(session)s %(name)s >> %(catapult)s\n'
1775 1776 b' rm -f "$TESTTMP/.still-running"\n'
1776 1777 b'}\n'
1777 1778 b'trap "rtendtracing" 0\n'
1778 1779 b'touch "$TESTTMP/.still-running"\n'
1779 1780 b'while [ -f "$TESTTMP/.still-running" ]; do sleep 1; done '
1780 1781 b'> %(catapult)s &\n'
1781 1782 b'HGCATAPULTSESSION=%(session)s ; export HGCATAPULTSESSION\n'
1782 1783 b'echo START %(session)s %(name)s >> %(catapult)s\n'
1783 1784 % {
1784 1785 b'name': cataname,
1785 1786 b'session': session,
1786 1787 b'catapult': hgcatapult,
1787 1788 }
1788 1789 )
1789 1790
1790 1791 if self._case:
1791 1792 casestr = b'#'.join(self._case)
1792 1793 if isinstance(casestr, str):
1793 1794 quoted = shellquote(casestr)
1794 1795 else:
1795 1796 quoted = shellquote(casestr.decode('utf8')).encode('utf8')
1796 1797 script.append(b'TESTCASE=%s\n' % quoted)
1797 1798 script.append(b'export TESTCASE\n')
1798 1799
1799 1800 n = 0
1800 1801 for n, l in enumerate(lines):
1801 1802 if not l.endswith(b'\n'):
1802 1803 l += b'\n'
1803 1804 if l.startswith(b'#require'):
1804 1805 lsplit = l.split()
1805 1806 if len(lsplit) < 2 or lsplit[0] != b'#require':
1806 1807 after.setdefault(pos, []).append(
1807 1808 b' !!! invalid #require\n'
1808 1809 )
1809 1810 if not skipping:
1810 1811 haveresult, message = self._hghave(lsplit[1:])
1811 1812 if not haveresult:
1812 1813 script = [b'echo "%s"\nexit 80\n' % message]
1813 1814 break
1814 1815 after.setdefault(pos, []).append(l)
1815 1816 elif l.startswith(b'#if'):
1816 1817 lsplit = l.split()
1817 1818 if len(lsplit) < 2 or lsplit[0] != b'#if':
1818 1819 after.setdefault(pos, []).append(b' !!! invalid #if\n')
1819 1820 if skipping is not None:
1820 1821 after.setdefault(pos, []).append(b' !!! nested #if\n')
1821 1822 skipping = not self._iftest(lsplit[1:])
1822 1823 after.setdefault(pos, []).append(l)
1823 1824 elif l.startswith(b'#else'):
1824 1825 if skipping is None:
1825 1826 after.setdefault(pos, []).append(b' !!! missing #if\n')
1826 1827 skipping = not skipping
1827 1828 after.setdefault(pos, []).append(l)
1828 1829 elif l.startswith(b'#endif'):
1829 1830 if skipping is None:
1830 1831 after.setdefault(pos, []).append(b' !!! missing #if\n')
1831 1832 skipping = None
1832 1833 after.setdefault(pos, []).append(l)
1833 1834 elif skipping:
1834 1835 after.setdefault(pos, []).append(l)
1835 1836 elif l.startswith(b' >>> '): # python inlines
1836 1837 after.setdefault(pos, []).append(l)
1837 1838 prepos = pos
1838 1839 pos = n
1839 1840 if not inpython:
1840 1841 # We've just entered a Python block. Add the header.
1841 1842 inpython = True
1842 1843 addsalt(prepos, False) # Make sure we report the exit code.
1843 1844 script.append(b'"%s" -m heredoctest <<EOF\n' % PYTHON)
1844 1845 addsalt(n, True)
1845 1846 script.append(l[2:])
1846 1847 elif l.startswith(b' ... '): # python inlines
1847 1848 after.setdefault(prepos, []).append(l)
1848 1849 script.append(l[2:])
1849 1850 elif l.startswith(b' $ '): # commands
1850 1851 if inpython:
1851 1852 script.append(b'EOF\n')
1852 1853 inpython = False
1853 1854 after.setdefault(pos, []).append(l)
1854 1855 prepos = pos
1855 1856 pos = n
1856 1857 addsalt(n, False)
1857 1858 rawcmd = l[4:]
1858 1859 cmd = rawcmd.split()
1859 1860 toggletrace(rawcmd)
1860 1861 if len(cmd) == 2 and cmd[0] == b'cd':
1861 1862 rawcmd = b'cd %s || exit 1\n' % cmd[1]
1862 1863 script.append(rawcmd)
1863 1864 elif l.startswith(b' > '): # continuations
1864 1865 after.setdefault(prepos, []).append(l)
1865 1866 script.append(l[4:])
1866 1867 elif l.startswith(b' '): # results
1867 1868 # Queue up a list of expected results.
1868 1869 expected.setdefault(pos, []).append(l[2:])
1869 1870 else:
1870 1871 if inpython:
1871 1872 script.append(b'EOF\n')
1872 1873 inpython = False
1873 1874 # Non-command/result. Queue up for merged output.
1874 1875 after.setdefault(pos, []).append(l)
1875 1876
1876 1877 if inpython:
1877 1878 script.append(b'EOF\n')
1878 1879 if skipping is not None:
1879 1880 after.setdefault(pos, []).append(b' !!! missing #endif\n')
1880 1881 addsalt(n + 1, False)
1881 1882 # Need to end any current per-command trace
1882 1883 if activetrace:
1883 1884 toggletrace()
1884 1885 return salt, script, after, expected
1885 1886
1886 1887 def _processoutput(self, exitcode, output, salt, after, expected):
1887 1888 # Merge the script output back into a unified test.
1888 1889 warnonly = WARN_UNDEFINED # 1: not yet; 2: yes; 3: for sure not
1889 1890 if exitcode != 0:
1890 1891 warnonly = WARN_NO
1891 1892
1892 1893 pos = -1
1893 1894 postout = []
1894 1895 for out_rawline in output:
1895 1896 out_line, cmd_line = out_rawline, None
1896 1897 if salt in out_rawline:
1897 1898 out_line, cmd_line = out_rawline.split(salt, 1)
1898 1899
1899 1900 pos, postout, warnonly = self._process_out_line(
1900 1901 out_line, pos, postout, expected, warnonly
1901 1902 )
1902 1903 pos, postout = self._process_cmd_line(cmd_line, pos, postout, after)
1903 1904
1904 1905 if pos in after:
1905 1906 postout += after.pop(pos)
1906 1907
1907 1908 if warnonly == WARN_YES:
1908 1909 exitcode = False # Set exitcode to warned.
1909 1910
1910 1911 return exitcode, postout
1911 1912
1912 1913 def _process_out_line(self, out_line, pos, postout, expected, warnonly):
1913 1914 while out_line:
1914 1915 if not out_line.endswith(b'\n'):
1915 1916 out_line += b' (no-eol)\n'
1916 1917
1917 1918 # Find the expected output at the current position.
1918 1919 els = [None]
1919 1920 if expected.get(pos, None):
1920 1921 els = expected[pos]
1921 1922
1922 1923 optional = []
1923 1924 for i, el in enumerate(els):
1924 1925 r = False
1925 1926 if el:
1926 1927 r, exact = self.linematch(el, out_line)
1927 1928 if isinstance(r, str):
1928 1929 if r == '-glob':
1929 1930 out_line = ''.join(el.rsplit(' (glob)', 1))
1930 1931 r = '' # Warn only this line.
1931 1932 elif r == "retry":
1932 1933 postout.append(b' ' + el)
1933 1934 else:
1934 1935 log('\ninfo, unknown linematch result: %r\n' % r)
1935 1936 r = False
1936 1937 if r:
1937 1938 els.pop(i)
1938 1939 break
1939 1940 if el:
1940 1941 if isoptional(el):
1941 1942 optional.append(i)
1942 1943 else:
1943 1944 m = optline.match(el)
1944 1945 if m:
1945 1946 conditions = [c for c in m.group(2).split(b' ')]
1946 1947
1947 1948 if not self._iftest(conditions):
1948 1949 optional.append(i)
1949 1950 if exact:
1950 1951 # Don't allow line to be matches against a later
1951 1952 # line in the output
1952 1953 els.pop(i)
1953 1954 break
1954 1955
1955 1956 if r:
1956 1957 if r == "retry":
1957 1958 continue
1958 1959 # clean up any optional leftovers
1959 1960 for i in optional:
1960 1961 postout.append(b' ' + els[i])
1961 1962 for i in reversed(optional):
1962 1963 del els[i]
1963 1964 postout.append(b' ' + el)
1964 1965 else:
1965 1966 if self.NEEDESCAPE(out_line):
1966 1967 out_line = TTest._stringescape(
1967 1968 b'%s (esc)\n' % out_line.rstrip(b'\n')
1968 1969 )
1969 1970 postout.append(b' ' + out_line) # Let diff deal with it.
1970 1971 if r != '': # If line failed.
1971 1972 warnonly = WARN_NO
1972 1973 elif warnonly == WARN_UNDEFINED:
1973 1974 warnonly = WARN_YES
1974 1975 break
1975 1976 else:
1976 1977 # clean up any optional leftovers
1977 1978 while expected.get(pos, None):
1978 1979 el = expected[pos].pop(0)
1979 1980 if el:
1980 1981 if not isoptional(el):
1981 1982 m = optline.match(el)
1982 1983 if m:
1983 1984 conditions = [c for c in m.group(2).split(b' ')]
1984 1985
1985 1986 if self._iftest(conditions):
1986 1987 # Don't append as optional line
1987 1988 continue
1988 1989 else:
1989 1990 continue
1990 1991 postout.append(b' ' + el)
1991 1992 return pos, postout, warnonly
1992 1993
1993 1994 def _process_cmd_line(self, cmd_line, pos, postout, after):
1994 1995 """process a "command" part of a line from unified test output"""
1995 1996 if cmd_line:
1996 1997 # Add on last return code.
1997 1998 ret = int(cmd_line.split()[1])
1998 1999 if ret != 0:
1999 2000 postout.append(b' [%d]\n' % ret)
2000 2001 if pos in after:
2001 2002 # Merge in non-active test bits.
2002 2003 postout += after.pop(pos)
2003 2004 pos = int(cmd_line.split()[0])
2004 2005 return pos, postout
2005 2006
2006 2007 @staticmethod
2007 2008 def rematch(el, l):
2008 2009 try:
2009 2010 # parse any flags at the beginning of the regex. Only 'i' is
2010 2011 # supported right now, but this should be easy to extend.
2011 2012 flags, el = re.match(br'^(\(\?i\))?(.*)', el).groups()[0:2]
2012 2013 flags = flags or b''
2013 2014 el = flags + b'(?:' + el + b')'
2014 2015 # use \Z to ensure that the regex matches to the end of the string
2015 2016 if os.name == 'nt':
2016 2017 return re.match(el + br'\r?\n\Z', l)
2017 2018 return re.match(el + br'\n\Z', l)
2018 2019 except re.error:
2019 2020 # el is an invalid regex
2020 2021 return False
2021 2022
2022 2023 @staticmethod
2023 2024 def globmatch(el, l):
2024 2025 # The only supported special characters are * and ? plus / which also
2025 2026 # matches \ on windows. Escaping of these characters is supported.
2026 2027 if el + b'\n' == l:
2027 2028 if os.altsep:
2028 2029 # matching on "/" is not needed for this line
2029 2030 for pat in checkcodeglobpats:
2030 2031 if pat.match(el):
2031 2032 return True
2032 2033 return b'-glob'
2033 2034 return True
2034 2035 el = el.replace(b'$LOCALIP', b'*')
2035 2036 i, n = 0, len(el)
2036 2037 res = b''
2037 2038 while i < n:
2038 2039 c = el[i : i + 1]
2039 2040 i += 1
2040 2041 if c == b'\\' and i < n and el[i : i + 1] in b'*?\\/':
2041 2042 res += el[i - 1 : i + 1]
2042 2043 i += 1
2043 2044 elif c == b'*':
2044 2045 res += b'.*'
2045 2046 elif c == b'?':
2046 2047 res += b'.'
2047 2048 elif c == b'/' and os.altsep:
2048 2049 res += b'[/\\\\]'
2049 2050 else:
2050 2051 res += re.escape(c)
2051 2052 return TTest.rematch(res, l)
2052 2053
2053 2054 def linematch(self, el, l):
2054 2055 if el == l: # perfect match (fast)
2055 2056 return True, True
2056 2057 retry = False
2057 2058 if isoptional(el):
2058 2059 retry = "retry"
2059 2060 el = el[: -len(MARK_OPTIONAL)] + b"\n"
2060 2061 else:
2061 2062 m = optline.match(el)
2062 2063 if m:
2063 2064 conditions = [c for c in m.group(2).split(b' ')]
2064 2065
2065 2066 el = m.group(1) + b"\n"
2066 2067 if not self._iftest(conditions):
2067 2068 # listed feature missing, should not match
2068 2069 return "retry", False
2069 2070
2070 2071 if el.endswith(b" (esc)\n"):
2071 2072 if PYTHON3:
2072 2073 el = el[:-7].decode('unicode_escape') + '\n'
2073 2074 el = el.encode('latin-1')
2074 2075 else:
2075 2076 el = el[:-7].decode('string-escape') + '\n'
2076 2077 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
2077 2078 return True, True
2078 2079 if el.endswith(b" (re)\n"):
2079 2080 return (TTest.rematch(el[:-6], l) or retry), False
2080 2081 if el.endswith(b" (glob)\n"):
2081 2082 # ignore '(glob)' added to l by 'replacements'
2082 2083 if l.endswith(b" (glob)\n"):
2083 2084 l = l[:-8] + b"\n"
2084 2085 return (TTest.globmatch(el[:-8], l) or retry), False
2085 2086 if os.altsep:
2086 2087 _l = l.replace(b'\\', b'/')
2087 2088 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
2088 2089 return True, True
2089 2090 return retry, True
2090 2091
2091 2092 @staticmethod
2092 2093 def parsehghaveoutput(lines):
2093 2094 '''Parse hghave log lines.
2094 2095
2095 2096 Return tuple of lists (missing, failed):
2096 2097 * the missing/unknown features
2097 2098 * the features for which existence check failed'''
2098 2099 missing = []
2099 2100 failed = []
2100 2101 for line in lines:
2101 2102 if line.startswith(TTest.SKIPPED_PREFIX):
2102 2103 line = line.splitlines()[0]
2103 2104 missing.append(_bytes2sys(line[len(TTest.SKIPPED_PREFIX) :]))
2104 2105 elif line.startswith(TTest.FAILED_PREFIX):
2105 2106 line = line.splitlines()[0]
2106 2107 failed.append(_bytes2sys(line[len(TTest.FAILED_PREFIX) :]))
2107 2108
2108 2109 return missing, failed
2109 2110
2110 2111 @staticmethod
2111 2112 def _escapef(m):
2112 2113 return TTest.ESCAPEMAP[m.group(0)]
2113 2114
2114 2115 @staticmethod
2115 2116 def _stringescape(s):
2116 2117 return TTest.ESCAPESUB(TTest._escapef, s)
2117 2118
2118 2119
2119 2120 iolock = threading.RLock()
2120 2121 firstlock = threading.RLock()
2121 2122 firsterror = False
2122 2123
2123 2124
2124 2125 class TestResult(unittest._TextTestResult):
2125 2126 """Holds results when executing via unittest."""
2126 2127
2127 2128 # Don't worry too much about accessing the non-public _TextTestResult.
2128 2129 # It is relatively common in Python testing tools.
2129 2130 def __init__(self, options, *args, **kwargs):
2130 2131 super(TestResult, self).__init__(*args, **kwargs)
2131 2132
2132 2133 self._options = options
2133 2134
2134 2135 # unittest.TestResult didn't have skipped until 2.7. We need to
2135 2136 # polyfill it.
2136 2137 self.skipped = []
2137 2138
2138 2139 # We have a custom "ignored" result that isn't present in any Python
2139 2140 # unittest implementation. It is very similar to skipped. It may make
2140 2141 # sense to map it into skip some day.
2141 2142 self.ignored = []
2142 2143
2143 2144 self.times = []
2144 2145 self._firststarttime = None
2145 2146 # Data stored for the benefit of generating xunit reports.
2146 2147 self.successes = []
2147 2148 self.faildata = {}
2148 2149
2149 2150 if options.color == 'auto':
2150 2151 self.color = pygmentspresent and self.stream.isatty()
2151 2152 elif options.color == 'never':
2152 2153 self.color = False
2153 2154 else: # 'always', for testing purposes
2154 2155 self.color = pygmentspresent
2155 2156
2156 2157 def onStart(self, test):
2157 2158 """ Can be overriden by custom TestResult
2158 2159 """
2159 2160
2160 2161 def onEnd(self):
2161 2162 """ Can be overriden by custom TestResult
2162 2163 """
2163 2164
2164 2165 def addFailure(self, test, reason):
2165 2166 self.failures.append((test, reason))
2166 2167
2167 2168 if self._options.first:
2168 2169 self.stop()
2169 2170 else:
2170 2171 with iolock:
2171 2172 if reason == "timed out":
2172 2173 self.stream.write('t')
2173 2174 else:
2174 2175 if not self._options.nodiff:
2175 2176 self.stream.write('\n')
2176 2177 # Exclude the '\n' from highlighting to lex correctly
2177 2178 formatted = 'ERROR: %s output changed\n' % test
2178 2179 self.stream.write(highlightmsg(formatted, self.color))
2179 2180 self.stream.write('!')
2180 2181
2181 2182 self.stream.flush()
2182 2183
2183 2184 def addSuccess(self, test):
2184 2185 with iolock:
2185 2186 super(TestResult, self).addSuccess(test)
2186 2187 self.successes.append(test)
2187 2188
2188 2189 def addError(self, test, err):
2189 2190 super(TestResult, self).addError(test, err)
2190 2191 if self._options.first:
2191 2192 self.stop()
2192 2193
2193 2194 # Polyfill.
2194 2195 def addSkip(self, test, reason):
2195 2196 self.skipped.append((test, reason))
2196 2197 with iolock:
2197 2198 if self.showAll:
2198 2199 self.stream.writeln('skipped %s' % reason)
2199 2200 else:
2200 2201 self.stream.write('s')
2201 2202 self.stream.flush()
2202 2203
2203 2204 def addIgnore(self, test, reason):
2204 2205 self.ignored.append((test, reason))
2205 2206 with iolock:
2206 2207 if self.showAll:
2207 2208 self.stream.writeln('ignored %s' % reason)
2208 2209 else:
2209 2210 if reason not in ('not retesting', "doesn't match keyword"):
2210 2211 self.stream.write('i')
2211 2212 else:
2212 2213 self.testsRun += 1
2213 2214 self.stream.flush()
2214 2215
2215 2216 def addOutputMismatch(self, test, ret, got, expected):
2216 2217 """Record a mismatch in test output for a particular test."""
2217 2218 if self.shouldStop or firsterror:
2218 2219 # don't print, some other test case already failed and
2219 2220 # printed, we're just stale and probably failed due to our
2220 2221 # temp dir getting cleaned up.
2221 2222 return
2222 2223
2223 2224 accepted = False
2224 2225 lines = []
2225 2226
2226 2227 with iolock:
2227 2228 if self._options.nodiff:
2228 2229 pass
2229 2230 elif self._options.view:
2230 2231 v = self._options.view
2231 2232 subprocess.call(
2232 2233 r'"%s" "%s" "%s"'
2233 2234 % (v, _bytes2sys(test.refpath), _bytes2sys(test.errpath)),
2234 2235 shell=True,
2235 2236 )
2236 2237 else:
2237 2238 servefail, lines = getdiff(
2238 2239 expected, got, test.refpath, test.errpath
2239 2240 )
2240 2241 self.stream.write('\n')
2241 2242 for line in lines:
2242 2243 line = highlightdiff(line, self.color)
2243 2244 if PYTHON3:
2244 2245 self.stream.flush()
2245 2246 self.stream.buffer.write(line)
2246 2247 self.stream.buffer.flush()
2247 2248 else:
2248 2249 self.stream.write(line)
2249 2250 self.stream.flush()
2250 2251
2251 2252 if servefail:
2252 2253 raise test.failureException(
2253 2254 'server failed to start (HGPORT=%s)' % test._startport
2254 2255 )
2255 2256
2256 2257 # handle interactive prompt without releasing iolock
2257 2258 if self._options.interactive:
2258 2259 if test.readrefout() != expected:
2259 2260 self.stream.write(
2260 2261 'Reference output has changed (run again to prompt '
2261 2262 'changes)'
2262 2263 )
2263 2264 else:
2264 2265 self.stream.write('Accept this change? [y/N] ')
2265 2266 self.stream.flush()
2266 2267 answer = sys.stdin.readline().strip()
2267 2268 if answer.lower() in ('y', 'yes'):
2268 2269 if test.path.endswith(b'.t'):
2269 2270 rename(test.errpath, test.path)
2270 2271 else:
2271 2272 rename(test.errpath, '%s.out' % test.path)
2272 2273 accepted = True
2273 2274 if not accepted:
2274 2275 self.faildata[test.name] = b''.join(lines)
2275 2276
2276 2277 return accepted
2277 2278
2278 2279 def startTest(self, test):
2279 2280 super(TestResult, self).startTest(test)
2280 2281
2281 2282 # os.times module computes the user time and system time spent by
2282 2283 # child's processes along with real elapsed time taken by a process.
2283 2284 # This module has one limitation. It can only work for Linux user
2284 2285 # and not for Windows. Hence why we fall back to another function
2285 2286 # for wall time calculations.
2286 2287 test.started_times = os.times()
2287 2288 # TODO use a monotonic clock once support for Python 2.7 is dropped.
2288 2289 test.started_time = time.time()
2289 2290 if self._firststarttime is None: # thread racy but irrelevant
2290 2291 self._firststarttime = test.started_time
2291 2292
2292 2293 def stopTest(self, test, interrupted=False):
2293 2294 super(TestResult, self).stopTest(test)
2294 2295
2295 2296 test.stopped_times = os.times()
2296 2297 stopped_time = time.time()
2297 2298
2298 2299 starttime = test.started_times
2299 2300 endtime = test.stopped_times
2300 2301 origin = self._firststarttime
2301 2302 self.times.append(
2302 2303 (
2303 2304 test.name,
2304 2305 endtime[2] - starttime[2], # user space CPU time
2305 2306 endtime[3] - starttime[3], # sys space CPU time
2306 2307 stopped_time - test.started_time, # real time
2307 2308 test.started_time - origin, # start date in run context
2308 2309 stopped_time - origin, # end date in run context
2309 2310 )
2310 2311 )
2311 2312
2312 2313 if interrupted:
2313 2314 with iolock:
2314 2315 self.stream.writeln(
2315 2316 'INTERRUPTED: %s (after %d seconds)'
2316 2317 % (test.name, self.times[-1][3])
2317 2318 )
2318 2319
2319 2320
2320 2321 def getTestResult():
2321 2322 """
2322 2323 Returns the relevant test result
2323 2324 """
2324 2325 if "CUSTOM_TEST_RESULT" in os.environ:
2325 2326 testresultmodule = __import__(os.environ["CUSTOM_TEST_RESULT"])
2326 2327 return testresultmodule.TestResult
2327 2328 else:
2328 2329 return TestResult
2329 2330
2330 2331
2331 2332 class TestSuite(unittest.TestSuite):
2332 2333 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
2333 2334
2334 2335 def __init__(
2335 2336 self,
2336 2337 testdir,
2337 2338 jobs=1,
2338 2339 whitelist=None,
2339 2340 blacklist=None,
2340 2341 keywords=None,
2341 2342 loop=False,
2342 2343 runs_per_test=1,
2343 2344 loadtest=None,
2344 2345 showchannels=False,
2345 2346 *args,
2346 2347 **kwargs
2347 2348 ):
2348 2349 """Create a new instance that can run tests with a configuration.
2349 2350
2350 2351 testdir specifies the directory where tests are executed from. This
2351 2352 is typically the ``tests`` directory from Mercurial's source
2352 2353 repository.
2353 2354
2354 2355 jobs specifies the number of jobs to run concurrently. Each test
2355 2356 executes on its own thread. Tests actually spawn new processes, so
2356 2357 state mutation should not be an issue.
2357 2358
2358 2359 If there is only one job, it will use the main thread.
2359 2360
2360 2361 whitelist and blacklist denote tests that have been whitelisted and
2361 2362 blacklisted, respectively. These arguments don't belong in TestSuite.
2362 2363 Instead, whitelist and blacklist should be handled by the thing that
2363 2364 populates the TestSuite with tests. They are present to preserve
2364 2365 backwards compatible behavior which reports skipped tests as part
2365 2366 of the results.
2366 2367
2367 2368 keywords denotes key words that will be used to filter which tests
2368 2369 to execute. This arguably belongs outside of TestSuite.
2369 2370
2370 2371 loop denotes whether to loop over tests forever.
2371 2372 """
2372 2373 super(TestSuite, self).__init__(*args, **kwargs)
2373 2374
2374 2375 self._jobs = jobs
2375 2376 self._whitelist = whitelist
2376 2377 self._blacklist = blacklist
2377 2378 self._keywords = keywords
2378 2379 self._loop = loop
2379 2380 self._runs_per_test = runs_per_test
2380 2381 self._loadtest = loadtest
2381 2382 self._showchannels = showchannels
2382 2383
2383 2384 def run(self, result):
2384 2385 # We have a number of filters that need to be applied. We do this
2385 2386 # here instead of inside Test because it makes the running logic for
2386 2387 # Test simpler.
2387 2388 tests = []
2388 2389 num_tests = [0]
2389 2390 for test in self._tests:
2390 2391
2391 2392 def get():
2392 2393 num_tests[0] += 1
2393 2394 if getattr(test, 'should_reload', False):
2394 2395 return self._loadtest(test, num_tests[0])
2395 2396 return test
2396 2397
2397 2398 if not os.path.exists(test.path):
2398 2399 result.addSkip(test, "Doesn't exist")
2399 2400 continue
2400 2401
2401 2402 is_whitelisted = self._whitelist and (
2402 2403 test.relpath in self._whitelist or test.bname in self._whitelist
2403 2404 )
2404 2405 if not is_whitelisted:
2405 2406 is_blacklisted = self._blacklist and (
2406 2407 test.relpath in self._blacklist
2407 2408 or test.bname in self._blacklist
2408 2409 )
2409 2410 if is_blacklisted:
2410 2411 result.addSkip(test, 'blacklisted')
2411 2412 continue
2412 2413 if self._keywords:
2413 2414 with open(test.path, 'rb') as f:
2414 2415 t = f.read().lower() + test.bname.lower()
2415 2416 ignored = False
2416 2417 for k in self._keywords.lower().split():
2417 2418 if k not in t:
2418 2419 result.addIgnore(test, "doesn't match keyword")
2419 2420 ignored = True
2420 2421 break
2421 2422
2422 2423 if ignored:
2423 2424 continue
2424 2425 for _ in xrange(self._runs_per_test):
2425 2426 tests.append(get())
2426 2427
2427 2428 runtests = list(tests)
2428 2429 done = queue.Queue()
2429 2430 running = 0
2430 2431
2431 2432 channels = [""] * self._jobs
2432 2433
2433 2434 def job(test, result):
2434 2435 for n, v in enumerate(channels):
2435 2436 if not v:
2436 2437 channel = n
2437 2438 break
2438 2439 else:
2439 2440 raise ValueError('Could not find output channel')
2440 2441 channels[channel] = "=" + test.name[5:].split(".")[0]
2441 2442 try:
2442 2443 test(result)
2443 2444 done.put(None)
2444 2445 except KeyboardInterrupt:
2445 2446 pass
2446 2447 except: # re-raises
2447 2448 done.put(('!', test, 'run-test raised an error, see traceback'))
2448 2449 raise
2449 2450 finally:
2450 2451 try:
2451 2452 channels[channel] = ''
2452 2453 except IndexError:
2453 2454 pass
2454 2455
2455 2456 def stat():
2456 2457 count = 0
2457 2458 while channels:
2458 2459 d = '\n%03s ' % count
2459 2460 for n, v in enumerate(channels):
2460 2461 if v:
2461 2462 d += v[0]
2462 2463 channels[n] = v[1:] or '.'
2463 2464 else:
2464 2465 d += ' '
2465 2466 d += ' '
2466 2467 with iolock:
2467 2468 sys.stdout.write(d + ' ')
2468 2469 sys.stdout.flush()
2469 2470 for x in xrange(10):
2470 2471 if channels:
2471 2472 time.sleep(0.1)
2472 2473 count += 1
2473 2474
2474 2475 stoppedearly = False
2475 2476
2476 2477 if self._showchannels:
2477 2478 statthread = threading.Thread(target=stat, name="stat")
2478 2479 statthread.start()
2479 2480
2480 2481 try:
2481 2482 while tests or running:
2482 2483 if not done.empty() or running == self._jobs or not tests:
2483 2484 try:
2484 2485 done.get(True, 1)
2485 2486 running -= 1
2486 2487 if result and result.shouldStop:
2487 2488 stoppedearly = True
2488 2489 break
2489 2490 except queue.Empty:
2490 2491 continue
2491 2492 if tests and not running == self._jobs:
2492 2493 test = tests.pop(0)
2493 2494 if self._loop:
2494 2495 if getattr(test, 'should_reload', False):
2495 2496 num_tests[0] += 1
2496 2497 tests.append(self._loadtest(test, num_tests[0]))
2497 2498 else:
2498 2499 tests.append(test)
2499 2500 if self._jobs == 1:
2500 2501 job(test, result)
2501 2502 else:
2502 2503 t = threading.Thread(
2503 2504 target=job, name=test.name, args=(test, result)
2504 2505 )
2505 2506 t.start()
2506 2507 running += 1
2507 2508
2508 2509 # If we stop early we still need to wait on started tests to
2509 2510 # finish. Otherwise, there is a race between the test completing
2510 2511 # and the test's cleanup code running. This could result in the
2511 2512 # test reporting incorrect.
2512 2513 if stoppedearly:
2513 2514 while running:
2514 2515 try:
2515 2516 done.get(True, 1)
2516 2517 running -= 1
2517 2518 except queue.Empty:
2518 2519 continue
2519 2520 except KeyboardInterrupt:
2520 2521 for test in runtests:
2521 2522 test.abort()
2522 2523
2523 2524 channels = []
2524 2525
2525 2526 return result
2526 2527
2527 2528
2528 2529 # Save the most recent 5 wall-clock runtimes of each test to a
2529 2530 # human-readable text file named .testtimes. Tests are sorted
2530 2531 # alphabetically, while times for each test are listed from oldest to
2531 2532 # newest.
2532 2533
2533 2534
2534 2535 def loadtimes(outputdir):
2535 2536 times = []
2536 2537 try:
2537 2538 with open(os.path.join(outputdir, b'.testtimes')) as fp:
2538 2539 for line in fp:
2539 2540 m = re.match('(.*?) ([0-9. ]+)', line)
2540 2541 times.append(
2541 2542 (m.group(1), [float(t) for t in m.group(2).split()])
2542 2543 )
2543 2544 except IOError as err:
2544 2545 if err.errno != errno.ENOENT:
2545 2546 raise
2546 2547 return times
2547 2548
2548 2549
2549 2550 def savetimes(outputdir, result):
2550 2551 saved = dict(loadtimes(outputdir))
2551 2552 maxruns = 5
2552 2553 skipped = {str(t[0]) for t in result.skipped}
2553 2554 for tdata in result.times:
2554 2555 test, real = tdata[0], tdata[3]
2555 2556 if test not in skipped:
2556 2557 ts = saved.setdefault(test, [])
2557 2558 ts.append(real)
2558 2559 ts[:] = ts[-maxruns:]
2559 2560
2560 2561 fd, tmpname = tempfile.mkstemp(
2561 2562 prefix=b'.testtimes', dir=outputdir, text=True
2562 2563 )
2563 2564 with os.fdopen(fd, 'w') as fp:
2564 2565 for name, ts in sorted(saved.items()):
2565 2566 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2566 2567 timepath = os.path.join(outputdir, b'.testtimes')
2567 2568 try:
2568 2569 os.unlink(timepath)
2569 2570 except OSError:
2570 2571 pass
2571 2572 try:
2572 2573 os.rename(tmpname, timepath)
2573 2574 except OSError:
2574 2575 pass
2575 2576
2576 2577
2577 2578 class TextTestRunner(unittest.TextTestRunner):
2578 2579 """Custom unittest test runner that uses appropriate settings."""
2579 2580
2580 2581 def __init__(self, runner, *args, **kwargs):
2581 2582 super(TextTestRunner, self).__init__(*args, **kwargs)
2582 2583
2583 2584 self._runner = runner
2584 2585
2585 2586 self._result = getTestResult()(
2586 2587 self._runner.options, self.stream, self.descriptions, self.verbosity
2587 2588 )
2588 2589
2589 2590 def listtests(self, test):
2590 2591 test = sorted(test, key=lambda t: t.name)
2591 2592
2592 2593 self._result.onStart(test)
2593 2594
2594 2595 for t in test:
2595 2596 print(t.name)
2596 2597 self._result.addSuccess(t)
2597 2598
2598 2599 if self._runner.options.xunit:
2599 2600 with open(self._runner.options.xunit, "wb") as xuf:
2600 2601 self._writexunit(self._result, xuf)
2601 2602
2602 2603 if self._runner.options.json:
2603 2604 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2604 2605 with open(jsonpath, 'w') as fp:
2605 2606 self._writejson(self._result, fp)
2606 2607
2607 2608 return self._result
2608 2609
2609 2610 def run(self, test):
2610 2611 self._result.onStart(test)
2611 2612 test(self._result)
2612 2613
2613 2614 failed = len(self._result.failures)
2614 2615 skipped = len(self._result.skipped)
2615 2616 ignored = len(self._result.ignored)
2616 2617
2617 2618 with iolock:
2618 2619 self.stream.writeln('')
2619 2620
2620 2621 if not self._runner.options.noskips:
2621 2622 for test, msg in sorted(
2622 2623 self._result.skipped, key=lambda s: s[0].name
2623 2624 ):
2624 2625 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2625 2626 msg = highlightmsg(formatted, self._result.color)
2626 2627 self.stream.write(msg)
2627 2628 for test, msg in sorted(
2628 2629 self._result.failures, key=lambda f: f[0].name
2629 2630 ):
2630 2631 formatted = 'Failed %s: %s\n' % (test.name, msg)
2631 2632 self.stream.write(highlightmsg(formatted, self._result.color))
2632 2633 for test, msg in sorted(
2633 2634 self._result.errors, key=lambda e: e[0].name
2634 2635 ):
2635 2636 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2636 2637
2637 2638 if self._runner.options.xunit:
2638 2639 with open(self._runner.options.xunit, "wb") as xuf:
2639 2640 self._writexunit(self._result, xuf)
2640 2641
2641 2642 if self._runner.options.json:
2642 2643 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2643 2644 with open(jsonpath, 'w') as fp:
2644 2645 self._writejson(self._result, fp)
2645 2646
2646 2647 self._runner._checkhglib('Tested')
2647 2648
2648 2649 savetimes(self._runner._outputdir, self._result)
2649 2650
2650 2651 if failed and self._runner.options.known_good_rev:
2651 2652 self._bisecttests(t for t, m in self._result.failures)
2652 2653 self.stream.writeln(
2653 2654 '# Ran %d tests, %d skipped, %d failed.'
2654 2655 % (self._result.testsRun, skipped + ignored, failed)
2655 2656 )
2656 2657 if failed:
2657 2658 self.stream.writeln(
2658 2659 'python hash seed: %s' % os.environ['PYTHONHASHSEED']
2659 2660 )
2660 2661 if self._runner.options.time:
2661 2662 self.printtimes(self._result.times)
2662 2663
2663 2664 if self._runner.options.exceptions:
2664 2665 exceptions = aggregateexceptions(
2665 2666 os.path.join(self._runner._outputdir, b'exceptions')
2666 2667 )
2667 2668
2668 2669 self.stream.writeln('Exceptions Report:')
2669 2670 self.stream.writeln(
2670 2671 '%d total from %d frames'
2671 2672 % (exceptions['total'], len(exceptions['exceptioncounts']))
2672 2673 )
2673 2674 combined = exceptions['combined']
2674 2675 for key in sorted(combined, key=combined.get, reverse=True):
2675 2676 frame, line, exc = key
2676 2677 totalcount, testcount, leastcount, leasttest = combined[key]
2677 2678
2678 2679 self.stream.writeln(
2679 2680 '%d (%d tests)\t%s: %s (%s - %d total)'
2680 2681 % (
2681 2682 totalcount,
2682 2683 testcount,
2683 2684 frame,
2684 2685 exc,
2685 2686 leasttest,
2686 2687 leastcount,
2687 2688 )
2688 2689 )
2689 2690
2690 2691 self.stream.flush()
2691 2692
2692 2693 return self._result
2693 2694
2694 2695 def _bisecttests(self, tests):
2695 2696 bisectcmd = ['hg', 'bisect']
2696 2697 bisectrepo = self._runner.options.bisect_repo
2697 2698 if bisectrepo:
2698 2699 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2699 2700
2700 2701 def pread(args):
2701 2702 env = os.environ.copy()
2702 2703 env['HGPLAIN'] = '1'
2703 2704 p = subprocess.Popen(
2704 2705 args, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, env=env
2705 2706 )
2706 2707 data = p.stdout.read()
2707 2708 p.wait()
2708 2709 return data
2709 2710
2710 2711 for test in tests:
2711 2712 pread(bisectcmd + ['--reset']),
2712 2713 pread(bisectcmd + ['--bad', '.'])
2713 2714 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2714 2715 # TODO: we probably need to forward more options
2715 2716 # that alter hg's behavior inside the tests.
2716 2717 opts = ''
2717 2718 withhg = self._runner.options.with_hg
2718 2719 if withhg:
2719 2720 opts += ' --with-hg=%s ' % shellquote(_bytes2sys(withhg))
2720 2721 rtc = '%s %s %s %s' % (sysexecutable, sys.argv[0], opts, test)
2721 2722 data = pread(bisectcmd + ['--command', rtc])
2722 2723 m = re.search(
2723 2724 (
2724 2725 br'\nThe first (?P<goodbad>bad|good) revision '
2725 2726 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2726 2727 br'summary: +(?P<summary>[^\n]+)\n'
2727 2728 ),
2728 2729 data,
2729 2730 (re.MULTILINE | re.DOTALL),
2730 2731 )
2731 2732 if m is None:
2732 2733 self.stream.writeln(
2733 2734 'Failed to identify failure point for %s' % test
2734 2735 )
2735 2736 continue
2736 2737 dat = m.groupdict()
2737 2738 verb = 'broken' if dat['goodbad'] == b'bad' else 'fixed'
2738 2739 self.stream.writeln(
2739 2740 '%s %s by %s (%s)'
2740 2741 % (
2741 2742 test,
2742 2743 verb,
2743 2744 dat['node'].decode('ascii'),
2744 2745 dat['summary'].decode('utf8', 'ignore'),
2745 2746 )
2746 2747 )
2747 2748
2748 2749 def printtimes(self, times):
2749 2750 # iolock held by run
2750 2751 self.stream.writeln('# Producing time report')
2751 2752 times.sort(key=lambda t: (t[3]))
2752 2753 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2753 2754 self.stream.writeln(
2754 2755 '%-7s %-7s %-7s %-7s %-7s %s'
2755 2756 % ('start', 'end', 'cuser', 'csys', 'real', 'Test')
2756 2757 )
2757 2758 for tdata in times:
2758 2759 test = tdata[0]
2759 2760 cuser, csys, real, start, end = tdata[1:6]
2760 2761 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2761 2762
2762 2763 @staticmethod
2763 2764 def _writexunit(result, outf):
2764 2765 # See http://llg.cubic.org/docs/junit/ for a reference.
2765 2766 timesd = {t[0]: t[3] for t in result.times}
2766 2767 doc = minidom.Document()
2767 2768 s = doc.createElement('testsuite')
2768 2769 s.setAttribute('errors', "0") # TODO
2769 2770 s.setAttribute('failures', str(len(result.failures)))
2770 2771 s.setAttribute('name', 'run-tests')
2771 2772 s.setAttribute(
2772 2773 'skipped', str(len(result.skipped) + len(result.ignored))
2773 2774 )
2774 2775 s.setAttribute('tests', str(result.testsRun))
2775 2776 doc.appendChild(s)
2776 2777 for tc in result.successes:
2777 2778 t = doc.createElement('testcase')
2778 2779 t.setAttribute('name', tc.name)
2779 2780 tctime = timesd.get(tc.name)
2780 2781 if tctime is not None:
2781 2782 t.setAttribute('time', '%.3f' % tctime)
2782 2783 s.appendChild(t)
2783 2784 for tc, err in sorted(result.faildata.items()):
2784 2785 t = doc.createElement('testcase')
2785 2786 t.setAttribute('name', tc)
2786 2787 tctime = timesd.get(tc)
2787 2788 if tctime is not None:
2788 2789 t.setAttribute('time', '%.3f' % tctime)
2789 2790 # createCDATASection expects a unicode or it will
2790 2791 # convert using default conversion rules, which will
2791 2792 # fail if string isn't ASCII.
2792 2793 err = cdatasafe(err).decode('utf-8', 'replace')
2793 2794 cd = doc.createCDATASection(err)
2794 2795 # Use 'failure' here instead of 'error' to match errors = 0,
2795 2796 # failures = len(result.failures) in the testsuite element.
2796 2797 failelem = doc.createElement('failure')
2797 2798 failelem.setAttribute('message', 'output changed')
2798 2799 failelem.setAttribute('type', 'output-mismatch')
2799 2800 failelem.appendChild(cd)
2800 2801 t.appendChild(failelem)
2801 2802 s.appendChild(t)
2802 2803 for tc, message in result.skipped:
2803 2804 # According to the schema, 'skipped' has no attributes. So store
2804 2805 # the skip message as a text node instead.
2805 2806 t = doc.createElement('testcase')
2806 2807 t.setAttribute('name', tc.name)
2807 2808 binmessage = message.encode('utf-8')
2808 2809 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2809 2810 cd = doc.createCDATASection(message)
2810 2811 skipelem = doc.createElement('skipped')
2811 2812 skipelem.appendChild(cd)
2812 2813 t.appendChild(skipelem)
2813 2814 s.appendChild(t)
2814 2815 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2815 2816
2816 2817 @staticmethod
2817 2818 def _writejson(result, outf):
2818 2819 timesd = {}
2819 2820 for tdata in result.times:
2820 2821 test = tdata[0]
2821 2822 timesd[test] = tdata[1:]
2822 2823
2823 2824 outcome = {}
2824 2825 groups = [
2825 2826 ('success', ((tc, None) for tc in result.successes)),
2826 2827 ('failure', result.failures),
2827 2828 ('skip', result.skipped),
2828 2829 ]
2829 2830 for res, testcases in groups:
2830 2831 for tc, __ in testcases:
2831 2832 if tc.name in timesd:
2832 2833 diff = result.faildata.get(tc.name, b'')
2833 2834 try:
2834 2835 diff = diff.decode('unicode_escape')
2835 2836 except UnicodeDecodeError as e:
2836 2837 diff = '%r decoding diff, sorry' % e
2837 2838 tres = {
2838 2839 'result': res,
2839 2840 'time': ('%0.3f' % timesd[tc.name][2]),
2840 2841 'cuser': ('%0.3f' % timesd[tc.name][0]),
2841 2842 'csys': ('%0.3f' % timesd[tc.name][1]),
2842 2843 'start': ('%0.3f' % timesd[tc.name][3]),
2843 2844 'end': ('%0.3f' % timesd[tc.name][4]),
2844 2845 'diff': diff,
2845 2846 }
2846 2847 else:
2847 2848 # blacklisted test
2848 2849 tres = {'result': res}
2849 2850
2850 2851 outcome[tc.name] = tres
2851 2852 jsonout = json.dumps(
2852 2853 outcome, sort_keys=True, indent=4, separators=(',', ': ')
2853 2854 )
2854 2855 outf.writelines(("testreport =", jsonout))
2855 2856
2856 2857
2857 2858 def sorttests(testdescs, previoustimes, shuffle=False):
2858 2859 """Do an in-place sort of tests."""
2859 2860 if shuffle:
2860 2861 random.shuffle(testdescs)
2861 2862 return
2862 2863
2863 2864 if previoustimes:
2864 2865
2865 2866 def sortkey(f):
2866 2867 f = f['path']
2867 2868 if f in previoustimes:
2868 2869 # Use most recent time as estimate
2869 2870 return -(previoustimes[f][-1])
2870 2871 else:
2871 2872 # Default to a rather arbitrary value of 1 second for new tests
2872 2873 return -1.0
2873 2874
2874 2875 else:
2875 2876 # keywords for slow tests
2876 2877 slow = {
2877 2878 b'svn': 10,
2878 2879 b'cvs': 10,
2879 2880 b'hghave': 10,
2880 2881 b'largefiles-update': 10,
2881 2882 b'run-tests': 10,
2882 2883 b'corruption': 10,
2883 2884 b'race': 10,
2884 2885 b'i18n': 10,
2885 2886 b'check': 100,
2886 2887 b'gendoc': 100,
2887 2888 b'contrib-perf': 200,
2888 2889 b'merge-combination': 100,
2889 2890 }
2890 2891 perf = {}
2891 2892
2892 2893 def sortkey(f):
2893 2894 # run largest tests first, as they tend to take the longest
2894 2895 f = f['path']
2895 2896 try:
2896 2897 return perf[f]
2897 2898 except KeyError:
2898 2899 try:
2899 2900 val = -os.stat(f).st_size
2900 2901 except OSError as e:
2901 2902 if e.errno != errno.ENOENT:
2902 2903 raise
2903 2904 perf[f] = -1e9 # file does not exist, tell early
2904 2905 return -1e9
2905 2906 for kw, mul in slow.items():
2906 2907 if kw in f:
2907 2908 val *= mul
2908 2909 if f.endswith(b'.py'):
2909 2910 val /= 10.0
2910 2911 perf[f] = val / 1000.0
2911 2912 return perf[f]
2912 2913
2913 2914 testdescs.sort(key=sortkey)
2914 2915
2915 2916
2916 2917 class TestRunner(object):
2917 2918 """Holds context for executing tests.
2918 2919
2919 2920 Tests rely on a lot of state. This object holds it for them.
2920 2921 """
2921 2922
2922 2923 # Programs required to run tests.
2923 2924 REQUIREDTOOLS = [
2924 2925 b'diff',
2925 2926 b'grep',
2926 2927 b'unzip',
2927 2928 b'gunzip',
2928 2929 b'bunzip2',
2929 2930 b'sed',
2930 2931 ]
2931 2932
2932 2933 # Maps file extensions to test class.
2933 2934 TESTTYPES = [
2934 2935 (b'.py', PythonTest),
2935 2936 (b'.t', TTest),
2936 2937 ]
2937 2938
2938 2939 def __init__(self):
2939 2940 self.options = None
2940 2941 self._hgroot = None
2941 2942 self._testdir = None
2942 2943 self._outputdir = None
2943 2944 self._hgtmp = None
2944 2945 self._installdir = None
2945 2946 self._bindir = None
2946 2947 self._tmpbinddir = None
2947 2948 self._pythondir = None
2948 2949 self._coveragefile = None
2949 2950 self._createdfiles = []
2950 2951 self._hgcommand = None
2951 2952 self._hgpath = None
2952 2953 self._portoffset = 0
2953 2954 self._ports = {}
2954 2955
2955 2956 def run(self, args, parser=None):
2956 2957 """Run the test suite."""
2957 2958 oldmask = os.umask(0o22)
2958 2959 try:
2959 2960 parser = parser or getparser()
2960 2961 options = parseargs(args, parser)
2961 2962 tests = [_sys2bytes(a) for a in options.tests]
2962 2963 if options.test_list is not None:
2963 2964 for listfile in options.test_list:
2964 2965 with open(listfile, 'rb') as f:
2965 2966 tests.extend(t for t in f.read().splitlines() if t)
2966 2967 self.options = options
2967 2968
2968 2969 self._checktools()
2969 2970 testdescs = self.findtests(tests)
2970 2971 if options.profile_runner:
2971 2972 import statprof
2972 2973
2973 2974 statprof.start()
2974 2975 result = self._run(testdescs)
2975 2976 if options.profile_runner:
2976 2977 statprof.stop()
2977 2978 statprof.display()
2978 2979 return result
2979 2980
2980 2981 finally:
2981 2982 os.umask(oldmask)
2982 2983
2983 2984 def _run(self, testdescs):
2984 2985 testdir = getcwdb()
2985 2986 self._testdir = osenvironb[b'TESTDIR'] = getcwdb()
2986 2987 # assume all tests in same folder for now
2987 2988 if testdescs:
2988 2989 pathname = os.path.dirname(testdescs[0]['path'])
2989 2990 if pathname:
2990 2991 testdir = os.path.join(testdir, pathname)
2991 2992 self._testdir = osenvironb[b'TESTDIR'] = testdir
2992 2993 if self.options.outputdir:
2993 2994 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
2994 2995 else:
2995 2996 self._outputdir = getcwdb()
2996 2997 if testdescs and pathname:
2997 2998 self._outputdir = os.path.join(self._outputdir, pathname)
2998 2999 previoustimes = {}
2999 3000 if self.options.order_by_runtime:
3000 3001 previoustimes = dict(loadtimes(self._outputdir))
3001 3002 sorttests(testdescs, previoustimes, shuffle=self.options.random)
3002 3003
3003 3004 if 'PYTHONHASHSEED' not in os.environ:
3004 3005 # use a random python hash seed all the time
3005 3006 # we do the randomness ourself to know what seed is used
3006 3007 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
3007 3008
3008 3009 # Rayon (Rust crate for multi-threading) will use all logical CPU cores
3009 3010 # by default, causing thrashing on high-cpu-count systems.
3010 3011 # Setting its limit to 3 during tests should still let us uncover
3011 3012 # multi-threading bugs while keeping the thrashing reasonable.
3012 3013 os.environ.setdefault("RAYON_NUM_THREADS", "3")
3013 3014
3014 3015 if self.options.tmpdir:
3015 3016 self.options.keep_tmpdir = True
3016 3017 tmpdir = _sys2bytes(self.options.tmpdir)
3017 3018 if os.path.exists(tmpdir):
3018 3019 # Meaning of tmpdir has changed since 1.3: we used to create
3019 3020 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
3020 3021 # tmpdir already exists.
3021 3022 print("error: temp dir %r already exists" % tmpdir)
3022 3023 return 1
3023 3024
3024 3025 os.makedirs(tmpdir)
3025 3026 else:
3026 3027 d = None
3027 3028 if os.name == 'nt':
3028 3029 # without this, we get the default temp dir location, but
3029 3030 # in all lowercase, which causes troubles with paths (issue3490)
3030 3031 d = osenvironb.get(b'TMP', None)
3031 3032 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
3032 3033
3033 3034 self._hgtmp = osenvironb[b'HGTMP'] = os.path.realpath(tmpdir)
3034 3035
3035 3036 if self.options.with_hg:
3036 3037 self._installdir = None
3037 3038 whg = self.options.with_hg
3038 3039 self._bindir = os.path.dirname(os.path.realpath(whg))
3039 3040 assert isinstance(self._bindir, bytes)
3040 3041 self._hgcommand = os.path.basename(whg)
3041 3042 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
3042 3043 os.makedirs(self._tmpbindir)
3043 3044
3044 3045 normbin = os.path.normpath(os.path.abspath(whg))
3045 3046 normbin = normbin.replace(_sys2bytes(os.sep), b'/')
3046 3047
3047 3048 # Other Python scripts in the test harness need to
3048 3049 # `import mercurial`. If `hg` is a Python script, we assume
3049 3050 # the Mercurial modules are relative to its path and tell the tests
3050 3051 # to load Python modules from its directory.
3051 3052 with open(whg, 'rb') as fh:
3052 3053 initial = fh.read(1024)
3053 3054
3054 3055 if re.match(b'#!.*python', initial):
3055 3056 self._pythondir = self._bindir
3056 3057 # If it looks like our in-repo Rust binary, use the source root.
3057 3058 # This is a bit hacky. But rhg is still not supported outside the
3058 3059 # source directory. So until it is, do the simple thing.
3059 3060 elif re.search(b'/rust/target/[^/]+/hg', normbin):
3060 3061 self._pythondir = os.path.dirname(self._testdir)
3061 3062 # Fall back to the legacy behavior.
3062 3063 else:
3063 3064 self._pythondir = self._bindir
3064 3065
3065 3066 else:
3066 3067 self._installdir = os.path.join(self._hgtmp, b"install")
3067 3068 self._bindir = os.path.join(self._installdir, b"bin")
3068 3069 self._hgcommand = b'hg'
3069 3070 self._tmpbindir = self._bindir
3070 3071 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
3071 3072
3072 3073 # Force the use of hg.exe instead of relying on MSYS to recognize hg is
3073 3074 # a python script and feed it to python.exe. Legacy stdio is force
3074 3075 # enabled by hg.exe, and this is a more realistic way to launch hg
3075 3076 # anyway.
3076 3077 if os.name == 'nt' and not self._hgcommand.endswith(b'.exe'):
3077 3078 self._hgcommand += b'.exe'
3078 3079
3079 3080 # set CHGHG, then replace "hg" command by "chg"
3080 3081 chgbindir = self._bindir
3081 3082 if self.options.chg or self.options.with_chg:
3082 3083 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
3083 3084 else:
3084 3085 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
3085 3086 if self.options.chg:
3086 3087 self._hgcommand = b'chg'
3087 3088 elif self.options.with_chg:
3088 3089 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
3089 3090 self._hgcommand = os.path.basename(self.options.with_chg)
3090 3091
3091 3092 osenvironb[b"BINDIR"] = self._bindir
3092 3093 osenvironb[b"PYTHON"] = PYTHON
3093 3094
3094 3095 fileb = _sys2bytes(__file__)
3095 3096 runtestdir = os.path.abspath(os.path.dirname(fileb))
3096 3097 osenvironb[b'RUNTESTDIR'] = runtestdir
3097 3098 if PYTHON3:
3098 3099 sepb = _sys2bytes(os.pathsep)
3099 3100 else:
3100 3101 sepb = os.pathsep
3101 3102 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
3102 3103 if os.path.islink(__file__):
3103 3104 # test helper will likely be at the end of the symlink
3104 3105 realfile = os.path.realpath(fileb)
3105 3106 realdir = os.path.abspath(os.path.dirname(realfile))
3106 3107 path.insert(2, realdir)
3107 3108 if chgbindir != self._bindir:
3108 3109 path.insert(1, chgbindir)
3109 3110 if self._testdir != runtestdir:
3110 3111 path = [self._testdir] + path
3111 3112 if self._tmpbindir != self._bindir:
3112 3113 path = [self._tmpbindir] + path
3113 3114 osenvironb[b"PATH"] = sepb.join(path)
3114 3115
3115 3116 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
3116 3117 # can run .../tests/run-tests.py test-foo where test-foo
3117 3118 # adds an extension to HGRC. Also include run-test.py directory to
3118 3119 # import modules like heredoctest.
3119 3120 pypath = [self._pythondir, self._testdir, runtestdir]
3120 3121 # We have to augment PYTHONPATH, rather than simply replacing
3121 3122 # it, in case external libraries are only available via current
3122 3123 # PYTHONPATH. (In particular, the Subversion bindings on OS X
3123 3124 # are in /opt/subversion.)
3124 3125 oldpypath = osenvironb.get(IMPL_PATH)
3125 3126 if oldpypath:
3126 3127 pypath.append(oldpypath)
3127 3128 osenvironb[IMPL_PATH] = sepb.join(pypath)
3128 3129
3129 3130 if self.options.pure:
3130 3131 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
3131 3132 os.environ["HGMODULEPOLICY"] = "py"
3132 3133 if self.options.rust:
3133 3134 os.environ["HGMODULEPOLICY"] = "rust+c"
3134 3135 if self.options.no_rust:
3135 3136 current_policy = os.environ.get("HGMODULEPOLICY", "")
3136 3137 if current_policy.startswith("rust+"):
3137 3138 os.environ["HGMODULEPOLICY"] = current_policy[len("rust+") :]
3138 3139 os.environ.pop("HGWITHRUSTEXT", None)
3139 3140
3140 3141 if self.options.allow_slow_tests:
3141 3142 os.environ["HGTEST_SLOW"] = "slow"
3142 3143 elif 'HGTEST_SLOW' in os.environ:
3143 3144 del os.environ['HGTEST_SLOW']
3144 3145
3145 3146 self._coveragefile = os.path.join(self._testdir, b'.coverage')
3146 3147
3147 3148 if self.options.exceptions:
3148 3149 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
3149 3150 try:
3150 3151 os.makedirs(exceptionsdir)
3151 3152 except OSError as e:
3152 3153 if e.errno != errno.EEXIST:
3153 3154 raise
3154 3155
3155 3156 # Remove all existing exception reports.
3156 3157 for f in os.listdir(exceptionsdir):
3157 3158 os.unlink(os.path.join(exceptionsdir, f))
3158 3159
3159 3160 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
3160 3161 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
3161 3162 self.options.extra_config_opt.append(
3162 3163 'extensions.logexceptions=%s' % logexceptions.decode('utf-8')
3163 3164 )
3164 3165
3165 3166 vlog("# Using TESTDIR", _bytes2sys(self._testdir))
3166 3167 vlog("# Using RUNTESTDIR", _bytes2sys(osenvironb[b'RUNTESTDIR']))
3167 3168 vlog("# Using HGTMP", _bytes2sys(self._hgtmp))
3168 3169 vlog("# Using PATH", os.environ["PATH"])
3169 3170 vlog(
3170 3171 "# Using", _bytes2sys(IMPL_PATH), _bytes2sys(osenvironb[IMPL_PATH]),
3171 3172 )
3172 3173 vlog("# Writing to directory", _bytes2sys(self._outputdir))
3173 3174
3174 3175 try:
3175 3176 return self._runtests(testdescs) or 0
3176 3177 finally:
3177 3178 time.sleep(0.1)
3178 3179 self._cleanup()
3179 3180
3180 3181 def findtests(self, args):
3181 3182 """Finds possible test files from arguments.
3182 3183
3183 3184 If you wish to inject custom tests into the test harness, this would
3184 3185 be a good function to monkeypatch or override in a derived class.
3185 3186 """
3186 3187 if not args:
3187 3188 if self.options.changed:
3188 3189 proc = Popen4(
3189 3190 b'hg st --rev "%s" -man0 .'
3190 3191 % _sys2bytes(self.options.changed),
3191 3192 None,
3192 3193 0,
3193 3194 )
3194 3195 stdout, stderr = proc.communicate()
3195 3196 args = stdout.strip(b'\0').split(b'\0')
3196 3197 else:
3197 3198 args = os.listdir(b'.')
3198 3199
3199 3200 expanded_args = []
3200 3201 for arg in args:
3201 3202 if os.path.isdir(arg):
3202 3203 if not arg.endswith(b'/'):
3203 3204 arg += b'/'
3204 3205 expanded_args.extend([arg + a for a in os.listdir(arg)])
3205 3206 else:
3206 3207 expanded_args.append(arg)
3207 3208 args = expanded_args
3208 3209
3209 3210 testcasepattern = re.compile(br'([\w-]+\.t|py)(?:#([a-zA-Z0-9_\-.#]+))')
3210 3211 tests = []
3211 3212 for t in args:
3212 3213 case = []
3213 3214
3214 3215 if not (
3215 3216 os.path.basename(t).startswith(b'test-')
3216 3217 and (t.endswith(b'.py') or t.endswith(b'.t'))
3217 3218 ):
3218 3219
3219 3220 m = testcasepattern.match(os.path.basename(t))
3220 3221 if m is not None:
3221 3222 t_basename, casestr = m.groups()
3222 3223 t = os.path.join(os.path.dirname(t), t_basename)
3223 3224 if casestr:
3224 3225 case = casestr.split(b'#')
3225 3226 else:
3226 3227 continue
3227 3228
3228 3229 if t.endswith(b'.t'):
3229 3230 # .t file may contain multiple test cases
3230 3231 casedimensions = parsettestcases(t)
3231 3232 if casedimensions:
3232 3233 cases = []
3233 3234
3234 3235 def addcases(case, casedimensions):
3235 3236 if not casedimensions:
3236 3237 cases.append(case)
3237 3238 else:
3238 3239 for c in casedimensions[0]:
3239 3240 addcases(case + [c], casedimensions[1:])
3240 3241
3241 3242 addcases([], casedimensions)
3242 3243 if case and case in cases:
3243 3244 cases = [case]
3244 3245 elif case:
3245 3246 # Ignore invalid cases
3246 3247 cases = []
3247 3248 else:
3248 3249 pass
3249 3250 tests += [{'path': t, 'case': c} for c in sorted(cases)]
3250 3251 else:
3251 3252 tests.append({'path': t})
3252 3253 else:
3253 3254 tests.append({'path': t})
3254 3255
3255 3256 if self.options.retest:
3256 3257 retest_args = []
3257 3258 for test in tests:
3258 3259 errpath = self._geterrpath(test)
3259 3260 if os.path.exists(errpath):
3260 3261 retest_args.append(test)
3261 3262 tests = retest_args
3262 3263 return tests
3263 3264
3264 3265 def _runtests(self, testdescs):
3265 3266 def _reloadtest(test, i):
3266 3267 # convert a test back to its description dict
3267 3268 desc = {'path': test.path}
3268 3269 case = getattr(test, '_case', [])
3269 3270 if case:
3270 3271 desc['case'] = case
3271 3272 return self._gettest(desc, i)
3272 3273
3273 3274 try:
3274 3275 if self.options.restart:
3275 3276 orig = list(testdescs)
3276 3277 while testdescs:
3277 3278 desc = testdescs[0]
3278 3279 errpath = self._geterrpath(desc)
3279 3280 if os.path.exists(errpath):
3280 3281 break
3281 3282 testdescs.pop(0)
3282 3283 if not testdescs:
3283 3284 print("running all tests")
3284 3285 testdescs = orig
3285 3286
3286 3287 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
3287 3288 num_tests = len(tests) * self.options.runs_per_test
3288 3289
3289 3290 jobs = min(num_tests, self.options.jobs)
3290 3291
3291 3292 failed = False
3292 3293 kws = self.options.keywords
3293 3294 if kws is not None and PYTHON3:
3294 3295 kws = kws.encode('utf-8')
3295 3296
3296 3297 suite = TestSuite(
3297 3298 self._testdir,
3298 3299 jobs=jobs,
3299 3300 whitelist=self.options.whitelisted,
3300 3301 blacklist=self.options.blacklist,
3301 3302 keywords=kws,
3302 3303 loop=self.options.loop,
3303 3304 runs_per_test=self.options.runs_per_test,
3304 3305 showchannels=self.options.showchannels,
3305 3306 tests=tests,
3306 3307 loadtest=_reloadtest,
3307 3308 )
3308 3309 verbosity = 1
3309 3310 if self.options.list_tests:
3310 3311 verbosity = 0
3311 3312 elif self.options.verbose:
3312 3313 verbosity = 2
3313 3314 runner = TextTestRunner(self, verbosity=verbosity)
3314 3315
3315 3316 if self.options.list_tests:
3316 3317 result = runner.listtests(suite)
3317 3318 else:
3318 3319 if self._installdir:
3319 3320 self._installhg()
3320 3321 self._checkhglib("Testing")
3321 3322 else:
3322 3323 self._usecorrectpython()
3323 3324 if self.options.chg:
3324 3325 assert self._installdir
3325 3326 self._installchg()
3326 3327
3327 3328 log(
3328 3329 'running %d tests using %d parallel processes'
3329 3330 % (num_tests, jobs)
3330 3331 )
3331 3332
3332 3333 result = runner.run(suite)
3333 3334
3334 3335 if result.failures or result.errors:
3335 3336 failed = True
3336 3337
3337 3338 result.onEnd()
3338 3339
3339 3340 if self.options.anycoverage:
3340 3341 self._outputcoverage()
3341 3342 except KeyboardInterrupt:
3342 3343 failed = True
3343 3344 print("\ninterrupted!")
3344 3345
3345 3346 if failed:
3346 3347 return 1
3347 3348
3348 3349 def _geterrpath(self, test):
3349 3350 # test['path'] is a relative path
3350 3351 if 'case' in test:
3351 3352 # for multiple dimensions test cases
3352 3353 casestr = b'#'.join(test['case'])
3353 3354 errpath = b'%s#%s.err' % (test['path'], casestr)
3354 3355 else:
3355 3356 errpath = b'%s.err' % test['path']
3356 3357 if self.options.outputdir:
3357 3358 self._outputdir = canonpath(_sys2bytes(self.options.outputdir))
3358 3359 errpath = os.path.join(self._outputdir, errpath)
3359 3360 return errpath
3360 3361
3361 3362 def _getport(self, count):
3362 3363 port = self._ports.get(count) # do we have a cached entry?
3363 3364 if port is None:
3364 3365 portneeded = 3
3365 3366 # above 100 tries we just give up and let test reports failure
3366 3367 for tries in xrange(100):
3367 3368 allfree = True
3368 3369 port = self.options.port + self._portoffset
3369 3370 for idx in xrange(portneeded):
3370 3371 if not checkportisavailable(port + idx):
3371 3372 allfree = False
3372 3373 break
3373 3374 self._portoffset += portneeded
3374 3375 if allfree:
3375 3376 break
3376 3377 self._ports[count] = port
3377 3378 return port
3378 3379
3379 3380 def _gettest(self, testdesc, count):
3380 3381 """Obtain a Test by looking at its filename.
3381 3382
3382 3383 Returns a Test instance. The Test may not be runnable if it doesn't
3383 3384 map to a known type.
3384 3385 """
3385 3386 path = testdesc['path']
3386 3387 lctest = path.lower()
3387 3388 testcls = Test
3388 3389
3389 3390 for ext, cls in self.TESTTYPES:
3390 3391 if lctest.endswith(ext):
3391 3392 testcls = cls
3392 3393 break
3393 3394
3394 3395 refpath = os.path.join(getcwdb(), path)
3395 3396 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
3396 3397
3397 3398 # extra keyword parameters. 'case' is used by .t tests
3398 3399 kwds = {k: testdesc[k] for k in ['case'] if k in testdesc}
3399 3400
3400 3401 t = testcls(
3401 3402 refpath,
3402 3403 self._outputdir,
3403 3404 tmpdir,
3404 3405 keeptmpdir=self.options.keep_tmpdir,
3405 3406 debug=self.options.debug,
3406 3407 first=self.options.first,
3407 3408 timeout=self.options.timeout,
3408 3409 startport=self._getport(count),
3409 3410 extraconfigopts=self.options.extra_config_opt,
3410 3411 shell=self.options.shell,
3411 3412 hgcommand=self._hgcommand,
3412 3413 usechg=bool(self.options.with_chg or self.options.chg),
3413 3414 chgdebug=self.options.chg_debug,
3414 3415 useipv6=useipv6,
3415 3416 **kwds
3416 3417 )
3417 3418 t.should_reload = True
3418 3419 return t
3419 3420
3420 3421 def _cleanup(self):
3421 3422 """Clean up state from this test invocation."""
3422 3423 if self.options.keep_tmpdir:
3423 3424 return
3424 3425
3425 3426 vlog("# Cleaning up HGTMP", _bytes2sys(self._hgtmp))
3426 3427 shutil.rmtree(self._hgtmp, True)
3427 3428 for f in self._createdfiles:
3428 3429 try:
3429 3430 os.remove(f)
3430 3431 except OSError:
3431 3432 pass
3432 3433
3433 3434 def _usecorrectpython(self):
3434 3435 """Configure the environment to use the appropriate Python in tests."""
3435 3436 # Tests must use the same interpreter as us or bad things will happen.
3436 3437 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
3437 3438
3438 3439 # os.symlink() is a thing with py3 on Windows, but it requires
3439 3440 # Administrator rights.
3440 3441 if getattr(os, 'symlink', None) and os.name != 'nt':
3441 3442 vlog(
3442 3443 "# Making python executable in test path a symlink to '%s'"
3443 3444 % sysexecutable
3444 3445 )
3445 3446 mypython = os.path.join(self._tmpbindir, pyexename)
3446 3447 try:
3447 3448 if os.readlink(mypython) == sysexecutable:
3448 3449 return
3449 3450 os.unlink(mypython)
3450 3451 except OSError as err:
3451 3452 if err.errno != errno.ENOENT:
3452 3453 raise
3453 3454 if self._findprogram(pyexename) != sysexecutable:
3454 3455 try:
3455 3456 os.symlink(sysexecutable, mypython)
3456 3457 self._createdfiles.append(mypython)
3457 3458 except OSError as err:
3458 3459 # child processes may race, which is harmless
3459 3460 if err.errno != errno.EEXIST:
3460 3461 raise
3461 3462 else:
3462 3463 exedir, exename = os.path.split(sysexecutable)
3463 3464 vlog(
3464 3465 "# Modifying search path to find %s as %s in '%s'"
3465 3466 % (exename, pyexename, exedir)
3466 3467 )
3467 3468 path = os.environ['PATH'].split(os.pathsep)
3468 3469 while exedir in path:
3469 3470 path.remove(exedir)
3470 3471 os.environ['PATH'] = os.pathsep.join([exedir] + path)
3471 3472 if not self._findprogram(pyexename):
3472 3473 print("WARNING: Cannot find %s in search path" % pyexename)
3473 3474
3474 3475 def _installhg(self):
3475 3476 """Install hg into the test environment.
3476 3477
3477 3478 This will also configure hg with the appropriate testing settings.
3478 3479 """
3479 3480 vlog("# Performing temporary installation of HG")
3480 3481 installerrs = os.path.join(self._hgtmp, b"install.err")
3481 3482 compiler = ''
3482 3483 if self.options.compiler:
3483 3484 compiler = '--compiler ' + self.options.compiler
3484 3485 setup_opts = b""
3485 3486 if self.options.pure:
3486 3487 setup_opts = b"--pure"
3487 3488 elif self.options.rust:
3488 3489 setup_opts = b"--rust"
3489 3490 elif self.options.no_rust:
3490 3491 setup_opts = b"--no-rust"
3491 3492
3492 3493 # Run installer in hg root
3493 3494 script = os.path.realpath(sys.argv[0])
3494 3495 exe = sysexecutable
3495 3496 if PYTHON3:
3496 3497 compiler = _sys2bytes(compiler)
3497 3498 script = _sys2bytes(script)
3498 3499 exe = _sys2bytes(exe)
3499 3500 hgroot = os.path.dirname(os.path.dirname(script))
3500 3501 self._hgroot = hgroot
3501 3502 os.chdir(hgroot)
3502 3503 nohome = b'--home=""'
3503 3504 if os.name == 'nt':
3504 3505 # The --home="" trick works only on OS where os.sep == '/'
3505 3506 # because of a distutils convert_path() fast-path. Avoid it at
3506 3507 # least on Windows for now, deal with .pydistutils.cfg bugs
3507 3508 # when they happen.
3508 3509 nohome = b''
3509 3510 cmd = (
3510 3511 b'"%(exe)s" setup.py %(setup_opts)s clean --all'
3511 3512 b' build %(compiler)s --build-base="%(base)s"'
3512 3513 b' install --force --prefix="%(prefix)s"'
3513 3514 b' --install-lib="%(libdir)s"'
3514 3515 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
3515 3516 % {
3516 3517 b'exe': exe,
3517 3518 b'setup_opts': setup_opts,
3518 3519 b'compiler': compiler,
3519 3520 b'base': os.path.join(self._hgtmp, b"build"),
3520 3521 b'prefix': self._installdir,
3521 3522 b'libdir': self._pythondir,
3522 3523 b'bindir': self._bindir,
3523 3524 b'nohome': nohome,
3524 3525 b'logfile': installerrs,
3525 3526 }
3526 3527 )
3527 3528
3528 3529 # setuptools requires install directories to exist.
3529 3530 def makedirs(p):
3530 3531 try:
3531 3532 os.makedirs(p)
3532 3533 except OSError as e:
3533 3534 if e.errno != errno.EEXIST:
3534 3535 raise
3535 3536
3536 3537 makedirs(self._pythondir)
3537 3538 makedirs(self._bindir)
3538 3539
3539 3540 vlog("# Running", cmd.decode("utf-8"))
3540 3541 if subprocess.call(_bytes2sys(cmd), shell=True) == 0:
3541 3542 if not self.options.verbose:
3542 3543 try:
3543 3544 os.remove(installerrs)
3544 3545 except OSError as e:
3545 3546 if e.errno != errno.ENOENT:
3546 3547 raise
3547 3548 else:
3548 3549 with open(installerrs, 'rb') as f:
3549 3550 for line in f:
3550 3551 if PYTHON3:
3551 3552 sys.stdout.buffer.write(line)
3552 3553 else:
3553 3554 sys.stdout.write(line)
3554 3555 sys.exit(1)
3555 3556 os.chdir(self._testdir)
3556 3557
3557 3558 self._usecorrectpython()
3558 3559
3559 3560 hgbat = os.path.join(self._bindir, b'hg.bat')
3560 3561 if os.path.isfile(hgbat):
3561 3562 # hg.bat expects to be put in bin/scripts while run-tests.py
3562 3563 # installation layout put it in bin/ directly. Fix it
3563 3564 with open(hgbat, 'rb') as f:
3564 3565 data = f.read()
3565 3566 if br'"%~dp0..\python" "%~dp0hg" %*' in data:
3566 3567 data = data.replace(
3567 3568 br'"%~dp0..\python" "%~dp0hg" %*',
3568 3569 b'"%~dp0python" "%~dp0hg" %*',
3569 3570 )
3570 3571 with open(hgbat, 'wb') as f:
3571 3572 f.write(data)
3572 3573 else:
3573 3574 print('WARNING: cannot fix hg.bat reference to python.exe')
3574 3575
3575 3576 if self.options.anycoverage:
3576 3577 custom = os.path.join(
3577 3578 osenvironb[b'RUNTESTDIR'], b'sitecustomize.py'
3578 3579 )
3579 3580 target = os.path.join(self._pythondir, b'sitecustomize.py')
3580 3581 vlog('# Installing coverage trigger to %s' % target)
3581 3582 shutil.copyfile(custom, target)
3582 3583 rc = os.path.join(self._testdir, b'.coveragerc')
3583 3584 vlog('# Installing coverage rc to %s' % rc)
3584 3585 osenvironb[b'COVERAGE_PROCESS_START'] = rc
3585 3586 covdir = os.path.join(self._installdir, b'..', b'coverage')
3586 3587 try:
3587 3588 os.mkdir(covdir)
3588 3589 except OSError as e:
3589 3590 if e.errno != errno.EEXIST:
3590 3591 raise
3591 3592
3592 3593 osenvironb[b'COVERAGE_DIR'] = covdir
3593 3594
3594 3595 def _checkhglib(self, verb):
3595 3596 """Ensure that the 'mercurial' package imported by python is
3596 3597 the one we expect it to be. If not, print a warning to stderr."""
3597 3598 if (self._bindir == self._pythondir) and (
3598 3599 self._bindir != self._tmpbindir
3599 3600 ):
3600 3601 # The pythondir has been inferred from --with-hg flag.
3601 3602 # We cannot expect anything sensible here.
3602 3603 return
3603 3604 expecthg = os.path.join(self._pythondir, b'mercurial')
3604 3605 actualhg = self._gethgpath()
3605 3606 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
3606 3607 sys.stderr.write(
3607 3608 'warning: %s with unexpected mercurial lib: %s\n'
3608 3609 ' (expected %s)\n' % (verb, actualhg, expecthg)
3609 3610 )
3610 3611
3611 3612 def _gethgpath(self):
3612 3613 """Return the path to the mercurial package that is actually found by
3613 3614 the current Python interpreter."""
3614 3615 if self._hgpath is not None:
3615 3616 return self._hgpath
3616 3617
3617 3618 cmd = b'"%s" -c "import mercurial; print (mercurial.__path__[0])"'
3618 3619 cmd = cmd % PYTHON
3619 3620 if PYTHON3:
3620 3621 cmd = _bytes2sys(cmd)
3621 3622
3622 3623 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
3623 3624 out, err = p.communicate()
3624 3625
3625 3626 self._hgpath = out.strip()
3626 3627
3627 3628 return self._hgpath
3628 3629
3629 3630 def _installchg(self):
3630 3631 """Install chg into the test environment"""
3631 3632 vlog('# Performing temporary installation of CHG')
3632 3633 assert os.path.dirname(self._bindir) == self._installdir
3633 3634 assert self._hgroot, 'must be called after _installhg()'
3634 3635 cmd = b'"%(make)s" clean install PREFIX="%(prefix)s"' % {
3635 3636 b'make': b'make', # TODO: switch by option or environment?
3636 3637 b'prefix': self._installdir,
3637 3638 }
3638 3639 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
3639 3640 vlog("# Running", cmd)
3640 3641 proc = subprocess.Popen(
3641 3642 cmd,
3642 3643 shell=True,
3643 3644 cwd=cwd,
3644 3645 stdin=subprocess.PIPE,
3645 3646 stdout=subprocess.PIPE,
3646 3647 stderr=subprocess.STDOUT,
3647 3648 )
3648 3649 out, _err = proc.communicate()
3649 3650 if proc.returncode != 0:
3650 3651 if PYTHON3:
3651 3652 sys.stdout.buffer.write(out)
3652 3653 else:
3653 3654 sys.stdout.write(out)
3654 3655 sys.exit(1)
3655 3656
3656 3657 def _outputcoverage(self):
3657 3658 """Produce code coverage output."""
3658 3659 import coverage
3659 3660
3660 3661 coverage = coverage.coverage
3661 3662
3662 3663 vlog('# Producing coverage report')
3663 3664 # chdir is the easiest way to get short, relative paths in the
3664 3665 # output.
3665 3666 os.chdir(self._hgroot)
3666 3667 covdir = os.path.join(_bytes2sys(self._installdir), '..', 'coverage')
3667 3668 cov = coverage(data_file=os.path.join(covdir, 'cov'))
3668 3669
3669 3670 # Map install directory paths back to source directory.
3670 3671 cov.config.paths['srcdir'] = ['.', _bytes2sys(self._pythondir)]
3671 3672
3672 3673 cov.combine()
3673 3674
3674 3675 omit = [
3675 3676 _bytes2sys(os.path.join(x, b'*'))
3676 3677 for x in [self._bindir, self._testdir]
3677 3678 ]
3678 3679 cov.report(ignore_errors=True, omit=omit)
3679 3680
3680 3681 if self.options.htmlcov:
3681 3682 htmldir = os.path.join(_bytes2sys(self._outputdir), 'htmlcov')
3682 3683 cov.html_report(directory=htmldir, omit=omit)
3683 3684 if self.options.annotate:
3684 3685 adir = os.path.join(_bytes2sys(self._outputdir), 'annotated')
3685 3686 if not os.path.isdir(adir):
3686 3687 os.mkdir(adir)
3687 3688 cov.annotate(directory=adir, omit=omit)
3688 3689
3689 3690 def _findprogram(self, program):
3690 3691 """Search PATH for a executable program"""
3691 3692 dpb = _sys2bytes(os.defpath)
3692 3693 sepb = _sys2bytes(os.pathsep)
3693 3694 for p in osenvironb.get(b'PATH', dpb).split(sepb):
3694 3695 name = os.path.join(p, program)
3695 3696 if os.name == 'nt' or os.access(name, os.X_OK):
3696 3697 return _bytes2sys(name)
3697 3698 return None
3698 3699
3699 3700 def _checktools(self):
3700 3701 """Ensure tools required to run tests are present."""
3701 3702 for p in self.REQUIREDTOOLS:
3702 3703 if os.name == 'nt' and not p.endswith(b'.exe'):
3703 3704 p += b'.exe'
3704 3705 found = self._findprogram(p)
3705 3706 p = p.decode("utf-8")
3706 3707 if found:
3707 3708 vlog("# Found prerequisite", p, "at", found)
3708 3709 else:
3709 3710 print("WARNING: Did not find prerequisite tool: %s " % p)
3710 3711
3711 3712
3712 3713 def aggregateexceptions(path):
3713 3714 exceptioncounts = collections.Counter()
3714 3715 testsbyfailure = collections.defaultdict(set)
3715 3716 failuresbytest = collections.defaultdict(set)
3716 3717
3717 3718 for f in os.listdir(path):
3718 3719 with open(os.path.join(path, f), 'rb') as fh:
3719 3720 data = fh.read().split(b'\0')
3720 3721 if len(data) != 5:
3721 3722 continue
3722 3723
3723 3724 exc, mainframe, hgframe, hgline, testname = data
3724 3725 exc = exc.decode('utf-8')
3725 3726 mainframe = mainframe.decode('utf-8')
3726 3727 hgframe = hgframe.decode('utf-8')
3727 3728 hgline = hgline.decode('utf-8')
3728 3729 testname = testname.decode('utf-8')
3729 3730
3730 3731 key = (hgframe, hgline, exc)
3731 3732 exceptioncounts[key] += 1
3732 3733 testsbyfailure[key].add(testname)
3733 3734 failuresbytest[testname].add(key)
3734 3735
3735 3736 # Find test having fewest failures for each failure.
3736 3737 leastfailing = {}
3737 3738 for key, tests in testsbyfailure.items():
3738 3739 fewesttest = None
3739 3740 fewestcount = 99999999
3740 3741 for test in sorted(tests):
3741 3742 if len(failuresbytest[test]) < fewestcount:
3742 3743 fewesttest = test
3743 3744 fewestcount = len(failuresbytest[test])
3744 3745
3745 3746 leastfailing[key] = (fewestcount, fewesttest)
3746 3747
3747 3748 # Create a combined counter so we can sort by total occurrences and
3748 3749 # impacted tests.
3749 3750 combined = {}
3750 3751 for key in exceptioncounts:
3751 3752 combined[key] = (
3752 3753 exceptioncounts[key],
3753 3754 len(testsbyfailure[key]),
3754 3755 leastfailing[key][0],
3755 3756 leastfailing[key][1],
3756 3757 )
3757 3758
3758 3759 return {
3759 3760 'exceptioncounts': exceptioncounts,
3760 3761 'total': sum(exceptioncounts.values()),
3761 3762 'combined': combined,
3762 3763 'leastfailing': leastfailing,
3763 3764 'byfailure': testsbyfailure,
3764 3765 'bytest': failuresbytest,
3765 3766 }
3766 3767
3767 3768
3768 3769 if __name__ == '__main__':
3769 3770 runner = TestRunner()
3770 3771
3771 3772 try:
3772 3773 import msvcrt
3773 3774
3774 3775 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3775 3776 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3776 3777 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3777 3778 except ImportError:
3778 3779 pass
3779 3780
3780 3781 sys.exit(runner.run(sys.argv[1:]))
@@ -1,30 +1,30 b''
1 1 $ cat >> $HGRCPATH << EOF
2 2 > [extensions]
3 3 > absorb=
4 4 > EOF
5 5
6 6 Abort absorb if there is an unfinished operation.
7 7
8 8 $ hg init abortunresolved
9 9 $ cd abortunresolved
10 10
11 11 $ echo "foo1" > foo.whole
12 12 $ hg commit -Aqm "foo 1"
13 13
14 14 $ hg update null
15 15 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
16 16 $ echo "foo2" > foo.whole
17 17 $ hg commit -Aqm "foo 2"
18 18
19 19 $ hg --config extensions.rebase= rebase -r 1 -d 0
20 20 rebasing 1:c3b6dc0e177a tip "foo 2"
21 21 merging foo.whole
22 22 warning: conflicts while merging foo.whole! (edit, then use 'hg resolve --mark')
23 23 unresolved conflicts (see 'hg resolve', then 'hg rebase --continue')
24 [1]
24 [240]
25 25
26 26 $ hg --config extensions.rebase= absorb
27 27 abort: rebase in progress
28 28 (use 'hg rebase --continue', 'hg rebase --abort', or 'hg rebase --stop')
29 29 [255]
30 30
@@ -1,227 +1,228 b''
1 1 Create a repository:
2 2
3 3 #if no-extraextensions
4 4 $ hg config
5 5 devel.all-warnings=true
6 6 devel.default-date=0 0
7 7 extensions.fsmonitor= (fsmonitor !)
8 8 largefiles.usercache=$TESTTMP/.cache/largefiles
9 9 lfs.usercache=$TESTTMP/.cache/lfs
10 10 ui.slash=True
11 11 ui.interactive=False
12 ui.detailed-exit-code=True
12 13 ui.merge=internal:merge
13 14 ui.mergemarkers=detailed
14 15 ui.promptecho=True
15 16 web.address=localhost
16 17 web\.ipv6=(?:True|False) (re)
17 18 web.server-header=testing stub value
18 19 #endif
19 20
20 21 $ hg init t
21 22 $ cd t
22 23
23 24 Prepare a changeset:
24 25
25 26 $ echo a > a
26 27 $ hg add a
27 28
28 29 $ hg status
29 30 A a
30 31
31 32 Writes to stdio succeed and fail appropriately
32 33
33 34 #if devfull
34 35 $ hg status 2>/dev/full
35 36 A a
36 37
37 38 $ hg status >/dev/full
38 39 abort: No space left on device
39 40 [255]
40 41 #endif
41 42
42 43 #if devfull
43 44 $ hg status >/dev/full 2>&1
44 45 [255]
45 46
46 47 $ hg status ENOENT 2>/dev/full
47 48 [255]
48 49 #endif
49 50
50 51 $ hg commit -m test
51 52
52 53 This command is ancient:
53 54
54 55 $ hg history
55 56 changeset: 0:acb14030fe0a
56 57 tag: tip
57 58 user: test
58 59 date: Thu Jan 01 00:00:00 1970 +0000
59 60 summary: test
60 61
61 62
62 63 Verify that updating to revision 0 via commands.update() works properly
63 64
64 65 $ cat <<EOF > update_to_rev0.py
65 66 > from mercurial import commands, hg, ui as uimod
66 67 > myui = uimod.ui.load()
67 68 > repo = hg.repository(myui, path=b'.')
68 69 > commands.update(myui, repo, rev=b"0")
69 70 > EOF
70 71 $ hg up null
71 72 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
72 73 $ "$PYTHON" ./update_to_rev0.py
73 74 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
74 75 $ hg identify -n
75 76 0
76 77
77 78
78 79 Poke around at hashes:
79 80
80 81 $ hg manifest --debug
81 82 b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3 644 a
82 83
83 84 $ hg cat a
84 85 a
85 86
86 87 Verify should succeed:
87 88
88 89 $ hg verify
89 90 checking changesets
90 91 checking manifests
91 92 crosschecking files in changesets and manifests
92 93 checking files
93 94 checked 1 changesets with 1 changes to 1 files
94 95
95 96 Repository root:
96 97
97 98 $ hg root
98 99 $TESTTMP/t
99 100 $ hg log -l1 -T '{reporoot}\n'
100 101 $TESTTMP/t
101 102 $ hg root -Tjson | sed 's|\\\\|\\|g'
102 103 [
103 104 {
104 105 "hgpath": "$TESTTMP/t/.hg",
105 106 "reporoot": "$TESTTMP/t",
106 107 "storepath": "$TESTTMP/t/.hg/store"
107 108 }
108 109 ]
109 110
110 111 At the end...
111 112
112 113 $ cd ..
113 114
114 115 Status message redirection:
115 116
116 117 $ hg init empty
117 118
118 119 status messages are sent to stdout by default:
119 120
120 121 $ hg outgoing -R t empty -Tjson 2>/dev/null
121 122 comparing with empty
122 123 searching for changes
123 124 [
124 125 {
125 126 "bookmarks": [],
126 127 "branch": "default",
127 128 "date": [0, 0],
128 129 "desc": "test",
129 130 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
130 131 "parents": ["0000000000000000000000000000000000000000"],
131 132 "phase": "draft",
132 133 "rev": 0,
133 134 "tags": ["tip"],
134 135 "user": "test"
135 136 }
136 137 ]
137 138
138 139 which can be configured to send to stderr, so the output wouldn't be
139 140 interleaved:
140 141
141 142 $ cat <<'EOF' >> "$HGRCPATH"
142 143 > [ui]
143 144 > message-output = stderr
144 145 > EOF
145 146 $ hg outgoing -R t empty -Tjson 2>/dev/null
146 147 [
147 148 {
148 149 "bookmarks": [],
149 150 "branch": "default",
150 151 "date": [0, 0],
151 152 "desc": "test",
152 153 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
153 154 "parents": ["0000000000000000000000000000000000000000"],
154 155 "phase": "draft",
155 156 "rev": 0,
156 157 "tags": ["tip"],
157 158 "user": "test"
158 159 }
159 160 ]
160 161 $ hg outgoing -R t empty -Tjson >/dev/null
161 162 comparing with empty
162 163 searching for changes
163 164
164 165 this option should be turned off by HGPLAIN= since it may break scripting use:
165 166
166 167 $ HGPLAIN= hg outgoing -R t empty -Tjson 2>/dev/null
167 168 comparing with empty
168 169 searching for changes
169 170 [
170 171 {
171 172 "bookmarks": [],
172 173 "branch": "default",
173 174 "date": [0, 0],
174 175 "desc": "test",
175 176 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
176 177 "parents": ["0000000000000000000000000000000000000000"],
177 178 "phase": "draft",
178 179 "rev": 0,
179 180 "tags": ["tip"],
180 181 "user": "test"
181 182 }
182 183 ]
183 184
184 185 but still overridden by --config:
185 186
186 187 $ HGPLAIN= hg outgoing -R t empty -Tjson --config ui.message-output=stderr \
187 188 > 2>/dev/null
188 189 [
189 190 {
190 191 "bookmarks": [],
191 192 "branch": "default",
192 193 "date": [0, 0],
193 194 "desc": "test",
194 195 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
195 196 "parents": ["0000000000000000000000000000000000000000"],
196 197 "phase": "draft",
197 198 "rev": 0,
198 199 "tags": ["tip"],
199 200 "user": "test"
200 201 }
201 202 ]
202 203
203 204 Invalid ui.message-output option:
204 205
205 206 $ hg log -R t --config ui.message-output=bad
206 207 abort: invalid ui.message-output destination: bad
207 208 [255]
208 209
209 210 Underlying message streams should be updated when ui.fout/ferr are set:
210 211
211 212 $ cat <<'EOF' > capui.py
212 213 > from mercurial import pycompat, registrar
213 214 > cmdtable = {}
214 215 > command = registrar.command(cmdtable)
215 216 > @command(b'capui', norepo=True)
216 217 > def capui(ui):
217 218 > out = ui.fout
218 219 > ui.fout = pycompat.bytesio()
219 220 > ui.status(b'status\n')
220 221 > ui.ferr = pycompat.bytesio()
221 222 > ui.warn(b'warn\n')
222 223 > out.write(b'stdout: %s' % ui.fout.getvalue())
223 224 > out.write(b'stderr: %s' % ui.ferr.getvalue())
224 225 > EOF
225 226 $ hg --config extensions.capui=capui.py --config ui.message-output=stdio capui
226 227 stdout: status
227 228 stderr: warn
@@ -1,105 +1,105 b''
1 1 $ echo "[extensions]" >> $HGRCPATH
2 2 $ echo "rebase=" >> $HGRCPATH
3 3
4 4 initialize repository
5 5
6 6 $ hg init
7 7
8 8 $ echo 'a' > a
9 9 $ hg ci -A -m "0"
10 10 adding a
11 11
12 12 $ echo 'b' > b
13 13 $ hg ci -A -m "1"
14 14 adding b
15 15
16 16 $ hg up 0
17 17 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
18 18 $ echo 'c' > c
19 19 $ hg ci -A -m "2"
20 20 adding c
21 21 created new head
22 22
23 23 $ echo 'd' > d
24 24 $ hg ci -A -m "3"
25 25 adding d
26 26
27 27 $ hg bookmark -r 1 one
28 28 $ hg bookmark -r 3 two
29 29 $ hg up -q two
30 30
31 31 bookmark list
32 32
33 33 $ hg bookmark
34 34 one 1:925d80f479bb
35 35 * two 3:2ae46b1d99a7
36 36
37 37 rebase
38 38
39 39 $ hg rebase -s two -d one
40 40 rebasing 3:2ae46b1d99a7 two tip "3"
41 41 saved backup bundle to $TESTTMP/.hg/strip-backup/2ae46b1d99a7-e6b057bc-rebase.hg
42 42
43 43 $ hg log
44 44 changeset: 3:42e5ed2cdcf4
45 45 bookmark: two
46 46 tag: tip
47 47 parent: 1:925d80f479bb
48 48 user: test
49 49 date: Thu Jan 01 00:00:00 1970 +0000
50 50 summary: 3
51 51
52 52 changeset: 2:db815d6d32e6
53 53 parent: 0:f7b1eb17ad24
54 54 user: test
55 55 date: Thu Jan 01 00:00:00 1970 +0000
56 56 summary: 2
57 57
58 58 changeset: 1:925d80f479bb
59 59 bookmark: one
60 60 user: test
61 61 date: Thu Jan 01 00:00:00 1970 +0000
62 62 summary: 1
63 63
64 64 changeset: 0:f7b1eb17ad24
65 65 user: test
66 66 date: Thu Jan 01 00:00:00 1970 +0000
67 67 summary: 0
68 68
69 69 aborted rebase should restore active bookmark.
70 70
71 71 $ hg up 1
72 72 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
73 73 (leaving bookmark two)
74 74 $ echo 'e' > d
75 75 $ hg ci -A -m "4"
76 76 adding d
77 77 created new head
78 78 $ hg bookmark three
79 79 $ hg rebase -s three -d two
80 80 rebasing 4:dd7c838e8362 three tip "4"
81 81 merging d
82 82 warning: conflicts while merging d! (edit, then use 'hg resolve --mark')
83 83 unresolved conflicts (see 'hg resolve', then 'hg rebase --continue')
84 [1]
84 [240]
85 85 $ hg rebase --abort
86 86 rebase aborted
87 87 $ hg bookmark
88 88 one 1:925d80f479bb
89 89 * three 4:dd7c838e8362
90 90 two 3:42e5ed2cdcf4
91 91
92 92 after aborted rebase, restoring a bookmark that has been removed should not fail
93 93
94 94 $ hg rebase -s three -d two
95 95 rebasing 4:dd7c838e8362 three tip "4"
96 96 merging d
97 97 warning: conflicts while merging d! (edit, then use 'hg resolve --mark')
98 98 unresolved conflicts (see 'hg resolve', then 'hg rebase --continue')
99 [1]
99 [240]
100 100 $ hg bookmark -d three
101 101 $ hg rebase --abort
102 102 rebase aborted
103 103 $ hg bookmark
104 104 one 1:925d80f479bb
105 105 two 3:42e5ed2cdcf4
@@ -1,1156 +1,1158 b''
1 1 #if windows
2 2 $ PYTHONPATH="$TESTDIR/../contrib;$PYTHONPATH"
3 3 #else
4 4 $ PYTHONPATH="$TESTDIR/../contrib:$PYTHONPATH"
5 5 #endif
6 6 $ export PYTHONPATH
7 7
8 8 typical client does not want echo-back messages, so test without it:
9 9
10 10 $ grep -v '^promptecho ' < $HGRCPATH >> $HGRCPATH.new
11 11 $ mv $HGRCPATH.new $HGRCPATH
12 12
13 13 $ hg init repo
14 14 $ cd repo
15 15
16 16 >>> from __future__ import absolute_import
17 17 >>> import os
18 18 >>> import sys
19 19 >>> from hgclient import bprint, check, readchannel, runcommand
20 20 >>> @check
21 21 ... def hellomessage(server):
22 22 ... ch, data = readchannel(server)
23 23 ... bprint(b'%c, %r' % (ch, data))
24 24 ... # run an arbitrary command to make sure the next thing the server
25 25 ... # sends isn't part of the hello message
26 26 ... runcommand(server, [b'id'])
27 27 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
28 28 *** runcommand id
29 29 000000000000 tip
30 30
31 31 >>> from hgclient import check
32 32 >>> @check
33 33 ... def unknowncommand(server):
34 34 ... server.stdin.write(b'unknowncommand\n')
35 35 abort: unknown command unknowncommand
36 36
37 37 >>> from hgclient import check, readchannel, runcommand
38 38 >>> @check
39 39 ... def checkruncommand(server):
40 40 ... # hello block
41 41 ... readchannel(server)
42 42 ...
43 43 ... # no args
44 44 ... runcommand(server, [])
45 45 ...
46 46 ... # global options
47 47 ... runcommand(server, [b'id', b'--quiet'])
48 48 ...
49 49 ... # make sure global options don't stick through requests
50 50 ... runcommand(server, [b'id'])
51 51 ...
52 52 ... # --config
53 53 ... runcommand(server, [b'id', b'--config', b'ui.quiet=True'])
54 54 ...
55 55 ... # make sure --config doesn't stick
56 56 ... runcommand(server, [b'id'])
57 57 ...
58 58 ... # negative return code should be masked
59 59 ... runcommand(server, [b'id', b'-runknown'])
60 60 *** runcommand
61 61 Mercurial Distributed SCM
62 62
63 63 basic commands:
64 64
65 65 add add the specified files on the next commit
66 66 annotate show changeset information by line for each file
67 67 clone make a copy of an existing repository
68 68 commit commit the specified files or all outstanding changes
69 69 diff diff repository (or selected files)
70 70 export dump the header and diffs for one or more changesets
71 71 forget forget the specified files on the next commit
72 72 init create a new repository in the given directory
73 73 log show revision history of entire repository or files
74 74 merge merge another revision into working directory
75 75 pull pull changes from the specified source
76 76 push push changes to the specified destination
77 77 remove remove the specified files on the next commit
78 78 serve start stand-alone webserver
79 79 status show changed files in the working directory
80 80 summary summarize working directory state
81 81 update update working directory (or switch revisions)
82 82
83 83 (use 'hg help' for the full list of commands or 'hg -v' for details)
84 84 *** runcommand id --quiet
85 85 000000000000
86 86 *** runcommand id
87 87 000000000000 tip
88 88 *** runcommand id --config ui.quiet=True
89 89 000000000000
90 90 *** runcommand id
91 91 000000000000 tip
92 92 *** runcommand id -runknown
93 93 abort: unknown revision 'unknown'!
94 94 [255]
95 95
96 96 >>> from hgclient import bprint, check, readchannel
97 97 >>> @check
98 98 ... def inputeof(server):
99 99 ... readchannel(server)
100 100 ... server.stdin.write(b'runcommand\n')
101 101 ... # close stdin while server is waiting for input
102 102 ... server.stdin.close()
103 103 ...
104 104 ... # server exits with 1 if the pipe closed while reading the command
105 105 ... bprint(b'server exit code =', b'%d' % server.wait())
106 106 server exit code = 1
107 107
108 108 >>> from hgclient import check, readchannel, runcommand, stringio
109 109 >>> @check
110 110 ... def serverinput(server):
111 111 ... readchannel(server)
112 112 ...
113 113 ... patch = b"""
114 114 ... # HG changeset patch
115 115 ... # User test
116 116 ... # Date 0 0
117 117 ... # Node ID c103a3dec114d882c98382d684d8af798d09d857
118 118 ... # Parent 0000000000000000000000000000000000000000
119 119 ... 1
120 120 ...
121 121 ... diff -r 000000000000 -r c103a3dec114 a
122 122 ... --- /dev/null Thu Jan 01 00:00:00 1970 +0000
123 123 ... +++ b/a Thu Jan 01 00:00:00 1970 +0000
124 124 ... @@ -0,0 +1,1 @@
125 125 ... +1
126 126 ... """
127 127 ...
128 128 ... runcommand(server, [b'import', b'-'], input=stringio(patch))
129 129 ... runcommand(server, [b'log'])
130 130 *** runcommand import -
131 131 applying patch from stdin
132 132 *** runcommand log
133 133 changeset: 0:eff892de26ec
134 134 tag: tip
135 135 user: test
136 136 date: Thu Jan 01 00:00:00 1970 +0000
137 137 summary: 1
138 138
139 139
140 140 check strict parsing of early options:
141 141
142 142 >>> import os
143 143 >>> from hgclient import check, readchannel, runcommand
144 144 >>> os.environ['HGPLAIN'] = '+strictflags'
145 145 >>> @check
146 146 ... def cwd(server):
147 147 ... readchannel(server)
148 148 ... runcommand(server, [b'log', b'-b', b'--config=alias.log=!echo pwned',
149 149 ... b'default'])
150 150 *** runcommand log -b --config=alias.log=!echo pwned default
151 151 abort: unknown revision '--config=alias.log=!echo pwned'!
152 152 [255]
153 153
154 154 check that "histedit --commands=-" can read rules from the input channel:
155 155
156 156 >>> from hgclient import check, readchannel, runcommand, stringio
157 157 >>> @check
158 158 ... def serverinput(server):
159 159 ... readchannel(server)
160 160 ... rules = b'pick eff892de26ec\n'
161 161 ... runcommand(server, [b'histedit', b'0', b'--commands=-',
162 162 ... b'--config', b'extensions.histedit='],
163 163 ... input=stringio(rules))
164 164 *** runcommand histedit 0 --commands=- --config extensions.histedit=
165 165
166 166 check that --cwd doesn't persist between requests:
167 167
168 168 $ mkdir foo
169 169 $ touch foo/bar
170 170 >>> from hgclient import check, readchannel, runcommand
171 171 >>> @check
172 172 ... def cwd(server):
173 173 ... readchannel(server)
174 174 ... runcommand(server, [b'--cwd', b'foo', b'st', b'bar'])
175 175 ... runcommand(server, [b'st', b'foo/bar'])
176 176 *** runcommand --cwd foo st bar
177 177 ? bar
178 178 *** runcommand st foo/bar
179 179 ? foo/bar
180 180
181 181 $ rm foo/bar
182 182
183 183
184 184 check that local configs for the cached repo aren't inherited when -R is used:
185 185
186 186 $ cat <<EOF >> .hg/hgrc
187 187 > [ui]
188 188 > foo = bar
189 189 > EOF
190 190
191 191 #if no-extraextensions
192 192
193 193 >>> from hgclient import check, readchannel, runcommand, sep
194 194 >>> @check
195 195 ... def localhgrc(server):
196 196 ... readchannel(server)
197 197 ...
198 198 ... # the cached repo local hgrc contains ui.foo=bar, so showconfig should
199 199 ... # show it
200 200 ... runcommand(server, [b'showconfig'], outfilter=sep)
201 201 ...
202 202 ... # but not for this repo
203 203 ... runcommand(server, [b'init', b'foo'])
204 204 ... runcommand(server, [b'-R', b'foo', b'showconfig', b'ui', b'defaults'])
205 205 *** runcommand showconfig
206 206 bundle.mainreporoot=$TESTTMP/repo
207 207 devel.all-warnings=true
208 208 devel.default-date=0 0
209 209 extensions.fsmonitor= (fsmonitor !)
210 210 largefiles.usercache=$TESTTMP/.cache/largefiles
211 211 lfs.usercache=$TESTTMP/.cache/lfs
212 212 ui.slash=True
213 213 ui.interactive=False
214 ui.detailed-exit-code=True
214 215 ui.merge=internal:merge
215 216 ui.mergemarkers=detailed
216 217 ui.foo=bar
217 218 ui.nontty=true
218 219 web.address=localhost
219 220 web\.ipv6=(?:True|False) (re)
220 221 web.server-header=testing stub value
221 222 *** runcommand init foo
222 223 *** runcommand -R foo showconfig ui defaults
223 224 ui.slash=True
224 225 ui.interactive=False
226 ui.detailed-exit-code=True
225 227 ui.merge=internal:merge
226 228 ui.mergemarkers=detailed
227 229 ui.nontty=true
228 230 #endif
229 231
230 232 $ rm -R foo
231 233
232 234 #if windows
233 235 $ PYTHONPATH="$TESTTMP/repo;$PYTHONPATH"
234 236 #else
235 237 $ PYTHONPATH="$TESTTMP/repo:$PYTHONPATH"
236 238 #endif
237 239
238 240 $ cat <<EOF > hook.py
239 241 > import sys
240 242 > from hgclient import bprint
241 243 > def hook(**args):
242 244 > bprint(b'hook talking')
243 245 > bprint(b'now try to read something: %r' % sys.stdin.read())
244 246 > EOF
245 247
246 248 >>> from hgclient import check, readchannel, runcommand, stringio
247 249 >>> @check
248 250 ... def hookoutput(server):
249 251 ... readchannel(server)
250 252 ... runcommand(server, [b'--config',
251 253 ... b'hooks.pre-identify=python:hook.hook',
252 254 ... b'id'],
253 255 ... input=stringio(b'some input'))
254 256 *** runcommand --config hooks.pre-identify=python:hook.hook id
255 257 eff892de26ec tip
256 258 hook talking
257 259 now try to read something: ''
258 260
259 261 Clean hook cached version
260 262 $ rm hook.py*
261 263 $ rm -Rf __pycache__
262 264
263 265 $ echo a >> a
264 266 >>> import os
265 267 >>> from hgclient import check, readchannel, runcommand
266 268 >>> @check
267 269 ... def outsidechanges(server):
268 270 ... readchannel(server)
269 271 ... runcommand(server, [b'status'])
270 272 ... os.system('hg ci -Am2')
271 273 ... runcommand(server, [b'tip'])
272 274 ... runcommand(server, [b'status'])
273 275 *** runcommand status
274 276 M a
275 277 *** runcommand tip
276 278 changeset: 1:d3a0a68be6de
277 279 tag: tip
278 280 user: test
279 281 date: Thu Jan 01 00:00:00 1970 +0000
280 282 summary: 2
281 283
282 284 *** runcommand status
283 285
284 286 >>> import os
285 287 >>> from hgclient import bprint, check, readchannel, runcommand
286 288 >>> @check
287 289 ... def bookmarks(server):
288 290 ... readchannel(server)
289 291 ... runcommand(server, [b'bookmarks'])
290 292 ...
291 293 ... # changes .hg/bookmarks
292 294 ... os.system('hg bookmark -i bm1')
293 295 ... os.system('hg bookmark -i bm2')
294 296 ... runcommand(server, [b'bookmarks'])
295 297 ...
296 298 ... # changes .hg/bookmarks.current
297 299 ... os.system('hg upd bm1 -q')
298 300 ... runcommand(server, [b'bookmarks'])
299 301 ...
300 302 ... runcommand(server, [b'bookmarks', b'bm3'])
301 303 ... f = open('a', 'ab')
302 304 ... f.write(b'a\n') and None
303 305 ... f.close()
304 306 ... runcommand(server, [b'commit', b'-Amm'])
305 307 ... runcommand(server, [b'bookmarks'])
306 308 ... bprint(b'')
307 309 *** runcommand bookmarks
308 310 no bookmarks set
309 311 *** runcommand bookmarks
310 312 bm1 1:d3a0a68be6de
311 313 bm2 1:d3a0a68be6de
312 314 *** runcommand bookmarks
313 315 * bm1 1:d3a0a68be6de
314 316 bm2 1:d3a0a68be6de
315 317 *** runcommand bookmarks bm3
316 318 *** runcommand commit -Amm
317 319 *** runcommand bookmarks
318 320 bm1 1:d3a0a68be6de
319 321 bm2 1:d3a0a68be6de
320 322 * bm3 2:aef17e88f5f0
321 323
322 324
323 325 >>> import os
324 326 >>> from hgclient import check, readchannel, runcommand
325 327 >>> @check
326 328 ... def tagscache(server):
327 329 ... readchannel(server)
328 330 ... runcommand(server, [b'id', b'-t', b'-r', b'0'])
329 331 ... os.system('hg tag -r 0 foo')
330 332 ... runcommand(server, [b'id', b'-t', b'-r', b'0'])
331 333 *** runcommand id -t -r 0
332 334
333 335 *** runcommand id -t -r 0
334 336 foo
335 337
336 338 >>> import os
337 339 >>> from hgclient import check, readchannel, runcommand
338 340 >>> @check
339 341 ... def setphase(server):
340 342 ... readchannel(server)
341 343 ... runcommand(server, [b'phase', b'-r', b'.'])
342 344 ... os.system('hg phase -r . -p')
343 345 ... runcommand(server, [b'phase', b'-r', b'.'])
344 346 *** runcommand phase -r .
345 347 3: draft
346 348 *** runcommand phase -r .
347 349 3: public
348 350
349 351 $ echo a >> a
350 352 >>> from hgclient import bprint, check, readchannel, runcommand
351 353 >>> @check
352 354 ... def rollback(server):
353 355 ... readchannel(server)
354 356 ... runcommand(server, [b'phase', b'-r', b'.', b'-p'])
355 357 ... runcommand(server, [b'commit', b'-Am.'])
356 358 ... runcommand(server, [b'rollback'])
357 359 ... runcommand(server, [b'phase', b'-r', b'.'])
358 360 ... bprint(b'')
359 361 *** runcommand phase -r . -p
360 362 no phases changed
361 363 *** runcommand commit -Am.
362 364 *** runcommand rollback
363 365 repository tip rolled back to revision 3 (undo commit)
364 366 working directory now based on revision 3
365 367 *** runcommand phase -r .
366 368 3: public
367 369
368 370
369 371 >>> import os
370 372 >>> from hgclient import check, readchannel, runcommand
371 373 >>> @check
372 374 ... def branch(server):
373 375 ... readchannel(server)
374 376 ... runcommand(server, [b'branch'])
375 377 ... os.system('hg branch foo')
376 378 ... runcommand(server, [b'branch'])
377 379 ... os.system('hg branch default')
378 380 *** runcommand branch
379 381 default
380 382 marked working directory as branch foo
381 383 (branches are permanent and global, did you want a bookmark?)
382 384 *** runcommand branch
383 385 foo
384 386 marked working directory as branch default
385 387 (branches are permanent and global, did you want a bookmark?)
386 388
387 389 $ touch .hgignore
388 390 >>> import os
389 391 >>> from hgclient import bprint, check, readchannel, runcommand
390 392 >>> @check
391 393 ... def hgignore(server):
392 394 ... readchannel(server)
393 395 ... runcommand(server, [b'commit', b'-Am.'])
394 396 ... f = open('ignored-file', 'ab')
395 397 ... f.write(b'') and None
396 398 ... f.close()
397 399 ... f = open('.hgignore', 'ab')
398 400 ... f.write(b'ignored-file')
399 401 ... f.close()
400 402 ... runcommand(server, [b'status', b'-i', b'-u'])
401 403 ... bprint(b'')
402 404 *** runcommand commit -Am.
403 405 adding .hgignore
404 406 *** runcommand status -i -u
405 407 I ignored-file
406 408
407 409
408 410 cache of non-public revisions should be invalidated on repository change
409 411 (issue4855):
410 412
411 413 >>> import os
412 414 >>> from hgclient import bprint, check, readchannel, runcommand
413 415 >>> @check
414 416 ... def phasesetscacheaftercommit(server):
415 417 ... readchannel(server)
416 418 ... # load _phasecache._phaserevs and _phasesets
417 419 ... runcommand(server, [b'log', b'-qr', b'draft()'])
418 420 ... # create draft commits by another process
419 421 ... for i in range(5, 7):
420 422 ... f = open('a', 'ab')
421 423 ... f.seek(0, os.SEEK_END)
422 424 ... f.write(b'a\n') and None
423 425 ... f.close()
424 426 ... os.system('hg commit -Aqm%d' % i)
425 427 ... # new commits should be listed as draft revisions
426 428 ... runcommand(server, [b'log', b'-qr', b'draft()'])
427 429 ... bprint(b'')
428 430 *** runcommand log -qr draft()
429 431 4:7966c8e3734d
430 432 *** runcommand log -qr draft()
431 433 4:7966c8e3734d
432 434 5:41f6602d1c4f
433 435 6:10501e202c35
434 436
435 437
436 438 >>> import os
437 439 >>> from hgclient import bprint, check, readchannel, runcommand
438 440 >>> @check
439 441 ... def phasesetscacheafterstrip(server):
440 442 ... readchannel(server)
441 443 ... # load _phasecache._phaserevs and _phasesets
442 444 ... runcommand(server, [b'log', b'-qr', b'draft()'])
443 445 ... # strip cached revisions by another process
444 446 ... os.system('hg --config extensions.strip= strip -q 5')
445 447 ... # shouldn't abort by "unknown revision '6'"
446 448 ... runcommand(server, [b'log', b'-qr', b'draft()'])
447 449 ... bprint(b'')
448 450 *** runcommand log -qr draft()
449 451 4:7966c8e3734d
450 452 5:41f6602d1c4f
451 453 6:10501e202c35
452 454 *** runcommand log -qr draft()
453 455 4:7966c8e3734d
454 456
455 457
456 458 cache of phase roots should be invalidated on strip (issue3827):
457 459
458 460 >>> import os
459 461 >>> from hgclient import check, readchannel, runcommand, sep
460 462 >>> @check
461 463 ... def phasecacheafterstrip(server):
462 464 ... readchannel(server)
463 465 ...
464 466 ... # create new head, 5:731265503d86
465 467 ... runcommand(server, [b'update', b'-C', b'0'])
466 468 ... f = open('a', 'ab')
467 469 ... f.write(b'a\n') and None
468 470 ... f.close()
469 471 ... runcommand(server, [b'commit', b'-Am.', b'a'])
470 472 ... runcommand(server, [b'log', b'-Gq'])
471 473 ...
472 474 ... # make it public; draft marker moves to 4:7966c8e3734d
473 475 ... runcommand(server, [b'phase', b'-p', b'.'])
474 476 ... # load _phasecache.phaseroots
475 477 ... runcommand(server, [b'phase', b'.'], outfilter=sep)
476 478 ...
477 479 ... # strip 1::4 outside server
478 480 ... os.system('hg -q --config extensions.mq= strip 1')
479 481 ...
480 482 ... # shouldn't raise "7966c8e3734d: no node!"
481 483 ... runcommand(server, [b'branches'])
482 484 *** runcommand update -C 0
483 485 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
484 486 (leaving bookmark bm3)
485 487 *** runcommand commit -Am. a
486 488 created new head
487 489 *** runcommand log -Gq
488 490 @ 5:731265503d86
489 491 |
490 492 | o 4:7966c8e3734d
491 493 | |
492 494 | o 3:b9b85890c400
493 495 | |
494 496 | o 2:aef17e88f5f0
495 497 | |
496 498 | o 1:d3a0a68be6de
497 499 |/
498 500 o 0:eff892de26ec
499 501
500 502 *** runcommand phase -p .
501 503 *** runcommand phase .
502 504 5: public
503 505 *** runcommand branches
504 506 default 1:731265503d86
505 507
506 508 in-memory cache must be reloaded if transaction is aborted. otherwise
507 509 changelog and manifest would have invalid node:
508 510
509 511 $ echo a >> a
510 512 >>> from hgclient import check, readchannel, runcommand
511 513 >>> @check
512 514 ... def txabort(server):
513 515 ... readchannel(server)
514 516 ... runcommand(server, [b'commit', b'--config', b'hooks.pretxncommit=false',
515 517 ... b'-mfoo'])
516 518 ... runcommand(server, [b'verify'])
517 519 *** runcommand commit --config hooks.pretxncommit=false -mfoo
518 520 transaction abort!
519 521 rollback completed
520 522 abort: pretxncommit hook exited with status 1
521 523 [255]
522 524 *** runcommand verify
523 525 checking changesets
524 526 checking manifests
525 527 crosschecking files in changesets and manifests
526 528 checking files
527 529 checked 2 changesets with 2 changes to 1 files
528 530 $ hg revert --no-backup -aq
529 531
530 532 $ cat >> .hg/hgrc << EOF
531 533 > [experimental]
532 534 > evolution.createmarkers=True
533 535 > EOF
534 536
535 537 >>> import os
536 538 >>> from hgclient import check, readchannel, runcommand
537 539 >>> @check
538 540 ... def obsolete(server):
539 541 ... readchannel(server)
540 542 ...
541 543 ... runcommand(server, [b'up', b'null'])
542 544 ... runcommand(server, [b'phase', b'-df', b'tip'])
543 545 ... cmd = 'hg debugobsolete `hg log -r tip --template {node}`'
544 546 ... if os.name == 'nt':
545 547 ... cmd = 'sh -c "%s"' % cmd # run in sh, not cmd.exe
546 548 ... os.system(cmd)
547 549 ... runcommand(server, [b'log', b'--hidden'])
548 550 ... runcommand(server, [b'log'])
549 551 *** runcommand up null
550 552 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
551 553 *** runcommand phase -df tip
552 554 1 new obsolescence markers
553 555 obsoleted 1 changesets
554 556 *** runcommand log --hidden
555 557 changeset: 1:731265503d86
556 558 tag: tip
557 559 user: test
558 560 date: Thu Jan 01 00:00:00 1970 +0000
559 561 obsolete: pruned
560 562 summary: .
561 563
562 564 changeset: 0:eff892de26ec
563 565 bookmark: bm1
564 566 bookmark: bm2
565 567 bookmark: bm3
566 568 user: test
567 569 date: Thu Jan 01 00:00:00 1970 +0000
568 570 summary: 1
569 571
570 572 *** runcommand log
571 573 changeset: 0:eff892de26ec
572 574 bookmark: bm1
573 575 bookmark: bm2
574 576 bookmark: bm3
575 577 tag: tip
576 578 user: test
577 579 date: Thu Jan 01 00:00:00 1970 +0000
578 580 summary: 1
579 581
580 582
581 583 $ cat <<EOF >> .hg/hgrc
582 584 > [extensions]
583 585 > mq =
584 586 > EOF
585 587
586 588 >>> import os
587 589 >>> from hgclient import check, readchannel, runcommand
588 590 >>> @check
589 591 ... def mqoutsidechanges(server):
590 592 ... readchannel(server)
591 593 ...
592 594 ... # load repo.mq
593 595 ... runcommand(server, [b'qapplied'])
594 596 ... os.system('hg qnew 0.diff')
595 597 ... # repo.mq should be invalidated
596 598 ... runcommand(server, [b'qapplied'])
597 599 ...
598 600 ... runcommand(server, [b'qpop', b'--all'])
599 601 ... os.system('hg qqueue --create foo')
600 602 ... # repo.mq should be recreated to point to new queue
601 603 ... runcommand(server, [b'qqueue', b'--active'])
602 604 *** runcommand qapplied
603 605 *** runcommand qapplied
604 606 0.diff
605 607 *** runcommand qpop --all
606 608 popping 0.diff
607 609 patch queue now empty
608 610 *** runcommand qqueue --active
609 611 foo
610 612
611 613 $ cat <<'EOF' > ../dbgui.py
612 614 > import os
613 615 > import sys
614 616 > from mercurial import commands, registrar
615 617 > cmdtable = {}
616 618 > command = registrar.command(cmdtable)
617 619 > @command(b"debuggetpass", norepo=True)
618 620 > def debuggetpass(ui):
619 621 > ui.write(b"%s\n" % ui.getpass())
620 622 > @command(b"debugprompt", norepo=True)
621 623 > def debugprompt(ui):
622 624 > ui.write(b"%s\n" % ui.prompt(b"prompt:"))
623 625 > @command(b"debugpromptchoice", norepo=True)
624 626 > def debugpromptchoice(ui):
625 627 > msg = b"promptchoice (y/n)? $$ &Yes $$ &No"
626 628 > ui.write(b"%d\n" % ui.promptchoice(msg))
627 629 > @command(b"debugreadstdin", norepo=True)
628 630 > def debugreadstdin(ui):
629 631 > ui.write(b"read: %r\n" % sys.stdin.read(1))
630 632 > @command(b"debugwritestdout", norepo=True)
631 633 > def debugwritestdout(ui):
632 634 > os.write(1, b"low-level stdout fd and\n")
633 635 > sys.stdout.write("stdout should be redirected to stderr\n")
634 636 > sys.stdout.flush()
635 637 > EOF
636 638 $ cat <<EOF >> .hg/hgrc
637 639 > [extensions]
638 640 > dbgui = ../dbgui.py
639 641 > EOF
640 642
641 643 >>> from hgclient import check, readchannel, runcommand, stringio
642 644 >>> @check
643 645 ... def getpass(server):
644 646 ... readchannel(server)
645 647 ... runcommand(server, [b'debuggetpass', b'--config',
646 648 ... b'ui.interactive=True'],
647 649 ... input=stringio(b'1234\n'))
648 650 ... runcommand(server, [b'debuggetpass', b'--config',
649 651 ... b'ui.interactive=True'],
650 652 ... input=stringio(b'\n'))
651 653 ... runcommand(server, [b'debuggetpass', b'--config',
652 654 ... b'ui.interactive=True'],
653 655 ... input=stringio(b''))
654 656 ... runcommand(server, [b'debugprompt', b'--config',
655 657 ... b'ui.interactive=True'],
656 658 ... input=stringio(b'5678\n'))
657 659 ... runcommand(server, [b'debugprompt', b'--config',
658 660 ... b'ui.interactive=True'],
659 661 ... input=stringio(b'\nremainder\nshould\nnot\nbe\nread\n'))
660 662 ... runcommand(server, [b'debugreadstdin'])
661 663 ... runcommand(server, [b'debugwritestdout'])
662 664 *** runcommand debuggetpass --config ui.interactive=True
663 665 password: 1234
664 666 *** runcommand debuggetpass --config ui.interactive=True
665 667 password:
666 668 *** runcommand debuggetpass --config ui.interactive=True
667 669 password: abort: response expected
668 670 [255]
669 671 *** runcommand debugprompt --config ui.interactive=True
670 672 prompt: 5678
671 673 *** runcommand debugprompt --config ui.interactive=True
672 674 prompt: y
673 675 *** runcommand debugreadstdin
674 676 read: ''
675 677 *** runcommand debugwritestdout
676 678 low-level stdout fd and
677 679 stdout should be redirected to stderr
678 680
679 681
680 682 run commandserver in commandserver, which is silly but should work:
681 683
682 684 >>> from hgclient import bprint, check, readchannel, runcommand, stringio
683 685 >>> @check
684 686 ... def nested(server):
685 687 ... bprint(b'%c, %r' % readchannel(server))
686 688 ... class nestedserver(object):
687 689 ... stdin = stringio(b'getencoding\n')
688 690 ... stdout = stringio()
689 691 ... runcommand(server, [b'serve', b'--cmdserver', b'pipe'],
690 692 ... output=nestedserver.stdout, input=nestedserver.stdin)
691 693 ... nestedserver.stdout.seek(0)
692 694 ... bprint(b'%c, %r' % readchannel(nestedserver)) # hello
693 695 ... bprint(b'%c, %r' % readchannel(nestedserver)) # getencoding
694 696 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
695 697 *** runcommand serve --cmdserver pipe
696 698 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
697 699 r, '*' (glob)
698 700
699 701
700 702 start without repository:
701 703
702 704 $ cd ..
703 705
704 706 >>> from hgclient import bprint, check, readchannel, runcommand
705 707 >>> @check
706 708 ... def hellomessage(server):
707 709 ... ch, data = readchannel(server)
708 710 ... bprint(b'%c, %r' % (ch, data))
709 711 ... # run an arbitrary command to make sure the next thing the server
710 712 ... # sends isn't part of the hello message
711 713 ... runcommand(server, [b'id'])
712 714 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
713 715 *** runcommand id
714 716 abort: there is no Mercurial repository here (.hg not found)
715 717 [255]
716 718
717 719 >>> from hgclient import check, readchannel, runcommand
718 720 >>> @check
719 721 ... def startwithoutrepo(server):
720 722 ... readchannel(server)
721 723 ... runcommand(server, [b'init', b'repo2'])
722 724 ... runcommand(server, [b'id', b'-R', b'repo2'])
723 725 *** runcommand init repo2
724 726 *** runcommand id -R repo2
725 727 000000000000 tip
726 728
727 729
728 730 don't fall back to cwd if invalid -R path is specified (issue4805):
729 731
730 732 $ cd repo
731 733 $ hg serve --cmdserver pipe -R ../nonexistent
732 734 abort: repository ../nonexistent not found!
733 735 [255]
734 736 $ cd ..
735 737
736 738
737 739 #if no-windows
738 740
739 741 option to not shutdown on SIGINT:
740 742
741 743 $ cat <<'EOF' > dbgint.py
742 744 > import os
743 745 > import signal
744 746 > import time
745 747 > from mercurial import commands, registrar
746 748 > cmdtable = {}
747 749 > command = registrar.command(cmdtable)
748 750 > @command(b"debugsleep", norepo=True)
749 751 > def debugsleep(ui):
750 752 > time.sleep(1)
751 753 > @command(b"debugsuicide", norepo=True)
752 754 > def debugsuicide(ui):
753 755 > os.kill(os.getpid(), signal.SIGINT)
754 756 > time.sleep(1)
755 757 > EOF
756 758
757 759 >>> import signal
758 760 >>> import time
759 761 >>> from hgclient import checkwith, readchannel, runcommand
760 762 >>> @checkwith(extraargs=[b'--config', b'cmdserver.shutdown-on-interrupt=False',
761 763 ... b'--config', b'extensions.dbgint=dbgint.py'])
762 764 ... def nointr(server):
763 765 ... readchannel(server)
764 766 ... server.send_signal(signal.SIGINT) # server won't be terminated
765 767 ... time.sleep(1)
766 768 ... runcommand(server, [b'debugsleep'])
767 769 ... server.send_signal(signal.SIGINT) # server won't be terminated
768 770 ... runcommand(server, [b'debugsleep'])
769 771 ... runcommand(server, [b'debugsuicide']) # command can be interrupted
770 772 ... server.send_signal(signal.SIGTERM) # server will be terminated
771 773 ... time.sleep(1)
772 774 *** runcommand debugsleep
773 775 *** runcommand debugsleep
774 776 *** runcommand debugsuicide
775 777 interrupted!
776 778 killed!
777 779 [255]
778 780
779 781 #endif
780 782
781 783
782 784 structured message channel:
783 785
784 786 $ cat <<'EOF' >> repo2/.hg/hgrc
785 787 > [ui]
786 788 > # server --config should precede repository option
787 789 > message-output = stdio
788 790 > EOF
789 791
790 792 >>> from hgclient import bprint, checkwith, readchannel, runcommand
791 793 >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel',
792 794 ... b'--config', b'cmdserver.message-encodings=foo cbor'])
793 795 ... def verify(server):
794 796 ... _ch, data = readchannel(server)
795 797 ... bprint(data)
796 798 ... runcommand(server, [b'-R', b'repo2', b'verify'])
797 799 capabilities: getencoding runcommand
798 800 encoding: ascii
799 801 message-encoding: cbor
800 802 pid: * (glob)
801 803 pgid: * (glob) (no-windows !)
802 804 *** runcommand -R repo2 verify
803 805 message: '\xa2DdataTchecking changesets\nDtypeFstatus'
804 806 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
805 807 message: '\xa2DdataSchecking manifests\nDtypeFstatus'
806 808 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
807 809 message: '\xa2DdataX0crosschecking files in changesets and manifests\nDtypeFstatus'
808 810 message: '\xa6Ditem@Cpos\xf6EtopicMcrosscheckingEtotal\xf6DtypeHprogressDunit@'
809 811 message: '\xa2DdataOchecking files\nDtypeFstatus'
810 812 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
811 813 message: '\xa2DdataX/checked 0 changesets with 0 changes to 0 files\nDtypeFstatus'
812 814
813 815 >>> from hgclient import checkwith, readchannel, runcommand, stringio
814 816 >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel',
815 817 ... b'--config', b'cmdserver.message-encodings=cbor',
816 818 ... b'--config', b'extensions.dbgui=dbgui.py'])
817 819 ... def prompt(server):
818 820 ... readchannel(server)
819 821 ... interactive = [b'--config', b'ui.interactive=True']
820 822 ... runcommand(server, [b'debuggetpass'] + interactive,
821 823 ... input=stringio(b'1234\n'))
822 824 ... runcommand(server, [b'debugprompt'] + interactive,
823 825 ... input=stringio(b'5678\n'))
824 826 ... runcommand(server, [b'debugpromptchoice'] + interactive,
825 827 ... input=stringio(b'n\n'))
826 828 *** runcommand debuggetpass --config ui.interactive=True
827 829 message: '\xa3DdataJpassword: Hpassword\xf5DtypeFprompt'
828 830 1234
829 831 *** runcommand debugprompt --config ui.interactive=True
830 832 message: '\xa3DdataGprompt:GdefaultAyDtypeFprompt'
831 833 5678
832 834 *** runcommand debugpromptchoice --config ui.interactive=True
833 835 message: '\xa4Gchoices\x82\x82AyCYes\x82AnBNoDdataTpromptchoice (y/n)? GdefaultAyDtypeFprompt'
834 836 1
835 837
836 838 bad message encoding:
837 839
838 840 $ hg serve --cmdserver pipe --config ui.message-output=channel
839 841 abort: no supported message encodings:
840 842 [255]
841 843 $ hg serve --cmdserver pipe --config ui.message-output=channel \
842 844 > --config cmdserver.message-encodings='foo bar'
843 845 abort: no supported message encodings: foo bar
844 846 [255]
845 847
846 848 unix domain socket:
847 849
848 850 $ cd repo
849 851 $ hg update -q
850 852
851 853 #if unix-socket unix-permissions
852 854
853 855 >>> from hgclient import bprint, check, readchannel, runcommand, stringio, unixserver
854 856 >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log')
855 857 >>> def hellomessage(conn):
856 858 ... ch, data = readchannel(conn)
857 859 ... bprint(b'%c, %r' % (ch, data))
858 860 ... runcommand(conn, [b'id'])
859 861 >>> check(hellomessage, server.connect)
860 862 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
861 863 *** runcommand id
862 864 eff892de26ec tip bm1/bm2/bm3
863 865 >>> def unknowncommand(conn):
864 866 ... readchannel(conn)
865 867 ... conn.stdin.write(b'unknowncommand\n')
866 868 >>> check(unknowncommand, server.connect) # error sent to server.log
867 869 >>> def serverinput(conn):
868 870 ... readchannel(conn)
869 871 ... patch = b"""
870 872 ... # HG changeset patch
871 873 ... # User test
872 874 ... # Date 0 0
873 875 ... 2
874 876 ...
875 877 ... diff -r eff892de26ec -r 1ed24be7e7a0 a
876 878 ... --- a/a
877 879 ... +++ b/a
878 880 ... @@ -1,1 +1,2 @@
879 881 ... 1
880 882 ... +2
881 883 ... """
882 884 ... runcommand(conn, [b'import', b'-'], input=stringio(patch))
883 885 ... runcommand(conn, [b'log', b'-rtip', b'-q'])
884 886 >>> check(serverinput, server.connect)
885 887 *** runcommand import -
886 888 applying patch from stdin
887 889 *** runcommand log -rtip -q
888 890 2:1ed24be7e7a0
889 891 >>> server.shutdown()
890 892
891 893 $ cat .hg/server.log
892 894 listening at .hg/server.sock
893 895 abort: unknown command unknowncommand
894 896 killed!
895 897 $ rm .hg/server.log
896 898
897 899 if server crashed before hello, traceback will be sent to 'e' channel as
898 900 last ditch:
899 901
900 902 $ cat <<'EOF' > ../earlycrasher.py
901 903 > from mercurial import commandserver, extensions
902 904 > def _serverequest(orig, ui, repo, conn, createcmdserver, prereposetups):
903 905 > def createcmdserver(*args, **kwargs):
904 906 > raise Exception('crash')
905 907 > return orig(ui, repo, conn, createcmdserver, prereposetups)
906 908 > def extsetup(ui):
907 909 > extensions.wrapfunction(commandserver, b'_serverequest', _serverequest)
908 910 > EOF
909 911 $ cat <<EOF >> .hg/hgrc
910 912 > [extensions]
911 913 > earlycrasher = ../earlycrasher.py
912 914 > EOF
913 915 >>> from hgclient import bprint, check, readchannel, unixserver
914 916 >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log')
915 917 >>> def earlycrash(conn):
916 918 ... while True:
917 919 ... try:
918 920 ... ch, data = readchannel(conn)
919 921 ... for l in data.splitlines(True):
920 922 ... if not l.startswith(b' '):
921 923 ... bprint(b'%c, %r' % (ch, l))
922 924 ... except EOFError:
923 925 ... break
924 926 >>> check(earlycrash, server.connect)
925 927 e, 'Traceback (most recent call last):\n'
926 928 e, 'Exception: crash\n'
927 929 >>> server.shutdown()
928 930
929 931 $ cat .hg/server.log | grep -v '^ '
930 932 listening at .hg/server.sock
931 933 Traceback (most recent call last):
932 934 Exception: crash
933 935 killed!
934 936 #endif
935 937 #if no-unix-socket
936 938
937 939 $ hg serve --cmdserver unix -a .hg/server.sock
938 940 abort: unsupported platform
939 941 [255]
940 942
941 943 #endif
942 944
943 945 $ cd ..
944 946
945 947 Test that accessing to invalid changelog cache is avoided at
946 948 subsequent operations even if repo object is reused even after failure
947 949 of transaction (see 0a7610758c42 also)
948 950
949 951 "hg log" after failure of transaction is needed to detect invalid
950 952 cache in repoview: this can't detect by "hg verify" only.
951 953
952 954 Combination of "finalization" and "empty-ness of changelog" (2 x 2 =
953 955 4) are tested, because '00changelog.i' are differently changed in each
954 956 cases.
955 957
956 958 $ cat > $TESTTMP/failafterfinalize.py <<EOF
957 959 > # extension to abort transaction after finalization forcibly
958 960 > from mercurial import commands, error, extensions, lock as lockmod
959 961 > from mercurial import registrar
960 962 > cmdtable = {}
961 963 > command = registrar.command(cmdtable)
962 964 > configtable = {}
963 965 > configitem = registrar.configitem(configtable)
964 966 > configitem(b'failafterfinalize', b'fail',
965 967 > default=None,
966 968 > )
967 969 > def fail(tr):
968 970 > raise error.Abort(b'fail after finalization')
969 971 > def reposetup(ui, repo):
970 972 > class failrepo(repo.__class__):
971 973 > def commitctx(self, ctx, error=False, origctx=None):
972 974 > if self.ui.configbool(b'failafterfinalize', b'fail'):
973 975 > # 'sorted()' by ASCII code on category names causes
974 976 > # invoking 'fail' after finalization of changelog
975 977 > # using "'cl-%i' % id(self)" as category name
976 978 > self.currenttransaction().addfinalize(b'zzzzzzzz', fail)
977 979 > return super(failrepo, self).commitctx(ctx, error, origctx)
978 980 > repo.__class__ = failrepo
979 981 > EOF
980 982
981 983 $ hg init repo3
982 984 $ cd repo3
983 985
984 986 $ cat <<EOF >> $HGRCPATH
985 987 > [command-templates]
986 988 > log = {rev} {desc|firstline} ({files})\n
987 989 >
988 990 > [extensions]
989 991 > failafterfinalize = $TESTTMP/failafterfinalize.py
990 992 > EOF
991 993
992 994 - test failure with "empty changelog"
993 995
994 996 $ echo foo > foo
995 997 $ hg add foo
996 998
997 999 (failure before finalization)
998 1000
999 1001 >>> from hgclient import check, readchannel, runcommand
1000 1002 >>> @check
1001 1003 ... def abort(server):
1002 1004 ... readchannel(server)
1003 1005 ... runcommand(server, [b'commit',
1004 1006 ... b'--config', b'hooks.pretxncommit=false',
1005 1007 ... b'-mfoo'])
1006 1008 ... runcommand(server, [b'log'])
1007 1009 ... runcommand(server, [b'verify', b'-q'])
1008 1010 *** runcommand commit --config hooks.pretxncommit=false -mfoo
1009 1011 transaction abort!
1010 1012 rollback completed
1011 1013 abort: pretxncommit hook exited with status 1
1012 1014 [255]
1013 1015 *** runcommand log
1014 1016 *** runcommand verify -q
1015 1017
1016 1018 (failure after finalization)
1017 1019
1018 1020 >>> from hgclient import check, readchannel, runcommand
1019 1021 >>> @check
1020 1022 ... def abort(server):
1021 1023 ... readchannel(server)
1022 1024 ... runcommand(server, [b'commit',
1023 1025 ... b'--config', b'failafterfinalize.fail=true',
1024 1026 ... b'-mfoo'])
1025 1027 ... runcommand(server, [b'log'])
1026 1028 ... runcommand(server, [b'verify', b'-q'])
1027 1029 *** runcommand commit --config failafterfinalize.fail=true -mfoo
1028 1030 transaction abort!
1029 1031 rollback completed
1030 1032 abort: fail after finalization
1031 1033 [255]
1032 1034 *** runcommand log
1033 1035 *** runcommand verify -q
1034 1036
1035 1037 - test failure with "not-empty changelog"
1036 1038
1037 1039 $ echo bar > bar
1038 1040 $ hg add bar
1039 1041 $ hg commit -mbar bar
1040 1042
1041 1043 (failure before finalization)
1042 1044
1043 1045 >>> from hgclient import check, readchannel, runcommand
1044 1046 >>> @check
1045 1047 ... def abort(server):
1046 1048 ... readchannel(server)
1047 1049 ... runcommand(server, [b'commit',
1048 1050 ... b'--config', b'hooks.pretxncommit=false',
1049 1051 ... b'-mfoo', b'foo'])
1050 1052 ... runcommand(server, [b'log'])
1051 1053 ... runcommand(server, [b'verify', b'-q'])
1052 1054 *** runcommand commit --config hooks.pretxncommit=false -mfoo foo
1053 1055 transaction abort!
1054 1056 rollback completed
1055 1057 abort: pretxncommit hook exited with status 1
1056 1058 [255]
1057 1059 *** runcommand log
1058 1060 0 bar (bar)
1059 1061 *** runcommand verify -q
1060 1062
1061 1063 (failure after finalization)
1062 1064
1063 1065 >>> from hgclient import check, readchannel, runcommand
1064 1066 >>> @check
1065 1067 ... def abort(server):
1066 1068 ... readchannel(server)
1067 1069 ... runcommand(server, [b'commit',
1068 1070 ... b'--config', b'failafterfinalize.fail=true',
1069 1071 ... b'-mfoo', b'foo'])
1070 1072 ... runcommand(server, [b'log'])
1071 1073 ... runcommand(server, [b'verify', b'-q'])
1072 1074 *** runcommand commit --config failafterfinalize.fail=true -mfoo foo
1073 1075 transaction abort!
1074 1076 rollback completed
1075 1077 abort: fail after finalization
1076 1078 [255]
1077 1079 *** runcommand log
1078 1080 0 bar (bar)
1079 1081 *** runcommand verify -q
1080 1082
1081 1083 $ cd ..
1082 1084
1083 1085 Test symlink traversal over cached audited paths:
1084 1086 -------------------------------------------------
1085 1087
1086 1088 #if symlink
1087 1089
1088 1090 set up symlink hell
1089 1091
1090 1092 $ mkdir merge-symlink-out
1091 1093 $ hg init merge-symlink
1092 1094 $ cd merge-symlink
1093 1095 $ touch base
1094 1096 $ hg commit -qAm base
1095 1097 $ ln -s ../merge-symlink-out a
1096 1098 $ hg commit -qAm 'symlink a -> ../merge-symlink-out'
1097 1099 $ hg up -q 0
1098 1100 $ mkdir a
1099 1101 $ touch a/poisoned
1100 1102 $ hg commit -qAm 'file a/poisoned'
1101 1103 $ hg log -G -T '{rev}: {desc}\n'
1102 1104 @ 2: file a/poisoned
1103 1105 |
1104 1106 | o 1: symlink a -> ../merge-symlink-out
1105 1107 |/
1106 1108 o 0: base
1107 1109
1108 1110
1109 1111 try trivial merge after update: cache of audited paths should be discarded,
1110 1112 and the merge should fail (issue5628)
1111 1113
1112 1114 $ hg up -q null
1113 1115 >>> from hgclient import check, readchannel, runcommand
1114 1116 >>> @check
1115 1117 ... def merge(server):
1116 1118 ... readchannel(server)
1117 1119 ... # audit a/poisoned as a good path
1118 1120 ... runcommand(server, [b'up', b'-qC', b'2'])
1119 1121 ... runcommand(server, [b'up', b'-qC', b'1'])
1120 1122 ... # here a is a symlink, so a/poisoned is bad
1121 1123 ... runcommand(server, [b'merge', b'2'])
1122 1124 *** runcommand up -qC 2
1123 1125 *** runcommand up -qC 1
1124 1126 *** runcommand merge 2
1125 1127 abort: path 'a/poisoned' traverses symbolic link 'a'
1126 1128 [255]
1127 1129 $ ls ../merge-symlink-out
1128 1130
1129 1131 cache of repo.auditor should be discarded, so matcher would never traverse
1130 1132 symlinks:
1131 1133
1132 1134 $ hg up -qC 0
1133 1135 $ touch ../merge-symlink-out/poisoned
1134 1136 >>> from hgclient import check, readchannel, runcommand
1135 1137 >>> @check
1136 1138 ... def files(server):
1137 1139 ... readchannel(server)
1138 1140 ... runcommand(server, [b'up', b'-qC', b'2'])
1139 1141 ... # audit a/poisoned as a good path
1140 1142 ... runcommand(server, [b'files', b'a/poisoned'])
1141 1143 ... runcommand(server, [b'up', b'-qC', b'0'])
1142 1144 ... runcommand(server, [b'up', b'-qC', b'1'])
1143 1145 ... # here 'a' is a symlink, so a/poisoned should be warned
1144 1146 ... runcommand(server, [b'files', b'a/poisoned'])
1145 1147 *** runcommand up -qC 2
1146 1148 *** runcommand files a/poisoned
1147 1149 a/poisoned
1148 1150 *** runcommand up -qC 0
1149 1151 *** runcommand up -qC 1
1150 1152 *** runcommand files a/poisoned
1151 1153 abort: path 'a/poisoned' traverses symbolic link 'a'
1152 1154 [255]
1153 1155
1154 1156 $ cd ..
1155 1157
1156 1158 #endif
@@ -1,726 +1,726 b''
1 1 Test for the heuristic copytracing algorithm
2 2 ============================================
3 3
4 4 $ cat >> $TESTTMP/copytrace.sh << '__EOF__'
5 5 > initclient() {
6 6 > cat >> $1/.hg/hgrc <<EOF
7 7 > [experimental]
8 8 > copytrace = heuristics
9 9 > copytrace.sourcecommitlimit = -1
10 10 > EOF
11 11 > }
12 12 > __EOF__
13 13 $ . "$TESTTMP/copytrace.sh"
14 14
15 15 $ cat >> $HGRCPATH << EOF
16 16 > [extensions]
17 17 > rebase=
18 18 > [alias]
19 19 > l = log -G -T 'rev: {rev}\ndesc: {desc}\n'
20 20 > pl = log -G -T 'rev: {rev}, phase: {phase}\ndesc: {desc}\n'
21 21 > EOF
22 22
23 23 NOTE: calling initclient() set copytrace.sourcecommitlimit=-1 as we want to
24 24 prevent the full copytrace algorithm to run and test the heuristic algorithm
25 25 without complexing the test cases with public and draft commits.
26 26
27 27 Check filename heuristics (same dirname and same basename)
28 28 ----------------------------------------------------------
29 29
30 30 $ hg init repo
31 31 $ initclient repo
32 32 $ cd repo
33 33 $ echo a > a
34 34 $ mkdir dir
35 35 $ echo a > dir/file.txt
36 36 $ hg addremove
37 37 adding a
38 38 adding dir/file.txt
39 39 $ hg ci -m initial
40 40 $ hg mv a b
41 41 $ hg mv -q dir dir2
42 42 $ hg ci -m 'mv a b, mv dir/ dir2/'
43 43 $ hg up -q 0
44 44 $ echo b > a
45 45 $ echo b > dir/file.txt
46 46 $ hg ci -qm 'mod a, mod dir/file.txt'
47 47
48 48 $ hg l
49 49 @ rev: 2
50 50 | desc: mod a, mod dir/file.txt
51 51 | o rev: 1
52 52 |/ desc: mv a b, mv dir/ dir2/
53 53 o rev: 0
54 54 desc: initial
55 55
56 56 $ hg rebase -s . -d 1
57 57 rebasing 2:557f403c0afd tip "mod a, mod dir/file.txt"
58 58 merging b and a to b
59 59 merging dir2/file.txt and dir/file.txt to dir2/file.txt
60 60 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/557f403c0afd-9926eeff-rebase.hg
61 61 $ cd ..
62 62 $ rm -rf repo
63 63
64 64 Make sure filename heuristics do not when they are not related
65 65 --------------------------------------------------------------
66 66
67 67 $ hg init repo
68 68 $ initclient repo
69 69 $ cd repo
70 70 $ echo 'somecontent' > a
71 71 $ hg add a
72 72 $ hg ci -m initial
73 73 $ hg rm a
74 74 $ echo 'completelydifferentcontext' > b
75 75 $ hg add b
76 76 $ hg ci -m 'rm a, add b'
77 77 $ hg up -q 0
78 78 $ printf 'somecontent\nmoarcontent' > a
79 79 $ hg ci -qm 'mode a'
80 80
81 81 $ hg l
82 82 @ rev: 2
83 83 | desc: mode a
84 84 | o rev: 1
85 85 |/ desc: rm a, add b
86 86 o rev: 0
87 87 desc: initial
88 88
89 89 $ hg rebase -s . -d 1
90 90 rebasing 2:d526312210b9 tip "mode a"
91 91 file 'a' was deleted in local [dest] but was modified in other [source].
92 92 You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.
93 93 What do you want to do? u
94 94 unresolved conflicts (see 'hg resolve', then 'hg rebase --continue')
95 [1]
95 [240]
96 96
97 97 $ cd ..
98 98 $ rm -rf repo
99 99
100 100 Test when lca didn't modified the file that was moved
101 101 -----------------------------------------------------
102 102
103 103 $ hg init repo
104 104 $ initclient repo
105 105 $ cd repo
106 106 $ echo 'somecontent' > a
107 107 $ hg add a
108 108 $ hg ci -m initial
109 109 $ echo c > c
110 110 $ hg add c
111 111 $ hg ci -m randomcommit
112 112 $ hg mv a b
113 113 $ hg ci -m 'mv a b'
114 114 $ hg up -q 1
115 115 $ echo b > a
116 116 $ hg ci -qm 'mod a'
117 117
118 118 $ hg pl
119 119 @ rev: 3, phase: draft
120 120 | desc: mod a
121 121 | o rev: 2, phase: draft
122 122 |/ desc: mv a b
123 123 o rev: 1, phase: draft
124 124 | desc: randomcommit
125 125 o rev: 0, phase: draft
126 126 desc: initial
127 127
128 128 $ hg rebase -s . -d 2
129 129 rebasing 3:9d5cf99c3d9f tip "mod a"
130 130 merging b and a to b
131 131 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/9d5cf99c3d9f-f02358cc-rebase.hg
132 132 $ cd ..
133 133 $ rm -rf repo
134 134
135 135 Rebase "backwards"
136 136 ------------------
137 137
138 138 $ hg init repo
139 139 $ initclient repo
140 140 $ cd repo
141 141 $ echo 'somecontent' > a
142 142 $ hg add a
143 143 $ hg ci -m initial
144 144 $ echo c > c
145 145 $ hg add c
146 146 $ hg ci -m randomcommit
147 147 $ hg mv a b
148 148 $ hg ci -m 'mv a b'
149 149 $ hg up -q 2
150 150 $ echo b > b
151 151 $ hg ci -qm 'mod b'
152 152
153 153 $ hg l
154 154 @ rev: 3
155 155 | desc: mod b
156 156 o rev: 2
157 157 | desc: mv a b
158 158 o rev: 1
159 159 | desc: randomcommit
160 160 o rev: 0
161 161 desc: initial
162 162
163 163 $ hg rebase -s . -d 0
164 164 rebasing 3:fbe97126b396 tip "mod b"
165 165 merging a and b to a
166 166 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/fbe97126b396-cf5452a1-rebase.hg
167 167 $ cd ..
168 168 $ rm -rf repo
169 169
170 170 Check a few potential move candidates
171 171 -------------------------------------
172 172
173 173 $ hg init repo
174 174 $ initclient repo
175 175 $ cd repo
176 176 $ mkdir dir
177 177 $ echo a > dir/a
178 178 $ hg add dir/a
179 179 $ hg ci -qm initial
180 180 $ hg mv dir/a dir/b
181 181 $ hg ci -qm 'mv dir/a dir/b'
182 182 $ mkdir dir2
183 183 $ echo b > dir2/a
184 184 $ hg add dir2/a
185 185 $ hg ci -qm 'create dir2/a'
186 186 $ hg up -q 0
187 187 $ echo b > dir/a
188 188 $ hg ci -qm 'mod dir/a'
189 189
190 190 $ hg l
191 191 @ rev: 3
192 192 | desc: mod dir/a
193 193 | o rev: 2
194 194 | | desc: create dir2/a
195 195 | o rev: 1
196 196 |/ desc: mv dir/a dir/b
197 197 o rev: 0
198 198 desc: initial
199 199
200 200 $ hg rebase -s . -d 2
201 201 rebasing 3:6b2f4cece40f tip "mod dir/a"
202 202 merging dir/b and dir/a to dir/b
203 203 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/6b2f4cece40f-503efe60-rebase.hg
204 204 $ cd ..
205 205 $ rm -rf repo
206 206
207 207 Test the copytrace.movecandidateslimit with many move candidates
208 208 ----------------------------------------------------------------
209 209
210 210 $ hg init repo
211 211 $ initclient repo
212 212 $ cd repo
213 213 $ echo a > a
214 214 $ hg add a
215 215 $ hg ci -m initial
216 216 $ hg mv a foo
217 217 $ echo a > b
218 218 $ echo a > c
219 219 $ echo a > d
220 220 $ echo a > e
221 221 $ echo a > f
222 222 $ echo a > g
223 223 $ hg add b
224 224 $ hg add c
225 225 $ hg add d
226 226 $ hg add e
227 227 $ hg add f
228 228 $ hg add g
229 229 $ hg ci -m 'mv a foo, add many files'
230 230 $ hg up -q ".^"
231 231 $ echo b > a
232 232 $ hg ci -m 'mod a'
233 233 created new head
234 234
235 235 $ hg l
236 236 @ rev: 2
237 237 | desc: mod a
238 238 | o rev: 1
239 239 |/ desc: mv a foo, add many files
240 240 o rev: 0
241 241 desc: initial
242 242
243 243 With small limit
244 244
245 245 $ hg rebase -s 2 -d 1 --config experimental.copytrace.movecandidateslimit=0
246 246 rebasing 2:ef716627c70b tip "mod a"
247 247 skipping copytracing for 'a', more candidates than the limit: 7
248 248 file 'a' was deleted in local [dest] but was modified in other [source].
249 249 You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.
250 250 What do you want to do? u
251 251 unresolved conflicts (see 'hg resolve', then 'hg rebase --continue')
252 [1]
252 [240]
253 253
254 254 $ hg rebase --abort
255 255 rebase aborted
256 256
257 257 With default limit which is 100
258 258
259 259 $ hg rebase -s 2 -d 1
260 260 rebasing 2:ef716627c70b tip "mod a"
261 261 merging foo and a to foo
262 262 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/ef716627c70b-24681561-rebase.hg
263 263
264 264 $ cd ..
265 265 $ rm -rf repo
266 266
267 267 Move file in one branch and delete it in another
268 268 -----------------------------------------------
269 269
270 270 $ hg init repo
271 271 $ initclient repo
272 272 $ cd repo
273 273 $ echo a > a
274 274 $ hg add a
275 275 $ hg ci -m initial
276 276 $ hg mv a b
277 277 $ hg ci -m 'mv a b'
278 278 $ hg up -q ".^"
279 279 $ hg rm a
280 280 $ hg ci -m 'del a'
281 281 created new head
282 282
283 283 $ hg pl
284 284 @ rev: 2, phase: draft
285 285 | desc: del a
286 286 | o rev: 1, phase: draft
287 287 |/ desc: mv a b
288 288 o rev: 0, phase: draft
289 289 desc: initial
290 290
291 291 $ hg rebase -s 1 -d 2
292 292 rebasing 1:472e38d57782 "mv a b"
293 293 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/472e38d57782-17d50e29-rebase.hg
294 294 $ hg up -q c492ed3c7e35dcd1dc938053b8adf56e2cfbd062
295 295 $ ls -A
296 296 .hg
297 297 b
298 298 $ cd ..
299 299 $ rm -rf repo
300 300
301 301 Move a directory in draft branch
302 302 --------------------------------
303 303
304 304 $ hg init repo
305 305 $ initclient repo
306 306 $ cd repo
307 307 $ mkdir dir
308 308 $ echo a > dir/a
309 309 $ hg add dir/a
310 310 $ hg ci -qm initial
311 311 $ echo b > dir/a
312 312 $ hg ci -qm 'mod dir/a'
313 313 $ hg up -q ".^"
314 314 $ hg mv -q dir/ dir2
315 315 $ hg ci -qm 'mv dir/ dir2/'
316 316
317 317 $ hg l
318 318 @ rev: 2
319 319 | desc: mv dir/ dir2/
320 320 | o rev: 1
321 321 |/ desc: mod dir/a
322 322 o rev: 0
323 323 desc: initial
324 324
325 325 $ hg rebase -s . -d 1
326 326 rebasing 2:a33d80b6e352 tip "mv dir/ dir2/"
327 327 merging dir/a and dir2/a to dir2/a
328 328 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/a33d80b6e352-fecb9ada-rebase.hg
329 329 $ cd ..
330 330 $ rm -rf server
331 331 $ rm -rf repo
332 332
333 333 Move file twice and rebase mod on top of moves
334 334 ----------------------------------------------
335 335
336 336 $ hg init repo
337 337 $ initclient repo
338 338 $ cd repo
339 339 $ echo a > a
340 340 $ hg add a
341 341 $ hg ci -m initial
342 342 $ hg mv a b
343 343 $ hg ci -m 'mv a b'
344 344 $ hg mv b c
345 345 $ hg ci -m 'mv b c'
346 346 $ hg up -q 0
347 347 $ echo c > a
348 348 $ hg ci -m 'mod a'
349 349 created new head
350 350
351 351 $ hg l
352 352 @ rev: 3
353 353 | desc: mod a
354 354 | o rev: 2
355 355 | | desc: mv b c
356 356 | o rev: 1
357 357 |/ desc: mv a b
358 358 o rev: 0
359 359 desc: initial
360 360 $ hg rebase -s . -d 2
361 361 rebasing 3:d41316942216 tip "mod a"
362 362 merging c and a to c
363 363 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/d41316942216-2b5949bc-rebase.hg
364 364
365 365 $ cd ..
366 366 $ rm -rf repo
367 367
368 368 Move file twice and rebase moves on top of mods
369 369 -----------------------------------------------
370 370
371 371 $ hg init repo
372 372 $ initclient repo
373 373 $ cd repo
374 374 $ echo a > a
375 375 $ hg add a
376 376 $ hg ci -m initial
377 377 $ hg mv a b
378 378 $ hg ci -m 'mv a b'
379 379 $ hg mv b c
380 380 $ hg ci -m 'mv b c'
381 381 $ hg up -q 0
382 382 $ echo c > a
383 383 $ hg ci -m 'mod a'
384 384 created new head
385 385 $ hg l
386 386 @ rev: 3
387 387 | desc: mod a
388 388 | o rev: 2
389 389 | | desc: mv b c
390 390 | o rev: 1
391 391 |/ desc: mv a b
392 392 o rev: 0
393 393 desc: initial
394 394 $ hg rebase -s 1 -d .
395 395 rebasing 1:472e38d57782 "mv a b"
396 396 merging a and b to b
397 397 rebasing 2:d3efd280421d "mv b c"
398 398 merging b and c to c
399 399 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/472e38d57782-ab8d3c58-rebase.hg
400 400
401 401 $ cd ..
402 402 $ rm -rf repo
403 403
404 404 Move one file and add another file in the same folder in one branch, modify file in another branch
405 405 --------------------------------------------------------------------------------------------------
406 406
407 407 $ hg init repo
408 408 $ initclient repo
409 409 $ cd repo
410 410 $ echo a > a
411 411 $ hg add a
412 412 $ hg ci -m initial
413 413 $ hg mv a b
414 414 $ hg ci -m 'mv a b'
415 415 $ echo c > c
416 416 $ hg add c
417 417 $ hg ci -m 'add c'
418 418 $ hg up -q 0
419 419 $ echo b > a
420 420 $ hg ci -m 'mod a'
421 421 created new head
422 422
423 423 $ hg l
424 424 @ rev: 3
425 425 | desc: mod a
426 426 | o rev: 2
427 427 | | desc: add c
428 428 | o rev: 1
429 429 |/ desc: mv a b
430 430 o rev: 0
431 431 desc: initial
432 432
433 433 $ hg rebase -s . -d 2
434 434 rebasing 3:ef716627c70b tip "mod a"
435 435 merging b and a to b
436 436 saved backup bundle to $TESTTMP/repo/.hg/strip-backup/ef716627c70b-24681561-rebase.hg
437 437 $ ls -A
438 438 .hg
439 439 b
440 440 c
441 441 $ cat b
442 442 b
443 443 $ rm -rf repo
444 444
445 445 Merge test
446 446 ----------
447 447
448 448 $ hg init repo
449 449 $ initclient repo
450 450 $ cd repo
451 451 $ echo a > a
452 452 $ hg add a
453 453 $ hg ci -m initial
454 454 $ echo b > a
455 455 $ hg ci -m 'modify a'
456 456 $ hg up -q 0
457 457 $ hg mv a b
458 458 $ hg ci -m 'mv a b'
459 459 created new head
460 460 $ hg up -q 2
461 461
462 462 $ hg l
463 463 @ rev: 2
464 464 | desc: mv a b
465 465 | o rev: 1
466 466 |/ desc: modify a
467 467 o rev: 0
468 468 desc: initial
469 469
470 470 $ hg merge 1
471 471 merging b and a to b
472 472 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
473 473 (branch merge, don't forget to commit)
474 474 $ hg ci -m merge
475 475 $ ls -A
476 476 .hg
477 477 b
478 478 $ cd ..
479 479 $ rm -rf repo
480 480
481 481 Copy and move file
482 482 ------------------
483 483
484 484 $ hg init repo
485 485 $ initclient repo
486 486 $ cd repo
487 487 $ echo a > a
488 488 $ hg add a
489 489 $ hg ci -m initial
490 490 $ hg cp a c
491 491 $ hg mv a b
492 492 $ hg ci -m 'cp a c, mv a b'
493 493 $ hg up -q 0
494 494 $ echo b > a
495 495 $ hg ci -m 'mod a'
496 496 created new head
497 497
498 498 $ hg l
499 499 @ rev: 2
500 500 | desc: mod a
501 501 | o rev: 1
502 502 |/ desc: cp a c, mv a b
503 503 o rev: 0
504 504 desc: initial
505 505
506 506 $ hg rebase -s . -d 1
507 507 rebasing 2:ef716627c70b tip "mod a"
508 508 merging b and a to b
509 509 merging c and a to c
510 510 saved backup bundle to $TESTTMP/repo/repo/.hg/strip-backup/ef716627c70b-24681561-rebase.hg
511 511 $ ls -A
512 512 .hg
513 513 b
514 514 c
515 515 $ cat b
516 516 b
517 517 $ cat c
518 518 b
519 519 $ cd ..
520 520 $ rm -rf repo
521 521
522 522 Do a merge commit with many consequent moves in one branch
523 523 ----------------------------------------------------------
524 524
525 525 $ hg init repo
526 526 $ initclient repo
527 527 $ cd repo
528 528 $ echo a > a
529 529 $ hg add a
530 530 $ hg ci -m initial
531 531 $ echo b > a
532 532 $ hg ci -qm 'mod a'
533 533 $ hg up -q ".^"
534 534 $ hg mv a b
535 535 $ hg ci -qm 'mv a b'
536 536 $ hg mv b c
537 537 $ hg ci -qm 'mv b c'
538 538 $ hg up -q 1
539 539 $ hg l
540 540 o rev: 3
541 541 | desc: mv b c
542 542 o rev: 2
543 543 | desc: mv a b
544 544 | @ rev: 1
545 545 |/ desc: mod a
546 546 o rev: 0
547 547 desc: initial
548 548
549 549 $ hg merge 3
550 550 merging a and c to c
551 551 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
552 552 (branch merge, don't forget to commit)
553 553 $ hg ci -qm 'merge'
554 554 $ hg pl
555 555 @ rev: 4, phase: draft
556 556 |\ desc: merge
557 557 | o rev: 3, phase: draft
558 558 | | desc: mv b c
559 559 | o rev: 2, phase: draft
560 560 | | desc: mv a b
561 561 o | rev: 1, phase: draft
562 562 |/ desc: mod a
563 563 o rev: 0, phase: draft
564 564 desc: initial
565 565 $ ls -A
566 566 .hg
567 567 c
568 568 $ cd ..
569 569 $ rm -rf repo
570 570
571 571 Test shelve/unshelve
572 572 -------------------
573 573
574 574 $ hg init repo
575 575 $ initclient repo
576 576 $ cd repo
577 577 $ echo a > a
578 578 $ hg add a
579 579 $ hg ci -m initial
580 580 $ echo b > a
581 581 $ hg shelve
582 582 shelved as default
583 583 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
584 584 $ hg mv a b
585 585 $ hg ci -m 'mv a b'
586 586
587 587 $ hg l
588 588 @ rev: 1
589 589 | desc: mv a b
590 590 o rev: 0
591 591 desc: initial
592 592 $ hg unshelve
593 593 unshelving change 'default'
594 594 rebasing shelved changes
595 595 merging b and a to b
596 596 $ ls -A
597 597 .hg
598 598 b
599 599 $ cat b
600 600 b
601 601 $ cd ..
602 602 $ rm -rf repo
603 603
604 604 Test full copytrace ability on draft branch
605 605 -------------------------------------------
606 606
607 607 File directory and base name changed in same move
608 608 $ hg init repo
609 609 $ initclient repo
610 610 $ mkdir repo/dir1
611 611 $ cd repo/dir1
612 612 $ echo a > a
613 613 $ hg add a
614 614 $ hg ci -qm initial
615 615 $ cd ..
616 616 $ hg mv -q dir1 dir2
617 617 $ hg mv dir2/a dir2/b
618 618 $ hg ci -qm 'mv a b; mv dir1 dir2'
619 619 $ hg up -q '.^'
620 620 $ cd dir1
621 621 $ echo b >> a
622 622 $ cd ..
623 623 $ hg ci -qm 'mod a'
624 624
625 625 $ hg pl
626 626 @ rev: 2, phase: draft
627 627 | desc: mod a
628 628 | o rev: 1, phase: draft
629 629 |/ desc: mv a b; mv dir1 dir2
630 630 o rev: 0, phase: draft
631 631 desc: initial
632 632
633 633 $ hg rebase -s . -d 1 --config experimental.copytrace.sourcecommitlimit=100
634 634 rebasing 2:6207d2d318e7 tip "mod a"
635 635 merging dir2/b and dir1/a to dir2/b
636 636 saved backup bundle to $TESTTMP/repo/repo/.hg/strip-backup/6207d2d318e7-1c9779ad-rebase.hg
637 637 $ cat dir2/b
638 638 a
639 639 b
640 640 $ cd ..
641 641 $ rm -rf repo
642 642
643 643 Move directory in one merge parent, while adding file to original directory
644 644 in other merge parent. File moved on rebase.
645 645
646 646 $ hg init repo
647 647 $ initclient repo
648 648 $ mkdir repo/dir1
649 649 $ cd repo/dir1
650 650 $ echo dummy > dummy
651 651 $ hg add dummy
652 652 $ cd ..
653 653 $ hg ci -qm initial
654 654 $ cd dir1
655 655 $ echo a > a
656 656 $ hg add a
657 657 $ cd ..
658 658 $ hg ci -qm 'hg add dir1/a'
659 659 $ hg up -q '.^'
660 660 $ hg mv -q dir1 dir2
661 661 $ hg ci -qm 'mv dir1 dir2'
662 662
663 663 $ hg pl
664 664 @ rev: 2, phase: draft
665 665 | desc: mv dir1 dir2
666 666 | o rev: 1, phase: draft
667 667 |/ desc: hg add dir1/a
668 668 o rev: 0, phase: draft
669 669 desc: initial
670 670
671 671 $ hg rebase -s . -d 1 --config experimental.copytrace.sourcecommitlimit=100
672 672 rebasing 2:e8919e7df8d0 tip "mv dir1 dir2"
673 673 saved backup bundle to $TESTTMP/repo/repo/.hg/strip-backup/e8919e7df8d0-f62fab62-rebase.hg
674 674 $ ls dir2
675 675 a
676 676 dummy
677 677 $ rm -rf repo
678 678
679 679 Testing the sourcecommitlimit config
680 680 -----------------------------------
681 681
682 682 $ hg init repo
683 683 $ initclient repo
684 684 $ cd repo
685 685 $ echo a > a
686 686 $ hg ci -Aqm "added a"
687 687 $ echo "more things" >> a
688 688 $ hg ci -qm "added more things to a"
689 689 $ hg up 0
690 690 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
691 691 $ echo b > b
692 692 $ hg ci -Aqm "added b"
693 693 $ mkdir foo
694 694 $ hg mv a foo/bar
695 695 $ hg ci -m "Moved a to foo/bar"
696 696 $ hg pl
697 697 @ rev: 3, phase: draft
698 698 | desc: Moved a to foo/bar
699 699 o rev: 2, phase: draft
700 700 | desc: added b
701 701 | o rev: 1, phase: draft
702 702 |/ desc: added more things to a
703 703 o rev: 0, phase: draft
704 704 desc: added a
705 705
706 706 When the sourcecommitlimit is small and we have more drafts, we use heuristics only
707 707
708 708 $ hg rebase -s 1 -d .
709 709 rebasing 1:8b6e13696c38 "added more things to a"
710 710 file 'a' was deleted in local [dest] but was modified in other [source].
711 711 You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.
712 712 What do you want to do? u
713 713 unresolved conflicts (see 'hg resolve', then 'hg rebase --continue')
714 [1]
714 [240]
715 715
716 716 But when we have "sourcecommitlimit > (no. of drafts from base to c1)", we do
717 717 fullcopytracing
718 718
719 719 $ hg rebase --abort
720 720 rebase aborted
721 721 $ hg rebase -s 1 -d . --config experimental.copytrace.sourcecommitlimit=100
722 722 rebasing 1:8b6e13696c38 "added more things to a"
723 723 merging foo/bar and a to foo/bar
724 724 saved backup bundle to $TESTTMP/repo/repo/repo/.hg/strip-backup/8b6e13696c38-fc14ac83-rebase.hg
725 725 $ cd ..
726 726 $ rm -rf repo
@@ -1,1693 +1,1693 b''
1 1 A script that implements uppercasing of specific lines in a file. This
2 2 approximates the behavior of code formatters well enough for our tests.
3 3
4 4 $ UPPERCASEPY="$TESTTMP/uppercase.py"
5 5 $ cat > $UPPERCASEPY <<EOF
6 6 > import sys
7 7 > from mercurial.utils.procutil import setbinary
8 8 > setbinary(sys.stdin)
9 9 > setbinary(sys.stdout)
10 10 > lines = set()
11 11 > for arg in sys.argv[1:]:
12 12 > if arg == 'all':
13 13 > sys.stdout.write(sys.stdin.read().upper())
14 14 > sys.exit(0)
15 15 > else:
16 16 > first, last = arg.split('-')
17 17 > lines.update(range(int(first), int(last) + 1))
18 18 > for i, line in enumerate(sys.stdin.readlines()):
19 19 > if i + 1 in lines:
20 20 > sys.stdout.write(line.upper())
21 21 > else:
22 22 > sys.stdout.write(line)
23 23 > EOF
24 24 $ TESTLINES="foo\nbar\nbaz\nqux\n"
25 25 $ printf $TESTLINES | "$PYTHON" $UPPERCASEPY
26 26 foo
27 27 bar
28 28 baz
29 29 qux
30 30 $ printf $TESTLINES | "$PYTHON" $UPPERCASEPY all
31 31 FOO
32 32 BAR
33 33 BAZ
34 34 QUX
35 35 $ printf $TESTLINES | "$PYTHON" $UPPERCASEPY 1-1
36 36 FOO
37 37 bar
38 38 baz
39 39 qux
40 40 $ printf $TESTLINES | "$PYTHON" $UPPERCASEPY 1-2
41 41 FOO
42 42 BAR
43 43 baz
44 44 qux
45 45 $ printf $TESTLINES | "$PYTHON" $UPPERCASEPY 2-3
46 46 foo
47 47 BAR
48 48 BAZ
49 49 qux
50 50 $ printf $TESTLINES | "$PYTHON" $UPPERCASEPY 2-2 4-4
51 51 foo
52 52 BAR
53 53 baz
54 54 QUX
55 55
56 56 Set up the config with two simple fixers: one that fixes specific line ranges,
57 57 and one that always fixes the whole file. They both "fix" files by converting
58 58 letters to uppercase. They use different file extensions, so each test case can
59 59 choose which behavior to use by naming files.
60 60
61 61 $ cat >> $HGRCPATH <<EOF
62 62 > [extensions]
63 63 > fix =
64 64 > [experimental]
65 65 > evolution.createmarkers=True
66 66 > evolution.allowunstable=True
67 67 > [fix]
68 68 > uppercase-whole-file:command="$PYTHON" $UPPERCASEPY all
69 69 > uppercase-whole-file:pattern=set:**.whole
70 70 > uppercase-changed-lines:command="$PYTHON" $UPPERCASEPY
71 71 > uppercase-changed-lines:linerange={first}-{last}
72 72 > uppercase-changed-lines:pattern=set:**.changed
73 73 > EOF
74 74
75 75 Help text for fix.
76 76
77 77 $ hg help fix
78 78 hg fix [OPTION]... [FILE]...
79 79
80 80 rewrite file content in changesets or working directory
81 81
82 82 Runs any configured tools to fix the content of files. Only affects files
83 83 with changes, unless file arguments are provided. Only affects changed
84 84 lines of files, unless the --whole flag is used. Some tools may always
85 85 affect the whole file regardless of --whole.
86 86
87 87 If --working-dir is used, files with uncommitted changes in the working
88 88 copy will be fixed. Note that no backup are made.
89 89
90 90 If revisions are specified with --source, those revisions and their
91 91 descendants will be checked, and they may be replaced with new revisions
92 92 that have fixed file content. By automatically including the descendants,
93 93 no merging, rebasing, or evolution will be required. If an ancestor of the
94 94 working copy is included, then the working copy itself will also be fixed,
95 95 and the working copy will be updated to the fixed parent.
96 96
97 97 When determining what lines of each file to fix at each revision, the
98 98 whole set of revisions being fixed is considered, so that fixes to earlier
99 99 revisions are not forgotten in later ones. The --base flag can be used to
100 100 override this default behavior, though it is not usually desirable to do
101 101 so.
102 102
103 103 (use 'hg help -e fix' to show help for the fix extension)
104 104
105 105 options ([+] can be repeated):
106 106
107 107 --all fix all non-public non-obsolete revisions
108 108 --base REV [+] revisions to diff against (overrides automatic selection,
109 109 and applies to every revision being fixed)
110 110 -s --source REV [+] fix the specified revisions and their descendants
111 111 -w --working-dir fix the working directory
112 112 --whole always fix every line of a file
113 113
114 114 (some details hidden, use --verbose to show complete help)
115 115
116 116 $ hg help -e fix
117 117 fix extension - rewrite file content in changesets or working copy
118 118 (EXPERIMENTAL)
119 119
120 120 Provides a command that runs configured tools on the contents of modified
121 121 files, writing back any fixes to the working copy or replacing changesets.
122 122
123 123 Here is an example configuration that causes 'hg fix' to apply automatic
124 124 formatting fixes to modified lines in C++ code:
125 125
126 126 [fix]
127 127 clang-format:command=clang-format --assume-filename={rootpath}
128 128 clang-format:linerange=--lines={first}:{last}
129 129 clang-format:pattern=set:**.cpp or **.hpp
130 130
131 131 The :command suboption forms the first part of the shell command that will be
132 132 used to fix a file. The content of the file is passed on standard input, and
133 133 the fixed file content is expected on standard output. Any output on standard
134 134 error will be displayed as a warning. If the exit status is not zero, the file
135 135 will not be affected. A placeholder warning is displayed if there is a non-
136 136 zero exit status but no standard error output. Some values may be substituted
137 137 into the command:
138 138
139 139 {rootpath} The path of the file being fixed, relative to the repo root
140 140 {basename} The name of the file being fixed, without the directory path
141 141
142 142 If the :linerange suboption is set, the tool will only be run if there are
143 143 changed lines in a file. The value of this suboption is appended to the shell
144 144 command once for every range of changed lines in the file. Some values may be
145 145 substituted into the command:
146 146
147 147 {first} The 1-based line number of the first line in the modified range
148 148 {last} The 1-based line number of the last line in the modified range
149 149
150 150 Deleted sections of a file will be ignored by :linerange, because there is no
151 151 corresponding line range in the version being fixed.
152 152
153 153 By default, tools that set :linerange will only be executed if there is at
154 154 least one changed line range. This is meant to prevent accidents like running
155 155 a code formatter in such a way that it unexpectedly reformats the whole file.
156 156 If such a tool needs to operate on unchanged files, it should set the
157 157 :skipclean suboption to false.
158 158
159 159 The :pattern suboption determines which files will be passed through each
160 160 configured tool. See 'hg help patterns' for possible values. However, all
161 161 patterns are relative to the repo root, even if that text says they are
162 162 relative to the current working directory. If there are file arguments to 'hg
163 163 fix', the intersection of these patterns is used.
164 164
165 165 There is also a configurable limit for the maximum size of file that will be
166 166 processed by 'hg fix':
167 167
168 168 [fix]
169 169 maxfilesize = 2MB
170 170
171 171 Normally, execution of configured tools will continue after a failure
172 172 (indicated by a non-zero exit status). It can also be configured to abort
173 173 after the first such failure, so that no files will be affected if any tool
174 174 fails. This abort will also cause 'hg fix' to exit with a non-zero status:
175 175
176 176 [fix]
177 177 failure = abort
178 178
179 179 When multiple tools are configured to affect a file, they execute in an order
180 180 defined by the :priority suboption. The priority suboption has a default value
181 181 of zero for each tool. Tools are executed in order of descending priority. The
182 182 execution order of tools with equal priority is unspecified. For example, you
183 183 could use the 'sort' and 'head' utilities to keep only the 10 smallest numbers
184 184 in a text file by ensuring that 'sort' runs before 'head':
185 185
186 186 [fix]
187 187 sort:command = sort -n
188 188 head:command = head -n 10
189 189 sort:pattern = numbers.txt
190 190 head:pattern = numbers.txt
191 191 sort:priority = 2
192 192 head:priority = 1
193 193
194 194 To account for changes made by each tool, the line numbers used for
195 195 incremental formatting are recomputed before executing the next tool. So, each
196 196 tool may see different values for the arguments added by the :linerange
197 197 suboption.
198 198
199 199 Each fixer tool is allowed to return some metadata in addition to the fixed
200 200 file content. The metadata must be placed before the file content on stdout,
201 201 separated from the file content by a zero byte. The metadata is parsed as a
202 202 JSON value (so, it should be UTF-8 encoded and contain no zero bytes). A fixer
203 203 tool is expected to produce this metadata encoding if and only if the
204 204 :metadata suboption is true:
205 205
206 206 [fix]
207 207 tool:command = tool --prepend-json-metadata
208 208 tool:metadata = true
209 209
210 210 The metadata values are passed to hooks, which can be used to print summaries
211 211 or perform other post-fixing work. The supported hooks are:
212 212
213 213 "postfixfile"
214 214 Run once for each file in each revision where any fixer tools made changes
215 215 to the file content. Provides "$HG_REV" and "$HG_PATH" to identify the file,
216 216 and "$HG_METADATA" with a map of fixer names to metadata values from fixer
217 217 tools that affected the file. Fixer tools that didn't affect the file have a
218 218 value of None. Only fixer tools that executed are present in the metadata.
219 219
220 220 "postfix"
221 221 Run once after all files and revisions have been handled. Provides
222 222 "$HG_REPLACEMENTS" with information about what revisions were created and
223 223 made obsolete. Provides a boolean "$HG_WDIRWRITTEN" to indicate whether any
224 224 files in the working copy were updated. Provides a list "$HG_METADATA"
225 225 mapping fixer tool names to lists of metadata values returned from
226 226 executions that modified a file. This aggregates the same metadata
227 227 previously passed to the "postfixfile" hook.
228 228
229 229 Fixer tools are run in the repository's root directory. This allows them to
230 230 read configuration files from the working copy, or even write to the working
231 231 copy. The working copy is not updated to match the revision being fixed. In
232 232 fact, several revisions may be fixed in parallel. Writes to the working copy
233 233 are not amended into the revision being fixed; fixer tools should always write
234 234 fixed file content back to stdout as documented above.
235 235
236 236 list of commands:
237 237
238 238 fix rewrite file content in changesets or working directory
239 239
240 240 (use 'hg help -v -e fix' to show built-in aliases and global options)
241 241
242 242 There is no default behavior in the absence of --rev and --working-dir.
243 243
244 244 $ hg init badusage
245 245 $ cd badusage
246 246
247 247 $ hg fix
248 248 abort: no changesets specified
249 249 (use --source or --working-dir)
250 250 [255]
251 251 $ hg fix --whole
252 252 abort: no changesets specified
253 253 (use --source or --working-dir)
254 254 [255]
255 255 $ hg fix --base 0
256 256 abort: no changesets specified
257 257 (use --source or --working-dir)
258 258 [255]
259 259
260 260 Fixing a public revision isn't allowed. It should abort early enough that
261 261 nothing happens, even to the working directory.
262 262
263 263 $ printf "hello\n" > hello.whole
264 264 $ hg commit -Aqm "hello"
265 265 $ hg phase -r 0 --public
266 266 $ hg fix -r 0
267 267 abort: cannot fix public changesets
268 268 (see 'hg help phases' for details)
269 269 [255]
270 270 $ hg fix -r 0 --working-dir
271 271 abort: cannot fix public changesets
272 272 (see 'hg help phases' for details)
273 273 [255]
274 274 $ hg cat -r tip hello.whole
275 275 hello
276 276 $ cat hello.whole
277 277 hello
278 278
279 279 $ cd ..
280 280
281 281 Fixing a clean working directory should do nothing. Even the --whole flag
282 282 shouldn't cause any clean files to be fixed. Specifying a clean file explicitly
283 283 should only fix it if the fixer always fixes the whole file. The combination of
284 284 an explicit filename and --whole should format the entire file regardless.
285 285
286 286 $ hg init fixcleanwdir
287 287 $ cd fixcleanwdir
288 288
289 289 $ printf "hello\n" > hello.changed
290 290 $ printf "world\n" > hello.whole
291 291 $ hg commit -Aqm "foo"
292 292 $ hg fix --working-dir
293 293 $ hg diff
294 294 $ hg fix --working-dir --whole
295 295 $ hg diff
296 296 $ hg fix --working-dir *
297 297 $ cat *
298 298 hello
299 299 WORLD
300 300 $ hg revert --all --no-backup
301 301 reverting hello.whole
302 302 $ hg fix --working-dir * --whole
303 303 $ cat *
304 304 HELLO
305 305 WORLD
306 306
307 307 The same ideas apply to fixing a revision, so we create a revision that doesn't
308 308 modify either of the files in question and try fixing it. This also tests that
309 309 we ignore a file that doesn't match any configured fixer.
310 310
311 311 $ hg revert --all --no-backup
312 312 reverting hello.changed
313 313 reverting hello.whole
314 314 $ printf "unimportant\n" > some.file
315 315 $ hg commit -Aqm "some other file"
316 316
317 317 $ hg fix -r .
318 318 $ hg cat -r tip *
319 319 hello
320 320 world
321 321 unimportant
322 322 $ hg fix -r . --whole
323 323 $ hg cat -r tip *
324 324 hello
325 325 world
326 326 unimportant
327 327 $ hg fix -r . *
328 328 $ hg cat -r tip *
329 329 hello
330 330 WORLD
331 331 unimportant
332 332 $ hg fix -r . * --whole --config experimental.evolution.allowdivergence=true
333 333 2 new content-divergent changesets
334 334 $ hg cat -r tip *
335 335 HELLO
336 336 WORLD
337 337 unimportant
338 338
339 339 $ cd ..
340 340
341 341 Fixing the working directory should still work if there are no revisions.
342 342
343 343 $ hg init norevisions
344 344 $ cd norevisions
345 345
346 346 $ printf "something\n" > something.whole
347 347 $ hg add
348 348 adding something.whole
349 349 $ hg fix --working-dir
350 350 $ cat something.whole
351 351 SOMETHING
352 352
353 353 $ cd ..
354 354
355 355 Test the effect of fixing the working directory for each possible status, with
356 356 and without providing explicit file arguments.
357 357
358 358 $ hg init implicitlyfixstatus
359 359 $ cd implicitlyfixstatus
360 360
361 361 $ printf "modified\n" > modified.whole
362 362 $ printf "removed\n" > removed.whole
363 363 $ printf "deleted\n" > deleted.whole
364 364 $ printf "clean\n" > clean.whole
365 365 $ printf "ignored.whole" > .hgignore
366 366 $ hg commit -Aqm "stuff"
367 367
368 368 $ printf "modified!!!\n" > modified.whole
369 369 $ printf "unknown\n" > unknown.whole
370 370 $ printf "ignored\n" > ignored.whole
371 371 $ printf "added\n" > added.whole
372 372 $ hg add added.whole
373 373 $ hg remove removed.whole
374 374 $ rm deleted.whole
375 375
376 376 $ hg status --all
377 377 M modified.whole
378 378 A added.whole
379 379 R removed.whole
380 380 ! deleted.whole
381 381 ? unknown.whole
382 382 I ignored.whole
383 383 C .hgignore
384 384 C clean.whole
385 385
386 386 $ hg fix --working-dir
387 387
388 388 $ hg status --all
389 389 M modified.whole
390 390 A added.whole
391 391 R removed.whole
392 392 ! deleted.whole
393 393 ? unknown.whole
394 394 I ignored.whole
395 395 C .hgignore
396 396 C clean.whole
397 397
398 398 $ cat *.whole
399 399 ADDED
400 400 clean
401 401 ignored
402 402 MODIFIED!!!
403 403 unknown
404 404
405 405 $ printf "modified!!!\n" > modified.whole
406 406 $ printf "added\n" > added.whole
407 407
408 408 Listing the files explicitly causes untracked files to also be fixed, but
409 409 ignored files are still unaffected.
410 410
411 411 $ hg fix --working-dir *.whole
412 412
413 413 $ hg status --all
414 414 M clean.whole
415 415 M modified.whole
416 416 A added.whole
417 417 R removed.whole
418 418 ! deleted.whole
419 419 ? unknown.whole
420 420 I ignored.whole
421 421 C .hgignore
422 422
423 423 $ cat *.whole
424 424 ADDED
425 425 CLEAN
426 426 ignored
427 427 MODIFIED!!!
428 428 UNKNOWN
429 429
430 430 $ cd ..
431 431
432 432 Test that incremental fixing works on files with additions, deletions, and
433 433 changes in multiple line ranges. Note that deletions do not generally cause
434 434 neighboring lines to be fixed, so we don't return a line range for purely
435 435 deleted sections. In the future we should support a :deletion config that
436 436 allows fixers to know where deletions are located.
437 437
438 438 $ hg init incrementalfixedlines
439 439 $ cd incrementalfixedlines
440 440
441 441 $ printf "a\nb\nc\nd\ne\nf\ng\n" > foo.txt
442 442 $ hg commit -Aqm "foo"
443 443 $ printf "zz\na\nc\ndd\nee\nff\nf\ngg\n" > foo.txt
444 444
445 445 $ hg --config "fix.fail:command=echo" \
446 446 > --config "fix.fail:linerange={first}:{last}" \
447 447 > --config "fix.fail:pattern=foo.txt" \
448 448 > fix --working-dir
449 449 $ cat foo.txt
450 450 1:1 4:6 8:8
451 451
452 452 $ cd ..
453 453
454 454 Test that --whole fixes all lines regardless of the diffs present.
455 455
456 456 $ hg init wholeignoresdiffs
457 457 $ cd wholeignoresdiffs
458 458
459 459 $ printf "a\nb\nc\nd\ne\nf\ng\n" > foo.changed
460 460 $ hg commit -Aqm "foo"
461 461 $ printf "zz\na\nc\ndd\nee\nff\nf\ngg\n" > foo.changed
462 462
463 463 $ hg fix --working-dir
464 464 $ cat foo.changed
465 465 ZZ
466 466 a
467 467 c
468 468 DD
469 469 EE
470 470 FF
471 471 f
472 472 GG
473 473
474 474 $ hg fix --working-dir --whole
475 475 $ cat foo.changed
476 476 ZZ
477 477 A
478 478 C
479 479 DD
480 480 EE
481 481 FF
482 482 F
483 483 GG
484 484
485 485 $ cd ..
486 486
487 487 We should do nothing with symlinks, and their targets should be unaffected. Any
488 488 other behavior would be more complicated to implement and harder to document.
489 489
490 490 #if symlink
491 491 $ hg init dontmesswithsymlinks
492 492 $ cd dontmesswithsymlinks
493 493
494 494 $ printf "hello\n" > hello.whole
495 495 $ ln -s hello.whole hellolink
496 496 $ hg add
497 497 adding hello.whole
498 498 adding hellolink
499 499 $ hg fix --working-dir hellolink
500 500 $ hg status
501 501 A hello.whole
502 502 A hellolink
503 503
504 504 $ cd ..
505 505 #endif
506 506
507 507 We should allow fixers to run on binary files, even though this doesn't sound
508 508 like a common use case. There's not much benefit to disallowing it, and users
509 509 can add "and not binary()" to their filesets if needed. The Mercurial
510 510 philosophy is generally to not handle binary files specially anyway.
511 511
512 512 $ hg init cantouchbinaryfiles
513 513 $ cd cantouchbinaryfiles
514 514
515 515 $ printf "hello\0\n" > hello.whole
516 516 $ hg add
517 517 adding hello.whole
518 518 $ hg fix --working-dir 'set:binary()'
519 519 $ cat hello.whole
520 520 HELLO\x00 (esc)
521 521
522 522 $ cd ..
523 523
524 524 We have a config for the maximum size of file we will attempt to fix. This can
525 525 be helpful to avoid running unsuspecting fixer tools on huge inputs, which
526 526 could happen by accident without a well considered configuration. A more
527 527 precise configuration could use the size() fileset function if one global limit
528 528 is undesired.
529 529
530 530 $ hg init maxfilesize
531 531 $ cd maxfilesize
532 532
533 533 $ printf "this file is huge\n" > hello.whole
534 534 $ hg add
535 535 adding hello.whole
536 536 $ hg --config fix.maxfilesize=10 fix --working-dir
537 537 ignoring file larger than 10 bytes: hello.whole
538 538 $ cat hello.whole
539 539 this file is huge
540 540
541 541 $ cd ..
542 542
543 543 If we specify a file to fix, other files should be left alone, even if they
544 544 have changes.
545 545
546 546 $ hg init fixonlywhatitellyouto
547 547 $ cd fixonlywhatitellyouto
548 548
549 549 $ printf "fix me!\n" > fixme.whole
550 550 $ printf "not me.\n" > notme.whole
551 551 $ hg add
552 552 adding fixme.whole
553 553 adding notme.whole
554 554 $ hg fix --working-dir fixme.whole
555 555 $ cat *.whole
556 556 FIX ME!
557 557 not me.
558 558
559 559 $ cd ..
560 560
561 561 If we try to fix a missing file, we still fix other files.
562 562
563 563 $ hg init fixmissingfile
564 564 $ cd fixmissingfile
565 565
566 566 $ printf "fix me!\n" > foo.whole
567 567 $ hg add
568 568 adding foo.whole
569 569 $ hg fix --working-dir foo.whole bar.whole
570 570 bar.whole: $ENOENT$
571 571 $ cat *.whole
572 572 FIX ME!
573 573
574 574 $ cd ..
575 575
576 576 Specifying a directory name should fix all its files and subdirectories.
577 577
578 578 $ hg init fixdirectory
579 579 $ cd fixdirectory
580 580
581 581 $ mkdir -p dir1/dir2
582 582 $ printf "foo\n" > foo.whole
583 583 $ printf "bar\n" > dir1/bar.whole
584 584 $ printf "baz\n" > dir1/dir2/baz.whole
585 585 $ hg add
586 586 adding dir1/bar.whole
587 587 adding dir1/dir2/baz.whole
588 588 adding foo.whole
589 589 $ hg fix --working-dir dir1
590 590 $ cat foo.whole dir1/bar.whole dir1/dir2/baz.whole
591 591 foo
592 592 BAR
593 593 BAZ
594 594
595 595 $ cd ..
596 596
597 597 Fixing a file in the working directory that needs no fixes should not actually
598 598 write back to the file, so for example the mtime shouldn't change.
599 599
600 600 $ hg init donttouchunfixedfiles
601 601 $ cd donttouchunfixedfiles
602 602
603 603 $ printf "NO FIX NEEDED\n" > foo.whole
604 604 $ hg add
605 605 adding foo.whole
606 606 $ cp -p foo.whole foo.whole.orig
607 607 $ cp -p foo.whole.orig foo.whole
608 608 $ sleep 2 # mtime has a resolution of one or two seconds.
609 609 $ hg fix --working-dir
610 610 $ f foo.whole.orig --newer foo.whole
611 611 foo.whole.orig: newer than foo.whole
612 612
613 613 $ cd ..
614 614
615 615 When a fixer prints to stderr, we don't assume that it has failed. We show the
616 616 error messages to the user, and we still let the fixer affect the file it was
617 617 fixing if its exit code is zero. Some code formatters might emit error messages
618 618 on stderr and nothing on stdout, which would cause us the clear the file,
619 619 except that they also exit with a non-zero code. We show the user which fixer
620 620 emitted the stderr, and which revision, but we assume that the fixer will print
621 621 the filename if it is relevant (since the issue may be non-specific). There is
622 622 also a config to abort (without affecting any files whatsoever) if we see any
623 623 tool with a non-zero exit status.
624 624
625 625 $ hg init showstderr
626 626 $ cd showstderr
627 627
628 628 $ printf "hello\n" > hello.txt
629 629 $ hg add
630 630 adding hello.txt
631 631 $ cat > $TESTTMP/work.sh <<'EOF'
632 632 > printf 'HELLO\n'
633 633 > printf "$@: some\nerror that didn't stop the tool" >&2
634 634 > exit 0 # success despite the stderr output
635 635 > EOF
636 636 $ hg --config "fix.work:command=sh $TESTTMP/work.sh {rootpath}" \
637 637 > --config "fix.work:pattern=hello.txt" \
638 638 > fix --working-dir
639 639 [wdir] work: hello.txt: some
640 640 [wdir] work: error that didn't stop the tool
641 641 $ cat hello.txt
642 642 HELLO
643 643
644 644 $ printf "goodbye\n" > hello.txt
645 645 $ printf "foo\n" > foo.whole
646 646 $ hg add
647 647 adding foo.whole
648 648 $ cat > $TESTTMP/fail.sh <<'EOF'
649 649 > printf 'GOODBYE\n'
650 650 > printf "$@: some\nerror that did stop the tool\n" >&2
651 651 > exit 42 # success despite the stdout output
652 652 > EOF
653 653 $ hg --config "fix.fail:command=sh $TESTTMP/fail.sh {rootpath}" \
654 654 > --config "fix.fail:pattern=hello.txt" \
655 655 > --config "fix.failure=abort" \
656 656 > fix --working-dir
657 657 [wdir] fail: hello.txt: some
658 658 [wdir] fail: error that did stop the tool
659 659 abort: no fixes will be applied
660 660 (use --config fix.failure=continue to apply any successful fixes anyway)
661 661 [255]
662 662 $ cat hello.txt
663 663 goodbye
664 664 $ cat foo.whole
665 665 foo
666 666
667 667 $ hg --config "fix.fail:command=sh $TESTTMP/fail.sh {rootpath}" \
668 668 > --config "fix.fail:pattern=hello.txt" \
669 669 > fix --working-dir
670 670 [wdir] fail: hello.txt: some
671 671 [wdir] fail: error that did stop the tool
672 672 $ cat hello.txt
673 673 goodbye
674 674 $ cat foo.whole
675 675 FOO
676 676
677 677 $ hg --config "fix.fail:command=exit 42" \
678 678 > --config "fix.fail:pattern=hello.txt" \
679 679 > fix --working-dir
680 680 [wdir] fail: exited with status 42
681 681
682 682 $ cd ..
683 683
684 684 Fixing the working directory and its parent revision at the same time should
685 685 check out the replacement revision for the parent. This prevents any new
686 686 uncommitted changes from appearing. We test this for a clean working directory
687 687 and a dirty one. In both cases, all lines/files changed since the grandparent
688 688 will be fixed. The grandparent is the "baserev" for both the parent and the
689 689 working copy.
690 690
691 691 $ hg init fixdotandcleanwdir
692 692 $ cd fixdotandcleanwdir
693 693
694 694 $ printf "hello\n" > hello.whole
695 695 $ printf "world\n" > world.whole
696 696 $ hg commit -Aqm "the parent commit"
697 697
698 698 $ hg parents --template '{rev} {desc}\n'
699 699 0 the parent commit
700 700 $ hg fix --working-dir -r .
701 701 $ hg parents --template '{rev} {desc}\n'
702 702 1 the parent commit
703 703 $ hg cat -r . *.whole
704 704 HELLO
705 705 WORLD
706 706 $ cat *.whole
707 707 HELLO
708 708 WORLD
709 709 $ hg status
710 710
711 711 $ cd ..
712 712
713 713 Same test with a dirty working copy.
714 714
715 715 $ hg init fixdotanddirtywdir
716 716 $ cd fixdotanddirtywdir
717 717
718 718 $ printf "hello\n" > hello.whole
719 719 $ printf "world\n" > world.whole
720 720 $ hg commit -Aqm "the parent commit"
721 721
722 722 $ printf "hello,\n" > hello.whole
723 723 $ printf "world!\n" > world.whole
724 724
725 725 $ hg parents --template '{rev} {desc}\n'
726 726 0 the parent commit
727 727 $ hg fix --working-dir -r .
728 728 $ hg parents --template '{rev} {desc}\n'
729 729 1 the parent commit
730 730 $ hg cat -r . *.whole
731 731 HELLO
732 732 WORLD
733 733 $ cat *.whole
734 734 HELLO,
735 735 WORLD!
736 736 $ hg status
737 737 M hello.whole
738 738 M world.whole
739 739
740 740 $ cd ..
741 741
742 742 When we have a chain of commits that change mutually exclusive lines of code,
743 743 we should be able to do incremental fixing that causes each commit in the chain
744 744 to include fixes made to the previous commits. This prevents children from
745 745 backing out the fixes made in their parents. A dirty working directory is
746 746 conceptually similar to another commit in the chain.
747 747
748 748 $ hg init incrementallyfixchain
749 749 $ cd incrementallyfixchain
750 750
751 751 $ cat > file.changed <<EOF
752 752 > first
753 753 > second
754 754 > third
755 755 > fourth
756 756 > fifth
757 757 > EOF
758 758 $ hg commit -Aqm "the common ancestor (the baserev)"
759 759 $ cat > file.changed <<EOF
760 760 > first (changed)
761 761 > second
762 762 > third
763 763 > fourth
764 764 > fifth
765 765 > EOF
766 766 $ hg commit -Aqm "the first commit to fix"
767 767 $ cat > file.changed <<EOF
768 768 > first (changed)
769 769 > second
770 770 > third (changed)
771 771 > fourth
772 772 > fifth
773 773 > EOF
774 774 $ hg commit -Aqm "the second commit to fix"
775 775 $ cat > file.changed <<EOF
776 776 > first (changed)
777 777 > second
778 778 > third (changed)
779 779 > fourth
780 780 > fifth (changed)
781 781 > EOF
782 782
783 783 $ hg fix -r . -r '.^' --working-dir
784 784
785 785 $ hg parents --template '{rev}\n'
786 786 4
787 787 $ hg cat -r '.^^' file.changed
788 788 first
789 789 second
790 790 third
791 791 fourth
792 792 fifth
793 793 $ hg cat -r '.^' file.changed
794 794 FIRST (CHANGED)
795 795 second
796 796 third
797 797 fourth
798 798 fifth
799 799 $ hg cat -r . file.changed
800 800 FIRST (CHANGED)
801 801 second
802 802 THIRD (CHANGED)
803 803 fourth
804 804 fifth
805 805 $ cat file.changed
806 806 FIRST (CHANGED)
807 807 second
808 808 THIRD (CHANGED)
809 809 fourth
810 810 FIFTH (CHANGED)
811 811
812 812 $ cd ..
813 813
814 814 If we incrementally fix a merge commit, we should fix any lines that changed
815 815 versus either parent. You could imagine only fixing the intersection or some
816 816 other subset, but this is necessary if either parent is being fixed. It
817 817 prevents us from forgetting fixes made in either parent.
818 818
819 819 $ hg init incrementallyfixmergecommit
820 820 $ cd incrementallyfixmergecommit
821 821
822 822 $ printf "a\nb\nc\n" > file.changed
823 823 $ hg commit -Aqm "ancestor"
824 824
825 825 $ printf "aa\nb\nc\n" > file.changed
826 826 $ hg commit -m "change a"
827 827
828 828 $ hg checkout '.^'
829 829 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
830 830 $ printf "a\nb\ncc\n" > file.changed
831 831 $ hg commit -m "change c"
832 832 created new head
833 833
834 834 $ hg merge
835 835 merging file.changed
836 836 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
837 837 (branch merge, don't forget to commit)
838 838 $ hg commit -m "merge"
839 839 $ hg cat -r . file.changed
840 840 aa
841 841 b
842 842 cc
843 843
844 844 $ hg fix -r . --working-dir
845 845 $ hg cat -r . file.changed
846 846 AA
847 847 b
848 848 CC
849 849
850 850 $ cd ..
851 851
852 852 Abort fixing revisions if there is an unfinished operation. We don't want to
853 853 make things worse by editing files or stripping/obsoleting things. Also abort
854 854 fixing the working directory if there are unresolved merge conflicts.
855 855
856 856 $ hg init abortunresolved
857 857 $ cd abortunresolved
858 858
859 859 $ echo "foo1" > foo.whole
860 860 $ hg commit -Aqm "foo 1"
861 861
862 862 $ hg update null
863 863 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
864 864 $ echo "foo2" > foo.whole
865 865 $ hg commit -Aqm "foo 2"
866 866
867 867 $ hg --config extensions.rebase= rebase -r 1 -d 0
868 868 rebasing 1:c3b6dc0e177a tip "foo 2"
869 869 merging foo.whole
870 870 warning: conflicts while merging foo.whole! (edit, then use 'hg resolve --mark')
871 871 unresolved conflicts (see 'hg resolve', then 'hg rebase --continue')
872 [1]
872 [240]
873 873
874 874 $ hg --config extensions.rebase= fix --working-dir
875 875 abort: unresolved conflicts
876 876 (use 'hg resolve')
877 877 [255]
878 878
879 879 $ hg --config extensions.rebase= fix -r .
880 880 abort: rebase in progress
881 881 (use 'hg rebase --continue', 'hg rebase --abort', or 'hg rebase --stop')
882 882 [255]
883 883
884 884 $ cd ..
885 885
886 886 When fixing a file that was renamed, we should diff against the source of the
887 887 rename for incremental fixing and we should correctly reproduce the rename in
888 888 the replacement revision.
889 889
890 890 $ hg init fixrenamecommit
891 891 $ cd fixrenamecommit
892 892
893 893 $ printf "a\nb\nc\n" > source.changed
894 894 $ hg commit -Aqm "source revision"
895 895 $ hg move source.changed dest.changed
896 896 $ printf "a\nb\ncc\n" > dest.changed
897 897 $ hg commit -m "dest revision"
898 898
899 899 $ hg fix -r .
900 900 $ hg log -r tip --copies --template "{file_copies}\n"
901 901 dest.changed (source.changed)
902 902 $ hg cat -r tip dest.changed
903 903 a
904 904 b
905 905 CC
906 906
907 907 $ cd ..
908 908
909 909 When fixing revisions that remove files we must ensure that the replacement
910 910 actually removes the file, whereas it could accidentally leave it unchanged or
911 911 write an empty string to it.
912 912
913 913 $ hg init fixremovedfile
914 914 $ cd fixremovedfile
915 915
916 916 $ printf "foo\n" > foo.whole
917 917 $ printf "bar\n" > bar.whole
918 918 $ hg commit -Aqm "add files"
919 919 $ hg remove bar.whole
920 920 $ hg commit -m "remove file"
921 921 $ hg status --change .
922 922 R bar.whole
923 923 $ hg fix -r . foo.whole
924 924 $ hg status --change tip
925 925 M foo.whole
926 926 R bar.whole
927 927
928 928 $ cd ..
929 929
930 930 If fixing a revision finds no fixes to make, no replacement revision should be
931 931 created.
932 932
933 933 $ hg init nofixesneeded
934 934 $ cd nofixesneeded
935 935
936 936 $ printf "FOO\n" > foo.whole
937 937 $ hg commit -Aqm "add file"
938 938 $ hg log --template '{rev}\n'
939 939 0
940 940 $ hg fix -r .
941 941 $ hg log --template '{rev}\n'
942 942 0
943 943
944 944 $ cd ..
945 945
946 946 If fixing a commit reverts all the changes in the commit, we replace it with a
947 947 commit that changes no files.
948 948
949 949 $ hg init nochangesleft
950 950 $ cd nochangesleft
951 951
952 952 $ printf "FOO\n" > foo.whole
953 953 $ hg commit -Aqm "add file"
954 954 $ printf "foo\n" > foo.whole
955 955 $ hg commit -m "edit file"
956 956 $ hg status --change .
957 957 M foo.whole
958 958 $ hg fix -r .
959 959 $ hg status --change tip
960 960
961 961 $ cd ..
962 962
963 963 If we fix a parent and child revision together, the child revision must be
964 964 replaced if the parent is replaced, even if the diffs of the child needed no
965 965 fixes. However, we're free to not replace revisions that need no fixes and have
966 966 no ancestors that are replaced.
967 967
968 968 $ hg init mustreplacechild
969 969 $ cd mustreplacechild
970 970
971 971 $ printf "FOO\n" > foo.whole
972 972 $ hg commit -Aqm "add foo"
973 973 $ printf "foo\n" > foo.whole
974 974 $ hg commit -m "edit foo"
975 975 $ printf "BAR\n" > bar.whole
976 976 $ hg commit -Aqm "add bar"
977 977
978 978 $ hg log --graph --template '{rev} {files}'
979 979 @ 2 bar.whole
980 980 |
981 981 o 1 foo.whole
982 982 |
983 983 o 0 foo.whole
984 984
985 985 $ hg fix -r 0:2
986 986 $ hg log --graph --template '{rev} {files}'
987 987 o 4 bar.whole
988 988 |
989 989 o 3
990 990 |
991 991 | @ 2 bar.whole
992 992 | |
993 993 | x 1 foo.whole
994 994 |/
995 995 o 0 foo.whole
996 996
997 997
998 998 $ cd ..
999 999
1000 1000 It's also possible that the child needs absolutely no changes, but we still
1001 1001 need to replace it to update its parent. If we skipped replacing the child
1002 1002 because it had no file content changes, it would become an orphan for no good
1003 1003 reason.
1004 1004
1005 1005 $ hg init mustreplacechildevenifnop
1006 1006 $ cd mustreplacechildevenifnop
1007 1007
1008 1008 $ printf "Foo\n" > foo.whole
1009 1009 $ hg commit -Aqm "add a bad foo"
1010 1010 $ printf "FOO\n" > foo.whole
1011 1011 $ hg commit -m "add a good foo"
1012 1012 $ hg fix -r . -r '.^'
1013 1013 $ hg log --graph --template '{rev} {desc}'
1014 1014 o 3 add a good foo
1015 1015 |
1016 1016 o 2 add a bad foo
1017 1017
1018 1018 @ 1 add a good foo
1019 1019 |
1020 1020 x 0 add a bad foo
1021 1021
1022 1022
1023 1023 $ cd ..
1024 1024
1025 1025 Similar to the case above, the child revision may become empty as a result of
1026 1026 fixing its parent. We should still create an empty replacement child.
1027 1027 TODO: determine how this should interact with ui.allowemptycommit given that
1028 1028 the empty replacement could have children.
1029 1029
1030 1030 $ hg init mustreplacechildevenifempty
1031 1031 $ cd mustreplacechildevenifempty
1032 1032
1033 1033 $ printf "foo\n" > foo.whole
1034 1034 $ hg commit -Aqm "add foo"
1035 1035 $ printf "Foo\n" > foo.whole
1036 1036 $ hg commit -m "edit foo"
1037 1037 $ hg fix -r . -r '.^'
1038 1038 $ hg log --graph --template '{rev} {desc}\n' --stat
1039 1039 o 3 edit foo
1040 1040 |
1041 1041 o 2 add foo
1042 1042 foo.whole | 1 +
1043 1043 1 files changed, 1 insertions(+), 0 deletions(-)
1044 1044
1045 1045 @ 1 edit foo
1046 1046 | foo.whole | 2 +-
1047 1047 | 1 files changed, 1 insertions(+), 1 deletions(-)
1048 1048 |
1049 1049 x 0 add foo
1050 1050 foo.whole | 1 +
1051 1051 1 files changed, 1 insertions(+), 0 deletions(-)
1052 1052
1053 1053
1054 1054 $ cd ..
1055 1055
1056 1056 Fixing a secret commit should replace it with another secret commit.
1057 1057
1058 1058 $ hg init fixsecretcommit
1059 1059 $ cd fixsecretcommit
1060 1060
1061 1061 $ printf "foo\n" > foo.whole
1062 1062 $ hg commit -Aqm "add foo" --secret
1063 1063 $ hg fix -r .
1064 1064 $ hg log --template '{rev} {phase}\n'
1065 1065 1 secret
1066 1066 0 secret
1067 1067
1068 1068 $ cd ..
1069 1069
1070 1070 We should also preserve phase when fixing a draft commit while the user has
1071 1071 their default set to secret.
1072 1072
1073 1073 $ hg init respectphasesnewcommit
1074 1074 $ cd respectphasesnewcommit
1075 1075
1076 1076 $ printf "foo\n" > foo.whole
1077 1077 $ hg commit -Aqm "add foo"
1078 1078 $ hg --config phases.newcommit=secret fix -r .
1079 1079 $ hg log --template '{rev} {phase}\n'
1080 1080 1 draft
1081 1081 0 draft
1082 1082
1083 1083 $ cd ..
1084 1084
1085 1085 Debug output should show what fixer commands are being subprocessed, which is
1086 1086 useful for anyone trying to set up a new config.
1087 1087
1088 1088 $ hg init debugoutput
1089 1089 $ cd debugoutput
1090 1090
1091 1091 $ printf "foo\nbar\nbaz\n" > foo.changed
1092 1092 $ hg commit -Aqm "foo"
1093 1093 $ printf "Foo\nbar\nBaz\n" > foo.changed
1094 1094 $ hg --debug fix --working-dir
1095 1095 subprocess: * $TESTTMP/uppercase.py 1-1 3-3 (glob)
1096 1096
1097 1097 $ cd ..
1098 1098
1099 1099 Fixing an obsolete revision can cause divergence, so we abort unless the user
1100 1100 configures to allow it. This is not yet smart enough to know whether there is a
1101 1101 successor, but even then it is not likely intentional or idiomatic to fix an
1102 1102 obsolete revision.
1103 1103
1104 1104 $ hg init abortobsoleterev
1105 1105 $ cd abortobsoleterev
1106 1106
1107 1107 $ printf "foo\n" > foo.changed
1108 1108 $ hg commit -Aqm "foo"
1109 1109 $ hg debugobsolete `hg parents --template '{node}'`
1110 1110 1 new obsolescence markers
1111 1111 obsoleted 1 changesets
1112 1112 $ hg --hidden fix -r 0
1113 1113 abort: fixing obsolete revision could cause divergence
1114 1114 [255]
1115 1115
1116 1116 $ hg --hidden fix -r 0 --config experimental.evolution.allowdivergence=true
1117 1117 $ hg cat -r tip foo.changed
1118 1118 FOO
1119 1119
1120 1120 $ cd ..
1121 1121
1122 1122 Test all of the available substitution values for fixer commands.
1123 1123
1124 1124 $ hg init substitution
1125 1125 $ cd substitution
1126 1126
1127 1127 $ mkdir foo
1128 1128 $ printf "hello\ngoodbye\n" > foo/bar
1129 1129 $ hg add
1130 1130 adding foo/bar
1131 1131 $ hg --config "fix.fail:command=printf '%s\n' '{rootpath}' '{basename}'" \
1132 1132 > --config "fix.fail:linerange='{first}' '{last}'" \
1133 1133 > --config "fix.fail:pattern=foo/bar" \
1134 1134 > fix --working-dir
1135 1135 $ cat foo/bar
1136 1136 foo/bar
1137 1137 bar
1138 1138 1
1139 1139 2
1140 1140
1141 1141 $ cd ..
1142 1142
1143 1143 The --base flag should allow picking the revisions to diff against for changed
1144 1144 files and incremental line formatting.
1145 1145
1146 1146 $ hg init baseflag
1147 1147 $ cd baseflag
1148 1148
1149 1149 $ printf "one\ntwo\n" > foo.changed
1150 1150 $ printf "bar\n" > bar.changed
1151 1151 $ hg commit -Aqm "first"
1152 1152 $ printf "one\nTwo\n" > foo.changed
1153 1153 $ hg commit -m "second"
1154 1154 $ hg fix -w --base .
1155 1155 $ hg status
1156 1156 $ hg fix -w --base null
1157 1157 $ cat foo.changed
1158 1158 ONE
1159 1159 TWO
1160 1160 $ cat bar.changed
1161 1161 BAR
1162 1162
1163 1163 $ cd ..
1164 1164
1165 1165 If the user asks to fix the parent of another commit, they are asking to create
1166 1166 an orphan. We must respect experimental.evolution.allowunstable.
1167 1167
1168 1168 $ hg init allowunstable
1169 1169 $ cd allowunstable
1170 1170
1171 1171 $ printf "one\n" > foo.whole
1172 1172 $ hg commit -Aqm "first"
1173 1173 $ printf "two\n" > foo.whole
1174 1174 $ hg commit -m "second"
1175 1175 $ hg --config experimental.evolution.allowunstable=False fix -r '.^'
1176 1176 abort: cannot fix changeset with children
1177 1177 [255]
1178 1178 $ hg fix -r '.^'
1179 1179 1 new orphan changesets
1180 1180 $ hg cat -r 2 foo.whole
1181 1181 ONE
1182 1182
1183 1183 $ cd ..
1184 1184
1185 1185 The --base flag affects the set of files being fixed. So while the --whole flag
1186 1186 makes the base irrelevant for changed line ranges, it still changes the
1187 1187 meaning and effect of the command. In this example, no files or lines are fixed
1188 1188 until we specify the base, but then we do fix unchanged lines.
1189 1189
1190 1190 $ hg init basewhole
1191 1191 $ cd basewhole
1192 1192 $ printf "foo1\n" > foo.changed
1193 1193 $ hg commit -Aqm "first"
1194 1194 $ printf "foo2\n" >> foo.changed
1195 1195 $ printf "bar\n" > bar.changed
1196 1196 $ hg commit -Aqm "second"
1197 1197
1198 1198 $ hg fix --working-dir --whole
1199 1199 $ cat *.changed
1200 1200 bar
1201 1201 foo1
1202 1202 foo2
1203 1203
1204 1204 $ hg fix --working-dir --base 0 --whole
1205 1205 $ cat *.changed
1206 1206 BAR
1207 1207 FOO1
1208 1208 FOO2
1209 1209
1210 1210 $ cd ..
1211 1211
1212 1212 The execution order of tools can be controlled. This example doesn't work if
1213 1213 you sort after truncating, but the config defines the correct order while the
1214 1214 definitions are out of order (which might imply the incorrect order given the
1215 1215 implementation of fix). The goal is to use multiple tools to select the lowest
1216 1216 5 numbers in the file.
1217 1217
1218 1218 $ hg init priorityexample
1219 1219 $ cd priorityexample
1220 1220
1221 1221 $ cat >> .hg/hgrc <<EOF
1222 1222 > [fix]
1223 1223 > head:command = head -n 5
1224 1224 > head:pattern = numbers.txt
1225 1225 > head:priority = 1
1226 1226 > sort:command = sort -n
1227 1227 > sort:pattern = numbers.txt
1228 1228 > sort:priority = 2
1229 1229 > EOF
1230 1230
1231 1231 $ printf "8\n2\n3\n6\n7\n4\n9\n5\n1\n0\n" > numbers.txt
1232 1232 $ hg add -q
1233 1233 $ hg fix -w
1234 1234 $ cat numbers.txt
1235 1235 0
1236 1236 1
1237 1237 2
1238 1238 3
1239 1239 4
1240 1240
1241 1241 And of course we should be able to break this by reversing the execution order.
1242 1242 Test negative priorities while we're at it.
1243 1243
1244 1244 $ cat >> .hg/hgrc <<EOF
1245 1245 > [fix]
1246 1246 > head:priority = -1
1247 1247 > sort:priority = -2
1248 1248 > EOF
1249 1249 $ printf "8\n2\n3\n6\n7\n4\n9\n5\n1\n0\n" > numbers.txt
1250 1250 $ hg fix -w
1251 1251 $ cat numbers.txt
1252 1252 2
1253 1253 3
1254 1254 6
1255 1255 7
1256 1256 8
1257 1257
1258 1258 $ cd ..
1259 1259
1260 1260 It's possible for repeated applications of a fixer tool to create cycles in the
1261 1261 generated content of a file. For example, two users with different versions of
1262 1262 a code formatter might fight over the formatting when they run hg fix. In the
1263 1263 absence of other changes, this means we could produce commits with the same
1264 1264 hash in subsequent runs of hg fix. This is a problem unless we support
1265 1265 obsolescence cycles well. We avoid this by adding an extra field to the
1266 1266 successor which forces it to have a new hash. That's why this test creates
1267 1267 three revisions instead of two.
1268 1268
1269 1269 $ hg init cyclictool
1270 1270 $ cd cyclictool
1271 1271
1272 1272 $ cat >> .hg/hgrc <<EOF
1273 1273 > [fix]
1274 1274 > swapletters:command = tr ab ba
1275 1275 > swapletters:pattern = foo
1276 1276 > EOF
1277 1277
1278 1278 $ echo ab > foo
1279 1279 $ hg commit -Aqm foo
1280 1280
1281 1281 $ hg fix -r 0
1282 1282 $ hg fix -r 1
1283 1283
1284 1284 $ hg cat -r 0 foo --hidden
1285 1285 ab
1286 1286 $ hg cat -r 1 foo --hidden
1287 1287 ba
1288 1288 $ hg cat -r 2 foo
1289 1289 ab
1290 1290
1291 1291 $ cd ..
1292 1292
1293 1293 We run fixer tools in the repo root so they can look for config files or other
1294 1294 important things in the working directory. This does NOT mean we are
1295 1295 reconstructing a working copy of every revision being fixed; we're just giving
1296 1296 the tool knowledge of the repo's location in case it can do something
1297 1297 reasonable with that.
1298 1298
1299 1299 $ hg init subprocesscwd
1300 1300 $ cd subprocesscwd
1301 1301
1302 1302 $ cat >> .hg/hgrc <<EOF
1303 1303 > [fix]
1304 1304 > printcwd:command = "$PYTHON" -c "import os; print(os.getcwd())"
1305 1305 > printcwd:pattern = relpath:foo/bar
1306 1306 > filesetpwd:command = "$PYTHON" -c "import os; print('fs: ' + os.getcwd())"
1307 1307 > filesetpwd:pattern = set:**quux
1308 1308 > EOF
1309 1309
1310 1310 $ mkdir foo
1311 1311 $ printf "bar\n" > foo/bar
1312 1312 $ printf "quux\n" > quux
1313 1313 $ hg commit -Aqm blah
1314 1314
1315 1315 $ hg fix -w -r . foo/bar
1316 1316 $ hg cat -r tip foo/bar
1317 1317 $TESTTMP/subprocesscwd
1318 1318 $ cat foo/bar
1319 1319 $TESTTMP/subprocesscwd
1320 1320
1321 1321 $ cd foo
1322 1322
1323 1323 $ hg fix -w -r . bar
1324 1324 $ hg cat -r tip bar ../quux
1325 1325 $TESTTMP/subprocesscwd
1326 1326 quux
1327 1327 $ cat bar ../quux
1328 1328 $TESTTMP/subprocesscwd
1329 1329 quux
1330 1330 $ echo modified > bar
1331 1331 $ hg fix -w bar
1332 1332 $ cat bar
1333 1333 $TESTTMP/subprocesscwd
1334 1334
1335 1335 Apparently fixing p1() and its descendants doesn't include wdir() unless
1336 1336 explicitly stated.
1337 1337
1338 1338 $ hg fix -r '.::'
1339 1339 $ hg cat -r . ../quux
1340 1340 quux
1341 1341 $ hg cat -r tip ../quux
1342 1342 fs: $TESTTMP/subprocesscwd
1343 1343 $ cat ../quux
1344 1344 quux
1345 1345
1346 1346 Clean files are not fixed unless explicitly named
1347 1347 $ echo 'dirty' > ../quux
1348 1348
1349 1349 $ hg fix --working-dir
1350 1350 $ cat ../quux
1351 1351 fs: $TESTTMP/subprocesscwd
1352 1352
1353 1353 $ cd ../..
1354 1354
1355 1355 Tools configured without a pattern are ignored. It would be too dangerous to
1356 1356 run them on all files, because this might happen while testing a configuration
1357 1357 that also deletes all of the file content. There is no reasonable subset of the
1358 1358 files to use as a default. Users should be explicit about what files are
1359 1359 affected by a tool. This test also confirms that we don't crash when the
1360 1360 pattern config is missing, and that we only warn about it once.
1361 1361
1362 1362 $ hg init nopatternconfigured
1363 1363 $ cd nopatternconfigured
1364 1364
1365 1365 $ printf "foo" > foo
1366 1366 $ printf "bar" > bar
1367 1367 $ hg add -q
1368 1368 $ hg fix --debug --working-dir --config "fix.nopattern:command=echo fixed"
1369 1369 fixer tool has no pattern configuration: nopattern
1370 1370 $ cat foo bar
1371 1371 foobar (no-eol)
1372 1372 $ hg fix --debug --working-dir --config "fix.nocommand:pattern=foo.bar"
1373 1373 fixer tool has no command configuration: nocommand
1374 1374
1375 1375 $ cd ..
1376 1376
1377 1377 Tools can be disabled. Disabled tools do nothing but print a debug message.
1378 1378
1379 1379 $ hg init disabled
1380 1380 $ cd disabled
1381 1381
1382 1382 $ printf "foo\n" > foo
1383 1383 $ hg add -q
1384 1384 $ hg fix --debug --working-dir --config "fix.disabled:command=echo fixed" \
1385 1385 > --config "fix.disabled:pattern=foo" \
1386 1386 > --config "fix.disabled:enabled=false"
1387 1387 ignoring disabled fixer tool: disabled
1388 1388 $ cat foo
1389 1389 foo
1390 1390
1391 1391 $ cd ..
1392 1392
1393 1393 Test that we can configure a fixer to affect all files regardless of the cwd.
1394 1394 The way we invoke matching must not prohibit this.
1395 1395
1396 1396 $ hg init affectallfiles
1397 1397 $ cd affectallfiles
1398 1398
1399 1399 $ mkdir foo bar
1400 1400 $ printf "foo" > foo/file
1401 1401 $ printf "bar" > bar/file
1402 1402 $ printf "baz" > baz_file
1403 1403 $ hg add -q
1404 1404
1405 1405 $ cd bar
1406 1406 $ hg fix --working-dir --config "fix.cooltool:command=echo fixed" \
1407 1407 > --config "fix.cooltool:pattern=glob:**"
1408 1408 $ cd ..
1409 1409
1410 1410 $ cat foo/file
1411 1411 fixed
1412 1412 $ cat bar/file
1413 1413 fixed
1414 1414 $ cat baz_file
1415 1415 fixed
1416 1416
1417 1417 $ cd ..
1418 1418
1419 1419 Tools should be able to run on unchanged files, even if they set :linerange.
1420 1420 This includes a corner case where deleted chunks of a file are not considered
1421 1421 changes.
1422 1422
1423 1423 $ hg init skipclean
1424 1424 $ cd skipclean
1425 1425
1426 1426 $ printf "a\nb\nc\n" > foo
1427 1427 $ printf "a\nb\nc\n" > bar
1428 1428 $ printf "a\nb\nc\n" > baz
1429 1429 $ hg commit -Aqm "base"
1430 1430
1431 1431 $ printf "a\nc\n" > foo
1432 1432 $ printf "a\nx\nc\n" > baz
1433 1433
1434 1434 $ cat >> print.py <<EOF
1435 1435 > import sys
1436 1436 > for a in sys.argv[1:]:
1437 1437 > print(a)
1438 1438 > EOF
1439 1439
1440 1440 $ hg fix --working-dir foo bar baz \
1441 1441 > --config "fix.changedlines:command=\"$PYTHON\" print.py \"Line ranges:\"" \
1442 1442 > --config 'fix.changedlines:linerange="{first} through {last}"' \
1443 1443 > --config 'fix.changedlines:pattern=glob:**' \
1444 1444 > --config 'fix.changedlines:skipclean=false'
1445 1445
1446 1446 $ cat foo
1447 1447 Line ranges:
1448 1448 $ cat bar
1449 1449 Line ranges:
1450 1450 $ cat baz
1451 1451 Line ranges:
1452 1452 2 through 2
1453 1453
1454 1454 $ cd ..
1455 1455
1456 1456 Test various cases around merges. We were previously dropping files if they were
1457 1457 created on only the p2 side of the merge, so let's test permutations of:
1458 1458 * added, was fixed
1459 1459 * added, considered for fixing but was already good
1460 1460 * added, not considered for fixing
1461 1461 * modified, was fixed
1462 1462 * modified, considered for fixing but was already good
1463 1463 * modified, not considered for fixing
1464 1464
1465 1465 Before the bug was fixed where we would drop files, this test demonstrated the
1466 1466 following issues:
1467 1467 * new_in_r1.ignored, new_in_r1_already_good.changed, and
1468 1468 > mod_in_r1_already_good.changed were NOT in the manifest for the merge commit
1469 1469 * mod_in_r1.ignored had its contents from r0, NOT r1.
1470 1470
1471 1471 We're also setting a named branch for every commit to demonstrate that the
1472 1472 branch is kept intact and there aren't issues updating to another branch in the
1473 1473 middle of fix.
1474 1474
1475 1475 $ hg init merge_keeps_files
1476 1476 $ cd merge_keeps_files
1477 1477 $ for f in r0 mod_in_r1 mod_in_r2 mod_in_merge mod_in_child; do
1478 1478 > for c in changed whole ignored; do
1479 1479 > printf "hello\n" > $f.$c
1480 1480 > done
1481 1481 > printf "HELLO\n" > "mod_in_${f}_already_good.changed"
1482 1482 > done
1483 1483 $ hg branch -q r0
1484 1484 $ hg ci -Aqm 'r0'
1485 1485 $ hg phase -p
1486 1486 $ make_test_files() {
1487 1487 > printf "world\n" >> "mod_in_$1.changed"
1488 1488 > printf "world\n" >> "mod_in_$1.whole"
1489 1489 > printf "world\n" >> "mod_in_$1.ignored"
1490 1490 > printf "WORLD\n" >> "mod_in_$1_already_good.changed"
1491 1491 > printf "new in $1\n" > "new_in_$1.changed"
1492 1492 > printf "new in $1\n" > "new_in_$1.whole"
1493 1493 > printf "new in $1\n" > "new_in_$1.ignored"
1494 1494 > printf "ALREADY GOOD, NEW IN THIS REV\n" > "new_in_$1_already_good.changed"
1495 1495 > }
1496 1496 $ make_test_commit() {
1497 1497 > make_test_files "$1"
1498 1498 > hg branch -q "$1"
1499 1499 > hg ci -Aqm "$2"
1500 1500 > }
1501 1501 $ make_test_commit r1 "merge me, pt1"
1502 1502 $ hg co -q ".^"
1503 1503 $ make_test_commit r2 "merge me, pt2"
1504 1504 $ hg merge -qr 1
1505 1505 $ make_test_commit merge "evil merge"
1506 1506 $ make_test_commit child "child of merge"
1507 1507 $ make_test_files wdir
1508 1508 $ hg fix -r 'not public()' -w
1509 1509 $ hg log -G -T'{rev}:{shortest(node,8)}: branch:{branch} desc:{desc}'
1510 1510 @ 8:c22ce900: branch:child desc:child of merge
1511 1511 |
1512 1512 o 7:5a30615a: branch:merge desc:evil merge
1513 1513 |\
1514 1514 | o 6:4e5acdc4: branch:r2 desc:merge me, pt2
1515 1515 | |
1516 1516 o | 5:eea01878: branch:r1 desc:merge me, pt1
1517 1517 |/
1518 1518 o 0:0c548d87: branch:r0 desc:r0
1519 1519
1520 1520 $ hg files -r tip
1521 1521 mod_in_child.changed
1522 1522 mod_in_child.ignored
1523 1523 mod_in_child.whole
1524 1524 mod_in_child_already_good.changed
1525 1525 mod_in_merge.changed
1526 1526 mod_in_merge.ignored
1527 1527 mod_in_merge.whole
1528 1528 mod_in_merge_already_good.changed
1529 1529 mod_in_mod_in_child_already_good.changed
1530 1530 mod_in_mod_in_merge_already_good.changed
1531 1531 mod_in_mod_in_r1_already_good.changed
1532 1532 mod_in_mod_in_r2_already_good.changed
1533 1533 mod_in_r0_already_good.changed
1534 1534 mod_in_r1.changed
1535 1535 mod_in_r1.ignored
1536 1536 mod_in_r1.whole
1537 1537 mod_in_r1_already_good.changed
1538 1538 mod_in_r2.changed
1539 1539 mod_in_r2.ignored
1540 1540 mod_in_r2.whole
1541 1541 mod_in_r2_already_good.changed
1542 1542 new_in_child.changed
1543 1543 new_in_child.ignored
1544 1544 new_in_child.whole
1545 1545 new_in_child_already_good.changed
1546 1546 new_in_merge.changed
1547 1547 new_in_merge.ignored
1548 1548 new_in_merge.whole
1549 1549 new_in_merge_already_good.changed
1550 1550 new_in_r1.changed
1551 1551 new_in_r1.ignored
1552 1552 new_in_r1.whole
1553 1553 new_in_r1_already_good.changed
1554 1554 new_in_r2.changed
1555 1555 new_in_r2.ignored
1556 1556 new_in_r2.whole
1557 1557 new_in_r2_already_good.changed
1558 1558 r0.changed
1559 1559 r0.ignored
1560 1560 r0.whole
1561 1561 $ for f in "$(hg files -r tip)"; do hg cat -r tip $f -T'{path}:\n{data}\n'; done
1562 1562 mod_in_child.changed:
1563 1563 hello
1564 1564 WORLD
1565 1565
1566 1566 mod_in_child.ignored:
1567 1567 hello
1568 1568 world
1569 1569
1570 1570 mod_in_child.whole:
1571 1571 HELLO
1572 1572 WORLD
1573 1573
1574 1574 mod_in_child_already_good.changed:
1575 1575 WORLD
1576 1576
1577 1577 mod_in_merge.changed:
1578 1578 hello
1579 1579 WORLD
1580 1580
1581 1581 mod_in_merge.ignored:
1582 1582 hello
1583 1583 world
1584 1584
1585 1585 mod_in_merge.whole:
1586 1586 HELLO
1587 1587 WORLD
1588 1588
1589 1589 mod_in_merge_already_good.changed:
1590 1590 WORLD
1591 1591
1592 1592 mod_in_mod_in_child_already_good.changed:
1593 1593 HELLO
1594 1594
1595 1595 mod_in_mod_in_merge_already_good.changed:
1596 1596 HELLO
1597 1597
1598 1598 mod_in_mod_in_r1_already_good.changed:
1599 1599 HELLO
1600 1600
1601 1601 mod_in_mod_in_r2_already_good.changed:
1602 1602 HELLO
1603 1603
1604 1604 mod_in_r0_already_good.changed:
1605 1605 HELLO
1606 1606
1607 1607 mod_in_r1.changed:
1608 1608 hello
1609 1609 WORLD
1610 1610
1611 1611 mod_in_r1.ignored:
1612 1612 hello
1613 1613 world
1614 1614
1615 1615 mod_in_r1.whole:
1616 1616 HELLO
1617 1617 WORLD
1618 1618
1619 1619 mod_in_r1_already_good.changed:
1620 1620 WORLD
1621 1621
1622 1622 mod_in_r2.changed:
1623 1623 hello
1624 1624 WORLD
1625 1625
1626 1626 mod_in_r2.ignored:
1627 1627 hello
1628 1628 world
1629 1629
1630 1630 mod_in_r2.whole:
1631 1631 HELLO
1632 1632 WORLD
1633 1633
1634 1634 mod_in_r2_already_good.changed:
1635 1635 WORLD
1636 1636
1637 1637 new_in_child.changed:
1638 1638 NEW IN CHILD
1639 1639
1640 1640 new_in_child.ignored:
1641 1641 new in child
1642 1642
1643 1643 new_in_child.whole:
1644 1644 NEW IN CHILD
1645 1645
1646 1646 new_in_child_already_good.changed:
1647 1647 ALREADY GOOD, NEW IN THIS REV
1648 1648
1649 1649 new_in_merge.changed:
1650 1650 NEW IN MERGE
1651 1651
1652 1652 new_in_merge.ignored:
1653 1653 new in merge
1654 1654
1655 1655 new_in_merge.whole:
1656 1656 NEW IN MERGE
1657 1657
1658 1658 new_in_merge_already_good.changed:
1659 1659 ALREADY GOOD, NEW IN THIS REV
1660 1660
1661 1661 new_in_r1.changed:
1662 1662 NEW IN R1
1663 1663
1664 1664 new_in_r1.ignored:
1665 1665 new in r1
1666 1666
1667 1667 new_in_r1.whole:
1668 1668 NEW IN R1
1669 1669
1670 1670 new_in_r1_already_good.changed:
1671 1671 ALREADY GOOD, NEW IN THIS REV
1672 1672
1673 1673 new_in_r2.changed:
1674 1674 NEW IN R2
1675 1675
1676 1676 new_in_r2.ignored:
1677 1677 new in r2
1678 1678
1679 1679 new_in_r2.whole:
1680 1680 NEW IN R2
1681 1681
1682 1682 new_in_r2_already_good.changed:
1683 1683 ALREADY GOOD, NEW IN THIS REV
1684 1684
1685 1685 r0.changed:
1686 1686 hello
1687 1687
1688 1688 r0.ignored:
1689 1689 hello
1690 1690
1691 1691 r0.whole:
1692 1692 hello
1693 1693
@@ -1,605 +1,605 b''
1 1 #testcases abortcommand abortflag
2 2
3 3 #if abortflag
4 4 $ cat >> $HGRCPATH <<EOF
5 5 > [alias]
6 6 > abort = histedit --abort
7 7 > EOF
8 8 #endif
9 9
10 10 Test argument handling and various data parsing
11 11 ==================================================
12 12
13 13
14 14 Enable extensions used by this test.
15 15 $ cat >>$HGRCPATH <<EOF
16 16 > [extensions]
17 17 > histedit=
18 18 > EOF
19 19
20 20 Repo setup.
21 21 $ hg init foo
22 22 $ cd foo
23 23 $ echo alpha >> alpha
24 24 $ hg addr
25 25 adding alpha
26 26 $ hg ci -m one
27 27 $ echo alpha >> alpha
28 28 $ hg ci -m two
29 29 $ echo alpha >> alpha
30 30 $ hg ci -m three
31 31 $ echo alpha >> alpha
32 32 $ hg ci -m four
33 33 $ echo alpha >> alpha
34 34 $ hg ci -m five
35 35
36 36 $ hg log --style compact --graph
37 37 @ 4[tip] 08d98a8350f3 1970-01-01 00:00 +0000 test
38 38 | five
39 39 |
40 40 o 3 c8e68270e35a 1970-01-01 00:00 +0000 test
41 41 | four
42 42 |
43 43 o 2 eb57da33312f 1970-01-01 00:00 +0000 test
44 44 | three
45 45 |
46 46 o 1 579e40513370 1970-01-01 00:00 +0000 test
47 47 | two
48 48 |
49 49 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
50 50 one
51 51
52 52
53 53 histedit --continue/--abort with no existing state
54 54 --------------------------------------------------
55 55
56 56 $ hg histedit --continue
57 57 abort: no histedit in progress
58 58 [255]
59 59 $ hg abort
60 60 abort: no histedit in progress (abortflag !)
61 61 abort: no operation in progress (abortcommand !)
62 62 [255]
63 63
64 64 Run a dummy edit to make sure we get tip^^ correctly via revsingle.
65 65 --------------------------------------------------------------------
66 66
67 67 $ HGEDITOR=cat hg histedit "tip^^"
68 68 pick eb57da33312f 2 three
69 69 pick c8e68270e35a 3 four
70 70 pick 08d98a8350f3 4 five
71 71
72 72 # Edit history between eb57da33312f and 08d98a8350f3
73 73 #
74 74 # Commits are listed from least to most recent
75 75 #
76 76 # You can reorder changesets by reordering the lines
77 77 #
78 78 # Commands:
79 79 #
80 80 # e, edit = use commit, but stop for amending
81 81 # m, mess = edit commit message without changing commit content
82 82 # p, pick = use commit
83 83 # b, base = checkout changeset and apply further changesets from there
84 84 # d, drop = remove commit from history
85 85 # f, fold = use commit, but combine it with the one above
86 86 # r, roll = like fold, but discard this commit's description and date
87 87 #
88 88
89 89 Run on a revision not ancestors of the current working directory.
90 90 --------------------------------------------------------------------
91 91
92 92 $ hg up 2
93 93 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
94 94 $ hg histedit -r 4
95 95 abort: 08d98a8350f3 is not an ancestor of working directory
96 96 [255]
97 97 $ hg up --quiet
98 98
99 99
100 100 Test that we pick the minimum of a revrange
101 101 ---------------------------------------
102 102
103 103 $ HGEDITOR=cat hg histedit '2::' --commands - << EOF
104 104 > pick eb57da33312f 2 three
105 105 > pick c8e68270e35a 3 four
106 106 > pick 08d98a8350f3 4 five
107 107 > EOF
108 108 $ hg up --quiet
109 109
110 110 $ HGEDITOR=cat hg histedit 'tip:2' --commands - << EOF
111 111 > pick eb57da33312f 2 three
112 112 > pick c8e68270e35a 3 four
113 113 > pick 08d98a8350f3 4 five
114 114 > EOF
115 115 $ hg up --quiet
116 116
117 117 Test config specified default
118 118 -----------------------------
119 119
120 120 $ HGEDITOR=cat hg histedit --config "histedit.defaultrev=only(.) - ::eb57da33312f" --commands - << EOF
121 121 > pick c8e68270e35a 3 four
122 122 > pick 08d98a8350f3 4 five
123 123 > EOF
124 124
125 125 Test invalid config default
126 126 ---------------------------
127 127
128 128 $ hg histedit --config "histedit.defaultrev="
129 129 abort: config option histedit.defaultrev can't be empty
130 130 [255]
131 131
132 132 Run on a revision not descendants of the initial parent
133 133 --------------------------------------------------------------------
134 134
135 135 Test the message shown for inconsistent histedit state, which may be
136 136 created (and forgotten) by Mercurial earlier than 2.7. This emulates
137 137 Mercurial earlier than 2.7 by renaming ".hg/histedit-state"
138 138 temporarily.
139 139
140 140 $ hg log -G -T '{rev} {shortest(node)} {desc}\n' -r 2::
141 141 @ 4 08d9 five
142 142 |
143 143 o 3 c8e6 four
144 144 |
145 145 o 2 eb57 three
146 146 |
147 147 ~
148 148 $ HGEDITOR=cat hg histedit -r 4 --commands - << EOF
149 149 > edit 08d98a8350f3 4 five
150 150 > EOF
151 151 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
152 152 Editing (08d98a8350f3), you may commit or record as needed now.
153 153 (hg histedit --continue to resume)
154 [1]
154 [240]
155 155
156 156 $ hg graft --continue
157 157 abort: no graft in progress
158 158 (continue: hg histedit --continue)
159 159 [255]
160 160
161 161 $ mv .hg/histedit-state .hg/histedit-state.back
162 162 $ hg update --quiet --clean 2
163 163 $ echo alpha >> alpha
164 164 $ mv .hg/histedit-state.back .hg/histedit-state
165 165
166 166 $ hg histedit --continue
167 167 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/08d98a8350f3-02594089-histedit.hg
168 168 $ hg log -G -T '{rev} {shortest(node)} {desc}\n' -r 2::
169 169 @ 4 f5ed five
170 170 |
171 171 | o 3 c8e6 four
172 172 |/
173 173 o 2 eb57 three
174 174 |
175 175 ~
176 176
177 177 $ hg unbundle -q $TESTTMP/foo/.hg/strip-backup/08d98a8350f3-02594089-histedit.hg
178 178 $ hg strip -q -r f5ed --config extensions.strip=
179 179 $ hg up -q 08d98a8350f3
180 180
181 181 Test that missing revisions are detected
182 182 ---------------------------------------
183 183
184 184 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
185 185 > pick eb57da33312f 2 three
186 186 > pick 08d98a8350f3 4 five
187 187 > EOF
188 188 hg: parse error: missing rules for changeset c8e68270e35a
189 189 (use "drop c8e68270e35a" to discard, see also: 'hg help -e histedit.config')
190 190 [255]
191 191
192 192 Test that extra revisions are detected
193 193 ---------------------------------------
194 194
195 195 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
196 196 > pick 6058cbb6cfd7 0 one
197 197 > pick c8e68270e35a 3 four
198 198 > pick 08d98a8350f3 4 five
199 199 > EOF
200 200 hg: parse error: pick "6058cbb6cfd7" changeset was not a candidate
201 201 (only use listed changesets)
202 202 [255]
203 203
204 204 Test malformed line
205 205 ---------------------------------------
206 206
207 207 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
208 208 > pickeb57da33312f2three
209 209 > pick c8e68270e35a 3 four
210 210 > pick 08d98a8350f3 4 five
211 211 > EOF
212 212 hg: parse error: malformed line "pickeb57da33312f2three"
213 213 [255]
214 214
215 215 Test unknown changeset
216 216 ---------------------------------------
217 217
218 218 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
219 219 > pick 0123456789ab 2 three
220 220 > pick c8e68270e35a 3 four
221 221 > pick 08d98a8350f3 4 five
222 222 > EOF
223 223 hg: parse error: unknown changeset 0123456789ab listed
224 224 [255]
225 225
226 226 Test unknown command
227 227 ---------------------------------------
228 228
229 229 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
230 230 > coin eb57da33312f 2 three
231 231 > pick c8e68270e35a 3 four
232 232 > pick 08d98a8350f3 4 five
233 233 > EOF
234 234 hg: parse error: unknown action "coin"
235 235 [255]
236 236
237 237 Test duplicated changeset
238 238 ---------------------------------------
239 239
240 240 So one is missing and one appear twice.
241 241
242 242 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
243 243 > pick eb57da33312f 2 three
244 244 > pick eb57da33312f 2 three
245 245 > pick 08d98a8350f3 4 five
246 246 > EOF
247 247 hg: parse error: duplicated command for changeset eb57da33312f
248 248 [255]
249 249
250 250 Test bogus rev
251 251 ---------------------------------------
252 252
253 253 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
254 254 > pick eb57da33312f 2 three
255 255 > pick 0u98
256 256 > pick 08d98a8350f3 4 five
257 257 > EOF
258 258 hg: parse error: invalid changeset 0u98
259 259 [255]
260 260
261 261 Test short version of command
262 262 ---------------------------------------
263 263
264 264 Note: we use varying amounts of white space between command name and changeset
265 265 short hash. This tests issue3893.
266 266
267 267 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
268 268 > pick eb57da33312f 2 three
269 269 > p c8e68270e35a 3 four
270 270 > f 08d98a8350f3 4 five
271 271 > EOF
272 272 four
273 273 ***
274 274 five
275 275
276 276
277 277
278 278 HG: Enter commit message. Lines beginning with 'HG:' are removed.
279 279 HG: Leave message empty to abort commit.
280 280 HG: --
281 281 HG: user: test
282 282 HG: branch 'default'
283 283 HG: changed alpha
284 284 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/c8e68270e35a-63d8b8d8-histedit.hg
285 285
286 286 $ hg update -q 2
287 287 $ echo x > x
288 288 $ hg add x
289 289 $ hg commit -m'x' x
290 290 created new head
291 291 $ hg histedit -r 'heads(all())'
292 292 abort: The specified revisions must have exactly one common root
293 293 [255]
294 294
295 295 Test that trimming description using multi-byte characters
296 296 --------------------------------------------------------------------
297 297
298 298 $ "$PYTHON" <<EOF
299 299 > fp = open('logfile', 'wb')
300 300 > fp.write(b'12345678901234567890123456789012345678901234567890' +
301 301 > b'12345') # there are 5 more columns for 80 columns
302 302 >
303 303 > # 2 x 4 = 8 columns, but 3 x 4 = 12 bytes
304 304 > fp.write(u'\u3042\u3044\u3046\u3048'.encode('utf-8'))
305 305 >
306 306 > fp.close()
307 307 > EOF
308 308 $ echo xx >> x
309 309 $ hg --encoding utf-8 commit --logfile logfile
310 310
311 311 $ HGEDITOR=cat hg --encoding utf-8 histedit tip
312 312 pick 3d3ea1f3a10b 5 1234567890123456789012345678901234567890123456789012345\xe3\x81\x82... (esc)
313 313
314 314 # Edit history between 3d3ea1f3a10b and 3d3ea1f3a10b
315 315 #
316 316 # Commits are listed from least to most recent
317 317 #
318 318 # You can reorder changesets by reordering the lines
319 319 #
320 320 # Commands:
321 321 #
322 322 # e, edit = use commit, but stop for amending
323 323 # m, mess = edit commit message without changing commit content
324 324 # p, pick = use commit
325 325 # b, base = checkout changeset and apply further changesets from there
326 326 # d, drop = remove commit from history
327 327 # f, fold = use commit, but combine it with the one above
328 328 # r, roll = like fold, but discard this commit's description and date
329 329 #
330 330
331 331 Test --continue with --keep
332 332
333 333 $ hg strip -q -r . --config extensions.strip=
334 334 $ hg histedit '.^' -q --keep --commands - << EOF
335 335 > edit eb57da33312f 2 three
336 336 > pick f3cfcca30c44 4 x
337 337 > EOF
338 338 Editing (eb57da33312f), you may commit or record as needed now.
339 339 (hg histedit --continue to resume)
340 [1]
340 [240]
341 341 $ echo edit >> alpha
342 342 $ hg histedit -q --continue
343 343 $ hg log -G -T '{rev}:{node|short} {desc}'
344 344 @ 6:8fda0c726bf2 x
345 345 |
346 346 o 5:63379946892c three
347 347 |
348 348 | o 4:f3cfcca30c44 x
349 349 | |
350 350 | | o 3:2a30f3cfee78 four
351 351 | |/ ***
352 352 | | five
353 353 | o 2:eb57da33312f three
354 354 |/
355 355 o 1:579e40513370 two
356 356 |
357 357 o 0:6058cbb6cfd7 one
358 358
359 359
360 360 Test that abort fails gracefully on exception
361 361 ----------------------------------------------
362 362 $ hg histedit . -q --commands - << EOF
363 363 > edit 8fda0c726bf2 6 x
364 364 > EOF
365 365 Editing (8fda0c726bf2), you may commit or record as needed now.
366 366 (hg histedit --continue to resume)
367 [1]
367 [240]
368 368 Corrupt histedit state file
369 369 $ sed 's/8fda0c726bf2/123456789012/' .hg/histedit-state > ../corrupt-histedit
370 370 $ mv ../corrupt-histedit .hg/histedit-state
371 371 $ hg abort
372 372 warning: encountered an exception during histedit --abort; the repository may not have been completely cleaned up
373 373 abort: $TESTTMP/foo/.hg/strip-backup/*-histedit.hg: $ENOENT$ (glob) (windows !)
374 374 abort: $ENOENT$: '$TESTTMP/foo/.hg/strip-backup/*-histedit.hg' (glob) (no-windows !)
375 375 [255]
376 376 Histedit state has been exited
377 377 $ hg summary -q
378 378 parent: 5:63379946892c
379 379 commit: 1 added, 1 unknown (new branch head)
380 380 update: 4 new changesets (update)
381 381
382 382 $ cd ..
383 383
384 384 Set up default base revision tests
385 385
386 386 $ hg init defaultbase
387 387 $ cd defaultbase
388 388 $ touch foo
389 389 $ hg -q commit -A -m root
390 390 $ echo 1 > foo
391 391 $ hg commit -m 'public 1'
392 392 $ hg phase --force --public -r .
393 393 $ echo 2 > foo
394 394 $ hg commit -m 'draft after public'
395 395 $ hg -q up -r 1
396 396 $ echo 3 > foo
397 397 $ hg commit -m 'head 1 public'
398 398 created new head
399 399 $ hg phase --force --public -r .
400 400 $ echo 4 > foo
401 401 $ hg commit -m 'head 1 draft 1'
402 402 $ echo 5 > foo
403 403 $ hg commit -m 'head 1 draft 2'
404 404 $ hg -q up -r 2
405 405 $ echo 6 > foo
406 406 $ hg commit -m 'head 2 commit 1'
407 407 $ echo 7 > foo
408 408 $ hg commit -m 'head 2 commit 2'
409 409 $ hg -q up -r 2
410 410 $ echo 8 > foo
411 411 $ hg commit -m 'head 3'
412 412 created new head
413 413 $ hg -q up -r 2
414 414 $ echo 9 > foo
415 415 $ hg commit -m 'head 4'
416 416 created new head
417 417 $ hg merge --tool :local -r 8
418 418 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
419 419 (branch merge, don't forget to commit)
420 420 $ hg commit -m 'merge head 3 into head 4'
421 421 $ echo 11 > foo
422 422 $ hg commit -m 'commit 1 after merge'
423 423 $ echo 12 > foo
424 424 $ hg commit -m 'commit 2 after merge'
425 425
426 426 $ hg log -G -T '{rev}:{node|short} {phase} {desc}\n'
427 427 @ 12:8cde254db839 draft commit 2 after merge
428 428 |
429 429 o 11:6f2f0241f119 draft commit 1 after merge
430 430 |
431 431 o 10:90506cc76b00 draft merge head 3 into head 4
432 432 |\
433 433 | o 9:f8607a373a97 draft head 4
434 434 | |
435 435 o | 8:0da92be05148 draft head 3
436 436 |/
437 437 | o 7:4c35cdf97d5e draft head 2 commit 2
438 438 | |
439 439 | o 6:931820154288 draft head 2 commit 1
440 440 |/
441 441 | o 5:8cdc02b9bc63 draft head 1 draft 2
442 442 | |
443 443 | o 4:463b8c0d2973 draft head 1 draft 1
444 444 | |
445 445 | o 3:23a0c4eefcbf public head 1 public
446 446 | |
447 447 o | 2:4117331c3abb draft draft after public
448 448 |/
449 449 o 1:4426d359ea59 public public 1
450 450 |
451 451 o 0:54136a8ddf32 public root
452 452
453 453
454 454 Default base revision should stop at public changesets
455 455
456 456 $ hg -q up 8cdc02b9bc63
457 457 $ hg histedit --commands - <<EOF
458 458 > pick 463b8c0d2973
459 459 > pick 8cdc02b9bc63
460 460 > EOF
461 461
462 462 Default base revision should stop at branchpoint
463 463
464 464 $ hg -q up 4c35cdf97d5e
465 465 $ hg histedit --commands - <<EOF
466 466 > pick 931820154288
467 467 > pick 4c35cdf97d5e
468 468 > EOF
469 469
470 470 Default base revision should stop at merge commit
471 471
472 472 $ hg -q up 8cde254db839
473 473 $ hg histedit --commands - <<EOF
474 474 > pick 6f2f0241f119
475 475 > pick 8cde254db839
476 476 > EOF
477 477
478 478 commit --amend should abort if histedit is in progress
479 479 (issue4800) and markers are not being created.
480 480 Eventually, histedit could perhaps look at `source` extra,
481 481 in which case this test should be revisited.
482 482
483 483 $ hg -q up 8cde254db839
484 484 $ hg histedit 6f2f0241f119 --commands - <<EOF
485 485 > pick 8cde254db839
486 486 > edit 6f2f0241f119
487 487 > EOF
488 488 merging foo
489 489 warning: conflicts while merging foo! (edit, then use 'hg resolve --mark')
490 490 Fix up the change (pick 8cde254db839)
491 491 (hg histedit --continue to resume)
492 [1]
492 [240]
493 493 $ hg resolve -m --all
494 494 (no more unresolved files)
495 495 continue: hg histedit --continue
496 496 $ hg histedit --cont
497 497 merging foo
498 498 warning: conflicts while merging foo! (edit, then use 'hg resolve --mark')
499 499 Editing (6f2f0241f119), you may commit or record as needed now.
500 500 (hg histedit --continue to resume)
501 [1]
501 [240]
502 502 $ hg resolve -m --all
503 503 (no more unresolved files)
504 504 continue: hg histedit --continue
505 505 $ hg commit --amend -m 'reject this fold'
506 506 abort: histedit in progress
507 507 (use 'hg histedit --continue' or 'hg histedit --abort')
508 508 [255]
509 509
510 510 With markers enabled, histedit does not get confused, and
511 511 amend should not be blocked by the ongoing histedit.
512 512
513 513 $ cat >>$HGRCPATH <<EOF
514 514 > [experimental]
515 515 > evolution.createmarkers=True
516 516 > evolution.allowunstable=True
517 517 > EOF
518 518 $ hg commit --amend -m 'allow this fold'
519 519 $ hg histedit --continue
520 520
521 521 $ cd ..
522 522
523 523 Test autoverb feature
524 524
525 525 $ hg init autoverb
526 526 $ cd autoverb
527 527 $ echo alpha >> alpha
528 528 $ hg ci -qAm one
529 529 $ echo alpha >> alpha
530 530 $ hg ci -qm two
531 531 $ echo beta >> beta
532 532 $ hg ci -qAm "roll! one"
533 533
534 534 $ hg log --style compact --graph
535 535 @ 2[tip] 4f34d0f8b5fa 1970-01-01 00:00 +0000 test
536 536 | roll! one
537 537 |
538 538 o 1 579e40513370 1970-01-01 00:00 +0000 test
539 539 | two
540 540 |
541 541 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
542 542 one
543 543
544 544
545 545 Check that 'roll' is selected by default
546 546
547 547 $ HGEDITOR=cat hg histedit 0 --config experimental.histedit.autoverb=True
548 548 pick 6058cbb6cfd7 0 one
549 549 roll 4f34d0f8b5fa 2 roll! one
550 550 pick 579e40513370 1 two
551 551
552 552 # Edit history between 6058cbb6cfd7 and 4f34d0f8b5fa
553 553 #
554 554 # Commits are listed from least to most recent
555 555 #
556 556 # You can reorder changesets by reordering the lines
557 557 #
558 558 # Commands:
559 559 #
560 560 # e, edit = use commit, but stop for amending
561 561 # m, mess = edit commit message without changing commit content
562 562 # p, pick = use commit
563 563 # b, base = checkout changeset and apply further changesets from there
564 564 # d, drop = remove commit from history
565 565 # f, fold = use commit, but combine it with the one above
566 566 # r, roll = like fold, but discard this commit's description and date
567 567 #
568 568
569 569 $ cd ..
570 570
571 571 Check that histedit's commands accept revsets
572 572 $ hg init bar
573 573 $ cd bar
574 574 $ echo w >> a
575 575 $ hg ci -qAm "adds a"
576 576 $ echo x >> b
577 577 $ hg ci -qAm "adds b"
578 578 $ echo y >> c
579 579 $ hg ci -qAm "adds c"
580 580 $ echo z >> d
581 581 $ hg ci -qAm "adds d"
582 582 $ hg log -G -T '{rev} {desc}\n'
583 583 @ 3 adds d
584 584 |
585 585 o 2 adds c
586 586 |
587 587 o 1 adds b
588 588 |
589 589 o 0 adds a
590 590
591 591 $ HGEDITOR=cat hg histedit "2" --commands - << EOF
592 592 > base -4 adds c
593 593 > pick 2 adds c
594 594 > pick tip adds d
595 595 > EOF
596 596 $ hg log -G -T '{rev} {desc}\n'
597 597 @ 5 adds d
598 598 |
599 599 o 4 adds c
600 600 |
601 601 | o 1 adds b
602 602 |/
603 603 o 0 adds a
604 604
605 605
@@ -1,556 +1,556 b''
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > histedit=
6 6 > strip=
7 7 > mockmakedate = $TESTDIR/mockmakedate.py
8 8 > EOF
9 9
10 10 $ initrepo ()
11 11 > {
12 12 > hg init r
13 13 > cd r
14 14 > for x in a b c d e f g; do
15 15 > echo $x > $x
16 16 > hg add $x
17 17 > hg ci -m $x
18 18 > done
19 19 > }
20 20
21 21 $ initrepo
22 22
23 23 log before edit
24 24 $ hg log --graph
25 25 @ changeset: 6:3c6a8ed2ebe8
26 26 | tag: tip
27 27 | user: test
28 28 | date: Thu Jan 01 00:00:00 1970 +0000
29 29 | summary: g
30 30 |
31 31 o changeset: 5:652413bf663e
32 32 | user: test
33 33 | date: Thu Jan 01 00:00:00 1970 +0000
34 34 | summary: f
35 35 |
36 36 o changeset: 4:e860deea161a
37 37 | user: test
38 38 | date: Thu Jan 01 00:00:00 1970 +0000
39 39 | summary: e
40 40 |
41 41 o changeset: 3:055a42cdd887
42 42 | user: test
43 43 | date: Thu Jan 01 00:00:00 1970 +0000
44 44 | summary: d
45 45 |
46 46 o changeset: 2:177f92b77385
47 47 | user: test
48 48 | date: Thu Jan 01 00:00:00 1970 +0000
49 49 | summary: c
50 50 |
51 51 o changeset: 1:d2ae7f538514
52 52 | user: test
53 53 | date: Thu Jan 01 00:00:00 1970 +0000
54 54 | summary: b
55 55 |
56 56 o changeset: 0:cb9a9f314b8b
57 57 user: test
58 58 date: Thu Jan 01 00:00:00 1970 +0000
59 59 summary: a
60 60
61 61 dirty a file
62 62 $ echo a > g
63 63 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF
64 64 > EOF
65 65 abort: uncommitted changes
66 66 [255]
67 67 $ echo g > g
68 68
69 69 edit the history
70 70 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
71 71 > pick 177f92b77385 c
72 72 > pick 055a42cdd887 d
73 73 > edit e860deea161a e
74 74 > pick 652413bf663e f
75 75 > pick 3c6a8ed2ebe8 g
76 76 > EOF
77 77 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
78 78 Editing (e860deea161a), you may commit or record as needed now.
79 79 (hg histedit --continue to resume)
80 80
81 81 try to update and get an error
82 82 $ hg update tip
83 83 abort: histedit in progress
84 84 (use 'hg histedit --continue' or 'hg histedit --abort')
85 85 [255]
86 86
87 87 edit the plan via the editor
88 88 $ cat >> $TESTTMP/editplan.sh <<EOF
89 89 > cat > \$1 <<EOF2
90 90 > drop e860deea161a e
91 91 > drop 652413bf663e f
92 92 > drop 3c6a8ed2ebe8 g
93 93 > EOF2
94 94 > EOF
95 95 $ HGEDITOR="sh $TESTTMP/editplan.sh" hg histedit --edit-plan
96 96 $ cat .hg/histedit-state
97 97 v1
98 98 055a42cdd88768532f9cf79daa407fc8d138de9b
99 99 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
100 100 False
101 101 3
102 102 drop
103 103 e860deea161a2f77de56603b340ebbb4536308ae
104 104 drop
105 105 652413bf663ef2a641cab26574e46d5f5a64a55a
106 106 drop
107 107 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
108 108 0
109 109 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
110 110
111 111 edit the plan via --commands
112 112 $ hg histedit --edit-plan --commands - 2>&1 << EOF
113 113 > edit e860deea161a e
114 114 > pick 652413bf663e f
115 115 > drop 3c6a8ed2ebe8 g
116 116 > EOF
117 117 $ cat .hg/histedit-state
118 118 v1
119 119 055a42cdd88768532f9cf79daa407fc8d138de9b
120 120 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
121 121 False
122 122 3
123 123 edit
124 124 e860deea161a2f77de56603b340ebbb4536308ae
125 125 pick
126 126 652413bf663ef2a641cab26574e46d5f5a64a55a
127 127 drop
128 128 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
129 129 0
130 130 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
131 131
132 132 Go at a random point and try to continue
133 133
134 134 $ hg id -n
135 135 3+
136 136 $ hg up 0
137 137 abort: histedit in progress
138 138 (use 'hg histedit --continue' or 'hg histedit --abort')
139 139 [255]
140 140
141 141 Try to delete necessary commit
142 142 $ hg strip -r 652413b
143 143 abort: histedit in progress, can't strip 652413bf663e
144 144 [255]
145 145
146 146 commit, then edit the revision
147 147 $ hg ci -m 'wat'
148 148 created new head
149 149 $ echo a > e
150 150
151 151 qnew should fail while we're in the middle of the edit step
152 152
153 153 $ hg --config extensions.mq= qnew please-fail
154 154 abort: histedit in progress
155 155 (use 'hg histedit --continue' or 'hg histedit --abort')
156 156 [255]
157 157 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
158 158
159 159 $ hg log --graph
160 160 @ changeset: 6:b5f70786f9b0
161 161 | tag: tip
162 162 | user: test
163 163 | date: Thu Jan 01 00:00:00 1970 +0000
164 164 | summary: f
165 165 |
166 166 o changeset: 5:a5e1ba2f7afb
167 167 | user: test
168 168 | date: Thu Jan 01 00:00:00 1970 +0000
169 169 | summary: foobaz
170 170 |
171 171 o changeset: 4:1a60820cd1f6
172 172 | user: test
173 173 | date: Thu Jan 01 00:00:00 1970 +0000
174 174 | summary: wat
175 175 |
176 176 o changeset: 3:055a42cdd887
177 177 | user: test
178 178 | date: Thu Jan 01 00:00:00 1970 +0000
179 179 | summary: d
180 180 |
181 181 o changeset: 2:177f92b77385
182 182 | user: test
183 183 | date: Thu Jan 01 00:00:00 1970 +0000
184 184 | summary: c
185 185 |
186 186 o changeset: 1:d2ae7f538514
187 187 | user: test
188 188 | date: Thu Jan 01 00:00:00 1970 +0000
189 189 | summary: b
190 190 |
191 191 o changeset: 0:cb9a9f314b8b
192 192 user: test
193 193 date: Thu Jan 01 00:00:00 1970 +0000
194 194 summary: a
195 195
196 196
197 197 $ hg cat e
198 198 a
199 199
200 200 Stripping necessary commits should not break --abort
201 201
202 202 $ hg histedit 1a60820cd1f6 --commands - 2>&1 << EOF| fixbundle
203 203 > edit 1a60820cd1f6 wat
204 204 > pick a5e1ba2f7afb foobaz
205 205 > pick b5f70786f9b0 g
206 206 > EOF
207 207 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
208 208 Editing (1a60820cd1f6), you may commit or record as needed now.
209 209 (hg histedit --continue to resume)
210 210
211 211 $ mv .hg/histedit-state .hg/histedit-state.bak
212 212 $ hg strip -q -r b5f70786f9b0
213 213 $ mv .hg/histedit-state.bak .hg/histedit-state
214 214 $ hg histedit --abort
215 215 adding changesets
216 216 adding manifests
217 217 adding file changes
218 218 added 1 changesets with 1 changes to 3 files
219 219 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
220 220 $ hg log -r .
221 221 changeset: 6:b5f70786f9b0
222 222 tag: tip
223 223 user: test
224 224 date: Thu Jan 01 00:00:00 1970 +0000
225 225 summary: f
226 226
227 227
228 228 check histedit_source
229 229
230 230 $ hg log --debug --rev 5
231 231 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
232 232 phase: draft
233 233 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
234 234 parent: -1:0000000000000000000000000000000000000000
235 235 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
236 236 user: test
237 237 date: Thu Jan 01 00:00:00 1970 +0000
238 238 files: e
239 239 extra: branch=default
240 240 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
241 241 description:
242 242 foobaz
243 243
244 244
245 245
246 246 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
247 247 > edit b5f70786f9b0 f
248 248 > EOF
249 249 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
250 250 Editing (b5f70786f9b0), you may commit or record as needed now.
251 251 (hg histedit --continue to resume)
252 252 $ hg status
253 253 A f
254 254
255 255 $ hg summary
256 256 parent: 5:a5e1ba2f7afb
257 257 foobaz
258 258 branch: default
259 259 commit: 1 added (new branch head)
260 260 update: 1 new changesets (update)
261 261 phases: 7 draft
262 262 hist: 1 remaining (histedit --continue)
263 263
264 264 (test also that editor is invoked if histedit is continued for
265 265 "edit" action)
266 266
267 267 $ HGEDITOR='cat' hg histedit --continue
268 268 f
269 269
270 270
271 271 HG: Enter commit message. Lines beginning with 'HG:' are removed.
272 272 HG: Leave message empty to abort commit.
273 273 HG: --
274 274 HG: user: test
275 275 HG: branch 'default'
276 276 HG: added f
277 277 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-histedit.hg
278 278
279 279 $ hg status
280 280
281 281 log after edit
282 282 $ hg log --limit 1
283 283 changeset: 6:a107ee126658
284 284 tag: tip
285 285 user: test
286 286 date: Thu Jan 01 00:00:00 1970 +0000
287 287 summary: f
288 288
289 289
290 290 say we'll change the message, but don't.
291 291 $ cat > ../edit.sh <<EOF
292 292 > cat "\$1" | sed s/pick/mess/ > tmp
293 293 > mv tmp "\$1"
294 294 > EOF
295 295 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
296 296 $ hg status
297 297 $ hg log --limit 1
298 298 changeset: 6:1fd3b2fe7754
299 299 tag: tip
300 300 user: test
301 301 date: Thu Jan 01 00:00:00 1970 +0000
302 302 summary: f
303 303
304 304
305 305 modify the message
306 306
307 307 check saving last-message.txt, at first
308 308
309 309 $ cat > $TESTTMP/commitfailure.py <<EOF
310 310 > from mercurial import error
311 311 > def reposetup(ui, repo):
312 312 > class commitfailure(repo.__class__):
313 313 > def commit(self, *args, **kwargs):
314 314 > raise error.Abort(b'emulating unexpected abort')
315 315 > repo.__class__ = commitfailure
316 316 > EOF
317 317 $ cat >> .hg/hgrc <<EOF
318 318 > [extensions]
319 319 > # this failure occurs before editor invocation
320 320 > commitfailure = $TESTTMP/commitfailure.py
321 321 > EOF
322 322
323 323 $ cat > $TESTTMP/editor.sh <<EOF
324 324 > echo "==== before editing"
325 325 > cat \$1
326 326 > echo "===="
327 327 > echo "check saving last-message.txt" >> \$1
328 328 > EOF
329 329
330 330 (test that editor is not invoked before transaction starting)
331 331
332 332 $ rm -f .hg/last-message.txt
333 333 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
334 334 > mess 1fd3b2fe7754 f
335 335 > EOF
336 336 abort: emulating unexpected abort
337 337 $ test -f .hg/last-message.txt
338 338 [1]
339 339
340 340 $ cat >> .hg/hgrc <<EOF
341 341 > [extensions]
342 342 > commitfailure = !
343 343 > EOF
344 344 $ hg histedit --abort -q
345 345
346 346 (test that editor is invoked and commit message is saved into
347 347 "last-message.txt")
348 348
349 349 $ cat >> .hg/hgrc <<EOF
350 350 > [hooks]
351 351 > # this failure occurs after editor invocation
352 352 > pretxncommit.unexpectedabort = false
353 353 > EOF
354 354
355 355 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
356 356 A f
357 357
358 358 $ rm -f .hg/last-message.txt
359 359 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
360 360 > mess 1fd3b2fe7754 f
361 361 > EOF
362 362 ==== before editing
363 363 f
364 364
365 365
366 366 HG: Enter commit message. Lines beginning with 'HG:' are removed.
367 367 HG: Leave message empty to abort commit.
368 368 HG: --
369 369 HG: user: test
370 370 HG: branch 'default'
371 371 HG: added f
372 372 ====
373 373 transaction abort!
374 374 rollback completed
375 375 note: commit message saved in .hg/last-message.txt
376 376 note: use 'hg commit --logfile .hg/last-message.txt --edit' to reuse it
377 377 abort: pretxncommit.unexpectedabort hook exited with status 1
378 378 [255]
379 379 $ cat .hg/last-message.txt
380 380 f
381 381
382 382
383 383 check saving last-message.txt
384 384
385 385 (test also that editor is invoked if histedit is continued for "message"
386 386 action)
387 387
388 388 $ HGEDITOR=cat hg histedit --continue
389 389 f
390 390
391 391
392 392 HG: Enter commit message. Lines beginning with 'HG:' are removed.
393 393 HG: Leave message empty to abort commit.
394 394 HG: --
395 395 HG: user: test
396 396 HG: branch 'default'
397 397 HG: added f
398 398 transaction abort!
399 399 rollback completed
400 400 note: commit message saved in .hg/last-message.txt
401 401 note: use 'hg commit --logfile .hg/last-message.txt --edit' to reuse it
402 402 abort: pretxncommit.unexpectedabort hook exited with status 1
403 403 [255]
404 404
405 405 $ cat >> .hg/hgrc <<EOF
406 406 > [hooks]
407 407 > pretxncommit.unexpectedabort =
408 408 > EOF
409 409 $ hg histedit --abort -q
410 410
411 411 then, check "modify the message" itself
412 412
413 413 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
414 414 > mess 1fd3b2fe7754 f
415 415 > EOF
416 416 $ hg status
417 417 $ hg log --limit 1
418 418 changeset: 6:62feedb1200e
419 419 tag: tip
420 420 user: test
421 421 date: Thu Jan 01 00:00:00 1970 +0000
422 422 summary: f
423 423
424 424
425 425 rollback should not work after a histedit
426 426 $ hg rollback
427 427 no rollback information available
428 428 [1]
429 429
430 430 $ cd ..
431 431 $ hg clone -qr0 r r0
432 432 $ cd r0
433 433 $ hg phase -fdr0
434 434 $ hg histedit --commands - 0 2>&1 << EOF
435 435 > edit cb9a9f314b8b a > $EDITED
436 436 > EOF
437 437 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
438 438 Editing (cb9a9f314b8b), you may commit or record as needed now.
439 439 (hg histedit --continue to resume)
440 [1]
440 [240]
441 441 $ HGEDITOR=true hg histedit --continue
442 442 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-histedit.hg
443 443
444 444 $ hg log -G
445 445 @ changeset: 0:0efcea34f18a
446 446 tag: tip
447 447 user: test
448 448 date: Thu Jan 01 00:00:00 1970 +0000
449 449 summary: a
450 450
451 451 $ echo foo >> b
452 452 $ hg addr
453 453 adding b
454 454 $ hg ci -m 'add b'
455 455 $ echo foo >> a
456 456 $ hg ci -m 'extend a'
457 457 $ hg phase --public 1
458 458 Attempting to fold a change into a public change should not work:
459 459 $ cat > ../edit.sh <<EOF
460 460 > cat "\$1" | sed s/pick/fold/ > tmp
461 461 > mv tmp "\$1"
462 462 > EOF
463 463 $ HGEDITOR="sh ../edit.sh" hg histedit 2
464 464 warning: histedit rules saved to: .hg/histedit-last-edit.txt
465 465 hg: parse error: first changeset cannot use verb "fold"
466 466 [255]
467 467 $ cat .hg/histedit-last-edit.txt
468 468 fold 0012be4a27ea 2 extend a
469 469
470 470 # Edit history between 0012be4a27ea and 0012be4a27ea
471 471 #
472 472 # Commits are listed from least to most recent
473 473 #
474 474 # You can reorder changesets by reordering the lines
475 475 #
476 476 # Commands:
477 477 #
478 478 # e, edit = use commit, but stop for amending
479 479 # m, mess = edit commit message without changing commit content
480 480 # p, fold = use commit
481 481 # b, base = checkout changeset and apply further changesets from there
482 482 # d, drop = remove commit from history
483 483 # f, fold = use commit, but combine it with the one above
484 484 # r, roll = like fold, but discard this commit's description and date
485 485 #
486 486
487 487 $ cd ..
488 488
489 489 ============================================
490 490 Test update-timestamp config option in mess|
491 491 ============================================
492 492
493 493 $ addwithdate ()
494 494 > {
495 495 > echo $1 > $1
496 496 > hg add $1
497 497 > hg ci -m $1 -d "$2 0"
498 498 > }
499 499
500 500 $ initrepo ()
501 501 > {
502 502 > hg init r2
503 503 > cd r2
504 504 > addwithdate a 1
505 505 > addwithdate b 2
506 506 > addwithdate c 3
507 507 > addwithdate d 4
508 508 > addwithdate e 5
509 509 > addwithdate f 6
510 510 > }
511 511
512 512 $ initrepo
513 513
514 514 log before edit
515 515
516 516 $ hg log --limit 1
517 517 changeset: 5:178e35e0ce73
518 518 tag: tip
519 519 user: test
520 520 date: Thu Jan 01 00:00:06 1970 +0000
521 521 summary: f
522 522
523 523 $ hg histedit tip --commands - 2>&1 --config rewrite.update-timestamp=True << EOF | fixbundle
524 524 > mess 178e35e0ce73 f
525 525 > EOF
526 526
527 527 log after edit
528 528
529 529 $ hg log --limit 1
530 530 changeset: 5:98bf456d476b
531 531 tag: tip
532 532 user: test
533 533 date: Thu Jan 01 00:00:00 1970 +0000
534 534 summary: f
535 535
536 536
537 537 $ cd ..
538 538
539 539 warn the user on editing tagged commits
540 540
541 541 $ hg init issue4017
542 542 $ cd issue4017
543 543 $ echo > a
544 544 $ hg ci -Am 'add a'
545 545 adding a
546 546 $ hg tag a
547 547 $ hg tags
548 548 tip 1:bd7ee4f3939b
549 549 a 0:a8a82d372bb3
550 550 $ hg histedit
551 551 warning: tags associated with the given changeset will be lost after histedit.
552 552 do you want to continue (yN)? n
553 553 abort: histedit cancelled
554 554
555 555 [255]
556 556 $ cd ..
@@ -1,706 +1,706 b''
1 1 Test histedit extension: Fold commands
2 2 ======================================
3 3
4 4 This test file is dedicated to testing the fold command in non conflicting
5 5 case.
6 6
7 7 Initialization
8 8 ---------------
9 9
10 10
11 11 $ . "$TESTDIR/histedit-helpers.sh"
12 12
13 13 $ cat >> $HGRCPATH <<EOF
14 14 > [alias]
15 15 > logt = log --template '{rev}:{node|short} {desc|firstline}\n'
16 16 > [extensions]
17 17 > histedit=
18 18 > mockmakedate = $TESTDIR/mockmakedate.py
19 19 > EOF
20 20
21 21
22 22 Simple folding
23 23 --------------------
24 24 $ addwithdate ()
25 25 > {
26 26 > echo $1 > $1
27 27 > hg add $1
28 28 > hg ci -m $1 -d "$2 0"
29 29 > }
30 30
31 31 $ initrepo ()
32 32 > {
33 33 > hg init r
34 34 > cd r
35 35 > addwithdate a 1
36 36 > addwithdate b 2
37 37 > addwithdate c 3
38 38 > addwithdate d 4
39 39 > addwithdate e 5
40 40 > addwithdate f 6
41 41 > }
42 42
43 43 $ initrepo
44 44
45 45 log before edit
46 46 $ hg logt --graph
47 47 @ 5:178e35e0ce73 f
48 48 |
49 49 o 4:1ddb6c90f2ee e
50 50 |
51 51 o 3:532247a8969b d
52 52 |
53 53 o 2:ff2c9fa2018b c
54 54 |
55 55 o 1:97d72e5f12c7 b
56 56 |
57 57 o 0:8580ff50825a a
58 58
59 59
60 60 $ hg histedit ff2c9fa2018b --commands - 2>&1 <<EOF | fixbundle
61 61 > pick 1ddb6c90f2ee e
62 62 > pick 178e35e0ce73 f
63 63 > fold ff2c9fa2018b c
64 64 > pick 532247a8969b d
65 65 > EOF
66 66
67 67 log after edit
68 68 $ hg logt --graph
69 69 @ 4:c4d7f3def76d d
70 70 |
71 71 o 3:575228819b7e f
72 72 |
73 73 o 2:505a591af19e e
74 74 |
75 75 o 1:97d72e5f12c7 b
76 76 |
77 77 o 0:8580ff50825a a
78 78
79 79
80 80 post-fold manifest
81 81 $ hg manifest
82 82 a
83 83 b
84 84 c
85 85 d
86 86 e
87 87 f
88 88
89 89
90 90 check histedit_source, including that it uses the later date, from the first changeset
91 91
92 92 $ hg log --debug --rev 3
93 93 changeset: 3:575228819b7e6ed69e8c0a6a383ee59a80db7358
94 94 phase: draft
95 95 parent: 2:505a591af19eed18f560af827b9e03d2076773dc
96 96 parent: -1:0000000000000000000000000000000000000000
97 97 manifest: 3:81eede616954057198ead0b2c73b41d1f392829a
98 98 user: test
99 99 date: Thu Jan 01 00:00:06 1970 +0000
100 100 files+: c f
101 101 extra: branch=default
102 102 extra: histedit_source=7cad1d7030207872dfd1c3a7cb430f24f2884086,ff2c9fa2018b15fa74b33363bda9527323e2a99f
103 103 description:
104 104 f
105 105 ***
106 106 c
107 107
108 108
109 109
110 110 rollup will fold without preserving the folded commit's message or date
111 111
112 112 $ OLDHGEDITOR=$HGEDITOR
113 113 $ HGEDITOR=false
114 114 $ hg histedit 97d72e5f12c7 --commands - 2>&1 <<EOF | fixbundle
115 115 > pick 97d72e5f12c7 b
116 116 > roll 505a591af19e e
117 117 > pick 575228819b7e f
118 118 > pick c4d7f3def76d d
119 119 > EOF
120 120
121 121 $ HGEDITOR=$OLDHGEDITOR
122 122
123 123 log after edit
124 124 $ hg logt --graph
125 125 @ 3:bab801520cec d
126 126 |
127 127 o 2:58c8f2bfc151 f
128 128 |
129 129 o 1:5d939c56c72e b
130 130 |
131 131 o 0:8580ff50825a a
132 132
133 133
134 134 description is taken from rollup target commit
135 135
136 136 $ hg log --debug --rev 1
137 137 changeset: 1:5d939c56c72e77e29f5167696218e2131a40f5cf
138 138 phase: draft
139 139 parent: 0:8580ff50825a50c8f716709acdf8de0deddcd6ab
140 140 parent: -1:0000000000000000000000000000000000000000
141 141 manifest: 1:b5e112a3a8354e269b1524729f0918662d847c38
142 142 user: test
143 143 date: Thu Jan 01 00:00:02 1970 +0000
144 144 files+: b e
145 145 extra: branch=default
146 146 extra: histedit_source=97d72e5f12c7e84f85064aa72e5a297142c36ed9,505a591af19eed18f560af827b9e03d2076773dc
147 147 description:
148 148 b
149 149
150 150
151 151
152 152 check saving last-message.txt
153 153
154 154 $ cat > $TESTTMP/abortfolding.py <<EOF
155 155 > from mercurial import util
156 156 > def abortfolding(ui, repo, hooktype, **kwargs):
157 157 > ctx = repo[kwargs.get('node')]
158 158 > if set(ctx.files()) == {b'c', b'd', b'f'}:
159 159 > return True # abort folding commit only
160 160 > ui.warn(b'allow non-folding commit\\n')
161 161 > EOF
162 162 $ cat > .hg/hgrc <<EOF
163 163 > [hooks]
164 164 > pretxncommit.abortfolding = python:$TESTTMP/abortfolding.py:abortfolding
165 165 > EOF
166 166
167 167 $ cat > $TESTTMP/editor.sh << EOF
168 168 > echo "==== before editing"
169 169 > cat \$1
170 170 > echo "===="
171 171 > echo "check saving last-message.txt" >> \$1
172 172 > EOF
173 173
174 174 $ rm -f .hg/last-message.txt
175 175 $ hg status --rev '58c8f2bfc151^1::bab801520cec'
176 176 A c
177 177 A d
178 178 A f
179 179 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit 58c8f2bfc151 --commands - 2>&1 <<EOF
180 180 > pick 58c8f2bfc151 f
181 181 > fold bab801520cec d
182 182 > EOF
183 183 allow non-folding commit
184 184 ==== before editing
185 185 f
186 186 ***
187 187 c
188 188 ***
189 189 d
190 190
191 191
192 192
193 193 HG: Enter commit message. Lines beginning with 'HG:' are removed.
194 194 HG: Leave message empty to abort commit.
195 195 HG: --
196 196 HG: user: test
197 197 HG: branch 'default'
198 198 HG: added c
199 199 HG: added d
200 200 HG: added f
201 201 ====
202 202 transaction abort!
203 203 rollback completed
204 204 abort: pretxncommit.abortfolding hook failed
205 205 [255]
206 206
207 207 $ cat .hg/last-message.txt
208 208 f
209 209 ***
210 210 c
211 211 ***
212 212 d
213 213
214 214
215 215
216 216 check saving last-message.txt
217 217
218 218 $ cd ..
219 219 $ rm -r r
220 220
221 221 folding preserves initial author but uses later date
222 222 ----------------------------------------------------
223 223
224 224 $ initrepo
225 225
226 226 $ hg ci -d '7 0' --user "someone else" --amend --quiet
227 227
228 228 tip before edit
229 229 $ hg log --rev .
230 230 changeset: 5:10c36dd37515
231 231 tag: tip
232 232 user: someone else
233 233 date: Thu Jan 01 00:00:07 1970 +0000
234 234 summary: f
235 235
236 236
237 237 $ hg --config progress.debug=1 --debug \
238 238 > histedit 1ddb6c90f2ee --commands - 2>&1 <<EOF | \
239 239 > egrep 'editing|unresolved'
240 240 > pick 1ddb6c90f2ee e
241 241 > fold 10c36dd37515 f
242 242 > EOF
243 243 editing: pick 1ddb6c90f2ee 4 e 1/2 changes (50.00%)
244 244 editing: fold 10c36dd37515 5 f 2/2 changes (100.00%)
245 245
246 246 tip after edit, which should use the later date, from the second changeset
247 247 $ hg log --rev .
248 248 changeset: 4:e4f3ec5d0b40
249 249 tag: tip
250 250 user: test
251 251 date: Thu Jan 01 00:00:07 1970 +0000
252 252 summary: e
253 253
254 254
255 255 $ cd ..
256 256 $ rm -r r
257 257
258 258 folding and creating no new change doesn't break:
259 259 -------------------------------------------------
260 260
261 261 folded content is dropped during a merge. The folded commit should properly disappear.
262 262
263 263 $ mkdir fold-to-empty-test
264 264 $ cd fold-to-empty-test
265 265 $ hg init
266 266 $ printf "1\n2\n3\n" > file
267 267 $ hg add file
268 268 $ hg commit -m '1+2+3'
269 269 $ echo 4 >> file
270 270 $ hg commit -m '+4'
271 271 $ echo 5 >> file
272 272 $ hg commit -m '+5'
273 273 $ echo 6 >> file
274 274 $ hg commit -m '+6'
275 275 $ hg logt --graph
276 276 @ 3:251d831eeec5 +6
277 277 |
278 278 o 2:888f9082bf99 +5
279 279 |
280 280 o 1:617f94f13c0f +4
281 281 |
282 282 o 0:0189ba417d34 1+2+3
283 283
284 284
285 285 $ hg histedit 1 --commands - << EOF
286 286 > pick 617f94f13c0f 1 +4
287 287 > drop 888f9082bf99 2 +5
288 288 > fold 251d831eeec5 3 +6
289 289 > EOF
290 290 merging file
291 291 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
292 292 Fix up the change (fold 251d831eeec5)
293 293 (hg histedit --continue to resume)
294 [1]
294 [240]
295 295 There were conflicts, we keep P1 content. This
296 296 should effectively drop the changes from +6.
297 297
298 298 $ hg status -v
299 299 M file
300 300 ? file.orig
301 301 # The repository is in an unfinished *histedit* state.
302 302
303 303 # Unresolved merge conflicts:
304 304 #
305 305 # file
306 306 #
307 307 # To mark files as resolved: hg resolve --mark FILE
308 308
309 309 # To continue: hg histedit --continue
310 310 # To abort: hg histedit --abort
311 311
312 312 $ hg resolve -l
313 313 U file
314 314 $ hg revert -r 'p1()' file
315 315 $ hg resolve --mark file
316 316 (no more unresolved files)
317 317 continue: hg histedit --continue
318 318 $ hg histedit --continue
319 319 251d831eeec5: empty changeset
320 320 saved backup bundle to $TESTTMP/fold-to-empty-test/.hg/strip-backup/888f9082bf99-daa0b8b3-histedit.hg
321 321 $ hg logt --graph
322 322 @ 1:617f94f13c0f +4
323 323 |
324 324 o 0:0189ba417d34 1+2+3
325 325
326 326
327 327 $ cd ..
328 328
329 329
330 330 Test fold through dropped
331 331 -------------------------
332 332
333 333
334 334 Test corner case where folded revision is separated from its parent by a
335 335 dropped revision.
336 336
337 337
338 338 $ hg init fold-with-dropped
339 339 $ cd fold-with-dropped
340 340 $ printf "1\n2\n3\n" > file
341 341 $ hg commit -Am '1+2+3'
342 342 adding file
343 343 $ echo 4 >> file
344 344 $ hg commit -m '+4'
345 345 $ echo 5 >> file
346 346 $ hg commit -m '+5'
347 347 $ echo 6 >> file
348 348 $ hg commit -m '+6'
349 349 $ hg logt -G
350 350 @ 3:251d831eeec5 +6
351 351 |
352 352 o 2:888f9082bf99 +5
353 353 |
354 354 o 1:617f94f13c0f +4
355 355 |
356 356 o 0:0189ba417d34 1+2+3
357 357
358 358 $ hg histedit 1 --commands - << EOF
359 359 > pick 617f94f13c0f 1 +4
360 360 > drop 888f9082bf99 2 +5
361 361 > fold 251d831eeec5 3 +6
362 362 > EOF
363 363 merging file
364 364 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
365 365 Fix up the change (fold 251d831eeec5)
366 366 (hg histedit --continue to resume)
367 [1]
367 [240]
368 368 $ cat > file << EOF
369 369 > 1
370 370 > 2
371 371 > 3
372 372 > 4
373 373 > 5
374 374 > EOF
375 375 $ hg resolve --mark file
376 376 (no more unresolved files)
377 377 continue: hg histedit --continue
378 378 $ hg commit -m '+5.2'
379 379 created new head
380 380 $ echo 6 >> file
381 381 $ HGEDITOR=cat hg histedit --continue
382 382 +4
383 383 ***
384 384 +5.2
385 385 ***
386 386 +6
387 387
388 388
389 389
390 390 HG: Enter commit message. Lines beginning with 'HG:' are removed.
391 391 HG: Leave message empty to abort commit.
392 392 HG: --
393 393 HG: user: test
394 394 HG: branch 'default'
395 395 HG: changed file
396 396 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-3d69522c-histedit.hg
397 397 $ hg logt -G
398 398 @ 1:10c647b2cdd5 +4
399 399 |
400 400 o 0:0189ba417d34 1+2+3
401 401
402 402 $ hg export tip
403 403 # HG changeset patch
404 404 # User test
405 405 # Date 0 0
406 406 # Thu Jan 01 00:00:00 1970 +0000
407 407 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
408 408 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
409 409 +4
410 410 ***
411 411 +5.2
412 412 ***
413 413 +6
414 414
415 415 diff -r 0189ba417d34 -r 10c647b2cdd5 file
416 416 --- a/file Thu Jan 01 00:00:00 1970 +0000
417 417 +++ b/file Thu Jan 01 00:00:00 1970 +0000
418 418 @@ -1,3 +1,6 @@
419 419 1
420 420 2
421 421 3
422 422 +4
423 423 +5
424 424 +6
425 425 $ cd ..
426 426
427 427
428 428 Folding with initial rename (issue3729)
429 429 ---------------------------------------
430 430
431 431 $ hg init fold-rename
432 432 $ cd fold-rename
433 433 $ echo a > a.txt
434 434 $ hg add a.txt
435 435 $ hg commit -m a
436 436 $ hg rename a.txt b.txt
437 437 $ hg commit -m rename
438 438 $ echo b >> b.txt
439 439 $ hg commit -m b
440 440
441 441 $ hg logt --follow b.txt
442 442 2:e0371e0426bc b
443 443 1:1c4f440a8085 rename
444 444 0:6c795aa153cb a
445 445
446 446 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
447 447 > pick 1c4f440a8085 rename
448 448 > fold e0371e0426bc b
449 449 > EOF
450 450
451 451 $ hg logt --follow b.txt
452 452 1:cf858d235c76 rename
453 453 0:6c795aa153cb a
454 454
455 455 $ cd ..
456 456
457 457 Folding with swapping
458 458 ---------------------
459 459
460 460 This is an excuse to test hook with histedit temporary commit (issue4422)
461 461
462 462
463 463 $ hg init issue4422
464 464 $ cd issue4422
465 465 $ echo a > a.txt
466 466 $ hg add a.txt
467 467 $ hg commit -m a
468 468 $ echo b > b.txt
469 469 $ hg add b.txt
470 470 $ hg commit -m b
471 471 $ echo c > c.txt
472 472 $ hg add c.txt
473 473 $ hg commit -m c
474 474
475 475 $ hg logt
476 476 2:a1a953ffb4b0 c
477 477 1:199b6bb90248 b
478 478 0:6c795aa153cb a
479 479
480 480 $ hg histedit 6c795aa153cb --config hooks.commit='echo commit $HG_NODE' --config hooks.tonative.commit=True \
481 481 > --commands - 2>&1 << EOF | fixbundle
482 482 > pick 199b6bb90248 b
483 483 > fold a1a953ffb4b0 c
484 484 > pick 6c795aa153cb a
485 485 > EOF
486 486 commit 9599899f62c05f4377548c32bf1c9f1a39634b0c
487 487
488 488 $ hg logt
489 489 1:9599899f62c0 a
490 490 0:79b99e9c8e49 b
491 491
492 492 Test unix -> windows style variable substitution in external hooks.
493 493
494 494 $ cat > $TESTTMP/tmp.hgrc <<'EOF'
495 495 > [hooks]
496 496 > pre-add = echo no variables
497 497 > post-add = echo ran $HG_ARGS, literal \$non-var, 'also $non-var', $HG_RESULT
498 498 > tonative.post-add = True
499 499 > EOF
500 500
501 501 $ echo "foo" > amended.txt
502 502 $ HGRCPATH=$TESTTMP/tmp.hgrc hg add -v amended.txt
503 503 running hook pre-add: echo no variables
504 504 no variables
505 505 adding amended.txt
506 506 converting hook "post-add" to native (windows !)
507 507 running hook post-add: echo ran %HG_ARGS%, literal $non-var, "also $non-var", %HG_RESULT% (windows !)
508 508 running hook post-add: echo ran $HG_ARGS, literal \$non-var, 'also $non-var', $HG_RESULT (no-windows !)
509 509 ran add -v amended.txt, literal $non-var, "also $non-var", 0 (windows !)
510 510 ran add -v amended.txt, literal $non-var, also $non-var, 0 (no-windows !)
511 511 $ hg ci -q --config extensions.largefiles= --amend -I amended.txt
512 512 The fsmonitor extension is incompatible with the largefiles extension and has been disabled. (fsmonitor !)
513 513
514 514 Test that folding multiple changes in a row doesn't show multiple
515 515 editors.
516 516
517 517 $ echo foo >> foo
518 518 $ hg add foo
519 519 $ hg ci -m foo1
520 520 $ echo foo >> foo
521 521 $ hg ci -m foo2
522 522 $ echo foo >> foo
523 523 $ hg ci -m foo3
524 524 $ hg logt
525 525 4:21679ff7675c foo3
526 526 3:b7389cc4d66e foo2
527 527 2:0e01aeef5fa8 foo1
528 528 1:578c7455730c a
529 529 0:79b99e9c8e49 b
530 530 $ cat > "$TESTTMP/editor.sh" <<EOF
531 531 > echo ran editor >> "$TESTTMP/editorlog.txt"
532 532 > cat \$1 >> "$TESTTMP/editorlog.txt"
533 533 > echo END >> "$TESTTMP/editorlog.txt"
534 534 > echo merged foos > \$1
535 535 > EOF
536 536 $ HGEDITOR="sh \"$TESTTMP/editor.sh\"" hg histedit 1 --commands - 2>&1 <<EOF | fixbundle
537 537 > pick 578c7455730c 1 a
538 538 > pick 0e01aeef5fa8 2 foo1
539 539 > fold b7389cc4d66e 3 foo2
540 540 > fold 21679ff7675c 4 foo3
541 541 > EOF
542 542 merging foo
543 543 $ hg logt
544 544 2:e8bedbda72c1 merged foos
545 545 1:578c7455730c a
546 546 0:79b99e9c8e49 b
547 547 Editor should have run only once
548 548 $ cat $TESTTMP/editorlog.txt
549 549 ran editor
550 550 foo1
551 551 ***
552 552 foo2
553 553 ***
554 554 foo3
555 555
556 556
557 557
558 558 HG: Enter commit message. Lines beginning with 'HG:' are removed.
559 559 HG: Leave message empty to abort commit.
560 560 HG: --
561 561 HG: user: test
562 562 HG: branch 'default'
563 563 HG: added foo
564 564 END
565 565
566 566 $ cd ..
567 567
568 568 Test rolling into a commit with multiple children (issue5498)
569 569
570 570 $ hg init roll
571 571 $ cd roll
572 572 $ echo a > a
573 573 $ hg commit -qAm aa
574 574 $ echo b > b
575 575 $ hg commit -qAm bb
576 576 $ hg up -q ".^"
577 577 $ echo c > c
578 578 $ hg commit -qAm cc
579 579 $ hg log -G -T '{node|short} {desc}'
580 580 @ 5db65b93a12b cc
581 581 |
582 582 | o 301d76bdc3ae bb
583 583 |/
584 584 o 8f0162e483d0 aa
585 585
586 586
587 587 $ hg histedit . --commands - << EOF
588 588 > r 5db65b93a12b
589 589 > EOF
590 590 hg: parse error: first changeset cannot use verb "roll"
591 591 [255]
592 592 $ hg log -G -T '{node|short} {desc}'
593 593 @ 5db65b93a12b cc
594 594 |
595 595 | o 301d76bdc3ae bb
596 596 |/
597 597 o 8f0162e483d0 aa
598 598
599 599
600 600 $ cd ..
601 601
602 602 ====================================
603 603 Test update-timestamp config option|
604 604 ====================================
605 605
606 606 $ addwithdate ()
607 607 > {
608 608 > echo $1 > $1
609 609 > hg add $1
610 610 > hg ci -m $1 -d "$2 0"
611 611 > }
612 612
613 613 $ initrepo ()
614 614 > {
615 615 > hg init r
616 616 > cd r
617 617 > addwithdate a 1
618 618 > addwithdate b 2
619 619 > addwithdate c 3
620 620 > addwithdate d 4
621 621 > addwithdate e 5
622 622 > addwithdate f 6
623 623 > }
624 624
625 625 $ initrepo
626 626
627 627 log before edit
628 628
629 629 $ hg log
630 630 changeset: 5:178e35e0ce73
631 631 tag: tip
632 632 user: test
633 633 date: Thu Jan 01 00:00:06 1970 +0000
634 634 summary: f
635 635
636 636 changeset: 4:1ddb6c90f2ee
637 637 user: test
638 638 date: Thu Jan 01 00:00:05 1970 +0000
639 639 summary: e
640 640
641 641 changeset: 3:532247a8969b
642 642 user: test
643 643 date: Thu Jan 01 00:00:04 1970 +0000
644 644 summary: d
645 645
646 646 changeset: 2:ff2c9fa2018b
647 647 user: test
648 648 date: Thu Jan 01 00:00:03 1970 +0000
649 649 summary: c
650 650
651 651 changeset: 1:97d72e5f12c7
652 652 user: test
653 653 date: Thu Jan 01 00:00:02 1970 +0000
654 654 summary: b
655 655
656 656 changeset: 0:8580ff50825a
657 657 user: test
658 658 date: Thu Jan 01 00:00:01 1970 +0000
659 659 summary: a
660 660
661 661
662 662 $ hg histedit 1ddb6c90f2ee --commands - 2>&1 --config rewrite.update-timestamp=True <<EOF | fixbundle
663 663 > pick 178e35e0ce73 f
664 664 > fold 1ddb6c90f2ee e
665 665 > EOF
666 666
667 667 log after edit
668 668 observe time from f is updated
669 669
670 670 $ hg log
671 671 changeset: 4:f7909b1863a2
672 672 tag: tip
673 673 user: test
674 674 date: Thu Jan 01 00:00:01 1970 +0000
675 675 summary: f
676 676
677 677 changeset: 3:532247a8969b
678 678 user: test
679 679 date: Thu Jan 01 00:00:04 1970 +0000
680 680 summary: d
681 681
682 682 changeset: 2:ff2c9fa2018b
683 683 user: test
684 684 date: Thu Jan 01 00:00:03 1970 +0000
685 685 summary: c
686 686
687 687 changeset: 1:97d72e5f12c7
688 688 user: test
689 689 date: Thu Jan 01 00:00:02 1970 +0000
690 690 summary: b
691 691
692 692 changeset: 0:8580ff50825a
693 693 user: test
694 694 date: Thu Jan 01 00:00:01 1970 +0000
695 695 summary: a
696 696
697 697 post-fold manifest
698 698 $ hg manifest
699 699 a
700 700 b
701 701 c
702 702 d
703 703 e
704 704 f
705 705
706 706 $ cd ..
@@ -1,80 +1,80 b''
1 1 #testcases abortcommand abortflag
2 2
3 3 #if abortflag
4 4 $ cat >> $HGRCPATH <<EOF
5 5 > [alias]
6 6 > abort = histedit --abort
7 7 > EOF
8 8 #endif
9 9
10 10 $ . "$TESTDIR/histedit-helpers.sh"
11 11
12 12 Enable extension used by this test
13 13 $ cat >>$HGRCPATH <<EOF
14 14 > [extensions]
15 15 > histedit=
16 16 > EOF
17 17
18 18 =================================
19 19 Test backup-bundle config option|
20 20 =================================
21 21 Repo setup:
22 22 $ hg init foo
23 23 $ cd foo
24 24 $ echo first>file
25 25 $ hg ci -qAm one
26 26 $ echo second>>file
27 27 $ hg ci -m two
28 28 $ echo third>>file
29 29 $ hg ci -m three
30 30 $ echo forth>>file
31 31 $ hg ci -m four
32 32 $ hg log -G --style compact
33 33 @ 3[tip] 7d5187087c79 1970-01-01 00:00 +0000 test
34 34 | four
35 35 |
36 36 o 2 80d23dfa866d 1970-01-01 00:00 +0000 test
37 37 | three
38 38 |
39 39 o 1 6153eb23e623 1970-01-01 00:00 +0000 test
40 40 | two
41 41 |
42 42 o 0 36b4bdd91f5b 1970-01-01 00:00 +0000 test
43 43 one
44 44
45 45 Test when `backup-bundle` config option is enabled:
46 46 $ hg histedit -r '36b4bdd91f5b' --commands - << EOF
47 47 > pick 36b4bdd91f5b 0 one
48 48 > pick 6153eb23e623 1 two
49 49 > roll 80d23dfa866d 2 three
50 50 > edit 7d5187087c79 3 four
51 51 > EOF
52 52 merging file
53 53 Editing (7d5187087c79), you may commit or record as needed now.
54 54 (hg histedit --continue to resume)
55 [1]
55 [240]
56 56 $ hg abort
57 57 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
58 58 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/1d8f701c7b35-cf7be322-backup.hg
59 59 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/5c0056670bce-b54b65d0-backup.hg
60 60
61 61 Test when `backup-bundle` config option is not enabled
62 62 Enable config option:
63 63 $ cat >>$HGRCPATH <<EOF
64 64 > [rewrite]
65 65 > backup-bundle = False
66 66 > EOF
67 67
68 68 $ hg histedit -r '36b4bdd91f5b' --commands - << EOF
69 69 > pick 36b4bdd91f5b 0 one
70 70 > pick 6153eb23e623 1 two
71 71 > roll 80d23dfa866d 2 three
72 72 > edit 7d5187087c79 3 four
73 73 > EOF
74 74 merging file
75 75 Editing (7d5187087c79), you may commit or record as needed now.
76 76 (hg histedit --continue to resume)
77 [1]
77 [240]
78 78
79 79 $ hg abort
80 80 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now