##// END OF EJS Templates
branching: merge stable into default
Raphaël Gomès -
r50559:f56873a7 merge default
parent child Browse files
Show More
@@ -1,1232 +1,1232 b''
1 1 # help.py - help data for mercurial
2 2 #
3 3 # Copyright 2006 Olivia Mackall <olivia@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
9 9 import itertools
10 10 import re
11 11 import textwrap
12 12
13 13 from typing import (
14 14 Callable,
15 15 Dict,
16 16 Iterable,
17 17 List,
18 18 Optional,
19 19 Set,
20 20 Tuple,
21 21 Union,
22 22 cast,
23 23 )
24 24
25 25 from .i18n import (
26 26 _,
27 27 gettext,
28 28 )
29 29 from .pycompat import getattr
30 30 from . import (
31 31 cmdutil,
32 32 encoding,
33 33 error,
34 34 extensions,
35 35 fancyopts,
36 36 filemerge,
37 37 fileset,
38 38 minirst,
39 39 pycompat,
40 40 registrar,
41 41 revset,
42 42 templatefilters,
43 43 templatefuncs,
44 44 templatekw,
45 45 ui as uimod,
46 46 util,
47 47 )
48 48 from .hgweb import webcommands
49 49 from .utils import (
50 50 compression,
51 51 resourceutil,
52 52 stringutil,
53 53 )
54 54
55 55 _DocLoader = Callable[[uimod.ui], bytes]
56 56 # Old extensions may not register with a category
57 57 _HelpEntry = Union["_HelpEntryNoCategory", "_HelpEntryWithCategory"]
58 58 _HelpEntryNoCategory = Tuple[List[bytes], bytes, _DocLoader]
59 59 _HelpEntryWithCategory = Tuple[List[bytes], bytes, _DocLoader, bytes]
60 60 _SelectFn = Callable[[object], bool]
61 61 _SynonymTable = Dict[bytes, List[bytes]]
62 62 _TopicHook = Callable[[uimod.ui, bytes, bytes], bytes]
63 63
64 64 _exclkeywords: Set[bytes] = {
65 65 b"(ADVANCED)",
66 66 b"(DEPRECATED)",
67 67 b"(EXPERIMENTAL)",
68 68 # i18n: "(ADVANCED)" is a keyword, must be translated consistently
69 69 _(b"(ADVANCED)"),
70 70 # i18n: "(DEPRECATED)" is a keyword, must be translated consistently
71 71 _(b"(DEPRECATED)"),
72 72 # i18n: "(EXPERIMENTAL)" is a keyword, must be translated consistently
73 73 _(b"(EXPERIMENTAL)"),
74 74 }
75 75
76 76 # The order in which command categories will be displayed.
77 77 # Extensions with custom categories should insert them into this list
78 78 # after/before the appropriate item, rather than replacing the list or
79 79 # assuming absolute positions.
80 80 CATEGORY_ORDER: List[bytes] = [
81 81 registrar.command.CATEGORY_REPO_CREATION,
82 82 registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT,
83 83 registrar.command.CATEGORY_COMMITTING,
84 84 registrar.command.CATEGORY_CHANGE_MANAGEMENT,
85 85 registrar.command.CATEGORY_CHANGE_ORGANIZATION,
86 86 registrar.command.CATEGORY_FILE_CONTENTS,
87 87 registrar.command.CATEGORY_CHANGE_NAVIGATION,
88 88 registrar.command.CATEGORY_WORKING_DIRECTORY,
89 89 registrar.command.CATEGORY_IMPORT_EXPORT,
90 90 registrar.command.CATEGORY_MAINTENANCE,
91 91 registrar.command.CATEGORY_HELP,
92 92 registrar.command.CATEGORY_MISC,
93 93 registrar.command.CATEGORY_NONE,
94 94 ]
95 95
96 96 # Human-readable category names. These are translated.
97 97 # Extensions with custom categories should add their names here.
98 98 CATEGORY_NAMES: Dict[bytes, bytes] = {
99 99 registrar.command.CATEGORY_REPO_CREATION: b'Repository creation',
100 100 registrar.command.CATEGORY_REMOTE_REPO_MANAGEMENT: b'Remote repository management',
101 101 registrar.command.CATEGORY_COMMITTING: b'Change creation',
102 102 registrar.command.CATEGORY_CHANGE_NAVIGATION: b'Change navigation',
103 103 registrar.command.CATEGORY_CHANGE_MANAGEMENT: b'Change manipulation',
104 104 registrar.command.CATEGORY_CHANGE_ORGANIZATION: b'Change organization',
105 105 registrar.command.CATEGORY_WORKING_DIRECTORY: b'Working directory management',
106 106 registrar.command.CATEGORY_FILE_CONTENTS: b'File content management',
107 107 registrar.command.CATEGORY_IMPORT_EXPORT: b'Change import/export',
108 108 registrar.command.CATEGORY_MAINTENANCE: b'Repository maintenance',
109 109 registrar.command.CATEGORY_HELP: b'Help',
110 110 registrar.command.CATEGORY_MISC: b'Miscellaneous commands',
111 111 registrar.command.CATEGORY_NONE: b'Uncategorized commands',
112 112 }
113 113
114 114 # Topic categories.
115 115 TOPIC_CATEGORY_IDS = b'ids'
116 116 TOPIC_CATEGORY_OUTPUT = b'output'
117 117 TOPIC_CATEGORY_CONFIG = b'config'
118 118 TOPIC_CATEGORY_CONCEPTS = b'concepts'
119 119 TOPIC_CATEGORY_MISC = b'misc'
120 120 TOPIC_CATEGORY_NONE = b'none'
121 121
122 122 # The order in which topic categories will be displayed.
123 123 # Extensions with custom categories should insert them into this list
124 124 # after/before the appropriate item, rather than replacing the list or
125 125 # assuming absolute positions.
126 126 TOPIC_CATEGORY_ORDER: List[bytes] = [
127 127 TOPIC_CATEGORY_IDS,
128 128 TOPIC_CATEGORY_OUTPUT,
129 129 TOPIC_CATEGORY_CONFIG,
130 130 TOPIC_CATEGORY_CONCEPTS,
131 131 TOPIC_CATEGORY_MISC,
132 132 TOPIC_CATEGORY_NONE,
133 133 ]
134 134
135 135 # Human-readable topic category names. These are translated.
136 136 TOPIC_CATEGORY_NAMES: Dict[bytes, bytes] = {
137 137 TOPIC_CATEGORY_IDS: b'Mercurial identifiers',
138 138 TOPIC_CATEGORY_OUTPUT: b'Mercurial output',
139 139 TOPIC_CATEGORY_CONFIG: b'Mercurial configuration',
140 140 TOPIC_CATEGORY_CONCEPTS: b'Concepts',
141 141 TOPIC_CATEGORY_MISC: b'Miscellaneous',
142 142 TOPIC_CATEGORY_NONE: b'Uncategorized topics',
143 143 }
144 144
145 145
146 146 def listexts(
147 147 header: bytes,
148 148 exts: Dict[bytes, bytes],
149 149 indent: int = 1,
150 150 showdeprecated: bool = False,
151 151 ) -> List[bytes]:
152 152 '''return a text listing of the given extensions'''
153 153 rst = []
154 154 if exts:
155 155 for name, desc in sorted(exts.items()):
156 156 if not showdeprecated and any(w in desc for w in _exclkeywords):
157 157 continue
158 158 rst.append(b'%s:%s: %s\n' % (b' ' * indent, name, desc))
159 159 if rst:
160 160 rst.insert(0, b'\n%s\n\n' % header)
161 161 return rst
162 162
163 163
164 164 def extshelp(ui: uimod.ui) -> bytes:
165 165 rst = loaddoc(b'extensions')(ui).splitlines(True)
166 166 rst.extend(
167 167 listexts(
168 168 _(b'enabled extensions:'), extensions.enabled(), showdeprecated=True
169 169 )
170 170 )
171 171 rst.extend(
172 172 listexts(
173 173 _(b'disabled extensions:'),
174 174 extensions.disabled(),
175 175 showdeprecated=ui.verbose,
176 176 )
177 177 )
178 178 doc = b''.join(rst)
179 179 return doc
180 180
181 181
182 182 def parsedefaultmarker(text: bytes) -> Optional[Tuple[bytes, List[bytes]]]:
183 183 """given a text 'abc (DEFAULT: def.ghi)',
184 184 returns (b'abc', (b'def', b'ghi')). Otherwise return None"""
185 185 if text[-1:] == b')':
186 186 marker = b' (DEFAULT: '
187 187 pos = text.find(marker)
188 188 if pos >= 0:
189 189 item = text[pos + len(marker) : -1]
190 190 return text[:pos], item.split(b'.', 2)
191 191
192 192
193 193 def optrst(header: bytes, options, verbose: bool, ui: uimod.ui) -> bytes:
194 194 data = []
195 195 multioccur = False
196 196 for option in options:
197 197 if len(option) == 5:
198 198 shortopt, longopt, default, desc, optlabel = option
199 199 else:
200 200 shortopt, longopt, default, desc = option
201 201 optlabel = _(b"VALUE") # default label
202 202
203 203 if not verbose and any(w in desc for w in _exclkeywords):
204 204 continue
205 205 defaultstrsuffix = b''
206 206 if default is None:
207 207 parseresult = parsedefaultmarker(desc)
208 208 if parseresult is not None:
209 209 (desc, (section, name)) = parseresult
210 210 if ui.configbool(section, name):
211 211 default = True
212 212 defaultstrsuffix = _(b' from config')
213 213 so = b''
214 214 if shortopt:
215 215 so = b'-' + shortopt
216 216 lo = b'--' + longopt
217 217 if default is True:
218 218 lo = b'--[no-]' + longopt
219 219
220 220 if isinstance(default, fancyopts.customopt):
221 221 default = default.getdefaultvalue()
222 222 if default and not callable(default):
223 223 # default is of unknown type, and in Python 2 we abused
224 224 # the %s-shows-repr property to handle integers etc. To
225 225 # match that behavior on Python 3, we do str(default) and
226 226 # then convert it to bytes.
227 227 defaultstr = pycompat.bytestr(default)
228 228 if default is True:
229 229 defaultstr = _(b"on")
230 230 desc += _(b" (default: %s)") % (defaultstr + defaultstrsuffix)
231 231
232 232 if isinstance(default, list):
233 233 lo += b" %s [+]" % optlabel
234 234 multioccur = True
235 235 elif (default is not None) and not isinstance(default, bool):
236 236 lo += b" %s" % optlabel
237 237
238 238 data.append((so, lo, desc))
239 239
240 240 if multioccur:
241 241 header += _(b" ([+] can be repeated)")
242 242
243 243 rst = [b'\n%s:\n\n' % header]
244 244 rst.extend(minirst.maketable(data, 1))
245 245
246 246 return b''.join(rst)
247 247
248 248
249 249 def indicateomitted(
250 250 rst: List[bytes], omitted: bytes, notomitted: Optional[bytes] = None
251 251 ) -> None:
252 252 rst.append(b'\n\n.. container:: omitted\n\n %s\n\n' % omitted)
253 253 if notomitted:
254 254 rst.append(b'\n\n.. container:: notomitted\n\n %s\n\n' % notomitted)
255 255
256 256
257 257 def filtercmd(ui: uimod.ui, cmd: bytes, func, kw: bytes, doc: bytes) -> bool:
258 258 if not ui.debugflag and cmd.startswith(b"debug") and kw != b"debug":
259 259 # Debug command, and user is not looking for those.
260 260 return True
261 261 if not ui.verbose:
262 262 if not kw and not doc:
263 263 # Command had no documentation, no point in showing it by default.
264 264 return True
265 265 if getattr(func, 'alias', False) and not getattr(func, 'owndoc', False):
266 266 # Alias didn't have its own documentation.
267 267 return True
268 268 if doc and any(w in doc for w in _exclkeywords):
269 269 # Documentation has excluded keywords.
270 270 return True
271 271 if kw == b"shortlist" and not getattr(func, 'helpbasic', False):
272 272 # We're presenting the short list but the command is not basic.
273 273 return True
274 274 if ui.configbool(b'help', b'hidden-command.%s' % cmd):
275 275 # Configuration explicitly hides the command.
276 276 return True
277 277 return False
278 278
279 279
280 280 def filtertopic(ui: uimod.ui, topic: bytes) -> bool:
281 281 return ui.configbool(b'help', b'hidden-topic.%s' % topic, False)
282 282
283 283
284 284 def topicmatch(
285 285 ui: uimod.ui, commands, kw: bytes
286 286 ) -> Dict[bytes, List[Tuple[bytes, bytes]]]:
287 287 """Return help topics matching kw.
288 288
289 289 Returns {'section': [(name, summary), ...], ...} where section is
290 290 one of topics, commands, extensions, or extensioncommands.
291 291 """
292 292 kw = encoding.lower(kw)
293 293
294 294 def lowercontains(container):
295 295 return kw in encoding.lower(container) # translated in helptable
296 296
297 297 results = {
298 298 b'topics': [],
299 299 b'commands': [],
300 300 b'extensions': [],
301 301 b'extensioncommands': [],
302 302 }
303 303 for topic in helptable:
304 304 names, header, doc = topic[0:3]
305 305 # Old extensions may use a str as doc.
306 306 if (
307 307 sum(map(lowercontains, names))
308 308 or lowercontains(header)
309 309 or (callable(doc) and lowercontains(doc(ui)))
310 310 ):
311 311 name = names[0]
312 312 if not filtertopic(ui, name):
313 313 results[b'topics'].append((names[0], header))
314 314 for cmd, entry in commands.table.items():
315 315 if len(entry) == 3:
316 316 summary = entry[2]
317 317 else:
318 318 summary = b''
319 319 # translate docs *before* searching there
320 320 func = entry[0]
321 321 docs = _(pycompat.getdoc(func)) or b''
322 322 if kw in cmd or lowercontains(summary) or lowercontains(docs):
323 323 if docs:
324 324 summary = stringutil.firstline(docs)
325 325 cmdname = cmdutil.parsealiases(cmd)[0]
326 326 if filtercmd(ui, cmdname, func, kw, docs):
327 327 continue
328 328 results[b'commands'].append((cmdname, summary))
329 329 for name, docs in itertools.chain(
330 330 extensions.enabled(False).items(),
331 331 extensions.disabled().items(),
332 332 ):
333 333 if not docs:
334 334 continue
335 335 name = name.rpartition(b'.')[-1]
336 336 if lowercontains(name) or lowercontains(docs):
337 337 # extension docs are already translated
338 338 results[b'extensions'].append((name, stringutil.firstline(docs)))
339 339 try:
340 340 mod = extensions.load(ui, name, b'')
341 341 except ImportError:
342 342 # debug message would be printed in extensions.load()
343 343 continue
344 344 for cmd, entry in getattr(mod, 'cmdtable', {}).items():
345 345 if kw in cmd or (len(entry) > 2 and lowercontains(entry[2])):
346 346 cmdname = cmdutil.parsealiases(cmd)[0]
347 347 func = entry[0]
348 348 cmddoc = pycompat.getdoc(func)
349 349 if cmddoc:
350 350 cmddoc = stringutil.firstline(gettext(cmddoc))
351 351 else:
352 352 cmddoc = _(b'(no help text available)')
353 353 if filtercmd(ui, cmdname, func, kw, cmddoc):
354 354 continue
355 355 results[b'extensioncommands'].append((cmdname, cmddoc))
356 356 return results
357 357
358 358
359 359 def loaddoc(topic: bytes, subdir: Optional[bytes] = None) -> _DocLoader:
360 360 """Return a delayed loader for help/topic.txt."""
361 361
362 362 def loader(ui: uimod.ui) -> bytes:
363 363 package = b'mercurial.helptext'
364 364 if subdir:
365 365 package += b'.' + subdir
366 366 with resourceutil.open_resource(package, topic + b'.txt') as fp:
367 367 doc = gettext(fp.read())
368 368 for rewriter in helphooks.get(topic, []):
369 369 doc = rewriter(ui, topic, doc)
370 370 return doc
371 371
372 372 return loader
373 373
374 374
375 375 internalstable: List[_HelpEntryNoCategory] = sorted(
376 376 [
377 377 (
378 378 [b'bid-merge'],
379 379 _(b'Bid Merge Algorithm'),
380 380 loaddoc(b'bid-merge', subdir=b'internals'),
381 381 ),
382 382 ([b'bundle2'], _(b'Bundle2'), loaddoc(b'bundle2', subdir=b'internals')),
383 383 ([b'bundles'], _(b'Bundles'), loaddoc(b'bundles', subdir=b'internals')),
384 384 ([b'cbor'], _(b'CBOR'), loaddoc(b'cbor', subdir=b'internals')),
385 385 ([b'censor'], _(b'Censor'), loaddoc(b'censor', subdir=b'internals')),
386 386 (
387 387 [b'changegroups'],
388 388 _(b'Changegroups'),
389 389 loaddoc(b'changegroups', subdir=b'internals'),
390 390 ),
391 391 (
392 392 [b'config'],
393 393 _(b'Config Registrar'),
394 394 loaddoc(b'config', subdir=b'internals'),
395 395 ),
396 396 (
397 397 [b'dirstate-v2'],
398 398 _(b'dirstate-v2 file format'),
399 399 loaddoc(b'dirstate-v2', subdir=b'internals'),
400 400 ),
401 401 (
402 402 [b'extensions', b'extension'],
403 403 _(b'Extension API'),
404 404 loaddoc(b'extensions', subdir=b'internals'),
405 405 ),
406 406 (
407 407 [b'mergestate'],
408 408 _(b'Mergestate'),
409 409 loaddoc(b'mergestate', subdir=b'internals'),
410 410 ),
411 411 (
412 412 [b'requirements'],
413 413 _(b'Repository Requirements'),
414 414 loaddoc(b'requirements', subdir=b'internals'),
415 415 ),
416 416 (
417 417 [b'revlogs'],
418 418 _(b'Revision Logs'),
419 419 loaddoc(b'revlogs', subdir=b'internals'),
420 420 ),
421 421 (
422 422 [b'wireprotocol'],
423 423 _(b'Wire Protocol'),
424 424 loaddoc(b'wireprotocol', subdir=b'internals'),
425 425 ),
426 426 (
427 427 [b'wireprotocolrpc'],
428 428 _(b'Wire Protocol RPC'),
429 429 loaddoc(b'wireprotocolrpc', subdir=b'internals'),
430 430 ),
431 431 (
432 432 [b'wireprotocolv2'],
433 433 _(b'Wire Protocol Version 2'),
434 434 loaddoc(b'wireprotocolv2', subdir=b'internals'),
435 435 ),
436 436 ]
437 437 )
438 438
439 439
440 440 def internalshelp(ui: uimod.ui) -> bytes:
441 441 """Generate the index for the "internals" topic."""
442 442 lines = [
443 443 b'To access a subtopic, use "hg help internals.{subtopic-name}"\n',
444 444 b'\n',
445 445 ]
446 446 for names, header, doc in internalstable:
447 447 lines.append(b' :%s: %s\n' % (names[0], header))
448 448
449 449 return b''.join(lines)
450 450
451 451
452 452 helptable: List[_HelpEntryWithCategory] = sorted(
453 453 [
454 454 (
455 455 [b'bundlespec'],
456 456 _(b"Bundle File Formats"),
457 457 loaddoc(b'bundlespec'),
458 458 TOPIC_CATEGORY_CONCEPTS,
459 459 ),
460 460 (
461 461 [b'color'],
462 462 _(b"Colorizing Outputs"),
463 463 loaddoc(b'color'),
464 464 TOPIC_CATEGORY_OUTPUT,
465 465 ),
466 466 (
467 467 [b"config", b"hgrc"],
468 468 _(b"Configuration Files"),
469 469 loaddoc(b'config'),
470 470 TOPIC_CATEGORY_CONFIG,
471 471 ),
472 472 (
473 473 [b'deprecated'],
474 474 _(b"Deprecated Features"),
475 475 loaddoc(b'deprecated'),
476 476 TOPIC_CATEGORY_MISC,
477 477 ),
478 478 (
479 479 [b"dates"],
480 480 _(b"Date Formats"),
481 481 loaddoc(b'dates'),
482 482 TOPIC_CATEGORY_OUTPUT,
483 483 ),
484 484 (
485 485 [b"flags"],
486 486 _(b"Command-line flags"),
487 487 loaddoc(b'flags'),
488 488 TOPIC_CATEGORY_CONFIG,
489 489 ),
490 490 (
491 491 [b"patterns"],
492 492 _(b"File Name Patterns"),
493 493 loaddoc(b'patterns'),
494 494 TOPIC_CATEGORY_IDS,
495 495 ),
496 496 (
497 497 [b'environment', b'env'],
498 498 _(b'Environment Variables'),
499 499 loaddoc(b'environment'),
500 500 TOPIC_CATEGORY_CONFIG,
501 501 ),
502 502 (
503 503 [
504 504 b'revisions',
505 505 b'revs',
506 506 b'revsets',
507 507 b'revset',
508 508 b'multirevs',
509 509 b'mrevs',
510 510 ],
511 511 _(b'Specifying Revisions'),
512 512 loaddoc(b'revisions'),
513 513 TOPIC_CATEGORY_IDS,
514 514 ),
515 515 (
516 516 [
517 517 b'rust',
518 518 b'rustext',
519 519 b'rhg',
520 520 ],
521 521 _(b'Rust in Mercurial'),
522 522 loaddoc(b'rust'),
523 523 TOPIC_CATEGORY_CONFIG,
524 524 ),
525 525 (
526 526 [b'filesets', b'fileset'],
527 527 _(b"Specifying File Sets"),
528 528 loaddoc(b'filesets'),
529 529 TOPIC_CATEGORY_IDS,
530 530 ),
531 531 (
532 532 [b'diffs'],
533 533 _(b'Diff Formats'),
534 534 loaddoc(b'diffs'),
535 535 TOPIC_CATEGORY_OUTPUT,
536 536 ),
537 537 (
538 538 [b'merge-tools', b'mergetools', b'mergetool'],
539 539 _(b'Merge Tools'),
540 540 loaddoc(b'merge-tools'),
541 541 TOPIC_CATEGORY_CONFIG,
542 542 ),
543 543 (
544 544 [b'templating', b'templates', b'template', b'style'],
545 545 _(b'Template Usage'),
546 546 loaddoc(b'templates'),
547 547 TOPIC_CATEGORY_OUTPUT,
548 548 ),
549 549 ([b'urls'], _(b'URL Paths'), loaddoc(b'urls'), TOPIC_CATEGORY_IDS),
550 550 (
551 551 [b"extensions"],
552 552 _(b"Using Additional Features"),
553 553 extshelp,
554 554 TOPIC_CATEGORY_CONFIG,
555 555 ),
556 556 (
557 557 [b"subrepos", b"subrepo"],
558 558 _(b"Subrepositories"),
559 559 loaddoc(b'subrepos'),
560 560 TOPIC_CATEGORY_CONCEPTS,
561 561 ),
562 562 (
563 563 [b"hgweb"],
564 564 _(b"Configuring hgweb"),
565 565 loaddoc(b'hgweb'),
566 566 TOPIC_CATEGORY_CONFIG,
567 567 ),
568 568 (
569 569 [b"glossary"],
570 570 _(b"Glossary"),
571 571 loaddoc(b'glossary'),
572 572 TOPIC_CATEGORY_CONCEPTS,
573 573 ),
574 574 (
575 575 [b"hgignore", b"ignore"],
576 576 _(b"Syntax for Mercurial Ignore Files"),
577 577 loaddoc(b'hgignore'),
578 578 TOPIC_CATEGORY_IDS,
579 579 ),
580 580 (
581 581 [b"phases"],
582 582 _(b"Working with Phases"),
583 583 loaddoc(b'phases'),
584 584 TOPIC_CATEGORY_CONCEPTS,
585 585 ),
586 586 (
587 587 [b"evolution"],
588 588 _(b"Safely rewriting history (EXPERIMENTAL)"),
589 589 loaddoc(b'evolution'),
590 590 TOPIC_CATEGORY_CONCEPTS,
591 591 ),
592 592 (
593 593 [b'scripting'],
594 594 _(b'Using Mercurial from scripts and automation'),
595 595 loaddoc(b'scripting'),
596 596 TOPIC_CATEGORY_MISC,
597 597 ),
598 598 (
599 599 [b'internals'],
600 600 _(b"Technical implementation topics"),
601 601 internalshelp,
602 602 TOPIC_CATEGORY_MISC,
603 603 ),
604 604 (
605 605 [b'pager'],
606 606 _(b"Pager Support"),
607 607 loaddoc(b'pager'),
608 608 TOPIC_CATEGORY_CONFIG,
609 609 ),
610 610 ]
611 611 )
612 612
613 613 # Maps topics with sub-topics to a list of their sub-topics.
614 614 subtopics: Dict[bytes, List[_HelpEntryNoCategory]] = {
615 615 b'internals': internalstable,
616 616 }
617 617
618 618 # Map topics to lists of callable taking the current topic help and
619 619 # returning the updated version
620 620 helphooks: Dict[bytes, List[_TopicHook]] = {}
621 621
622 622
623 623 def addtopichook(topic: bytes, rewriter: _TopicHook) -> None:
624 624 helphooks.setdefault(topic, []).append(rewriter)
625 625
626 626
627 627 def makeitemsdoc(
628 628 ui: uimod.ui,
629 629 topic: bytes,
630 630 doc: bytes,
631 631 marker: bytes,
632 632 items: Dict[bytes, bytes],
633 633 dedent: bool = False,
634 634 ) -> bytes:
635 635 """Extract docstring from the items key to function mapping, build a
636 636 single documentation block and use it to overwrite the marker in doc.
637 637 """
638 638 entries = []
639 639 for name in sorted(items):
640 640 text = (pycompat.getdoc(items[name]) or b'').rstrip()
641 641 if not text or not ui.verbose and any(w in text for w in _exclkeywords):
642 642 continue
643 643 text = gettext(text)
644 644 if dedent:
645 645 # Abuse latin1 to use textwrap.dedent() on bytes.
646 646 text = textwrap.dedent(text.decode('latin1')).encode('latin1')
647 647 lines = text.splitlines()
648 648 doclines = [lines[0]]
649 649 for l in lines[1:]:
650 650 # Stop once we find some Python doctest
651 651 if l.strip().startswith(b'>>>'):
652 652 break
653 653 if dedent:
654 654 doclines.append(l.rstrip())
655 655 else:
656 656 doclines.append(b' ' + l.strip())
657 657 entries.append(b'\n'.join(doclines))
658 658 entries = b'\n\n'.join(entries)
659 659 return doc.replace(marker, entries)
660 660
661 661
662 662 def addtopicsymbols(
663 663 topic: bytes, marker: bytes, symbols, dedent: bool = False
664 664 ) -> None:
665 665 def add(ui: uimod.ui, topic: bytes, doc: bytes):
666 666 return makeitemsdoc(ui, topic, doc, marker, symbols, dedent=dedent)
667 667
668 668 addtopichook(topic, add)
669 669
670 670
671 671 addtopicsymbols(
672 672 b'bundlespec',
673 673 b'.. bundlecompressionmarker',
674 674 compression.bundlecompressiontopics(),
675 675 )
676 676 addtopicsymbols(b'filesets', b'.. predicatesmarker', fileset.symbols)
677 677 addtopicsymbols(
678 678 b'merge-tools', b'.. internaltoolsmarker', filemerge.internalsdoc
679 679 )
680 680 addtopicsymbols(b'revisions', b'.. predicatesmarker', revset.symbols)
681 681 addtopicsymbols(b'templates', b'.. keywordsmarker', templatekw.keywords)
682 682 addtopicsymbols(b'templates', b'.. filtersmarker', templatefilters.filters)
683 683 addtopicsymbols(b'templates', b'.. functionsmarker', templatefuncs.funcs)
684 684 addtopicsymbols(
685 685 b'hgweb', b'.. webcommandsmarker', webcommands.commands, dedent=True
686 686 )
687 687
688 688
689 689 def inserttweakrc(ui: uimod.ui, topic: bytes, doc: bytes) -> bytes:
690 690 marker = b'.. tweakdefaultsmarker'
691 691 repl = uimod.tweakrc
692 692
693 693 def sub(m):
694 694 lines = [m.group(1) + s for s in repl.splitlines()]
695 695 return b'\n'.join(lines)
696 696
697 697 return re.sub(br'( *)%s' % re.escape(marker), sub, doc)
698 698
699 699
700 700 def _getcategorizedhelpcmds(
701 701 ui: uimod.ui, cmdtable, name: bytes, select: Optional[_SelectFn] = None
702 702 ) -> Tuple[Dict[bytes, List[bytes]], Dict[bytes, bytes], _SynonymTable]:
703 703 # Category -> list of commands
704 704 cats = {}
705 705 # Command -> short description
706 706 h = {}
707 707 # Command -> string showing synonyms
708 708 syns = {}
709 709 for c, e in cmdtable.items():
710 710 fs = cmdutil.parsealiases(c)
711 711 f = fs[0]
712 712 syns[f] = fs
713 713 func = e[0]
714 714 if select and not select(f):
715 715 continue
716 716 doc = pycompat.getdoc(func)
717 717 if filtercmd(ui, f, func, name, doc):
718 718 continue
719 719 doc = gettext(doc)
720 720 if not doc:
721 721 doc = _(b"(no help text available)")
722 722 h[f] = stringutil.firstline(doc).rstrip()
723 723
724 724 cat = getattr(func, 'helpcategory', None) or (
725 725 registrar.command.CATEGORY_NONE
726 726 )
727 727 cats.setdefault(cat, []).append(f)
728 728 return cats, h, syns
729 729
730 730
731 731 def _getcategorizedhelptopics(
732 732 ui: uimod.ui, topictable: List[_HelpEntry]
733 733 ) -> Tuple[Dict[bytes, List[Tuple[bytes, bytes]]], Dict[bytes, List[bytes]]]:
734 734 # Group commands by category.
735 735 topiccats = {}
736 736 syns = {}
737 737 for topic in topictable:
738 738 names, header, doc = topic[0:3]
739 739 if len(topic) > 3 and topic[3]:
740 740 category: bytes = cast(bytes, topic[3]) # help pytype
741 741 else:
742 742 category: bytes = TOPIC_CATEGORY_NONE
743 743
744 744 topicname = names[0]
745 745 syns[topicname] = list(names)
746 746 if not filtertopic(ui, topicname):
747 747 topiccats.setdefault(category, []).append((topicname, header))
748 748 return topiccats, syns
749 749
750 750
751 751 addtopichook(b'config', inserttweakrc)
752 752
753 753
754 754 def help_(
755 755 ui: uimod.ui,
756 756 commands,
757 757 name: bytes,
758 758 unknowncmd: bool = False,
759 759 full: bool = True,
760 760 subtopic: Optional[bytes] = None,
761 761 fullname: Optional[bytes] = None,
762 762 **opts
763 763 ) -> bytes:
764 764 """
765 765 Generate the help for 'name' as unformatted restructured text. If
766 766 'name' is None, describe the commands available.
767 767 """
768 768
769 769 opts = pycompat.byteskwargs(opts)
770 770
771 771 def helpcmd(name: bytes, subtopic: Optional[bytes]) -> List[bytes]:
772 772 try:
773 773 aliases, entry = cmdutil.findcmd(
774 774 name, commands.table, strict=unknowncmd
775 775 )
776 776 except error.AmbiguousCommand as inst:
777 777 # py3 fix: except vars can't be used outside the scope of the
778 778 # except block, nor can be used inside a lambda. python issue4617
779 779 prefix = inst.prefix
780 780 select = lambda c: cmdutil.parsealiases(c)[0].startswith(prefix)
781 781 rst = helplist(select)
782 782 return rst
783 783
784 784 rst = []
785 785
786 786 # check if it's an invalid alias and display its error if it is
787 787 if getattr(entry[0], 'badalias', None):
788 788 rst.append(entry[0].badalias + b'\n')
789 789 if entry[0].unknowncmd:
790 790 try:
791 791 rst.extend(helpextcmd(entry[0].cmdname))
792 792 except error.UnknownCommand:
793 793 pass
794 794 return rst
795 795
796 796 # synopsis
797 797 if len(entry) > 2:
798 798 if entry[2].startswith(b'hg'):
799 799 rst.append(b"%s\n" % entry[2])
800 800 else:
801 801 rst.append(b'hg %s %s\n' % (aliases[0], entry[2]))
802 802 else:
803 803 rst.append(b'hg %s\n' % aliases[0])
804 804 # aliases
805 805 if full and not ui.quiet and len(aliases) > 1:
806 806 rst.append(_(b"\naliases: %s\n") % b', '.join(aliases[1:]))
807 807 rst.append(b'\n')
808 808
809 809 # description
810 810 doc = gettext(pycompat.getdoc(entry[0]))
811 811 if not doc:
812 812 doc = _(b"(no help text available)")
813 813 if util.safehasattr(entry[0], b'definition'): # aliased command
814 814 source = entry[0].source
815 815 if entry[0].definition.startswith(b'!'): # shell alias
816 816 doc = _(b'shell alias for: %s\n\n%s\n\ndefined by: %s\n') % (
817 817 entry[0].definition[1:],
818 818 doc,
819 819 source,
820 820 )
821 821 else:
822 822 doc = _(b'alias for: hg %s\n\n%s\n\ndefined by: %s\n') % (
823 823 entry[0].definition,
824 824 doc,
825 825 source,
826 826 )
827 827 doc = doc.splitlines(True)
828 828 if ui.quiet or not full:
829 829 rst.append(doc[0])
830 830 else:
831 831 rst.extend(doc)
832 832 rst.append(b'\n')
833 833
834 834 # check if this command shadows a non-trivial (multi-line)
835 835 # extension help text
836 836 try:
837 837 mod = extensions.find(name)
838 838 doc = gettext(pycompat.getdoc(mod)) or b''
839 839 if b'\n' in doc.strip():
840 840 msg = _(
841 841 b"(use 'hg help -e %s' to show help for "
842 842 b"the %s extension)"
843 843 ) % (name, name)
844 844 rst.append(b'\n%s\n' % msg)
845 845 except KeyError:
846 846 pass
847 847
848 848 # options
849 849 if not ui.quiet and entry[1]:
850 850 rst.append(optrst(_(b"options"), entry[1], ui.verbose, ui))
851 851
852 852 if ui.verbose:
853 853 rst.append(
854 854 optrst(
855 855 _(b"global options"), commands.globalopts, ui.verbose, ui
856 856 )
857 857 )
858 858
859 859 if not ui.verbose:
860 860 if not full:
861 861 rst.append(_(b"\n(use 'hg %s -h' to show more help)\n") % name)
862 862 elif not ui.quiet:
863 863 rst.append(
864 864 _(
865 865 b'\n(some details hidden, use --verbose '
866 866 b'to show complete help)'
867 867 )
868 868 )
869 869
870 870 return rst
871 871
872 872 def helplist(select: Optional[_SelectFn] = None, **opts) -> List[bytes]:
873 873 cats, h, syns = _getcategorizedhelpcmds(
874 874 ui, commands.table, name, select
875 875 )
876 876
877 877 rst = []
878 878 if not h:
879 879 if not ui.quiet:
880 880 rst.append(_(b'no commands defined\n'))
881 881 return rst
882 882
883 883 # Output top header.
884 884 if not ui.quiet:
885 885 if name == b"shortlist":
886 886 rst.append(_(b'basic commands:\n\n'))
887 887 elif name == b"debug":
888 888 rst.append(_(b'debug commands (internal and unsupported):\n\n'))
889 889 else:
890 890 rst.append(_(b'list of commands:\n'))
891 891
892 892 def appendcmds(cmds: Iterable[bytes]) -> None:
893 893 cmds = sorted(cmds)
894 894 for c in cmds:
895 895 display_cmd = c
896 896 if ui.verbose:
897 897 display_cmd = b', '.join(syns[c])
898 898 display_cmd = display_cmd.replace(b':', br'\:')
899 899 rst.append(b' :%s: %s\n' % (display_cmd, h[c]))
900 900
901 901 if name in (b'shortlist', b'debug'):
902 902 # List without categories.
903 903 appendcmds(h)
904 904 else:
905 905 # Check that all categories have an order.
906 906 missing_order = set(cats.keys()) - set(CATEGORY_ORDER)
907 907 if missing_order:
908 908 ui.develwarn(
909 909 b'help categories missing from CATEGORY_ORDER: %s'
910 % missing_order
910 % stringutil.forcebytestr(missing_order)
911 911 )
912 912
913 913 # List per category.
914 914 for cat in CATEGORY_ORDER:
915 915 catfns = cats.get(cat, [])
916 916 if catfns:
917 917 if len(cats) > 1:
918 918 catname = gettext(CATEGORY_NAMES[cat])
919 919 rst.append(b"\n%s:\n" % catname)
920 920 rst.append(b"\n")
921 921 appendcmds(catfns)
922 922
923 923 ex = opts.get
924 924 anyopts = ex('keyword') or not (ex('command') or ex('extension'))
925 925 if not name and anyopts:
926 926 exts = listexts(
927 927 _(b'enabled extensions:'),
928 928 extensions.enabled(),
929 929 showdeprecated=ui.verbose,
930 930 )
931 931 if exts:
932 932 rst.append(b'\n')
933 933 rst.extend(exts)
934 934
935 935 rst.append(_(b"\nadditional help topics:\n"))
936 936 topiccats, topicsyns = _getcategorizedhelptopics(ui, helptable)
937 937
938 938 # Check that all categories have an order.
939 939 missing_order = set(topiccats.keys()) - set(TOPIC_CATEGORY_ORDER)
940 940 if missing_order:
941 941 ui.develwarn(
942 942 b'help categories missing from TOPIC_CATEGORY_ORDER: %s'
943 % missing_order
943 % stringutil.forcebytestr(missing_order)
944 944 )
945 945
946 946 # Output topics per category.
947 947 for cat in TOPIC_CATEGORY_ORDER:
948 948 topics = topiccats.get(cat, [])
949 949 if topics:
950 950 if len(topiccats) > 1:
951 951 catname = gettext(TOPIC_CATEGORY_NAMES[cat])
952 952 rst.append(b"\n%s:\n" % catname)
953 953 rst.append(b"\n")
954 954 for t, desc in topics:
955 955 rst.append(b" :%s: %s\n" % (t, desc))
956 956
957 957 if ui.quiet:
958 958 pass
959 959 elif ui.verbose:
960 960 rst.append(
961 961 b'\n%s\n'
962 962 % optrst(
963 963 _(b"global options"), commands.globalopts, ui.verbose, ui
964 964 )
965 965 )
966 966 if name == b'shortlist':
967 967 rst.append(
968 968 _(b"\n(use 'hg help' for the full list of commands)\n")
969 969 )
970 970 else:
971 971 if name == b'shortlist':
972 972 rst.append(
973 973 _(
974 974 b"\n(use 'hg help' for the full list of commands "
975 975 b"or 'hg -v' for details)\n"
976 976 )
977 977 )
978 978 elif name and not full:
979 979 rst.append(
980 980 _(b"\n(use 'hg help %s' to show the full help text)\n")
981 981 % name
982 982 )
983 983 elif name and syns and name in syns.keys():
984 984 rst.append(
985 985 _(
986 986 b"\n(use 'hg help -v -e %s' to show built-in "
987 987 b"aliases and global options)\n"
988 988 )
989 989 % name
990 990 )
991 991 else:
992 992 rst.append(
993 993 _(
994 994 b"\n(use 'hg help -v%s' to show built-in aliases "
995 995 b"and global options)\n"
996 996 )
997 997 % (name and b" " + name or b"")
998 998 )
999 999 return rst
1000 1000
1001 1001 def helptopic(name: bytes, subtopic: Optional[bytes] = None) -> List[bytes]:
1002 1002 # Look for sub-topic entry first.
1003 1003 header, doc = None, None
1004 1004 if subtopic and name in subtopics:
1005 1005 for names, header, doc in subtopics[name]:
1006 1006 if subtopic in names:
1007 1007 break
1008 1008 if not any(subtopic in s[0] for s in subtopics[name]):
1009 1009 raise error.UnknownCommand(name)
1010 1010
1011 1011 if not header:
1012 1012 for topic in helptable:
1013 1013 names, header, doc = topic[0:3]
1014 1014 if name in names:
1015 1015 break
1016 1016 else:
1017 1017 raise error.UnknownCommand(name)
1018 1018
1019 1019 rst = [minirst.section(header)]
1020 1020
1021 1021 # description
1022 1022 if not doc:
1023 1023 rst.append(b" %s\n" % _(b"(no help text available)"))
1024 1024 if callable(doc):
1025 1025 rst += [b" %s\n" % l for l in doc(ui).splitlines()]
1026 1026
1027 1027 if not ui.verbose:
1028 1028 omitted = _(
1029 1029 b'(some details hidden, use --verbose'
1030 1030 b' to show complete help)'
1031 1031 )
1032 1032 indicateomitted(rst, omitted)
1033 1033
1034 1034 try:
1035 1035 cmdutil.findcmd(name, commands.table)
1036 1036 rst.append(
1037 1037 _(b"\nuse 'hg help -c %s' to see help for the %s command\n")
1038 1038 % (name, name)
1039 1039 )
1040 1040 except error.UnknownCommand:
1041 1041 pass
1042 1042 return rst
1043 1043
1044 1044 def helpext(name: bytes, subtopic: Optional[bytes] = None) -> List[bytes]:
1045 1045 try:
1046 1046 mod = extensions.find(name)
1047 1047 doc = gettext(pycompat.getdoc(mod)) or _(b'no help text available')
1048 1048 except KeyError:
1049 1049 mod = None
1050 1050 doc = extensions.disabled_help(name)
1051 1051 if not doc:
1052 1052 raise error.UnknownCommand(name)
1053 1053
1054 1054 if b'\n' not in doc:
1055 1055 head, tail = doc, b""
1056 1056 else:
1057 1057 head, tail = doc.split(b'\n', 1)
1058 1058 rst = [_(b'%s extension - %s\n\n') % (name.rpartition(b'.')[-1], head)]
1059 1059 if tail:
1060 1060 rst.extend(tail.splitlines(True))
1061 1061 rst.append(b'\n')
1062 1062
1063 1063 if not ui.verbose:
1064 1064 omitted = _(
1065 1065 b'(some details hidden, use --verbose'
1066 1066 b' to show complete help)'
1067 1067 )
1068 1068 indicateomitted(rst, omitted)
1069 1069
1070 1070 if mod:
1071 1071 try:
1072 1072 ct = mod.cmdtable
1073 1073 except AttributeError:
1074 1074 ct = {}
1075 1075 modcmds = {c.partition(b'|')[0] for c in ct}
1076 1076 rst.extend(helplist(modcmds.__contains__))
1077 1077 else:
1078 1078 rst.append(
1079 1079 _(
1080 1080 b"(use 'hg help extensions' for information on enabling"
1081 1081 b" extensions)\n"
1082 1082 )
1083 1083 )
1084 1084 return rst
1085 1085
1086 1086 def helpextcmd(
1087 1087 name: bytes, subtopic: Optional[bytes] = None
1088 1088 ) -> List[bytes]:
1089 1089 cmd, ext, doc = extensions.disabledcmd(
1090 1090 ui, name, ui.configbool(b'ui', b'strict')
1091 1091 )
1092 1092 doc = stringutil.firstline(doc)
1093 1093
1094 1094 rst = listexts(
1095 1095 _(b"'%s' is provided by the following extension:") % cmd,
1096 1096 {ext: doc},
1097 1097 indent=4,
1098 1098 showdeprecated=True,
1099 1099 )
1100 1100 rst.append(b'\n')
1101 1101 rst.append(
1102 1102 _(
1103 1103 b"(use 'hg help extensions' for information on enabling "
1104 1104 b"extensions)\n"
1105 1105 )
1106 1106 )
1107 1107 return rst
1108 1108
1109 1109 rst = []
1110 1110 kw = opts.get(b'keyword')
1111 1111 if kw or name is None and any(opts[o] for o in opts):
1112 1112 matches = topicmatch(ui, commands, name or b'')
1113 1113 helpareas = []
1114 1114 if opts.get(b'extension'):
1115 1115 helpareas += [(b'extensions', _(b'Extensions'))]
1116 1116 if opts.get(b'command'):
1117 1117 helpareas += [(b'commands', _(b'Commands'))]
1118 1118 if not helpareas:
1119 1119 helpareas = [
1120 1120 (b'topics', _(b'Topics')),
1121 1121 (b'commands', _(b'Commands')),
1122 1122 (b'extensions', _(b'Extensions')),
1123 1123 (b'extensioncommands', _(b'Extension Commands')),
1124 1124 ]
1125 1125 for t, title in helpareas:
1126 1126 if matches[t]:
1127 1127 rst.append(b'%s:\n\n' % title)
1128 1128 rst.extend(minirst.maketable(sorted(matches[t]), 1))
1129 1129 rst.append(b'\n')
1130 1130 if not rst:
1131 1131 msg = _(b'no matches')
1132 1132 hint = _(b"try 'hg help' for a list of topics")
1133 1133 raise error.InputError(msg, hint=hint)
1134 1134 elif name and name != b'shortlist':
1135 1135 queries = []
1136 1136 if unknowncmd:
1137 1137 queries += [helpextcmd]
1138 1138 if opts.get(b'extension'):
1139 1139 queries += [helpext]
1140 1140 if opts.get(b'command'):
1141 1141 queries += [helpcmd]
1142 1142 if not queries:
1143 1143 queries = (helptopic, helpcmd, helpext, helpextcmd)
1144 1144 for f in queries:
1145 1145 try:
1146 1146 rst = f(name, subtopic)
1147 1147 break
1148 1148 except error.UnknownCommand:
1149 1149 pass
1150 1150 else:
1151 1151 if unknowncmd:
1152 1152 raise error.UnknownCommand(name)
1153 1153 else:
1154 1154 if fullname:
1155 1155 formatname = fullname
1156 1156 else:
1157 1157 formatname = name
1158 1158 if subtopic:
1159 1159 hintname = subtopic
1160 1160 else:
1161 1161 hintname = name
1162 1162 msg = _(b'no such help topic: %s') % formatname
1163 1163 hint = _(b"try 'hg help --keyword %s'") % hintname
1164 1164 raise error.InputError(msg, hint=hint)
1165 1165 else:
1166 1166 # program name
1167 1167 if not ui.quiet:
1168 1168 rst = [_(b"Mercurial Distributed SCM\n"), b'\n']
1169 1169 rst.extend(helplist(None, **pycompat.strkwargs(opts)))
1170 1170
1171 1171 return b''.join(rst)
1172 1172
1173 1173
1174 1174 def formattedhelp(
1175 1175 ui: uimod.ui,
1176 1176 commands,
1177 1177 fullname: Optional[bytes],
1178 1178 keep: Optional[Iterable[bytes]] = None,
1179 1179 unknowncmd: bool = False,
1180 1180 full: bool = True,
1181 1181 **opts
1182 1182 ) -> bytes:
1183 1183 """get help for a given topic (as a dotted name) as rendered rst
1184 1184
1185 1185 Either returns the rendered help text or raises an exception.
1186 1186 """
1187 1187 if keep is None:
1188 1188 keep = []
1189 1189 else:
1190 1190 keep = list(keep) # make a copy so we can mutate this later
1191 1191
1192 1192 # <fullname> := <name>[.<subtopic][.<section>]
1193 1193 name = subtopic = section = None
1194 1194 if fullname is not None:
1195 1195 nameparts = fullname.split(b'.')
1196 1196 name = nameparts.pop(0)
1197 1197 if nameparts and name in subtopics:
1198 1198 subtopic = nameparts.pop(0)
1199 1199 if nameparts:
1200 1200 section = encoding.lower(b'.'.join(nameparts))
1201 1201
1202 1202 textwidth = ui.configint(b'ui', b'textwidth')
1203 1203 termwidth = ui.termwidth() - 2
1204 1204 if textwidth <= 0 or termwidth < textwidth:
1205 1205 textwidth = termwidth
1206 1206 text = help_(
1207 1207 ui,
1208 1208 commands,
1209 1209 name,
1210 1210 fullname=fullname,
1211 1211 subtopic=subtopic,
1212 1212 unknowncmd=unknowncmd,
1213 1213 full=full,
1214 1214 **opts
1215 1215 )
1216 1216
1217 1217 blocks, pruned = minirst.parse(text, keep=keep)
1218 1218 if b'verbose' in pruned:
1219 1219 keep.append(b'omitted')
1220 1220 else:
1221 1221 keep.append(b'notomitted')
1222 1222 blocks, pruned = minirst.parse(text, keep=keep)
1223 1223 if section:
1224 1224 blocks = minirst.filtersections(blocks, section)
1225 1225
1226 1226 # We could have been given a weird ".foo" section without a name
1227 1227 # to look for, or we could have simply failed to found "foo.bar"
1228 1228 # because bar isn't a section of foo
1229 1229 if section and not (blocks and name):
1230 1230 raise error.InputError(_(b"help section not found: %s") % fullname)
1231 1231
1232 1232 return minirst.formatplain(blocks, textwidth)
@@ -1,1664 +1,1664 b''
1 1 # match.py - filename matching
2 2 #
3 3 # Copyright 2008, 2009 Olivia Mackall <olivia@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8
9 9 import bisect
10 10 import copy
11 11 import itertools
12 12 import os
13 13 import re
14 14
15 15 from .i18n import _
16 16 from .pycompat import open
17 17 from . import (
18 18 encoding,
19 19 error,
20 20 pathutil,
21 21 policy,
22 22 pycompat,
23 23 util,
24 24 )
25 25 from .utils import stringutil
26 26
27 27 rustmod = policy.importrust('dirstate')
28 28
29 29 allpatternkinds = (
30 30 b're',
31 31 b'glob',
32 32 b'path',
33 33 b'relglob',
34 34 b'relpath',
35 35 b'relre',
36 36 b'rootglob',
37 37 b'listfile',
38 38 b'listfile0',
39 39 b'set',
40 40 b'include',
41 41 b'subinclude',
42 42 b'rootfilesin',
43 43 )
44 44 cwdrelativepatternkinds = (b'relpath', b'glob')
45 45
46 46 propertycache = util.propertycache
47 47
48 48
49 49 def _rematcher(regex):
50 50 """compile the regexp with the best available regexp engine and return a
51 51 matcher function"""
52 52 m = util.re.compile(regex)
53 53 try:
54 54 # slightly faster, provided by facebook's re2 bindings
55 55 return m.test_match
56 56 except AttributeError:
57 57 return m.match
58 58
59 59
60 60 def _expandsets(cwd, kindpats, ctx=None, listsubrepos=False, badfn=None):
61 61 '''Returns the kindpats list with the 'set' patterns expanded to matchers'''
62 62 matchers = []
63 63 other = []
64 64
65 65 for kind, pat, source in kindpats:
66 66 if kind == b'set':
67 67 if ctx is None:
68 68 raise error.ProgrammingError(
69 69 b"fileset expression with no context"
70 70 )
71 71 matchers.append(ctx.matchfileset(cwd, pat, badfn=badfn))
72 72
73 73 if listsubrepos:
74 74 for subpath in ctx.substate:
75 75 sm = ctx.sub(subpath).matchfileset(cwd, pat, badfn=badfn)
76 76 pm = prefixdirmatcher(subpath, sm, badfn=badfn)
77 77 matchers.append(pm)
78 78
79 79 continue
80 80 other.append((kind, pat, source))
81 81 return matchers, other
82 82
83 83
84 84 def _expandsubinclude(kindpats, root):
85 85 """Returns the list of subinclude matcher args and the kindpats without the
86 86 subincludes in it."""
87 87 relmatchers = []
88 88 other = []
89 89
90 90 for kind, pat, source in kindpats:
91 91 if kind == b'subinclude':
92 92 sourceroot = pathutil.dirname(util.normpath(source))
93 93 pat = util.pconvert(pat)
94 94 path = pathutil.join(sourceroot, pat)
95 95
96 96 newroot = pathutil.dirname(path)
97 97 matcherargs = (newroot, b'', [], [b'include:%s' % path])
98 98
99 99 prefix = pathutil.canonpath(root, root, newroot)
100 100 if prefix:
101 101 prefix += b'/'
102 102 relmatchers.append((prefix, matcherargs))
103 103 else:
104 104 other.append((kind, pat, source))
105 105
106 106 return relmatchers, other
107 107
108 108
109 109 def _kindpatsalwaysmatch(kindpats):
110 110 """Checks whether the kindspats match everything, as e.g.
111 111 'relpath:.' does.
112 112 """
113 113 for kind, pat, source in kindpats:
114 114 if pat != b'' or kind not in [b'relpath', b'glob']:
115 115 return False
116 116 return True
117 117
118 118
119 119 def _buildkindpatsmatcher(
120 120 matchercls,
121 121 root,
122 122 cwd,
123 123 kindpats,
124 124 ctx=None,
125 125 listsubrepos=False,
126 126 badfn=None,
127 127 ):
128 128 matchers = []
129 129 fms, kindpats = _expandsets(
130 130 cwd,
131 131 kindpats,
132 132 ctx=ctx,
133 133 listsubrepos=listsubrepos,
134 134 badfn=badfn,
135 135 )
136 136 if kindpats:
137 137 m = matchercls(root, kindpats, badfn=badfn)
138 138 matchers.append(m)
139 139 if fms:
140 140 matchers.extend(fms)
141 141 if not matchers:
142 142 return nevermatcher(badfn=badfn)
143 143 if len(matchers) == 1:
144 144 return matchers[0]
145 145 return unionmatcher(matchers)
146 146
147 147
148 148 def match(
149 149 root,
150 150 cwd,
151 151 patterns=None,
152 152 include=None,
153 153 exclude=None,
154 154 default=b'glob',
155 155 auditor=None,
156 156 ctx=None,
157 157 listsubrepos=False,
158 158 warn=None,
159 159 badfn=None,
160 160 icasefs=False,
161 161 ):
162 162 r"""build an object to match a set of file patterns
163 163
164 164 arguments:
165 165 root - the canonical root of the tree you're matching against
166 166 cwd - the current working directory, if relevant
167 167 patterns - patterns to find
168 168 include - patterns to include (unless they are excluded)
169 169 exclude - patterns to exclude (even if they are included)
170 170 default - if a pattern in patterns has no explicit type, assume this one
171 171 auditor - optional path auditor
172 172 ctx - optional changecontext
173 173 listsubrepos - if True, recurse into subrepositories
174 174 warn - optional function used for printing warnings
175 175 badfn - optional bad() callback for this matcher instead of the default
176 176 icasefs - make a matcher for wdir on case insensitive filesystems, which
177 177 normalizes the given patterns to the case in the filesystem
178 178
179 179 a pattern is one of:
180 180 'glob:<glob>' - a glob relative to cwd
181 181 're:<regexp>' - a regular expression
182 182 'path:<path>' - a path relative to repository root, which is matched
183 183 recursively
184 184 'rootfilesin:<path>' - a path relative to repository root, which is
185 185 matched non-recursively (will not match subdirectories)
186 186 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
187 187 'relpath:<path>' - a path relative to cwd
188 188 'relre:<regexp>' - a regexp that needn't match the start of a name
189 189 'set:<fileset>' - a fileset expression
190 190 'include:<path>' - a file of patterns to read and include
191 191 'subinclude:<path>' - a file of patterns to match against files under
192 192 the same directory
193 193 '<something>' - a pattern of the specified default type
194 194
195 195 >>> def _match(root, *args, **kwargs):
196 196 ... return match(util.localpath(root), *args, **kwargs)
197 197
198 198 Usually a patternmatcher is returned:
199 199 >>> _match(b'/foo', b'.', [b're:.*\.c$', b'path:foo/a', b'*.py'])
200 200 <patternmatcher patterns='.*\\.c$|foo/a(?:/|$)|[^/]*\\.py$'>
201 201
202 202 Combining 'patterns' with 'include' (resp. 'exclude') gives an
203 203 intersectionmatcher (resp. a differencematcher):
204 204 >>> type(_match(b'/foo', b'.', [b're:.*\.c$'], include=[b'path:lib']))
205 205 <class 'mercurial.match.intersectionmatcher'>
206 206 >>> type(_match(b'/foo', b'.', [b're:.*\.c$'], exclude=[b'path:build']))
207 207 <class 'mercurial.match.differencematcher'>
208 208
209 209 Notice that, if 'patterns' is empty, an alwaysmatcher is returned:
210 210 >>> _match(b'/foo', b'.', [])
211 211 <alwaysmatcher>
212 212
213 213 The 'default' argument determines which kind of pattern is assumed if a
214 214 pattern has no prefix:
215 215 >>> _match(b'/foo', b'.', [b'.*\.c$'], default=b're')
216 216 <patternmatcher patterns='.*\\.c$'>
217 217 >>> _match(b'/foo', b'.', [b'main.py'], default=b'relpath')
218 218 <patternmatcher patterns='main\\.py(?:/|$)'>
219 219 >>> _match(b'/foo', b'.', [b'main.py'], default=b're')
220 220 <patternmatcher patterns='main.py'>
221 221
222 222 The primary use of matchers is to check whether a value (usually a file
223 223 name) matches againset one of the patterns given at initialization. There
224 224 are two ways of doing this check.
225 225
226 226 >>> m = _match(b'/foo', b'', [b're:.*\.c$', b'relpath:a'])
227 227
228 228 1. Calling the matcher with a file name returns True if any pattern
229 229 matches that file name:
230 230 >>> m(b'a')
231 231 True
232 232 >>> m(b'main.c')
233 233 True
234 234 >>> m(b'test.py')
235 235 False
236 236
237 237 2. Using the exact() method only returns True if the file name matches one
238 238 of the exact patterns (i.e. not re: or glob: patterns):
239 239 >>> m.exact(b'a')
240 240 True
241 241 >>> m.exact(b'main.c')
242 242 False
243 243 """
244 244 assert os.path.isabs(root)
245 245 cwd = os.path.join(root, util.localpath(cwd))
246 246 normalize = _donormalize
247 247 if icasefs:
248 248 dirstate = ctx.repo().dirstate
249 249 dsnormalize = dirstate.normalize
250 250
251 251 def normalize(patterns, default, root, cwd, auditor, warn):
252 252 kp = _donormalize(patterns, default, root, cwd, auditor, warn)
253 253 kindpats = []
254 254 for kind, pats, source in kp:
255 255 if kind not in (b're', b'relre'): # regex can't be normalized
256 256 p = pats
257 257 pats = dsnormalize(pats)
258 258
259 259 # Preserve the original to handle a case only rename.
260 260 if p != pats and p in dirstate:
261 261 kindpats.append((kind, p, source))
262 262
263 263 kindpats.append((kind, pats, source))
264 264 return kindpats
265 265
266 266 if patterns:
267 267 kindpats = normalize(patterns, default, root, cwd, auditor, warn)
268 268 if _kindpatsalwaysmatch(kindpats):
269 269 m = alwaysmatcher(badfn)
270 270 else:
271 271 m = _buildkindpatsmatcher(
272 272 patternmatcher,
273 273 root,
274 274 cwd,
275 275 kindpats,
276 276 ctx=ctx,
277 277 listsubrepos=listsubrepos,
278 278 badfn=badfn,
279 279 )
280 280 else:
281 281 # It's a little strange that no patterns means to match everything.
282 282 # Consider changing this to match nothing (probably using nevermatcher).
283 283 m = alwaysmatcher(badfn)
284 284
285 285 if include:
286 286 kindpats = normalize(include, b'glob', root, cwd, auditor, warn)
287 287 im = _buildkindpatsmatcher(
288 288 includematcher,
289 289 root,
290 290 cwd,
291 291 kindpats,
292 292 ctx=ctx,
293 293 listsubrepos=listsubrepos,
294 294 badfn=None,
295 295 )
296 296 m = intersectmatchers(m, im)
297 297 if exclude:
298 298 kindpats = normalize(exclude, b'glob', root, cwd, auditor, warn)
299 299 em = _buildkindpatsmatcher(
300 300 includematcher,
301 301 root,
302 302 cwd,
303 303 kindpats,
304 304 ctx=ctx,
305 305 listsubrepos=listsubrepos,
306 306 badfn=None,
307 307 )
308 308 m = differencematcher(m, em)
309 309 return m
310 310
311 311
312 312 def exact(files, badfn=None):
313 313 return exactmatcher(files, badfn=badfn)
314 314
315 315
316 316 def always(badfn=None):
317 317 return alwaysmatcher(badfn)
318 318
319 319
320 320 def never(badfn=None):
321 321 return nevermatcher(badfn)
322 322
323 323
324 324 def badmatch(match, badfn):
325 325 """Make a copy of the given matcher, replacing its bad method with the given
326 326 one.
327 327 """
328 328 m = copy.copy(match)
329 329 m.bad = badfn
330 330 return m
331 331
332 332
333 333 def _donormalize(patterns, default, root, cwd, auditor=None, warn=None):
334 334 """Convert 'kind:pat' from the patterns list to tuples with kind and
335 335 normalized and rooted patterns and with listfiles expanded."""
336 336 kindpats = []
337 337 for kind, pat in [_patsplit(p, default) for p in patterns]:
338 338 if kind in cwdrelativepatternkinds:
339 339 pat = pathutil.canonpath(root, cwd, pat, auditor=auditor)
340 340 elif kind in (b'relglob', b'path', b'rootfilesin', b'rootglob'):
341 341 pat = util.normpath(pat)
342 342 elif kind in (b'listfile', b'listfile0'):
343 343 try:
344 344 files = util.readfile(pat)
345 345 if kind == b'listfile0':
346 346 files = files.split(b'\0')
347 347 else:
348 348 files = files.splitlines()
349 349 files = [f for f in files if f]
350 350 except EnvironmentError:
351 351 raise error.Abort(_(b"unable to read file list (%s)") % pat)
352 352 for k, p, source in _donormalize(
353 353 files, default, root, cwd, auditor, warn
354 354 ):
355 355 kindpats.append((k, p, pat))
356 356 continue
357 357 elif kind == b'include':
358 358 try:
359 359 fullpath = os.path.join(root, util.localpath(pat))
360 360 includepats = readpatternfile(fullpath, warn)
361 361 for k, p, source in _donormalize(
362 362 includepats, default, root, cwd, auditor, warn
363 363 ):
364 364 kindpats.append((k, p, source or pat))
365 365 except error.Abort as inst:
366 366 raise error.Abort(
367 367 b'%s: %s'
368 368 % (
369 369 pat,
370 370 inst.message,
371 371 ) # pytype: disable=unsupported-operands
372 372 )
373 373 except IOError as inst:
374 374 if warn:
375 375 warn(
376 376 _(b"skipping unreadable pattern file '%s': %s\n")
377 377 % (pat, stringutil.forcebytestr(inst.strerror))
378 378 )
379 379 continue
380 380 # else: re or relre - which cannot be normalized
381 381 kindpats.append((kind, pat, b''))
382 382 return kindpats
383 383
384 384
385 385 class basematcher:
386 386 def __init__(self, badfn=None):
387 387 if badfn is not None:
388 388 self.bad = badfn
389 389
390 390 def __call__(self, fn):
391 391 return self.matchfn(fn)
392 392
393 393 # Callbacks related to how the matcher is used by dirstate.walk.
394 394 # Subscribers to these events must monkeypatch the matcher object.
395 395 def bad(self, f, msg):
396 396 """Callback from dirstate.walk for each explicit file that can't be
397 397 found/accessed, with an error message."""
398 398
399 399 # If an traversedir is set, it will be called when a directory discovered
400 400 # by recursive traversal is visited.
401 401 traversedir = None
402 402
403 403 @propertycache
404 404 def _files(self):
405 405 return []
406 406
407 407 def files(self):
408 408 """Explicitly listed files or patterns or roots:
409 409 if no patterns or .always(): empty list,
410 410 if exact: list exact files,
411 411 if not .anypats(): list all files and dirs,
412 412 else: optimal roots"""
413 413 return self._files
414 414
415 415 @propertycache
416 416 def _fileset(self):
417 417 return set(self._files)
418 418
419 419 def exact(self, f):
420 420 '''Returns True if f is in .files().'''
421 421 return f in self._fileset
422 422
423 423 def matchfn(self, f):
424 424 return False
425 425
426 426 def visitdir(self, dir):
427 427 """Decides whether a directory should be visited based on whether it
428 428 has potential matches in it or one of its subdirectories. This is
429 429 based on the match's primary, included, and excluded patterns.
430 430
431 431 Returns the string 'all' if the given directory and all subdirectories
432 432 should be visited. Otherwise returns True or False indicating whether
433 433 the given directory should be visited.
434 434 """
435 435 return True
436 436
437 437 def visitchildrenset(self, dir):
438 438 """Decides whether a directory should be visited based on whether it
439 439 has potential matches in it or one of its subdirectories, and
440 440 potentially lists which subdirectories of that directory should be
441 441 visited. This is based on the match's primary, included, and excluded
442 442 patterns.
443 443
444 444 This function is very similar to 'visitdir', and the following mapping
445 445 can be applied:
446 446
447 447 visitdir | visitchildrenlist
448 448 ----------+-------------------
449 449 False | set()
450 450 'all' | 'all'
451 451 True | 'this' OR non-empty set of subdirs -or files- to visit
452 452
453 453 Example:
454 454 Assume matchers ['path:foo/bar', 'rootfilesin:qux'], we would return
455 455 the following values (assuming the implementation of visitchildrenset
456 456 is capable of recognizing this; some implementations are not).
457 457
458 458 '' -> {'foo', 'qux'}
459 459 'baz' -> set()
460 460 'foo' -> {'bar'}
461 461 # Ideally this would be 'all', but since the prefix nature of matchers
462 462 # is applied to the entire matcher, we have to downgrade this to
463 463 # 'this' due to the non-prefix 'rootfilesin'-kind matcher being mixed
464 464 # in.
465 465 'foo/bar' -> 'this'
466 466 'qux' -> 'this'
467 467
468 468 Important:
469 469 Most matchers do not know if they're representing files or
470 470 directories. They see ['path:dir/f'] and don't know whether 'f' is a
471 471 file or a directory, so visitchildrenset('dir') for most matchers will
472 472 return {'f'}, but if the matcher knows it's a file (like exactmatcher
473 473 does), it may return 'this'. Do not rely on the return being a set
474 474 indicating that there are no files in this dir to investigate (or
475 475 equivalently that if there are files to investigate in 'dir' that it
476 476 will always return 'this').
477 477 """
478 478 return b'this'
479 479
480 480 def always(self):
481 481 """Matcher will match everything and .files() will be empty --
482 482 optimization might be possible."""
483 483 return False
484 484
485 485 def isexact(self):
486 486 """Matcher will match exactly the list of files in .files() --
487 487 optimization might be possible."""
488 488 return False
489 489
490 490 def prefix(self):
491 491 """Matcher will match the paths in .files() recursively --
492 492 optimization might be possible."""
493 493 return False
494 494
495 495 def anypats(self):
496 496 """None of .always(), .isexact(), and .prefix() is true --
497 497 optimizations will be difficult."""
498 498 return not self.always() and not self.isexact() and not self.prefix()
499 499
500 500
501 501 class alwaysmatcher(basematcher):
502 502 '''Matches everything.'''
503 503
504 504 def __init__(self, badfn=None):
505 505 super(alwaysmatcher, self).__init__(badfn)
506 506
507 507 def always(self):
508 508 return True
509 509
510 510 def matchfn(self, f):
511 511 return True
512 512
513 513 def visitdir(self, dir):
514 514 return b'all'
515 515
516 516 def visitchildrenset(self, dir):
517 517 return b'all'
518 518
519 519 def __repr__(self):
520 520 return r'<alwaysmatcher>'
521 521
522 522
523 523 class nevermatcher(basematcher):
524 524 '''Matches nothing.'''
525 525
526 526 def __init__(self, badfn=None):
527 527 super(nevermatcher, self).__init__(badfn)
528 528
529 529 # It's a little weird to say that the nevermatcher is an exact matcher
530 530 # or a prefix matcher, but it seems to make sense to let callers take
531 531 # fast paths based on either. There will be no exact matches, nor any
532 532 # prefixes (files() returns []), so fast paths iterating over them should
533 533 # be efficient (and correct).
534 534 def isexact(self):
535 535 return True
536 536
537 537 def prefix(self):
538 538 return True
539 539
540 540 def visitdir(self, dir):
541 541 return False
542 542
543 543 def visitchildrenset(self, dir):
544 544 return set()
545 545
546 546 def __repr__(self):
547 547 return r'<nevermatcher>'
548 548
549 549
550 550 class predicatematcher(basematcher):
551 551 """A matcher adapter for a simple boolean function"""
552 552
553 553 def __init__(self, predfn, predrepr=None, badfn=None):
554 554 super(predicatematcher, self).__init__(badfn)
555 555 self.matchfn = predfn
556 556 self._predrepr = predrepr
557 557
558 558 @encoding.strmethod
559 559 def __repr__(self):
560 560 s = stringutil.buildrepr(self._predrepr) or pycompat.byterepr(
561 561 self.matchfn
562 562 )
563 563 return b'<predicatenmatcher pred=%s>' % s
564 564
565 565
566 566 def path_or_parents_in_set(path, prefix_set):
567 567 """Returns True if `path` (or any parent of `path`) is in `prefix_set`."""
568 568 l = len(prefix_set)
569 569 if l == 0:
570 570 return False
571 571 if path in prefix_set:
572 572 return True
573 573 # If there's more than 5 paths in prefix_set, it's *probably* quicker to
574 574 # "walk up" the directory hierarchy instead, with the assumption that most
575 575 # directory hierarchies are relatively shallow and hash lookup is cheap.
576 576 if l > 5:
577 577 return any(
578 578 parentdir in prefix_set for parentdir in pathutil.finddirs(path)
579 579 )
580 580
581 581 # FIXME: Ideally we'd never get to this point if this is the case - we'd
582 582 # recognize ourselves as an 'always' matcher and skip this.
583 583 if b'' in prefix_set:
584 584 return True
585 585
586 586 sl = ord(b'/')
587 587
588 588 # We already checked that path isn't in prefix_set exactly, so
589 589 # `path[len(pf)] should never raise IndexError.
590 590 return any(path.startswith(pf) and path[len(pf)] == sl for pf in prefix_set)
591 591
592 592
593 593 class patternmatcher(basematcher):
594 594 r"""Matches a set of (kind, pat, source) against a 'root' directory.
595 595
596 596 >>> kindpats = [
597 597 ... (b're', br'.*\.c$', b''),
598 598 ... (b'path', b'foo/a', b''),
599 599 ... (b'relpath', b'b', b''),
600 600 ... (b'glob', b'*.h', b''),
601 601 ... ]
602 602 >>> m = patternmatcher(b'foo', kindpats)
603 603 >>> m(b'main.c') # matches re:.*\.c$
604 604 True
605 605 >>> m(b'b.txt')
606 606 False
607 607 >>> m(b'foo/a') # matches path:foo/a
608 608 True
609 609 >>> m(b'a') # does not match path:b, since 'root' is 'foo'
610 610 False
611 611 >>> m(b'b') # matches relpath:b, since 'root' is 'foo'
612 612 True
613 613 >>> m(b'lib.h') # matches glob:*.h
614 614 True
615 615
616 616 >>> m.files()
617 617 ['', 'foo/a', 'b', '']
618 618 >>> m.exact(b'foo/a')
619 619 True
620 620 >>> m.exact(b'b')
621 621 True
622 622 >>> m.exact(b'lib.h') # exact matches are for (rel)path kinds
623 623 False
624 624 """
625 625
626 626 def __init__(self, root, kindpats, badfn=None):
627 627 super(patternmatcher, self).__init__(badfn)
628 628
629 629 self._files = _explicitfiles(kindpats)
630 630 self._prefix = _prefix(kindpats)
631 631 self._pats, self.matchfn = _buildmatch(kindpats, b'$', root)
632 632
633 633 @propertycache
634 634 def _dirs(self):
635 635 return set(pathutil.dirs(self._fileset))
636 636
637 637 def visitdir(self, dir):
638 638 if self._prefix and dir in self._fileset:
639 639 return b'all'
640 640 return dir in self._dirs or path_or_parents_in_set(dir, self._fileset)
641 641
642 642 def visitchildrenset(self, dir):
643 643 ret = self.visitdir(dir)
644 644 if ret is True:
645 645 return b'this'
646 646 elif not ret:
647 647 return set()
648 648 assert ret == b'all'
649 649 return b'all'
650 650
651 651 def prefix(self):
652 652 return self._prefix
653 653
654 654 @encoding.strmethod
655 655 def __repr__(self):
656 656 return b'<patternmatcher patterns=%r>' % pycompat.bytestr(self._pats)
657 657
658 658
659 659 # This is basically a reimplementation of pathutil.dirs that stores the
660 660 # children instead of just a count of them, plus a small optional optimization
661 661 # to avoid some directories we don't need.
662 662 class _dirchildren:
663 663 def __init__(self, paths, onlyinclude=None):
664 664 self._dirs = {}
665 665 self._onlyinclude = onlyinclude or []
666 666 addpath = self.addpath
667 667 for f in paths:
668 668 addpath(f)
669 669
670 670 def addpath(self, path):
671 671 if path == b'':
672 672 return
673 673 dirs = self._dirs
674 674 findsplitdirs = _dirchildren._findsplitdirs
675 675 for d, b in findsplitdirs(path):
676 676 if d not in self._onlyinclude:
677 677 continue
678 678 dirs.setdefault(d, set()).add(b)
679 679
680 680 @staticmethod
681 681 def _findsplitdirs(path):
682 682 # yields (dirname, basename) tuples, walking back to the root. This is
683 683 # very similar to pathutil.finddirs, except:
684 684 # - produces a (dirname, basename) tuple, not just 'dirname'
685 685 # Unlike manifest._splittopdir, this does not suffix `dirname` with a
686 686 # slash.
687 687 oldpos = len(path)
688 688 pos = path.rfind(b'/')
689 689 while pos != -1:
690 690 yield path[:pos], path[pos + 1 : oldpos]
691 691 oldpos = pos
692 692 pos = path.rfind(b'/', 0, pos)
693 693 yield b'', path[:oldpos]
694 694
695 695 def get(self, path):
696 696 return self._dirs.get(path, set())
697 697
698 698
699 699 class includematcher(basematcher):
700 700 def __init__(self, root, kindpats, badfn=None):
701 701 super(includematcher, self).__init__(badfn)
702 702 if rustmod is not None:
703 703 # We need to pass the patterns to Rust because they can contain
704 704 # patterns from the user interface
705 705 self._kindpats = kindpats
706 706 self._pats, self.matchfn = _buildmatch(kindpats, b'(?:/|$)', root)
707 707 self._prefix = _prefix(kindpats)
708 708 roots, dirs, parents = _rootsdirsandparents(kindpats)
709 709 # roots are directories which are recursively included.
710 710 self._roots = set(roots)
711 711 # dirs are directories which are non-recursively included.
712 712 self._dirs = set(dirs)
713 713 # parents are directories which are non-recursively included because
714 714 # they are needed to get to items in _dirs or _roots.
715 715 self._parents = parents
716 716
717 717 def visitdir(self, dir):
718 718 if self._prefix and dir in self._roots:
719 719 return b'all'
720 720 return (
721 721 dir in self._dirs
722 722 or dir in self._parents
723 723 or path_or_parents_in_set(dir, self._roots)
724 724 )
725 725
726 726 @propertycache
727 727 def _allparentschildren(self):
728 728 # It may seem odd that we add dirs, roots, and parents, and then
729 729 # restrict to only parents. This is to catch the case of:
730 730 # dirs = ['foo/bar']
731 731 # parents = ['foo']
732 732 # if we asked for the children of 'foo', but had only added
733 733 # self._parents, we wouldn't be able to respond ['bar'].
734 734 return _dirchildren(
735 735 itertools.chain(self._dirs, self._roots, self._parents),
736 736 onlyinclude=self._parents,
737 737 )
738 738
739 739 def visitchildrenset(self, dir):
740 740 if self._prefix and dir in self._roots:
741 741 return b'all'
742 742 # Note: this does *not* include the 'dir in self._parents' case from
743 743 # visitdir, that's handled below.
744 744 if (
745 745 b'' in self._roots
746 746 or dir in self._dirs
747 747 or path_or_parents_in_set(dir, self._roots)
748 748 ):
749 749 return b'this'
750 750
751 751 if dir in self._parents:
752 752 return self._allparentschildren.get(dir) or set()
753 753 return set()
754 754
755 755 @encoding.strmethod
756 756 def __repr__(self):
757 757 return b'<includematcher includes=%r>' % pycompat.bytestr(self._pats)
758 758
759 759
760 760 class exactmatcher(basematcher):
761 761 r"""Matches the input files exactly. They are interpreted as paths, not
762 762 patterns (so no kind-prefixes).
763 763
764 764 >>> m = exactmatcher([b'a.txt', br're:.*\.c$'])
765 765 >>> m(b'a.txt')
766 766 True
767 767 >>> m(b'b.txt')
768 768 False
769 769
770 770 Input files that would be matched are exactly those returned by .files()
771 771 >>> m.files()
772 772 ['a.txt', 're:.*\\.c$']
773 773
774 774 So pattern 're:.*\.c$' is not considered as a regex, but as a file name
775 775 >>> m(b'main.c')
776 776 False
777 777 >>> m(br're:.*\.c$')
778 778 True
779 779 """
780 780
781 781 def __init__(self, files, badfn=None):
782 782 super(exactmatcher, self).__init__(badfn)
783 783
784 784 if isinstance(files, list):
785 785 self._files = files
786 786 else:
787 787 self._files = list(files)
788 788
789 789 matchfn = basematcher.exact
790 790
791 791 @propertycache
792 792 def _dirs(self):
793 793 return set(pathutil.dirs(self._fileset))
794 794
795 795 def visitdir(self, dir):
796 796 return dir in self._dirs
797 797
798 798 @propertycache
799 799 def _visitchildrenset_candidates(self):
800 800 """A memoized set of candidates for visitchildrenset."""
801 801 return self._fileset | self._dirs - {b''}
802 802
803 803 @propertycache
804 804 def _sorted_visitchildrenset_candidates(self):
805 805 """A memoized sorted list of candidates for visitchildrenset."""
806 806 return sorted(self._visitchildrenset_candidates)
807 807
808 808 def visitchildrenset(self, dir):
809 809 if not self._fileset or dir not in self._dirs:
810 810 return set()
811 811
812 812 if dir == b'':
813 813 candidates = self._visitchildrenset_candidates
814 814 else:
815 815 candidates = self._sorted_visitchildrenset_candidates
816 816 d = dir + b'/'
817 817 # Use bisect to find the first element potentially starting with d
818 818 # (i.e. >= d). This should always find at least one element (we'll
819 819 # assert later if this is not the case).
820 820 first = bisect.bisect_left(candidates, d)
821 821 # We need a representation of the first element that is > d that
822 822 # does not start with d, so since we added a `/` on the end of dir,
823 823 # we'll add whatever comes after slash (we could probably assume
824 824 # that `0` is after `/`, but let's not) to the end of dir instead.
825 825 dnext = dir + encoding.strtolocal(chr(ord(b'/') + 1))
826 826 # Use bisect to find the first element >= d_next
827 827 last = bisect.bisect_left(candidates, dnext, lo=first)
828 828 dlen = len(d)
829 829 candidates = {c[dlen:] for c in candidates[first:last]}
830 830 # self._dirs includes all of the directories, recursively, so if
831 831 # we're attempting to match foo/bar/baz.txt, it'll have '', 'foo',
832 832 # 'foo/bar' in it. Thus we can safely ignore a candidate that has a
833 833 # '/' in it, indicating a it's for a subdir-of-a-subdir; the
834 834 # immediate subdir will be in there without a slash.
835 835 ret = {c for c in candidates if b'/' not in c}
836 836 # We really do not expect ret to be empty, since that would imply that
837 837 # there's something in _dirs that didn't have a file in _fileset.
838 838 assert ret
839 839 return ret
840 840
841 841 def isexact(self):
842 842 return True
843 843
844 844 @encoding.strmethod
845 845 def __repr__(self):
846 846 return b'<exactmatcher files=%r>' % self._files
847 847
848 848
849 849 class differencematcher(basematcher):
850 850 """Composes two matchers by matching if the first matches and the second
851 851 does not.
852 852
853 853 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
854 854 """
855 855
856 856 def __init__(self, m1, m2):
857 857 super(differencematcher, self).__init__()
858 858 self._m1 = m1
859 859 self._m2 = m2
860 860 self.bad = m1.bad
861 861 self.traversedir = m1.traversedir
862 862
863 863 def matchfn(self, f):
864 864 return self._m1(f) and not self._m2(f)
865 865
866 866 @propertycache
867 867 def _files(self):
868 868 if self.isexact():
869 869 return [f for f in self._m1.files() if self(f)]
870 870 # If m1 is not an exact matcher, we can't easily figure out the set of
871 871 # files, because its files() are not always files. For example, if
872 872 # m1 is "path:dir" and m2 is "rootfileins:.", we don't
873 873 # want to remove "dir" from the set even though it would match m2,
874 874 # because the "dir" in m1 may not be a file.
875 875 return self._m1.files()
876 876
877 877 def visitdir(self, dir):
878 878 if self._m2.visitdir(dir) == b'all':
879 879 return False
880 880 elif not self._m2.visitdir(dir):
881 881 # m2 does not match dir, we can return 'all' here if possible
882 882 return self._m1.visitdir(dir)
883 883 return bool(self._m1.visitdir(dir))
884 884
885 885 def visitchildrenset(self, dir):
886 886 m2_set = self._m2.visitchildrenset(dir)
887 887 if m2_set == b'all':
888 888 return set()
889 889 m1_set = self._m1.visitchildrenset(dir)
890 890 # Possible values for m1: 'all', 'this', set(...), set()
891 891 # Possible values for m2: 'this', set(...), set()
892 892 # If m2 has nothing under here that we care about, return m1, even if
893 893 # it's 'all'. This is a change in behavior from visitdir, which would
894 894 # return True, not 'all', for some reason.
895 895 if not m2_set:
896 896 return m1_set
897 897 if m1_set in [b'all', b'this']:
898 898 # Never return 'all' here if m2_set is any kind of non-empty (either
899 899 # 'this' or set(foo)), since m2 might return set() for a
900 900 # subdirectory.
901 901 return b'this'
902 902 # Possible values for m1: set(...), set()
903 903 # Possible values for m2: 'this', set(...)
904 904 # We ignore m2's set results. They're possibly incorrect:
905 905 # m1 = path:dir/subdir, m2=rootfilesin:dir, visitchildrenset(''):
906 906 # m1 returns {'dir'}, m2 returns {'dir'}, if we subtracted we'd
907 907 # return set(), which is *not* correct, we still need to visit 'dir'!
908 908 return m1_set
909 909
910 910 def isexact(self):
911 911 return self._m1.isexact()
912 912
913 913 @encoding.strmethod
914 914 def __repr__(self):
915 915 return b'<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2)
916 916
917 917
918 918 def intersectmatchers(m1, m2):
919 919 """Composes two matchers by matching if both of them match.
920 920
921 921 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
922 922 """
923 923 if m1 is None or m2 is None:
924 924 return m1 or m2
925 925 if m1.always():
926 926 m = copy.copy(m2)
927 927 # TODO: Consider encapsulating these things in a class so there's only
928 928 # one thing to copy from m1.
929 929 m.bad = m1.bad
930 930 m.traversedir = m1.traversedir
931 931 return m
932 932 if m2.always():
933 933 m = copy.copy(m1)
934 934 return m
935 935 return intersectionmatcher(m1, m2)
936 936
937 937
938 938 class intersectionmatcher(basematcher):
939 939 def __init__(self, m1, m2):
940 940 super(intersectionmatcher, self).__init__()
941 941 self._m1 = m1
942 942 self._m2 = m2
943 943 self.bad = m1.bad
944 944 self.traversedir = m1.traversedir
945 945
946 946 @propertycache
947 947 def _files(self):
948 948 if self.isexact():
949 949 m1, m2 = self._m1, self._m2
950 950 if not m1.isexact():
951 951 m1, m2 = m2, m1
952 952 return [f for f in m1.files() if m2(f)]
953 953 # It neither m1 nor m2 is an exact matcher, we can't easily intersect
954 954 # the set of files, because their files() are not always files. For
955 955 # example, if intersecting a matcher "-I glob:foo.txt" with matcher of
956 956 # "path:dir2", we don't want to remove "dir2" from the set.
957 957 return self._m1.files() + self._m2.files()
958 958
959 959 def matchfn(self, f):
960 960 return self._m1(f) and self._m2(f)
961 961
962 962 def visitdir(self, dir):
963 963 visit1 = self._m1.visitdir(dir)
964 964 if visit1 == b'all':
965 965 return self._m2.visitdir(dir)
966 966 # bool() because visit1=True + visit2='all' should not be 'all'
967 967 return bool(visit1 and self._m2.visitdir(dir))
968 968
969 969 def visitchildrenset(self, dir):
970 970 m1_set = self._m1.visitchildrenset(dir)
971 971 if not m1_set:
972 972 return set()
973 973 m2_set = self._m2.visitchildrenset(dir)
974 974 if not m2_set:
975 975 return set()
976 976
977 977 if m1_set == b'all':
978 978 return m2_set
979 979 elif m2_set == b'all':
980 980 return m1_set
981 981
982 982 if m1_set == b'this' or m2_set == b'this':
983 983 return b'this'
984 984
985 985 assert isinstance(m1_set, set) and isinstance(m2_set, set)
986 986 return m1_set.intersection(m2_set)
987 987
988 988 def always(self):
989 989 return self._m1.always() and self._m2.always()
990 990
991 991 def isexact(self):
992 992 return self._m1.isexact() or self._m2.isexact()
993 993
994 994 @encoding.strmethod
995 995 def __repr__(self):
996 996 return b'<intersectionmatcher m1=%r, m2=%r>' % (self._m1, self._m2)
997 997
998 998
999 999 class subdirmatcher(basematcher):
1000 1000 """Adapt a matcher to work on a subdirectory only.
1001 1001
1002 1002 The paths are remapped to remove/insert the path as needed:
1003 1003
1004 1004 >>> from . import pycompat
1005 1005 >>> m1 = match(util.localpath(b'/root'), b'', [b'a.txt', b'sub/b.txt'], auditor=lambda name: None)
1006 1006 >>> m2 = subdirmatcher(b'sub', m1)
1007 1007 >>> m2(b'a.txt')
1008 1008 False
1009 1009 >>> m2(b'b.txt')
1010 1010 True
1011 1011 >>> m2.matchfn(b'a.txt')
1012 1012 False
1013 1013 >>> m2.matchfn(b'b.txt')
1014 1014 True
1015 1015 >>> m2.files()
1016 1016 ['b.txt']
1017 1017 >>> m2.exact(b'b.txt')
1018 1018 True
1019 1019 >>> def bad(f, msg):
1020 1020 ... print(pycompat.sysstr(b"%s: %s" % (f, msg)))
1021 1021 >>> m1.bad = bad
1022 1022 >>> m2.bad(b'x.txt', b'No such file')
1023 1023 sub/x.txt: No such file
1024 1024 """
1025 1025
1026 1026 def __init__(self, path, matcher):
1027 1027 super(subdirmatcher, self).__init__()
1028 1028 self._path = path
1029 1029 self._matcher = matcher
1030 1030 self._always = matcher.always()
1031 1031
1032 1032 self._files = [
1033 1033 f[len(path) + 1 :]
1034 1034 for f in matcher._files
1035 1035 if f.startswith(path + b"/")
1036 1036 ]
1037 1037
1038 1038 # If the parent repo had a path to this subrepo and the matcher is
1039 1039 # a prefix matcher, this submatcher always matches.
1040 1040 if matcher.prefix():
1041 1041 self._always = any(f == path for f in matcher._files)
1042 1042
1043 1043 def bad(self, f, msg):
1044 1044 self._matcher.bad(self._path + b"/" + f, msg)
1045 1045
1046 1046 def matchfn(self, f):
1047 1047 # Some information is lost in the superclass's constructor, so we
1048 1048 # can not accurately create the matching function for the subdirectory
1049 1049 # from the inputs. Instead, we override matchfn() and visitdir() to
1050 1050 # call the original matcher with the subdirectory path prepended.
1051 1051 return self._matcher.matchfn(self._path + b"/" + f)
1052 1052
1053 1053 def visitdir(self, dir):
1054 1054 if dir == b'':
1055 1055 dir = self._path
1056 1056 else:
1057 1057 dir = self._path + b"/" + dir
1058 1058 return self._matcher.visitdir(dir)
1059 1059
1060 1060 def visitchildrenset(self, dir):
1061 1061 if dir == b'':
1062 1062 dir = self._path
1063 1063 else:
1064 1064 dir = self._path + b"/" + dir
1065 1065 return self._matcher.visitchildrenset(dir)
1066 1066
1067 1067 def always(self):
1068 1068 return self._always
1069 1069
1070 1070 def prefix(self):
1071 1071 return self._matcher.prefix() and not self._always
1072 1072
1073 1073 @encoding.strmethod
1074 1074 def __repr__(self):
1075 1075 return b'<subdirmatcher path=%r, matcher=%r>' % (
1076 1076 self._path,
1077 1077 self._matcher,
1078 1078 )
1079 1079
1080 1080
1081 1081 class prefixdirmatcher(basematcher):
1082 1082 """Adapt a matcher to work on a parent directory.
1083 1083
1084 1084 The matcher's non-matching-attributes (bad, traversedir) are ignored.
1085 1085
1086 1086 The prefix path should usually be the relative path from the root of
1087 1087 this matcher to the root of the wrapped matcher.
1088 1088
1089 1089 >>> m1 = match(util.localpath(b'/root/d/e'), b'f', [b'../a.txt', b'b.txt'], auditor=lambda name: None)
1090 1090 >>> m2 = prefixdirmatcher(b'd/e', m1)
1091 1091 >>> m2(b'a.txt')
1092 1092 False
1093 1093 >>> m2(b'd/e/a.txt')
1094 1094 True
1095 1095 >>> m2(b'd/e/b.txt')
1096 1096 False
1097 1097 >>> m2.files()
1098 1098 ['d/e/a.txt', 'd/e/f/b.txt']
1099 1099 >>> m2.exact(b'd/e/a.txt')
1100 1100 True
1101 1101 >>> m2.visitdir(b'd')
1102 1102 True
1103 1103 >>> m2.visitdir(b'd/e')
1104 1104 True
1105 1105 >>> m2.visitdir(b'd/e/f')
1106 1106 True
1107 1107 >>> m2.visitdir(b'd/e/g')
1108 1108 False
1109 1109 >>> m2.visitdir(b'd/ef')
1110 1110 False
1111 1111 """
1112 1112
1113 1113 def __init__(self, path, matcher, badfn=None):
1114 1114 super(prefixdirmatcher, self).__init__(badfn)
1115 1115 if not path:
1116 1116 raise error.ProgrammingError(b'prefix path must not be empty')
1117 1117 self._path = path
1118 1118 self._pathprefix = path + b'/'
1119 1119 self._matcher = matcher
1120 1120
1121 1121 @propertycache
1122 1122 def _files(self):
1123 1123 return [self._pathprefix + f for f in self._matcher._files]
1124 1124
1125 1125 def matchfn(self, f):
1126 1126 if not f.startswith(self._pathprefix):
1127 1127 return False
1128 1128 return self._matcher.matchfn(f[len(self._pathprefix) :])
1129 1129
1130 1130 @propertycache
1131 1131 def _pathdirs(self):
1132 1132 return set(pathutil.finddirs(self._path))
1133 1133
1134 1134 def visitdir(self, dir):
1135 1135 if dir == self._path:
1136 1136 return self._matcher.visitdir(b'')
1137 1137 if dir.startswith(self._pathprefix):
1138 1138 return self._matcher.visitdir(dir[len(self._pathprefix) :])
1139 1139 return dir in self._pathdirs
1140 1140
1141 1141 def visitchildrenset(self, dir):
1142 1142 if dir == self._path:
1143 1143 return self._matcher.visitchildrenset(b'')
1144 1144 if dir.startswith(self._pathprefix):
1145 1145 return self._matcher.visitchildrenset(dir[len(self._pathprefix) :])
1146 1146 if dir in self._pathdirs:
1147 1147 return b'this'
1148 1148 return set()
1149 1149
1150 1150 def isexact(self):
1151 1151 return self._matcher.isexact()
1152 1152
1153 1153 def prefix(self):
1154 1154 return self._matcher.prefix()
1155 1155
1156 1156 @encoding.strmethod
1157 1157 def __repr__(self):
1158 1158 return b'<prefixdirmatcher path=%r, matcher=%r>' % (
1159 1159 pycompat.bytestr(self._path),
1160 1160 self._matcher,
1161 1161 )
1162 1162
1163 1163
1164 1164 class unionmatcher(basematcher):
1165 1165 """A matcher that is the union of several matchers.
1166 1166
1167 1167 The non-matching-attributes (bad, traversedir) are taken from the first
1168 1168 matcher.
1169 1169 """
1170 1170
1171 1171 def __init__(self, matchers):
1172 1172 m1 = matchers[0]
1173 1173 super(unionmatcher, self).__init__()
1174 1174 self.traversedir = m1.traversedir
1175 1175 self._matchers = matchers
1176 1176
1177 1177 def matchfn(self, f):
1178 1178 for match in self._matchers:
1179 1179 if match(f):
1180 1180 return True
1181 1181 return False
1182 1182
1183 1183 def visitdir(self, dir):
1184 1184 r = False
1185 1185 for m in self._matchers:
1186 1186 v = m.visitdir(dir)
1187 1187 if v == b'all':
1188 1188 return v
1189 1189 r |= v
1190 1190 return r
1191 1191
1192 1192 def visitchildrenset(self, dir):
1193 1193 r = set()
1194 1194 this = False
1195 1195 for m in self._matchers:
1196 1196 v = m.visitchildrenset(dir)
1197 1197 if not v:
1198 1198 continue
1199 1199 if v == b'all':
1200 1200 return v
1201 1201 if this or v == b'this':
1202 1202 this = True
1203 1203 # don't break, we might have an 'all' in here.
1204 1204 continue
1205 1205 assert isinstance(v, set)
1206 1206 r = r.union(v)
1207 1207 if this:
1208 1208 return b'this'
1209 1209 return r
1210 1210
1211 1211 @encoding.strmethod
1212 1212 def __repr__(self):
1213 1213 return b'<unionmatcher matchers=%r>' % self._matchers
1214 1214
1215 1215
1216 1216 def patkind(pattern, default=None):
1217 1217 r"""If pattern is 'kind:pat' with a known kind, return kind.
1218 1218
1219 1219 >>> patkind(br're:.*\.c$')
1220 1220 're'
1221 1221 >>> patkind(b'glob:*.c')
1222 1222 'glob'
1223 1223 >>> patkind(b'relpath:test.py')
1224 1224 'relpath'
1225 1225 >>> patkind(b'main.py')
1226 1226 >>> patkind(b'main.py', default=b're')
1227 1227 're'
1228 1228 """
1229 1229 return _patsplit(pattern, default)[0]
1230 1230
1231 1231
1232 1232 def _patsplit(pattern, default):
1233 1233 """Split a string into the optional pattern kind prefix and the actual
1234 1234 pattern."""
1235 1235 if b':' in pattern:
1236 1236 kind, pat = pattern.split(b':', 1)
1237 1237 if kind in allpatternkinds:
1238 1238 return kind, pat
1239 1239 return default, pattern
1240 1240
1241 1241
1242 1242 def _globre(pat):
1243 1243 r"""Convert an extended glob string to a regexp string.
1244 1244
1245 1245 >>> from . import pycompat
1246 1246 >>> def bprint(s):
1247 1247 ... print(pycompat.sysstr(s))
1248 1248 >>> bprint(_globre(br'?'))
1249 1249 .
1250 1250 >>> bprint(_globre(br'*'))
1251 1251 [^/]*
1252 1252 >>> bprint(_globre(br'**'))
1253 1253 .*
1254 1254 >>> bprint(_globre(br'**/a'))
1255 1255 (?:.*/)?a
1256 1256 >>> bprint(_globre(br'a/**/b'))
1257 1257 a/(?:.*/)?b
1258 1258 >>> bprint(_globre(br'[a*?!^][^b][!c]'))
1259 1259 [a*?!^][\^b][^c]
1260 1260 >>> bprint(_globre(br'{a,b}'))
1261 1261 (?:a|b)
1262 1262 >>> bprint(_globre(br'.\*\?'))
1263 1263 \.\*\?
1264 1264 """
1265 1265 i, n = 0, len(pat)
1266 1266 res = b''
1267 1267 group = 0
1268 1268 escape = util.stringutil.regexbytesescapemap.get
1269 1269
1270 1270 def peek():
1271 1271 return i < n and pat[i : i + 1]
1272 1272
1273 1273 while i < n:
1274 1274 c = pat[i : i + 1]
1275 1275 i += 1
1276 1276 if c not in b'*?[{},\\':
1277 1277 res += escape(c, c)
1278 1278 elif c == b'*':
1279 1279 if peek() == b'*':
1280 1280 i += 1
1281 1281 if peek() == b'/':
1282 1282 i += 1
1283 1283 res += b'(?:.*/)?'
1284 1284 else:
1285 1285 res += b'.*'
1286 1286 else:
1287 1287 res += b'[^/]*'
1288 1288 elif c == b'?':
1289 1289 res += b'.'
1290 1290 elif c == b'[':
1291 1291 j = i
1292 1292 if j < n and pat[j : j + 1] in b'!]':
1293 1293 j += 1
1294 1294 while j < n and pat[j : j + 1] != b']':
1295 1295 j += 1
1296 1296 if j >= n:
1297 1297 res += b'\\['
1298 1298 else:
1299 1299 stuff = pat[i:j].replace(b'\\', b'\\\\')
1300 1300 i = j + 1
1301 1301 if stuff[0:1] == b'!':
1302 1302 stuff = b'^' + stuff[1:]
1303 1303 elif stuff[0:1] == b'^':
1304 1304 stuff = b'\\' + stuff
1305 1305 res = b'%s[%s]' % (res, stuff)
1306 1306 elif c == b'{':
1307 1307 group += 1
1308 1308 res += b'(?:'
1309 1309 elif c == b'}' and group:
1310 1310 res += b')'
1311 1311 group -= 1
1312 1312 elif c == b',' and group:
1313 1313 res += b'|'
1314 1314 elif c == b'\\':
1315 1315 p = peek()
1316 1316 if p:
1317 1317 i += 1
1318 1318 res += escape(p, p)
1319 1319 else:
1320 1320 res += escape(c, c)
1321 1321 else:
1322 1322 res += escape(c, c)
1323 1323 return res
1324 1324
1325 1325
1326 FLAG_RE = util.re.compile(b'^\(\?([aiLmsux]+)\)(.*)')
1326 FLAG_RE = util.re.compile(br'^\(\?([aiLmsux]+)\)(.*)')
1327 1327
1328 1328
1329 1329 def _regex(kind, pat, globsuffix):
1330 1330 """Convert a (normalized) pattern of any kind into a
1331 1331 regular expression.
1332 1332 globsuffix is appended to the regexp of globs."""
1333 1333 if not pat and kind in (b'glob', b'relpath'):
1334 1334 return b''
1335 1335 if kind == b're':
1336 1336 return pat
1337 1337 if kind in (b'path', b'relpath'):
1338 1338 if pat == b'.':
1339 1339 return b''
1340 1340 return util.stringutil.reescape(pat) + b'(?:/|$)'
1341 1341 if kind == b'rootfilesin':
1342 1342 if pat == b'.':
1343 1343 escaped = b''
1344 1344 else:
1345 1345 # Pattern is a directory name.
1346 1346 escaped = util.stringutil.reescape(pat) + b'/'
1347 1347 # Anything after the pattern must be a non-directory.
1348 1348 return escaped + b'[^/]+$'
1349 1349 if kind == b'relglob':
1350 1350 globre = _globre(pat)
1351 1351 if globre.startswith(b'[^/]*'):
1352 1352 # When pat has the form *XYZ (common), make the returned regex more
1353 1353 # legible by returning the regex for **XYZ instead of **/*XYZ.
1354 1354 return b'.*' + globre[len(b'[^/]*') :] + globsuffix
1355 1355 return b'(?:|.*/)' + globre + globsuffix
1356 1356 if kind == b'relre':
1357 1357 flag = None
1358 1358 m = FLAG_RE.match(pat)
1359 1359 if m:
1360 1360 flag, pat = m.groups()
1361 1361 if not pat.startswith(b'^'):
1362 1362 pat = b'.*' + pat
1363 1363 if flag is not None:
1364 1364 pat = br'(?%s:%s)' % (flag, pat)
1365 1365 return pat
1366 1366 if kind in (b'glob', b'rootglob'):
1367 1367 return _globre(pat) + globsuffix
1368 1368 raise error.ProgrammingError(b'not a regex pattern: %s:%s' % (kind, pat))
1369 1369
1370 1370
1371 1371 def _buildmatch(kindpats, globsuffix, root):
1372 1372 """Return regexp string and a matcher function for kindpats.
1373 1373 globsuffix is appended to the regexp of globs."""
1374 1374 matchfuncs = []
1375 1375
1376 1376 subincludes, kindpats = _expandsubinclude(kindpats, root)
1377 1377 if subincludes:
1378 1378 submatchers = {}
1379 1379
1380 1380 def matchsubinclude(f):
1381 1381 for prefix, matcherargs in subincludes:
1382 1382 if f.startswith(prefix):
1383 1383 mf = submatchers.get(prefix)
1384 1384 if mf is None:
1385 1385 mf = match(*matcherargs)
1386 1386 submatchers[prefix] = mf
1387 1387
1388 1388 if mf(f[len(prefix) :]):
1389 1389 return True
1390 1390 return False
1391 1391
1392 1392 matchfuncs.append(matchsubinclude)
1393 1393
1394 1394 regex = b''
1395 1395 if kindpats:
1396 1396 if all(k == b'rootfilesin' for k, p, s in kindpats):
1397 1397 dirs = {p for k, p, s in kindpats}
1398 1398
1399 1399 def mf(f):
1400 1400 i = f.rfind(b'/')
1401 1401 if i >= 0:
1402 1402 dir = f[:i]
1403 1403 else:
1404 1404 dir = b'.'
1405 1405 return dir in dirs
1406 1406
1407 1407 regex = b'rootfilesin: %s' % stringutil.pprint(list(sorted(dirs)))
1408 1408 matchfuncs.append(mf)
1409 1409 else:
1410 1410 regex, mf = _buildregexmatch(kindpats, globsuffix)
1411 1411 matchfuncs.append(mf)
1412 1412
1413 1413 if len(matchfuncs) == 1:
1414 1414 return regex, matchfuncs[0]
1415 1415 else:
1416 1416 return regex, lambda f: any(mf(f) for mf in matchfuncs)
1417 1417
1418 1418
1419 1419 MAX_RE_SIZE = 20000
1420 1420
1421 1421
1422 1422 def _joinregexes(regexps):
1423 1423 """gather multiple regular expressions into a single one"""
1424 1424 return b'|'.join(regexps)
1425 1425
1426 1426
1427 1427 def _buildregexmatch(kindpats, globsuffix):
1428 1428 """Build a match function from a list of kinds and kindpats,
1429 1429 return regexp string and a matcher function.
1430 1430
1431 1431 Test too large input
1432 1432 >>> _buildregexmatch([
1433 1433 ... (b'relglob', b'?' * MAX_RE_SIZE, b'')
1434 1434 ... ], b'$')
1435 1435 Traceback (most recent call last):
1436 1436 ...
1437 1437 Abort: matcher pattern is too long (20009 bytes)
1438 1438 """
1439 1439 try:
1440 1440 allgroups = []
1441 1441 regexps = [_regex(k, p, globsuffix) for (k, p, s) in kindpats]
1442 1442 fullregexp = _joinregexes(regexps)
1443 1443
1444 1444 startidx = 0
1445 1445 groupsize = 0
1446 1446 for idx, r in enumerate(regexps):
1447 1447 piecesize = len(r)
1448 1448 if piecesize > MAX_RE_SIZE:
1449 1449 msg = _(b"matcher pattern is too long (%d bytes)") % piecesize
1450 1450 raise error.Abort(msg)
1451 1451 elif (groupsize + piecesize) > MAX_RE_SIZE:
1452 1452 group = regexps[startidx:idx]
1453 1453 allgroups.append(_joinregexes(group))
1454 1454 startidx = idx
1455 1455 groupsize = 0
1456 1456 groupsize += piecesize + 1
1457 1457
1458 1458 if startidx == 0:
1459 1459 matcher = _rematcher(fullregexp)
1460 1460 func = lambda s: bool(matcher(s))
1461 1461 else:
1462 1462 group = regexps[startidx:]
1463 1463 allgroups.append(_joinregexes(group))
1464 1464 allmatchers = [_rematcher(g) for g in allgroups]
1465 1465 func = lambda s: any(m(s) for m in allmatchers)
1466 1466 return fullregexp, func
1467 1467 except re.error:
1468 1468 for k, p, s in kindpats:
1469 1469 try:
1470 1470 _rematcher(_regex(k, p, globsuffix))
1471 1471 except re.error:
1472 1472 if s:
1473 1473 raise error.Abort(
1474 1474 _(b"%s: invalid pattern (%s): %s") % (s, k, p)
1475 1475 )
1476 1476 else:
1477 1477 raise error.Abort(_(b"invalid pattern (%s): %s") % (k, p))
1478 1478 raise error.Abort(_(b"invalid pattern"))
1479 1479
1480 1480
1481 1481 def _patternrootsanddirs(kindpats):
1482 1482 """Returns roots and directories corresponding to each pattern.
1483 1483
1484 1484 This calculates the roots and directories exactly matching the patterns and
1485 1485 returns a tuple of (roots, dirs) for each. It does not return other
1486 1486 directories which may also need to be considered, like the parent
1487 1487 directories.
1488 1488 """
1489 1489 r = []
1490 1490 d = []
1491 1491 for kind, pat, source in kindpats:
1492 1492 if kind in (b'glob', b'rootglob'): # find the non-glob prefix
1493 1493 root = []
1494 1494 for p in pat.split(b'/'):
1495 1495 if b'[' in p or b'{' in p or b'*' in p or b'?' in p:
1496 1496 break
1497 1497 root.append(p)
1498 1498 r.append(b'/'.join(root))
1499 1499 elif kind in (b'relpath', b'path'):
1500 1500 if pat == b'.':
1501 1501 pat = b''
1502 1502 r.append(pat)
1503 1503 elif kind in (b'rootfilesin',):
1504 1504 if pat == b'.':
1505 1505 pat = b''
1506 1506 d.append(pat)
1507 1507 else: # relglob, re, relre
1508 1508 r.append(b'')
1509 1509 return r, d
1510 1510
1511 1511
1512 1512 def _roots(kindpats):
1513 1513 '''Returns root directories to match recursively from the given patterns.'''
1514 1514 roots, dirs = _patternrootsanddirs(kindpats)
1515 1515 return roots
1516 1516
1517 1517
1518 1518 def _rootsdirsandparents(kindpats):
1519 1519 """Returns roots and exact directories from patterns.
1520 1520
1521 1521 `roots` are directories to match recursively, `dirs` should
1522 1522 be matched non-recursively, and `parents` are the implicitly required
1523 1523 directories to walk to items in either roots or dirs.
1524 1524
1525 1525 Returns a tuple of (roots, dirs, parents).
1526 1526
1527 1527 >>> r = _rootsdirsandparents(
1528 1528 ... [(b'glob', b'g/h/*', b''), (b'glob', b'g/h', b''),
1529 1529 ... (b'glob', b'g*', b'')])
1530 1530 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1531 1531 (['g/h', 'g/h', ''], []) ['', 'g']
1532 1532 >>> r = _rootsdirsandparents(
1533 1533 ... [(b'rootfilesin', b'g/h', b''), (b'rootfilesin', b'', b'')])
1534 1534 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1535 1535 ([], ['g/h', '']) ['', 'g']
1536 1536 >>> r = _rootsdirsandparents(
1537 1537 ... [(b'relpath', b'r', b''), (b'path', b'p/p', b''),
1538 1538 ... (b'path', b'', b'')])
1539 1539 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1540 1540 (['r', 'p/p', ''], []) ['', 'p']
1541 1541 >>> r = _rootsdirsandparents(
1542 1542 ... [(b'relglob', b'rg*', b''), (b're', b're/', b''),
1543 1543 ... (b'relre', b'rr', b'')])
1544 1544 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1545 1545 (['', '', ''], []) ['']
1546 1546 """
1547 1547 r, d = _patternrootsanddirs(kindpats)
1548 1548
1549 1549 p = set()
1550 1550 # Add the parents as non-recursive/exact directories, since they must be
1551 1551 # scanned to get to either the roots or the other exact directories.
1552 1552 p.update(pathutil.dirs(d))
1553 1553 p.update(pathutil.dirs(r))
1554 1554
1555 1555 # FIXME: all uses of this function convert these to sets, do so before
1556 1556 # returning.
1557 1557 # FIXME: all uses of this function do not need anything in 'roots' and
1558 1558 # 'dirs' to also be in 'parents', consider removing them before returning.
1559 1559 return r, d, p
1560 1560
1561 1561
1562 1562 def _explicitfiles(kindpats):
1563 1563 """Returns the potential explicit filenames from the patterns.
1564 1564
1565 1565 >>> _explicitfiles([(b'path', b'foo/bar', b'')])
1566 1566 ['foo/bar']
1567 1567 >>> _explicitfiles([(b'rootfilesin', b'foo/bar', b'')])
1568 1568 []
1569 1569 """
1570 1570 # Keep only the pattern kinds where one can specify filenames (vs only
1571 1571 # directory names).
1572 1572 filable = [kp for kp in kindpats if kp[0] not in (b'rootfilesin',)]
1573 1573 return _roots(filable)
1574 1574
1575 1575
1576 1576 def _prefix(kindpats):
1577 1577 '''Whether all the patterns match a prefix (i.e. recursively)'''
1578 1578 for kind, pat, source in kindpats:
1579 1579 if kind not in (b'path', b'relpath'):
1580 1580 return False
1581 1581 return True
1582 1582
1583 1583
1584 1584 _commentre = None
1585 1585
1586 1586
1587 1587 def readpatternfile(filepath, warn, sourceinfo=False):
1588 1588 """parse a pattern file, returning a list of
1589 1589 patterns. These patterns should be given to compile()
1590 1590 to be validated and converted into a match function.
1591 1591
1592 1592 trailing white space is dropped.
1593 1593 the escape character is backslash.
1594 1594 comments start with #.
1595 1595 empty lines are skipped.
1596 1596
1597 1597 lines can be of the following formats:
1598 1598
1599 1599 syntax: regexp # defaults following lines to non-rooted regexps
1600 1600 syntax: glob # defaults following lines to non-rooted globs
1601 1601 re:pattern # non-rooted regular expression
1602 1602 glob:pattern # non-rooted glob
1603 1603 rootglob:pat # rooted glob (same root as ^ in regexps)
1604 1604 pattern # pattern of the current default type
1605 1605
1606 1606 if sourceinfo is set, returns a list of tuples:
1607 1607 (pattern, lineno, originalline).
1608 1608 This is useful to debug ignore patterns.
1609 1609 """
1610 1610
1611 1611 syntaxes = {
1612 1612 b're': b'relre:',
1613 1613 b'regexp': b'relre:',
1614 1614 b'glob': b'relglob:',
1615 1615 b'rootglob': b'rootglob:',
1616 1616 b'include': b'include',
1617 1617 b'subinclude': b'subinclude',
1618 1618 }
1619 1619 syntax = b'relre:'
1620 1620 patterns = []
1621 1621
1622 1622 fp = open(filepath, b'rb')
1623 1623 for lineno, line in enumerate(fp, start=1):
1624 1624 if b"#" in line:
1625 1625 global _commentre
1626 1626 if not _commentre:
1627 1627 _commentre = util.re.compile(br'((?:^|[^\\])(?:\\\\)*)#.*')
1628 1628 # remove comments prefixed by an even number of escapes
1629 1629 m = _commentre.search(line)
1630 1630 if m:
1631 1631 line = line[: m.end(1)]
1632 1632 # fixup properly escaped comments that survived the above
1633 1633 line = line.replace(b"\\#", b"#")
1634 1634 line = line.rstrip()
1635 1635 if not line:
1636 1636 continue
1637 1637
1638 1638 if line.startswith(b'syntax:'):
1639 1639 s = line[7:].strip()
1640 1640 try:
1641 1641 syntax = syntaxes[s]
1642 1642 except KeyError:
1643 1643 if warn:
1644 1644 warn(
1645 1645 _(b"%s: ignoring invalid syntax '%s'\n") % (filepath, s)
1646 1646 )
1647 1647 continue
1648 1648
1649 1649 linesyntax = syntax
1650 1650 for s, rels in syntaxes.items():
1651 1651 if line.startswith(rels):
1652 1652 linesyntax = rels
1653 1653 line = line[len(rels) :]
1654 1654 break
1655 1655 elif line.startswith(s + b':'):
1656 1656 linesyntax = rels
1657 1657 line = line[len(s) + 1 :]
1658 1658 break
1659 1659 if sourceinfo:
1660 1660 patterns.append((linesyntax + line, lineno, line))
1661 1661 else:
1662 1662 patterns.append(linesyntax + line)
1663 1663 fp.close()
1664 1664 return patterns
@@ -1,526 +1,526 b''
1 1 # setdiscovery.py - improved discovery of common nodeset for mercurial
2 2 #
3 3 # Copyright 2010 Benoit Boissinot <bboissin@gmail.com>
4 4 # and Peter Arrenbrecht <peter@arrenbrecht.ch>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8 """
9 9 Algorithm works in the following way. You have two repository: local and
10 10 remote. They both contains a DAG of changelists.
11 11
12 12 The goal of the discovery protocol is to find one set of node *common*,
13 13 the set of nodes shared by local and remote.
14 14
15 15 One of the issue with the original protocol was latency, it could
16 16 potentially require lots of roundtrips to discover that the local repo was a
17 17 subset of remote (which is a very common case, you usually have few changes
18 18 compared to upstream, while upstream probably had lots of development).
19 19
20 20 The new protocol only requires one interface for the remote repo: `known()`,
21 21 which given a set of changelists tells you if they are present in the DAG.
22 22
23 23 The algorithm then works as follow:
24 24
25 25 - We will be using three sets, `common`, `missing`, `unknown`. Originally
26 26 all nodes are in `unknown`.
27 27 - Take a sample from `unknown`, call `remote.known(sample)`
28 28 - For each node that remote knows, move it and all its ancestors to `common`
29 29 - For each node that remote doesn't know, move it and all its descendants
30 30 to `missing`
31 31 - Iterate until `unknown` is empty
32 32
33 33 There are a couple optimizations, first is instead of starting with a random
34 34 sample of missing, start by sending all heads, in the case where the local
35 35 repo is a subset, you computed the answer in one round trip.
36 36
37 37 Then you can do something similar to the bisecting strategy used when
38 38 finding faulty changesets. Instead of random samples, you can try picking
39 39 nodes that will maximize the number of nodes that will be
40 40 classified with it (since all ancestors or descendants will be marked as well).
41 41 """
42 42
43 43
44 44 import collections
45 45 import random
46 46
47 47 from .i18n import _
48 48 from .node import nullrev
49 49 from . import (
50 50 error,
51 51 policy,
52 52 util,
53 53 )
54 54
55 55
56 56 def _updatesample(revs, heads, sample, parentfn, quicksamplesize=0):
57 57 """update an existing sample to match the expected size
58 58
59 59 The sample is updated with revs exponentially distant from each head of the
60 60 <revs> set. (H~1, H~2, H~4, H~8, etc).
61 61
62 62 If a target size is specified, the sampling will stop once this size is
63 63 reached. Otherwise sampling will happen until roots of the <revs> set are
64 64 reached.
65 65
66 66 :revs: set of revs we want to discover (if None, assume the whole dag)
67 67 :heads: set of DAG head revs
68 68 :sample: a sample to update
69 69 :parentfn: a callable to resolve parents for a revision
70 70 :quicksamplesize: optional target size of the sample"""
71 71 dist = {}
72 72 visit = collections.deque(heads)
73 73 seen = set()
74 74 factor = 1
75 75 while visit:
76 76 curr = visit.popleft()
77 77 if curr in seen:
78 78 continue
79 79 d = dist.setdefault(curr, 1)
80 80 if d > factor:
81 81 factor *= 2
82 82 if d == factor:
83 83 sample.add(curr)
84 84 if quicksamplesize and (len(sample) >= quicksamplesize):
85 85 return
86 86 seen.add(curr)
87 87
88 88 for p in parentfn(curr):
89 89 if p != nullrev and (not revs or p in revs):
90 90 dist.setdefault(p, d + 1)
91 91 visit.append(p)
92 92
93 93
94 94 def _limitsample(sample, desiredlen, randomize=True):
95 95 """return a random subset of sample of at most desiredlen item.
96 96
97 97 If randomize is False, though, a deterministic subset is returned.
98 98 This is meant for integration tests.
99 99 """
100 100 if len(sample) <= desiredlen:
101 101 return sample
102 sample = list(sample)
102 103 if randomize:
103 104 return set(random.sample(sample, desiredlen))
104 sample = list(sample)
105 105 sample.sort()
106 106 return set(sample[:desiredlen])
107 107
108 108
109 109 class partialdiscovery:
110 110 """an object representing ongoing discovery
111 111
112 112 Feed with data from the remote repository, this object keep track of the
113 113 current set of changeset in various states:
114 114
115 115 - common: revs also known remotely
116 116 - undecided: revs we don't have information on yet
117 117 - missing: revs missing remotely
118 118 (all tracked revisions are known locally)
119 119 """
120 120
121 121 def __init__(self, repo, targetheads, respectsize, randomize=True):
122 122 self._repo = repo
123 123 self._targetheads = targetheads
124 124 self._common = repo.changelog.incrementalmissingrevs()
125 125 self._undecided = None
126 126 self.missing = set()
127 127 self._childrenmap = None
128 128 self._respectsize = respectsize
129 129 self.randomize = randomize
130 130
131 131 def addcommons(self, commons):
132 132 """register nodes known as common"""
133 133 self._common.addbases(commons)
134 134 if self._undecided is not None:
135 135 self._common.removeancestorsfrom(self._undecided)
136 136
137 137 def addmissings(self, missings):
138 138 """register some nodes as missing"""
139 139 newmissing = self._repo.revs(b'%ld::%ld', missings, self.undecided)
140 140 if newmissing:
141 141 self.missing.update(newmissing)
142 142 self.undecided.difference_update(newmissing)
143 143
144 144 def addinfo(self, sample):
145 145 """consume an iterable of (rev, known) tuples"""
146 146 common = set()
147 147 missing = set()
148 148 for rev, known in sample:
149 149 if known:
150 150 common.add(rev)
151 151 else:
152 152 missing.add(rev)
153 153 if common:
154 154 self.addcommons(common)
155 155 if missing:
156 156 self.addmissings(missing)
157 157
158 158 def hasinfo(self):
159 159 """return True is we have any clue about the remote state"""
160 160 return self._common.hasbases()
161 161
162 162 def iscomplete(self):
163 163 """True if all the necessary data have been gathered"""
164 164 return self._undecided is not None and not self._undecided
165 165
166 166 @property
167 167 def undecided(self):
168 168 if self._undecided is not None:
169 169 return self._undecided
170 170 self._undecided = set(self._common.missingancestors(self._targetheads))
171 171 return self._undecided
172 172
173 173 def stats(self):
174 174 return {
175 175 'undecided': len(self.undecided),
176 176 }
177 177
178 178 def commonheads(self):
179 179 """the heads of the known common set"""
180 180 # heads(common) == heads(common.bases) since common represents
181 181 # common.bases and all its ancestors
182 182 return self._common.basesheads()
183 183
184 184 def _parentsgetter(self):
185 185 getrev = self._repo.changelog.index.__getitem__
186 186
187 187 def getparents(r):
188 188 return getrev(r)[5:7]
189 189
190 190 return getparents
191 191
192 192 def _childrengetter(self):
193 193
194 194 if self._childrenmap is not None:
195 195 # During discovery, the `undecided` set keep shrinking.
196 196 # Therefore, the map computed for an iteration N will be
197 197 # valid for iteration N+1. Instead of computing the same
198 198 # data over and over we cached it the first time.
199 199 return self._childrenmap.__getitem__
200 200
201 201 # _updatesample() essentially does interaction over revisions to look
202 202 # up their children. This lookup is expensive and doing it in a loop is
203 203 # quadratic. We precompute the children for all relevant revisions and
204 204 # make the lookup in _updatesample() a simple dict lookup.
205 205 self._childrenmap = children = {}
206 206
207 207 parentrevs = self._parentsgetter()
208 208 revs = self.undecided
209 209
210 210 for rev in sorted(revs):
211 211 # Always ensure revision has an entry so we don't need to worry
212 212 # about missing keys.
213 213 children[rev] = []
214 214 for prev in parentrevs(rev):
215 215 if prev == nullrev:
216 216 continue
217 217 c = children.get(prev)
218 218 if c is not None:
219 219 c.append(rev)
220 220 return children.__getitem__
221 221
222 222 def takequicksample(self, headrevs, size):
223 223 """takes a quick sample of size <size>
224 224
225 225 It is meant for initial sampling and focuses on querying heads and close
226 226 ancestors of heads.
227 227
228 228 :headrevs: set of head revisions in local DAG to consider
229 229 :size: the maximum size of the sample"""
230 230 revs = self.undecided
231 231 if len(revs) <= size:
232 232 return list(revs)
233 233 sample = set(self._repo.revs(b'heads(%ld)', revs))
234 234
235 235 if len(sample) >= size:
236 236 return _limitsample(sample, size, randomize=self.randomize)
237 237
238 238 _updatesample(
239 239 None, headrevs, sample, self._parentsgetter(), quicksamplesize=size
240 240 )
241 241 return sample
242 242
243 243 def takefullsample(self, headrevs, size):
244 244 revs = self.undecided
245 245 if len(revs) <= size:
246 246 return list(revs)
247 247 repo = self._repo
248 248 sample = set(repo.revs(b'heads(%ld)', revs))
249 249 parentrevs = self._parentsgetter()
250 250
251 251 # update from heads
252 252 revsheads = sample.copy()
253 253 _updatesample(revs, revsheads, sample, parentrevs)
254 254
255 255 # update from roots
256 256 revsroots = set(repo.revs(b'roots(%ld)', revs))
257 257 childrenrevs = self._childrengetter()
258 258 _updatesample(revs, revsroots, sample, childrenrevs)
259 259 assert sample
260 260
261 261 if not self._respectsize:
262 262 size = max(size, min(len(revsroots), len(revsheads)))
263 263
264 264 sample = _limitsample(sample, size, randomize=self.randomize)
265 265 if len(sample) < size:
266 266 more = size - len(sample)
267 267 takefrom = list(revs - sample)
268 268 if self.randomize:
269 269 sample.update(random.sample(takefrom, more))
270 270 else:
271 271 takefrom.sort()
272 272 sample.update(takefrom[:more])
273 273 return sample
274 274
275 275
276 276 pure_partialdiscovery = partialdiscovery
277 277
278 278 partialdiscovery = policy.importrust(
279 279 'discovery', member='PartialDiscovery', default=partialdiscovery
280 280 )
281 281
282 282
283 283 def findcommonheads(
284 284 ui,
285 285 local,
286 286 remote,
287 287 abortwhenunrelated=True,
288 288 ancestorsof=None,
289 289 audit=None,
290 290 ):
291 291 """Return a tuple (common, anyincoming, remoteheads) used to identify
292 292 missing nodes from or in remote.
293 293
294 294 The audit argument is an optional dictionnary that a caller can pass. it
295 295 will be updated with extra data about the discovery, this is useful for
296 296 debug.
297 297 """
298 298
299 299 samplegrowth = float(ui.config(b'devel', b'discovery.grow-sample.rate'))
300 300
301 301 if audit is not None:
302 302 audit[b'total-queries'] = 0
303 303
304 304 start = util.timer()
305 305
306 306 roundtrips = 0
307 307 cl = local.changelog
308 308 clnode = cl.node
309 309 clrev = cl.rev
310 310
311 311 if ancestorsof is not None:
312 312 ownheads = [clrev(n) for n in ancestorsof]
313 313 else:
314 314 ownheads = [rev for rev in cl.headrevs() if rev != nullrev]
315 315
316 316 initial_head_exchange = ui.configbool(b'devel', b'discovery.exchange-heads')
317 317 initialsamplesize = ui.configint(b'devel', b'discovery.sample-size.initial')
318 318 fullsamplesize = ui.configint(b'devel', b'discovery.sample-size')
319 319 # We also ask remote about all the local heads. That set can be arbitrarily
320 320 # large, so we used to limit it size to `initialsamplesize`. We no longer
321 321 # do as it proved counter productive. The skipped heads could lead to a
322 322 # large "undecided" set, slower to be clarified than if we asked the
323 323 # question for all heads right away.
324 324 #
325 325 # We are already fetching all server heads using the `heads` commands,
326 326 # sending a equivalent number of heads the other way should not have a
327 327 # significant impact. In addition, it is very likely that we are going to
328 328 # have to issue "known" request for an equivalent amount of revisions in
329 329 # order to decide if theses heads are common or missing.
330 330 #
331 331 # find a detailled analysis below.
332 332 #
333 333 # Case A: local and server both has few heads
334 334 #
335 335 # Ownheads is below initialsamplesize, limit would not have any effect.
336 336 #
337 337 # Case B: local has few heads and server has many
338 338 #
339 339 # Ownheads is below initialsamplesize, limit would not have any effect.
340 340 #
341 341 # Case C: local and server both has many heads
342 342 #
343 343 # We now transfert some more data, but not significantly more than is
344 344 # already transfered to carry the server heads.
345 345 #
346 346 # Case D: local has many heads, server has few
347 347 #
348 348 # D.1 local heads are mostly known remotely
349 349 #
350 350 # All the known head will have be part of a `known` request at some
351 351 # point for the discovery to finish. Sending them all earlier is
352 352 # actually helping.
353 353 #
354 354 # (This case is fairly unlikely, it requires the numerous heads to all
355 355 # be merged server side in only a few heads)
356 356 #
357 357 # D.2 local heads are mostly missing remotely
358 358 #
359 359 # To determine that the heads are missing, we'll have to issue `known`
360 360 # request for them or one of their ancestors. This amount of `known`
361 361 # request will likely be in the same order of magnitude than the amount
362 362 # of local heads.
363 363 #
364 364 # The only case where we can be more efficient using `known` request on
365 365 # ancestors are case were all the "missing" local heads are based on a
366 366 # few changeset, also "missing". This means we would have a "complex"
367 367 # graph (with many heads) attached to, but very independant to a the
368 368 # "simple" graph on the server. This is a fairly usual case and have
369 369 # not been met in the wild so far.
370 370 if initial_head_exchange:
371 371 if remote.limitedarguments:
372 372 sample = _limitsample(ownheads, initialsamplesize)
373 373 # indices between sample and externalized version must match
374 374 sample = list(sample)
375 375 else:
376 376 sample = ownheads
377 377
378 378 ui.debug(b"query 1; heads\n")
379 379 roundtrips += 1
380 380 with remote.commandexecutor() as e:
381 381 fheads = e.callcommand(b'heads', {})
382 382 if audit is not None:
383 383 audit[b'total-queries'] += len(sample)
384 384 fknown = e.callcommand(
385 385 b'known',
386 386 {
387 387 b'nodes': [clnode(r) for r in sample],
388 388 },
389 389 )
390 390
391 391 srvheadhashes, yesno = fheads.result(), fknown.result()
392 392
393 393 if audit is not None:
394 394 audit[b'total-roundtrips'] = 1
395 395
396 396 if cl.tiprev() == nullrev:
397 397 if srvheadhashes != [cl.nullid]:
398 398 return [cl.nullid], True, srvheadhashes
399 399 return [cl.nullid], False, []
400 400 else:
401 401 # we still need the remote head for the function return
402 402 with remote.commandexecutor() as e:
403 403 fheads = e.callcommand(b'heads', {})
404 404 srvheadhashes = fheads.result()
405 405
406 406 # start actual discovery (we note this before the next "if" for
407 407 # compatibility reasons)
408 408 ui.status(_(b"searching for changes\n"))
409 409
410 410 knownsrvheads = [] # revnos of remote heads that are known locally
411 411 for node in srvheadhashes:
412 412 if node == cl.nullid:
413 413 continue
414 414
415 415 try:
416 416 knownsrvheads.append(clrev(node))
417 417 # Catches unknown and filtered nodes.
418 418 except error.LookupError:
419 419 continue
420 420
421 421 if initial_head_exchange:
422 422 # early exit if we know all the specified remote heads already
423 423 if len(knownsrvheads) == len(srvheadhashes):
424 424 ui.debug(b"all remote heads known locally\n")
425 425 return srvheadhashes, False, srvheadhashes
426 426
427 427 if len(sample) == len(ownheads) and all(yesno):
428 428 ui.note(_(b"all local changesets known remotely\n"))
429 429 ownheadhashes = [clnode(r) for r in ownheads]
430 430 return ownheadhashes, True, srvheadhashes
431 431
432 432 # full blown discovery
433 433
434 434 # if the server has a limit to its arguments size, we can't grow the sample.
435 435 configbool = local.ui.configbool
436 436 grow_sample = configbool(b'devel', b'discovery.grow-sample')
437 437 grow_sample = grow_sample and not remote.limitedarguments
438 438
439 439 dynamic_sample = configbool(b'devel', b'discovery.grow-sample.dynamic')
440 440 hard_limit_sample = not (dynamic_sample or remote.limitedarguments)
441 441
442 442 randomize = ui.configbool(b'devel', b'discovery.randomize')
443 443 if cl.index.rust_ext_compat:
444 444 pd = partialdiscovery
445 445 else:
446 446 pd = pure_partialdiscovery
447 447 disco = pd(local, ownheads, hard_limit_sample, randomize=randomize)
448 448 if initial_head_exchange:
449 449 # treat remote heads (and maybe own heads) as a first implicit sample
450 450 # response
451 451 disco.addcommons(knownsrvheads)
452 452 disco.addinfo(zip(sample, yesno))
453 453
454 454 full = not initial_head_exchange
455 455 progress = ui.makeprogress(_(b'searching'), unit=_(b'queries'))
456 456 while not disco.iscomplete():
457 457
458 458 if full or disco.hasinfo():
459 459 if full:
460 460 ui.note(_(b"sampling from both directions\n"))
461 461 else:
462 462 ui.debug(b"taking initial sample\n")
463 463 samplefunc = disco.takefullsample
464 464 targetsize = fullsamplesize
465 465 if grow_sample:
466 466 fullsamplesize = int(fullsamplesize * samplegrowth)
467 467 else:
468 468 # use even cheaper initial sample
469 469 ui.debug(b"taking quick initial sample\n")
470 470 samplefunc = disco.takequicksample
471 471 targetsize = initialsamplesize
472 472 sample = samplefunc(ownheads, targetsize)
473 473
474 474 roundtrips += 1
475 475 progress.update(roundtrips)
476 476 stats = disco.stats()
477 477 ui.debug(
478 478 b"query %i; still undecided: %i, sample size is: %i\n"
479 479 % (roundtrips, stats['undecided'], len(sample))
480 480 )
481 481
482 482 # indices between sample and externalized version must match
483 483 sample = list(sample)
484 484
485 485 with remote.commandexecutor() as e:
486 486 if audit is not None:
487 487 audit[b'total-queries'] += len(sample)
488 488 yesno = e.callcommand(
489 489 b'known',
490 490 {
491 491 b'nodes': [clnode(r) for r in sample],
492 492 },
493 493 ).result()
494 494
495 495 full = True
496 496
497 497 disco.addinfo(zip(sample, yesno))
498 498
499 499 result = disco.commonheads()
500 500 elapsed = util.timer() - start
501 501 progress.complete()
502 502 ui.debug(b"%d total queries in %.4fs\n" % (roundtrips, elapsed))
503 503 msg = (
504 504 b'found %d common and %d unknown server heads,'
505 505 b' %d roundtrips in %.4fs\n'
506 506 )
507 507 missing = set(result) - set(knownsrvheads)
508 508 ui.log(b'discovery', msg, len(result), len(missing), roundtrips, elapsed)
509 509
510 510 if audit is not None:
511 511 audit[b'total-roundtrips'] = roundtrips
512 512
513 513 if not result and srvheadhashes != [cl.nullid]:
514 514 if abortwhenunrelated:
515 515 raise error.Abort(_(b"repository is unrelated"))
516 516 else:
517 517 ui.warn(_(b"warning: repository is unrelated\n"))
518 518 return (
519 519 {cl.nullid},
520 520 True,
521 521 srvheadhashes,
522 522 )
523 523
524 524 anyincoming = srvheadhashes != [cl.nullid]
525 525 result = {clnode(r) for r in result}
526 526 return result, anyincoming, srvheadhashes
@@ -1,1892 +1,1900 b''
1 1 ============================================================================================
2 2 Test cases where there are race condition between two clients pushing to the same repository
3 3 ============================================================================================
4 4
5 5 This file tests cases where two clients push to a server at the same time. The
6 6 "raced" client is done preparing it push bundle when the "racing" client
7 7 perform its push. The "raced" client starts its actual push after the "racing"
8 8 client push is fully complete.
9 9
10 10 A set of extension and shell functions ensures this scheduling.
11 11
12 12 $ cat >> delaypush.py << EOF
13 13 > """small extension orchestrate push race
14 14 >
15 15 > Client with the extensions will create a file when ready and get stuck until
16 16 > a file is created."""
17 17 >
18 18 > import errno
19 19 > import os
20 20 > import time
21 21 >
22 22 > from mercurial import (
23 23 > exchange,
24 24 > extensions,
25 25 > registrar,
26 26 > )
27 27 >
28 28 > configtable = {}
29 29 > configitem = registrar.configitem(configtable)
30 30 >
31 31 > configitem(b'delaypush', b'ready-path',
32 32 > default=None,
33 33 > )
34 34 > configitem(b'delaypush', b'release-path',
35 35 > default=None,
36 36 > )
37 37 >
38 38 > def delaypush(orig, pushop):
39 39 > # notify we are done preparing
40 40 > ui = pushop.repo.ui
41 41 > readypath = ui.config(b'delaypush', b'ready-path')
42 42 > if readypath is not None:
43 43 > with open(readypath, 'w') as r:
44 44 > r.write('foo')
45 45 > ui.status(b'wrote ready: %s\n' % readypath)
46 46 > # now wait for the other process to be done
47 47 > watchpath = ui.config(b'delaypush', b'release-path')
48 48 > if watchpath is not None:
49 49 > ui.status(b'waiting on: %s\n' % watchpath)
50 50 > limit = 100
51 > test_default_timeout = os.environ.get('HGTEST_TIMEOUT_DEFAULT')
52 > test_timeout = os.environ.get('HGTEST_TIMEOUT')
53 > if (
54 > test_default_timeout is not None
55 > and test_timeout is not None
56 > and test_default_timeout < test_timeout
57 > ):
58 > limit = int(limit * (test_timeout / test_default_timeout))
51 59 > while 0 < limit and not os.path.exists(watchpath):
52 60 > limit -= 1
53 61 > time.sleep(0.1)
54 62 > if limit <= 0:
55 63 > ui.warn(b'exiting without watchfile: %s' % watchpath)
56 64 > else:
57 65 > # delete the file at the end of the push
58 66 > def delete():
59 67 > try:
60 68 > os.unlink(watchpath)
61 69 > except FileNotFoundError:
62 70 > pass
63 71 > ui.atexit(delete)
64 72 > return orig(pushop)
65 73 >
66 74 > def uisetup(ui):
67 75 > extensions.wrapfunction(exchange, b'_pushbundle2', delaypush)
68 76 > EOF
69 77
70 78 $ waiton () {
71 79 > # wait for a file to be created (then delete it)
72 80 > count=100
73 81 > while [ ! -f $1 ] ;
74 82 > do
75 83 > sleep 0.1;
76 84 > count=`expr $count - 1`;
77 85 > if [ $count -lt 0 ];
78 86 > then
79 87 > break
80 88 > fi;
81 89 > done
82 90 > [ -f $1 ] || echo "ready file still missing: $1"
83 91 > rm -f $1
84 92 > }
85 93
86 94 $ release () {
87 95 > # create a file and wait for it be deleted
88 96 > count=100
89 97 > touch $1
90 98 > while [ -f $1 ] ;
91 99 > do
92 100 > sleep 0.1;
93 101 > count=`expr $count - 1`;
94 102 > if [ $count -lt 0 ];
95 103 > then
96 104 > break
97 105 > fi;
98 106 > done
99 107 > [ ! -f $1 ] || echo "delay file still exist: $1"
100 108 > }
101 109
102 110 $ cat >> $HGRCPATH << EOF
103 111 > [ui]
104 112 > # simplify output
105 113 > logtemplate = {node|short} {desc} ({branch})
106 114 > [phases]
107 115 > publish = no
108 116 > [experimental]
109 117 > evolution=true
110 118 > [alias]
111 119 > graph = log -G --rev 'sort(all(), "topo")'
112 120 > EOF
113 121
114 122 We tests multiple cases:
115 123 * strict: no race detected,
116 124 * unrelated: race on unrelated heads are allowed.
117 125
118 126 #testcases strict unrelated
119 127
120 128 #if strict
121 129
122 130 $ cat >> $HGRCPATH << EOF
123 131 > [server]
124 132 > concurrent-push-mode = strict
125 133 > EOF
126 134
127 135 #endif
128 136
129 137 Setup
130 138 -----
131 139
132 140 create a repo with one root
133 141
134 142 $ hg init server
135 143 $ cd server
136 144 $ echo root > root
137 145 $ hg ci -Am "C-ROOT"
138 146 adding root
139 147 $ cd ..
140 148
141 149 clone it in two clients
142 150
143 151 $ hg clone ssh://user@dummy/server client-racy
144 152 requesting all changes
145 153 adding changesets
146 154 adding manifests
147 155 adding file changes
148 156 added 1 changesets with 1 changes to 1 files
149 157 new changesets 842e2fac6304 (1 drafts)
150 158 updating to branch default
151 159 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
152 160 $ hg clone ssh://user@dummy/server client-other
153 161 requesting all changes
154 162 adding changesets
155 163 adding manifests
156 164 adding file changes
157 165 added 1 changesets with 1 changes to 1 files
158 166 new changesets 842e2fac6304 (1 drafts)
159 167 updating to branch default
160 168 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
161 169
162 170 setup one to allow race on push
163 171
164 172 $ cat >> client-racy/.hg/hgrc << EOF
165 173 > [extensions]
166 174 > delaypush = $TESTTMP/delaypush.py
167 175 > [delaypush]
168 176 > ready-path = $TESTTMP/readyfile
169 177 > release-path = $TESTTMP/watchfile
170 178 > EOF
171 179
172 180 Simple race, both try to push to the server at the same time
173 181 ------------------------------------------------------------
174 182
175 183 Both try to replace the same head
176 184
177 185 # a
178 186 # | b
179 187 # |/
180 188 # *
181 189
182 190 Creating changesets
183 191
184 192 $ echo b > client-other/a
185 193 $ hg -R client-other/ add client-other/a
186 194 $ hg -R client-other/ commit -m "C-A"
187 195 $ echo b > client-racy/b
188 196 $ hg -R client-racy/ add client-racy/b
189 197 $ hg -R client-racy/ commit -m "C-B"
190 198
191 199 Pushing
192 200
193 201 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
194 202
195 203 $ waiton $TESTTMP/readyfile
196 204
197 205 $ hg -R client-other push -r 'tip'
198 206 pushing to ssh://user@dummy/server
199 207 searching for changes
200 208 remote: adding changesets
201 209 remote: adding manifests
202 210 remote: adding file changes
203 211 remote: added 1 changesets with 1 changes to 1 files
204 212
205 213 $ release $TESTTMP/watchfile
206 214
207 215 Check the result of the push
208 216
209 217 $ cat ./push-log
210 218 pushing to ssh://user@dummy/server
211 219 searching for changes
212 220 wrote ready: $TESTTMP/readyfile
213 221 waiting on: $TESTTMP/watchfile
214 222 abort: push failed:
215 223 'remote repository changed while pushing - please try again'
216 224
217 225 $ hg -R server graph
218 226 o 98217d5a1659 C-A (default)
219 227 |
220 228 @ 842e2fac6304 C-ROOT (default)
221 229
222 230
223 231 Pushing on two different heads
224 232 ------------------------------
225 233
226 234 Both try to replace a different head
227 235
228 236 # a b
229 237 # | |
230 238 # * *
231 239 # |/
232 240 # *
233 241
234 242 (resync-all)
235 243
236 244 $ hg -R ./server pull ./client-racy
237 245 pulling from ./client-racy
238 246 searching for changes
239 247 adding changesets
240 248 adding manifests
241 249 adding file changes
242 250 added 1 changesets with 1 changes to 1 files (+1 heads)
243 251 new changesets a9149a1428e2 (1 drafts)
244 252 (run 'hg heads' to see heads, 'hg merge' to merge)
245 253 $ hg -R ./client-other pull
246 254 pulling from ssh://user@dummy/server
247 255 searching for changes
248 256 adding changesets
249 257 adding manifests
250 258 adding file changes
251 259 added 1 changesets with 1 changes to 1 files (+1 heads)
252 260 new changesets a9149a1428e2 (1 drafts)
253 261 (run 'hg heads' to see heads, 'hg merge' to merge)
254 262 $ hg -R ./client-racy pull
255 263 pulling from ssh://user@dummy/server
256 264 searching for changes
257 265 adding changesets
258 266 adding manifests
259 267 adding file changes
260 268 added 1 changesets with 1 changes to 1 files (+1 heads)
261 269 new changesets 98217d5a1659 (1 drafts)
262 270 (run 'hg heads' to see heads, 'hg merge' to merge)
263 271
264 272 $ hg -R server graph
265 273 o a9149a1428e2 C-B (default)
266 274 |
267 275 | o 98217d5a1659 C-A (default)
268 276 |/
269 277 @ 842e2fac6304 C-ROOT (default)
270 278
271 279
272 280 Creating changesets
273 281
274 282 $ echo aa >> client-other/a
275 283 $ hg -R client-other/ commit -m "C-C"
276 284 $ echo bb >> client-racy/b
277 285 $ hg -R client-racy/ commit -m "C-D"
278 286
279 287 Pushing
280 288
281 289 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
282 290
283 291 $ waiton $TESTTMP/readyfile
284 292
285 293 $ hg -R client-other push -r 'tip'
286 294 pushing to ssh://user@dummy/server
287 295 searching for changes
288 296 remote: adding changesets
289 297 remote: adding manifests
290 298 remote: adding file changes
291 299 remote: added 1 changesets with 1 changes to 1 files
292 300
293 301 $ release $TESTTMP/watchfile
294 302
295 303 Check the result of the push
296 304
297 305 #if strict
298 306 $ cat ./push-log
299 307 pushing to ssh://user@dummy/server
300 308 searching for changes
301 309 wrote ready: $TESTTMP/readyfile
302 310 waiting on: $TESTTMP/watchfile
303 311 abort: push failed:
304 312 'remote repository changed while pushing - please try again'
305 313
306 314 $ hg -R server graph
307 315 o 51c544a58128 C-C (default)
308 316 |
309 317 o 98217d5a1659 C-A (default)
310 318 |
311 319 | o a9149a1428e2 C-B (default)
312 320 |/
313 321 @ 842e2fac6304 C-ROOT (default)
314 322
315 323 #endif
316 324 #if unrelated
317 325
318 326 (The two heads are unrelated, push should be allowed)
319 327
320 328 $ cat ./push-log
321 329 pushing to ssh://user@dummy/server
322 330 searching for changes
323 331 wrote ready: $TESTTMP/readyfile
324 332 waiting on: $TESTTMP/watchfile
325 333 remote: adding changesets
326 334 remote: adding manifests
327 335 remote: adding file changes
328 336 remote: added 1 changesets with 1 changes to 1 files
329 337
330 338 $ hg -R server graph
331 339 o 59e76faf78bd C-D (default)
332 340 |
333 341 o a9149a1428e2 C-B (default)
334 342 |
335 343 | o 51c544a58128 C-C (default)
336 344 | |
337 345 | o 98217d5a1659 C-A (default)
338 346 |/
339 347 @ 842e2fac6304 C-ROOT (default)
340 348
341 349 #endif
342 350
343 351 Pushing while someone creates a new head
344 352 -----------------------------------------
345 353
346 354 Pushing a new changeset while someone creates a new branch.
347 355
348 356 # a (raced)
349 357 # |
350 358 # * b
351 359 # |/
352 360 # *
353 361
354 362 (resync-all)
355 363
356 364 #if strict
357 365
358 366 $ hg -R ./server pull ./client-racy
359 367 pulling from ./client-racy
360 368 searching for changes
361 369 adding changesets
362 370 adding manifests
363 371 adding file changes
364 372 added 1 changesets with 1 changes to 1 files
365 373 new changesets 59e76faf78bd (1 drafts)
366 374 (run 'hg update' to get a working copy)
367 375
368 376 #endif
369 377 #if unrelated
370 378
371 379 $ hg -R ./server pull ./client-racy
372 380 pulling from ./client-racy
373 381 searching for changes
374 382 no changes found
375 383
376 384 #endif
377 385
378 386 $ hg -R ./client-other pull
379 387 pulling from ssh://user@dummy/server
380 388 searching for changes
381 389 adding changesets
382 390 adding manifests
383 391 adding file changes
384 392 added 1 changesets with 1 changes to 1 files
385 393 new changesets 59e76faf78bd (1 drafts)
386 394 (run 'hg update' to get a working copy)
387 395 $ hg -R ./client-racy pull
388 396 pulling from ssh://user@dummy/server
389 397 searching for changes
390 398 adding changesets
391 399 adding manifests
392 400 adding file changes
393 401 added 1 changesets with 1 changes to 1 files
394 402 new changesets 51c544a58128 (1 drafts)
395 403 (run 'hg update' to get a working copy)
396 404
397 405 $ hg -R server graph
398 406 o 59e76faf78bd C-D (default)
399 407 |
400 408 o a9149a1428e2 C-B (default)
401 409 |
402 410 | o 51c544a58128 C-C (default)
403 411 | |
404 412 | o 98217d5a1659 C-A (default)
405 413 |/
406 414 @ 842e2fac6304 C-ROOT (default)
407 415
408 416
409 417 Creating changesets
410 418
411 419 (new head)
412 420
413 421 $ hg -R client-other/ up 'desc("C-A")'
414 422 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
415 423 $ echo aaa >> client-other/a
416 424 $ hg -R client-other/ commit -m "C-E"
417 425 created new head
418 426
419 427 (children of existing head)
420 428
421 429 $ hg -R client-racy/ up 'desc("C-C")'
422 430 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
423 431 $ echo bbb >> client-racy/a
424 432 $ hg -R client-racy/ commit -m "C-F"
425 433
426 434 Pushing
427 435
428 436 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
429 437
430 438 $ waiton $TESTTMP/readyfile
431 439
432 440 $ hg -R client-other push -fr 'tip'
433 441 pushing to ssh://user@dummy/server
434 442 searching for changes
435 443 remote: adding changesets
436 444 remote: adding manifests
437 445 remote: adding file changes
438 446 remote: added 1 changesets with 1 changes to 1 files (+1 heads)
439 447
440 448 $ release $TESTTMP/watchfile
441 449
442 450 Check the result of the push
443 451
444 452 #if strict
445 453
446 454 $ cat ./push-log
447 455 pushing to ssh://user@dummy/server
448 456 searching for changes
449 457 wrote ready: $TESTTMP/readyfile
450 458 waiting on: $TESTTMP/watchfile
451 459 abort: push failed:
452 460 'remote repository changed while pushing - please try again'
453 461
454 462 $ hg -R server graph
455 463 o d603e2c0cdd7 C-E (default)
456 464 |
457 465 | o 51c544a58128 C-C (default)
458 466 |/
459 467 o 98217d5a1659 C-A (default)
460 468 |
461 469 | o 59e76faf78bd C-D (default)
462 470 | |
463 471 | o a9149a1428e2 C-B (default)
464 472 |/
465 473 @ 842e2fac6304 C-ROOT (default)
466 474
467 475
468 476 #endif
469 477
470 478 #if unrelated
471 479
472 480 (The racing new head do not affect existing heads, push should go through)
473 481
474 482 $ cat ./push-log
475 483 pushing to ssh://user@dummy/server
476 484 searching for changes
477 485 wrote ready: $TESTTMP/readyfile
478 486 waiting on: $TESTTMP/watchfile
479 487 remote: adding changesets
480 488 remote: adding manifests
481 489 remote: adding file changes
482 490 remote: added 1 changesets with 1 changes to 1 files
483 491
484 492 $ hg -R server graph
485 493 o d9e379a8c432 C-F (default)
486 494 |
487 495 o 51c544a58128 C-C (default)
488 496 |
489 497 | o d603e2c0cdd7 C-E (default)
490 498 |/
491 499 o 98217d5a1659 C-A (default)
492 500 |
493 501 | o 59e76faf78bd C-D (default)
494 502 | |
495 503 | o a9149a1428e2 C-B (default)
496 504 |/
497 505 @ 842e2fac6304 C-ROOT (default)
498 506
499 507 #endif
500 508
501 509 Pushing touching different named branch (same topo): new branch raced
502 510 ---------------------------------------------------------------------
503 511
504 512 Pushing two children on the same head, one is a different named branch
505 513
506 514 # a (raced, branch-a)
507 515 # |
508 516 # | b (default branch)
509 517 # |/
510 518 # *
511 519
512 520 (resync-all)
513 521
514 522 #if strict
515 523
516 524 $ hg -R ./server pull ./client-racy
517 525 pulling from ./client-racy
518 526 searching for changes
519 527 adding changesets
520 528 adding manifests
521 529 adding file changes
522 530 added 1 changesets with 1 changes to 1 files
523 531 new changesets d9e379a8c432 (1 drafts)
524 532 (run 'hg update' to get a working copy)
525 533
526 534 #endif
527 535 #if unrelated
528 536
529 537 $ hg -R ./server pull ./client-racy
530 538 pulling from ./client-racy
531 539 searching for changes
532 540 no changes found
533 541
534 542 #endif
535 543
536 544 $ hg -R ./client-other pull
537 545 pulling from ssh://user@dummy/server
538 546 searching for changes
539 547 adding changesets
540 548 adding manifests
541 549 adding file changes
542 550 added 1 changesets with 1 changes to 1 files
543 551 new changesets d9e379a8c432 (1 drafts)
544 552 (run 'hg update' to get a working copy)
545 553 $ hg -R ./client-racy pull
546 554 pulling from ssh://user@dummy/server
547 555 searching for changes
548 556 adding changesets
549 557 adding manifests
550 558 adding file changes
551 559 added 1 changesets with 1 changes to 1 files (+1 heads)
552 560 new changesets d603e2c0cdd7 (1 drafts)
553 561 (run 'hg heads .' to see heads, 'hg merge' to merge)
554 562
555 563 $ hg -R server graph
556 564 o d9e379a8c432 C-F (default)
557 565 |
558 566 o 51c544a58128 C-C (default)
559 567 |
560 568 | o d603e2c0cdd7 C-E (default)
561 569 |/
562 570 o 98217d5a1659 C-A (default)
563 571 |
564 572 | o 59e76faf78bd C-D (default)
565 573 | |
566 574 | o a9149a1428e2 C-B (default)
567 575 |/
568 576 @ 842e2fac6304 C-ROOT (default)
569 577
570 578
571 579 Creating changesets
572 580
573 581 (update existing head)
574 582
575 583 $ hg -R client-other/ up 'desc("C-F")'
576 584 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
577 585 $ echo aaa >> client-other/a
578 586 $ hg -R client-other/ commit -m "C-G"
579 587
580 588 (new named branch from that existing head)
581 589
582 590 $ hg -R client-racy/ up 'desc("C-F")'
583 591 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
584 592 $ echo bbb >> client-racy/a
585 593 $ hg -R client-racy/ branch my-first-test-branch
586 594 marked working directory as branch my-first-test-branch
587 595 (branches are permanent and global, did you want a bookmark?)
588 596 $ hg -R client-racy/ commit -m "C-H"
589 597
590 598 Pushing
591 599
592 600 $ hg -R client-racy push -r 'tip' --new-branch > ./push-log 2>&1 &
593 601
594 602 $ waiton $TESTTMP/readyfile
595 603
596 604 $ hg -R client-other push -fr 'tip'
597 605 pushing to ssh://user@dummy/server
598 606 searching for changes
599 607 remote: adding changesets
600 608 remote: adding manifests
601 609 remote: adding file changes
602 610 remote: added 1 changesets with 1 changes to 1 files
603 611
604 612 $ release $TESTTMP/watchfile
605 613
606 614 Check the result of the push
607 615
608 616 #if strict
609 617 $ cat ./push-log
610 618 pushing to ssh://user@dummy/server
611 619 searching for changes
612 620 wrote ready: $TESTTMP/readyfile
613 621 waiting on: $TESTTMP/watchfile
614 622 abort: push failed:
615 623 'remote repository changed while pushing - please try again'
616 624
617 625 $ hg -R server graph
618 626 o 75d69cba5402 C-G (default)
619 627 |
620 628 o d9e379a8c432 C-F (default)
621 629 |
622 630 o 51c544a58128 C-C (default)
623 631 |
624 632 | o d603e2c0cdd7 C-E (default)
625 633 |/
626 634 o 98217d5a1659 C-A (default)
627 635 |
628 636 | o 59e76faf78bd C-D (default)
629 637 | |
630 638 | o a9149a1428e2 C-B (default)
631 639 |/
632 640 @ 842e2fac6304 C-ROOT (default)
633 641
634 642 #endif
635 643 #if unrelated
636 644
637 645 (unrelated named branches are unrelated)
638 646
639 647 $ cat ./push-log
640 648 pushing to ssh://user@dummy/server
641 649 searching for changes
642 650 wrote ready: $TESTTMP/readyfile
643 651 waiting on: $TESTTMP/watchfile
644 652 remote: adding changesets
645 653 remote: adding manifests
646 654 remote: adding file changes
647 655 remote: added 1 changesets with 1 changes to 1 files (+1 heads)
648 656
649 657 $ hg -R server graph
650 658 o 833be552cfe6 C-H (my-first-test-branch)
651 659 |
652 660 | o 75d69cba5402 C-G (default)
653 661 |/
654 662 o d9e379a8c432 C-F (default)
655 663 |
656 664 o 51c544a58128 C-C (default)
657 665 |
658 666 | o d603e2c0cdd7 C-E (default)
659 667 |/
660 668 o 98217d5a1659 C-A (default)
661 669 |
662 670 | o 59e76faf78bd C-D (default)
663 671 | |
664 672 | o a9149a1428e2 C-B (default)
665 673 |/
666 674 @ 842e2fac6304 C-ROOT (default)
667 675
668 676 #endif
669 677
670 678 The racing new head do not affect existing heads, push should go through
671 679
672 680 pushing touching different named branch (same topo): old branch raced
673 681 ---------------------------------------------------------------------
674 682
675 683 Pushing two children on the same head, one is a different named branch
676 684
677 685 # a (raced, default-branch)
678 686 # |
679 687 # | b (new branch)
680 688 # |/
681 689 # * (default-branch)
682 690
683 691 (resync-all)
684 692
685 693 #if strict
686 694
687 695 $ hg -R ./server pull ./client-racy
688 696 pulling from ./client-racy
689 697 searching for changes
690 698 adding changesets
691 699 adding manifests
692 700 adding file changes
693 701 added 1 changesets with 1 changes to 1 files (+1 heads)
694 702 new changesets 833be552cfe6 (1 drafts)
695 703 (run 'hg heads .' to see heads, 'hg merge' to merge)
696 704
697 705 #endif
698 706 #if unrelated
699 707
700 708 $ hg -R ./server pull ./client-racy
701 709 pulling from ./client-racy
702 710 searching for changes
703 711 no changes found
704 712
705 713 #endif
706 714
707 715 $ hg -R ./client-other pull
708 716 pulling from ssh://user@dummy/server
709 717 searching for changes
710 718 adding changesets
711 719 adding manifests
712 720 adding file changes
713 721 added 1 changesets with 1 changes to 1 files (+1 heads)
714 722 new changesets 833be552cfe6 (1 drafts)
715 723 (run 'hg heads .' to see heads, 'hg merge' to merge)
716 724 $ hg -R ./client-racy pull
717 725 pulling from ssh://user@dummy/server
718 726 searching for changes
719 727 adding changesets
720 728 adding manifests
721 729 adding file changes
722 730 added 1 changesets with 1 changes to 1 files (+1 heads)
723 731 new changesets 75d69cba5402 (1 drafts)
724 732 (run 'hg heads' to see heads)
725 733
726 734 $ hg -R server graph
727 735 o 833be552cfe6 C-H (my-first-test-branch)
728 736 |
729 737 | o 75d69cba5402 C-G (default)
730 738 |/
731 739 o d9e379a8c432 C-F (default)
732 740 |
733 741 o 51c544a58128 C-C (default)
734 742 |
735 743 | o d603e2c0cdd7 C-E (default)
736 744 |/
737 745 o 98217d5a1659 C-A (default)
738 746 |
739 747 | o 59e76faf78bd C-D (default)
740 748 | |
741 749 | o a9149a1428e2 C-B (default)
742 750 |/
743 751 @ 842e2fac6304 C-ROOT (default)
744 752
745 753
746 754 Creating changesets
747 755
748 756 (new named branch from one head)
749 757
750 758 $ hg -R client-other/ up 'desc("C-G")'
751 759 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
752 760 $ echo aaa >> client-other/a
753 761 $ hg -R client-other/ branch my-second-test-branch
754 762 marked working directory as branch my-second-test-branch
755 763 $ hg -R client-other/ commit -m "C-I"
756 764
757 765 (children "updating" that same head)
758 766
759 767 $ hg -R client-racy/ up 'desc("C-G")'
760 768 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
761 769 $ echo bbb >> client-racy/a
762 770 $ hg -R client-racy/ commit -m "C-J"
763 771
764 772 Pushing
765 773
766 774 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
767 775
768 776 $ waiton $TESTTMP/readyfile
769 777
770 778 $ hg -R client-other push -fr 'tip' --new-branch
771 779 pushing to ssh://user@dummy/server
772 780 searching for changes
773 781 remote: adding changesets
774 782 remote: adding manifests
775 783 remote: adding file changes
776 784 remote: added 1 changesets with 1 changes to 1 files
777 785
778 786 $ release $TESTTMP/watchfile
779 787
780 788 Check the result of the push
781 789
782 790 #if strict
783 791
784 792 $ cat ./push-log
785 793 pushing to ssh://user@dummy/server
786 794 searching for changes
787 795 wrote ready: $TESTTMP/readyfile
788 796 waiting on: $TESTTMP/watchfile
789 797 abort: push failed:
790 798 'remote repository changed while pushing - please try again'
791 799
792 800 $ hg -R server graph
793 801 o b35ed749f288 C-I (my-second-test-branch)
794 802 |
795 803 o 75d69cba5402 C-G (default)
796 804 |
797 805 | o 833be552cfe6 C-H (my-first-test-branch)
798 806 |/
799 807 o d9e379a8c432 C-F (default)
800 808 |
801 809 o 51c544a58128 C-C (default)
802 810 |
803 811 | o d603e2c0cdd7 C-E (default)
804 812 |/
805 813 o 98217d5a1659 C-A (default)
806 814 |
807 815 | o 59e76faf78bd C-D (default)
808 816 | |
809 817 | o a9149a1428e2 C-B (default)
810 818 |/
811 819 @ 842e2fac6304 C-ROOT (default)
812 820
813 821
814 822 #endif
815 823
816 824 #if unrelated
817 825
818 826 (unrelated named branches are unrelated)
819 827
820 828 $ cat ./push-log
821 829 pushing to ssh://user@dummy/server
822 830 searching for changes
823 831 wrote ready: $TESTTMP/readyfile
824 832 waiting on: $TESTTMP/watchfile
825 833 remote: adding changesets
826 834 remote: adding manifests
827 835 remote: adding file changes
828 836 remote: added 1 changesets with 1 changes to 1 files (+1 heads)
829 837
830 838 $ hg -R server graph
831 839 o 89420bf00fae C-J (default)
832 840 |
833 841 | o b35ed749f288 C-I (my-second-test-branch)
834 842 |/
835 843 o 75d69cba5402 C-G (default)
836 844 |
837 845 | o 833be552cfe6 C-H (my-first-test-branch)
838 846 |/
839 847 o d9e379a8c432 C-F (default)
840 848 |
841 849 o 51c544a58128 C-C (default)
842 850 |
843 851 | o d603e2c0cdd7 C-E (default)
844 852 |/
845 853 o 98217d5a1659 C-A (default)
846 854 |
847 855 | o 59e76faf78bd C-D (default)
848 856 | |
849 857 | o a9149a1428e2 C-B (default)
850 858 |/
851 859 @ 842e2fac6304 C-ROOT (default)
852 860
853 861
854 862 #endif
855 863
856 864 pushing racing push touch multiple heads
857 865 ----------------------------------------
858 866
859 867 There are multiple heads, but the racing push touch all of them
860 868
861 869 # a (raced)
862 870 # | b
863 871 # |/|
864 872 # * *
865 873 # |/
866 874 # *
867 875
868 876 (resync-all)
869 877
870 878 #if strict
871 879
872 880 $ hg -R ./server pull ./client-racy
873 881 pulling from ./client-racy
874 882 searching for changes
875 883 adding changesets
876 884 adding manifests
877 885 adding file changes
878 886 added 1 changesets with 1 changes to 1 files (+1 heads)
879 887 new changesets 89420bf00fae (1 drafts)
880 888 (run 'hg heads .' to see heads, 'hg merge' to merge)
881 889
882 890 #endif
883 891
884 892 #if unrelated
885 893
886 894 $ hg -R ./server pull ./client-racy
887 895 pulling from ./client-racy
888 896 searching for changes
889 897 no changes found
890 898
891 899 #endif
892 900
893 901 $ hg -R ./client-other pull
894 902 pulling from ssh://user@dummy/server
895 903 searching for changes
896 904 adding changesets
897 905 adding manifests
898 906 adding file changes
899 907 added 1 changesets with 1 changes to 1 files (+1 heads)
900 908 new changesets 89420bf00fae (1 drafts)
901 909 (run 'hg heads' to see heads)
902 910 $ hg -R ./client-racy pull
903 911 pulling from ssh://user@dummy/server
904 912 searching for changes
905 913 adding changesets
906 914 adding manifests
907 915 adding file changes
908 916 added 1 changesets with 1 changes to 1 files (+1 heads)
909 917 new changesets b35ed749f288 (1 drafts)
910 918 (run 'hg heads .' to see heads, 'hg merge' to merge)
911 919
912 920 $ hg -R server graph
913 921 o 89420bf00fae C-J (default)
914 922 |
915 923 | o b35ed749f288 C-I (my-second-test-branch)
916 924 |/
917 925 o 75d69cba5402 C-G (default)
918 926 |
919 927 | o 833be552cfe6 C-H (my-first-test-branch)
920 928 |/
921 929 o d9e379a8c432 C-F (default)
922 930 |
923 931 o 51c544a58128 C-C (default)
924 932 |
925 933 | o d603e2c0cdd7 C-E (default)
926 934 |/
927 935 o 98217d5a1659 C-A (default)
928 936 |
929 937 | o 59e76faf78bd C-D (default)
930 938 | |
931 939 | o a9149a1428e2 C-B (default)
932 940 |/
933 941 @ 842e2fac6304 C-ROOT (default)
934 942
935 943
936 944 Creating changesets
937 945
938 946 (merges heads)
939 947
940 948 $ hg -R client-other/ up 'desc("C-E")'
941 949 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
942 950 $ hg -R client-other/ merge 'desc("C-D")'
943 951 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
944 952 (branch merge, don't forget to commit)
945 953 $ hg -R client-other/ commit -m "C-K"
946 954
947 955 (update one head)
948 956
949 957 $ hg -R client-racy/ up 'desc("C-D")'
950 958 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
951 959 $ echo bbb >> client-racy/b
952 960 $ hg -R client-racy/ commit -m "C-L"
953 961
954 962 Pushing
955 963
956 964 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
957 965
958 966 $ waiton $TESTTMP/readyfile
959 967
960 968 $ hg -R client-other push -fr 'tip' --new-branch
961 969 pushing to ssh://user@dummy/server
962 970 searching for changes
963 971 remote: adding changesets
964 972 remote: adding manifests
965 973 remote: adding file changes
966 974 remote: added 1 changesets with 0 changes to 0 files (-1 heads)
967 975
968 976 $ release $TESTTMP/watchfile
969 977
970 978 Check the result of the push
971 979
972 980 $ cat ./push-log
973 981 pushing to ssh://user@dummy/server
974 982 searching for changes
975 983 wrote ready: $TESTTMP/readyfile
976 984 waiting on: $TESTTMP/watchfile
977 985 abort: push failed:
978 986 'remote repository changed while pushing - please try again'
979 987
980 988 $ hg -R server graph
981 989 o be705100c623 C-K (default)
982 990 |\
983 991 | o d603e2c0cdd7 C-E (default)
984 992 | |
985 993 o | 59e76faf78bd C-D (default)
986 994 | |
987 995 | | o 89420bf00fae C-J (default)
988 996 | | |
989 997 | | | o b35ed749f288 C-I (my-second-test-branch)
990 998 | | |/
991 999 | | o 75d69cba5402 C-G (default)
992 1000 | | |
993 1001 | | | o 833be552cfe6 C-H (my-first-test-branch)
994 1002 | | |/
995 1003 | | o d9e379a8c432 C-F (default)
996 1004 | | |
997 1005 | | o 51c544a58128 C-C (default)
998 1006 | |/
999 1007 o | a9149a1428e2 C-B (default)
1000 1008 | |
1001 1009 | o 98217d5a1659 C-A (default)
1002 1010 |/
1003 1011 @ 842e2fac6304 C-ROOT (default)
1004 1012
1005 1013
1006 1014 pushing raced push touch multiple heads
1007 1015 ---------------------------------------
1008 1016
1009 1017 There are multiple heads, the raced push touch all of them
1010 1018
1011 1019 # b
1012 1020 # | a (raced)
1013 1021 # |/|
1014 1022 # * *
1015 1023 # |/
1016 1024 # *
1017 1025
1018 1026 (resync-all)
1019 1027
1020 1028 $ hg -R ./server pull ./client-racy
1021 1029 pulling from ./client-racy
1022 1030 searching for changes
1023 1031 adding changesets
1024 1032 adding manifests
1025 1033 adding file changes
1026 1034 added 1 changesets with 1 changes to 1 files (+1 heads)
1027 1035 new changesets cac2cead0ff0 (1 drafts)
1028 1036 (run 'hg heads .' to see heads, 'hg merge' to merge)
1029 1037 $ hg -R ./client-other pull
1030 1038 pulling from ssh://user@dummy/server
1031 1039 searching for changes
1032 1040 adding changesets
1033 1041 adding manifests
1034 1042 adding file changes
1035 1043 added 1 changesets with 1 changes to 1 files (+1 heads)
1036 1044 new changesets cac2cead0ff0 (1 drafts)
1037 1045 (run 'hg heads .' to see heads, 'hg merge' to merge)
1038 1046 $ hg -R ./client-racy pull
1039 1047 pulling from ssh://user@dummy/server
1040 1048 searching for changes
1041 1049 adding changesets
1042 1050 adding manifests
1043 1051 adding file changes
1044 1052 added 1 changesets with 0 changes to 0 files
1045 1053 new changesets be705100c623 (1 drafts)
1046 1054 (run 'hg update' to get a working copy)
1047 1055
1048 1056 $ hg -R server graph
1049 1057 o cac2cead0ff0 C-L (default)
1050 1058 |
1051 1059 | o be705100c623 C-K (default)
1052 1060 |/|
1053 1061 | o d603e2c0cdd7 C-E (default)
1054 1062 | |
1055 1063 o | 59e76faf78bd C-D (default)
1056 1064 | |
1057 1065 | | o 89420bf00fae C-J (default)
1058 1066 | | |
1059 1067 | | | o b35ed749f288 C-I (my-second-test-branch)
1060 1068 | | |/
1061 1069 | | o 75d69cba5402 C-G (default)
1062 1070 | | |
1063 1071 | | | o 833be552cfe6 C-H (my-first-test-branch)
1064 1072 | | |/
1065 1073 | | o d9e379a8c432 C-F (default)
1066 1074 | | |
1067 1075 | | o 51c544a58128 C-C (default)
1068 1076 | |/
1069 1077 o | a9149a1428e2 C-B (default)
1070 1078 | |
1071 1079 | o 98217d5a1659 C-A (default)
1072 1080 |/
1073 1081 @ 842e2fac6304 C-ROOT (default)
1074 1082
1075 1083
1076 1084 Creating changesets
1077 1085
1078 1086 (update existing head)
1079 1087
1080 1088 $ echo aaa >> client-other/a
1081 1089 $ hg -R client-other/ commit -m "C-M"
1082 1090
1083 1091 (merge heads)
1084 1092
1085 1093 $ hg -R client-racy/ merge 'desc("C-K")'
1086 1094 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1087 1095 (branch merge, don't forget to commit)
1088 1096 $ hg -R client-racy/ commit -m "C-N"
1089 1097
1090 1098 Pushing
1091 1099
1092 1100 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1093 1101
1094 1102 $ waiton $TESTTMP/readyfile
1095 1103
1096 1104 $ hg -R client-other push -fr 'tip' --new-branch
1097 1105 pushing to ssh://user@dummy/server
1098 1106 searching for changes
1099 1107 remote: adding changesets
1100 1108 remote: adding manifests
1101 1109 remote: adding file changes
1102 1110 remote: added 1 changesets with 1 changes to 1 files
1103 1111
1104 1112 $ release $TESTTMP/watchfile
1105 1113
1106 1114 Check the result of the push
1107 1115
1108 1116 $ cat ./push-log
1109 1117 pushing to ssh://user@dummy/server
1110 1118 searching for changes
1111 1119 wrote ready: $TESTTMP/readyfile
1112 1120 waiting on: $TESTTMP/watchfile
1113 1121 abort: push failed:
1114 1122 'remote repository changed while pushing - please try again'
1115 1123
1116 1124 $ hg -R server graph
1117 1125 o 6fd3090135df C-M (default)
1118 1126 |
1119 1127 o be705100c623 C-K (default)
1120 1128 |\
1121 1129 | o d603e2c0cdd7 C-E (default)
1122 1130 | |
1123 1131 +---o cac2cead0ff0 C-L (default)
1124 1132 | |
1125 1133 o | 59e76faf78bd C-D (default)
1126 1134 | |
1127 1135 | | o 89420bf00fae C-J (default)
1128 1136 | | |
1129 1137 | | | o b35ed749f288 C-I (my-second-test-branch)
1130 1138 | | |/
1131 1139 | | o 75d69cba5402 C-G (default)
1132 1140 | | |
1133 1141 | | | o 833be552cfe6 C-H (my-first-test-branch)
1134 1142 | | |/
1135 1143 | | o d9e379a8c432 C-F (default)
1136 1144 | | |
1137 1145 | | o 51c544a58128 C-C (default)
1138 1146 | |/
1139 1147 o | a9149a1428e2 C-B (default)
1140 1148 | |
1141 1149 | o 98217d5a1659 C-A (default)
1142 1150 |/
1143 1151 @ 842e2fac6304 C-ROOT (default)
1144 1152
1145 1153
1146 1154 racing commit push a new head behind another named branch
1147 1155 ---------------------------------------------------------
1148 1156
1149 1157 non-continuous branch are valid case, we tests for them.
1150 1158
1151 1159 # b (branch default)
1152 1160 # |
1153 1161 # o (branch foo)
1154 1162 # |
1155 1163 # | a (raced, branch default)
1156 1164 # |/
1157 1165 # * (branch foo)
1158 1166 # |
1159 1167 # * (branch default)
1160 1168
1161 1169 (resync-all + other branch)
1162 1170
1163 1171 $ hg -R ./server pull ./client-racy
1164 1172 pulling from ./client-racy
1165 1173 searching for changes
1166 1174 adding changesets
1167 1175 adding manifests
1168 1176 adding file changes
1169 1177 added 1 changesets with 0 changes to 0 files
1170 1178 new changesets 866a66e18630 (1 drafts)
1171 1179 (run 'hg update' to get a working copy)
1172 1180
1173 1181 (creates named branch on head)
1174 1182
1175 1183 $ hg -R ./server/ up 'desc("C-N")'
1176 1184 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1177 1185 $ hg -R ./server/ branch other
1178 1186 marked working directory as branch other
1179 1187 $ hg -R ./server/ ci -m "C-Z"
1180 1188 $ hg -R ./server/ up null
1181 1189 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
1182 1190
1183 1191 (sync client)
1184 1192
1185 1193 $ hg -R ./client-other pull
1186 1194 pulling from ssh://user@dummy/server
1187 1195 searching for changes
1188 1196 adding changesets
1189 1197 adding manifests
1190 1198 adding file changes
1191 1199 added 2 changesets with 0 changes to 0 files
1192 1200 new changesets 866a66e18630:55a6f1c01b48 (2 drafts)
1193 1201 (run 'hg update' to get a working copy)
1194 1202 $ hg -R ./client-racy pull
1195 1203 pulling from ssh://user@dummy/server
1196 1204 searching for changes
1197 1205 adding changesets
1198 1206 adding manifests
1199 1207 adding file changes
1200 1208 added 2 changesets with 1 changes to 1 files (+1 heads)
1201 1209 new changesets 6fd3090135df:55a6f1c01b48 (2 drafts)
1202 1210 (run 'hg heads .' to see heads, 'hg merge' to merge)
1203 1211
1204 1212 $ hg -R server graph
1205 1213 o 55a6f1c01b48 C-Z (other)
1206 1214 |
1207 1215 o 866a66e18630 C-N (default)
1208 1216 |\
1209 1217 +---o 6fd3090135df C-M (default)
1210 1218 | |
1211 1219 | o cac2cead0ff0 C-L (default)
1212 1220 | |
1213 1221 o | be705100c623 C-K (default)
1214 1222 |\|
1215 1223 o | d603e2c0cdd7 C-E (default)
1216 1224 | |
1217 1225 | o 59e76faf78bd C-D (default)
1218 1226 | |
1219 1227 | | o 89420bf00fae C-J (default)
1220 1228 | | |
1221 1229 | | | o b35ed749f288 C-I (my-second-test-branch)
1222 1230 | | |/
1223 1231 | | o 75d69cba5402 C-G (default)
1224 1232 | | |
1225 1233 | | | o 833be552cfe6 C-H (my-first-test-branch)
1226 1234 | | |/
1227 1235 | | o d9e379a8c432 C-F (default)
1228 1236 | | |
1229 1237 +---o 51c544a58128 C-C (default)
1230 1238 | |
1231 1239 | o a9149a1428e2 C-B (default)
1232 1240 | |
1233 1241 o | 98217d5a1659 C-A (default)
1234 1242 |/
1235 1243 o 842e2fac6304 C-ROOT (default)
1236 1244
1237 1245
1238 1246 Creating changesets
1239 1247
1240 1248 (update default head through another named branch one)
1241 1249
1242 1250 $ hg -R client-other/ up 'desc("C-Z")'
1243 1251 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
1244 1252 $ echo aaa >> client-other/a
1245 1253 $ hg -R client-other/ commit -m "C-O"
1246 1254 $ echo aaa >> client-other/a
1247 1255 $ hg -R client-other/ branch --force default
1248 1256 marked working directory as branch default
1249 1257 $ hg -R client-other/ commit -m "C-P"
1250 1258 created new head
1251 1259
1252 1260 (update default head)
1253 1261
1254 1262 $ hg -R client-racy/ up 'desc("C-Z")'
1255 1263 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1256 1264 $ echo bbb >> client-other/a
1257 1265 $ hg -R client-racy/ branch --force default
1258 1266 marked working directory as branch default
1259 1267 $ hg -R client-racy/ commit -m "C-Q"
1260 1268 created new head
1261 1269
1262 1270 Pushing
1263 1271
1264 1272 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1265 1273
1266 1274 $ waiton $TESTTMP/readyfile
1267 1275
1268 1276 $ hg -R client-other push -fr 'tip' --new-branch
1269 1277 pushing to ssh://user@dummy/server
1270 1278 searching for changes
1271 1279 remote: adding changesets
1272 1280 remote: adding manifests
1273 1281 remote: adding file changes
1274 1282 remote: added 2 changesets with 1 changes to 1 files
1275 1283
1276 1284 $ release $TESTTMP/watchfile
1277 1285
1278 1286 Check the result of the push
1279 1287
1280 1288 $ cat ./push-log
1281 1289 pushing to ssh://user@dummy/server
1282 1290 searching for changes
1283 1291 wrote ready: $TESTTMP/readyfile
1284 1292 waiting on: $TESTTMP/watchfile
1285 1293 abort: push failed:
1286 1294 'remote repository changed while pushing - please try again'
1287 1295
1288 1296 $ hg -R server graph
1289 1297 o 1b58ee3f79e5 C-P (default)
1290 1298 |
1291 1299 o d0a85b2252a9 C-O (other)
1292 1300 |
1293 1301 o 55a6f1c01b48 C-Z (other)
1294 1302 |
1295 1303 o 866a66e18630 C-N (default)
1296 1304 |\
1297 1305 +---o 6fd3090135df C-M (default)
1298 1306 | |
1299 1307 | o cac2cead0ff0 C-L (default)
1300 1308 | |
1301 1309 o | be705100c623 C-K (default)
1302 1310 |\|
1303 1311 o | d603e2c0cdd7 C-E (default)
1304 1312 | |
1305 1313 | o 59e76faf78bd C-D (default)
1306 1314 | |
1307 1315 | | o 89420bf00fae C-J (default)
1308 1316 | | |
1309 1317 | | | o b35ed749f288 C-I (my-second-test-branch)
1310 1318 | | |/
1311 1319 | | o 75d69cba5402 C-G (default)
1312 1320 | | |
1313 1321 | | | o 833be552cfe6 C-H (my-first-test-branch)
1314 1322 | | |/
1315 1323 | | o d9e379a8c432 C-F (default)
1316 1324 | | |
1317 1325 +---o 51c544a58128 C-C (default)
1318 1326 | |
1319 1327 | o a9149a1428e2 C-B (default)
1320 1328 | |
1321 1329 o | 98217d5a1659 C-A (default)
1322 1330 |/
1323 1331 o 842e2fac6304 C-ROOT (default)
1324 1332
1325 1333
1326 1334 raced commit push a new head behind another named branch
1327 1335 ---------------------------------------------------------
1328 1336
1329 1337 non-continuous branch are valid case, we tests for them.
1330 1338
1331 1339 # b (raced branch default)
1332 1340 # |
1333 1341 # o (branch foo)
1334 1342 # |
1335 1343 # | a (branch default)
1336 1344 # |/
1337 1345 # * (branch foo)
1338 1346 # |
1339 1347 # * (branch default)
1340 1348
1341 1349 (resync-all)
1342 1350
1343 1351 $ hg -R ./server pull ./client-racy
1344 1352 pulling from ./client-racy
1345 1353 searching for changes
1346 1354 adding changesets
1347 1355 adding manifests
1348 1356 adding file changes
1349 1357 added 1 changesets with 0 changes to 0 files (+1 heads)
1350 1358 new changesets b0ee3d6f51bc (1 drafts)
1351 1359 (run 'hg heads .' to see heads, 'hg merge' to merge)
1352 1360 $ hg -R ./client-other pull
1353 1361 pulling from ssh://user@dummy/server
1354 1362 searching for changes
1355 1363 adding changesets
1356 1364 adding manifests
1357 1365 adding file changes
1358 1366 added 1 changesets with 0 changes to 0 files (+1 heads)
1359 1367 new changesets b0ee3d6f51bc (1 drafts)
1360 1368 (run 'hg heads .' to see heads, 'hg merge' to merge)
1361 1369 $ hg -R ./client-racy pull
1362 1370 pulling from ssh://user@dummy/server
1363 1371 searching for changes
1364 1372 adding changesets
1365 1373 adding manifests
1366 1374 adding file changes
1367 1375 added 2 changesets with 1 changes to 1 files (+1 heads)
1368 1376 new changesets d0a85b2252a9:1b58ee3f79e5 (2 drafts)
1369 1377 (run 'hg heads .' to see heads, 'hg merge' to merge)
1370 1378
1371 1379 $ hg -R server graph
1372 1380 o b0ee3d6f51bc C-Q (default)
1373 1381 |
1374 1382 | o 1b58ee3f79e5 C-P (default)
1375 1383 | |
1376 1384 | o d0a85b2252a9 C-O (other)
1377 1385 |/
1378 1386 o 55a6f1c01b48 C-Z (other)
1379 1387 |
1380 1388 o 866a66e18630 C-N (default)
1381 1389 |\
1382 1390 +---o 6fd3090135df C-M (default)
1383 1391 | |
1384 1392 | o cac2cead0ff0 C-L (default)
1385 1393 | |
1386 1394 o | be705100c623 C-K (default)
1387 1395 |\|
1388 1396 o | d603e2c0cdd7 C-E (default)
1389 1397 | |
1390 1398 | o 59e76faf78bd C-D (default)
1391 1399 | |
1392 1400 | | o 89420bf00fae C-J (default)
1393 1401 | | |
1394 1402 | | | o b35ed749f288 C-I (my-second-test-branch)
1395 1403 | | |/
1396 1404 | | o 75d69cba5402 C-G (default)
1397 1405 | | |
1398 1406 | | | o 833be552cfe6 C-H (my-first-test-branch)
1399 1407 | | |/
1400 1408 | | o d9e379a8c432 C-F (default)
1401 1409 | | |
1402 1410 +---o 51c544a58128 C-C (default)
1403 1411 | |
1404 1412 | o a9149a1428e2 C-B (default)
1405 1413 | |
1406 1414 o | 98217d5a1659 C-A (default)
1407 1415 |/
1408 1416 o 842e2fac6304 C-ROOT (default)
1409 1417
1410 1418
1411 1419 Creating changesets
1412 1420
1413 1421 (update 'other' named branch head)
1414 1422
1415 1423 $ hg -R client-other/ up 'desc("C-P")'
1416 1424 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1417 1425 $ echo aaa >> client-other/a
1418 1426 $ hg -R client-other/ branch --force other
1419 1427 marked working directory as branch other
1420 1428 $ hg -R client-other/ commit -m "C-R"
1421 1429 created new head
1422 1430
1423 1431 (update 'other named brnach through a 'default' changeset')
1424 1432
1425 1433 $ hg -R client-racy/ up 'desc("C-P")'
1426 1434 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1427 1435 $ echo bbb >> client-racy/a
1428 1436 $ hg -R client-racy/ commit -m "C-S"
1429 1437 $ echo bbb >> client-racy/a
1430 1438 $ hg -R client-racy/ branch --force other
1431 1439 marked working directory as branch other
1432 1440 $ hg -R client-racy/ commit -m "C-T"
1433 1441 created new head
1434 1442
1435 1443 Pushing
1436 1444
1437 1445 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1438 1446
1439 1447 $ waiton $TESTTMP/readyfile
1440 1448
1441 1449 $ hg -R client-other push -fr 'tip' --new-branch
1442 1450 pushing to ssh://user@dummy/server
1443 1451 searching for changes
1444 1452 remote: adding changesets
1445 1453 remote: adding manifests
1446 1454 remote: adding file changes
1447 1455 remote: added 1 changesets with 1 changes to 1 files
1448 1456
1449 1457 $ release $TESTTMP/watchfile
1450 1458
1451 1459 Check the result of the push
1452 1460
1453 1461 $ cat ./push-log
1454 1462 pushing to ssh://user@dummy/server
1455 1463 searching for changes
1456 1464 wrote ready: $TESTTMP/readyfile
1457 1465 waiting on: $TESTTMP/watchfile
1458 1466 abort: push failed:
1459 1467 'remote repository changed while pushing - please try again'
1460 1468
1461 1469 $ hg -R server graph
1462 1470 o de7b9e2ba3f6 C-R (other)
1463 1471 |
1464 1472 o 1b58ee3f79e5 C-P (default)
1465 1473 |
1466 1474 o d0a85b2252a9 C-O (other)
1467 1475 |
1468 1476 | o b0ee3d6f51bc C-Q (default)
1469 1477 |/
1470 1478 o 55a6f1c01b48 C-Z (other)
1471 1479 |
1472 1480 o 866a66e18630 C-N (default)
1473 1481 |\
1474 1482 +---o 6fd3090135df C-M (default)
1475 1483 | |
1476 1484 | o cac2cead0ff0 C-L (default)
1477 1485 | |
1478 1486 o | be705100c623 C-K (default)
1479 1487 |\|
1480 1488 o | d603e2c0cdd7 C-E (default)
1481 1489 | |
1482 1490 | o 59e76faf78bd C-D (default)
1483 1491 | |
1484 1492 | | o 89420bf00fae C-J (default)
1485 1493 | | |
1486 1494 | | | o b35ed749f288 C-I (my-second-test-branch)
1487 1495 | | |/
1488 1496 | | o 75d69cba5402 C-G (default)
1489 1497 | | |
1490 1498 | | | o 833be552cfe6 C-H (my-first-test-branch)
1491 1499 | | |/
1492 1500 | | o d9e379a8c432 C-F (default)
1493 1501 | | |
1494 1502 +---o 51c544a58128 C-C (default)
1495 1503 | |
1496 1504 | o a9149a1428e2 C-B (default)
1497 1505 | |
1498 1506 o | 98217d5a1659 C-A (default)
1499 1507 |/
1500 1508 o 842e2fac6304 C-ROOT (default)
1501 1509
1502 1510
1503 1511 raced commit push a new head obsoleting the one touched by the racing push
1504 1512 --------------------------------------------------------------------------
1505 1513
1506 1514 # b (racing)
1507 1515 # |
1508 1516 # ø⇠◔ a (raced)
1509 1517 # |/
1510 1518 # *
1511 1519
1512 1520 (resync-all)
1513 1521
1514 1522 $ hg -R ./server pull ./client-racy
1515 1523 pulling from ./client-racy
1516 1524 searching for changes
1517 1525 adding changesets
1518 1526 adding manifests
1519 1527 adding file changes
1520 1528 added 2 changesets with 2 changes to 1 files (+1 heads)
1521 1529 new changesets 2efd43f7b5ba:3d57ed3c1091 (2 drafts)
1522 1530 (run 'hg heads .' to see heads, 'hg merge' to merge)
1523 1531 $ hg -R ./client-other pull
1524 1532 pulling from ssh://user@dummy/server
1525 1533 searching for changes
1526 1534 adding changesets
1527 1535 adding manifests
1528 1536 adding file changes
1529 1537 added 2 changesets with 2 changes to 1 files (+1 heads)
1530 1538 new changesets 2efd43f7b5ba:3d57ed3c1091 (2 drafts)
1531 1539 (run 'hg heads' to see heads, 'hg merge' to merge)
1532 1540 $ hg -R ./client-racy pull
1533 1541 pulling from ssh://user@dummy/server
1534 1542 searching for changes
1535 1543 adding changesets
1536 1544 adding manifests
1537 1545 adding file changes
1538 1546 added 1 changesets with 1 changes to 1 files (+1 heads)
1539 1547 new changesets de7b9e2ba3f6 (1 drafts)
1540 1548 (run 'hg heads' to see heads, 'hg merge' to merge)
1541 1549
1542 1550 $ hg -R server graph
1543 1551 o 3d57ed3c1091 C-T (other)
1544 1552 |
1545 1553 o 2efd43f7b5ba C-S (default)
1546 1554 |
1547 1555 | o de7b9e2ba3f6 C-R (other)
1548 1556 |/
1549 1557 o 1b58ee3f79e5 C-P (default)
1550 1558 |
1551 1559 o d0a85b2252a9 C-O (other)
1552 1560 |
1553 1561 | o b0ee3d6f51bc C-Q (default)
1554 1562 |/
1555 1563 o 55a6f1c01b48 C-Z (other)
1556 1564 |
1557 1565 o 866a66e18630 C-N (default)
1558 1566 |\
1559 1567 +---o 6fd3090135df C-M (default)
1560 1568 | |
1561 1569 | o cac2cead0ff0 C-L (default)
1562 1570 | |
1563 1571 o | be705100c623 C-K (default)
1564 1572 |\|
1565 1573 o | d603e2c0cdd7 C-E (default)
1566 1574 | |
1567 1575 | o 59e76faf78bd C-D (default)
1568 1576 | |
1569 1577 | | o 89420bf00fae C-J (default)
1570 1578 | | |
1571 1579 | | | o b35ed749f288 C-I (my-second-test-branch)
1572 1580 | | |/
1573 1581 | | o 75d69cba5402 C-G (default)
1574 1582 | | |
1575 1583 | | | o 833be552cfe6 C-H (my-first-test-branch)
1576 1584 | | |/
1577 1585 | | o d9e379a8c432 C-F (default)
1578 1586 | | |
1579 1587 +---o 51c544a58128 C-C (default)
1580 1588 | |
1581 1589 | o a9149a1428e2 C-B (default)
1582 1590 | |
1583 1591 o | 98217d5a1659 C-A (default)
1584 1592 |/
1585 1593 o 842e2fac6304 C-ROOT (default)
1586 1594
1587 1595
1588 1596 Creating changesets and markers
1589 1597
1590 1598 (continue existing head)
1591 1599
1592 1600 $ hg -R client-other/ up 'desc("C-Q")'
1593 1601 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1594 1602 $ echo aaa >> client-other/a
1595 1603 $ hg -R client-other/ commit -m "C-U"
1596 1604
1597 1605 (new topo branch obsoleting that same head)
1598 1606
1599 1607 $ hg -R client-racy/ up 'desc("C-Z")'
1600 1608 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1601 1609 $ echo bbb >> client-racy/a
1602 1610 $ hg -R client-racy/ branch --force default
1603 1611 marked working directory as branch default
1604 1612 $ hg -R client-racy/ commit -m "C-V"
1605 1613 created new head
1606 1614 $ ID_Q=`hg -R client-racy log -T '{node}\n' -r 'desc("C-Q")'`
1607 1615 $ ID_V=`hg -R client-racy log -T '{node}\n' -r 'desc("C-V")'`
1608 1616 $ hg -R client-racy debugobsolete $ID_Q $ID_V
1609 1617 1 new obsolescence markers
1610 1618 obsoleted 1 changesets
1611 1619
1612 1620 Pushing
1613 1621
1614 1622 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1615 1623
1616 1624 $ waiton $TESTTMP/readyfile
1617 1625
1618 1626 $ hg -R client-other push -fr 'tip' --new-branch
1619 1627 pushing to ssh://user@dummy/server
1620 1628 searching for changes
1621 1629 remote: adding changesets
1622 1630 remote: adding manifests
1623 1631 remote: adding file changes
1624 1632 remote: added 1 changesets with 0 changes to 0 files
1625 1633
1626 1634 $ release $TESTTMP/watchfile
1627 1635
1628 1636 Check the result of the push
1629 1637
1630 1638 $ cat ./push-log
1631 1639 pushing to ssh://user@dummy/server
1632 1640 searching for changes
1633 1641 wrote ready: $TESTTMP/readyfile
1634 1642 waiting on: $TESTTMP/watchfile
1635 1643 abort: push failed:
1636 1644 'remote repository changed while pushing - please try again'
1637 1645
1638 1646 $ hg -R server debugobsolete
1639 1647 $ hg -R server graph
1640 1648 o a98a47d8b85b C-U (default)
1641 1649 |
1642 1650 o b0ee3d6f51bc C-Q (default)
1643 1651 |
1644 1652 | o 3d57ed3c1091 C-T (other)
1645 1653 | |
1646 1654 | o 2efd43f7b5ba C-S (default)
1647 1655 | |
1648 1656 | | o de7b9e2ba3f6 C-R (other)
1649 1657 | |/
1650 1658 | o 1b58ee3f79e5 C-P (default)
1651 1659 | |
1652 1660 | o d0a85b2252a9 C-O (other)
1653 1661 |/
1654 1662 o 55a6f1c01b48 C-Z (other)
1655 1663 |
1656 1664 o 866a66e18630 C-N (default)
1657 1665 |\
1658 1666 +---o 6fd3090135df C-M (default)
1659 1667 | |
1660 1668 | o cac2cead0ff0 C-L (default)
1661 1669 | |
1662 1670 o | be705100c623 C-K (default)
1663 1671 |\|
1664 1672 o | d603e2c0cdd7 C-E (default)
1665 1673 | |
1666 1674 | o 59e76faf78bd C-D (default)
1667 1675 | |
1668 1676 | | o 89420bf00fae C-J (default)
1669 1677 | | |
1670 1678 | | | o b35ed749f288 C-I (my-second-test-branch)
1671 1679 | | |/
1672 1680 | | o 75d69cba5402 C-G (default)
1673 1681 | | |
1674 1682 | | | o 833be552cfe6 C-H (my-first-test-branch)
1675 1683 | | |/
1676 1684 | | o d9e379a8c432 C-F (default)
1677 1685 | | |
1678 1686 +---o 51c544a58128 C-C (default)
1679 1687 | |
1680 1688 | o a9149a1428e2 C-B (default)
1681 1689 | |
1682 1690 o | 98217d5a1659 C-A (default)
1683 1691 |/
1684 1692 o 842e2fac6304 C-ROOT (default)
1685 1693
1686 1694
1687 1695 racing commit push a new head obsoleting the one touched by the raced push
1688 1696 --------------------------------------------------------------------------
1689 1697
1690 1698 (mirror test case of the previous one
1691 1699
1692 1700 # a (raced branch default)
1693 1701 # |
1694 1702 # ø⇠◔ b (racing)
1695 1703 # |/
1696 1704 # *
1697 1705
1698 1706 (resync-all)
1699 1707
1700 1708 $ hg -R ./server pull ./client-racy
1701 1709 pulling from ./client-racy
1702 1710 searching for changes
1703 1711 adding changesets
1704 1712 adding manifests
1705 1713 adding file changes
1706 1714 added 1 changesets with 1 changes to 1 files (+1 heads)
1707 1715 1 new obsolescence markers
1708 1716 obsoleted 1 changesets
1709 1717 1 new orphan changesets
1710 1718 new changesets 720c5163ecf6 (1 drafts)
1711 1719 (run 'hg heads .' to see heads, 'hg merge' to merge)
1712 1720 $ hg -R ./client-other pull
1713 1721 pulling from ssh://user@dummy/server
1714 1722 searching for changes
1715 1723 adding changesets
1716 1724 adding manifests
1717 1725 adding file changes
1718 1726 added 1 changesets with 1 changes to 1 files (+1 heads)
1719 1727 1 new obsolescence markers
1720 1728 obsoleted 1 changesets
1721 1729 1 new orphan changesets
1722 1730 new changesets 720c5163ecf6 (1 drafts)
1723 1731 (run 'hg heads .' to see heads, 'hg merge' to merge)
1724 1732 $ hg -R ./client-racy pull
1725 1733 pulling from ssh://user@dummy/server
1726 1734 searching for changes
1727 1735 adding changesets
1728 1736 adding manifests
1729 1737 adding file changes
1730 1738 added 1 changesets with 0 changes to 0 files
1731 1739 1 new orphan changesets
1732 1740 new changesets a98a47d8b85b (1 drafts)
1733 1741 (run 'hg update' to get a working copy)
1734 1742
1735 1743 $ hg -R server debugobsolete
1736 1744 b0ee3d6f51bc4c0ca6d4f2907708027a6c376233 720c5163ecf64dcc6216bee2d62bf3edb1882499 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
1737 1745 $ hg -R server graph
1738 1746 o 720c5163ecf6 C-V (default)
1739 1747 |
1740 1748 | * a98a47d8b85b C-U (default)
1741 1749 | |
1742 1750 | x b0ee3d6f51bc C-Q (default)
1743 1751 |/
1744 1752 | o 3d57ed3c1091 C-T (other)
1745 1753 | |
1746 1754 | o 2efd43f7b5ba C-S (default)
1747 1755 | |
1748 1756 | | o de7b9e2ba3f6 C-R (other)
1749 1757 | |/
1750 1758 | o 1b58ee3f79e5 C-P (default)
1751 1759 | |
1752 1760 | o d0a85b2252a9 C-O (other)
1753 1761 |/
1754 1762 o 55a6f1c01b48 C-Z (other)
1755 1763 |
1756 1764 o 866a66e18630 C-N (default)
1757 1765 |\
1758 1766 +---o 6fd3090135df C-M (default)
1759 1767 | |
1760 1768 | o cac2cead0ff0 C-L (default)
1761 1769 | |
1762 1770 o | be705100c623 C-K (default)
1763 1771 |\|
1764 1772 o | d603e2c0cdd7 C-E (default)
1765 1773 | |
1766 1774 | o 59e76faf78bd C-D (default)
1767 1775 | |
1768 1776 | | o 89420bf00fae C-J (default)
1769 1777 | | |
1770 1778 | | | o b35ed749f288 C-I (my-second-test-branch)
1771 1779 | | |/
1772 1780 | | o 75d69cba5402 C-G (default)
1773 1781 | | |
1774 1782 | | | o 833be552cfe6 C-H (my-first-test-branch)
1775 1783 | | |/
1776 1784 | | o d9e379a8c432 C-F (default)
1777 1785 | | |
1778 1786 +---o 51c544a58128 C-C (default)
1779 1787 | |
1780 1788 | o a9149a1428e2 C-B (default)
1781 1789 | |
1782 1790 o | 98217d5a1659 C-A (default)
1783 1791 |/
1784 1792 o 842e2fac6304 C-ROOT (default)
1785 1793
1786 1794
1787 1795 Creating changesets and markers
1788 1796
1789 1797 (new topo branch obsoleting that same head)
1790 1798
1791 1799 $ hg -R client-other/ up 'desc("C-Q")'
1792 1800 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1793 1801 $ echo bbb >> client-other/a
1794 1802 $ hg -R client-other/ branch --force default
1795 1803 marked working directory as branch default
1796 1804 $ hg -R client-other/ commit -m "C-W"
1797 1805 1 new orphan changesets
1798 1806 created new head
1799 1807 $ ID_V=`hg -R client-other log -T '{node}\n' -r 'desc("C-V")'`
1800 1808 $ ID_W=`hg -R client-other log -T '{node}\n' -r 'desc("C-W")'`
1801 1809 $ hg -R client-other debugobsolete $ID_V $ID_W
1802 1810 1 new obsolescence markers
1803 1811 obsoleted 1 changesets
1804 1812
1805 1813 (continue the same head)
1806 1814
1807 1815 $ echo aaa >> client-racy/a
1808 1816 $ hg -R client-racy/ commit -m "C-X"
1809 1817
1810 1818 Pushing
1811 1819
1812 1820 $ hg -R client-racy push -r 'tip' > ./push-log 2>&1 &
1813 1821
1814 1822 $ waiton $TESTTMP/readyfile
1815 1823
1816 1824 $ hg -R client-other push -fr 'tip' --new-branch
1817 1825 pushing to ssh://user@dummy/server
1818 1826 searching for changes
1819 1827 remote: adding changesets
1820 1828 remote: adding manifests
1821 1829 remote: adding file changes
1822 1830 remote: added 1 changesets with 0 changes to 1 files (+1 heads)
1823 1831 remote: 1 new obsolescence markers
1824 1832 remote: obsoleted 1 changesets
1825 1833 remote: 1 new orphan changesets
1826 1834
1827 1835 $ release $TESTTMP/watchfile
1828 1836
1829 1837 Check the result of the push
1830 1838
1831 1839 $ cat ./push-log
1832 1840 pushing to ssh://user@dummy/server
1833 1841 searching for changes
1834 1842 wrote ready: $TESTTMP/readyfile
1835 1843 waiting on: $TESTTMP/watchfile
1836 1844 abort: push failed:
1837 1845 'remote repository changed while pushing - please try again'
1838 1846
1839 1847 $ hg -R server debugobsolete
1840 1848 b0ee3d6f51bc4c0ca6d4f2907708027a6c376233 720c5163ecf64dcc6216bee2d62bf3edb1882499 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
1841 1849 720c5163ecf64dcc6216bee2d62bf3edb1882499 39bc0598afe90ab18da460bafecc0fa953b77596 0 (Thu Jan 01 00:00:00 1970 +0000) {'user': 'test'}
1842 1850 $ hg -R server graph --hidden
1843 1851 * 39bc0598afe9 C-W (default)
1844 1852 |
1845 1853 | * a98a47d8b85b C-U (default)
1846 1854 |/
1847 1855 x b0ee3d6f51bc C-Q (default)
1848 1856 |
1849 1857 | o 3d57ed3c1091 C-T (other)
1850 1858 | |
1851 1859 | o 2efd43f7b5ba C-S (default)
1852 1860 | |
1853 1861 | | o de7b9e2ba3f6 C-R (other)
1854 1862 | |/
1855 1863 | o 1b58ee3f79e5 C-P (default)
1856 1864 | |
1857 1865 | o d0a85b2252a9 C-O (other)
1858 1866 |/
1859 1867 | x 720c5163ecf6 C-V (default)
1860 1868 |/
1861 1869 o 55a6f1c01b48 C-Z (other)
1862 1870 |
1863 1871 o 866a66e18630 C-N (default)
1864 1872 |\
1865 1873 +---o 6fd3090135df C-M (default)
1866 1874 | |
1867 1875 | o cac2cead0ff0 C-L (default)
1868 1876 | |
1869 1877 o | be705100c623 C-K (default)
1870 1878 |\|
1871 1879 o | d603e2c0cdd7 C-E (default)
1872 1880 | |
1873 1881 | o 59e76faf78bd C-D (default)
1874 1882 | |
1875 1883 | | o 89420bf00fae C-J (default)
1876 1884 | | |
1877 1885 | | | o b35ed749f288 C-I (my-second-test-branch)
1878 1886 | | |/
1879 1887 | | o 75d69cba5402 C-G (default)
1880 1888 | | |
1881 1889 | | | o 833be552cfe6 C-H (my-first-test-branch)
1882 1890 | | |/
1883 1891 | | o d9e379a8c432 C-F (default)
1884 1892 | | |
1885 1893 +---o 51c544a58128 C-C (default)
1886 1894 | |
1887 1895 | o a9149a1428e2 C-B (default)
1888 1896 | |
1889 1897 o | 98217d5a1659 C-A (default)
1890 1898 |/
1891 1899 o 842e2fac6304 C-ROOT (default)
1892 1900
General Comments 0
You need to be logged in to leave comments. Login now