##// END OF EJS Templates
merge with stable
Augie Fackler -
r42453:838f3a09 merge default
parent child Browse files
Show More
@@ -1,3384 +1,3384 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import copy as copymod
11 11 import errno
12 12 import os
13 13 import re
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 hex,
18 18 nullid,
19 19 nullrev,
20 20 short,
21 21 )
22 22
23 23 from . import (
24 24 bookmarks,
25 25 changelog,
26 26 copies,
27 27 crecord as crecordmod,
28 28 dirstateguard,
29 29 encoding,
30 30 error,
31 31 formatter,
32 32 logcmdutil,
33 33 match as matchmod,
34 34 merge as mergemod,
35 35 mergeutil,
36 36 obsolete,
37 37 patch,
38 38 pathutil,
39 39 phases,
40 40 pycompat,
41 41 revlog,
42 42 rewriteutil,
43 43 scmutil,
44 44 smartset,
45 45 subrepoutil,
46 46 templatekw,
47 47 templater,
48 48 util,
49 49 vfs as vfsmod,
50 50 )
51 51
52 52 from .utils import (
53 53 dateutil,
54 54 stringutil,
55 55 )
56 56
57 57 stringio = util.stringio
58 58
59 59 # templates of common command options
60 60
61 61 dryrunopts = [
62 62 ('n', 'dry-run', None,
63 63 _('do not perform actions, just print output')),
64 64 ]
65 65
66 66 confirmopts = [
67 67 ('', 'confirm', None,
68 68 _('ask before applying actions')),
69 69 ]
70 70
71 71 remoteopts = [
72 72 ('e', 'ssh', '',
73 73 _('specify ssh command to use'), _('CMD')),
74 74 ('', 'remotecmd', '',
75 75 _('specify hg command to run on the remote side'), _('CMD')),
76 76 ('', 'insecure', None,
77 77 _('do not verify server certificate (ignoring web.cacerts config)')),
78 78 ]
79 79
80 80 walkopts = [
81 81 ('I', 'include', [],
82 82 _('include names matching the given patterns'), _('PATTERN')),
83 83 ('X', 'exclude', [],
84 84 _('exclude names matching the given patterns'), _('PATTERN')),
85 85 ]
86 86
87 87 commitopts = [
88 88 ('m', 'message', '',
89 89 _('use text as commit message'), _('TEXT')),
90 90 ('l', 'logfile', '',
91 91 _('read commit message from file'), _('FILE')),
92 92 ]
93 93
94 94 commitopts2 = [
95 95 ('d', 'date', '',
96 96 _('record the specified date as commit date'), _('DATE')),
97 97 ('u', 'user', '',
98 98 _('record the specified user as committer'), _('USER')),
99 99 ]
100 100
101 101 formatteropts = [
102 102 ('T', 'template', '',
103 103 _('display with template'), _('TEMPLATE')),
104 104 ]
105 105
106 106 templateopts = [
107 107 ('', 'style', '',
108 108 _('display using template map file (DEPRECATED)'), _('STYLE')),
109 109 ('T', 'template', '',
110 110 _('display with template'), _('TEMPLATE')),
111 111 ]
112 112
113 113 logopts = [
114 114 ('p', 'patch', None, _('show patch')),
115 115 ('g', 'git', None, _('use git extended diff format')),
116 116 ('l', 'limit', '',
117 117 _('limit number of changes displayed'), _('NUM')),
118 118 ('M', 'no-merges', None, _('do not show merges')),
119 119 ('', 'stat', None, _('output diffstat-style summary of changes')),
120 120 ('G', 'graph', None, _("show the revision DAG")),
121 121 ] + templateopts
122 122
123 123 diffopts = [
124 124 ('a', 'text', None, _('treat all files as text')),
125 125 ('g', 'git', None, _('use git extended diff format')),
126 126 ('', 'binary', None, _('generate binary diffs in git mode (default)')),
127 127 ('', 'nodates', None, _('omit dates from diff headers'))
128 128 ]
129 129
130 130 diffwsopts = [
131 131 ('w', 'ignore-all-space', None,
132 132 _('ignore white space when comparing lines')),
133 133 ('b', 'ignore-space-change', None,
134 134 _('ignore changes in the amount of white space')),
135 135 ('B', 'ignore-blank-lines', None,
136 136 _('ignore changes whose lines are all blank')),
137 137 ('Z', 'ignore-space-at-eol', None,
138 138 _('ignore changes in whitespace at EOL')),
139 139 ]
140 140
141 141 diffopts2 = [
142 142 ('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')),
143 143 ('p', 'show-function', None, _('show which function each change is in')),
144 144 ('', 'reverse', None, _('produce a diff that undoes the changes')),
145 145 ] + diffwsopts + [
146 146 ('U', 'unified', '',
147 147 _('number of lines of context to show'), _('NUM')),
148 148 ('', 'stat', None, _('output diffstat-style summary of changes')),
149 149 ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')),
150 150 ]
151 151
152 152 mergetoolopts = [
153 153 ('t', 'tool', '', _('specify merge tool'), _('TOOL')),
154 154 ]
155 155
156 156 similarityopts = [
157 157 ('s', 'similarity', '',
158 158 _('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY'))
159 159 ]
160 160
161 161 subrepoopts = [
162 162 ('S', 'subrepos', None,
163 163 _('recurse into subrepositories'))
164 164 ]
165 165
166 166 debugrevlogopts = [
167 167 ('c', 'changelog', False, _('open changelog')),
168 168 ('m', 'manifest', False, _('open manifest')),
169 169 ('', 'dir', '', _('open directory manifest')),
170 170 ]
171 171
172 172 # special string such that everything below this line will be ingored in the
173 173 # editor text
174 174 _linebelow = "^HG: ------------------------ >8 ------------------------$"
175 175
176 176 def ishunk(x):
177 177 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
178 178 return isinstance(x, hunkclasses)
179 179
180 180 def newandmodified(chunks, originalchunks):
181 181 newlyaddedandmodifiedfiles = set()
182 182 for chunk in chunks:
183 183 if (ishunk(chunk) and chunk.header.isnewfile() and chunk not in
184 184 originalchunks):
185 185 newlyaddedandmodifiedfiles.add(chunk.header.filename())
186 186 return newlyaddedandmodifiedfiles
187 187
188 188 def parsealiases(cmd):
189 189 return cmd.split("|")
190 190
191 191 def setupwrapcolorwrite(ui):
192 192 # wrap ui.write so diff output can be labeled/colorized
193 193 def wrapwrite(orig, *args, **kw):
194 194 label = kw.pop(r'label', '')
195 195 for chunk, l in patch.difflabel(lambda: args):
196 196 orig(chunk, label=label + l)
197 197
198 198 oldwrite = ui.write
199 199 def wrap(*args, **kwargs):
200 200 return wrapwrite(oldwrite, *args, **kwargs)
201 201 setattr(ui, 'write', wrap)
202 202 return oldwrite
203 203
204 204 def filterchunks(ui, originalhunks, usecurses, testfile, match,
205 205 operation=None):
206 206 try:
207 207 if usecurses:
208 208 if testfile:
209 209 recordfn = crecordmod.testdecorator(
210 210 testfile, crecordmod.testchunkselector)
211 211 else:
212 212 recordfn = crecordmod.chunkselector
213 213
214 214 return crecordmod.filterpatch(ui, originalhunks, recordfn,
215 215 operation)
216 216 except crecordmod.fallbackerror as e:
217 217 ui.warn('%s\n' % e.message)
218 218 ui.warn(_('falling back to text mode\n'))
219 219
220 220 return patch.filterpatch(ui, originalhunks, match, operation)
221 221
222 222 def recordfilter(ui, originalhunks, match, operation=None):
223 223 """ Prompts the user to filter the originalhunks and return a list of
224 224 selected hunks.
225 225 *operation* is used for to build ui messages to indicate the user what
226 226 kind of filtering they are doing: reverting, committing, shelving, etc.
227 227 (see patch.filterpatch).
228 228 """
229 229 usecurses = crecordmod.checkcurses(ui)
230 230 testfile = ui.config('experimental', 'crecordtest')
231 231 oldwrite = setupwrapcolorwrite(ui)
232 232 try:
233 233 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
234 234 testfile, match, operation)
235 235 finally:
236 236 ui.write = oldwrite
237 237 return newchunks, newopts
238 238
239 239 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
240 240 filterfn, *pats, **opts):
241 241 opts = pycompat.byteskwargs(opts)
242 242 if not ui.interactive():
243 243 if cmdsuggest:
244 244 msg = _('running non-interactively, use %s instead') % cmdsuggest
245 245 else:
246 246 msg = _('running non-interactively')
247 247 raise error.Abort(msg)
248 248
249 249 # make sure username is set before going interactive
250 250 if not opts.get('user'):
251 251 ui.username() # raise exception, username not provided
252 252
253 253 def recordfunc(ui, repo, message, match, opts):
254 254 """This is generic record driver.
255 255
256 256 Its job is to interactively filter local changes, and
257 257 accordingly prepare working directory into a state in which the
258 258 job can be delegated to a non-interactive commit command such as
259 259 'commit' or 'qrefresh'.
260 260
261 261 After the actual job is done by non-interactive command, the
262 262 working directory is restored to its original state.
263 263
264 264 In the end we'll record interesting changes, and everything else
265 265 will be left in place, so the user can continue working.
266 266 """
267 267
268 268 checkunfinished(repo, commit=True)
269 269 wctx = repo[None]
270 270 merge = len(wctx.parents()) > 1
271 271 if merge:
272 272 raise error.Abort(_('cannot partially commit a merge '
273 273 '(use "hg commit" instead)'))
274 274
275 def fail(f, msg):
276 raise error.Abort('%s: %s' % (f, msg))
277
278 force = opts.get('force')
279 if not force:
280 vdirs = []
281 match.explicitdir = vdirs.append
282 match.bad = fail
283
275 284 status = repo.status(match=match)
276 285
277 286 overrides = {(b'ui', b'commitsubrepos'): True}
278 287
279 288 with repo.ui.configoverride(overrides, b'record'):
280 289 # subrepoutil.precommit() modifies the status
281 290 tmpstatus = scmutil.status(copymod.copy(status[0]),
282 291 copymod.copy(status[1]),
283 292 copymod.copy(status[2]),
284 293 copymod.copy(status[3]),
285 294 copymod.copy(status[4]),
286 295 copymod.copy(status[5]),
287 296 copymod.copy(status[6]))
288 297
289 298 # Force allows -X subrepo to skip the subrepo.
290 299 subs, commitsubs, newstate = subrepoutil.precommit(
291 300 repo.ui, wctx, tmpstatus, match, force=True)
292 301 for s in subs:
293 302 if s in commitsubs:
294 303 dirtyreason = wctx.sub(s).dirtyreason(True)
295 304 raise error.Abort(dirtyreason)
296 305
297 def fail(f, msg):
298 raise error.Abort('%s: %s' % (f, msg))
299
300 force = opts.get('force')
301 if not force:
302 vdirs = []
303 match.explicitdir = vdirs.append
304 match.bad = fail
305
306 306 if not force:
307 307 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
308 308 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True,
309 309 section='commands',
310 310 configprefix='commit.interactive.')
311 311 diffopts.nodates = True
312 312 diffopts.git = True
313 313 diffopts.showfunc = True
314 314 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
315 315 originalchunks = patch.parsepatch(originaldiff)
316 316 match = scmutil.match(repo[None], pats)
317 317
318 318 # 1. filter patch, since we are intending to apply subset of it
319 319 try:
320 320 chunks, newopts = filterfn(ui, originalchunks, match)
321 321 except error.PatchError as err:
322 322 raise error.Abort(_('error parsing patch: %s') % err)
323 323 opts.update(newopts)
324 324
325 325 # We need to keep a backup of files that have been newly added and
326 326 # modified during the recording process because there is a previous
327 327 # version without the edit in the workdir
328 328 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
329 329 contenders = set()
330 330 for h in chunks:
331 331 try:
332 332 contenders.update(set(h.files()))
333 333 except AttributeError:
334 334 pass
335 335
336 336 changed = status.modified + status.added + status.removed
337 337 newfiles = [f for f in changed if f in contenders]
338 338 if not newfiles:
339 339 ui.status(_('no changes to record\n'))
340 340 return 0
341 341
342 342 modified = set(status.modified)
343 343
344 344 # 2. backup changed files, so we can restore them in the end
345 345
346 346 if backupall:
347 347 tobackup = changed
348 348 else:
349 349 tobackup = [f for f in newfiles if f in modified or f in
350 350 newlyaddedandmodifiedfiles]
351 351 backups = {}
352 352 if tobackup:
353 353 backupdir = repo.vfs.join('record-backups')
354 354 try:
355 355 os.mkdir(backupdir)
356 356 except OSError as err:
357 357 if err.errno != errno.EEXIST:
358 358 raise
359 359 try:
360 360 # backup continues
361 361 for f in tobackup:
362 362 fd, tmpname = pycompat.mkstemp(prefix=f.replace('/', '_') + '.',
363 363 dir=backupdir)
364 364 os.close(fd)
365 365 ui.debug('backup %r as %r\n' % (f, tmpname))
366 366 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
367 367 backups[f] = tmpname
368 368
369 369 fp = stringio()
370 370 for c in chunks:
371 371 fname = c.filename()
372 372 if fname in backups:
373 373 c.write(fp)
374 374 dopatch = fp.tell()
375 375 fp.seek(0)
376 376
377 377 # 2.5 optionally review / modify patch in text editor
378 378 if opts.get('review', False):
379 379 patchtext = (crecordmod.diffhelptext
380 380 + crecordmod.patchhelptext
381 381 + fp.read())
382 382 reviewedpatch = ui.edit(patchtext, "",
383 383 action="diff",
384 384 repopath=repo.path)
385 385 fp.truncate(0)
386 386 fp.write(reviewedpatch)
387 387 fp.seek(0)
388 388
389 389 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
390 390 # 3a. apply filtered patch to clean repo (clean)
391 391 if backups:
392 392 # Equivalent to hg.revert
393 393 m = scmutil.matchfiles(repo, backups.keys())
394 394 mergemod.update(repo, repo.dirstate.p1(), branchmerge=False,
395 395 force=True, matcher=m)
396 396
397 397 # 3b. (apply)
398 398 if dopatch:
399 399 try:
400 400 ui.debug('applying patch\n')
401 401 ui.debug(fp.getvalue())
402 402 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
403 403 except error.PatchError as err:
404 404 raise error.Abort(pycompat.bytestr(err))
405 405 del fp
406 406
407 407 # 4. We prepared working directory according to filtered
408 408 # patch. Now is the time to delegate the job to
409 409 # commit/qrefresh or the like!
410 410
411 411 # Make all of the pathnames absolute.
412 412 newfiles = [repo.wjoin(nf) for nf in newfiles]
413 413 return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
414 414 finally:
415 415 # 5. finally restore backed-up files
416 416 try:
417 417 dirstate = repo.dirstate
418 418 for realname, tmpname in backups.iteritems():
419 419 ui.debug('restoring %r to %r\n' % (tmpname, realname))
420 420
421 421 if dirstate[realname] == 'n':
422 422 # without normallookup, restoring timestamp
423 423 # may cause partially committed files
424 424 # to be treated as unmodified
425 425 dirstate.normallookup(realname)
426 426
427 427 # copystat=True here and above are a hack to trick any
428 428 # editors that have f open that we haven't modified them.
429 429 #
430 430 # Also note that this racy as an editor could notice the
431 431 # file's mtime before we've finished writing it.
432 432 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
433 433 os.unlink(tmpname)
434 434 if tobackup:
435 435 os.rmdir(backupdir)
436 436 except OSError:
437 437 pass
438 438
439 439 def recordinwlock(ui, repo, message, match, opts):
440 440 with repo.wlock():
441 441 return recordfunc(ui, repo, message, match, opts)
442 442
443 443 return commit(ui, repo, recordinwlock, pats, opts)
444 444
445 445 class dirnode(object):
446 446 """
447 447 Represent a directory in user working copy with information required for
448 448 the purpose of tersing its status.
449 449
450 450 path is the path to the directory, without a trailing '/'
451 451
452 452 statuses is a set of statuses of all files in this directory (this includes
453 453 all the files in all the subdirectories too)
454 454
455 455 files is a list of files which are direct child of this directory
456 456
457 457 subdirs is a dictionary of sub-directory name as the key and it's own
458 458 dirnode object as the value
459 459 """
460 460
461 461 def __init__(self, dirpath):
462 462 self.path = dirpath
463 463 self.statuses = set()
464 464 self.files = []
465 465 self.subdirs = {}
466 466
467 467 def _addfileindir(self, filename, status):
468 468 """Add a file in this directory as a direct child."""
469 469 self.files.append((filename, status))
470 470
471 471 def addfile(self, filename, status):
472 472 """
473 473 Add a file to this directory or to its direct parent directory.
474 474
475 475 If the file is not direct child of this directory, we traverse to the
476 476 directory of which this file is a direct child of and add the file
477 477 there.
478 478 """
479 479
480 480 # the filename contains a path separator, it means it's not the direct
481 481 # child of this directory
482 482 if '/' in filename:
483 483 subdir, filep = filename.split('/', 1)
484 484
485 485 # does the dirnode object for subdir exists
486 486 if subdir not in self.subdirs:
487 487 subdirpath = pathutil.join(self.path, subdir)
488 488 self.subdirs[subdir] = dirnode(subdirpath)
489 489
490 490 # try adding the file in subdir
491 491 self.subdirs[subdir].addfile(filep, status)
492 492
493 493 else:
494 494 self._addfileindir(filename, status)
495 495
496 496 if status not in self.statuses:
497 497 self.statuses.add(status)
498 498
499 499 def iterfilepaths(self):
500 500 """Yield (status, path) for files directly under this directory."""
501 501 for f, st in self.files:
502 502 yield st, pathutil.join(self.path, f)
503 503
504 504 def tersewalk(self, terseargs):
505 505 """
506 506 Yield (status, path) obtained by processing the status of this
507 507 dirnode.
508 508
509 509 terseargs is the string of arguments passed by the user with `--terse`
510 510 flag.
511 511
512 512 Following are the cases which can happen:
513 513
514 514 1) All the files in the directory (including all the files in its
515 515 subdirectories) share the same status and the user has asked us to terse
516 516 that status. -> yield (status, dirpath). dirpath will end in '/'.
517 517
518 518 2) Otherwise, we do following:
519 519
520 520 a) Yield (status, filepath) for all the files which are in this
521 521 directory (only the ones in this directory, not the subdirs)
522 522
523 523 b) Recurse the function on all the subdirectories of this
524 524 directory
525 525 """
526 526
527 527 if len(self.statuses) == 1:
528 528 onlyst = self.statuses.pop()
529 529
530 530 # Making sure we terse only when the status abbreviation is
531 531 # passed as terse argument
532 532 if onlyst in terseargs:
533 533 yield onlyst, self.path + '/'
534 534 return
535 535
536 536 # add the files to status list
537 537 for st, fpath in self.iterfilepaths():
538 538 yield st, fpath
539 539
540 540 #recurse on the subdirs
541 541 for dirobj in self.subdirs.values():
542 542 for st, fpath in dirobj.tersewalk(terseargs):
543 543 yield st, fpath
544 544
545 545 def tersedir(statuslist, terseargs):
546 546 """
547 547 Terse the status if all the files in a directory shares the same status.
548 548
549 549 statuslist is scmutil.status() object which contains a list of files for
550 550 each status.
551 551 terseargs is string which is passed by the user as the argument to `--terse`
552 552 flag.
553 553
554 554 The function makes a tree of objects of dirnode class, and at each node it
555 555 stores the information required to know whether we can terse a certain
556 556 directory or not.
557 557 """
558 558 # the order matters here as that is used to produce final list
559 559 allst = ('m', 'a', 'r', 'd', 'u', 'i', 'c')
560 560
561 561 # checking the argument validity
562 562 for s in pycompat.bytestr(terseargs):
563 563 if s not in allst:
564 564 raise error.Abort(_("'%s' not recognized") % s)
565 565
566 566 # creating a dirnode object for the root of the repo
567 567 rootobj = dirnode('')
568 568 pstatus = ('modified', 'added', 'deleted', 'clean', 'unknown',
569 569 'ignored', 'removed')
570 570
571 571 tersedict = {}
572 572 for attrname in pstatus:
573 573 statuschar = attrname[0:1]
574 574 for f in getattr(statuslist, attrname):
575 575 rootobj.addfile(f, statuschar)
576 576 tersedict[statuschar] = []
577 577
578 578 # we won't be tersing the root dir, so add files in it
579 579 for st, fpath in rootobj.iterfilepaths():
580 580 tersedict[st].append(fpath)
581 581
582 582 # process each sub-directory and build tersedict
583 583 for subdir in rootobj.subdirs.values():
584 584 for st, f in subdir.tersewalk(terseargs):
585 585 tersedict[st].append(f)
586 586
587 587 tersedlist = []
588 588 for st in allst:
589 589 tersedict[st].sort()
590 590 tersedlist.append(tersedict[st])
591 591
592 592 return tersedlist
593 593
594 594 def _commentlines(raw):
595 595 '''Surround lineswith a comment char and a new line'''
596 596 lines = raw.splitlines()
597 597 commentedlines = ['# %s' % line for line in lines]
598 598 return '\n'.join(commentedlines) + '\n'
599 599
600 600 def _conflictsmsg(repo):
601 601 mergestate = mergemod.mergestate.read(repo)
602 602 if not mergestate.active():
603 603 return
604 604
605 605 m = scmutil.match(repo[None])
606 606 unresolvedlist = [f for f in mergestate.unresolved() if m(f)]
607 607 if unresolvedlist:
608 608 mergeliststr = '\n'.join(
609 609 [' %s' % util.pathto(repo.root, encoding.getcwd(), path)
610 610 for path in sorted(unresolvedlist)])
611 611 msg = _('''Unresolved merge conflicts:
612 612
613 613 %s
614 614
615 615 To mark files as resolved: hg resolve --mark FILE''') % mergeliststr
616 616 else:
617 617 msg = _('No unresolved merge conflicts.')
618 618
619 619 return _commentlines(msg)
620 620
621 621 def _helpmessage(continuecmd, abortcmd):
622 622 msg = _('To continue: %s\n'
623 623 'To abort: %s') % (continuecmd, abortcmd)
624 624 return _commentlines(msg)
625 625
626 626 def _rebasemsg():
627 627 return _helpmessage('hg rebase --continue', 'hg rebase --abort')
628 628
629 629 def _histeditmsg():
630 630 return _helpmessage('hg histedit --continue', 'hg histedit --abort')
631 631
632 632 def _unshelvemsg():
633 633 return _helpmessage('hg unshelve --continue', 'hg unshelve --abort')
634 634
635 635 def _graftmsg():
636 636 return _helpmessage('hg graft --continue', 'hg graft --abort')
637 637
638 638 def _mergemsg():
639 639 return _helpmessage('hg commit', 'hg merge --abort')
640 640
641 641 def _bisectmsg():
642 642 msg = _('To mark the changeset good: hg bisect --good\n'
643 643 'To mark the changeset bad: hg bisect --bad\n'
644 644 'To abort: hg bisect --reset\n')
645 645 return _commentlines(msg)
646 646
647 647 def fileexistspredicate(filename):
648 648 return lambda repo: repo.vfs.exists(filename)
649 649
650 650 def _mergepredicate(repo):
651 651 return len(repo[None].parents()) > 1
652 652
653 653 STATES = (
654 654 # (state, predicate to detect states, helpful message function)
655 655 ('histedit', fileexistspredicate('histedit-state'), _histeditmsg),
656 656 ('bisect', fileexistspredicate('bisect.state'), _bisectmsg),
657 657 ('graft', fileexistspredicate('graftstate'), _graftmsg),
658 658 ('unshelve', fileexistspredicate('shelvedstate'), _unshelvemsg),
659 659 ('rebase', fileexistspredicate('rebasestate'), _rebasemsg),
660 660 # The merge state is part of a list that will be iterated over.
661 661 # They need to be last because some of the other unfinished states may also
662 662 # be in a merge or update state (eg. rebase, histedit, graft, etc).
663 663 # We want those to have priority.
664 664 ('merge', _mergepredicate, _mergemsg),
665 665 )
666 666
667 667 def _getrepostate(repo):
668 668 # experimental config: commands.status.skipstates
669 669 skip = set(repo.ui.configlist('commands', 'status.skipstates'))
670 670 for state, statedetectionpredicate, msgfn in STATES:
671 671 if state in skip:
672 672 continue
673 673 if statedetectionpredicate(repo):
674 674 return (state, statedetectionpredicate, msgfn)
675 675
676 676 def morestatus(repo, fm):
677 677 statetuple = _getrepostate(repo)
678 678 label = 'status.morestatus'
679 679 if statetuple:
680 680 state, statedetectionpredicate, helpfulmsg = statetuple
681 681 statemsg = _('The repository is in an unfinished *%s* state.') % state
682 682 fm.plain('%s\n' % _commentlines(statemsg), label=label)
683 683 conmsg = _conflictsmsg(repo)
684 684 if conmsg:
685 685 fm.plain('%s\n' % conmsg, label=label)
686 686 if helpfulmsg:
687 687 helpmsg = helpfulmsg()
688 688 fm.plain('%s\n' % helpmsg, label=label)
689 689
690 690 def findpossible(cmd, table, strict=False):
691 691 """
692 692 Return cmd -> (aliases, command table entry)
693 693 for each matching command.
694 694 Return debug commands (or their aliases) only if no normal command matches.
695 695 """
696 696 choice = {}
697 697 debugchoice = {}
698 698
699 699 if cmd in table:
700 700 # short-circuit exact matches, "log" alias beats "log|history"
701 701 keys = [cmd]
702 702 else:
703 703 keys = table.keys()
704 704
705 705 allcmds = []
706 706 for e in keys:
707 707 aliases = parsealiases(e)
708 708 allcmds.extend(aliases)
709 709 found = None
710 710 if cmd in aliases:
711 711 found = cmd
712 712 elif not strict:
713 713 for a in aliases:
714 714 if a.startswith(cmd):
715 715 found = a
716 716 break
717 717 if found is not None:
718 718 if aliases[0].startswith("debug") or found.startswith("debug"):
719 719 debugchoice[found] = (aliases, table[e])
720 720 else:
721 721 choice[found] = (aliases, table[e])
722 722
723 723 if not choice and debugchoice:
724 724 choice = debugchoice
725 725
726 726 return choice, allcmds
727 727
728 728 def findcmd(cmd, table, strict=True):
729 729 """Return (aliases, command table entry) for command string."""
730 730 choice, allcmds = findpossible(cmd, table, strict)
731 731
732 732 if cmd in choice:
733 733 return choice[cmd]
734 734
735 735 if len(choice) > 1:
736 736 clist = sorted(choice)
737 737 raise error.AmbiguousCommand(cmd, clist)
738 738
739 739 if choice:
740 740 return list(choice.values())[0]
741 741
742 742 raise error.UnknownCommand(cmd, allcmds)
743 743
744 744 def changebranch(ui, repo, revs, label):
745 745 """ Change the branch name of given revs to label """
746 746
747 747 with repo.wlock(), repo.lock(), repo.transaction('branches'):
748 748 # abort in case of uncommitted merge or dirty wdir
749 749 bailifchanged(repo)
750 750 revs = scmutil.revrange(repo, revs)
751 751 if not revs:
752 752 raise error.Abort("empty revision set")
753 753 roots = repo.revs('roots(%ld)', revs)
754 754 if len(roots) > 1:
755 755 raise error.Abort(_("cannot change branch of non-linear revisions"))
756 756 rewriteutil.precheck(repo, revs, 'change branch of')
757 757
758 758 root = repo[roots.first()]
759 759 rpb = {parent.branch() for parent in root.parents()}
760 760 if label not in rpb and label in repo.branchmap():
761 761 raise error.Abort(_("a branch of the same name already exists"))
762 762
763 763 if repo.revs('obsolete() and %ld', revs):
764 764 raise error.Abort(_("cannot change branch of a obsolete changeset"))
765 765
766 766 # make sure only topological heads
767 767 if repo.revs('heads(%ld) - head()', revs):
768 768 raise error.Abort(_("cannot change branch in middle of a stack"))
769 769
770 770 replacements = {}
771 771 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
772 772 # mercurial.subrepo -> mercurial.cmdutil
773 773 from . import context
774 774 for rev in revs:
775 775 ctx = repo[rev]
776 776 oldbranch = ctx.branch()
777 777 # check if ctx has same branch
778 778 if oldbranch == label:
779 779 continue
780 780
781 781 def filectxfn(repo, newctx, path):
782 782 try:
783 783 return ctx[path]
784 784 except error.ManifestLookupError:
785 785 return None
786 786
787 787 ui.debug("changing branch of '%s' from '%s' to '%s'\n"
788 788 % (hex(ctx.node()), oldbranch, label))
789 789 extra = ctx.extra()
790 790 extra['branch_change'] = hex(ctx.node())
791 791 # While changing branch of set of linear commits, make sure that
792 792 # we base our commits on new parent rather than old parent which
793 793 # was obsoleted while changing the branch
794 794 p1 = ctx.p1().node()
795 795 p2 = ctx.p2().node()
796 796 if p1 in replacements:
797 797 p1 = replacements[p1][0]
798 798 if p2 in replacements:
799 799 p2 = replacements[p2][0]
800 800
801 801 mc = context.memctx(repo, (p1, p2),
802 802 ctx.description(),
803 803 ctx.files(),
804 804 filectxfn,
805 805 user=ctx.user(),
806 806 date=ctx.date(),
807 807 extra=extra,
808 808 branch=label)
809 809
810 810 newnode = repo.commitctx(mc)
811 811 replacements[ctx.node()] = (newnode,)
812 812 ui.debug('new node id is %s\n' % hex(newnode))
813 813
814 814 # create obsmarkers and move bookmarks
815 815 scmutil.cleanupnodes(repo, replacements, 'branch-change', fixphase=True)
816 816
817 817 # move the working copy too
818 818 wctx = repo[None]
819 819 # in-progress merge is a bit too complex for now.
820 820 if len(wctx.parents()) == 1:
821 821 newid = replacements.get(wctx.p1().node())
822 822 if newid is not None:
823 823 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
824 824 # mercurial.cmdutil
825 825 from . import hg
826 826 hg.update(repo, newid[0], quietempty=True)
827 827
828 828 ui.status(_("changed branch on %d changesets\n") % len(replacements))
829 829
830 830 def findrepo(p):
831 831 while not os.path.isdir(os.path.join(p, ".hg")):
832 832 oldp, p = p, os.path.dirname(p)
833 833 if p == oldp:
834 834 return None
835 835
836 836 return p
837 837
838 838 def bailifchanged(repo, merge=True, hint=None):
839 839 """ enforce the precondition that working directory must be clean.
840 840
841 841 'merge' can be set to false if a pending uncommitted merge should be
842 842 ignored (such as when 'update --check' runs).
843 843
844 844 'hint' is the usual hint given to Abort exception.
845 845 """
846 846
847 847 if merge and repo.dirstate.p2() != nullid:
848 848 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
849 849 modified, added, removed, deleted = repo.status()[:4]
850 850 if modified or added or removed or deleted:
851 851 raise error.Abort(_('uncommitted changes'), hint=hint)
852 852 ctx = repo[None]
853 853 for s in sorted(ctx.substate):
854 854 ctx.sub(s).bailifchanged(hint=hint)
855 855
856 856 def logmessage(ui, opts):
857 857 """ get the log message according to -m and -l option """
858 858 message = opts.get('message')
859 859 logfile = opts.get('logfile')
860 860
861 861 if message and logfile:
862 862 raise error.Abort(_('options --message and --logfile are mutually '
863 863 'exclusive'))
864 864 if not message and logfile:
865 865 try:
866 866 if isstdiofilename(logfile):
867 867 message = ui.fin.read()
868 868 else:
869 869 message = '\n'.join(util.readfile(logfile).splitlines())
870 870 except IOError as inst:
871 871 raise error.Abort(_("can't read commit message '%s': %s") %
872 872 (logfile, encoding.strtolocal(inst.strerror)))
873 873 return message
874 874
875 875 def mergeeditform(ctxorbool, baseformname):
876 876 """return appropriate editform name (referencing a committemplate)
877 877
878 878 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
879 879 merging is committed.
880 880
881 881 This returns baseformname with '.merge' appended if it is a merge,
882 882 otherwise '.normal' is appended.
883 883 """
884 884 if isinstance(ctxorbool, bool):
885 885 if ctxorbool:
886 886 return baseformname + ".merge"
887 887 elif len(ctxorbool.parents()) > 1:
888 888 return baseformname + ".merge"
889 889
890 890 return baseformname + ".normal"
891 891
892 892 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
893 893 editform='', **opts):
894 894 """get appropriate commit message editor according to '--edit' option
895 895
896 896 'finishdesc' is a function to be called with edited commit message
897 897 (= 'description' of the new changeset) just after editing, but
898 898 before checking empty-ness. It should return actual text to be
899 899 stored into history. This allows to change description before
900 900 storing.
901 901
902 902 'extramsg' is a extra message to be shown in the editor instead of
903 903 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
904 904 is automatically added.
905 905
906 906 'editform' is a dot-separated list of names, to distinguish
907 907 the purpose of commit text editing.
908 908
909 909 'getcommiteditor' returns 'commitforceeditor' regardless of
910 910 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
911 911 they are specific for usage in MQ.
912 912 """
913 913 if edit or finishdesc or extramsg:
914 914 return lambda r, c, s: commitforceeditor(r, c, s,
915 915 finishdesc=finishdesc,
916 916 extramsg=extramsg,
917 917 editform=editform)
918 918 elif editform:
919 919 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
920 920 else:
921 921 return commiteditor
922 922
923 923 def _escapecommandtemplate(tmpl):
924 924 parts = []
925 925 for typ, start, end in templater.scantemplate(tmpl, raw=True):
926 926 if typ == b'string':
927 927 parts.append(stringutil.escapestr(tmpl[start:end]))
928 928 else:
929 929 parts.append(tmpl[start:end])
930 930 return b''.join(parts)
931 931
932 932 def rendercommandtemplate(ui, tmpl, props):
933 933 r"""Expand a literal template 'tmpl' in a way suitable for command line
934 934
935 935 '\' in outermost string is not taken as an escape character because it
936 936 is a directory separator on Windows.
937 937
938 938 >>> from . import ui as uimod
939 939 >>> ui = uimod.ui()
940 940 >>> rendercommandtemplate(ui, b'c:\\{path}', {b'path': b'foo'})
941 941 'c:\\foo'
942 942 >>> rendercommandtemplate(ui, b'{"c:\\{path}"}', {'path': b'foo'})
943 943 'c:{path}'
944 944 """
945 945 if not tmpl:
946 946 return tmpl
947 947 t = formatter.maketemplater(ui, _escapecommandtemplate(tmpl))
948 948 return t.renderdefault(props)
949 949
950 950 def rendertemplate(ctx, tmpl, props=None):
951 951 """Expand a literal template 'tmpl' byte-string against one changeset
952 952
953 953 Each props item must be a stringify-able value or a callable returning
954 954 such value, i.e. no bare list nor dict should be passed.
955 955 """
956 956 repo = ctx.repo()
957 957 tres = formatter.templateresources(repo.ui, repo)
958 958 t = formatter.maketemplater(repo.ui, tmpl, defaults=templatekw.keywords,
959 959 resources=tres)
960 960 mapping = {'ctx': ctx}
961 961 if props:
962 962 mapping.update(props)
963 963 return t.renderdefault(mapping)
964 964
965 965 def _buildfntemplate(pat, total=None, seqno=None, revwidth=None, pathname=None):
966 966 r"""Convert old-style filename format string to template string
967 967
968 968 >>> _buildfntemplate(b'foo-%b-%n.patch', seqno=0)
969 969 'foo-{reporoot|basename}-{seqno}.patch'
970 970 >>> _buildfntemplate(b'%R{tags % "{tag}"}%H')
971 971 '{rev}{tags % "{tag}"}{node}'
972 972
973 973 '\' in outermost strings has to be escaped because it is a directory
974 974 separator on Windows:
975 975
976 976 >>> _buildfntemplate(b'c:\\tmp\\%R\\%n.patch', seqno=0)
977 977 'c:\\\\tmp\\\\{rev}\\\\{seqno}.patch'
978 978 >>> _buildfntemplate(b'\\\\foo\\bar.patch')
979 979 '\\\\\\\\foo\\\\bar.patch'
980 980 >>> _buildfntemplate(b'\\{tags % "{tag}"}')
981 981 '\\\\{tags % "{tag}"}'
982 982
983 983 but inner strings follow the template rules (i.e. '\' is taken as an
984 984 escape character):
985 985
986 986 >>> _buildfntemplate(br'{"c:\tmp"}', seqno=0)
987 987 '{"c:\\tmp"}'
988 988 """
989 989 expander = {
990 990 b'H': b'{node}',
991 991 b'R': b'{rev}',
992 992 b'h': b'{node|short}',
993 993 b'm': br'{sub(r"[^\w]", "_", desc|firstline)}',
994 994 b'r': b'{if(revwidth, pad(rev, revwidth, "0", left=True), rev)}',
995 995 b'%': b'%',
996 996 b'b': b'{reporoot|basename}',
997 997 }
998 998 if total is not None:
999 999 expander[b'N'] = b'{total}'
1000 1000 if seqno is not None:
1001 1001 expander[b'n'] = b'{seqno}'
1002 1002 if total is not None and seqno is not None:
1003 1003 expander[b'n'] = b'{pad(seqno, total|stringify|count, "0", left=True)}'
1004 1004 if pathname is not None:
1005 1005 expander[b's'] = b'{pathname|basename}'
1006 1006 expander[b'd'] = b'{if(pathname|dirname, pathname|dirname, ".")}'
1007 1007 expander[b'p'] = b'{pathname}'
1008 1008
1009 1009 newname = []
1010 1010 for typ, start, end in templater.scantemplate(pat, raw=True):
1011 1011 if typ != b'string':
1012 1012 newname.append(pat[start:end])
1013 1013 continue
1014 1014 i = start
1015 1015 while i < end:
1016 1016 n = pat.find(b'%', i, end)
1017 1017 if n < 0:
1018 1018 newname.append(stringutil.escapestr(pat[i:end]))
1019 1019 break
1020 1020 newname.append(stringutil.escapestr(pat[i:n]))
1021 1021 if n + 2 > end:
1022 1022 raise error.Abort(_("incomplete format spec in output "
1023 1023 "filename"))
1024 1024 c = pat[n + 1:n + 2]
1025 1025 i = n + 2
1026 1026 try:
1027 1027 newname.append(expander[c])
1028 1028 except KeyError:
1029 1029 raise error.Abort(_("invalid format spec '%%%s' in output "
1030 1030 "filename") % c)
1031 1031 return ''.join(newname)
1032 1032
1033 1033 def makefilename(ctx, pat, **props):
1034 1034 if not pat:
1035 1035 return pat
1036 1036 tmpl = _buildfntemplate(pat, **props)
1037 1037 # BUG: alias expansion shouldn't be made against template fragments
1038 1038 # rewritten from %-format strings, but we have no easy way to partially
1039 1039 # disable the expansion.
1040 1040 return rendertemplate(ctx, tmpl, pycompat.byteskwargs(props))
1041 1041
1042 1042 def isstdiofilename(pat):
1043 1043 """True if the given pat looks like a filename denoting stdin/stdout"""
1044 1044 return not pat or pat == '-'
1045 1045
1046 1046 class _unclosablefile(object):
1047 1047 def __init__(self, fp):
1048 1048 self._fp = fp
1049 1049
1050 1050 def close(self):
1051 1051 pass
1052 1052
1053 1053 def __iter__(self):
1054 1054 return iter(self._fp)
1055 1055
1056 1056 def __getattr__(self, attr):
1057 1057 return getattr(self._fp, attr)
1058 1058
1059 1059 def __enter__(self):
1060 1060 return self
1061 1061
1062 1062 def __exit__(self, exc_type, exc_value, exc_tb):
1063 1063 pass
1064 1064
1065 1065 def makefileobj(ctx, pat, mode='wb', **props):
1066 1066 writable = mode not in ('r', 'rb')
1067 1067
1068 1068 if isstdiofilename(pat):
1069 1069 repo = ctx.repo()
1070 1070 if writable:
1071 1071 fp = repo.ui.fout
1072 1072 else:
1073 1073 fp = repo.ui.fin
1074 1074 return _unclosablefile(fp)
1075 1075 fn = makefilename(ctx, pat, **props)
1076 1076 return open(fn, mode)
1077 1077
1078 1078 def openstorage(repo, cmd, file_, opts, returnrevlog=False):
1079 1079 """opens the changelog, manifest, a filelog or a given revlog"""
1080 1080 cl = opts['changelog']
1081 1081 mf = opts['manifest']
1082 1082 dir = opts['dir']
1083 1083 msg = None
1084 1084 if cl and mf:
1085 1085 msg = _('cannot specify --changelog and --manifest at the same time')
1086 1086 elif cl and dir:
1087 1087 msg = _('cannot specify --changelog and --dir at the same time')
1088 1088 elif cl or mf or dir:
1089 1089 if file_:
1090 1090 msg = _('cannot specify filename with --changelog or --manifest')
1091 1091 elif not repo:
1092 1092 msg = _('cannot specify --changelog or --manifest or --dir '
1093 1093 'without a repository')
1094 1094 if msg:
1095 1095 raise error.Abort(msg)
1096 1096
1097 1097 r = None
1098 1098 if repo:
1099 1099 if cl:
1100 1100 r = repo.unfiltered().changelog
1101 1101 elif dir:
1102 1102 if 'treemanifest' not in repo.requirements:
1103 1103 raise error.Abort(_("--dir can only be used on repos with "
1104 1104 "treemanifest enabled"))
1105 1105 if not dir.endswith('/'):
1106 1106 dir = dir + '/'
1107 1107 dirlog = repo.manifestlog.getstorage(dir)
1108 1108 if len(dirlog):
1109 1109 r = dirlog
1110 1110 elif mf:
1111 1111 r = repo.manifestlog.getstorage(b'')
1112 1112 elif file_:
1113 1113 filelog = repo.file(file_)
1114 1114 if len(filelog):
1115 1115 r = filelog
1116 1116
1117 1117 # Not all storage may be revlogs. If requested, try to return an actual
1118 1118 # revlog instance.
1119 1119 if returnrevlog:
1120 1120 if isinstance(r, revlog.revlog):
1121 1121 pass
1122 1122 elif util.safehasattr(r, '_revlog'):
1123 1123 r = r._revlog
1124 1124 elif r is not None:
1125 1125 raise error.Abort(_('%r does not appear to be a revlog') % r)
1126 1126
1127 1127 if not r:
1128 1128 if not returnrevlog:
1129 1129 raise error.Abort(_('cannot give path to non-revlog'))
1130 1130
1131 1131 if not file_:
1132 1132 raise error.CommandError(cmd, _('invalid arguments'))
1133 1133 if not os.path.isfile(file_):
1134 1134 raise error.Abort(_("revlog '%s' not found") % file_)
1135 1135 r = revlog.revlog(vfsmod.vfs(encoding.getcwd(), audit=False),
1136 1136 file_[:-2] + ".i")
1137 1137 return r
1138 1138
1139 1139 def openrevlog(repo, cmd, file_, opts):
1140 1140 """Obtain a revlog backing storage of an item.
1141 1141
1142 1142 This is similar to ``openstorage()`` except it always returns a revlog.
1143 1143
1144 1144 In most cases, a caller cares about the main storage object - not the
1145 1145 revlog backing it. Therefore, this function should only be used by code
1146 1146 that needs to examine low-level revlog implementation details. e.g. debug
1147 1147 commands.
1148 1148 """
1149 1149 return openstorage(repo, cmd, file_, opts, returnrevlog=True)
1150 1150
1151 1151 def copy(ui, repo, pats, opts, rename=False):
1152 1152 # called with the repo lock held
1153 1153 #
1154 1154 # hgsep => pathname that uses "/" to separate directories
1155 1155 # ossep => pathname that uses os.sep to separate directories
1156 1156 cwd = repo.getcwd()
1157 1157 targets = {}
1158 1158 after = opts.get("after")
1159 1159 dryrun = opts.get("dry_run")
1160 1160 wctx = repo[None]
1161 1161
1162 1162 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
1163 1163 def walkpat(pat):
1164 1164 srcs = []
1165 1165 if after:
1166 1166 badstates = '?'
1167 1167 else:
1168 1168 badstates = '?r'
1169 1169 m = scmutil.match(wctx, [pat], opts, globbed=True)
1170 1170 for abs in wctx.walk(m):
1171 1171 state = repo.dirstate[abs]
1172 1172 rel = uipathfn(abs)
1173 1173 exact = m.exact(abs)
1174 1174 if state in badstates:
1175 1175 if exact and state == '?':
1176 1176 ui.warn(_('%s: not copying - file is not managed\n') % rel)
1177 1177 if exact and state == 'r':
1178 1178 ui.warn(_('%s: not copying - file has been marked for'
1179 1179 ' remove\n') % rel)
1180 1180 continue
1181 1181 # abs: hgsep
1182 1182 # rel: ossep
1183 1183 srcs.append((abs, rel, exact))
1184 1184 return srcs
1185 1185
1186 1186 # abssrc: hgsep
1187 1187 # relsrc: ossep
1188 1188 # otarget: ossep
1189 1189 def copyfile(abssrc, relsrc, otarget, exact):
1190 1190 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1191 1191 if '/' in abstarget:
1192 1192 # We cannot normalize abstarget itself, this would prevent
1193 1193 # case only renames, like a => A.
1194 1194 abspath, absname = abstarget.rsplit('/', 1)
1195 1195 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
1196 1196 reltarget = repo.pathto(abstarget, cwd)
1197 1197 target = repo.wjoin(abstarget)
1198 1198 src = repo.wjoin(abssrc)
1199 1199 state = repo.dirstate[abstarget]
1200 1200
1201 1201 scmutil.checkportable(ui, abstarget)
1202 1202
1203 1203 # check for collisions
1204 1204 prevsrc = targets.get(abstarget)
1205 1205 if prevsrc is not None:
1206 1206 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
1207 1207 (reltarget, repo.pathto(abssrc, cwd),
1208 1208 repo.pathto(prevsrc, cwd)))
1209 1209 return True # report a failure
1210 1210
1211 1211 # check for overwrites
1212 1212 exists = os.path.lexists(target)
1213 1213 samefile = False
1214 1214 if exists and abssrc != abstarget:
1215 1215 if (repo.dirstate.normalize(abssrc) ==
1216 1216 repo.dirstate.normalize(abstarget)):
1217 1217 if not rename:
1218 1218 ui.warn(_("%s: can't copy - same file\n") % reltarget)
1219 1219 return True # report a failure
1220 1220 exists = False
1221 1221 samefile = True
1222 1222
1223 1223 if not after and exists or after and state in 'mn':
1224 1224 if not opts['force']:
1225 1225 if state in 'mn':
1226 1226 msg = _('%s: not overwriting - file already committed\n')
1227 1227 if after:
1228 1228 flags = '--after --force'
1229 1229 else:
1230 1230 flags = '--force'
1231 1231 if rename:
1232 1232 hint = _("('hg rename %s' to replace the file by "
1233 1233 'recording a rename)\n') % flags
1234 1234 else:
1235 1235 hint = _("('hg copy %s' to replace the file by "
1236 1236 'recording a copy)\n') % flags
1237 1237 else:
1238 1238 msg = _('%s: not overwriting - file exists\n')
1239 1239 if rename:
1240 1240 hint = _("('hg rename --after' to record the rename)\n")
1241 1241 else:
1242 1242 hint = _("('hg copy --after' to record the copy)\n")
1243 1243 ui.warn(msg % reltarget)
1244 1244 ui.warn(hint)
1245 1245 return True # report a failure
1246 1246
1247 1247 if after:
1248 1248 if not exists:
1249 1249 if rename:
1250 1250 ui.warn(_('%s: not recording move - %s does not exist\n') %
1251 1251 (relsrc, reltarget))
1252 1252 else:
1253 1253 ui.warn(_('%s: not recording copy - %s does not exist\n') %
1254 1254 (relsrc, reltarget))
1255 1255 return True # report a failure
1256 1256 elif not dryrun:
1257 1257 try:
1258 1258 if exists:
1259 1259 os.unlink(target)
1260 1260 targetdir = os.path.dirname(target) or '.'
1261 1261 if not os.path.isdir(targetdir):
1262 1262 os.makedirs(targetdir)
1263 1263 if samefile:
1264 1264 tmp = target + "~hgrename"
1265 1265 os.rename(src, tmp)
1266 1266 os.rename(tmp, target)
1267 1267 else:
1268 1268 # Preserve stat info on renames, not on copies; this matches
1269 1269 # Linux CLI behavior.
1270 1270 util.copyfile(src, target, copystat=rename)
1271 1271 srcexists = True
1272 1272 except IOError as inst:
1273 1273 if inst.errno == errno.ENOENT:
1274 1274 ui.warn(_('%s: deleted in working directory\n') % relsrc)
1275 1275 srcexists = False
1276 1276 else:
1277 1277 ui.warn(_('%s: cannot copy - %s\n') %
1278 1278 (relsrc, encoding.strtolocal(inst.strerror)))
1279 1279 return True # report a failure
1280 1280
1281 1281 if ui.verbose or not exact:
1282 1282 if rename:
1283 1283 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
1284 1284 else:
1285 1285 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
1286 1286
1287 1287 targets[abstarget] = abssrc
1288 1288
1289 1289 # fix up dirstate
1290 1290 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
1291 1291 dryrun=dryrun, cwd=cwd)
1292 1292 if rename and not dryrun:
1293 1293 if not after and srcexists and not samefile:
1294 1294 rmdir = repo.ui.configbool('experimental', 'removeemptydirs')
1295 1295 repo.wvfs.unlinkpath(abssrc, rmdir=rmdir)
1296 1296 wctx.forget([abssrc])
1297 1297
1298 1298 # pat: ossep
1299 1299 # dest ossep
1300 1300 # srcs: list of (hgsep, hgsep, ossep, bool)
1301 1301 # return: function that takes hgsep and returns ossep
1302 1302 def targetpathfn(pat, dest, srcs):
1303 1303 if os.path.isdir(pat):
1304 1304 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1305 1305 abspfx = util.localpath(abspfx)
1306 1306 if destdirexists:
1307 1307 striplen = len(os.path.split(abspfx)[0])
1308 1308 else:
1309 1309 striplen = len(abspfx)
1310 1310 if striplen:
1311 1311 striplen += len(pycompat.ossep)
1312 1312 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1313 1313 elif destdirexists:
1314 1314 res = lambda p: os.path.join(dest,
1315 1315 os.path.basename(util.localpath(p)))
1316 1316 else:
1317 1317 res = lambda p: dest
1318 1318 return res
1319 1319
1320 1320 # pat: ossep
1321 1321 # dest ossep
1322 1322 # srcs: list of (hgsep, hgsep, ossep, bool)
1323 1323 # return: function that takes hgsep and returns ossep
1324 1324 def targetpathafterfn(pat, dest, srcs):
1325 1325 if matchmod.patkind(pat):
1326 1326 # a mercurial pattern
1327 1327 res = lambda p: os.path.join(dest,
1328 1328 os.path.basename(util.localpath(p)))
1329 1329 else:
1330 1330 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1331 1331 if len(abspfx) < len(srcs[0][0]):
1332 1332 # A directory. Either the target path contains the last
1333 1333 # component of the source path or it does not.
1334 1334 def evalpath(striplen):
1335 1335 score = 0
1336 1336 for s in srcs:
1337 1337 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1338 1338 if os.path.lexists(t):
1339 1339 score += 1
1340 1340 return score
1341 1341
1342 1342 abspfx = util.localpath(abspfx)
1343 1343 striplen = len(abspfx)
1344 1344 if striplen:
1345 1345 striplen += len(pycompat.ossep)
1346 1346 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1347 1347 score = evalpath(striplen)
1348 1348 striplen1 = len(os.path.split(abspfx)[0])
1349 1349 if striplen1:
1350 1350 striplen1 += len(pycompat.ossep)
1351 1351 if evalpath(striplen1) > score:
1352 1352 striplen = striplen1
1353 1353 res = lambda p: os.path.join(dest,
1354 1354 util.localpath(p)[striplen:])
1355 1355 else:
1356 1356 # a file
1357 1357 if destdirexists:
1358 1358 res = lambda p: os.path.join(dest,
1359 1359 os.path.basename(util.localpath(p)))
1360 1360 else:
1361 1361 res = lambda p: dest
1362 1362 return res
1363 1363
1364 1364 pats = scmutil.expandpats(pats)
1365 1365 if not pats:
1366 1366 raise error.Abort(_('no source or destination specified'))
1367 1367 if len(pats) == 1:
1368 1368 raise error.Abort(_('no destination specified'))
1369 1369 dest = pats.pop()
1370 1370 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1371 1371 if not destdirexists:
1372 1372 if len(pats) > 1 or matchmod.patkind(pats[0]):
1373 1373 raise error.Abort(_('with multiple sources, destination must be an '
1374 1374 'existing directory'))
1375 1375 if util.endswithsep(dest):
1376 1376 raise error.Abort(_('destination %s is not a directory') % dest)
1377 1377
1378 1378 tfn = targetpathfn
1379 1379 if after:
1380 1380 tfn = targetpathafterfn
1381 1381 copylist = []
1382 1382 for pat in pats:
1383 1383 srcs = walkpat(pat)
1384 1384 if not srcs:
1385 1385 continue
1386 1386 copylist.append((tfn(pat, dest, srcs), srcs))
1387 1387 if not copylist:
1388 1388 raise error.Abort(_('no files to copy'))
1389 1389
1390 1390 errors = 0
1391 1391 for targetpath, srcs in copylist:
1392 1392 for abssrc, relsrc, exact in srcs:
1393 1393 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1394 1394 errors += 1
1395 1395
1396 1396 return errors != 0
1397 1397
1398 1398 ## facility to let extension process additional data into an import patch
1399 1399 # list of identifier to be executed in order
1400 1400 extrapreimport = [] # run before commit
1401 1401 extrapostimport = [] # run after commit
1402 1402 # mapping from identifier to actual import function
1403 1403 #
1404 1404 # 'preimport' are run before the commit is made and are provided the following
1405 1405 # arguments:
1406 1406 # - repo: the localrepository instance,
1407 1407 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1408 1408 # - extra: the future extra dictionary of the changeset, please mutate it,
1409 1409 # - opts: the import options.
1410 1410 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1411 1411 # mutation of in memory commit and more. Feel free to rework the code to get
1412 1412 # there.
1413 1413 extrapreimportmap = {}
1414 1414 # 'postimport' are run after the commit is made and are provided the following
1415 1415 # argument:
1416 1416 # - ctx: the changectx created by import.
1417 1417 extrapostimportmap = {}
1418 1418
1419 1419 def tryimportone(ui, repo, patchdata, parents, opts, msgs, updatefunc):
1420 1420 """Utility function used by commands.import to import a single patch
1421 1421
1422 1422 This function is explicitly defined here to help the evolve extension to
1423 1423 wrap this part of the import logic.
1424 1424
1425 1425 The API is currently a bit ugly because it a simple code translation from
1426 1426 the import command. Feel free to make it better.
1427 1427
1428 1428 :patchdata: a dictionary containing parsed patch data (such as from
1429 1429 ``patch.extract()``)
1430 1430 :parents: nodes that will be parent of the created commit
1431 1431 :opts: the full dict of option passed to the import command
1432 1432 :msgs: list to save commit message to.
1433 1433 (used in case we need to save it when failing)
1434 1434 :updatefunc: a function that update a repo to a given node
1435 1435 updatefunc(<repo>, <node>)
1436 1436 """
1437 1437 # avoid cycle context -> subrepo -> cmdutil
1438 1438 from . import context
1439 1439
1440 1440 tmpname = patchdata.get('filename')
1441 1441 message = patchdata.get('message')
1442 1442 user = opts.get('user') or patchdata.get('user')
1443 1443 date = opts.get('date') or patchdata.get('date')
1444 1444 branch = patchdata.get('branch')
1445 1445 nodeid = patchdata.get('nodeid')
1446 1446 p1 = patchdata.get('p1')
1447 1447 p2 = patchdata.get('p2')
1448 1448
1449 1449 nocommit = opts.get('no_commit')
1450 1450 importbranch = opts.get('import_branch')
1451 1451 update = not opts.get('bypass')
1452 1452 strip = opts["strip"]
1453 1453 prefix = opts["prefix"]
1454 1454 sim = float(opts.get('similarity') or 0)
1455 1455
1456 1456 if not tmpname:
1457 1457 return None, None, False
1458 1458
1459 1459 rejects = False
1460 1460
1461 1461 cmdline_message = logmessage(ui, opts)
1462 1462 if cmdline_message:
1463 1463 # pickup the cmdline msg
1464 1464 message = cmdline_message
1465 1465 elif message:
1466 1466 # pickup the patch msg
1467 1467 message = message.strip()
1468 1468 else:
1469 1469 # launch the editor
1470 1470 message = None
1471 1471 ui.debug('message:\n%s\n' % (message or ''))
1472 1472
1473 1473 if len(parents) == 1:
1474 1474 parents.append(repo[nullid])
1475 1475 if opts.get('exact'):
1476 1476 if not nodeid or not p1:
1477 1477 raise error.Abort(_('not a Mercurial patch'))
1478 1478 p1 = repo[p1]
1479 1479 p2 = repo[p2 or nullid]
1480 1480 elif p2:
1481 1481 try:
1482 1482 p1 = repo[p1]
1483 1483 p2 = repo[p2]
1484 1484 # Without any options, consider p2 only if the
1485 1485 # patch is being applied on top of the recorded
1486 1486 # first parent.
1487 1487 if p1 != parents[0]:
1488 1488 p1 = parents[0]
1489 1489 p2 = repo[nullid]
1490 1490 except error.RepoError:
1491 1491 p1, p2 = parents
1492 1492 if p2.node() == nullid:
1493 1493 ui.warn(_("warning: import the patch as a normal revision\n"
1494 1494 "(use --exact to import the patch as a merge)\n"))
1495 1495 else:
1496 1496 p1, p2 = parents
1497 1497
1498 1498 n = None
1499 1499 if update:
1500 1500 if p1 != parents[0]:
1501 1501 updatefunc(repo, p1.node())
1502 1502 if p2 != parents[1]:
1503 1503 repo.setparents(p1.node(), p2.node())
1504 1504
1505 1505 if opts.get('exact') or importbranch:
1506 1506 repo.dirstate.setbranch(branch or 'default')
1507 1507
1508 1508 partial = opts.get('partial', False)
1509 1509 files = set()
1510 1510 try:
1511 1511 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1512 1512 files=files, eolmode=None, similarity=sim / 100.0)
1513 1513 except error.PatchError as e:
1514 1514 if not partial:
1515 1515 raise error.Abort(pycompat.bytestr(e))
1516 1516 if partial:
1517 1517 rejects = True
1518 1518
1519 1519 files = list(files)
1520 1520 if nocommit:
1521 1521 if message:
1522 1522 msgs.append(message)
1523 1523 else:
1524 1524 if opts.get('exact') or p2:
1525 1525 # If you got here, you either use --force and know what
1526 1526 # you are doing or used --exact or a merge patch while
1527 1527 # being updated to its first parent.
1528 1528 m = None
1529 1529 else:
1530 1530 m = scmutil.matchfiles(repo, files or [])
1531 1531 editform = mergeeditform(repo[None], 'import.normal')
1532 1532 if opts.get('exact'):
1533 1533 editor = None
1534 1534 else:
1535 1535 editor = getcommiteditor(editform=editform,
1536 1536 **pycompat.strkwargs(opts))
1537 1537 extra = {}
1538 1538 for idfunc in extrapreimport:
1539 1539 extrapreimportmap[idfunc](repo, patchdata, extra, opts)
1540 1540 overrides = {}
1541 1541 if partial:
1542 1542 overrides[('ui', 'allowemptycommit')] = True
1543 1543 with repo.ui.configoverride(overrides, 'import'):
1544 1544 n = repo.commit(message, user,
1545 1545 date, match=m,
1546 1546 editor=editor, extra=extra)
1547 1547 for idfunc in extrapostimport:
1548 1548 extrapostimportmap[idfunc](repo[n])
1549 1549 else:
1550 1550 if opts.get('exact') or importbranch:
1551 1551 branch = branch or 'default'
1552 1552 else:
1553 1553 branch = p1.branch()
1554 1554 store = patch.filestore()
1555 1555 try:
1556 1556 files = set()
1557 1557 try:
1558 1558 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1559 1559 files, eolmode=None)
1560 1560 except error.PatchError as e:
1561 1561 raise error.Abort(stringutil.forcebytestr(e))
1562 1562 if opts.get('exact'):
1563 1563 editor = None
1564 1564 else:
1565 1565 editor = getcommiteditor(editform='import.bypass')
1566 1566 memctx = context.memctx(repo, (p1.node(), p2.node()),
1567 1567 message,
1568 1568 files=files,
1569 1569 filectxfn=store,
1570 1570 user=user,
1571 1571 date=date,
1572 1572 branch=branch,
1573 1573 editor=editor)
1574 1574 n = memctx.commit()
1575 1575 finally:
1576 1576 store.close()
1577 1577 if opts.get('exact') and nocommit:
1578 1578 # --exact with --no-commit is still useful in that it does merge
1579 1579 # and branch bits
1580 1580 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1581 1581 elif opts.get('exact') and (not n or hex(n) != nodeid):
1582 1582 raise error.Abort(_('patch is damaged or loses information'))
1583 1583 msg = _('applied to working directory')
1584 1584 if n:
1585 1585 # i18n: refers to a short changeset id
1586 1586 msg = _('created %s') % short(n)
1587 1587 return msg, n, rejects
1588 1588
1589 1589 # facility to let extensions include additional data in an exported patch
1590 1590 # list of identifiers to be executed in order
1591 1591 extraexport = []
1592 1592 # mapping from identifier to actual export function
1593 1593 # function as to return a string to be added to the header or None
1594 1594 # it is given two arguments (sequencenumber, changectx)
1595 1595 extraexportmap = {}
1596 1596
1597 1597 def _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts):
1598 1598 node = scmutil.binnode(ctx)
1599 1599 parents = [p.node() for p in ctx.parents() if p]
1600 1600 branch = ctx.branch()
1601 1601 if switch_parent:
1602 1602 parents.reverse()
1603 1603
1604 1604 if parents:
1605 1605 prev = parents[0]
1606 1606 else:
1607 1607 prev = nullid
1608 1608
1609 1609 fm.context(ctx=ctx)
1610 1610 fm.plain('# HG changeset patch\n')
1611 1611 fm.write('user', '# User %s\n', ctx.user())
1612 1612 fm.plain('# Date %d %d\n' % ctx.date())
1613 1613 fm.write('date', '# %s\n', fm.formatdate(ctx.date()))
1614 1614 fm.condwrite(branch and branch != 'default',
1615 1615 'branch', '# Branch %s\n', branch)
1616 1616 fm.write('node', '# Node ID %s\n', hex(node))
1617 1617 fm.plain('# Parent %s\n' % hex(prev))
1618 1618 if len(parents) > 1:
1619 1619 fm.plain('# Parent %s\n' % hex(parents[1]))
1620 1620 fm.data(parents=fm.formatlist(pycompat.maplist(hex, parents), name='node'))
1621 1621
1622 1622 # TODO: redesign extraexportmap function to support formatter
1623 1623 for headerid in extraexport:
1624 1624 header = extraexportmap[headerid](seqno, ctx)
1625 1625 if header is not None:
1626 1626 fm.plain('# %s\n' % header)
1627 1627
1628 1628 fm.write('desc', '%s\n', ctx.description().rstrip())
1629 1629 fm.plain('\n')
1630 1630
1631 1631 if fm.isplain():
1632 1632 chunkiter = patch.diffui(repo, prev, node, match, opts=diffopts)
1633 1633 for chunk, label in chunkiter:
1634 1634 fm.plain(chunk, label=label)
1635 1635 else:
1636 1636 chunkiter = patch.diff(repo, prev, node, match, opts=diffopts)
1637 1637 # TODO: make it structured?
1638 1638 fm.data(diff=b''.join(chunkiter))
1639 1639
1640 1640 def _exportfile(repo, revs, fm, dest, switch_parent, diffopts, match):
1641 1641 """Export changesets to stdout or a single file"""
1642 1642 for seqno, rev in enumerate(revs, 1):
1643 1643 ctx = repo[rev]
1644 1644 if not dest.startswith('<'):
1645 1645 repo.ui.note("%s\n" % dest)
1646 1646 fm.startitem()
1647 1647 _exportsingle(repo, ctx, fm, match, switch_parent, seqno, diffopts)
1648 1648
1649 1649 def _exportfntemplate(repo, revs, basefm, fntemplate, switch_parent, diffopts,
1650 1650 match):
1651 1651 """Export changesets to possibly multiple files"""
1652 1652 total = len(revs)
1653 1653 revwidth = max(len(str(rev)) for rev in revs)
1654 1654 filemap = util.sortdict() # filename: [(seqno, rev), ...]
1655 1655
1656 1656 for seqno, rev in enumerate(revs, 1):
1657 1657 ctx = repo[rev]
1658 1658 dest = makefilename(ctx, fntemplate,
1659 1659 total=total, seqno=seqno, revwidth=revwidth)
1660 1660 filemap.setdefault(dest, []).append((seqno, rev))
1661 1661
1662 1662 for dest in filemap:
1663 1663 with formatter.maybereopen(basefm, dest) as fm:
1664 1664 repo.ui.note("%s\n" % dest)
1665 1665 for seqno, rev in filemap[dest]:
1666 1666 fm.startitem()
1667 1667 ctx = repo[rev]
1668 1668 _exportsingle(repo, ctx, fm, match, switch_parent, seqno,
1669 1669 diffopts)
1670 1670
1671 1671 def export(repo, revs, basefm, fntemplate='hg-%h.patch', switch_parent=False,
1672 1672 opts=None, match=None):
1673 1673 '''export changesets as hg patches
1674 1674
1675 1675 Args:
1676 1676 repo: The repository from which we're exporting revisions.
1677 1677 revs: A list of revisions to export as revision numbers.
1678 1678 basefm: A formatter to which patches should be written.
1679 1679 fntemplate: An optional string to use for generating patch file names.
1680 1680 switch_parent: If True, show diffs against second parent when not nullid.
1681 1681 Default is false, which always shows diff against p1.
1682 1682 opts: diff options to use for generating the patch.
1683 1683 match: If specified, only export changes to files matching this matcher.
1684 1684
1685 1685 Returns:
1686 1686 Nothing.
1687 1687
1688 1688 Side Effect:
1689 1689 "HG Changeset Patch" data is emitted to one of the following
1690 1690 destinations:
1691 1691 fntemplate specified: Each rev is written to a unique file named using
1692 1692 the given template.
1693 1693 Otherwise: All revs will be written to basefm.
1694 1694 '''
1695 1695 scmutil.prefetchfiles(repo, revs, match)
1696 1696
1697 1697 if not fntemplate:
1698 1698 _exportfile(repo, revs, basefm, '<unnamed>', switch_parent, opts, match)
1699 1699 else:
1700 1700 _exportfntemplate(repo, revs, basefm, fntemplate, switch_parent, opts,
1701 1701 match)
1702 1702
1703 1703 def exportfile(repo, revs, fp, switch_parent=False, opts=None, match=None):
1704 1704 """Export changesets to the given file stream"""
1705 1705 scmutil.prefetchfiles(repo, revs, match)
1706 1706
1707 1707 dest = getattr(fp, 'name', '<unnamed>')
1708 1708 with formatter.formatter(repo.ui, fp, 'export', {}) as fm:
1709 1709 _exportfile(repo, revs, fm, dest, switch_parent, opts, match)
1710 1710
1711 1711 def showmarker(fm, marker, index=None):
1712 1712 """utility function to display obsolescence marker in a readable way
1713 1713
1714 1714 To be used by debug function."""
1715 1715 if index is not None:
1716 1716 fm.write('index', '%i ', index)
1717 1717 fm.write('prednode', '%s ', hex(marker.prednode()))
1718 1718 succs = marker.succnodes()
1719 1719 fm.condwrite(succs, 'succnodes', '%s ',
1720 1720 fm.formatlist(map(hex, succs), name='node'))
1721 1721 fm.write('flag', '%X ', marker.flags())
1722 1722 parents = marker.parentnodes()
1723 1723 if parents is not None:
1724 1724 fm.write('parentnodes', '{%s} ',
1725 1725 fm.formatlist(map(hex, parents), name='node', sep=', '))
1726 1726 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1727 1727 meta = marker.metadata().copy()
1728 1728 meta.pop('date', None)
1729 1729 smeta = pycompat.rapply(pycompat.maybebytestr, meta)
1730 1730 fm.write('metadata', '{%s}', fm.formatdict(smeta, fmt='%r: %r', sep=', '))
1731 1731 fm.plain('\n')
1732 1732
1733 1733 def finddate(ui, repo, date):
1734 1734 """Find the tipmost changeset that matches the given date spec"""
1735 1735
1736 1736 df = dateutil.matchdate(date)
1737 1737 m = scmutil.matchall(repo)
1738 1738 results = {}
1739 1739
1740 1740 def prep(ctx, fns):
1741 1741 d = ctx.date()
1742 1742 if df(d[0]):
1743 1743 results[ctx.rev()] = d
1744 1744
1745 1745 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1746 1746 rev = ctx.rev()
1747 1747 if rev in results:
1748 1748 ui.status(_("found revision %s from %s\n") %
1749 1749 (rev, dateutil.datestr(results[rev])))
1750 1750 return '%d' % rev
1751 1751
1752 1752 raise error.Abort(_("revision matching date not found"))
1753 1753
1754 1754 def increasingwindows(windowsize=8, sizelimit=512):
1755 1755 while True:
1756 1756 yield windowsize
1757 1757 if windowsize < sizelimit:
1758 1758 windowsize *= 2
1759 1759
1760 1760 def _walkrevs(repo, opts):
1761 1761 # Default --rev value depends on --follow but --follow behavior
1762 1762 # depends on revisions resolved from --rev...
1763 1763 follow = opts.get('follow') or opts.get('follow_first')
1764 1764 if opts.get('rev'):
1765 1765 revs = scmutil.revrange(repo, opts['rev'])
1766 1766 elif follow and repo.dirstate.p1() == nullid:
1767 1767 revs = smartset.baseset()
1768 1768 elif follow:
1769 1769 revs = repo.revs('reverse(:.)')
1770 1770 else:
1771 1771 revs = smartset.spanset(repo)
1772 1772 revs.reverse()
1773 1773 return revs
1774 1774
1775 1775 class FileWalkError(Exception):
1776 1776 pass
1777 1777
1778 1778 def walkfilerevs(repo, match, follow, revs, fncache):
1779 1779 '''Walks the file history for the matched files.
1780 1780
1781 1781 Returns the changeset revs that are involved in the file history.
1782 1782
1783 1783 Throws FileWalkError if the file history can't be walked using
1784 1784 filelogs alone.
1785 1785 '''
1786 1786 wanted = set()
1787 1787 copies = []
1788 1788 minrev, maxrev = min(revs), max(revs)
1789 1789 def filerevs(filelog, last):
1790 1790 """
1791 1791 Only files, no patterns. Check the history of each file.
1792 1792
1793 1793 Examines filelog entries within minrev, maxrev linkrev range
1794 1794 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1795 1795 tuples in backwards order
1796 1796 """
1797 1797 cl_count = len(repo)
1798 1798 revs = []
1799 1799 for j in pycompat.xrange(0, last + 1):
1800 1800 linkrev = filelog.linkrev(j)
1801 1801 if linkrev < minrev:
1802 1802 continue
1803 1803 # only yield rev for which we have the changelog, it can
1804 1804 # happen while doing "hg log" during a pull or commit
1805 1805 if linkrev >= cl_count:
1806 1806 break
1807 1807
1808 1808 parentlinkrevs = []
1809 1809 for p in filelog.parentrevs(j):
1810 1810 if p != nullrev:
1811 1811 parentlinkrevs.append(filelog.linkrev(p))
1812 1812 n = filelog.node(j)
1813 1813 revs.append((linkrev, parentlinkrevs,
1814 1814 follow and filelog.renamed(n)))
1815 1815
1816 1816 return reversed(revs)
1817 1817 def iterfiles():
1818 1818 pctx = repo['.']
1819 1819 for filename in match.files():
1820 1820 if follow:
1821 1821 if filename not in pctx:
1822 1822 raise error.Abort(_('cannot follow file not in parent '
1823 1823 'revision: "%s"') % filename)
1824 1824 yield filename, pctx[filename].filenode()
1825 1825 else:
1826 1826 yield filename, None
1827 1827 for filename_node in copies:
1828 1828 yield filename_node
1829 1829
1830 1830 for file_, node in iterfiles():
1831 1831 filelog = repo.file(file_)
1832 1832 if not len(filelog):
1833 1833 if node is None:
1834 1834 # A zero count may be a directory or deleted file, so
1835 1835 # try to find matching entries on the slow path.
1836 1836 if follow:
1837 1837 raise error.Abort(
1838 1838 _('cannot follow nonexistent file: "%s"') % file_)
1839 1839 raise FileWalkError("Cannot walk via filelog")
1840 1840 else:
1841 1841 continue
1842 1842
1843 1843 if node is None:
1844 1844 last = len(filelog) - 1
1845 1845 else:
1846 1846 last = filelog.rev(node)
1847 1847
1848 1848 # keep track of all ancestors of the file
1849 1849 ancestors = {filelog.linkrev(last)}
1850 1850
1851 1851 # iterate from latest to oldest revision
1852 1852 for rev, flparentlinkrevs, copied in filerevs(filelog, last):
1853 1853 if not follow:
1854 1854 if rev > maxrev:
1855 1855 continue
1856 1856 else:
1857 1857 # Note that last might not be the first interesting
1858 1858 # rev to us:
1859 1859 # if the file has been changed after maxrev, we'll
1860 1860 # have linkrev(last) > maxrev, and we still need
1861 1861 # to explore the file graph
1862 1862 if rev not in ancestors:
1863 1863 continue
1864 1864 # XXX insert 1327 fix here
1865 1865 if flparentlinkrevs:
1866 1866 ancestors.update(flparentlinkrevs)
1867 1867
1868 1868 fncache.setdefault(rev, []).append(file_)
1869 1869 wanted.add(rev)
1870 1870 if copied:
1871 1871 copies.append(copied)
1872 1872
1873 1873 return wanted
1874 1874
1875 1875 class _followfilter(object):
1876 1876 def __init__(self, repo, onlyfirst=False):
1877 1877 self.repo = repo
1878 1878 self.startrev = nullrev
1879 1879 self.roots = set()
1880 1880 self.onlyfirst = onlyfirst
1881 1881
1882 1882 def match(self, rev):
1883 1883 def realparents(rev):
1884 1884 if self.onlyfirst:
1885 1885 return self.repo.changelog.parentrevs(rev)[0:1]
1886 1886 else:
1887 1887 return filter(lambda x: x != nullrev,
1888 1888 self.repo.changelog.parentrevs(rev))
1889 1889
1890 1890 if self.startrev == nullrev:
1891 1891 self.startrev = rev
1892 1892 return True
1893 1893
1894 1894 if rev > self.startrev:
1895 1895 # forward: all descendants
1896 1896 if not self.roots:
1897 1897 self.roots.add(self.startrev)
1898 1898 for parent in realparents(rev):
1899 1899 if parent in self.roots:
1900 1900 self.roots.add(rev)
1901 1901 return True
1902 1902 else:
1903 1903 # backwards: all parents
1904 1904 if not self.roots:
1905 1905 self.roots.update(realparents(self.startrev))
1906 1906 if rev in self.roots:
1907 1907 self.roots.remove(rev)
1908 1908 self.roots.update(realparents(rev))
1909 1909 return True
1910 1910
1911 1911 return False
1912 1912
1913 1913 def walkchangerevs(repo, match, opts, prepare):
1914 1914 '''Iterate over files and the revs in which they changed.
1915 1915
1916 1916 Callers most commonly need to iterate backwards over the history
1917 1917 in which they are interested. Doing so has awful (quadratic-looking)
1918 1918 performance, so we use iterators in a "windowed" way.
1919 1919
1920 1920 We walk a window of revisions in the desired order. Within the
1921 1921 window, we first walk forwards to gather data, then in the desired
1922 1922 order (usually backwards) to display it.
1923 1923
1924 1924 This function returns an iterator yielding contexts. Before
1925 1925 yielding each context, the iterator will first call the prepare
1926 1926 function on each context in the window in forward order.'''
1927 1927
1928 1928 allfiles = opts.get('all_files')
1929 1929 follow = opts.get('follow') or opts.get('follow_first')
1930 1930 revs = _walkrevs(repo, opts)
1931 1931 if not revs:
1932 1932 return []
1933 1933 wanted = set()
1934 1934 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
1935 1935 fncache = {}
1936 1936 change = repo.__getitem__
1937 1937
1938 1938 # First step is to fill wanted, the set of revisions that we want to yield.
1939 1939 # When it does not induce extra cost, we also fill fncache for revisions in
1940 1940 # wanted: a cache of filenames that were changed (ctx.files()) and that
1941 1941 # match the file filtering conditions.
1942 1942
1943 1943 if match.always() or allfiles:
1944 1944 # No files, no patterns. Display all revs.
1945 1945 wanted = revs
1946 1946 elif not slowpath:
1947 1947 # We only have to read through the filelog to find wanted revisions
1948 1948
1949 1949 try:
1950 1950 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1951 1951 except FileWalkError:
1952 1952 slowpath = True
1953 1953
1954 1954 # We decided to fall back to the slowpath because at least one
1955 1955 # of the paths was not a file. Check to see if at least one of them
1956 1956 # existed in history, otherwise simply return
1957 1957 for path in match.files():
1958 1958 if path == '.' or path in repo.store:
1959 1959 break
1960 1960 else:
1961 1961 return []
1962 1962
1963 1963 if slowpath:
1964 1964 # We have to read the changelog to match filenames against
1965 1965 # changed files
1966 1966
1967 1967 if follow:
1968 1968 raise error.Abort(_('can only follow copies/renames for explicit '
1969 1969 'filenames'))
1970 1970
1971 1971 # The slow path checks files modified in every changeset.
1972 1972 # This is really slow on large repos, so compute the set lazily.
1973 1973 class lazywantedset(object):
1974 1974 def __init__(self):
1975 1975 self.set = set()
1976 1976 self.revs = set(revs)
1977 1977
1978 1978 # No need to worry about locality here because it will be accessed
1979 1979 # in the same order as the increasing window below.
1980 1980 def __contains__(self, value):
1981 1981 if value in self.set:
1982 1982 return True
1983 1983 elif not value in self.revs:
1984 1984 return False
1985 1985 else:
1986 1986 self.revs.discard(value)
1987 1987 ctx = change(value)
1988 1988 if allfiles:
1989 1989 matches = list(ctx.manifest().walk(match))
1990 1990 else:
1991 1991 matches = [f for f in ctx.files() if match(f)]
1992 1992 if matches:
1993 1993 fncache[value] = matches
1994 1994 self.set.add(value)
1995 1995 return True
1996 1996 return False
1997 1997
1998 1998 def discard(self, value):
1999 1999 self.revs.discard(value)
2000 2000 self.set.discard(value)
2001 2001
2002 2002 wanted = lazywantedset()
2003 2003
2004 2004 # it might be worthwhile to do this in the iterator if the rev range
2005 2005 # is descending and the prune args are all within that range
2006 2006 for rev in opts.get('prune', ()):
2007 2007 rev = repo[rev].rev()
2008 2008 ff = _followfilter(repo)
2009 2009 stop = min(revs[0], revs[-1])
2010 2010 for x in pycompat.xrange(rev, stop - 1, -1):
2011 2011 if ff.match(x):
2012 2012 wanted = wanted - [x]
2013 2013
2014 2014 # Now that wanted is correctly initialized, we can iterate over the
2015 2015 # revision range, yielding only revisions in wanted.
2016 2016 def iterate():
2017 2017 if follow and match.always():
2018 2018 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
2019 2019 def want(rev):
2020 2020 return ff.match(rev) and rev in wanted
2021 2021 else:
2022 2022 def want(rev):
2023 2023 return rev in wanted
2024 2024
2025 2025 it = iter(revs)
2026 2026 stopiteration = False
2027 2027 for windowsize in increasingwindows():
2028 2028 nrevs = []
2029 2029 for i in pycompat.xrange(windowsize):
2030 2030 rev = next(it, None)
2031 2031 if rev is None:
2032 2032 stopiteration = True
2033 2033 break
2034 2034 elif want(rev):
2035 2035 nrevs.append(rev)
2036 2036 for rev in sorted(nrevs):
2037 2037 fns = fncache.get(rev)
2038 2038 ctx = change(rev)
2039 2039 if not fns:
2040 2040 def fns_generator():
2041 2041 if allfiles:
2042 2042 fiter = iter(ctx)
2043 2043 else:
2044 2044 fiter = ctx.files()
2045 2045 for f in fiter:
2046 2046 if match(f):
2047 2047 yield f
2048 2048 fns = fns_generator()
2049 2049 prepare(ctx, fns)
2050 2050 for rev in nrevs:
2051 2051 yield change(rev)
2052 2052
2053 2053 if stopiteration:
2054 2054 break
2055 2055
2056 2056 return iterate()
2057 2057
2058 2058 def add(ui, repo, match, prefix, uipathfn, explicitonly, **opts):
2059 2059 bad = []
2060 2060
2061 2061 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2062 2062 names = []
2063 2063 wctx = repo[None]
2064 2064 cca = None
2065 2065 abort, warn = scmutil.checkportabilityalert(ui)
2066 2066 if abort or warn:
2067 2067 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2068 2068
2069 2069 match = repo.narrowmatch(match, includeexact=True)
2070 2070 badmatch = matchmod.badmatch(match, badfn)
2071 2071 dirstate = repo.dirstate
2072 2072 # We don't want to just call wctx.walk here, since it would return a lot of
2073 2073 # clean files, which we aren't interested in and takes time.
2074 2074 for f in sorted(dirstate.walk(badmatch, subrepos=sorted(wctx.substate),
2075 2075 unknown=True, ignored=False, full=False)):
2076 2076 exact = match.exact(f)
2077 2077 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2078 2078 if cca:
2079 2079 cca(f)
2080 2080 names.append(f)
2081 2081 if ui.verbose or not exact:
2082 2082 ui.status(_('adding %s\n') % uipathfn(f),
2083 2083 label='ui.addremove.added')
2084 2084
2085 2085 for subpath in sorted(wctx.substate):
2086 2086 sub = wctx.sub(subpath)
2087 2087 try:
2088 2088 submatch = matchmod.subdirmatcher(subpath, match)
2089 2089 subprefix = repo.wvfs.reljoin(prefix, subpath)
2090 2090 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2091 2091 if opts.get(r'subrepos'):
2092 2092 bad.extend(sub.add(ui, submatch, subprefix, subuipathfn, False,
2093 2093 **opts))
2094 2094 else:
2095 2095 bad.extend(sub.add(ui, submatch, subprefix, subuipathfn, True,
2096 2096 **opts))
2097 2097 except error.LookupError:
2098 2098 ui.status(_("skipping missing subrepository: %s\n")
2099 2099 % uipathfn(subpath))
2100 2100
2101 2101 if not opts.get(r'dry_run'):
2102 2102 rejected = wctx.add(names, prefix)
2103 2103 bad.extend(f for f in rejected if f in match.files())
2104 2104 return bad
2105 2105
2106 2106 def addwebdirpath(repo, serverpath, webconf):
2107 2107 webconf[serverpath] = repo.root
2108 2108 repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
2109 2109
2110 2110 for r in repo.revs('filelog("path:.hgsub")'):
2111 2111 ctx = repo[r]
2112 2112 for subpath in ctx.substate:
2113 2113 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
2114 2114
2115 2115 def forget(ui, repo, match, prefix, uipathfn, explicitonly, dryrun,
2116 2116 interactive):
2117 2117 if dryrun and interactive:
2118 2118 raise error.Abort(_("cannot specify both --dry-run and --interactive"))
2119 2119 bad = []
2120 2120 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2121 2121 wctx = repo[None]
2122 2122 forgot = []
2123 2123
2124 2124 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2125 2125 forget = sorted(s.modified + s.added + s.deleted + s.clean)
2126 2126 if explicitonly:
2127 2127 forget = [f for f in forget if match.exact(f)]
2128 2128
2129 2129 for subpath in sorted(wctx.substate):
2130 2130 sub = wctx.sub(subpath)
2131 2131 submatch = matchmod.subdirmatcher(subpath, match)
2132 2132 subprefix = repo.wvfs.reljoin(prefix, subpath)
2133 2133 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2134 2134 try:
2135 2135 subbad, subforgot = sub.forget(submatch, subprefix, subuipathfn,
2136 2136 dryrun=dryrun,
2137 2137 interactive=interactive)
2138 2138 bad.extend([subpath + '/' + f for f in subbad])
2139 2139 forgot.extend([subpath + '/' + f for f in subforgot])
2140 2140 except error.LookupError:
2141 2141 ui.status(_("skipping missing subrepository: %s\n")
2142 2142 % uipathfn(subpath))
2143 2143
2144 2144 if not explicitonly:
2145 2145 for f in match.files():
2146 2146 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2147 2147 if f not in forgot:
2148 2148 if repo.wvfs.exists(f):
2149 2149 # Don't complain if the exact case match wasn't given.
2150 2150 # But don't do this until after checking 'forgot', so
2151 2151 # that subrepo files aren't normalized, and this op is
2152 2152 # purely from data cached by the status walk above.
2153 2153 if repo.dirstate.normalize(f) in repo.dirstate:
2154 2154 continue
2155 2155 ui.warn(_('not removing %s: '
2156 2156 'file is already untracked\n')
2157 2157 % uipathfn(f))
2158 2158 bad.append(f)
2159 2159
2160 2160 if interactive:
2161 2161 responses = _('[Ynsa?]'
2162 2162 '$$ &Yes, forget this file'
2163 2163 '$$ &No, skip this file'
2164 2164 '$$ &Skip remaining files'
2165 2165 '$$ Include &all remaining files'
2166 2166 '$$ &? (display help)')
2167 2167 for filename in forget[:]:
2168 2168 r = ui.promptchoice(_('forget %s %s') %
2169 2169 (uipathfn(filename), responses))
2170 2170 if r == 4: # ?
2171 2171 while r == 4:
2172 2172 for c, t in ui.extractchoices(responses)[1]:
2173 2173 ui.write('%s - %s\n' % (c, encoding.lower(t)))
2174 2174 r = ui.promptchoice(_('forget %s %s') %
2175 2175 (uipathfn(filename), responses))
2176 2176 if r == 0: # yes
2177 2177 continue
2178 2178 elif r == 1: # no
2179 2179 forget.remove(filename)
2180 2180 elif r == 2: # Skip
2181 2181 fnindex = forget.index(filename)
2182 2182 del forget[fnindex:]
2183 2183 break
2184 2184 elif r == 3: # All
2185 2185 break
2186 2186
2187 2187 for f in forget:
2188 2188 if ui.verbose or not match.exact(f) or interactive:
2189 2189 ui.status(_('removing %s\n') % uipathfn(f),
2190 2190 label='ui.addremove.removed')
2191 2191
2192 2192 if not dryrun:
2193 2193 rejected = wctx.forget(forget, prefix)
2194 2194 bad.extend(f for f in rejected if f in match.files())
2195 2195 forgot.extend(f for f in forget if f not in rejected)
2196 2196 return bad, forgot
2197 2197
2198 2198 def files(ui, ctx, m, uipathfn, fm, fmt, subrepos):
2199 2199 ret = 1
2200 2200
2201 2201 needsfctx = ui.verbose or {'size', 'flags'} & fm.datahint()
2202 2202 for f in ctx.matches(m):
2203 2203 fm.startitem()
2204 2204 fm.context(ctx=ctx)
2205 2205 if needsfctx:
2206 2206 fc = ctx[f]
2207 2207 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2208 2208 fm.data(path=f)
2209 2209 fm.plain(fmt % uipathfn(f))
2210 2210 ret = 0
2211 2211
2212 2212 for subpath in sorted(ctx.substate):
2213 2213 submatch = matchmod.subdirmatcher(subpath, m)
2214 2214 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2215 2215 if (subrepos or m.exact(subpath) or any(submatch.files())):
2216 2216 sub = ctx.sub(subpath)
2217 2217 try:
2218 2218 recurse = m.exact(subpath) or subrepos
2219 2219 if sub.printfiles(ui, submatch, subuipathfn, fm, fmt,
2220 2220 recurse) == 0:
2221 2221 ret = 0
2222 2222 except error.LookupError:
2223 2223 ui.status(_("skipping missing subrepository: %s\n")
2224 2224 % uipathfn(subpath))
2225 2225
2226 2226 return ret
2227 2227
2228 2228 def remove(ui, repo, m, prefix, uipathfn, after, force, subrepos, dryrun,
2229 2229 warnings=None):
2230 2230 ret = 0
2231 2231 s = repo.status(match=m, clean=True)
2232 2232 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2233 2233
2234 2234 wctx = repo[None]
2235 2235
2236 2236 if warnings is None:
2237 2237 warnings = []
2238 2238 warn = True
2239 2239 else:
2240 2240 warn = False
2241 2241
2242 2242 subs = sorted(wctx.substate)
2243 2243 progress = ui.makeprogress(_('searching'), total=len(subs),
2244 2244 unit=_('subrepos'))
2245 2245 for subpath in subs:
2246 2246 submatch = matchmod.subdirmatcher(subpath, m)
2247 2247 subprefix = repo.wvfs.reljoin(prefix, subpath)
2248 2248 subuipathfn = scmutil.subdiruipathfn(subpath, uipathfn)
2249 2249 if subrepos or m.exact(subpath) or any(submatch.files()):
2250 2250 progress.increment()
2251 2251 sub = wctx.sub(subpath)
2252 2252 try:
2253 2253 if sub.removefiles(submatch, subprefix, subuipathfn, after,
2254 2254 force, subrepos, dryrun, warnings):
2255 2255 ret = 1
2256 2256 except error.LookupError:
2257 2257 warnings.append(_("skipping missing subrepository: %s\n")
2258 2258 % uipathfn(subpath))
2259 2259 progress.complete()
2260 2260
2261 2261 # warn about failure to delete explicit files/dirs
2262 2262 deleteddirs = util.dirs(deleted)
2263 2263 files = m.files()
2264 2264 progress = ui.makeprogress(_('deleting'), total=len(files),
2265 2265 unit=_('files'))
2266 2266 for f in files:
2267 2267 def insubrepo():
2268 2268 for subpath in wctx.substate:
2269 2269 if f.startswith(subpath + '/'):
2270 2270 return True
2271 2271 return False
2272 2272
2273 2273 progress.increment()
2274 2274 isdir = f in deleteddirs or wctx.hasdir(f)
2275 2275 if (f in repo.dirstate or isdir or f == '.'
2276 2276 or insubrepo() or f in subs):
2277 2277 continue
2278 2278
2279 2279 if repo.wvfs.exists(f):
2280 2280 if repo.wvfs.isdir(f):
2281 2281 warnings.append(_('not removing %s: no tracked files\n')
2282 2282 % uipathfn(f))
2283 2283 else:
2284 2284 warnings.append(_('not removing %s: file is untracked\n')
2285 2285 % uipathfn(f))
2286 2286 # missing files will generate a warning elsewhere
2287 2287 ret = 1
2288 2288 progress.complete()
2289 2289
2290 2290 if force:
2291 2291 list = modified + deleted + clean + added
2292 2292 elif after:
2293 2293 list = deleted
2294 2294 remaining = modified + added + clean
2295 2295 progress = ui.makeprogress(_('skipping'), total=len(remaining),
2296 2296 unit=_('files'))
2297 2297 for f in remaining:
2298 2298 progress.increment()
2299 2299 if ui.verbose or (f in files):
2300 2300 warnings.append(_('not removing %s: file still exists\n')
2301 2301 % uipathfn(f))
2302 2302 ret = 1
2303 2303 progress.complete()
2304 2304 else:
2305 2305 list = deleted + clean
2306 2306 progress = ui.makeprogress(_('skipping'),
2307 2307 total=(len(modified) + len(added)),
2308 2308 unit=_('files'))
2309 2309 for f in modified:
2310 2310 progress.increment()
2311 2311 warnings.append(_('not removing %s: file is modified (use -f'
2312 2312 ' to force removal)\n') % uipathfn(f))
2313 2313 ret = 1
2314 2314 for f in added:
2315 2315 progress.increment()
2316 2316 warnings.append(_("not removing %s: file has been marked for add"
2317 2317 " (use 'hg forget' to undo add)\n") % uipathfn(f))
2318 2318 ret = 1
2319 2319 progress.complete()
2320 2320
2321 2321 list = sorted(list)
2322 2322 progress = ui.makeprogress(_('deleting'), total=len(list),
2323 2323 unit=_('files'))
2324 2324 for f in list:
2325 2325 if ui.verbose or not m.exact(f):
2326 2326 progress.increment()
2327 2327 ui.status(_('removing %s\n') % uipathfn(f),
2328 2328 label='ui.addremove.removed')
2329 2329 progress.complete()
2330 2330
2331 2331 if not dryrun:
2332 2332 with repo.wlock():
2333 2333 if not after:
2334 2334 for f in list:
2335 2335 if f in added:
2336 2336 continue # we never unlink added files on remove
2337 2337 rmdir = repo.ui.configbool('experimental',
2338 2338 'removeemptydirs')
2339 2339 repo.wvfs.unlinkpath(f, ignoremissing=True, rmdir=rmdir)
2340 2340 repo[None].forget(list)
2341 2341
2342 2342 if warn:
2343 2343 for warning in warnings:
2344 2344 ui.warn(warning)
2345 2345
2346 2346 return ret
2347 2347
2348 2348 def _updatecatformatter(fm, ctx, matcher, path, decode):
2349 2349 """Hook for adding data to the formatter used by ``hg cat``.
2350 2350
2351 2351 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2352 2352 this method first."""
2353 2353 data = ctx[path].data()
2354 2354 if decode:
2355 2355 data = ctx.repo().wwritedata(path, data)
2356 2356 fm.startitem()
2357 2357 fm.context(ctx=ctx)
2358 2358 fm.write('data', '%s', data)
2359 2359 fm.data(path=path)
2360 2360
2361 2361 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2362 2362 err = 1
2363 2363 opts = pycompat.byteskwargs(opts)
2364 2364
2365 2365 def write(path):
2366 2366 filename = None
2367 2367 if fntemplate:
2368 2368 filename = makefilename(ctx, fntemplate,
2369 2369 pathname=os.path.join(prefix, path))
2370 2370 # attempt to create the directory if it does not already exist
2371 2371 try:
2372 2372 os.makedirs(os.path.dirname(filename))
2373 2373 except OSError:
2374 2374 pass
2375 2375 with formatter.maybereopen(basefm, filename) as fm:
2376 2376 _updatecatformatter(fm, ctx, matcher, path, opts.get('decode'))
2377 2377
2378 2378 # Automation often uses hg cat on single files, so special case it
2379 2379 # for performance to avoid the cost of parsing the manifest.
2380 2380 if len(matcher.files()) == 1 and not matcher.anypats():
2381 2381 file = matcher.files()[0]
2382 2382 mfl = repo.manifestlog
2383 2383 mfnode = ctx.manifestnode()
2384 2384 try:
2385 2385 if mfnode and mfl[mfnode].find(file)[0]:
2386 2386 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2387 2387 write(file)
2388 2388 return 0
2389 2389 except KeyError:
2390 2390 pass
2391 2391
2392 2392 scmutil.prefetchfiles(repo, [ctx.rev()], matcher)
2393 2393
2394 2394 for abs in ctx.walk(matcher):
2395 2395 write(abs)
2396 2396 err = 0
2397 2397
2398 2398 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2399 2399 for subpath in sorted(ctx.substate):
2400 2400 sub = ctx.sub(subpath)
2401 2401 try:
2402 2402 submatch = matchmod.subdirmatcher(subpath, matcher)
2403 2403 subprefix = os.path.join(prefix, subpath)
2404 2404 if not sub.cat(submatch, basefm, fntemplate, subprefix,
2405 2405 **pycompat.strkwargs(opts)):
2406 2406 err = 0
2407 2407 except error.RepoLookupError:
2408 2408 ui.status(_("skipping missing subrepository: %s\n") %
2409 2409 uipathfn(subpath))
2410 2410
2411 2411 return err
2412 2412
2413 2413 def commit(ui, repo, commitfunc, pats, opts):
2414 2414 '''commit the specified files or all outstanding changes'''
2415 2415 date = opts.get('date')
2416 2416 if date:
2417 2417 opts['date'] = dateutil.parsedate(date)
2418 2418 message = logmessage(ui, opts)
2419 2419 matcher = scmutil.match(repo[None], pats, opts)
2420 2420
2421 2421 dsguard = None
2422 2422 # extract addremove carefully -- this function can be called from a command
2423 2423 # that doesn't support addremove
2424 2424 if opts.get('addremove'):
2425 2425 dsguard = dirstateguard.dirstateguard(repo, 'commit')
2426 2426 with dsguard or util.nullcontextmanager():
2427 2427 if dsguard:
2428 2428 relative = scmutil.anypats(pats, opts)
2429 2429 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2430 2430 if scmutil.addremove(repo, matcher, "", uipathfn, opts) != 0:
2431 2431 raise error.Abort(
2432 2432 _("failed to mark all new/missing files as added/removed"))
2433 2433
2434 2434 return commitfunc(ui, repo, message, matcher, opts)
2435 2435
2436 2436 def samefile(f, ctx1, ctx2):
2437 2437 if f in ctx1.manifest():
2438 2438 a = ctx1.filectx(f)
2439 2439 if f in ctx2.manifest():
2440 2440 b = ctx2.filectx(f)
2441 2441 return (not a.cmp(b)
2442 2442 and a.flags() == b.flags())
2443 2443 else:
2444 2444 return False
2445 2445 else:
2446 2446 return f not in ctx2.manifest()
2447 2447
2448 2448 def amend(ui, repo, old, extra, pats, opts):
2449 2449 # avoid cycle context -> subrepo -> cmdutil
2450 2450 from . import context
2451 2451
2452 2452 # amend will reuse the existing user if not specified, but the obsolete
2453 2453 # marker creation requires that the current user's name is specified.
2454 2454 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2455 2455 ui.username() # raise exception if username not set
2456 2456
2457 2457 ui.note(_('amending changeset %s\n') % old)
2458 2458 base = old.p1()
2459 2459
2460 2460 with repo.wlock(), repo.lock(), repo.transaction('amend'):
2461 2461 # Participating changesets:
2462 2462 #
2463 2463 # wctx o - workingctx that contains changes from working copy
2464 2464 # | to go into amending commit
2465 2465 # |
2466 2466 # old o - changeset to amend
2467 2467 # |
2468 2468 # base o - first parent of the changeset to amend
2469 2469 wctx = repo[None]
2470 2470
2471 2471 # Copy to avoid mutating input
2472 2472 extra = extra.copy()
2473 2473 # Update extra dict from amended commit (e.g. to preserve graft
2474 2474 # source)
2475 2475 extra.update(old.extra())
2476 2476
2477 2477 # Also update it from the from the wctx
2478 2478 extra.update(wctx.extra())
2479 2479
2480 2480 user = opts.get('user') or old.user()
2481 2481
2482 2482 datemaydiffer = False # date-only change should be ignored?
2483 2483 if opts.get('date') and opts.get('currentdate'):
2484 2484 raise error.Abort(_('--date and --currentdate are mutually '
2485 2485 'exclusive'))
2486 2486 if opts.get('date'):
2487 2487 date = dateutil.parsedate(opts.get('date'))
2488 2488 elif opts.get('currentdate'):
2489 2489 date = dateutil.makedate()
2490 2490 elif (ui.configbool('rewrite', 'update-timestamp')
2491 2491 and opts.get('currentdate') is None):
2492 2492 date = dateutil.makedate()
2493 2493 datemaydiffer = True
2494 2494 else:
2495 2495 date = old.date()
2496 2496
2497 2497 if len(old.parents()) > 1:
2498 2498 # ctx.files() isn't reliable for merges, so fall back to the
2499 2499 # slower repo.status() method
2500 2500 files = {fn for st in base.status(old)[:3] for fn in st}
2501 2501 else:
2502 2502 files = set(old.files())
2503 2503
2504 2504 # add/remove the files to the working copy if the "addremove" option
2505 2505 # was specified.
2506 2506 matcher = scmutil.match(wctx, pats, opts)
2507 2507 relative = scmutil.anypats(pats, opts)
2508 2508 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=relative)
2509 2509 if (opts.get('addremove')
2510 2510 and scmutil.addremove(repo, matcher, "", uipathfn, opts)):
2511 2511 raise error.Abort(
2512 2512 _("failed to mark all new/missing files as added/removed"))
2513 2513
2514 2514 # Check subrepos. This depends on in-place wctx._status update in
2515 2515 # subrepo.precommit(). To minimize the risk of this hack, we do
2516 2516 # nothing if .hgsub does not exist.
2517 2517 if '.hgsub' in wctx or '.hgsub' in old:
2518 2518 subs, commitsubs, newsubstate = subrepoutil.precommit(
2519 2519 ui, wctx, wctx._status, matcher)
2520 2520 # amend should abort if commitsubrepos is enabled
2521 2521 assert not commitsubs
2522 2522 if subs:
2523 2523 subrepoutil.writestate(repo, newsubstate)
2524 2524
2525 2525 ms = mergemod.mergestate.read(repo)
2526 2526 mergeutil.checkunresolved(ms)
2527 2527
2528 2528 filestoamend = set(f for f in wctx.files() if matcher(f))
2529 2529
2530 2530 changes = (len(filestoamend) > 0)
2531 2531 if changes:
2532 2532 # Recompute copies (avoid recording a -> b -> a)
2533 2533 copied = copies.pathcopies(base, wctx, matcher)
2534 2534 if old.p2:
2535 2535 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2536 2536
2537 2537 # Prune files which were reverted by the updates: if old
2538 2538 # introduced file X and the file was renamed in the working
2539 2539 # copy, then those two files are the same and
2540 2540 # we can discard X from our list of files. Likewise if X
2541 2541 # was removed, it's no longer relevant. If X is missing (aka
2542 2542 # deleted), old X must be preserved.
2543 2543 files.update(filestoamend)
2544 2544 files = [f for f in files if (not samefile(f, wctx, base)
2545 2545 or f in wctx.deleted())]
2546 2546
2547 2547 def filectxfn(repo, ctx_, path):
2548 2548 try:
2549 2549 # If the file being considered is not amongst the files
2550 2550 # to be amended, we should return the file context from the
2551 2551 # old changeset. This avoids issues when only some files in
2552 2552 # the working copy are being amended but there are also
2553 2553 # changes to other files from the old changeset.
2554 2554 if path not in filestoamend:
2555 2555 return old.filectx(path)
2556 2556
2557 2557 # Return None for removed files.
2558 2558 if path in wctx.removed():
2559 2559 return None
2560 2560
2561 2561 fctx = wctx[path]
2562 2562 flags = fctx.flags()
2563 2563 mctx = context.memfilectx(repo, ctx_,
2564 2564 fctx.path(), fctx.data(),
2565 2565 islink='l' in flags,
2566 2566 isexec='x' in flags,
2567 2567 copysource=copied.get(path))
2568 2568 return mctx
2569 2569 except KeyError:
2570 2570 return None
2571 2571 else:
2572 2572 ui.note(_('copying changeset %s to %s\n') % (old, base))
2573 2573
2574 2574 # Use version of files as in the old cset
2575 2575 def filectxfn(repo, ctx_, path):
2576 2576 try:
2577 2577 return old.filectx(path)
2578 2578 except KeyError:
2579 2579 return None
2580 2580
2581 2581 # See if we got a message from -m or -l, if not, open the editor with
2582 2582 # the message of the changeset to amend.
2583 2583 message = logmessage(ui, opts)
2584 2584
2585 2585 editform = mergeeditform(old, 'commit.amend')
2586 2586 editor = getcommiteditor(editform=editform,
2587 2587 **pycompat.strkwargs(opts))
2588 2588
2589 2589 if not message:
2590 2590 editor = getcommiteditor(edit=True, editform=editform)
2591 2591 message = old.description()
2592 2592
2593 2593 pureextra = extra.copy()
2594 2594 extra['amend_source'] = old.hex()
2595 2595
2596 2596 new = context.memctx(repo,
2597 2597 parents=[base.node(), old.p2().node()],
2598 2598 text=message,
2599 2599 files=files,
2600 2600 filectxfn=filectxfn,
2601 2601 user=user,
2602 2602 date=date,
2603 2603 extra=extra,
2604 2604 editor=editor)
2605 2605
2606 2606 newdesc = changelog.stripdesc(new.description())
2607 2607 if ((not changes)
2608 2608 and newdesc == old.description()
2609 2609 and user == old.user()
2610 2610 and (date == old.date() or datemaydiffer)
2611 2611 and pureextra == old.extra()):
2612 2612 # nothing changed. continuing here would create a new node
2613 2613 # anyway because of the amend_source noise.
2614 2614 #
2615 2615 # This not what we expect from amend.
2616 2616 return old.node()
2617 2617
2618 2618 commitphase = None
2619 2619 if opts.get('secret'):
2620 2620 commitphase = phases.secret
2621 2621 newid = repo.commitctx(new)
2622 2622
2623 2623 # Reroute the working copy parent to the new changeset
2624 2624 repo.setparents(newid, nullid)
2625 2625 mapping = {old.node(): (newid,)}
2626 2626 obsmetadata = None
2627 2627 if opts.get('note'):
2628 2628 obsmetadata = {'note': encoding.fromlocal(opts['note'])}
2629 2629 backup = ui.configbool('rewrite', 'backup-bundle')
2630 2630 scmutil.cleanupnodes(repo, mapping, 'amend', metadata=obsmetadata,
2631 2631 fixphase=True, targetphase=commitphase,
2632 2632 backup=backup)
2633 2633
2634 2634 # Fixing the dirstate because localrepo.commitctx does not update
2635 2635 # it. This is rather convenient because we did not need to update
2636 2636 # the dirstate for all the files in the new commit which commitctx
2637 2637 # could have done if it updated the dirstate. Now, we can
2638 2638 # selectively update the dirstate only for the amended files.
2639 2639 dirstate = repo.dirstate
2640 2640
2641 2641 # Update the state of the files which were added and
2642 2642 # and modified in the amend to "normal" in the dirstate.
2643 2643 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2644 2644 for f in normalfiles:
2645 2645 dirstate.normal(f)
2646 2646
2647 2647 # Update the state of files which were removed in the amend
2648 2648 # to "removed" in the dirstate.
2649 2649 removedfiles = set(wctx.removed()) & filestoamend
2650 2650 for f in removedfiles:
2651 2651 dirstate.drop(f)
2652 2652
2653 2653 return newid
2654 2654
2655 2655 def commiteditor(repo, ctx, subs, editform=''):
2656 2656 if ctx.description():
2657 2657 return ctx.description()
2658 2658 return commitforceeditor(repo, ctx, subs, editform=editform,
2659 2659 unchangedmessagedetection=True)
2660 2660
2661 2661 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2662 2662 editform='', unchangedmessagedetection=False):
2663 2663 if not extramsg:
2664 2664 extramsg = _("Leave message empty to abort commit.")
2665 2665
2666 2666 forms = [e for e in editform.split('.') if e]
2667 2667 forms.insert(0, 'changeset')
2668 2668 templatetext = None
2669 2669 while forms:
2670 2670 ref = '.'.join(forms)
2671 2671 if repo.ui.config('committemplate', ref):
2672 2672 templatetext = committext = buildcommittemplate(
2673 2673 repo, ctx, subs, extramsg, ref)
2674 2674 break
2675 2675 forms.pop()
2676 2676 else:
2677 2677 committext = buildcommittext(repo, ctx, subs, extramsg)
2678 2678
2679 2679 # run editor in the repository root
2680 2680 olddir = encoding.getcwd()
2681 2681 os.chdir(repo.root)
2682 2682
2683 2683 # make in-memory changes visible to external process
2684 2684 tr = repo.currenttransaction()
2685 2685 repo.dirstate.write(tr)
2686 2686 pending = tr and tr.writepending() and repo.root
2687 2687
2688 2688 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2689 2689 editform=editform, pending=pending,
2690 2690 repopath=repo.path, action='commit')
2691 2691 text = editortext
2692 2692
2693 2693 # strip away anything below this special string (used for editors that want
2694 2694 # to display the diff)
2695 2695 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2696 2696 if stripbelow:
2697 2697 text = text[:stripbelow.start()]
2698 2698
2699 2699 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2700 2700 os.chdir(olddir)
2701 2701
2702 2702 if finishdesc:
2703 2703 text = finishdesc(text)
2704 2704 if not text.strip():
2705 2705 raise error.Abort(_("empty commit message"))
2706 2706 if unchangedmessagedetection and editortext == templatetext:
2707 2707 raise error.Abort(_("commit message unchanged"))
2708 2708
2709 2709 return text
2710 2710
2711 2711 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
2712 2712 ui = repo.ui
2713 2713 spec = formatter.templatespec(ref, None, None)
2714 2714 t = logcmdutil.changesettemplater(ui, repo, spec)
2715 2715 t.t.cache.update((k, templater.unquotestring(v))
2716 2716 for k, v in repo.ui.configitems('committemplate'))
2717 2717
2718 2718 if not extramsg:
2719 2719 extramsg = '' # ensure that extramsg is string
2720 2720
2721 2721 ui.pushbuffer()
2722 2722 t.show(ctx, extramsg=extramsg)
2723 2723 return ui.popbuffer()
2724 2724
2725 2725 def hgprefix(msg):
2726 2726 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2727 2727
2728 2728 def buildcommittext(repo, ctx, subs, extramsg):
2729 2729 edittext = []
2730 2730 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2731 2731 if ctx.description():
2732 2732 edittext.append(ctx.description())
2733 2733 edittext.append("")
2734 2734 edittext.append("") # Empty line between message and comments.
2735 2735 edittext.append(hgprefix(_("Enter commit message."
2736 2736 " Lines beginning with 'HG:' are removed.")))
2737 2737 edittext.append(hgprefix(extramsg))
2738 2738 edittext.append("HG: --")
2739 2739 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2740 2740 if ctx.p2():
2741 2741 edittext.append(hgprefix(_("branch merge")))
2742 2742 if ctx.branch():
2743 2743 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2744 2744 if bookmarks.isactivewdirparent(repo):
2745 2745 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2746 2746 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2747 2747 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2748 2748 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2749 2749 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2750 2750 if not added and not modified and not removed:
2751 2751 edittext.append(hgprefix(_("no files changed")))
2752 2752 edittext.append("")
2753 2753
2754 2754 return "\n".join(edittext)
2755 2755
2756 2756 def commitstatus(repo, node, branch, bheads=None, opts=None):
2757 2757 if opts is None:
2758 2758 opts = {}
2759 2759 ctx = repo[node]
2760 2760 parents = ctx.parents()
2761 2761
2762 2762 if (not opts.get('amend') and bheads and node not in bheads and not
2763 2763 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2764 2764 repo.ui.status(_('created new head\n'))
2765 2765 # The message is not printed for initial roots. For the other
2766 2766 # changesets, it is printed in the following situations:
2767 2767 #
2768 2768 # Par column: for the 2 parents with ...
2769 2769 # N: null or no parent
2770 2770 # B: parent is on another named branch
2771 2771 # C: parent is a regular non head changeset
2772 2772 # H: parent was a branch head of the current branch
2773 2773 # Msg column: whether we print "created new head" message
2774 2774 # In the following, it is assumed that there already exists some
2775 2775 # initial branch heads of the current branch, otherwise nothing is
2776 2776 # printed anyway.
2777 2777 #
2778 2778 # Par Msg Comment
2779 2779 # N N y additional topo root
2780 2780 #
2781 2781 # B N y additional branch root
2782 2782 # C N y additional topo head
2783 2783 # H N n usual case
2784 2784 #
2785 2785 # B B y weird additional branch root
2786 2786 # C B y branch merge
2787 2787 # H B n merge with named branch
2788 2788 #
2789 2789 # C C y additional head from merge
2790 2790 # C H n merge with a head
2791 2791 #
2792 2792 # H H n head merge: head count decreases
2793 2793
2794 2794 if not opts.get('close_branch'):
2795 2795 for r in parents:
2796 2796 if r.closesbranch() and r.branch() == branch:
2797 2797 repo.ui.status(_('reopening closed branch head %d\n') % r.rev())
2798 2798
2799 2799 if repo.ui.debugflag:
2800 2800 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx.hex()))
2801 2801 elif repo.ui.verbose:
2802 2802 repo.ui.write(_('committed changeset %d:%s\n') % (ctx.rev(), ctx))
2803 2803
2804 2804 def postcommitstatus(repo, pats, opts):
2805 2805 return repo.status(match=scmutil.match(repo[None], pats, opts))
2806 2806
2807 2807 def revert(ui, repo, ctx, parents, *pats, **opts):
2808 2808 opts = pycompat.byteskwargs(opts)
2809 2809 parent, p2 = parents
2810 2810 node = ctx.node()
2811 2811
2812 2812 mf = ctx.manifest()
2813 2813 if node == p2:
2814 2814 parent = p2
2815 2815
2816 2816 # need all matching names in dirstate and manifest of target rev,
2817 2817 # so have to walk both. do not print errors if files exist in one
2818 2818 # but not other. in both cases, filesets should be evaluated against
2819 2819 # workingctx to get consistent result (issue4497). this means 'set:**'
2820 2820 # cannot be used to select missing files from target rev.
2821 2821
2822 2822 # `names` is a mapping for all elements in working copy and target revision
2823 2823 # The mapping is in the form:
2824 2824 # <abs path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2825 2825 names = {}
2826 2826 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
2827 2827
2828 2828 with repo.wlock():
2829 2829 ## filling of the `names` mapping
2830 2830 # walk dirstate to fill `names`
2831 2831
2832 2832 interactive = opts.get('interactive', False)
2833 2833 wctx = repo[None]
2834 2834 m = scmutil.match(wctx, pats, opts)
2835 2835
2836 2836 # we'll need this later
2837 2837 targetsubs = sorted(s for s in wctx.substate if m(s))
2838 2838
2839 2839 if not m.always():
2840 2840 matcher = matchmod.badmatch(m, lambda x, y: False)
2841 2841 for abs in wctx.walk(matcher):
2842 2842 names[abs] = m.exact(abs)
2843 2843
2844 2844 # walk target manifest to fill `names`
2845 2845
2846 2846 def badfn(path, msg):
2847 2847 if path in names:
2848 2848 return
2849 2849 if path in ctx.substate:
2850 2850 return
2851 2851 path_ = path + '/'
2852 2852 for f in names:
2853 2853 if f.startswith(path_):
2854 2854 return
2855 2855 ui.warn("%s: %s\n" % (uipathfn(path), msg))
2856 2856
2857 2857 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2858 2858 if abs not in names:
2859 2859 names[abs] = m.exact(abs)
2860 2860
2861 2861 # Find status of all file in `names`.
2862 2862 m = scmutil.matchfiles(repo, names)
2863 2863
2864 2864 changes = repo.status(node1=node, match=m,
2865 2865 unknown=True, ignored=True, clean=True)
2866 2866 else:
2867 2867 changes = repo.status(node1=node, match=m)
2868 2868 for kind in changes:
2869 2869 for abs in kind:
2870 2870 names[abs] = m.exact(abs)
2871 2871
2872 2872 m = scmutil.matchfiles(repo, names)
2873 2873
2874 2874 modified = set(changes.modified)
2875 2875 added = set(changes.added)
2876 2876 removed = set(changes.removed)
2877 2877 _deleted = set(changes.deleted)
2878 2878 unknown = set(changes.unknown)
2879 2879 unknown.update(changes.ignored)
2880 2880 clean = set(changes.clean)
2881 2881 modadded = set()
2882 2882
2883 2883 # We need to account for the state of the file in the dirstate,
2884 2884 # even when we revert against something else than parent. This will
2885 2885 # slightly alter the behavior of revert (doing back up or not, delete
2886 2886 # or just forget etc).
2887 2887 if parent == node:
2888 2888 dsmodified = modified
2889 2889 dsadded = added
2890 2890 dsremoved = removed
2891 2891 # store all local modifications, useful later for rename detection
2892 2892 localchanges = dsmodified | dsadded
2893 2893 modified, added, removed = set(), set(), set()
2894 2894 else:
2895 2895 changes = repo.status(node1=parent, match=m)
2896 2896 dsmodified = set(changes.modified)
2897 2897 dsadded = set(changes.added)
2898 2898 dsremoved = set(changes.removed)
2899 2899 # store all local modifications, useful later for rename detection
2900 2900 localchanges = dsmodified | dsadded
2901 2901
2902 2902 # only take into account for removes between wc and target
2903 2903 clean |= dsremoved - removed
2904 2904 dsremoved &= removed
2905 2905 # distinct between dirstate remove and other
2906 2906 removed -= dsremoved
2907 2907
2908 2908 modadded = added & dsmodified
2909 2909 added -= modadded
2910 2910
2911 2911 # tell newly modified apart.
2912 2912 dsmodified &= modified
2913 2913 dsmodified |= modified & dsadded # dirstate added may need backup
2914 2914 modified -= dsmodified
2915 2915
2916 2916 # We need to wait for some post-processing to update this set
2917 2917 # before making the distinction. The dirstate will be used for
2918 2918 # that purpose.
2919 2919 dsadded = added
2920 2920
2921 2921 # in case of merge, files that are actually added can be reported as
2922 2922 # modified, we need to post process the result
2923 2923 if p2 != nullid:
2924 2924 mergeadd = set(dsmodified)
2925 2925 for path in dsmodified:
2926 2926 if path in mf:
2927 2927 mergeadd.remove(path)
2928 2928 dsadded |= mergeadd
2929 2929 dsmodified -= mergeadd
2930 2930
2931 2931 # if f is a rename, update `names` to also revert the source
2932 2932 for f in localchanges:
2933 2933 src = repo.dirstate.copied(f)
2934 2934 # XXX should we check for rename down to target node?
2935 2935 if src and src not in names and repo.dirstate[src] == 'r':
2936 2936 dsremoved.add(src)
2937 2937 names[src] = True
2938 2938
2939 2939 # determine the exact nature of the deleted changesets
2940 2940 deladded = set(_deleted)
2941 2941 for path in _deleted:
2942 2942 if path in mf:
2943 2943 deladded.remove(path)
2944 2944 deleted = _deleted - deladded
2945 2945
2946 2946 # distinguish between file to forget and the other
2947 2947 added = set()
2948 2948 for abs in dsadded:
2949 2949 if repo.dirstate[abs] != 'a':
2950 2950 added.add(abs)
2951 2951 dsadded -= added
2952 2952
2953 2953 for abs in deladded:
2954 2954 if repo.dirstate[abs] == 'a':
2955 2955 dsadded.add(abs)
2956 2956 deladded -= dsadded
2957 2957
2958 2958 # For files marked as removed, we check if an unknown file is present at
2959 2959 # the same path. If a such file exists it may need to be backed up.
2960 2960 # Making the distinction at this stage helps have simpler backup
2961 2961 # logic.
2962 2962 removunk = set()
2963 2963 for abs in removed:
2964 2964 target = repo.wjoin(abs)
2965 2965 if os.path.lexists(target):
2966 2966 removunk.add(abs)
2967 2967 removed -= removunk
2968 2968
2969 2969 dsremovunk = set()
2970 2970 for abs in dsremoved:
2971 2971 target = repo.wjoin(abs)
2972 2972 if os.path.lexists(target):
2973 2973 dsremovunk.add(abs)
2974 2974 dsremoved -= dsremovunk
2975 2975
2976 2976 # action to be actually performed by revert
2977 2977 # (<list of file>, message>) tuple
2978 2978 actions = {'revert': ([], _('reverting %s\n')),
2979 2979 'add': ([], _('adding %s\n')),
2980 2980 'remove': ([], _('removing %s\n')),
2981 2981 'drop': ([], _('removing %s\n')),
2982 2982 'forget': ([], _('forgetting %s\n')),
2983 2983 'undelete': ([], _('undeleting %s\n')),
2984 2984 'noop': (None, _('no changes needed to %s\n')),
2985 2985 'unknown': (None, _('file not managed: %s\n')),
2986 2986 }
2987 2987
2988 2988 # "constant" that convey the backup strategy.
2989 2989 # All set to `discard` if `no-backup` is set do avoid checking
2990 2990 # no_backup lower in the code.
2991 2991 # These values are ordered for comparison purposes
2992 2992 backupinteractive = 3 # do backup if interactively modified
2993 2993 backup = 2 # unconditionally do backup
2994 2994 check = 1 # check if the existing file differs from target
2995 2995 discard = 0 # never do backup
2996 2996 if opts.get('no_backup'):
2997 2997 backupinteractive = backup = check = discard
2998 2998 if interactive:
2999 2999 dsmodifiedbackup = backupinteractive
3000 3000 else:
3001 3001 dsmodifiedbackup = backup
3002 3002 tobackup = set()
3003 3003
3004 3004 backupanddel = actions['remove']
3005 3005 if not opts.get('no_backup'):
3006 3006 backupanddel = actions['drop']
3007 3007
3008 3008 disptable = (
3009 3009 # dispatch table:
3010 3010 # file state
3011 3011 # action
3012 3012 # make backup
3013 3013
3014 3014 ## Sets that results that will change file on disk
3015 3015 # Modified compared to target, no local change
3016 3016 (modified, actions['revert'], discard),
3017 3017 # Modified compared to target, but local file is deleted
3018 3018 (deleted, actions['revert'], discard),
3019 3019 # Modified compared to target, local change
3020 3020 (dsmodified, actions['revert'], dsmodifiedbackup),
3021 3021 # Added since target
3022 3022 (added, actions['remove'], discard),
3023 3023 # Added in working directory
3024 3024 (dsadded, actions['forget'], discard),
3025 3025 # Added since target, have local modification
3026 3026 (modadded, backupanddel, backup),
3027 3027 # Added since target but file is missing in working directory
3028 3028 (deladded, actions['drop'], discard),
3029 3029 # Removed since target, before working copy parent
3030 3030 (removed, actions['add'], discard),
3031 3031 # Same as `removed` but an unknown file exists at the same path
3032 3032 (removunk, actions['add'], check),
3033 3033 # Removed since targe, marked as such in working copy parent
3034 3034 (dsremoved, actions['undelete'], discard),
3035 3035 # Same as `dsremoved` but an unknown file exists at the same path
3036 3036 (dsremovunk, actions['undelete'], check),
3037 3037 ## the following sets does not result in any file changes
3038 3038 # File with no modification
3039 3039 (clean, actions['noop'], discard),
3040 3040 # Existing file, not tracked anywhere
3041 3041 (unknown, actions['unknown'], discard),
3042 3042 )
3043 3043
3044 3044 for abs, exact in sorted(names.items()):
3045 3045 # target file to be touch on disk (relative to cwd)
3046 3046 target = repo.wjoin(abs)
3047 3047 # search the entry in the dispatch table.
3048 3048 # if the file is in any of these sets, it was touched in the working
3049 3049 # directory parent and we are sure it needs to be reverted.
3050 3050 for table, (xlist, msg), dobackup in disptable:
3051 3051 if abs not in table:
3052 3052 continue
3053 3053 if xlist is not None:
3054 3054 xlist.append(abs)
3055 3055 if dobackup:
3056 3056 # If in interactive mode, don't automatically create
3057 3057 # .orig files (issue4793)
3058 3058 if dobackup == backupinteractive:
3059 3059 tobackup.add(abs)
3060 3060 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3061 3061 absbakname = scmutil.backuppath(ui, repo, abs)
3062 3062 bakname = os.path.relpath(absbakname,
3063 3063 start=repo.root)
3064 3064 ui.note(_('saving current version of %s as %s\n') %
3065 3065 (uipathfn(abs), uipathfn(bakname)))
3066 3066 if not opts.get('dry_run'):
3067 3067 if interactive:
3068 3068 util.copyfile(target, absbakname)
3069 3069 else:
3070 3070 util.rename(target, absbakname)
3071 3071 if opts.get('dry_run'):
3072 3072 if ui.verbose or not exact:
3073 3073 ui.status(msg % uipathfn(abs))
3074 3074 elif exact:
3075 3075 ui.warn(msg % uipathfn(abs))
3076 3076 break
3077 3077
3078 3078 if not opts.get('dry_run'):
3079 3079 needdata = ('revert', 'add', 'undelete')
3080 3080 oplist = [actions[name][0] for name in needdata]
3081 3081 prefetch = scmutil.prefetchfiles
3082 3082 matchfiles = scmutil.matchfiles
3083 3083 prefetch(repo, [ctx.rev()],
3084 3084 matchfiles(repo,
3085 3085 [f for sublist in oplist for f in sublist]))
3086 3086 match = scmutil.match(repo[None], pats)
3087 3087 _performrevert(repo, parents, ctx, names, uipathfn, actions,
3088 3088 match, interactive, tobackup)
3089 3089
3090 3090 if targetsubs:
3091 3091 # Revert the subrepos on the revert list
3092 3092 for sub in targetsubs:
3093 3093 try:
3094 3094 wctx.sub(sub).revert(ctx.substate[sub], *pats,
3095 3095 **pycompat.strkwargs(opts))
3096 3096 except KeyError:
3097 3097 raise error.Abort("subrepository '%s' does not exist in %s!"
3098 3098 % (sub, short(ctx.node())))
3099 3099
3100 3100 def _performrevert(repo, parents, ctx, names, uipathfn, actions,
3101 3101 match, interactive=False, tobackup=None):
3102 3102 """function that actually perform all the actions computed for revert
3103 3103
3104 3104 This is an independent function to let extension to plug in and react to
3105 3105 the imminent revert.
3106 3106
3107 3107 Make sure you have the working directory locked when calling this function.
3108 3108 """
3109 3109 parent, p2 = parents
3110 3110 node = ctx.node()
3111 3111 excluded_files = []
3112 3112
3113 3113 def checkout(f):
3114 3114 fc = ctx[f]
3115 3115 repo.wwrite(f, fc.data(), fc.flags())
3116 3116
3117 3117 def doremove(f):
3118 3118 try:
3119 3119 rmdir = repo.ui.configbool('experimental', 'removeemptydirs')
3120 3120 repo.wvfs.unlinkpath(f, rmdir=rmdir)
3121 3121 except OSError:
3122 3122 pass
3123 3123 repo.dirstate.remove(f)
3124 3124
3125 3125 def prntstatusmsg(action, f):
3126 3126 exact = names[f]
3127 3127 if repo.ui.verbose or not exact:
3128 3128 repo.ui.status(actions[action][1] % uipathfn(f))
3129 3129
3130 3130 audit_path = pathutil.pathauditor(repo.root, cached=True)
3131 3131 for f in actions['forget'][0]:
3132 3132 if interactive:
3133 3133 choice = repo.ui.promptchoice(
3134 3134 _("forget added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f))
3135 3135 if choice == 0:
3136 3136 prntstatusmsg('forget', f)
3137 3137 repo.dirstate.drop(f)
3138 3138 else:
3139 3139 excluded_files.append(f)
3140 3140 else:
3141 3141 prntstatusmsg('forget', f)
3142 3142 repo.dirstate.drop(f)
3143 3143 for f in actions['remove'][0]:
3144 3144 audit_path(f)
3145 3145 if interactive:
3146 3146 choice = repo.ui.promptchoice(
3147 3147 _("remove added file %s (Yn)?$$ &Yes $$ &No") % uipathfn(f))
3148 3148 if choice == 0:
3149 3149 prntstatusmsg('remove', f)
3150 3150 doremove(f)
3151 3151 else:
3152 3152 excluded_files.append(f)
3153 3153 else:
3154 3154 prntstatusmsg('remove', f)
3155 3155 doremove(f)
3156 3156 for f in actions['drop'][0]:
3157 3157 audit_path(f)
3158 3158 prntstatusmsg('drop', f)
3159 3159 repo.dirstate.remove(f)
3160 3160
3161 3161 normal = None
3162 3162 if node == parent:
3163 3163 # We're reverting to our parent. If possible, we'd like status
3164 3164 # to report the file as clean. We have to use normallookup for
3165 3165 # merges to avoid losing information about merged/dirty files.
3166 3166 if p2 != nullid:
3167 3167 normal = repo.dirstate.normallookup
3168 3168 else:
3169 3169 normal = repo.dirstate.normal
3170 3170
3171 3171 newlyaddedandmodifiedfiles = set()
3172 3172 if interactive:
3173 3173 # Prompt the user for changes to revert
3174 3174 torevert = [f for f in actions['revert'][0] if f not in excluded_files]
3175 3175 m = scmutil.matchfiles(repo, torevert)
3176 3176 diffopts = patch.difffeatureopts(repo.ui, whitespace=True,
3177 3177 section='commands',
3178 3178 configprefix='revert.interactive.')
3179 3179 diffopts.nodates = True
3180 3180 diffopts.git = True
3181 3181 operation = 'apply'
3182 3182 if node == parent:
3183 3183 if repo.ui.configbool('experimental',
3184 3184 'revert.interactive.select-to-keep'):
3185 3185 operation = 'keep'
3186 3186 else:
3187 3187 operation = 'discard'
3188 3188
3189 3189 if operation == 'apply':
3190 3190 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3191 3191 else:
3192 3192 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3193 3193 originalchunks = patch.parsepatch(diff)
3194 3194
3195 3195 try:
3196 3196
3197 3197 chunks, opts = recordfilter(repo.ui, originalchunks, match,
3198 3198 operation=operation)
3199 3199 if operation == 'discard':
3200 3200 chunks = patch.reversehunks(chunks)
3201 3201
3202 3202 except error.PatchError as err:
3203 3203 raise error.Abort(_('error parsing patch: %s') % err)
3204 3204
3205 3205 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3206 3206 if tobackup is None:
3207 3207 tobackup = set()
3208 3208 # Apply changes
3209 3209 fp = stringio()
3210 3210 # chunks are serialized per file, but files aren't sorted
3211 3211 for f in sorted(set(c.header.filename() for c in chunks if ishunk(c))):
3212 3212 prntstatusmsg('revert', f)
3213 3213 files = set()
3214 3214 for c in chunks:
3215 3215 if ishunk(c):
3216 3216 abs = c.header.filename()
3217 3217 # Create a backup file only if this hunk should be backed up
3218 3218 if c.header.filename() in tobackup:
3219 3219 target = repo.wjoin(abs)
3220 3220 bakname = scmutil.backuppath(repo.ui, repo, abs)
3221 3221 util.copyfile(target, bakname)
3222 3222 tobackup.remove(abs)
3223 3223 if abs not in files:
3224 3224 files.add(abs)
3225 3225 if operation == 'keep':
3226 3226 checkout(abs)
3227 3227 c.write(fp)
3228 3228 dopatch = fp.tell()
3229 3229 fp.seek(0)
3230 3230 if dopatch:
3231 3231 try:
3232 3232 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3233 3233 except error.PatchError as err:
3234 3234 raise error.Abort(pycompat.bytestr(err))
3235 3235 del fp
3236 3236 else:
3237 3237 for f in actions['revert'][0]:
3238 3238 prntstatusmsg('revert', f)
3239 3239 checkout(f)
3240 3240 if normal:
3241 3241 normal(f)
3242 3242
3243 3243 for f in actions['add'][0]:
3244 3244 # Don't checkout modified files, they are already created by the diff
3245 3245 if f not in newlyaddedandmodifiedfiles:
3246 3246 prntstatusmsg('add', f)
3247 3247 checkout(f)
3248 3248 repo.dirstate.add(f)
3249 3249
3250 3250 normal = repo.dirstate.normallookup
3251 3251 if node == parent and p2 == nullid:
3252 3252 normal = repo.dirstate.normal
3253 3253 for f in actions['undelete'][0]:
3254 3254 if interactive:
3255 3255 choice = repo.ui.promptchoice(
3256 3256 _("add back removed file %s (Yn)?$$ &Yes $$ &No") % f)
3257 3257 if choice == 0:
3258 3258 prntstatusmsg('undelete', f)
3259 3259 checkout(f)
3260 3260 normal(f)
3261 3261 else:
3262 3262 excluded_files.append(f)
3263 3263 else:
3264 3264 prntstatusmsg('undelete', f)
3265 3265 checkout(f)
3266 3266 normal(f)
3267 3267
3268 3268 copied = copies.pathcopies(repo[parent], ctx)
3269 3269
3270 3270 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3271 3271 if f in copied:
3272 3272 repo.dirstate.copy(copied[f], f)
3273 3273
3274 3274 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3275 3275 # commands.outgoing. "missing" is "missing" of the result of
3276 3276 # "findcommonoutgoing()"
3277 3277 outgoinghooks = util.hooks()
3278 3278
3279 3279 # a list of (ui, repo) functions called by commands.summary
3280 3280 summaryhooks = util.hooks()
3281 3281
3282 3282 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3283 3283 #
3284 3284 # functions should return tuple of booleans below, if 'changes' is None:
3285 3285 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3286 3286 #
3287 3287 # otherwise, 'changes' is a tuple of tuples below:
3288 3288 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3289 3289 # - (desturl, destbranch, destpeer, outgoing)
3290 3290 summaryremotehooks = util.hooks()
3291 3291
3292 3292 # A list of state files kept by multistep operations like graft.
3293 3293 # Since graft cannot be aborted, it is considered 'clearable' by update.
3294 3294 # note: bisect is intentionally excluded
3295 3295 # (state file, clearable, allowcommit, error, hint)
3296 3296 unfinishedstates = [
3297 3297 ('graftstate', True, False, _('graft in progress'),
3298 3298 _("use 'hg graft --continue' or 'hg graft --stop' to stop")),
3299 3299 ('updatestate', True, False, _('last update was interrupted'),
3300 3300 _("use 'hg update' to get a consistent checkout"))
3301 3301 ]
3302 3302
3303 3303 def checkunfinished(repo, commit=False):
3304 3304 '''Look for an unfinished multistep operation, like graft, and abort
3305 3305 if found. It's probably good to check this right before
3306 3306 bailifchanged().
3307 3307 '''
3308 3308 # Check for non-clearable states first, so things like rebase will take
3309 3309 # precedence over update.
3310 3310 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3311 3311 if clearable or (commit and allowcommit):
3312 3312 continue
3313 3313 if repo.vfs.exists(f):
3314 3314 raise error.Abort(msg, hint=hint)
3315 3315
3316 3316 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3317 3317 if not clearable or (commit and allowcommit):
3318 3318 continue
3319 3319 if repo.vfs.exists(f):
3320 3320 raise error.Abort(msg, hint=hint)
3321 3321
3322 3322 def clearunfinished(repo):
3323 3323 '''Check for unfinished operations (as above), and clear the ones
3324 3324 that are clearable.
3325 3325 '''
3326 3326 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3327 3327 if not clearable and repo.vfs.exists(f):
3328 3328 raise error.Abort(msg, hint=hint)
3329 3329 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3330 3330 if clearable and repo.vfs.exists(f):
3331 3331 util.unlink(repo.vfs.join(f))
3332 3332
3333 3333 afterresolvedstates = [
3334 3334 ('graftstate',
3335 3335 _('hg graft --continue')),
3336 3336 ]
3337 3337
3338 3338 def howtocontinue(repo):
3339 3339 '''Check for an unfinished operation and return the command to finish
3340 3340 it.
3341 3341
3342 3342 afterresolvedstates tuples define a .hg/{file} and the corresponding
3343 3343 command needed to finish it.
3344 3344
3345 3345 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3346 3346 a boolean.
3347 3347 '''
3348 3348 contmsg = _("continue: %s")
3349 3349 for f, msg in afterresolvedstates:
3350 3350 if repo.vfs.exists(f):
3351 3351 return contmsg % msg, True
3352 3352 if repo[None].dirty(missing=True, merge=False, branch=False):
3353 3353 return contmsg % _("hg commit"), False
3354 3354 return None, None
3355 3355
3356 3356 def checkafterresolved(repo):
3357 3357 '''Inform the user about the next action after completing hg resolve
3358 3358
3359 3359 If there's a matching afterresolvedstates, howtocontinue will yield
3360 3360 repo.ui.warn as the reporter.
3361 3361
3362 3362 Otherwise, it will yield repo.ui.note.
3363 3363 '''
3364 3364 msg, warning = howtocontinue(repo)
3365 3365 if msg is not None:
3366 3366 if warning:
3367 3367 repo.ui.warn("%s\n" % msg)
3368 3368 else:
3369 3369 repo.ui.note("%s\n" % msg)
3370 3370
3371 3371 def wrongtooltocontinue(repo, task):
3372 3372 '''Raise an abort suggesting how to properly continue if there is an
3373 3373 active task.
3374 3374
3375 3375 Uses howtocontinue() to find the active task.
3376 3376
3377 3377 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3378 3378 a hint.
3379 3379 '''
3380 3380 after = howtocontinue(repo)
3381 3381 hint = None
3382 3382 if after[1]:
3383 3383 hint = after[0]
3384 3384 raise error.Abort(_('no %s in progress') % task, hint=hint)
@@ -1,698 +1,699 b''
1 1 # parser.py - simple top-down operator precedence parser for mercurial
2 2 #
3 3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # see http://effbot.org/zone/simple-top-down-parsing.htm and
9 9 # http://eli.thegreenplace.net/2010/01/02/top-down-operator-precedence-parsing/
10 10 # for background
11 11
12 12 # takes a tokenizer and elements
13 13 # tokenizer is an iterator that returns (type, value, pos) tuples
14 14 # elements is a mapping of types to binding strength, primary, prefix, infix
15 15 # and suffix actions
16 16 # an action is a tree node name, a tree label, and an optional match
17 17 # __call__(program) parses program into a labeled tree
18 18
19 19 from __future__ import absolute_import, print_function
20 20
21 21 from .i18n import _
22 22 from . import (
23 23 error,
24 24 pycompat,
25 25 util,
26 26 )
27 27 from .utils import (
28 28 stringutil,
29 29 )
30 30
31 31 class parser(object):
32 32 def __init__(self, elements, methods=None):
33 33 self._elements = elements
34 34 self._methods = methods
35 35 self.current = None
36 36 def _advance(self):
37 37 'advance the tokenizer'
38 38 t = self.current
39 39 self.current = next(self._iter, None)
40 40 return t
41 41 def _hasnewterm(self):
42 42 'True if next token may start new term'
43 43 return any(self._elements[self.current[0]][1:3])
44 44 def _match(self, m):
45 45 'make sure the tokenizer matches an end condition'
46 46 if self.current[0] != m:
47 47 raise error.ParseError(_("unexpected token: %s") % self.current[0],
48 48 self.current[2])
49 49 self._advance()
50 50 def _parseoperand(self, bind, m=None):
51 51 'gather right-hand-side operand until an end condition or binding met'
52 52 if m and self.current[0] == m:
53 53 expr = None
54 54 else:
55 55 expr = self._parse(bind)
56 56 if m:
57 57 self._match(m)
58 58 return expr
59 59 def _parse(self, bind=0):
60 60 token, value, pos = self._advance()
61 61 # handle prefix rules on current token, take as primary if unambiguous
62 62 primary, prefix = self._elements[token][1:3]
63 63 if primary and not (prefix and self._hasnewterm()):
64 64 expr = (primary, value)
65 65 elif prefix:
66 66 expr = (prefix[0], self._parseoperand(*prefix[1:]))
67 67 else:
68 68 raise error.ParseError(_("not a prefix: %s") % token, pos)
69 69 # gather tokens until we meet a lower binding strength
70 70 while bind < self._elements[self.current[0]][0]:
71 71 token, value, pos = self._advance()
72 72 # handle infix rules, take as suffix if unambiguous
73 73 infix, suffix = self._elements[token][3:]
74 74 if suffix and not (infix and self._hasnewterm()):
75 75 expr = (suffix, expr)
76 76 elif infix:
77 77 expr = (infix[0], expr, self._parseoperand(*infix[1:]))
78 78 else:
79 79 raise error.ParseError(_("not an infix: %s") % token, pos)
80 80 return expr
81 81 def parse(self, tokeniter):
82 82 'generate a parse tree from tokens'
83 83 self._iter = tokeniter
84 84 self._advance()
85 85 res = self._parse()
86 86 token, value, pos = self.current
87 87 return res, pos
88 88 def eval(self, tree):
89 89 'recursively evaluate a parse tree using node methods'
90 90 if not isinstance(tree, tuple):
91 91 return tree
92 92 return self._methods[tree[0]](*[self.eval(t) for t in tree[1:]])
93 93 def __call__(self, tokeniter):
94 94 'parse tokens into a parse tree and evaluate if methods given'
95 95 t = self.parse(tokeniter)
96 96 if self._methods:
97 97 return self.eval(t)
98 98 return t
99 99
100 100 def splitargspec(spec):
101 101 """Parse spec of function arguments into (poskeys, varkey, keys, optkey)
102 102
103 103 >>> splitargspec(b'')
104 104 ([], None, [], None)
105 105 >>> splitargspec(b'foo bar')
106 106 ([], None, ['foo', 'bar'], None)
107 107 >>> splitargspec(b'foo *bar baz **qux')
108 108 (['foo'], 'bar', ['baz'], 'qux')
109 109 >>> splitargspec(b'*foo')
110 110 ([], 'foo', [], None)
111 111 >>> splitargspec(b'**foo')
112 112 ([], None, [], 'foo')
113 113 """
114 114 optkey = None
115 115 pre, sep, post = spec.partition('**')
116 116 if sep:
117 117 posts = post.split()
118 118 if not posts:
119 119 raise error.ProgrammingError('no **optkey name provided')
120 120 if len(posts) > 1:
121 121 raise error.ProgrammingError('excessive **optkey names provided')
122 122 optkey = posts[0]
123 123
124 124 pre, sep, post = pre.partition('*')
125 125 pres = pre.split()
126 126 posts = post.split()
127 127 if sep:
128 128 if not posts:
129 129 raise error.ProgrammingError('no *varkey name provided')
130 130 return pres, posts[0], posts[1:], optkey
131 131 return [], None, pres, optkey
132 132
133 133 def buildargsdict(trees, funcname, argspec, keyvaluenode, keynode):
134 134 """Build dict from list containing positional and keyword arguments
135 135
136 136 Arguments are specified by a tuple of ``(poskeys, varkey, keys, optkey)``
137 137 where
138 138
139 139 - ``poskeys``: list of names of positional arguments
140 140 - ``varkey``: optional argument name that takes up remainder
141 141 - ``keys``: list of names that can be either positional or keyword arguments
142 142 - ``optkey``: optional argument name that takes up excess keyword arguments
143 143
144 144 If ``varkey`` specified, all ``keys`` must be given as keyword arguments.
145 145
146 146 Invalid keywords, too few positional arguments, or too many positional
147 147 arguments are rejected, but missing keyword arguments are just omitted.
148 148 """
149 149 poskeys, varkey, keys, optkey = argspec
150 kwstart = next((i for i, x in enumerate(trees) if x[0] == keyvaluenode),
150 kwstart = next((i for i, x in enumerate(trees)
151 if x and x[0] == keyvaluenode),
151 152 len(trees))
152 153 if kwstart < len(poskeys):
153 154 raise error.ParseError(_("%(func)s takes at least %(nargs)d positional "
154 155 "arguments")
155 156 % {'func': funcname, 'nargs': len(poskeys)})
156 157 if not varkey and kwstart > len(poskeys) + len(keys):
157 158 raise error.ParseError(_("%(func)s takes at most %(nargs)d positional "
158 159 "arguments")
159 160 % {'func': funcname,
160 161 'nargs': len(poskeys) + len(keys)})
161 162 args = util.sortdict()
162 163 # consume positional arguments
163 164 for k, x in zip(poskeys, trees[:kwstart]):
164 165 args[k] = x
165 166 if varkey:
166 167 args[varkey] = trees[len(args):kwstart]
167 168 else:
168 169 for k, x in zip(keys, trees[len(args):kwstart]):
169 170 args[k] = x
170 171 # remainder should be keyword arguments
171 172 if optkey:
172 173 args[optkey] = util.sortdict()
173 174 for x in trees[kwstart:]:
174 if x[0] != keyvaluenode or x[1][0] != keynode:
175 if not x or x[0] != keyvaluenode or x[1][0] != keynode:
175 176 raise error.ParseError(_("%(func)s got an invalid argument")
176 177 % {'func': funcname})
177 178 k = x[1][1]
178 179 if k in keys:
179 180 d = args
180 181 elif not optkey:
181 182 raise error.ParseError(_("%(func)s got an unexpected keyword "
182 183 "argument '%(key)s'")
183 184 % {'func': funcname, 'key': k})
184 185 else:
185 186 d = args[optkey]
186 187 if k in d:
187 188 raise error.ParseError(_("%(func)s got multiple values for keyword "
188 189 "argument '%(key)s'")
189 190 % {'func': funcname, 'key': k})
190 191 d[k] = x[2]
191 192 return args
192 193
193 194 def unescapestr(s):
194 195 try:
195 196 return stringutil.unescapestr(s)
196 197 except ValueError as e:
197 198 # mangle Python's exception into our format
198 199 raise error.ParseError(pycompat.bytestr(e).lower())
199 200
200 201 def _prettyformat(tree, leafnodes, level, lines):
201 202 if not isinstance(tree, tuple):
202 203 lines.append((level, stringutil.pprint(tree)))
203 204 elif tree[0] in leafnodes:
204 205 rs = map(stringutil.pprint, tree[1:])
205 206 lines.append((level, '(%s %s)' % (tree[0], ' '.join(rs))))
206 207 else:
207 208 lines.append((level, '(%s' % tree[0]))
208 209 for s in tree[1:]:
209 210 _prettyformat(s, leafnodes, level + 1, lines)
210 211 lines[-1:] = [(lines[-1][0], lines[-1][1] + ')')]
211 212
212 213 def prettyformat(tree, leafnodes):
213 214 lines = []
214 215 _prettyformat(tree, leafnodes, 0, lines)
215 216 output = '\n'.join((' ' * l + s) for l, s in lines)
216 217 return output
217 218
218 219 def simplifyinfixops(tree, targetnodes):
219 220 """Flatten chained infix operations to reduce usage of Python stack
220 221
221 222 >>> from . import pycompat
222 223 >>> def f(tree):
223 224 ... s = prettyformat(simplifyinfixops(tree, (b'or',)), (b'symbol',))
224 225 ... print(pycompat.sysstr(s))
225 226 >>> f((b'or',
226 227 ... (b'or',
227 228 ... (b'symbol', b'1'),
228 229 ... (b'symbol', b'2')),
229 230 ... (b'symbol', b'3')))
230 231 (or
231 232 (symbol '1')
232 233 (symbol '2')
233 234 (symbol '3'))
234 235 >>> f((b'func',
235 236 ... (b'symbol', b'p1'),
236 237 ... (b'or',
237 238 ... (b'or',
238 239 ... (b'func',
239 240 ... (b'symbol', b'sort'),
240 241 ... (b'list',
241 242 ... (b'or',
242 243 ... (b'or',
243 244 ... (b'symbol', b'1'),
244 245 ... (b'symbol', b'2')),
245 246 ... (b'symbol', b'3')),
246 247 ... (b'negate',
247 248 ... (b'symbol', b'rev')))),
248 249 ... (b'and',
249 250 ... (b'symbol', b'4'),
250 251 ... (b'group',
251 252 ... (b'or',
252 253 ... (b'or',
253 254 ... (b'symbol', b'5'),
254 255 ... (b'symbol', b'6')),
255 256 ... (b'symbol', b'7'))))),
256 257 ... (b'symbol', b'8'))))
257 258 (func
258 259 (symbol 'p1')
259 260 (or
260 261 (func
261 262 (symbol 'sort')
262 263 (list
263 264 (or
264 265 (symbol '1')
265 266 (symbol '2')
266 267 (symbol '3'))
267 268 (negate
268 269 (symbol 'rev'))))
269 270 (and
270 271 (symbol '4')
271 272 (group
272 273 (or
273 274 (symbol '5')
274 275 (symbol '6')
275 276 (symbol '7'))))
276 277 (symbol '8')))
277 278 """
278 279 if not isinstance(tree, tuple):
279 280 return tree
280 281 op = tree[0]
281 282 if op not in targetnodes:
282 283 return (op,) + tuple(simplifyinfixops(x, targetnodes) for x in tree[1:])
283 284
284 285 # walk down left nodes taking each right node. no recursion to left nodes
285 286 # because infix operators are left-associative, i.e. left tree is deep.
286 287 # e.g. '1 + 2 + 3' -> (+ (+ 1 2) 3) -> (+ 1 2 3)
287 288 simplified = []
288 289 x = tree
289 290 while x[0] == op:
290 291 l, r = x[1:]
291 292 simplified.append(simplifyinfixops(r, targetnodes))
292 293 x = l
293 294 simplified.append(simplifyinfixops(x, targetnodes))
294 295 simplified.append(op)
295 296 return tuple(reversed(simplified))
296 297
297 298 def _buildtree(template, placeholder, replstack):
298 299 if template == placeholder:
299 300 return replstack.pop()
300 301 if not isinstance(template, tuple):
301 302 return template
302 303 return tuple(_buildtree(x, placeholder, replstack) for x in template)
303 304
304 305 def buildtree(template, placeholder, *repls):
305 306 """Create new tree by substituting placeholders by replacements
306 307
307 308 >>> _ = (b'symbol', b'_')
308 309 >>> def f(template, *repls):
309 310 ... return buildtree(template, _, *repls)
310 311 >>> f((b'func', (b'symbol', b'only'), (b'list', _, _)),
311 312 ... ('symbol', '1'), ('symbol', '2'))
312 313 ('func', ('symbol', 'only'), ('list', ('symbol', '1'), ('symbol', '2')))
313 314 >>> f((b'and', _, (b'not', _)), (b'symbol', b'1'), (b'symbol', b'2'))
314 315 ('and', ('symbol', '1'), ('not', ('symbol', '2')))
315 316 """
316 317 if not isinstance(placeholder, tuple):
317 318 raise error.ProgrammingError('placeholder must be a node tuple')
318 319 replstack = list(reversed(repls))
319 320 r = _buildtree(template, placeholder, replstack)
320 321 if replstack:
321 322 raise error.ProgrammingError('too many replacements')
322 323 return r
323 324
324 325 def _matchtree(pattern, tree, placeholder, incompletenodes, matches):
325 326 if pattern == tree:
326 327 return True
327 328 if not isinstance(pattern, tuple) or not isinstance(tree, tuple):
328 329 return False
329 330 if pattern == placeholder and tree[0] not in incompletenodes:
330 331 matches.append(tree)
331 332 return True
332 333 if len(pattern) != len(tree):
333 334 return False
334 335 return all(_matchtree(p, x, placeholder, incompletenodes, matches)
335 336 for p, x in zip(pattern, tree))
336 337
337 338 def matchtree(pattern, tree, placeholder=None, incompletenodes=()):
338 339 """If a tree matches the pattern, return a list of the tree and nodes
339 340 matched with the placeholder; Otherwise None
340 341
341 342 >>> def f(pattern, tree):
342 343 ... m = matchtree(pattern, tree, _, {b'keyvalue', b'list'})
343 344 ... if m:
344 345 ... return m[1:]
345 346
346 347 >>> _ = (b'symbol', b'_')
347 348 >>> f((b'func', (b'symbol', b'ancestors'), _),
348 349 ... (b'func', (b'symbol', b'ancestors'), (b'symbol', b'1')))
349 350 [('symbol', '1')]
350 351 >>> f((b'func', (b'symbol', b'ancestors'), _),
351 352 ... (b'func', (b'symbol', b'ancestors'), None))
352 353 >>> f((b'range', (b'dagrange', _, _), _),
353 354 ... (b'range',
354 355 ... (b'dagrange', (b'symbol', b'1'), (b'symbol', b'2')),
355 356 ... (b'symbol', b'3')))
356 357 [('symbol', '1'), ('symbol', '2'), ('symbol', '3')]
357 358
358 359 The placeholder does not match the specified incomplete nodes because
359 360 an incomplete node (e.g. argument list) cannot construct an expression.
360 361
361 362 >>> f((b'func', (b'symbol', b'ancestors'), _),
362 363 ... (b'func', (b'symbol', b'ancestors'),
363 364 ... (b'list', (b'symbol', b'1'), (b'symbol', b'2'))))
364 365
365 366 The placeholder may be omitted, but which shouldn't match a None node.
366 367
367 368 >>> _ = None
368 369 >>> f((b'func', (b'symbol', b'ancestors'), None),
369 370 ... (b'func', (b'symbol', b'ancestors'), (b'symbol', b'0')))
370 371 """
371 372 if placeholder is not None and not isinstance(placeholder, tuple):
372 373 raise error.ProgrammingError('placeholder must be a node tuple')
373 374 matches = [tree]
374 375 if _matchtree(pattern, tree, placeholder, incompletenodes, matches):
375 376 return matches
376 377
377 378 def parseerrordetail(inst):
378 379 """Compose error message from specified ParseError object
379 380 """
380 381 if len(inst.args) > 1:
381 382 return _('at %d: %s') % (inst.args[1], inst.args[0])
382 383 else:
383 384 return inst.args[0]
384 385
385 386 class alias(object):
386 387 """Parsed result of alias"""
387 388
388 389 def __init__(self, name, args, err, replacement):
389 390 self.name = name
390 391 self.args = args
391 392 self.error = err
392 393 self.replacement = replacement
393 394 # whether own `error` information is already shown or not.
394 395 # this avoids showing same warning multiple times at each
395 396 # `expandaliases`.
396 397 self.warned = False
397 398
398 399 class basealiasrules(object):
399 400 """Parsing and expansion rule set of aliases
400 401
401 402 This is a helper for fileset/revset/template aliases. A concrete rule set
402 403 should be made by sub-classing this and implementing class/static methods.
403 404
404 405 It supports alias expansion of symbol and function-call styles::
405 406
406 407 # decl = defn
407 408 h = heads(default)
408 409 b($1) = ancestors($1) - ancestors(default)
409 410 """
410 411 # typically a config section, which will be included in error messages
411 412 _section = None
412 413 # tag of symbol node
413 414 _symbolnode = 'symbol'
414 415
415 416 def __new__(cls):
416 417 raise TypeError("'%s' is not instantiatable" % cls.__name__)
417 418
418 419 @staticmethod
419 420 def _parse(spec):
420 421 """Parse an alias name, arguments and definition"""
421 422 raise NotImplementedError
422 423
423 424 @staticmethod
424 425 def _trygetfunc(tree):
425 426 """Return (name, args) if tree is a function; otherwise None"""
426 427 raise NotImplementedError
427 428
428 429 @classmethod
429 430 def _builddecl(cls, decl):
430 431 """Parse an alias declaration into ``(name, args, errorstr)``
431 432
432 433 This function analyzes the parsed tree. The parsing rule is provided
433 434 by ``_parse()``.
434 435
435 436 - ``name``: of declared alias (may be ``decl`` itself at error)
436 437 - ``args``: list of argument names (or None for symbol declaration)
437 438 - ``errorstr``: detail about detected error (or None)
438 439
439 440 >>> sym = lambda x: (b'symbol', x)
440 441 >>> symlist = lambda *xs: (b'list',) + tuple(sym(x) for x in xs)
441 442 >>> func = lambda n, a: (b'func', sym(n), a)
442 443 >>> parsemap = {
443 444 ... b'foo': sym(b'foo'),
444 445 ... b'$foo': sym(b'$foo'),
445 446 ... b'foo::bar': (b'dagrange', sym(b'foo'), sym(b'bar')),
446 447 ... b'foo()': func(b'foo', None),
447 448 ... b'$foo()': func(b'$foo', None),
448 449 ... b'foo($1, $2)': func(b'foo', symlist(b'$1', b'$2')),
449 450 ... b'foo(bar_bar, baz.baz)':
450 451 ... func(b'foo', symlist(b'bar_bar', b'baz.baz')),
451 452 ... b'foo(bar($1, $2))':
452 453 ... func(b'foo', func(b'bar', symlist(b'$1', b'$2'))),
453 454 ... b'foo($1, $2, nested($1, $2))':
454 455 ... func(b'foo', (symlist(b'$1', b'$2') +
455 456 ... (func(b'nested', symlist(b'$1', b'$2')),))),
456 457 ... b'foo("bar")': func(b'foo', (b'string', b'bar')),
457 458 ... b'foo($1, $2': error.ParseError(b'unexpected token: end', 10),
458 459 ... b'foo("bar': error.ParseError(b'unterminated string', 5),
459 460 ... b'foo($1, $2, $1)': func(b'foo', symlist(b'$1', b'$2', b'$1')),
460 461 ... }
461 462 >>> def parse(expr):
462 463 ... x = parsemap[expr]
463 464 ... if isinstance(x, Exception):
464 465 ... raise x
465 466 ... return x
466 467 >>> def trygetfunc(tree):
467 468 ... if not tree or tree[0] != b'func' or tree[1][0] != b'symbol':
468 469 ... return None
469 470 ... if not tree[2]:
470 471 ... return tree[1][1], []
471 472 ... if tree[2][0] == b'list':
472 473 ... return tree[1][1], list(tree[2][1:])
473 474 ... return tree[1][1], [tree[2]]
474 475 >>> class aliasrules(basealiasrules):
475 476 ... _parse = staticmethod(parse)
476 477 ... _trygetfunc = staticmethod(trygetfunc)
477 478 >>> builddecl = aliasrules._builddecl
478 479 >>> builddecl(b'foo')
479 480 ('foo', None, None)
480 481 >>> builddecl(b'$foo')
481 482 ('$foo', None, "invalid symbol '$foo'")
482 483 >>> builddecl(b'foo::bar')
483 484 ('foo::bar', None, 'invalid format')
484 485 >>> builddecl(b'foo()')
485 486 ('foo', [], None)
486 487 >>> builddecl(b'$foo()')
487 488 ('$foo()', None, "invalid function '$foo'")
488 489 >>> builddecl(b'foo($1, $2)')
489 490 ('foo', ['$1', '$2'], None)
490 491 >>> builddecl(b'foo(bar_bar, baz.baz)')
491 492 ('foo', ['bar_bar', 'baz.baz'], None)
492 493 >>> builddecl(b'foo($1, $2, nested($1, $2))')
493 494 ('foo($1, $2, nested($1, $2))', None, 'invalid argument list')
494 495 >>> builddecl(b'foo(bar($1, $2))')
495 496 ('foo(bar($1, $2))', None, 'invalid argument list')
496 497 >>> builddecl(b'foo("bar")')
497 498 ('foo("bar")', None, 'invalid argument list')
498 499 >>> builddecl(b'foo($1, $2')
499 500 ('foo($1, $2', None, 'at 10: unexpected token: end')
500 501 >>> builddecl(b'foo("bar')
501 502 ('foo("bar', None, 'at 5: unterminated string')
502 503 >>> builddecl(b'foo($1, $2, $1)')
503 504 ('foo', None, 'argument names collide with each other')
504 505 """
505 506 try:
506 507 tree = cls._parse(decl)
507 508 except error.ParseError as inst:
508 509 return (decl, None, parseerrordetail(inst))
509 510
510 511 if tree[0] == cls._symbolnode:
511 512 # "name = ...." style
512 513 name = tree[1]
513 514 if name.startswith('$'):
514 515 return (decl, None, _("invalid symbol '%s'") % name)
515 516 return (name, None, None)
516 517
517 518 func = cls._trygetfunc(tree)
518 519 if func:
519 520 # "name(arg, ....) = ...." style
520 521 name, args = func
521 522 if name.startswith('$'):
522 523 return (decl, None, _("invalid function '%s'") % name)
523 524 if any(t[0] != cls._symbolnode for t in args):
524 525 return (decl, None, _("invalid argument list"))
525 526 if len(args) != len(set(args)):
526 527 return (name, None, _("argument names collide with each other"))
527 528 return (name, [t[1] for t in args], None)
528 529
529 530 return (decl, None, _("invalid format"))
530 531
531 532 @classmethod
532 533 def _relabelargs(cls, tree, args):
533 534 """Mark alias arguments as ``_aliasarg``"""
534 535 if not isinstance(tree, tuple):
535 536 return tree
536 537 op = tree[0]
537 538 if op != cls._symbolnode:
538 539 return (op,) + tuple(cls._relabelargs(x, args) for x in tree[1:])
539 540
540 541 assert len(tree) == 2
541 542 sym = tree[1]
542 543 if sym in args:
543 544 op = '_aliasarg'
544 545 elif sym.startswith('$'):
545 546 raise error.ParseError(_("invalid symbol '%s'") % sym)
546 547 return (op, sym)
547 548
548 549 @classmethod
549 550 def _builddefn(cls, defn, args):
550 551 """Parse an alias definition into a tree and marks substitutions
551 552
552 553 This function marks alias argument references as ``_aliasarg``. The
553 554 parsing rule is provided by ``_parse()``.
554 555
555 556 ``args`` is a list of alias argument names, or None if the alias
556 557 is declared as a symbol.
557 558
558 559 >>> from . import pycompat
559 560 >>> parsemap = {
560 561 ... b'$1 or foo': (b'or', (b'symbol', b'$1'), (b'symbol', b'foo')),
561 562 ... b'$1 or $bar':
562 563 ... (b'or', (b'symbol', b'$1'), (b'symbol', b'$bar')),
563 564 ... b'$10 or baz':
564 565 ... (b'or', (b'symbol', b'$10'), (b'symbol', b'baz')),
565 566 ... b'"$1" or "foo"':
566 567 ... (b'or', (b'string', b'$1'), (b'string', b'foo')),
567 568 ... }
568 569 >>> class aliasrules(basealiasrules):
569 570 ... _parse = staticmethod(parsemap.__getitem__)
570 571 ... _trygetfunc = staticmethod(lambda x: None)
571 572 >>> builddefn = aliasrules._builddefn
572 573 >>> def pprint(tree):
573 574 ... s = prettyformat(tree, (b'_aliasarg', b'string', b'symbol'))
574 575 ... print(pycompat.sysstr(s))
575 576 >>> args = [b'$1', b'$2', b'foo']
576 577 >>> pprint(builddefn(b'$1 or foo', args))
577 578 (or
578 579 (_aliasarg '$1')
579 580 (_aliasarg 'foo'))
580 581 >>> try:
581 582 ... builddefn(b'$1 or $bar', args)
582 583 ... except error.ParseError as inst:
583 584 ... print(pycompat.sysstr(parseerrordetail(inst)))
584 585 invalid symbol '$bar'
585 586 >>> args = [b'$1', b'$10', b'foo']
586 587 >>> pprint(builddefn(b'$10 or baz', args))
587 588 (or
588 589 (_aliasarg '$10')
589 590 (symbol 'baz'))
590 591 >>> pprint(builddefn(b'"$1" or "foo"', args))
591 592 (or
592 593 (string '$1')
593 594 (string 'foo'))
594 595 """
595 596 tree = cls._parse(defn)
596 597 if args:
597 598 args = set(args)
598 599 else:
599 600 args = set()
600 601 return cls._relabelargs(tree, args)
601 602
602 603 @classmethod
603 604 def build(cls, decl, defn):
604 605 """Parse an alias declaration and definition into an alias object"""
605 606 repl = efmt = None
606 607 name, args, err = cls._builddecl(decl)
607 608 if err:
608 609 efmt = _('bad declaration of %(section)s "%(name)s": %(error)s')
609 610 else:
610 611 try:
611 612 repl = cls._builddefn(defn, args)
612 613 except error.ParseError as inst:
613 614 err = parseerrordetail(inst)
614 615 efmt = _('bad definition of %(section)s "%(name)s": %(error)s')
615 616 if err:
616 617 err = efmt % {'section': cls._section, 'name': name, 'error': err}
617 618 return alias(name, args, err, repl)
618 619
619 620 @classmethod
620 621 def buildmap(cls, items):
621 622 """Parse a list of alias (name, replacement) pairs into a dict of
622 623 alias objects"""
623 624 aliases = {}
624 625 for decl, defn in items:
625 626 a = cls.build(decl, defn)
626 627 aliases[a.name] = a
627 628 return aliases
628 629
629 630 @classmethod
630 631 def _getalias(cls, aliases, tree):
631 632 """If tree looks like an unexpanded alias, return (alias, pattern-args)
632 633 pair. Return None otherwise.
633 634 """
634 635 if not isinstance(tree, tuple):
635 636 return None
636 637 if tree[0] == cls._symbolnode:
637 638 name = tree[1]
638 639 a = aliases.get(name)
639 640 if a and a.args is None:
640 641 return a, None
641 642 func = cls._trygetfunc(tree)
642 643 if func:
643 644 name, args = func
644 645 a = aliases.get(name)
645 646 if a and a.args is not None:
646 647 return a, args
647 648 return None
648 649
649 650 @classmethod
650 651 def _expandargs(cls, tree, args):
651 652 """Replace _aliasarg instances with the substitution value of the
652 653 same name in args, recursively.
653 654 """
654 655 if not isinstance(tree, tuple):
655 656 return tree
656 657 if tree[0] == '_aliasarg':
657 658 sym = tree[1]
658 659 return args[sym]
659 660 return tuple(cls._expandargs(t, args) for t in tree)
660 661
661 662 @classmethod
662 663 def _expand(cls, aliases, tree, expanding, cache):
663 664 if not isinstance(tree, tuple):
664 665 return tree
665 666 r = cls._getalias(aliases, tree)
666 667 if r is None:
667 668 return tuple(cls._expand(aliases, t, expanding, cache)
668 669 for t in tree)
669 670 a, l = r
670 671 if a.error:
671 672 raise error.Abort(a.error)
672 673 if a in expanding:
673 674 raise error.ParseError(_('infinite expansion of %(section)s '
674 675 '"%(name)s" detected')
675 676 % {'section': cls._section, 'name': a.name})
676 677 # get cacheable replacement tree by expanding aliases recursively
677 678 expanding.append(a)
678 679 if a.name not in cache:
679 680 cache[a.name] = cls._expand(aliases, a.replacement, expanding,
680 681 cache)
681 682 result = cache[a.name]
682 683 expanding.pop()
683 684 if a.args is None:
684 685 return result
685 686 # substitute function arguments in replacement tree
686 687 if len(l) != len(a.args):
687 688 raise error.ParseError(_('invalid number of arguments: %d')
688 689 % len(l))
689 690 l = [cls._expand(aliases, t, [], cache) for t in l]
690 691 return cls._expandargs(result, dict(zip(a.args, l)))
691 692
692 693 @classmethod
693 694 def expand(cls, aliases, tree):
694 695 """Expand aliases in tree, recursively.
695 696
696 697 'aliases' is a dictionary mapping user defined aliases to alias objects.
697 698 """
698 699 return cls._expand(aliases, tree, [], {})
@@ -1,880 +1,880 b''
1 1 # sslutil.py - SSL handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import hashlib
13 13 import os
14 14 import re
15 15 import ssl
16 16
17 17 from .i18n import _
18 18 from . import (
19 19 error,
20 20 node,
21 21 pycompat,
22 22 util,
23 23 )
24 24 from .utils import (
25 25 procutil,
26 26 stringutil,
27 27 )
28 28
29 29 # Python 2.7.9+ overhauled the built-in SSL/TLS features of Python. It added
30 30 # support for TLS 1.1, TLS 1.2, SNI, system CA stores, etc. These features are
31 31 # all exposed via the "ssl" module.
32 32 #
33 33 # Depending on the version of Python being used, SSL/TLS support is either
34 34 # modern/secure or legacy/insecure. Many operations in this module have
35 35 # separate code paths depending on support in Python.
36 36
37 37 configprotocols = {
38 38 'tls1.0',
39 39 'tls1.1',
40 40 'tls1.2',
41 41 }
42 42
43 43 hassni = getattr(ssl, 'HAS_SNI', False)
44 44
45 45 # TLS 1.1 and 1.2 may not be supported if the OpenSSL Python is compiled
46 46 # against doesn't support them.
47 47 supportedprotocols = {'tls1.0'}
48 48 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_1'):
49 49 supportedprotocols.add('tls1.1')
50 50 if util.safehasattr(ssl, 'PROTOCOL_TLSv1_2'):
51 51 supportedprotocols.add('tls1.2')
52 52
53 53 try:
54 54 # ssl.SSLContext was added in 2.7.9 and presence indicates modern
55 55 # SSL/TLS features are available.
56 56 SSLContext = ssl.SSLContext
57 57 modernssl = True
58 58 _canloaddefaultcerts = util.safehasattr(SSLContext, 'load_default_certs')
59 59 except AttributeError:
60 60 modernssl = False
61 61 _canloaddefaultcerts = False
62 62
63 63 # We implement SSLContext using the interface from the standard library.
64 64 class SSLContext(object):
65 65 def __init__(self, protocol):
66 66 # From the public interface of SSLContext
67 67 self.protocol = protocol
68 68 self.check_hostname = False
69 69 self.options = 0
70 70 self.verify_mode = ssl.CERT_NONE
71 71
72 72 # Used by our implementation.
73 73 self._certfile = None
74 74 self._keyfile = None
75 75 self._certpassword = None
76 76 self._cacerts = None
77 77 self._ciphers = None
78 78
79 79 def load_cert_chain(self, certfile, keyfile=None, password=None):
80 80 self._certfile = certfile
81 81 self._keyfile = keyfile
82 82 self._certpassword = password
83 83
84 84 def load_default_certs(self, purpose=None):
85 85 pass
86 86
87 87 def load_verify_locations(self, cafile=None, capath=None, cadata=None):
88 88 if capath:
89 89 raise error.Abort(_('capath not supported'))
90 90 if cadata:
91 91 raise error.Abort(_('cadata not supported'))
92 92
93 93 self._cacerts = cafile
94 94
95 95 def set_ciphers(self, ciphers):
96 96 self._ciphers = ciphers
97 97
98 98 def wrap_socket(self, socket, server_hostname=None, server_side=False):
99 99 # server_hostname is unique to SSLContext.wrap_socket and is used
100 100 # for SNI in that context. So there's nothing for us to do with it
101 101 # in this legacy code since we don't support SNI.
102 102
103 103 args = {
104 104 r'keyfile': self._keyfile,
105 105 r'certfile': self._certfile,
106 106 r'server_side': server_side,
107 107 r'cert_reqs': self.verify_mode,
108 108 r'ssl_version': self.protocol,
109 109 r'ca_certs': self._cacerts,
110 110 r'ciphers': self._ciphers,
111 111 }
112 112
113 113 return ssl.wrap_socket(socket, **args)
114 114
115 115 def _hostsettings(ui, hostname):
116 116 """Obtain security settings for a hostname.
117 117
118 118 Returns a dict of settings relevant to that hostname.
119 119 """
120 120 bhostname = pycompat.bytesurl(hostname)
121 121 s = {
122 122 # Whether we should attempt to load default/available CA certs
123 123 # if an explicit ``cafile`` is not defined.
124 124 'allowloaddefaultcerts': True,
125 125 # List of 2-tuple of (hash algorithm, hash).
126 126 'certfingerprints': [],
127 127 # Path to file containing concatenated CA certs. Used by
128 128 # SSLContext.load_verify_locations().
129 129 'cafile': None,
130 130 # Whether certificate verification should be disabled.
131 131 'disablecertverification': False,
132 132 # Whether the legacy [hostfingerprints] section has data for this host.
133 133 'legacyfingerprint': False,
134 134 # PROTOCOL_* constant to use for SSLContext.__init__.
135 135 'protocol': None,
136 136 # String representation of minimum protocol to be used for UI
137 137 # presentation.
138 138 'protocolui': None,
139 139 # ssl.CERT_* constant used by SSLContext.verify_mode.
140 140 'verifymode': None,
141 141 # Defines extra ssl.OP* bitwise options to set.
142 142 'ctxoptions': None,
143 143 # OpenSSL Cipher List to use (instead of default).
144 144 'ciphers': None,
145 145 }
146 146
147 147 # Allow minimum TLS protocol to be specified in the config.
148 148 def validateprotocol(protocol, key):
149 149 if protocol not in configprotocols:
150 150 raise error.Abort(
151 151 _('unsupported protocol from hostsecurity.%s: %s') %
152 152 (key, protocol),
153 153 hint=_('valid protocols: %s') %
154 154 ' '.join(sorted(configprotocols)))
155 155
156 156 # We default to TLS 1.1+ where we can because TLS 1.0 has known
157 157 # vulnerabilities (like BEAST and POODLE). We allow users to downgrade to
158 158 # TLS 1.0+ via config options in case a legacy server is encountered.
159 159 if 'tls1.1' in supportedprotocols:
160 160 defaultprotocol = 'tls1.1'
161 161 else:
162 162 # Let people know they are borderline secure.
163 163 # We don't document this config option because we want people to see
164 164 # the bold warnings on the web site.
165 165 # internal config: hostsecurity.disabletls10warning
166 166 if not ui.configbool('hostsecurity', 'disabletls10warning'):
167 167 ui.warn(_('warning: connecting to %s using legacy security '
168 168 'technology (TLS 1.0); see '
169 169 'https://mercurial-scm.org/wiki/SecureConnections for '
170 170 'more info\n') % bhostname)
171 171 defaultprotocol = 'tls1.0'
172 172
173 173 key = 'minimumprotocol'
174 174 protocol = ui.config('hostsecurity', key, defaultprotocol)
175 175 validateprotocol(protocol, key)
176 176
177 177 key = '%s:minimumprotocol' % bhostname
178 178 protocol = ui.config('hostsecurity', key, protocol)
179 179 validateprotocol(protocol, key)
180 180
181 181 # If --insecure is used, we allow the use of TLS 1.0 despite config options.
182 182 # We always print a "connection security to %s is disabled..." message when
183 183 # --insecure is used. So no need to print anything more here.
184 184 if ui.insecureconnections:
185 185 protocol = 'tls1.0'
186 186
187 187 s['protocol'], s['ctxoptions'], s['protocolui'] = protocolsettings(protocol)
188 188
189 189 ciphers = ui.config('hostsecurity', 'ciphers')
190 190 ciphers = ui.config('hostsecurity', '%s:ciphers' % bhostname, ciphers)
191 191 s['ciphers'] = ciphers
192 192
193 193 # Look for fingerprints in [hostsecurity] section. Value is a list
194 194 # of <alg>:<fingerprint> strings.
195 195 fingerprints = ui.configlist('hostsecurity', '%s:fingerprints' % bhostname)
196 196 for fingerprint in fingerprints:
197 197 if not (fingerprint.startswith(('sha1:', 'sha256:', 'sha512:'))):
198 198 raise error.Abort(_('invalid fingerprint for %s: %s') % (
199 199 bhostname, fingerprint),
200 200 hint=_('must begin with "sha1:", "sha256:", '
201 201 'or "sha512:"'))
202 202
203 203 alg, fingerprint = fingerprint.split(':', 1)
204 204 fingerprint = fingerprint.replace(':', '').lower()
205 205 s['certfingerprints'].append((alg, fingerprint))
206 206
207 207 # Fingerprints from [hostfingerprints] are always SHA-1.
208 208 for fingerprint in ui.configlist('hostfingerprints', bhostname):
209 209 fingerprint = fingerprint.replace(':', '').lower()
210 210 s['certfingerprints'].append(('sha1', fingerprint))
211 211 s['legacyfingerprint'] = True
212 212
213 213 # If a host cert fingerprint is defined, it is the only thing that
214 214 # matters. No need to validate CA certs.
215 215 if s['certfingerprints']:
216 216 s['verifymode'] = ssl.CERT_NONE
217 217 s['allowloaddefaultcerts'] = False
218 218
219 219 # If --insecure is used, don't take CAs into consideration.
220 220 elif ui.insecureconnections:
221 221 s['disablecertverification'] = True
222 222 s['verifymode'] = ssl.CERT_NONE
223 223 s['allowloaddefaultcerts'] = False
224 224
225 225 if ui.configbool('devel', 'disableloaddefaultcerts'):
226 226 s['allowloaddefaultcerts'] = False
227 227
228 228 # If both fingerprints and a per-host ca file are specified, issue a warning
229 229 # because users should not be surprised about what security is or isn't
230 230 # being performed.
231 231 cafile = ui.config('hostsecurity', '%s:verifycertsfile' % bhostname)
232 232 if s['certfingerprints'] and cafile:
233 233 ui.warn(_('(hostsecurity.%s:verifycertsfile ignored when host '
234 234 'fingerprints defined; using host fingerprints for '
235 235 'verification)\n') % bhostname)
236 236
237 237 # Try to hook up CA certificate validation unless something above
238 238 # makes it not necessary.
239 239 if s['verifymode'] is None:
240 240 # Look at per-host ca file first.
241 241 if cafile:
242 242 cafile = util.expandpath(cafile)
243 243 if not os.path.exists(cafile):
244 244 raise error.Abort(_('path specified by %s does not exist: %s') %
245 245 ('hostsecurity.%s:verifycertsfile' % (
246 246 bhostname,), cafile))
247 247 s['cafile'] = cafile
248 248 else:
249 249 # Find global certificates file in config.
250 250 cafile = ui.config('web', 'cacerts')
251 251
252 252 if cafile:
253 253 cafile = util.expandpath(cafile)
254 254 if not os.path.exists(cafile):
255 255 raise error.Abort(_('could not find web.cacerts: %s') %
256 256 cafile)
257 257 elif s['allowloaddefaultcerts']:
258 258 # CAs not defined in config. Try to find system bundles.
259 259 cafile = _defaultcacerts(ui)
260 260 if cafile:
261 261 ui.debug('using %s for CA file\n' % cafile)
262 262
263 263 s['cafile'] = cafile
264 264
265 265 # Require certificate validation if CA certs are being loaded and
266 266 # verification hasn't been disabled above.
267 267 if cafile or (_canloaddefaultcerts and s['allowloaddefaultcerts']):
268 268 s['verifymode'] = ssl.CERT_REQUIRED
269 269 else:
270 270 # At this point we don't have a fingerprint, aren't being
271 271 # explicitly insecure, and can't load CA certs. Connecting
272 272 # is insecure. We allow the connection and abort during
273 273 # validation (once we have the fingerprint to print to the
274 274 # user).
275 275 s['verifymode'] = ssl.CERT_NONE
276 276
277 277 assert s['protocol'] is not None
278 278 assert s['ctxoptions'] is not None
279 279 assert s['verifymode'] is not None
280 280
281 281 return s
282 282
283 283 def protocolsettings(protocol):
284 284 """Resolve the protocol for a config value.
285 285
286 286 Returns a 3-tuple of (protocol, options, ui value) where the first
287 287 2 items are values used by SSLContext and the last is a string value
288 288 of the ``minimumprotocol`` config option equivalent.
289 289 """
290 290 if protocol not in configprotocols:
291 291 raise ValueError('protocol value not supported: %s' % protocol)
292 292
293 293 # Despite its name, PROTOCOL_SSLv23 selects the highest protocol
294 294 # that both ends support, including TLS protocols. On legacy stacks,
295 295 # the highest it likely goes is TLS 1.0. On modern stacks, it can
296 296 # support TLS 1.2.
297 297 #
298 298 # The PROTOCOL_TLSv* constants select a specific TLS version
299 299 # only (as opposed to multiple versions). So the method for
300 300 # supporting multiple TLS versions is to use PROTOCOL_SSLv23 and
301 301 # disable protocols via SSLContext.options and OP_NO_* constants.
302 302 # However, SSLContext.options doesn't work unless we have the
303 303 # full/real SSLContext available to us.
304 304 if supportedprotocols == {'tls1.0'}:
305 305 if protocol != 'tls1.0':
306 306 raise error.Abort(_('current Python does not support protocol '
307 307 'setting %s') % protocol,
308 308 hint=_('upgrade Python or disable setting since '
309 309 'only TLS 1.0 is supported'))
310 310
311 311 return ssl.PROTOCOL_TLSv1, 0, 'tls1.0'
312 312
313 313 # WARNING: returned options don't work unless the modern ssl module
314 314 # is available. Be careful when adding options here.
315 315
316 316 # SSLv2 and SSLv3 are broken. We ban them outright.
317 317 options = ssl.OP_NO_SSLv2 | ssl.OP_NO_SSLv3
318 318
319 319 if protocol == 'tls1.0':
320 320 # Defaults above are to use TLS 1.0+
321 321 pass
322 322 elif protocol == 'tls1.1':
323 323 options |= ssl.OP_NO_TLSv1
324 324 elif protocol == 'tls1.2':
325 325 options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1
326 326 else:
327 327 raise error.Abort(_('this should not happen'))
328 328
329 329 # Prevent CRIME.
330 330 # There is no guarantee this attribute is defined on the module.
331 331 options |= getattr(ssl, 'OP_NO_COMPRESSION', 0)
332 332
333 333 return ssl.PROTOCOL_SSLv23, options, protocol
334 334
335 335 def wrapsocket(sock, keyfile, certfile, ui, serverhostname=None):
336 336 """Add SSL/TLS to a socket.
337 337
338 338 This is a glorified wrapper for ``ssl.wrap_socket()``. It makes sane
339 339 choices based on what security options are available.
340 340
341 341 In addition to the arguments supported by ``ssl.wrap_socket``, we allow
342 342 the following additional arguments:
343 343
344 344 * serverhostname - The expected hostname of the remote server. If the
345 345 server (and client) support SNI, this tells the server which certificate
346 346 to use.
347 347 """
348 348 if not serverhostname:
349 349 raise error.Abort(_('serverhostname argument is required'))
350 350
351 351 for f in (keyfile, certfile):
352 352 if f and not os.path.exists(f):
353 353 raise error.Abort(
354 354 _('certificate file (%s) does not exist; cannot connect to %s')
355 355 % (f, pycompat.bytesurl(serverhostname)),
356 356 hint=_('restore missing file or fix references '
357 357 'in Mercurial config'))
358 358
359 359 settings = _hostsettings(ui, serverhostname)
360 360
361 361 # We can't use ssl.create_default_context() because it calls
362 362 # load_default_certs() unless CA arguments are passed to it. We want to
363 363 # have explicit control over CA loading because implicitly loading
364 364 # CAs may undermine the user's intent. For example, a user may define a CA
365 365 # bundle with a specific CA cert removed. If the system/default CA bundle
366 366 # is loaded and contains that removed CA, you've just undone the user's
367 367 # choice.
368 368 sslcontext = SSLContext(settings['protocol'])
369 369
370 370 # This is a no-op unless using modern ssl.
371 371 sslcontext.options |= settings['ctxoptions']
372 372
373 373 # This still works on our fake SSLContext.
374 374 sslcontext.verify_mode = settings['verifymode']
375 375
376 376 if settings['ciphers']:
377 377 try:
378 378 sslcontext.set_ciphers(pycompat.sysstr(settings['ciphers']))
379 379 except ssl.SSLError as e:
380 380 raise error.Abort(
381 381 _('could not set ciphers: %s')
382 382 % stringutil.forcebytestr(e.args[0]),
383 383 hint=_('change cipher string (%s) in config') %
384 384 settings['ciphers'])
385 385
386 386 if certfile is not None:
387 387 def password():
388 388 f = keyfile or certfile
389 389 return ui.getpass(_('passphrase for %s: ') % f, '')
390 390 sslcontext.load_cert_chain(certfile, keyfile, password)
391 391
392 392 if settings['cafile'] is not None:
393 393 try:
394 394 sslcontext.load_verify_locations(cafile=settings['cafile'])
395 395 except ssl.SSLError as e:
396 396 if len(e.args) == 1: # pypy has different SSLError args
397 397 msg = e.args[0]
398 398 else:
399 399 msg = e.args[1]
400 400 raise error.Abort(_('error loading CA file %s: %s') % (
401 401 settings['cafile'], stringutil.forcebytestr(msg)),
402 402 hint=_('file is empty or malformed?'))
403 403 caloaded = True
404 404 elif settings['allowloaddefaultcerts']:
405 405 # This is a no-op on old Python.
406 406 sslcontext.load_default_certs()
407 407 caloaded = True
408 408 else:
409 409 caloaded = False
410 410
411 411 try:
412 412 sslsocket = sslcontext.wrap_socket(sock, server_hostname=serverhostname)
413 413 except ssl.SSLError as e:
414 414 # If we're doing certificate verification and no CA certs are loaded,
415 415 # that is almost certainly the reason why verification failed. Provide
416 416 # a hint to the user.
417 417 # Only modern ssl module exposes SSLContext.get_ca_certs() so we can
418 418 # only show this warning if modern ssl is available.
419 419 # The exception handler is here to handle bugs around cert attributes:
420 420 # https://bugs.python.org/issue20916#msg213479. (See issues5313.)
421 421 # When the main 20916 bug occurs, 'sslcontext.get_ca_certs()' is a
422 422 # non-empty list, but the following conditional is otherwise True.
423 423 try:
424 424 if (caloaded and settings['verifymode'] == ssl.CERT_REQUIRED and
425 425 modernssl and not sslcontext.get_ca_certs()):
426 426 ui.warn(_('(an attempt was made to load CA certificates but '
427 427 'none were loaded; see '
428 428 'https://mercurial-scm.org/wiki/SecureConnections '
429 429 'for how to configure Mercurial to avoid this '
430 430 'error)\n'))
431 431 except ssl.SSLError:
432 432 pass
433 433
434 434 # Try to print more helpful error messages for known failures.
435 435 if util.safehasattr(e, 'reason'):
436 436 # This error occurs when the client and server don't share a
437 437 # common/supported SSL/TLS protocol. We've disabled SSLv2 and SSLv3
438 438 # outright. Hopefully the reason for this error is that we require
439 439 # TLS 1.1+ and the server only supports TLS 1.0. Whatever the
440 440 # reason, try to emit an actionable warning.
441 441 if e.reason == r'UNSUPPORTED_PROTOCOL':
442 442 # We attempted TLS 1.0+.
443 443 if settings['protocolui'] == 'tls1.0':
444 444 # We support more than just TLS 1.0+. If this happens,
445 445 # the likely scenario is either the client or the server
446 446 # is really old. (e.g. server doesn't support TLS 1.0+ or
447 447 # client doesn't support modern TLS versions introduced
448 448 # several years from when this comment was written).
449 449 if supportedprotocols != {'tls1.0'}:
450 450 ui.warn(_(
451 451 '(could not communicate with %s using security '
452 452 'protocols %s; if you are using a modern Mercurial '
453 453 'version, consider contacting the operator of this '
454 454 'server; see '
455 455 'https://mercurial-scm.org/wiki/SecureConnections '
456 456 'for more info)\n') % (
457 457 pycompat.bytesurl(serverhostname),
458 458 ', '.join(sorted(supportedprotocols))))
459 459 else:
460 460 ui.warn(_(
461 461 '(could not communicate with %s using TLS 1.0; the '
462 462 'likely cause of this is the server no longer '
463 463 'supports TLS 1.0 because it has known security '
464 464 'vulnerabilities; see '
465 465 'https://mercurial-scm.org/wiki/SecureConnections '
466 466 'for more info)\n') %
467 467 pycompat.bytesurl(serverhostname))
468 468 else:
469 469 # We attempted TLS 1.1+. We can only get here if the client
470 470 # supports the configured protocol. So the likely reason is
471 471 # the client wants better security than the server can
472 472 # offer.
473 473 ui.warn(_(
474 474 '(could not negotiate a common security protocol (%s+) '
475 475 'with %s; the likely cause is Mercurial is configured '
476 476 'to be more secure than the server can support)\n') % (
477 477 settings['protocolui'],
478 478 pycompat.bytesurl(serverhostname)))
479 479 ui.warn(_('(consider contacting the operator of this '
480 480 'server and ask them to support modern TLS '
481 481 'protocol versions; or, set '
482 482 'hostsecurity.%s:minimumprotocol=tls1.0 to allow '
483 483 'use of legacy, less secure protocols when '
484 484 'communicating with this server)\n') %
485 485 pycompat.bytesurl(serverhostname))
486 486 ui.warn(_(
487 487 '(see https://mercurial-scm.org/wiki/SecureConnections '
488 488 'for more info)\n'))
489 489
490 490 elif (e.reason == r'CERTIFICATE_VERIFY_FAILED' and
491 491 pycompat.iswindows):
492 492
493 493 ui.warn(_('(the full certificate chain may not be available '
494 494 'locally; see "hg help debugssl")\n'))
495 495 raise
496 496
497 497 # check if wrap_socket failed silently because socket had been
498 498 # closed
499 499 # - see http://bugs.python.org/issue13721
500 500 if not sslsocket.cipher():
501 501 raise error.Abort(_('ssl connection failed'))
502 502
503 503 sslsocket._hgstate = {
504 504 'caloaded': caloaded,
505 505 'hostname': serverhostname,
506 506 'settings': settings,
507 507 'ui': ui,
508 508 }
509 509
510 510 return sslsocket
511 511
512 512 def wrapserversocket(sock, ui, certfile=None, keyfile=None, cafile=None,
513 513 requireclientcert=False):
514 514 """Wrap a socket for use by servers.
515 515
516 516 ``certfile`` and ``keyfile`` specify the files containing the certificate's
517 517 public and private keys, respectively. Both keys can be defined in the same
518 518 file via ``certfile`` (the private key must come first in the file).
519 519
520 520 ``cafile`` defines the path to certificate authorities.
521 521
522 522 ``requireclientcert`` specifies whether to require client certificates.
523 523
524 524 Typically ``cafile`` is only defined if ``requireclientcert`` is true.
525 525 """
526 526 # This function is not used much by core Mercurial, so the error messaging
527 527 # doesn't have to be as detailed as for wrapsocket().
528 528 for f in (certfile, keyfile, cafile):
529 529 if f and not os.path.exists(f):
530 530 raise error.Abort(_('referenced certificate file (%s) does not '
531 531 'exist') % f)
532 532
533 533 protocol, options, _protocolui = protocolsettings('tls1.0')
534 534
535 535 # This config option is intended for use in tests only. It is a giant
536 536 # footgun to kill security. Don't define it.
537 537 exactprotocol = ui.config('devel', 'serverexactprotocol')
538 538 if exactprotocol == 'tls1.0':
539 539 protocol = ssl.PROTOCOL_TLSv1
540 540 elif exactprotocol == 'tls1.1':
541 541 if 'tls1.1' not in supportedprotocols:
542 542 raise error.Abort(_('TLS 1.1 not supported by this Python'))
543 543 protocol = ssl.PROTOCOL_TLSv1_1
544 544 elif exactprotocol == 'tls1.2':
545 545 if 'tls1.2' not in supportedprotocols:
546 546 raise error.Abort(_('TLS 1.2 not supported by this Python'))
547 547 protocol = ssl.PROTOCOL_TLSv1_2
548 548 elif exactprotocol:
549 549 raise error.Abort(_('invalid value for serverexactprotocol: %s') %
550 550 exactprotocol)
551 551
552 552 if modernssl:
553 553 # We /could/ use create_default_context() here since it doesn't load
554 554 # CAs when configured for client auth. However, it is hard-coded to
555 555 # use ssl.PROTOCOL_SSLv23 which may not be appropriate here.
556 556 sslcontext = SSLContext(protocol)
557 557 sslcontext.options |= options
558 558
559 559 # Improve forward secrecy.
560 560 sslcontext.options |= getattr(ssl, 'OP_SINGLE_DH_USE', 0)
561 561 sslcontext.options |= getattr(ssl, 'OP_SINGLE_ECDH_USE', 0)
562 562
563 563 # Use the list of more secure ciphers if found in the ssl module.
564 564 if util.safehasattr(ssl, '_RESTRICTED_SERVER_CIPHERS'):
565 565 sslcontext.options |= getattr(ssl, 'OP_CIPHER_SERVER_PREFERENCE', 0)
566 566 sslcontext.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS)
567 567 else:
568 568 sslcontext = SSLContext(ssl.PROTOCOL_TLSv1)
569 569
570 570 if requireclientcert:
571 571 sslcontext.verify_mode = ssl.CERT_REQUIRED
572 572 else:
573 573 sslcontext.verify_mode = ssl.CERT_NONE
574 574
575 575 if certfile or keyfile:
576 576 sslcontext.load_cert_chain(certfile=certfile, keyfile=keyfile)
577 577
578 578 if cafile:
579 579 sslcontext.load_verify_locations(cafile=cafile)
580 580
581 581 return sslcontext.wrap_socket(sock, server_side=True)
582 582
583 583 class wildcarderror(Exception):
584 584 """Represents an error parsing wildcards in DNS name."""
585 585
586 586 def _dnsnamematch(dn, hostname, maxwildcards=1):
587 587 """Match DNS names according RFC 6125 section 6.4.3.
588 588
589 589 This code is effectively copied from CPython's ssl._dnsname_match.
590 590
591 591 Returns a bool indicating whether the expected hostname matches
592 592 the value in ``dn``.
593 593 """
594 594 pats = []
595 595 if not dn:
596 596 return False
597 597 dn = pycompat.bytesurl(dn)
598 598 hostname = pycompat.bytesurl(hostname)
599 599
600 600 pieces = dn.split('.')
601 601 leftmost = pieces[0]
602 602 remainder = pieces[1:]
603 603 wildcards = leftmost.count('*')
604 604 if wildcards > maxwildcards:
605 605 raise wildcarderror(
606 606 _('too many wildcards in certificate DNS name: %s') % dn)
607 607
608 608 # speed up common case w/o wildcards
609 609 if not wildcards:
610 610 return dn.lower() == hostname.lower()
611 611
612 612 # RFC 6125, section 6.4.3, subitem 1.
613 613 # The client SHOULD NOT attempt to match a presented identifier in which
614 614 # the wildcard character comprises a label other than the left-most label.
615 615 if leftmost == '*':
616 616 # When '*' is a fragment by itself, it matches a non-empty dotless
617 617 # fragment.
618 618 pats.append('[^.]+')
619 619 elif leftmost.startswith('xn--') or hostname.startswith('xn--'):
620 620 # RFC 6125, section 6.4.3, subitem 3.
621 621 # The client SHOULD NOT attempt to match a presented identifier
622 622 # where the wildcard character is embedded within an A-label or
623 623 # U-label of an internationalized domain name.
624 624 pats.append(stringutil.reescape(leftmost))
625 625 else:
626 626 # Otherwise, '*' matches any dotless string, e.g. www*
627 627 pats.append(stringutil.reescape(leftmost).replace(br'\*', '[^.]*'))
628 628
629 629 # add the remaining fragments, ignore any wildcards
630 630 for frag in remainder:
631 631 pats.append(stringutil.reescape(frag))
632 632
633 633 pat = re.compile(br'\A' + br'\.'.join(pats) + br'\Z', re.IGNORECASE)
634 634 return pat.match(hostname) is not None
635 635
636 636 def _verifycert(cert, hostname):
637 637 '''Verify that cert (in socket.getpeercert() format) matches hostname.
638 638 CRLs is not handled.
639 639
640 640 Returns error message if any problems are found and None on success.
641 641 '''
642 642 if not cert:
643 643 return _('no certificate received')
644 644
645 645 dnsnames = []
646 646 san = cert.get(r'subjectAltName', [])
647 647 for key, value in san:
648 648 if key == r'DNS':
649 649 try:
650 650 if _dnsnamematch(value, hostname):
651 651 return
652 652 except wildcarderror as e:
653 653 return stringutil.forcebytestr(e.args[0])
654 654
655 655 dnsnames.append(value)
656 656
657 657 if not dnsnames:
658 658 # The subject is only checked when there is no DNS in subjectAltName.
659 659 for sub in cert.get(r'subject', []):
660 660 for key, value in sub:
661 661 # According to RFC 2818 the most specific Common Name must
662 662 # be used.
663 663 if key == r'commonName':
664 664 # 'subject' entries are unicode.
665 665 try:
666 666 value = value.encode('ascii')
667 667 except UnicodeEncodeError:
668 668 return _('IDN in certificate not supported')
669 669
670 670 try:
671 671 if _dnsnamematch(value, hostname):
672 672 return
673 673 except wildcarderror as e:
674 674 return stringutil.forcebytestr(e.args[0])
675 675
676 676 dnsnames.append(value)
677 677
678 678 dnsnames = [pycompat.bytesurl(d) for d in dnsnames]
679 679 if len(dnsnames) > 1:
680 680 return _('certificate is for %s') % ', '.join(dnsnames)
681 681 elif len(dnsnames) == 1:
682 682 return _('certificate is for %s') % dnsnames[0]
683 683 else:
684 684 return _('no commonName or subjectAltName found in certificate')
685 685
686 686 def _plainapplepython():
687 687 """return true if this seems to be a pure Apple Python that
688 688 * is unfrozen and presumably has the whole mercurial module in the file
689 689 system
690 690 * presumably is an Apple Python that uses Apple OpenSSL which has patches
691 691 for using system certificate store CAs in addition to the provided
692 692 cacerts file
693 693 """
694 694 if (not pycompat.isdarwin or procutil.mainfrozen() or
695 695 not pycompat.sysexecutable):
696 696 return False
697 697 exe = os.path.realpath(pycompat.sysexecutable).lower()
698 698 return (exe.startswith('/usr/bin/python') or
699 699 exe.startswith('/system/library/frameworks/python.framework/'))
700 700
701 701 _systemcacertpaths = [
702 702 # RHEL, CentOS, and Fedora
703 703 '/etc/pki/tls/certs/ca-bundle.trust.crt',
704 704 # Debian, Ubuntu, Gentoo
705 705 '/etc/ssl/certs/ca-certificates.crt',
706 706 ]
707 707
708 708 def _defaultcacerts(ui):
709 709 """return path to default CA certificates or None.
710 710
711 711 It is assumed this function is called when the returned certificates
712 712 file will actually be used to validate connections. Therefore this
713 713 function may print warnings or debug messages assuming this usage.
714 714
715 715 We don't print a message when the Python is able to load default
716 716 CA certs because this scenario is detected at socket connect time.
717 717 """
718 718 # The "certifi" Python package provides certificates. If it is installed
719 719 # and usable, assume the user intends it to be used and use it.
720 720 try:
721 721 import certifi
722 722 certs = certifi.where()
723 723 if os.path.exists(certs):
724 724 ui.debug('using ca certificates from certifi\n')
725 return certs
725 return pycompat.fsencode(certs)
726 726 except (ImportError, AttributeError):
727 727 pass
728 728
729 729 # On Windows, only the modern ssl module is capable of loading the system
730 730 # CA certificates. If we're not capable of doing that, emit a warning
731 731 # because we'll get a certificate verification error later and the lack
732 732 # of loaded CA certificates will be the reason why.
733 733 # Assertion: this code is only called if certificates are being verified.
734 734 if pycompat.iswindows:
735 735 if not _canloaddefaultcerts:
736 736 ui.warn(_('(unable to load Windows CA certificates; see '
737 737 'https://mercurial-scm.org/wiki/SecureConnections for '
738 738 'how to configure Mercurial to avoid this message)\n'))
739 739
740 740 return None
741 741
742 742 # Apple's OpenSSL has patches that allow a specially constructed certificate
743 743 # to load the system CA store. If we're running on Apple Python, use this
744 744 # trick.
745 745 if _plainapplepython():
746 746 dummycert = os.path.join(
747 747 os.path.dirname(pycompat.fsencode(__file__)), 'dummycert.pem')
748 748 if os.path.exists(dummycert):
749 749 return dummycert
750 750
751 751 # The Apple OpenSSL trick isn't available to us. If Python isn't able to
752 752 # load system certs, we're out of luck.
753 753 if pycompat.isdarwin:
754 754 # FUTURE Consider looking for Homebrew or MacPorts installed certs
755 755 # files. Also consider exporting the keychain certs to a file during
756 756 # Mercurial install.
757 757 if not _canloaddefaultcerts:
758 758 ui.warn(_('(unable to load CA certificates; see '
759 759 'https://mercurial-scm.org/wiki/SecureConnections for '
760 760 'how to configure Mercurial to avoid this message)\n'))
761 761 return None
762 762
763 763 # / is writable on Windows. Out of an abundance of caution make sure
764 764 # we're not on Windows because paths from _systemcacerts could be installed
765 765 # by non-admin users.
766 766 assert not pycompat.iswindows
767 767
768 768 # Try to find CA certificates in well-known locations. We print a warning
769 769 # when using a found file because we don't want too much silent magic
770 770 # for security settings. The expectation is that proper Mercurial
771 771 # installs will have the CA certs path defined at install time and the
772 772 # installer/packager will make an appropriate decision on the user's
773 773 # behalf. We only get here and perform this setting as a feature of
774 774 # last resort.
775 775 if not _canloaddefaultcerts:
776 776 for path in _systemcacertpaths:
777 777 if os.path.isfile(path):
778 778 ui.warn(_('(using CA certificates from %s; if you see this '
779 779 'message, your Mercurial install is not properly '
780 780 'configured; see '
781 781 'https://mercurial-scm.org/wiki/SecureConnections '
782 782 'for how to configure Mercurial to avoid this '
783 783 'message)\n') % path)
784 784 return path
785 785
786 786 ui.warn(_('(unable to load CA certificates; see '
787 787 'https://mercurial-scm.org/wiki/SecureConnections for '
788 788 'how to configure Mercurial to avoid this message)\n'))
789 789
790 790 return None
791 791
792 792 def validatesocket(sock):
793 793 """Validate a socket meets security requirements.
794 794
795 795 The passed socket must have been created with ``wrapsocket()``.
796 796 """
797 797 shost = sock._hgstate['hostname']
798 798 host = pycompat.bytesurl(shost)
799 799 ui = sock._hgstate['ui']
800 800 settings = sock._hgstate['settings']
801 801
802 802 try:
803 803 peercert = sock.getpeercert(True)
804 804 peercert2 = sock.getpeercert()
805 805 except AttributeError:
806 806 raise error.Abort(_('%s ssl connection error') % host)
807 807
808 808 if not peercert:
809 809 raise error.Abort(_('%s certificate error: '
810 810 'no certificate received') % host)
811 811
812 812 if settings['disablecertverification']:
813 813 # We don't print the certificate fingerprint because it shouldn't
814 814 # be necessary: if the user requested certificate verification be
815 815 # disabled, they presumably already saw a message about the inability
816 816 # to verify the certificate and this message would have printed the
817 817 # fingerprint. So printing the fingerprint here adds little to no
818 818 # value.
819 819 ui.warn(_('warning: connection security to %s is disabled per current '
820 820 'settings; communication is susceptible to eavesdropping '
821 821 'and tampering\n') % host)
822 822 return
823 823
824 824 # If a certificate fingerprint is pinned, use it and only it to
825 825 # validate the remote cert.
826 826 peerfingerprints = {
827 827 'sha1': node.hex(hashlib.sha1(peercert).digest()),
828 828 'sha256': node.hex(hashlib.sha256(peercert).digest()),
829 829 'sha512': node.hex(hashlib.sha512(peercert).digest()),
830 830 }
831 831
832 832 def fmtfingerprint(s):
833 833 return ':'.join([s[x:x + 2] for x in range(0, len(s), 2)])
834 834
835 835 nicefingerprint = 'sha256:%s' % fmtfingerprint(peerfingerprints['sha256'])
836 836
837 837 if settings['certfingerprints']:
838 838 for hash, fingerprint in settings['certfingerprints']:
839 839 if peerfingerprints[hash].lower() == fingerprint:
840 840 ui.debug('%s certificate matched fingerprint %s:%s\n' %
841 841 (host, hash, fmtfingerprint(fingerprint)))
842 842 if settings['legacyfingerprint']:
843 843 ui.warn(_('(SHA-1 fingerprint for %s found in legacy '
844 844 '[hostfingerprints] section; '
845 845 'if you trust this fingerprint, remove the old '
846 846 'SHA-1 fingerprint from [hostfingerprints] and '
847 847 'add the following entry to the new '
848 848 '[hostsecurity] section: %s:fingerprints=%s)\n') %
849 849 (host, host, nicefingerprint))
850 850 return
851 851
852 852 # Pinned fingerprint didn't match. This is a fatal error.
853 853 if settings['legacyfingerprint']:
854 854 section = 'hostfingerprint'
855 855 nice = fmtfingerprint(peerfingerprints['sha1'])
856 856 else:
857 857 section = 'hostsecurity'
858 858 nice = '%s:%s' % (hash, fmtfingerprint(peerfingerprints[hash]))
859 859 raise error.Abort(_('certificate for %s has unexpected '
860 860 'fingerprint %s') % (host, nice),
861 861 hint=_('check %s configuration') % section)
862 862
863 863 # Security is enabled but no CAs are loaded. We can't establish trust
864 864 # for the cert so abort.
865 865 if not sock._hgstate['caloaded']:
866 866 raise error.Abort(
867 867 _('unable to verify security of %s (no loaded CA certificates); '
868 868 'refusing to connect') % host,
869 869 hint=_('see https://mercurial-scm.org/wiki/SecureConnections for '
870 870 'how to configure Mercurial to avoid this error or set '
871 871 'hostsecurity.%s:fingerprints=%s to trust this server') %
872 872 (host, nicefingerprint))
873 873
874 874 msg = _verifycert(peercert2, shost)
875 875 if msg:
876 876 raise error.Abort(_('%s certificate error: %s') % (host, msg),
877 877 hint=_('set hostsecurity.%s:certfingerprints=%s '
878 878 'config setting or use --insecure to connect '
879 879 'insecurely') %
880 880 (host, nicefingerprint))
@@ -1,1881 +1,1893 b''
1 1 Set up a repo
2 2
3 3 $ cat <<EOF >> $HGRCPATH
4 4 > [ui]
5 5 > interactive = true
6 6 > [extensions]
7 7 > record =
8 8 > EOF
9 9
10 10 $ hg init a
11 11 $ cd a
12 12
13 13 Select no files
14 14
15 15 $ touch empty-rw
16 16 $ hg add empty-rw
17 17
18 18 $ hg record --config ui.interactive=false
19 19 abort: running non-interactively, use commit instead
20 20 [255]
21 21 $ hg commit -i --config ui.interactive=false
22 22 abort: running non-interactively
23 23 [255]
24 24 $ hg commit -i empty-rw<<EOF
25 25 > n
26 26 > EOF
27 27 diff --git a/empty-rw b/empty-rw
28 28 new file mode 100644
29 29 abort: empty commit message
30 30 [255]
31 31
32 32 $ hg tip -p
33 33 changeset: -1:000000000000
34 34 tag: tip
35 35 user:
36 36 date: Thu Jan 01 00:00:00 1970 +0000
37 37
38 38
39 39
40 40 Select files but no hunks
41 41
42 42 $ hg commit -i empty-rw<<EOF
43 43 > y
44 44 > n
45 45 > EOF
46 46 diff --git a/empty-rw b/empty-rw
47 47 new file mode 100644
48 48 abort: empty commit message
49 49 [255]
50 50
51 51 $ hg tip -p
52 52 changeset: -1:000000000000
53 53 tag: tip
54 54 user:
55 55 date: Thu Jan 01 00:00:00 1970 +0000
56 56
57 57
58 58
59 59 Abort for untracked
60 60
61 61 $ touch untracked
62 62 $ hg commit -i -m should-fail empty-rw untracked
63 63 abort: untracked: file not tracked!
64 64 [255]
65 65 $ rm untracked
66 66
67 67 Record empty file
68 68
69 69 $ hg commit -i -d '0 0' -m empty empty-rw<<EOF
70 70 > y
71 71 > EOF
72 72 diff --git a/empty-rw b/empty-rw
73 73 new file mode 100644
74 74
75 75 $ hg tip -p
76 76 changeset: 0:c0708cf4e46e
77 77 tag: tip
78 78 user: test
79 79 date: Thu Jan 01 00:00:00 1970 +0000
80 80 summary: empty
81 81
82 82
83 83
84 84 Summary shows we updated to the new cset
85 85
86 86 $ hg summary
87 87 parent: 0:c0708cf4e46e tip
88 88 empty
89 89 branch: default
90 90 commit: (clean)
91 91 update: (current)
92 92 phases: 1 draft
93 93
94 94 Rename empty file
95 95
96 96 $ hg mv empty-rw empty-rename
97 97 $ hg commit -i -d '1 0' -m rename<<EOF
98 98 > y
99 99 > EOF
100 100 diff --git a/empty-rw b/empty-rename
101 101 rename from empty-rw
102 102 rename to empty-rename
103 103 examine changes to 'empty-rw' and 'empty-rename'? [Ynesfdaq?] y
104 104
105 105
106 106 $ hg tip -p
107 107 changeset: 1:d695e8dcb197
108 108 tag: tip
109 109 user: test
110 110 date: Thu Jan 01 00:00:01 1970 +0000
111 111 summary: rename
112 112
113 113
114 114
115 115 Copy empty file
116 116
117 117 $ hg cp empty-rename empty-copy
118 118 $ hg commit -i -d '2 0' -m copy<<EOF
119 119 > y
120 120 > EOF
121 121 diff --git a/empty-rename b/empty-copy
122 122 copy from empty-rename
123 123 copy to empty-copy
124 124 examine changes to 'empty-rename' and 'empty-copy'? [Ynesfdaq?] y
125 125
126 126
127 127 $ hg tip -p
128 128 changeset: 2:1d4b90bea524
129 129 tag: tip
130 130 user: test
131 131 date: Thu Jan 01 00:00:02 1970 +0000
132 132 summary: copy
133 133
134 134
135 135
136 136 Delete empty file
137 137
138 138 $ hg rm empty-copy
139 139 $ hg commit -i -d '3 0' -m delete<<EOF
140 140 > y
141 141 > EOF
142 142 diff --git a/empty-copy b/empty-copy
143 143 deleted file mode 100644
144 144 examine changes to 'empty-copy'? [Ynesfdaq?] y
145 145
146 146
147 147 $ hg tip -p
148 148 changeset: 3:b39a238f01a1
149 149 tag: tip
150 150 user: test
151 151 date: Thu Jan 01 00:00:03 1970 +0000
152 152 summary: delete
153 153
154 154
155 155
156 156 Add binary file
157 157
158 158 $ hg bundle --type v1 --base -2 tip.bundle
159 159 1 changesets found
160 160 $ hg add tip.bundle
161 161 $ hg commit -i -d '4 0' -m binary<<EOF
162 162 > y
163 163 > EOF
164 164 diff --git a/tip.bundle b/tip.bundle
165 165 new file mode 100644
166 166 this is a binary file
167 167 examine changes to 'tip.bundle'? [Ynesfdaq?] y
168 168
169 169
170 170 $ hg tip -p
171 171 changeset: 4:ad816da3711e
172 172 tag: tip
173 173 user: test
174 174 date: Thu Jan 01 00:00:04 1970 +0000
175 175 summary: binary
176 176
177 177 diff -r b39a238f01a1 -r ad816da3711e tip.bundle
178 178 Binary file tip.bundle has changed
179 179
180 180
181 181 Change binary file
182 182
183 183 $ hg bundle --base -2 --type v1 tip.bundle
184 184 1 changesets found
185 185 $ hg commit -i -d '5 0' -m binary-change<<EOF
186 186 > y
187 187 > EOF
188 188 diff --git a/tip.bundle b/tip.bundle
189 189 this modifies a binary file (all or nothing)
190 190 examine changes to 'tip.bundle'? [Ynesfdaq?] y
191 191
192 192
193 193 $ hg tip -p
194 194 changeset: 5:dccd6f3eb485
195 195 tag: tip
196 196 user: test
197 197 date: Thu Jan 01 00:00:05 1970 +0000
198 198 summary: binary-change
199 199
200 200 diff -r ad816da3711e -r dccd6f3eb485 tip.bundle
201 201 Binary file tip.bundle has changed
202 202
203 203
204 204 Rename and change binary file
205 205
206 206 $ hg mv tip.bundle top.bundle
207 207 $ hg bundle --base -2 --type v1 top.bundle
208 208 1 changesets found
209 209 $ hg commit -i -d '6 0' -m binary-change-rename<<EOF
210 210 > y
211 211 > EOF
212 212 diff --git a/tip.bundle b/top.bundle
213 213 rename from tip.bundle
214 214 rename to top.bundle
215 215 this modifies a binary file (all or nothing)
216 216 examine changes to 'tip.bundle' and 'top.bundle'? [Ynesfdaq?] y
217 217
218 218
219 219 $ hg tip -p
220 220 changeset: 6:7fa44105f5b3
221 221 tag: tip
222 222 user: test
223 223 date: Thu Jan 01 00:00:06 1970 +0000
224 224 summary: binary-change-rename
225 225
226 226 diff -r dccd6f3eb485 -r 7fa44105f5b3 tip.bundle
227 227 Binary file tip.bundle has changed
228 228 diff -r dccd6f3eb485 -r 7fa44105f5b3 top.bundle
229 229 Binary file top.bundle has changed
230 230
231 231
232 232 Add plain file
233 233
234 234 $ for i in 1 2 3 4 5 6 7 8 9 10; do
235 235 > echo $i >> plain
236 236 > done
237 237
238 238 $ hg add plain
239 239 $ hg commit -i -d '7 0' -m plain plain<<EOF
240 240 > y
241 241 > y
242 242 > EOF
243 243 diff --git a/plain b/plain
244 244 new file mode 100644
245 245 @@ -0,0 +1,10 @@
246 246 +1
247 247 +2
248 248 +3
249 249 +4
250 250 +5
251 251 +6
252 252 +7
253 253 +8
254 254 +9
255 255 +10
256 256 record this change to 'plain'? [Ynesfdaq?] y
257 257
258 258 $ hg tip -p
259 259 changeset: 7:11fb457c1be4
260 260 tag: tip
261 261 user: test
262 262 date: Thu Jan 01 00:00:07 1970 +0000
263 263 summary: plain
264 264
265 265 diff -r 7fa44105f5b3 -r 11fb457c1be4 plain
266 266 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
267 267 +++ b/plain Thu Jan 01 00:00:07 1970 +0000
268 268 @@ -0,0 +1,10 @@
269 269 +1
270 270 +2
271 271 +3
272 272 +4
273 273 +5
274 274 +6
275 275 +7
276 276 +8
277 277 +9
278 278 +10
279 279
280 280 Modify end of plain file with username unset
281 281
282 282 $ echo 11 >> plain
283 283 $ unset HGUSER
284 284 $ hg commit -i --config ui.username= -d '8 0' -m end plain
285 285 abort: no username supplied
286 286 (use 'hg config --edit' to set your username)
287 287 [255]
288 288
289 289
290 290 Modify end of plain file, also test that diffopts are accounted for
291 291
292 292 $ HGUSER="test"
293 293 $ export HGUSER
294 294 $ hg commit -i --config diff.showfunc=true -d '8 0' -m end plain <<EOF
295 295 > y
296 296 > y
297 297 > EOF
298 298 diff --git a/plain b/plain
299 299 1 hunks, 1 lines changed
300 300 @@ -8,3 +8,4 @@ 7
301 301 8
302 302 9
303 303 10
304 304 +11
305 305 record this change to 'plain'? [Ynesfdaq?] y
306 306
307 307
308 308 Modify end of plain file, no EOL
309 309
310 310 $ hg tip --template '{node}' >> plain
311 311 $ hg commit -i -d '9 0' -m noeol plain <<EOF
312 312 > y
313 313 > y
314 314 > EOF
315 315 diff --git a/plain b/plain
316 316 1 hunks, 1 lines changed
317 317 @@ -9,3 +9,4 @@ 8
318 318 9
319 319 10
320 320 11
321 321 +7264f99c5f5ff3261504828afa4fb4d406c3af54
322 322 \ No newline at end of file
323 323 record this change to 'plain'? [Ynesfdaq?] y
324 324
325 325
326 326 Record showfunc should preserve function across sections
327 327
328 328 $ cat > f1.py <<NO_CHECK_EOF
329 329 > def annotate(ui, repo, *pats, **opts):
330 330 > """show changeset information by line for each file
331 331 >
332 332 > List changes in files, showing the revision id responsible for
333 333 > each line.
334 334 >
335 335 > This command is useful for discovering when a change was made and
336 336 > by whom.
337 337 >
338 338 > If you include -f/-u/-d, the revision number is suppressed unless
339 339 > you also include -the revision number is suppressed unless
340 340 > you also include -n.
341 341 >
342 342 > Without the -a/--text option, annotate will avoid processing files
343 343 > it detects as binary. With -a, annotate will annotate the file
344 344 > anyway, although the results will probably be neither useful
345 345 > nor desirable.
346 346 >
347 347 > Returns 0 on success.
348 348 > """
349 349 > return 0
350 350 > def archive(ui, repo, dest, **opts):
351 351 > '''create an unversioned archive of a repository revision
352 352 >
353 353 > By default, the revision used is the parent of the working
354 354 > directory; use -r/--rev to specify a different revision.
355 355 >
356 356 > The archive type is automatically detected based on file
357 357 > extension (to override, use -t/--type).
358 358 >
359 359 > .. container:: verbose
360 360 >
361 361 > Valid types are:
362 362 > NO_CHECK_EOF
363 363 $ hg add f1.py
364 364 $ hg commit -m funcs
365 365 $ cat > f1.py <<NO_CHECK_EOF
366 366 > def annotate(ui, repo, *pats, **opts):
367 367 > """show changeset information by line for each file
368 368 >
369 369 > List changes in files, showing the revision id responsible for
370 370 > each line
371 371 >
372 372 > This command is useful for discovering when a change was made and
373 373 > by whom.
374 374 >
375 375 > Without the -a/--text option, annotate will avoid processing files
376 376 > it detects as binary. With -a, annotate will annotate the file
377 377 > anyway, although the results will probably be neither useful
378 378 > nor desirable.
379 379 >
380 380 > Returns 0 on success.
381 381 > """
382 382 > return 0
383 383 > def archive(ui, repo, dest, **opts):
384 384 > '''create an unversioned archive of a repository revision
385 385 >
386 386 > By default, the revision used is the parent of the working
387 387 > directory; use -r/--rev to specify a different revision.
388 388 >
389 389 > The archive type is automatically detected based on file
390 390 > extension (or override using -t/--type).
391 391 >
392 392 > .. container:: verbose
393 393 >
394 394 > Valid types are:
395 395 > NO_CHECK_EOF
396 396 $ hg commit -i -m interactive <<EOF
397 397 > y
398 398 > y
399 399 > y
400 400 > y
401 401 > EOF
402 402 diff --git a/f1.py b/f1.py
403 403 3 hunks, 6 lines changed
404 404 examine changes to 'f1.py'? [Ynesfdaq?] y
405 405
406 406 @@ -2,8 +2,8 @@ def annotate(ui, repo, *pats, **opts):
407 407 """show changeset information by line for each file
408 408
409 409 List changes in files, showing the revision id responsible for
410 410 - each line.
411 411 + each line
412 412
413 413 This command is useful for discovering when a change was made and
414 414 by whom.
415 415
416 416 record change 1/3 to 'f1.py'? [Ynesfdaq?] y
417 417
418 418 @@ -6,11 +6,7 @@ def annotate(ui, repo, *pats, **opts):
419 419
420 420 This command is useful for discovering when a change was made and
421 421 by whom.
422 422
423 423 - If you include -f/-u/-d, the revision number is suppressed unless
424 424 - you also include -the revision number is suppressed unless
425 425 - you also include -n.
426 426 -
427 427 Without the -a/--text option, annotate will avoid processing files
428 428 it detects as binary. With -a, annotate will annotate the file
429 429 anyway, although the results will probably be neither useful
430 430 record change 2/3 to 'f1.py'? [Ynesfdaq?] y
431 431
432 432 @@ -26,7 +22,7 @@ def archive(ui, repo, dest, **opts):
433 433 directory; use -r/--rev to specify a different revision.
434 434
435 435 The archive type is automatically detected based on file
436 436 - extension (to override, use -t/--type).
437 437 + extension (or override using -t/--type).
438 438
439 439 .. container:: verbose
440 440
441 441 record change 3/3 to 'f1.py'? [Ynesfdaq?] y
442 442
443 443
444 444 Modify end of plain file, add EOL
445 445
446 446 $ echo >> plain
447 447 $ echo 1 > plain2
448 448 $ hg add plain2
449 449 $ hg commit -i -d '10 0' -m eol plain plain2 <<EOF
450 450 > y
451 451 > y
452 452 > y
453 453 > y
454 454 > EOF
455 455 diff --git a/plain b/plain
456 456 1 hunks, 1 lines changed
457 457 @@ -9,4 +9,4 @@ 8
458 458 9
459 459 10
460 460 11
461 461 -7264f99c5f5ff3261504828afa4fb4d406c3af54
462 462 \ No newline at end of file
463 463 +7264f99c5f5ff3261504828afa4fb4d406c3af54
464 464 record change 1/2 to 'plain'? [Ynesfdaq?] y
465 465
466 466 diff --git a/plain2 b/plain2
467 467 new file mode 100644
468 468 @@ -0,0 +1,1 @@
469 469 +1
470 470 record change 2/2 to 'plain2'? [Ynesfdaq?] y
471 471
472 472 Modify beginning, trim end, record both, add another file to test
473 473 changes numbering
474 474
475 475 $ rm plain
476 476 $ for i in 2 2 3 4 5 6 7 8 9 10; do
477 477 > echo $i >> plain
478 478 > done
479 479 $ echo 2 >> plain2
480 480
481 481 $ hg commit -i -d '10 0' -m begin-and-end plain plain2 <<EOF
482 482 > y
483 483 > y
484 484 > y
485 485 > y
486 486 > y
487 487 > EOF
488 488 diff --git a/plain b/plain
489 489 2 hunks, 3 lines changed
490 490 @@ -1,4 +1,4 @@
491 491 -1
492 492 +2
493 493 2
494 494 3
495 495 4
496 496 record change 1/3 to 'plain'? [Ynesfdaq?] y
497 497
498 498 @@ -8,5 +8,3 @@ 7
499 499 8
500 500 9
501 501 10
502 502 -11
503 503 -7264f99c5f5ff3261504828afa4fb4d406c3af54
504 504 record change 2/3 to 'plain'? [Ynesfdaq?] y
505 505
506 506 diff --git a/plain2 b/plain2
507 507 1 hunks, 1 lines changed
508 508 @@ -1,1 +1,2 @@
509 509 1
510 510 +2
511 511 record change 3/3 to 'plain2'? [Ynesfdaq?] y
512 512
513 513
514 514 $ hg tip -p
515 515 changeset: 13:f941910cff62
516 516 tag: tip
517 517 user: test
518 518 date: Thu Jan 01 00:00:10 1970 +0000
519 519 summary: begin-and-end
520 520
521 521 diff -r 33abe24d946c -r f941910cff62 plain
522 522 --- a/plain Thu Jan 01 00:00:10 1970 +0000
523 523 +++ b/plain Thu Jan 01 00:00:10 1970 +0000
524 524 @@ -1,4 +1,4 @@
525 525 -1
526 526 +2
527 527 2
528 528 3
529 529 4
530 530 @@ -8,5 +8,3 @@
531 531 8
532 532 9
533 533 10
534 534 -11
535 535 -7264f99c5f5ff3261504828afa4fb4d406c3af54
536 536 diff -r 33abe24d946c -r f941910cff62 plain2
537 537 --- a/plain2 Thu Jan 01 00:00:10 1970 +0000
538 538 +++ b/plain2 Thu Jan 01 00:00:10 1970 +0000
539 539 @@ -1,1 +1,2 @@
540 540 1
541 541 +2
542 542
543 543
544 544 Trim beginning, modify end
545 545
546 546 $ rm plain
547 547 > for i in 4 5 6 7 8 9 10.new; do
548 548 > echo $i >> plain
549 549 > done
550 550
551 551 Record end
552 552
553 553 $ hg commit -i -d '11 0' -m end-only plain <<EOF
554 554 > n
555 555 > y
556 556 > EOF
557 557 diff --git a/plain b/plain
558 558 2 hunks, 4 lines changed
559 559 @@ -1,9 +1,6 @@
560 560 -2
561 561 -2
562 562 -3
563 563 4
564 564 5
565 565 6
566 566 7
567 567 8
568 568 9
569 569 record change 1/2 to 'plain'? [Ynesfdaq?] n
570 570
571 571 @@ -4,7 +1,7 @@
572 572 4
573 573 5
574 574 6
575 575 7
576 576 8
577 577 9
578 578 -10
579 579 +10.new
580 580 record change 2/2 to 'plain'? [Ynesfdaq?] y
581 581
582 582
583 583 $ hg tip -p
584 584 changeset: 14:4915f538659b
585 585 tag: tip
586 586 user: test
587 587 date: Thu Jan 01 00:00:11 1970 +0000
588 588 summary: end-only
589 589
590 590 diff -r f941910cff62 -r 4915f538659b plain
591 591 --- a/plain Thu Jan 01 00:00:10 1970 +0000
592 592 +++ b/plain Thu Jan 01 00:00:11 1970 +0000
593 593 @@ -7,4 +7,4 @@
594 594 7
595 595 8
596 596 9
597 597 -10
598 598 +10.new
599 599
600 600
601 601 Record beginning
602 602
603 603 $ hg commit -i -d '12 0' -m begin-only plain <<EOF
604 604 > y
605 605 > y
606 606 > EOF
607 607 diff --git a/plain b/plain
608 608 1 hunks, 3 lines changed
609 609 @@ -1,6 +1,3 @@
610 610 -2
611 611 -2
612 612 -3
613 613 4
614 614 5
615 615 6
616 616 record this change to 'plain'? [Ynesfdaq?] y
617 617
618 618
619 619 $ hg tip -p
620 620 changeset: 15:1b1f93d4b94b
621 621 tag: tip
622 622 user: test
623 623 date: Thu Jan 01 00:00:12 1970 +0000
624 624 summary: begin-only
625 625
626 626 diff -r 4915f538659b -r 1b1f93d4b94b plain
627 627 --- a/plain Thu Jan 01 00:00:11 1970 +0000
628 628 +++ b/plain Thu Jan 01 00:00:12 1970 +0000
629 629 @@ -1,6 +1,3 @@
630 630 -2
631 631 -2
632 632 -3
633 633 4
634 634 5
635 635 6
636 636
637 637
638 638 Add to beginning, trim from end
639 639
640 640 $ rm plain
641 641 $ for i in 1 2 3 4 5 6 7 8 9; do
642 642 > echo $i >> plain
643 643 > done
644 644
645 645 Record end
646 646
647 647 $ hg commit -i --traceback -d '13 0' -m end-again plain<<EOF
648 648 > n
649 649 > y
650 650 > EOF
651 651 diff --git a/plain b/plain
652 652 2 hunks, 4 lines changed
653 653 @@ -1,6 +1,9 @@
654 654 +1
655 655 +2
656 656 +3
657 657 4
658 658 5
659 659 6
660 660 7
661 661 8
662 662 9
663 663 record change 1/2 to 'plain'? [Ynesfdaq?] n
664 664
665 665 @@ -1,7 +4,6 @@
666 666 4
667 667 5
668 668 6
669 669 7
670 670 8
671 671 9
672 672 -10.new
673 673 record change 2/2 to 'plain'? [Ynesfdaq?] y
674 674
675 675
676 676 Add to beginning, middle, end
677 677
678 678 $ rm plain
679 679 $ for i in 1 2 3 4 5 5.new 5.reallynew 6 7 8 9 10 11; do
680 680 > echo $i >> plain
681 681 > done
682 682
683 683 Record beginning, middle, and test that format-breaking diffopts are ignored
684 684
685 685 $ hg commit -i --config diff.noprefix=True -d '14 0' -m middle-only plain <<EOF
686 686 > y
687 687 > y
688 688 > n
689 689 > EOF
690 690 diff --git a/plain b/plain
691 691 3 hunks, 7 lines changed
692 692 @@ -1,2 +1,5 @@
693 693 +1
694 694 +2
695 695 +3
696 696 4
697 697 5
698 698 record change 1/3 to 'plain'? [Ynesfdaq?] y
699 699
700 700 @@ -1,6 +4,8 @@
701 701 4
702 702 5
703 703 +5.new
704 704 +5.reallynew
705 705 6
706 706 7
707 707 8
708 708 9
709 709 record change 2/3 to 'plain'? [Ynesfdaq?] y
710 710
711 711 @@ -3,4 +8,6 @@
712 712 6
713 713 7
714 714 8
715 715 9
716 716 +10
717 717 +11
718 718 record change 3/3 to 'plain'? [Ynesfdaq?] n
719 719
720 720
721 721 $ hg tip -p
722 722 changeset: 17:41cf3f5c55ae
723 723 tag: tip
724 724 user: test
725 725 date: Thu Jan 01 00:00:14 1970 +0000
726 726 summary: middle-only
727 727
728 728 diff -r a69d252246e1 -r 41cf3f5c55ae plain
729 729 --- a/plain Thu Jan 01 00:00:13 1970 +0000
730 730 +++ b/plain Thu Jan 01 00:00:14 1970 +0000
731 731 @@ -1,5 +1,10 @@
732 732 +1
733 733 +2
734 734 +3
735 735 4
736 736 5
737 737 +5.new
738 738 +5.reallynew
739 739 6
740 740 7
741 741 8
742 742
743 743
744 744 Record end
745 745
746 746 $ hg commit -i -d '15 0' -m end-only plain <<EOF
747 747 > y
748 748 > y
749 749 > EOF
750 750 diff --git a/plain b/plain
751 751 1 hunks, 2 lines changed
752 752 @@ -9,3 +9,5 @@ 6
753 753 7
754 754 8
755 755 9
756 756 +10
757 757 +11
758 758 record this change to 'plain'? [Ynesfdaq?] y
759 759
760 760
761 761 $ hg tip -p
762 762 changeset: 18:58a72f46bc24
763 763 tag: tip
764 764 user: test
765 765 date: Thu Jan 01 00:00:15 1970 +0000
766 766 summary: end-only
767 767
768 768 diff -r 41cf3f5c55ae -r 58a72f46bc24 plain
769 769 --- a/plain Thu Jan 01 00:00:14 1970 +0000
770 770 +++ b/plain Thu Jan 01 00:00:15 1970 +0000
771 771 @@ -9,3 +9,5 @@
772 772 7
773 773 8
774 774 9
775 775 +10
776 776 +11
777 777
778 Interactive commit can name a directory instead of files (issue6131)
778 779
779 780 $ mkdir subdir
781 $ echo a > subdir/a
782 $ hg ci -d '16 0' -i subdir -Amsubdir <<EOF
783 > y
784 > y
785 > EOF
786 adding subdir/a
787 diff --git a/subdir/a b/subdir/a
788 new file mode 100644
789 examine changes to 'subdir/a'? [Ynesfdaq?] y
790
791 @@ -0,0 +1,1 @@
792 +a
793 record this change to 'subdir/a'? [Ynesfdaq?] y
794
780 795 $ cd subdir
781 $ echo a > a
782 $ hg ci -d '16 0' -Amsubdir
783 adding subdir/a
784 796
785 797 $ echo a >> a
786 798 $ hg commit -i -d '16 0' -m subdir-change a <<EOF
787 799 > y
788 800 > y
789 801 > EOF
790 802 diff --git a/subdir/a b/subdir/a
791 803 1 hunks, 1 lines changed
792 804 @@ -1,1 +1,2 @@
793 805 a
794 806 +a
795 807 record this change to 'subdir/a'? [Ynesfdaq?] y
796 808
797 809
798 810 $ hg tip -p
799 811 changeset: 20:e0f6b99f6c49
800 812 tag: tip
801 813 user: test
802 814 date: Thu Jan 01 00:00:16 1970 +0000
803 815 summary: subdir-change
804 816
805 817 diff -r abd26b51de37 -r e0f6b99f6c49 subdir/a
806 818 --- a/subdir/a Thu Jan 01 00:00:16 1970 +0000
807 819 +++ b/subdir/a Thu Jan 01 00:00:16 1970 +0000
808 820 @@ -1,1 +1,2 @@
809 821 a
810 822 +a
811 823
812 824
813 825 $ echo a > f1
814 826 $ echo b > f2
815 827 $ hg add f1 f2
816 828
817 829 $ hg ci -mz -d '17 0'
818 830
819 831 $ echo a >> f1
820 832 $ echo b >> f2
821 833
822 834 Help, quit
823 835
824 836 $ hg commit -i <<EOF
825 837 > ?
826 838 > q
827 839 > EOF
828 840 diff --git a/subdir/f1 b/subdir/f1
829 841 1 hunks, 1 lines changed
830 842 examine changes to 'subdir/f1'? [Ynesfdaq?] ?
831 843
832 844 y - yes, record this change
833 845 n - no, skip this change
834 846 e - edit this change manually
835 847 s - skip remaining changes to this file
836 848 f - record remaining changes to this file
837 849 d - done, skip remaining changes and files
838 850 a - record all changes to all remaining files
839 851 q - quit, recording no changes
840 852 ? - ? (display help)
841 853 examine changes to 'subdir/f1'? [Ynesfdaq?] q
842 854
843 855 abort: user quit
844 856 [255]
845 857
846 858 Patterns
847 859
848 860 $ hg commit -i 'glob:f*' << EOF
849 861 > y
850 862 > n
851 863 > y
852 864 > n
853 865 > EOF
854 866 diff --git a/subdir/f1 b/subdir/f1
855 867 1 hunks, 1 lines changed
856 868 examine changes to 'subdir/f1'? [Ynesfdaq?] y
857 869
858 870 @@ -1,1 +1,2 @@
859 871 a
860 872 +a
861 873 record change 1/2 to 'subdir/f1'? [Ynesfdaq?] n
862 874
863 875 diff --git a/subdir/f2 b/subdir/f2
864 876 1 hunks, 1 lines changed
865 877 examine changes to 'subdir/f2'? [Ynesfdaq?] y
866 878
867 879 @@ -1,1 +1,2 @@
868 880 b
869 881 +b
870 882 record change 2/2 to 'subdir/f2'? [Ynesfdaq?] n
871 883
872 884 no changes to record
873 885 [1]
874 886
875 887 #if gettext
876 888
877 889 Test translated help message
878 890
879 891 str.lower() instead of encoding.lower(str) on translated message might
880 892 make message meaningless, because some encoding uses 0x41(A) - 0x5a(Z)
881 893 as the second or later byte of multi-byte character.
882 894
883 895 For example, "\x8bL\x98^" (translation of "record" in ja_JP.cp932)
884 896 contains 0x4c (L). str.lower() replaces 0x4c(L) by 0x6c(l) and this
885 897 replacement makes message meaningless.
886 898
887 899 This tests that translated help message is lower()-ed correctly.
888 900
889 901 $ LANGUAGE=ja
890 902 $ export LANGUAGE
891 903
892 904 $ cat > $TESTTMP/escape.py <<EOF
893 905 > from __future__ import absolute_import
894 906 > from mercurial import (
895 907 > pycompat,
896 908 > )
897 909 > from mercurial.utils import (
898 910 > procutil,
899 911 > )
900 912 > def escape(c):
901 913 > o = ord(c)
902 914 > if o < 0x80:
903 915 > return c
904 916 > else:
905 917 > return br'\x%02x' % o # escape char setting MSB
906 918 > for l in procutil.stdin:
907 919 > procutil.stdout.write(
908 920 > b''.join(escape(c) for c in pycompat.iterbytestr(l)))
909 921 > EOF
910 922
911 923 $ hg commit -i --encoding cp932 2>&1 <<EOF | "$PYTHON" $TESTTMP/escape.py | grep '^y - '
912 924 > ?
913 925 > q
914 926 > EOF
915 927 y - \x82\xb1\x82\xcc\x95\xcf\x8dX\x82\xf0\x8bL\x98^(yes)
916 928
917 929 $ LANGUAGE=
918 930 #endif
919 931
920 932 Skip
921 933
922 934 $ hg commit -i <<EOF
923 935 > s
924 936 > EOF
925 937 diff --git a/subdir/f1 b/subdir/f1
926 938 1 hunks, 1 lines changed
927 939 examine changes to 'subdir/f1'? [Ynesfdaq?] s
928 940
929 941 diff --git a/subdir/f2 b/subdir/f2
930 942 1 hunks, 1 lines changed
931 943 examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected
932 944 [255]
933 945
934 946 No
935 947
936 948 $ hg commit -i <<EOF
937 949 > n
938 950 > EOF
939 951 diff --git a/subdir/f1 b/subdir/f1
940 952 1 hunks, 1 lines changed
941 953 examine changes to 'subdir/f1'? [Ynesfdaq?] n
942 954
943 955 diff --git a/subdir/f2 b/subdir/f2
944 956 1 hunks, 1 lines changed
945 957 examine changes to 'subdir/f2'? [Ynesfdaq?] abort: response expected
946 958 [255]
947 959
948 960 f, quit
949 961
950 962 $ hg commit -i <<EOF
951 963 > f
952 964 > q
953 965 > EOF
954 966 diff --git a/subdir/f1 b/subdir/f1
955 967 1 hunks, 1 lines changed
956 968 examine changes to 'subdir/f1'? [Ynesfdaq?] f
957 969
958 970 diff --git a/subdir/f2 b/subdir/f2
959 971 1 hunks, 1 lines changed
960 972 examine changes to 'subdir/f2'? [Ynesfdaq?] q
961 973
962 974 abort: user quit
963 975 [255]
964 976
965 977 s, all
966 978
967 979 $ hg commit -i -d '18 0' -mx <<EOF
968 980 > s
969 981 > a
970 982 > EOF
971 983 diff --git a/subdir/f1 b/subdir/f1
972 984 1 hunks, 1 lines changed
973 985 examine changes to 'subdir/f1'? [Ynesfdaq?] s
974 986
975 987 diff --git a/subdir/f2 b/subdir/f2
976 988 1 hunks, 1 lines changed
977 989 examine changes to 'subdir/f2'? [Ynesfdaq?] a
978 990
979 991
980 992 $ hg tip -p
981 993 changeset: 22:6afbbefacf35
982 994 tag: tip
983 995 user: test
984 996 date: Thu Jan 01 00:00:18 1970 +0000
985 997 summary: x
986 998
987 999 diff -r b73c401c693c -r 6afbbefacf35 subdir/f2
988 1000 --- a/subdir/f2 Thu Jan 01 00:00:17 1970 +0000
989 1001 +++ b/subdir/f2 Thu Jan 01 00:00:18 1970 +0000
990 1002 @@ -1,1 +1,2 @@
991 1003 b
992 1004 +b
993 1005
994 1006
995 1007 f
996 1008
997 1009 $ hg commit -i -d '19 0' -my <<EOF
998 1010 > f
999 1011 > EOF
1000 1012 diff --git a/subdir/f1 b/subdir/f1
1001 1013 1 hunks, 1 lines changed
1002 1014 examine changes to 'subdir/f1'? [Ynesfdaq?] f
1003 1015
1004 1016
1005 1017 $ hg tip -p
1006 1018 changeset: 23:715028a33949
1007 1019 tag: tip
1008 1020 user: test
1009 1021 date: Thu Jan 01 00:00:19 1970 +0000
1010 1022 summary: y
1011 1023
1012 1024 diff -r 6afbbefacf35 -r 715028a33949 subdir/f1
1013 1025 --- a/subdir/f1 Thu Jan 01 00:00:18 1970 +0000
1014 1026 +++ b/subdir/f1 Thu Jan 01 00:00:19 1970 +0000
1015 1027 @@ -1,1 +1,2 @@
1016 1028 a
1017 1029 +a
1018 1030
1019 1031
1020 1032 #if execbit
1021 1033
1022 1034 Preserve chmod +x
1023 1035
1024 1036 $ chmod +x f1
1025 1037 $ echo a >> f1
1026 1038 $ hg commit -i -d '20 0' -mz <<EOF
1027 1039 > y
1028 1040 > y
1029 1041 > y
1030 1042 > EOF
1031 1043 diff --git a/subdir/f1 b/subdir/f1
1032 1044 old mode 100644
1033 1045 new mode 100755
1034 1046 1 hunks, 1 lines changed
1035 1047 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1036 1048
1037 1049 @@ -1,2 +1,3 @@
1038 1050 a
1039 1051 a
1040 1052 +a
1041 1053 record this change to 'subdir/f1'? [Ynesfdaq?] y
1042 1054
1043 1055
1044 1056 $ hg tip --config diff.git=True -p
1045 1057 changeset: 24:db967c1e5884
1046 1058 tag: tip
1047 1059 user: test
1048 1060 date: Thu Jan 01 00:00:20 1970 +0000
1049 1061 summary: z
1050 1062
1051 1063 diff --git a/subdir/f1 b/subdir/f1
1052 1064 old mode 100644
1053 1065 new mode 100755
1054 1066 --- a/subdir/f1
1055 1067 +++ b/subdir/f1
1056 1068 @@ -1,2 +1,3 @@
1057 1069 a
1058 1070 a
1059 1071 +a
1060 1072
1061 1073
1062 1074 Preserve execute permission on original
1063 1075
1064 1076 $ echo b >> f1
1065 1077 $ hg commit -i -d '21 0' -maa <<EOF
1066 1078 > y
1067 1079 > y
1068 1080 > y
1069 1081 > EOF
1070 1082 diff --git a/subdir/f1 b/subdir/f1
1071 1083 1 hunks, 1 lines changed
1072 1084 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1073 1085
1074 1086 @@ -1,3 +1,4 @@
1075 1087 a
1076 1088 a
1077 1089 a
1078 1090 +b
1079 1091 record this change to 'subdir/f1'? [Ynesfdaq?] y
1080 1092
1081 1093
1082 1094 $ hg tip --config diff.git=True -p
1083 1095 changeset: 25:88903aef81c3
1084 1096 tag: tip
1085 1097 user: test
1086 1098 date: Thu Jan 01 00:00:21 1970 +0000
1087 1099 summary: aa
1088 1100
1089 1101 diff --git a/subdir/f1 b/subdir/f1
1090 1102 --- a/subdir/f1
1091 1103 +++ b/subdir/f1
1092 1104 @@ -1,3 +1,4 @@
1093 1105 a
1094 1106 a
1095 1107 a
1096 1108 +b
1097 1109
1098 1110
1099 1111 Preserve chmod -x
1100 1112
1101 1113 $ chmod -x f1
1102 1114 $ echo c >> f1
1103 1115 $ hg commit -i -d '22 0' -mab <<EOF
1104 1116 > y
1105 1117 > y
1106 1118 > y
1107 1119 > EOF
1108 1120 diff --git a/subdir/f1 b/subdir/f1
1109 1121 old mode 100755
1110 1122 new mode 100644
1111 1123 1 hunks, 1 lines changed
1112 1124 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1113 1125
1114 1126 @@ -2,3 +2,4 @@ a
1115 1127 a
1116 1128 a
1117 1129 b
1118 1130 +c
1119 1131 record this change to 'subdir/f1'? [Ynesfdaq?] y
1120 1132
1121 1133
1122 1134 $ hg tip --config diff.git=True -p
1123 1135 changeset: 26:7af84b6cf560
1124 1136 tag: tip
1125 1137 user: test
1126 1138 date: Thu Jan 01 00:00:22 1970 +0000
1127 1139 summary: ab
1128 1140
1129 1141 diff --git a/subdir/f1 b/subdir/f1
1130 1142 old mode 100755
1131 1143 new mode 100644
1132 1144 --- a/subdir/f1
1133 1145 +++ b/subdir/f1
1134 1146 @@ -2,3 +2,4 @@
1135 1147 a
1136 1148 a
1137 1149 b
1138 1150 +c
1139 1151
1140 1152
1141 1153 #else
1142 1154
1143 1155 Slightly bogus tests to get almost same repo structure as when x bit is used
1144 1156 - but with different hashes.
1145 1157
1146 1158 Mock "Preserve chmod +x"
1147 1159
1148 1160 $ echo a >> f1
1149 1161 $ hg commit -i -d '20 0' -mz <<EOF
1150 1162 > y
1151 1163 > y
1152 1164 > y
1153 1165 > EOF
1154 1166 diff --git a/subdir/f1 b/subdir/f1
1155 1167 1 hunks, 1 lines changed
1156 1168 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1157 1169
1158 1170 @@ -1,2 +1,3 @@
1159 1171 a
1160 1172 a
1161 1173 +a
1162 1174 record this change to 'subdir/f1'? [Ynesfdaq?] y
1163 1175
1164 1176
1165 1177 $ hg tip --config diff.git=True -p
1166 1178 changeset: 24:c26cfe2c4eb0
1167 1179 tag: tip
1168 1180 user: test
1169 1181 date: Thu Jan 01 00:00:20 1970 +0000
1170 1182 summary: z
1171 1183
1172 1184 diff --git a/subdir/f1 b/subdir/f1
1173 1185 --- a/subdir/f1
1174 1186 +++ b/subdir/f1
1175 1187 @@ -1,2 +1,3 @@
1176 1188 a
1177 1189 a
1178 1190 +a
1179 1191
1180 1192
1181 1193 Mock "Preserve execute permission on original"
1182 1194
1183 1195 $ echo b >> f1
1184 1196 $ hg commit -i -d '21 0' -maa <<EOF
1185 1197 > y
1186 1198 > y
1187 1199 > y
1188 1200 > EOF
1189 1201 diff --git a/subdir/f1 b/subdir/f1
1190 1202 1 hunks, 1 lines changed
1191 1203 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1192 1204
1193 1205 @@ -1,3 +1,4 @@
1194 1206 a
1195 1207 a
1196 1208 a
1197 1209 +b
1198 1210 record this change to 'subdir/f1'? [Ynesfdaq?] y
1199 1211
1200 1212
1201 1213 $ hg tip --config diff.git=True -p
1202 1214 changeset: 25:a48d2d60adde
1203 1215 tag: tip
1204 1216 user: test
1205 1217 date: Thu Jan 01 00:00:21 1970 +0000
1206 1218 summary: aa
1207 1219
1208 1220 diff --git a/subdir/f1 b/subdir/f1
1209 1221 --- a/subdir/f1
1210 1222 +++ b/subdir/f1
1211 1223 @@ -1,3 +1,4 @@
1212 1224 a
1213 1225 a
1214 1226 a
1215 1227 +b
1216 1228
1217 1229
1218 1230 Mock "Preserve chmod -x"
1219 1231
1220 1232 $ chmod -x f1
1221 1233 $ echo c >> f1
1222 1234 $ hg commit -i -d '22 0' -mab <<EOF
1223 1235 > y
1224 1236 > y
1225 1237 > y
1226 1238 > EOF
1227 1239 diff --git a/subdir/f1 b/subdir/f1
1228 1240 1 hunks, 1 lines changed
1229 1241 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1230 1242
1231 1243 @@ -2,3 +2,4 @@ a
1232 1244 a
1233 1245 a
1234 1246 b
1235 1247 +c
1236 1248 record this change to 'subdir/f1'? [Ynesfdaq?] y
1237 1249
1238 1250
1239 1251 $ hg tip --config diff.git=True -p
1240 1252 changeset: 26:5cc89ae210fa
1241 1253 tag: tip
1242 1254 user: test
1243 1255 date: Thu Jan 01 00:00:22 1970 +0000
1244 1256 summary: ab
1245 1257
1246 1258 diff --git a/subdir/f1 b/subdir/f1
1247 1259 --- a/subdir/f1
1248 1260 +++ b/subdir/f1
1249 1261 @@ -2,3 +2,4 @@
1250 1262 a
1251 1263 a
1252 1264 b
1253 1265 +c
1254 1266
1255 1267
1256 1268 #endif
1257 1269
1258 1270 $ cd ..
1259 1271
1260 1272
1261 1273 Abort early when a merge is in progress
1262 1274
1263 1275 $ hg up 4
1264 1276 1 files updated, 0 files merged, 7 files removed, 0 files unresolved
1265 1277
1266 1278 $ touch iwillmergethat
1267 1279 $ hg add iwillmergethat
1268 1280
1269 1281 $ hg branch thatbranch
1270 1282 marked working directory as branch thatbranch
1271 1283 (branches are permanent and global, did you want a bookmark?)
1272 1284
1273 1285 $ hg ci -m'new head'
1274 1286
1275 1287 $ hg up default
1276 1288 7 files updated, 0 files merged, 2 files removed, 0 files unresolved
1277 1289
1278 1290 $ hg merge thatbranch
1279 1291 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1280 1292 (branch merge, don't forget to commit)
1281 1293
1282 1294 $ hg commit -i -m'will abort'
1283 1295 abort: cannot partially commit a merge (use "hg commit" instead)
1284 1296 [255]
1285 1297
1286 1298 $ hg up -C
1287 1299 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1288 1300
1289 1301 Editing patch (and ignoring trailing text)
1290 1302
1291 1303 $ cat > editor.sh << '__EOF__'
1292 1304 > sed -e 7d -e '5s/^-/ /' -e '/^# ---/i\
1293 1305 > trailing\nditto' "$1" > tmp
1294 1306 > mv tmp "$1"
1295 1307 > __EOF__
1296 1308 $ cat > editedfile << '__EOF__'
1297 1309 > This is the first line
1298 1310 > This is the second line
1299 1311 > This is the third line
1300 1312 > __EOF__
1301 1313 $ hg add editedfile
1302 1314 $ hg commit -medit-patch-1
1303 1315 $ cat > editedfile << '__EOF__'
1304 1316 > This line has changed
1305 1317 > This change will be committed
1306 1318 > This is the third line
1307 1319 > __EOF__
1308 1320 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-2 <<EOF
1309 1321 > y
1310 1322 > e
1311 1323 > EOF
1312 1324 diff --git a/editedfile b/editedfile
1313 1325 1 hunks, 2 lines changed
1314 1326 examine changes to 'editedfile'? [Ynesfdaq?] y
1315 1327
1316 1328 @@ -1,3 +1,3 @@
1317 1329 -This is the first line
1318 1330 -This is the second line
1319 1331 +This line has changed
1320 1332 +This change will be committed
1321 1333 This is the third line
1322 1334 record this change to 'editedfile'? [Ynesfdaq?] e
1323 1335
1324 1336 $ cat editedfile
1325 1337 This line has changed
1326 1338 This change will be committed
1327 1339 This is the third line
1328 1340 $ hg cat -r tip editedfile
1329 1341 This is the first line
1330 1342 This change will be committed
1331 1343 This is the third line
1332 1344 $ hg revert editedfile
1333 1345
1334 1346 Trying to edit patch for whole file
1335 1347
1336 1348 $ echo "This is the fourth line" >> editedfile
1337 1349 $ hg commit -i <<EOF
1338 1350 > e
1339 1351 > q
1340 1352 > EOF
1341 1353 diff --git a/editedfile b/editedfile
1342 1354 1 hunks, 1 lines changed
1343 1355 examine changes to 'editedfile'? [Ynesfdaq?] e
1344 1356
1345 1357 cannot edit patch for whole file
1346 1358 examine changes to 'editedfile'? [Ynesfdaq?] q
1347 1359
1348 1360 abort: user quit
1349 1361 [255]
1350 1362 $ hg revert editedfile
1351 1363
1352 1364 Removing changes from patch
1353 1365
1354 1366 $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp
1355 1367 $ mv tmp editedfile
1356 1368 $ echo "This line has been added" >> editedfile
1357 1369 $ cat > editor.sh << '__EOF__'
1358 1370 > sed -e 's/^[-+]/ /' "$1" > tmp
1359 1371 > mv tmp "$1"
1360 1372 > __EOF__
1361 1373 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1362 1374 > y
1363 1375 > e
1364 1376 > EOF
1365 1377 diff --git a/editedfile b/editedfile
1366 1378 1 hunks, 3 lines changed
1367 1379 examine changes to 'editedfile'? [Ynesfdaq?] y
1368 1380
1369 1381 @@ -1,3 +1,3 @@
1370 1382 -This is the first line
1371 1383 -This change will be committed
1372 1384 -This is the third line
1373 1385 +This change will not be committed
1374 1386 +This is the second line
1375 1387 +This line has been added
1376 1388 record this change to 'editedfile'? [Ynesfdaq?] e
1377 1389
1378 1390 no changes to record
1379 1391 [1]
1380 1392 $ cat editedfile
1381 1393 This change will not be committed
1382 1394 This is the second line
1383 1395 This line has been added
1384 1396 $ hg cat -r tip editedfile
1385 1397 This is the first line
1386 1398 This change will be committed
1387 1399 This is the third line
1388 1400 $ hg revert editedfile
1389 1401
1390 1402 Invalid patch
1391 1403
1392 1404 $ sed -e '3s/third/second/' -e '2s/will/will not/' -e 1d editedfile > tmp
1393 1405 $ mv tmp editedfile
1394 1406 $ echo "This line has been added" >> editedfile
1395 1407 $ cat > editor.sh << '__EOF__'
1396 1408 > sed s/This/That/ "$1" > tmp
1397 1409 > mv tmp "$1"
1398 1410 > __EOF__
1399 1411 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1400 1412 > y
1401 1413 > e
1402 1414 > EOF
1403 1415 diff --git a/editedfile b/editedfile
1404 1416 1 hunks, 3 lines changed
1405 1417 examine changes to 'editedfile'? [Ynesfdaq?] y
1406 1418
1407 1419 @@ -1,3 +1,3 @@
1408 1420 -This is the first line
1409 1421 -This change will be committed
1410 1422 -This is the third line
1411 1423 +This change will not be committed
1412 1424 +This is the second line
1413 1425 +This line has been added
1414 1426 record this change to 'editedfile'? [Ynesfdaq?] e
1415 1427
1416 1428 patching file editedfile
1417 1429 Hunk #1 FAILED at 0
1418 1430 1 out of 1 hunks FAILED -- saving rejects to file editedfile.rej
1419 1431 abort: patch failed to apply
1420 1432 [255]
1421 1433 $ cat editedfile
1422 1434 This change will not be committed
1423 1435 This is the second line
1424 1436 This line has been added
1425 1437 $ hg cat -r tip editedfile
1426 1438 This is the first line
1427 1439 This change will be committed
1428 1440 This is the third line
1429 1441 $ cat editedfile.rej
1430 1442 --- editedfile
1431 1443 +++ editedfile
1432 1444 @@ -1,3 +1,3 @@
1433 1445 -That is the first line
1434 1446 -That change will be committed
1435 1447 -That is the third line
1436 1448 +That change will not be committed
1437 1449 +That is the second line
1438 1450 +That line has been added
1439 1451
1440 1452 Malformed patch - error handling
1441 1453
1442 1454 $ cat > editor.sh << '__EOF__'
1443 1455 > sed -e '/^@/p' "$1" > tmp
1444 1456 > mv tmp "$1"
1445 1457 > __EOF__
1446 1458 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1447 1459 > y
1448 1460 > e
1449 1461 > EOF
1450 1462 diff --git a/editedfile b/editedfile
1451 1463 1 hunks, 3 lines changed
1452 1464 examine changes to 'editedfile'? [Ynesfdaq?] y
1453 1465
1454 1466 @@ -1,3 +1,3 @@
1455 1467 -This is the first line
1456 1468 -This change will be committed
1457 1469 -This is the third line
1458 1470 +This change will not be committed
1459 1471 +This is the second line
1460 1472 +This line has been added
1461 1473 record this change to 'editedfile'? [Ynesfdaq?] e
1462 1474
1463 1475 abort: error parsing patch: unhandled transition: range -> range
1464 1476 [255]
1465 1477
1466 1478 Exiting editor with status 1, ignores the edit but does not stop the recording
1467 1479 session
1468 1480
1469 1481 $ HGEDITOR=false hg commit -i <<EOF
1470 1482 > y
1471 1483 > e
1472 1484 > n
1473 1485 > EOF
1474 1486 diff --git a/editedfile b/editedfile
1475 1487 1 hunks, 3 lines changed
1476 1488 examine changes to 'editedfile'? [Ynesfdaq?] y
1477 1489
1478 1490 @@ -1,3 +1,3 @@
1479 1491 -This is the first line
1480 1492 -This change will be committed
1481 1493 -This is the third line
1482 1494 +This change will not be committed
1483 1495 +This is the second line
1484 1496 +This line has been added
1485 1497 record this change to 'editedfile'? [Ynesfdaq?] e
1486 1498
1487 1499 editor exited with exit code 1
1488 1500 record this change to 'editedfile'? [Ynesfdaq?] n
1489 1501
1490 1502 no changes to record
1491 1503 [1]
1492 1504
1493 1505
1494 1506 random text in random positions is still an error
1495 1507
1496 1508 $ cat > editor.sh << '__EOF__'
1497 1509 > sed -e '/^@/i\
1498 1510 > other' "$1" > tmp
1499 1511 > mv tmp "$1"
1500 1512 > __EOF__
1501 1513 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i <<EOF
1502 1514 > y
1503 1515 > e
1504 1516 > EOF
1505 1517 diff --git a/editedfile b/editedfile
1506 1518 1 hunks, 3 lines changed
1507 1519 examine changes to 'editedfile'? [Ynesfdaq?] y
1508 1520
1509 1521 @@ -1,3 +1,3 @@
1510 1522 -This is the first line
1511 1523 -This change will be committed
1512 1524 -This is the third line
1513 1525 +This change will not be committed
1514 1526 +This is the second line
1515 1527 +This line has been added
1516 1528 record this change to 'editedfile'? [Ynesfdaq?] e
1517 1529
1518 1530 abort: error parsing patch: unhandled transition: file -> other
1519 1531 [255]
1520 1532
1521 1533 $ hg up -C
1522 1534 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1523 1535
1524 1536 With win32text
1525 1537
1526 1538 $ echo '[extensions]' >> .hg/hgrc
1527 1539 $ echo 'win32text = ' >> .hg/hgrc
1528 1540 $ echo '[decode]' >> .hg/hgrc
1529 1541 $ echo '** = cleverdecode:' >> .hg/hgrc
1530 1542 $ echo '[encode]' >> .hg/hgrc
1531 1543 $ echo '** = cleverencode:' >> .hg/hgrc
1532 1544 $ echo '[patch]' >> .hg/hgrc
1533 1545 $ echo 'eol = crlf' >> .hg/hgrc
1534 1546
1535 1547 Ignore win32text deprecation warning for now:
1536 1548
1537 1549 $ echo '[win32text]' >> .hg/hgrc
1538 1550 $ echo 'warn = no' >> .hg/hgrc
1539 1551
1540 1552 $ echo d >> subdir/f1
1541 1553 $ hg commit -i -d '24 0' -mw1 <<EOF
1542 1554 > y
1543 1555 > y
1544 1556 > EOF
1545 1557 diff --git a/subdir/f1 b/subdir/f1
1546 1558 1 hunks, 1 lines changed
1547 1559 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1548 1560
1549 1561 @@ -3,3 +3,4 @@ a
1550 1562 a
1551 1563 b
1552 1564 c
1553 1565 +d
1554 1566 record this change to 'subdir/f1'? [Ynesfdaq?] y
1555 1567
1556 1568
1557 1569 $ hg status -A subdir/f1
1558 1570 C subdir/f1
1559 1571 $ hg tip -p
1560 1572 changeset: 30:* (glob)
1561 1573 tag: tip
1562 1574 user: test
1563 1575 date: Thu Jan 01 00:00:24 1970 +0000
1564 1576 summary: w1
1565 1577
1566 1578 diff -r ???????????? -r ???????????? subdir/f1 (glob)
1567 1579 --- a/subdir/f1 Thu Jan 01 00:00:23 1970 +0000
1568 1580 +++ b/subdir/f1 Thu Jan 01 00:00:24 1970 +0000
1569 1581 @@ -3,3 +3,4 @@
1570 1582 a
1571 1583 b
1572 1584 c
1573 1585 +d
1574 1586
1575 1587
1576 1588
1577 1589 Test --user when ui.username not set
1578 1590 $ unset HGUSER
1579 1591 $ echo e >> subdir/f1
1580 1592 $ hg commit -i --config ui.username= -d '8 0' --user xyz -m "user flag" <<EOF
1581 1593 > y
1582 1594 > y
1583 1595 > EOF
1584 1596 diff --git a/subdir/f1 b/subdir/f1
1585 1597 1 hunks, 1 lines changed
1586 1598 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1587 1599
1588 1600 @@ -4,3 +4,4 @@ a
1589 1601 b
1590 1602 c
1591 1603 d
1592 1604 +e
1593 1605 record this change to 'subdir/f1'? [Ynesfdaq?] y
1594 1606
1595 1607 $ hg status -A subdir/f1
1596 1608 C subdir/f1
1597 1609 $ hg log --template '{author}\n' -l 1
1598 1610 xyz
1599 1611 $ HGUSER="test"
1600 1612 $ export HGUSER
1601 1613
1602 1614
1603 1615 Moving files
1604 1616
1605 1617 $ hg update -C .
1606 1618 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1607 1619 $ hg mv plain plain3
1608 1620 $ echo somechange >> plain3
1609 1621 $ hg commit -i -d '23 0' -mmoving_files << EOF
1610 1622 > y
1611 1623 > y
1612 1624 > EOF
1613 1625 diff --git a/plain b/plain3
1614 1626 rename from plain
1615 1627 rename to plain3
1616 1628 1 hunks, 1 lines changed
1617 1629 examine changes to 'plain' and 'plain3'? [Ynesfdaq?] y
1618 1630
1619 1631 @@ -11,3 +11,4 @@ 8
1620 1632 9
1621 1633 10
1622 1634 11
1623 1635 +somechange
1624 1636 record this change to 'plain3'? [Ynesfdaq?] y
1625 1637
1626 1638 The #if execbit block above changes the hash here on some systems
1627 1639 $ hg status -A plain3
1628 1640 C plain3
1629 1641 $ hg tip
1630 1642 changeset: 32:* (glob)
1631 1643 tag: tip
1632 1644 user: test
1633 1645 date: Thu Jan 01 00:00:23 1970 +0000
1634 1646 summary: moving_files
1635 1647
1636 1648 Editing patch of newly added file
1637 1649
1638 1650 $ hg update -C .
1639 1651 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
1640 1652 $ cat > editor.sh << '__EOF__'
1641 1653 > cat "$1" | sed "s/first/very/g" > tt
1642 1654 > mv tt "$1"
1643 1655 > __EOF__
1644 1656 $ cat > newfile << '__EOF__'
1645 1657 > This is the first line
1646 1658 > This is the second line
1647 1659 > This is the third line
1648 1660 > __EOF__
1649 1661 $ hg add newfile
1650 1662 $ HGEDITOR="\"sh\" \"`pwd`/editor.sh\"" hg commit -i -d '23 0' -medit-patch-new <<EOF
1651 1663 > y
1652 1664 > e
1653 1665 > EOF
1654 1666 diff --git a/newfile b/newfile
1655 1667 new file mode 100644
1656 1668 examine changes to 'newfile'? [Ynesfdaq?] y
1657 1669
1658 1670 @@ -0,0 +1,3 @@
1659 1671 +This is the first line
1660 1672 +This is the second line
1661 1673 +This is the third line
1662 1674 record this change to 'newfile'? [Ynesfdaq?] e
1663 1675
1664 1676 $ hg cat -r tip newfile
1665 1677 This is the very line
1666 1678 This is the second line
1667 1679 This is the third line
1668 1680
1669 1681 $ cat newfile
1670 1682 This is the first line
1671 1683 This is the second line
1672 1684 This is the third line
1673 1685
1674 1686 Add new file from within a subdirectory
1675 1687 $ hg update -C .
1676 1688 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1677 1689 $ mkdir folder
1678 1690 $ cd folder
1679 1691 $ echo "foo" > bar
1680 1692 $ hg add bar
1681 1693 $ hg commit -i -d '23 0' -mnewfilesubdir <<EOF
1682 1694 > y
1683 1695 > y
1684 1696 > EOF
1685 1697 diff --git a/folder/bar b/folder/bar
1686 1698 new file mode 100644
1687 1699 examine changes to 'folder/bar'? [Ynesfdaq?] y
1688 1700
1689 1701 @@ -0,0 +1,1 @@
1690 1702 +foo
1691 1703 record this change to 'folder/bar'? [Ynesfdaq?] y
1692 1704
1693 1705 The #if execbit block above changes the hashes here on some systems
1694 1706 $ hg tip -p
1695 1707 changeset: 34:* (glob)
1696 1708 tag: tip
1697 1709 user: test
1698 1710 date: Thu Jan 01 00:00:23 1970 +0000
1699 1711 summary: newfilesubdir
1700 1712
1701 1713 diff -r * -r * folder/bar (glob)
1702 1714 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1703 1715 +++ b/folder/bar Thu Jan 01 00:00:23 1970 +0000
1704 1716 @@ -0,0 +1,1 @@
1705 1717 +foo
1706 1718
1707 1719 $ cd ..
1708 1720
1709 1721 $ hg status -A folder/bar
1710 1722 C folder/bar
1711 1723
1712 1724 Clear win32text configuration before size/timestamp sensitive test
1713 1725
1714 1726 $ cat >> .hg/hgrc <<EOF
1715 1727 > [extensions]
1716 1728 > win32text = !
1717 1729 > [decode]
1718 1730 > ** = !
1719 1731 > [encode]
1720 1732 > ** = !
1721 1733 > [patch]
1722 1734 > eol = strict
1723 1735 > EOF
1724 1736 $ hg update -q -C null
1725 1737 $ hg update -q -C tip
1726 1738
1727 1739 Test that partially committed file is still treated as "modified",
1728 1740 even if none of mode, size and timestamp is changed on the filesystem
1729 1741 (see also issue4583).
1730 1742
1731 1743 $ cat > subdir/f1 <<EOF
1732 1744 > A
1733 1745 > a
1734 1746 > a
1735 1747 > b
1736 1748 > c
1737 1749 > d
1738 1750 > E
1739 1751 > EOF
1740 1752 $ hg diff --git subdir/f1
1741 1753 diff --git a/subdir/f1 b/subdir/f1
1742 1754 --- a/subdir/f1
1743 1755 +++ b/subdir/f1
1744 1756 @@ -1,7 +1,7 @@
1745 1757 -a
1746 1758 +A
1747 1759 a
1748 1760 a
1749 1761 b
1750 1762 c
1751 1763 d
1752 1764 -e
1753 1765 +E
1754 1766
1755 1767 $ touch -t 200001010000 subdir/f1
1756 1768
1757 1769 $ cat >> .hg/hgrc <<EOF
1758 1770 > # emulate invoking patch.internalpatch() at 2000-01-01 00:00
1759 1771 > [fakepatchtime]
1760 1772 > fakenow = 200001010000
1761 1773 >
1762 1774 > [extensions]
1763 1775 > fakepatchtime = $TESTDIR/fakepatchtime.py
1764 1776 > EOF
1765 1777 $ hg commit -i -m 'commit subdir/f1 partially' <<EOF
1766 1778 > y
1767 1779 > y
1768 1780 > n
1769 1781 > EOF
1770 1782 diff --git a/subdir/f1 b/subdir/f1
1771 1783 2 hunks, 2 lines changed
1772 1784 examine changes to 'subdir/f1'? [Ynesfdaq?] y
1773 1785
1774 1786 @@ -1,6 +1,6 @@
1775 1787 -a
1776 1788 +A
1777 1789 a
1778 1790 a
1779 1791 b
1780 1792 c
1781 1793 d
1782 1794 record change 1/2 to 'subdir/f1'? [Ynesfdaq?] y
1783 1795
1784 1796 @@ -2,6 +2,6 @@
1785 1797 a
1786 1798 a
1787 1799 b
1788 1800 c
1789 1801 d
1790 1802 -e
1791 1803 +E
1792 1804 record change 2/2 to 'subdir/f1'? [Ynesfdaq?] n
1793 1805
1794 1806 $ cat >> .hg/hgrc <<EOF
1795 1807 > [extensions]
1796 1808 > fakepatchtime = !
1797 1809 > EOF
1798 1810
1799 1811 $ hg debugstate | grep ' subdir/f1$'
1800 1812 n 0 -1 unset subdir/f1
1801 1813 $ hg status -A subdir/f1
1802 1814 M subdir/f1
1803 1815
1804 1816 Test commands.commit.interactive.unified=0
1805 1817
1806 1818 $ hg init $TESTTMP/b
1807 1819 $ cd $TESTTMP/b
1808 1820 $ cat > foo <<EOF
1809 1821 > 1
1810 1822 > 2
1811 1823 > 3
1812 1824 > 4
1813 1825 > 5
1814 1826 > EOF
1815 1827 $ hg ci -qAm initial
1816 1828 $ cat > foo <<EOF
1817 1829 > 1
1818 1830 > change1
1819 1831 > 2
1820 1832 > 3
1821 1833 > change2
1822 1834 > 4
1823 1835 > 5
1824 1836 > EOF
1825 1837 $ printf 'y\ny\ny\n' | hg ci -im initial --config commands.commit.interactive.unified=0
1826 1838 diff --git a/foo b/foo
1827 1839 2 hunks, 2 lines changed
1828 1840 examine changes to 'foo'? [Ynesfdaq?] y
1829 1841
1830 1842 @@ -1,0 +2,1 @@ 1
1831 1843 +change1
1832 1844 record change 1/2 to 'foo'? [Ynesfdaq?] y
1833 1845
1834 1846 @@ -3,0 +5,1 @@ 3
1835 1847 +change2
1836 1848 record change 2/2 to 'foo'? [Ynesfdaq?] y
1837 1849
1838 1850 $ cd $TESTTMP
1839 1851
1840 1852 Test diff.ignoreblanklines=1
1841 1853
1842 1854 $ hg init c
1843 1855 $ cd c
1844 1856 $ cat > foo <<EOF
1845 1857 > 1
1846 1858 > 2
1847 1859 > 3
1848 1860 > 4
1849 1861 > 5
1850 1862 > EOF
1851 1863 $ hg ci -qAm initial
1852 1864 $ cat > foo <<EOF
1853 1865 > 1
1854 1866 >
1855 1867 > 2
1856 1868 > 3
1857 1869 > change2
1858 1870 > 4
1859 1871 > 5
1860 1872 > EOF
1861 1873 $ printf 'y\ny\ny\n' | hg ci -im initial --config diff.ignoreblanklines=1
1862 1874 diff --git a/foo b/foo
1863 1875 2 hunks, 2 lines changed
1864 1876 examine changes to 'foo'? [Ynesfdaq?] y
1865 1877
1866 1878 @@ -1,3 +1,4 @@
1867 1879 1
1868 1880 +
1869 1881 2
1870 1882 3
1871 1883 record change 1/2 to 'foo'? [Ynesfdaq?] y
1872 1884
1873 1885 @@ -2,4 +3,5 @@
1874 1886 2
1875 1887 3
1876 1888 +change2
1877 1889 4
1878 1890 5
1879 1891 record change 2/2 to 'foo'? [Ynesfdaq?] y
1880 1892
1881 1893
@@ -1,3041 +1,3047 b''
1 1 $ HGENCODING=utf-8
2 2 $ export HGENCODING
3 3 $ cat > testrevset.py << EOF
4 4 > import mercurial.revset
5 5 >
6 6 > baseset = mercurial.revset.baseset
7 7 >
8 8 > def r3232(repo, subset, x):
9 9 > """"simple revset that return [3,2,3,2]
10 10 >
11 11 > revisions duplicated on purpose.
12 12 > """
13 13 > if 3 not in subset:
14 14 > if 2 in subset:
15 15 > return baseset([2, 2])
16 16 > return baseset()
17 17 > return baseset([3, 3, 2, 2])
18 18 >
19 19 > mercurial.revset.symbols[b'r3232'] = r3232
20 20 > EOF
21 21 $ cat >> $HGRCPATH << EOF
22 22 > [extensions]
23 23 > drawdag=$TESTDIR/drawdag.py
24 24 > testrevset=$TESTTMP/testrevset.py
25 25 > EOF
26 26
27 27 $ try() {
28 28 > hg debugrevspec --debug "$@"
29 29 > }
30 30
31 31 $ log() {
32 32 > hg log --template '{rev}\n' -r "$1"
33 33 > }
34 34
35 35 extension to build '_intlist()' and '_hexlist()', which is necessary because
36 36 these predicates use '\0' as a separator:
37 37
38 38 $ cat <<EOF > debugrevlistspec.py
39 39 > from __future__ import absolute_import
40 40 > from mercurial import (
41 41 > node as nodemod,
42 42 > registrar,
43 43 > revset,
44 44 > revsetlang,
45 45 > )
46 46 > from mercurial.utils import stringutil
47 47 > cmdtable = {}
48 48 > command = registrar.command(cmdtable)
49 49 > @command(b'debugrevlistspec',
50 50 > [(b'', b'optimize', None, b'print parsed tree after optimizing'),
51 51 > (b'', b'bin', None, b'unhexlify arguments')])
52 52 > def debugrevlistspec(ui, repo, fmt, *args, **opts):
53 53 > if opts['bin']:
54 54 > args = map(nodemod.bin, args)
55 55 > expr = revsetlang.formatspec(fmt, list(args))
56 56 > if ui.verbose:
57 57 > tree = revsetlang.parse(expr, lookup=revset.lookupfn(repo))
58 58 > ui.note(revsetlang.prettyformat(tree), b"\n")
59 59 > if opts["optimize"]:
60 60 > opttree = revsetlang.optimize(revsetlang.analyze(tree))
61 61 > ui.note(b"* optimized:\n", revsetlang.prettyformat(opttree),
62 62 > b"\n")
63 63 > func = revset.match(ui, expr, lookup=revset.lookupfn(repo))
64 64 > revs = func(repo)
65 65 > if ui.verbose:
66 66 > ui.note(b"* set:\n", stringutil.prettyrepr(revs), b"\n")
67 67 > for c in revs:
68 68 > ui.write(b"%d\n" % c)
69 69 > EOF
70 70 $ cat <<EOF >> $HGRCPATH
71 71 > [extensions]
72 72 > debugrevlistspec = $TESTTMP/debugrevlistspec.py
73 73 > EOF
74 74 $ trylist() {
75 75 > hg debugrevlistspec --debug "$@"
76 76 > }
77 77
78 78 $ hg init repo
79 79 $ cd repo
80 80
81 81 $ echo a > a
82 82 $ hg branch a
83 83 marked working directory as branch a
84 84 (branches are permanent and global, did you want a bookmark?)
85 85 $ hg ci -Aqm0
86 86
87 87 $ echo b > b
88 88 $ hg branch b
89 89 marked working directory as branch b
90 90 $ hg ci -Aqm1
91 91
92 92 $ rm a
93 93 $ hg branch a-b-c-
94 94 marked working directory as branch a-b-c-
95 95 $ hg ci -Aqm2 -u Bob
96 96
97 97 $ hg log -r "extra('branch', 'a-b-c-')" --template '{rev}\n'
98 98 2
99 99 $ hg log -r "extra('branch')" --template '{rev}\n'
100 100 0
101 101 1
102 102 2
103 103 $ hg log -r "extra('branch', 're:a')" --template '{rev} {branch}\n'
104 104 0 a
105 105 2 a-b-c-
106 106
107 107 $ hg co 1
108 108 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
109 109 $ hg branch +a+b+c+
110 110 marked working directory as branch +a+b+c+
111 111 $ hg ci -Aqm3
112 112
113 113 $ hg co 2 # interleave
114 114 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
115 115 $ echo bb > b
116 116 $ hg branch -- -a-b-c-
117 117 marked working directory as branch -a-b-c-
118 118 $ hg ci -Aqm4 -d "May 12 2005"
119 119
120 120 $ hg co 3
121 121 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
122 122 $ hg branch !a/b/c/
123 123 marked working directory as branch !a/b/c/
124 124 $ hg ci -Aqm"5 bug"
125 125
126 126 $ hg merge 4
127 127 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
128 128 (branch merge, don't forget to commit)
129 129 $ hg branch _a_b_c_
130 130 marked working directory as branch _a_b_c_
131 131 $ hg ci -Aqm"6 issue619"
132 132
133 133 $ hg branch .a.b.c.
134 134 marked working directory as branch .a.b.c.
135 135 $ hg ci -Aqm7
136 136
137 137 $ hg branch all
138 138 marked working directory as branch all
139 139
140 140 $ hg co 4
141 141 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
142 142 $ hg branch Γ©
143 143 marked working directory as branch \xc3\xa9 (esc)
144 144 $ hg ci -Aqm9
145 145
146 146 $ hg tag -r6 1.0
147 147 $ hg bookmark -r6 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
148 148
149 149 $ hg clone --quiet -U -r 7 . ../remote1
150 150 $ hg clone --quiet -U -r 8 . ../remote2
151 151 $ echo "[paths]" >> .hg/hgrc
152 152 $ echo "default = ../remote1" >> .hg/hgrc
153 153
154 154 trivial
155 155
156 156 $ try 0:1
157 157 (range
158 158 (symbol '0')
159 159 (symbol '1'))
160 160 * set:
161 161 <spanset+ 0:2>
162 162 0
163 163 1
164 164 $ try --optimize :
165 165 (rangeall
166 166 None)
167 167 * optimized:
168 168 (rangeall
169 169 None)
170 170 * set:
171 171 <spanset+ 0:10>
172 172 0
173 173 1
174 174 2
175 175 3
176 176 4
177 177 5
178 178 6
179 179 7
180 180 8
181 181 9
182 182 $ try 3::6
183 183 (dagrange
184 184 (symbol '3')
185 185 (symbol '6'))
186 186 * set:
187 187 <baseset+ [3, 5, 6]>
188 188 3
189 189 5
190 190 6
191 191 $ try '0|1|2'
192 192 (or
193 193 (list
194 194 (symbol '0')
195 195 (symbol '1')
196 196 (symbol '2')))
197 197 * set:
198 198 <baseset [0, 1, 2]>
199 199 0
200 200 1
201 201 2
202 202
203 203 names that should work without quoting
204 204
205 205 $ try a
206 206 (symbol 'a')
207 207 * set:
208 208 <baseset [0]>
209 209 0
210 210 $ try b-a
211 211 (minus
212 212 (symbol 'b')
213 213 (symbol 'a'))
214 214 * set:
215 215 <filteredset
216 216 <baseset [1]>,
217 217 <not
218 218 <baseset [0]>>>
219 219 1
220 220 $ try _a_b_c_
221 221 (symbol '_a_b_c_')
222 222 * set:
223 223 <baseset [6]>
224 224 6
225 225 $ try _a_b_c_-a
226 226 (minus
227 227 (symbol '_a_b_c_')
228 228 (symbol 'a'))
229 229 * set:
230 230 <filteredset
231 231 <baseset [6]>,
232 232 <not
233 233 <baseset [0]>>>
234 234 6
235 235 $ try .a.b.c.
236 236 (symbol '.a.b.c.')
237 237 * set:
238 238 <baseset [7]>
239 239 7
240 240 $ try .a.b.c.-a
241 241 (minus
242 242 (symbol '.a.b.c.')
243 243 (symbol 'a'))
244 244 * set:
245 245 <filteredset
246 246 <baseset [7]>,
247 247 <not
248 248 <baseset [0]>>>
249 249 7
250 250
251 251 names that should be caught by fallback mechanism
252 252
253 253 $ try -- '-a-b-c-'
254 254 (symbol '-a-b-c-')
255 255 * set:
256 256 <baseset [4]>
257 257 4
258 258 $ log -a-b-c-
259 259 4
260 260 $ try '+a+b+c+'
261 261 (symbol '+a+b+c+')
262 262 * set:
263 263 <baseset [3]>
264 264 3
265 265 $ try '+a+b+c+:'
266 266 (rangepost
267 267 (symbol '+a+b+c+'))
268 268 * set:
269 269 <spanset+ 3:10>
270 270 3
271 271 4
272 272 5
273 273 6
274 274 7
275 275 8
276 276 9
277 277 $ try ':+a+b+c+'
278 278 (rangepre
279 279 (symbol '+a+b+c+'))
280 280 * set:
281 281 <spanset+ 0:4>
282 282 0
283 283 1
284 284 2
285 285 3
286 286 $ try -- '-a-b-c-:+a+b+c+'
287 287 (range
288 288 (symbol '-a-b-c-')
289 289 (symbol '+a+b+c+'))
290 290 * set:
291 291 <spanset- 3:5>
292 292 4
293 293 3
294 294 $ log '-a-b-c-:+a+b+c+'
295 295 4
296 296 3
297 297
298 298 $ try -- -a-b-c--a # complains
299 299 (minus
300 300 (minus
301 301 (minus
302 302 (negate
303 303 (symbol 'a'))
304 304 (symbol 'b'))
305 305 (symbol 'c'))
306 306 (negate
307 307 (symbol 'a')))
308 308 abort: unknown revision '-a'!
309 309 [255]
310 310 $ try Γ©
311 311 (symbol '\xc3\xa9')
312 312 * set:
313 313 <baseset [9]>
314 314 9
315 315
316 316 no quoting needed
317 317
318 318 $ log ::a-b-c-
319 319 0
320 320 1
321 321 2
322 322
323 323 quoting needed
324 324
325 325 $ try '"-a-b-c-"-a'
326 326 (minus
327 327 (string '-a-b-c-')
328 328 (symbol 'a'))
329 329 * set:
330 330 <filteredset
331 331 <baseset [4]>,
332 332 <not
333 333 <baseset [0]>>>
334 334 4
335 335
336 336 $ log '1 or 2'
337 337 1
338 338 2
339 339 $ log '1|2'
340 340 1
341 341 2
342 342 $ log '1 and 2'
343 343 $ log '1&2'
344 344 $ try '1&2|3' # precedence - and is higher
345 345 (or
346 346 (list
347 347 (and
348 348 (symbol '1')
349 349 (symbol '2'))
350 350 (symbol '3')))
351 351 * set:
352 352 <addset
353 353 <baseset []>,
354 354 <baseset [3]>>
355 355 3
356 356 $ try '1|2&3'
357 357 (or
358 358 (list
359 359 (symbol '1')
360 360 (and
361 361 (symbol '2')
362 362 (symbol '3'))))
363 363 * set:
364 364 <addset
365 365 <baseset [1]>,
366 366 <baseset []>>
367 367 1
368 368 $ try '1&2&3' # associativity
369 369 (and
370 370 (and
371 371 (symbol '1')
372 372 (symbol '2'))
373 373 (symbol '3'))
374 374 * set:
375 375 <baseset []>
376 376 $ try '1|(2|3)'
377 377 (or
378 378 (list
379 379 (symbol '1')
380 380 (group
381 381 (or
382 382 (list
383 383 (symbol '2')
384 384 (symbol '3'))))))
385 385 * set:
386 386 <addset
387 387 <baseset [1]>,
388 388 <baseset [2, 3]>>
389 389 1
390 390 2
391 391 3
392 392 $ log '1.0' # tag
393 393 6
394 394 $ log 'a' # branch
395 395 0
396 396 $ log '2785f51ee'
397 397 0
398 398 $ log 'date(2005)'
399 399 4
400 400 $ log 'date(this is a test)'
401 401 hg: parse error at 10: unexpected token: symbol
402 402 (date(this is a test)
403 403 ^ here)
404 404 [255]
405 405 $ log 'date()'
406 406 hg: parse error: date requires a string
407 407 [255]
408 408 $ log 'date'
409 409 abort: unknown revision 'date'!
410 410 [255]
411 411 $ log 'date('
412 412 hg: parse error at 5: not a prefix: end
413 413 (date(
414 414 ^ here)
415 415 [255]
416 416 $ log 'date("\xy")'
417 417 hg: parse error: invalid \x escape* (glob)
418 418 [255]
419 419 $ log 'date(tip)'
420 420 hg: parse error: invalid date: 'tip'
421 421 [255]
422 422 $ log '0:date'
423 423 abort: unknown revision 'date'!
424 424 [255]
425 425 $ log '::"date"'
426 426 abort: unknown revision 'date'!
427 427 [255]
428 428 $ hg book date -r 4
429 429 $ log '0:date'
430 430 0
431 431 1
432 432 2
433 433 3
434 434 4
435 435 $ log '::date'
436 436 0
437 437 1
438 438 2
439 439 4
440 440 $ log '::"date"'
441 441 0
442 442 1
443 443 2
444 444 4
445 445 $ log 'date(2005) and 1::'
446 446 4
447 447 $ hg book -d date
448 448
449 449 function name should be a symbol
450 450
451 451 $ log '"date"(2005)'
452 452 hg: parse error: not a symbol
453 453 [255]
454 454
455 455 keyword arguments
456 456
457 457 $ log 'extra(branch, value=a)'
458 458 0
459 459
460 460 $ log 'extra(branch, a, b)'
461 461 hg: parse error: extra takes at most 2 positional arguments
462 462 [255]
463 463 $ log 'extra(a, label=b)'
464 464 hg: parse error: extra got multiple values for keyword argument 'label'
465 465 [255]
466 466 $ log 'extra(label=branch, default)'
467 467 hg: parse error: extra got an invalid argument
468 468 [255]
469 469 $ log 'extra(branch, foo+bar=baz)'
470 470 hg: parse error: extra got an invalid argument
471 471 [255]
472 472 $ log 'extra(unknown=branch)'
473 473 hg: parse error: extra got an unexpected keyword argument 'unknown'
474 474 [255]
475 $ log 'extra((), x)'
476 hg: parse error: first argument to extra must be a string
477 [255]
478 $ log 'extra(label=x, ())'
479 hg: parse error: extra got an invalid argument
480 [255]
475 481
476 482 $ try 'foo=bar|baz'
477 483 (keyvalue
478 484 (symbol 'foo')
479 485 (or
480 486 (list
481 487 (symbol 'bar')
482 488 (symbol 'baz'))))
483 489 hg: parse error: can't use a key-value pair in this context
484 490 [255]
485 491
486 492 right-hand side should be optimized recursively
487 493
488 494 $ try --optimize 'foo=(not public())'
489 495 (keyvalue
490 496 (symbol 'foo')
491 497 (group
492 498 (not
493 499 (func
494 500 (symbol 'public')
495 501 None))))
496 502 * optimized:
497 503 (keyvalue
498 504 (symbol 'foo')
499 505 (func
500 506 (symbol '_notpublic')
501 507 None))
502 508 hg: parse error: can't use a key-value pair in this context
503 509 [255]
504 510
505 511 relation-subscript operator has the highest binding strength (as function call):
506 512
507 513 $ hg debugrevspec -p parsed 'tip:tip^#generations[-1]'
508 514 * parsed:
509 515 (range
510 516 (symbol 'tip')
511 517 (relsubscript
512 518 (parentpost
513 519 (symbol 'tip'))
514 520 (symbol 'generations')
515 521 (negate
516 522 (symbol '1'))))
517 523 9
518 524 8
519 525 7
520 526 6
521 527 5
522 528 4
523 529
524 530 $ hg debugrevspec -p parsed --no-show-revs 'not public()#generations[0]'
525 531 * parsed:
526 532 (not
527 533 (relsubscript
528 534 (func
529 535 (symbol 'public')
530 536 None)
531 537 (symbol 'generations')
532 538 (symbol '0')))
533 539
534 540 left-hand side of relation-subscript operator should be optimized recursively:
535 541
536 542 $ hg debugrevspec -p analyzed -p optimized --no-show-revs \
537 543 > '(not public())#generations[0]'
538 544 * analyzed:
539 545 (relsubscript
540 546 (not
541 547 (func
542 548 (symbol 'public')
543 549 None))
544 550 (symbol 'generations')
545 551 (symbol '0'))
546 552 * optimized:
547 553 (relsubscript
548 554 (func
549 555 (symbol '_notpublic')
550 556 None)
551 557 (symbol 'generations')
552 558 (symbol '0'))
553 559
554 560 resolution of subscript and relation-subscript ternary operators:
555 561
556 562 $ hg debugrevspec -p analyzed 'tip[0]'
557 563 * analyzed:
558 564 (subscript
559 565 (symbol 'tip')
560 566 (symbol '0'))
561 567 hg: parse error: can't use a subscript in this context
562 568 [255]
563 569
564 570 $ hg debugrevspec -p analyzed 'tip#rel[0]'
565 571 * analyzed:
566 572 (relsubscript
567 573 (symbol 'tip')
568 574 (symbol 'rel')
569 575 (symbol '0'))
570 576 hg: parse error: unknown identifier: rel
571 577 [255]
572 578
573 579 $ hg debugrevspec -p analyzed '(tip#rel)[0]'
574 580 * analyzed:
575 581 (subscript
576 582 (relation
577 583 (symbol 'tip')
578 584 (symbol 'rel'))
579 585 (symbol '0'))
580 586 hg: parse error: can't use a subscript in this context
581 587 [255]
582 588
583 589 $ hg debugrevspec -p analyzed 'tip#rel[0][1]'
584 590 * analyzed:
585 591 (subscript
586 592 (relsubscript
587 593 (symbol 'tip')
588 594 (symbol 'rel')
589 595 (symbol '0'))
590 596 (symbol '1'))
591 597 hg: parse error: can't use a subscript in this context
592 598 [255]
593 599
594 600 $ hg debugrevspec -p analyzed 'tip#rel0#rel1[1]'
595 601 * analyzed:
596 602 (relsubscript
597 603 (relation
598 604 (symbol 'tip')
599 605 (symbol 'rel0'))
600 606 (symbol 'rel1')
601 607 (symbol '1'))
602 608 hg: parse error: unknown identifier: rel1
603 609 [255]
604 610
605 611 $ hg debugrevspec -p analyzed 'tip#rel0[0]#rel1[1]'
606 612 * analyzed:
607 613 (relsubscript
608 614 (relsubscript
609 615 (symbol 'tip')
610 616 (symbol 'rel0')
611 617 (symbol '0'))
612 618 (symbol 'rel1')
613 619 (symbol '1'))
614 620 hg: parse error: unknown identifier: rel1
615 621 [255]
616 622
617 623 parse errors of relation, subscript and relation-subscript operators:
618 624
619 625 $ hg debugrevspec '[0]'
620 626 hg: parse error at 0: not a prefix: [
621 627 ([0]
622 628 ^ here)
623 629 [255]
624 630 $ hg debugrevspec '.#'
625 631 hg: parse error at 2: not a prefix: end
626 632 (.#
627 633 ^ here)
628 634 [255]
629 635 $ hg debugrevspec '#rel'
630 636 hg: parse error at 0: not a prefix: #
631 637 (#rel
632 638 ^ here)
633 639 [255]
634 640 $ hg debugrevspec '.#rel[0'
635 641 hg: parse error at 7: unexpected token: end
636 642 (.#rel[0
637 643 ^ here)
638 644 [255]
639 645 $ hg debugrevspec '.]'
640 646 hg: parse error at 1: invalid token
641 647 (.]
642 648 ^ here)
643 649 [255]
644 650
645 651 $ hg debugrevspec '.#generations[a]'
646 652 hg: parse error: relation subscript must be an integer or a range
647 653 [255]
648 654 $ hg debugrevspec '.#generations[1-2]'
649 655 hg: parse error: relation subscript must be an integer or a range
650 656 [255]
651 657 $ hg debugrevspec '.#generations[foo:bar]'
652 658 hg: parse error: relation subscript bounds must be integers
653 659 [255]
654 660
655 661 suggested relations
656 662
657 663 $ hg debugrevspec '.#generafions[0]'
658 664 hg: parse error: unknown identifier: generafions
659 665 (did you mean generations?)
660 666 [255]
661 667
662 668 $ hg debugrevspec '.#f[0]'
663 669 hg: parse error: unknown identifier: f
664 670 [255]
665 671
666 672 parsed tree at stages:
667 673
668 674 $ hg debugrevspec -p all '()'
669 675 * parsed:
670 676 (group
671 677 None)
672 678 * expanded:
673 679 (group
674 680 None)
675 681 * concatenated:
676 682 (group
677 683 None)
678 684 * analyzed:
679 685 None
680 686 * optimized:
681 687 None
682 688 hg: parse error: missing argument
683 689 [255]
684 690
685 691 $ hg debugrevspec --no-optimized -p all '()'
686 692 * parsed:
687 693 (group
688 694 None)
689 695 * expanded:
690 696 (group
691 697 None)
692 698 * concatenated:
693 699 (group
694 700 None)
695 701 * analyzed:
696 702 None
697 703 hg: parse error: missing argument
698 704 [255]
699 705
700 706 $ hg debugrevspec -p parsed -p analyzed -p optimized '(0|1)-1'
701 707 * parsed:
702 708 (minus
703 709 (group
704 710 (or
705 711 (list
706 712 (symbol '0')
707 713 (symbol '1'))))
708 714 (symbol '1'))
709 715 * analyzed:
710 716 (and
711 717 (or
712 718 (list
713 719 (symbol '0')
714 720 (symbol '1')))
715 721 (not
716 722 (symbol '1')))
717 723 * optimized:
718 724 (difference
719 725 (func
720 726 (symbol '_list')
721 727 (string '0\x001'))
722 728 (symbol '1'))
723 729 0
724 730
725 731 $ hg debugrevspec -p unknown '0'
726 732 abort: invalid stage name: unknown
727 733 [255]
728 734
729 735 $ hg debugrevspec -p all --optimize '0'
730 736 abort: cannot use --optimize with --show-stage
731 737 [255]
732 738
733 739 verify optimized tree:
734 740
735 741 $ hg debugrevspec --verify '0|1'
736 742
737 743 $ hg debugrevspec --verify -v -p analyzed -p optimized 'r3232() & 2'
738 744 * analyzed:
739 745 (and
740 746 (func
741 747 (symbol 'r3232')
742 748 None)
743 749 (symbol '2'))
744 750 * optimized:
745 751 (andsmally
746 752 (func
747 753 (symbol 'r3232')
748 754 None)
749 755 (symbol '2'))
750 756 * analyzed set:
751 757 <baseset [2]>
752 758 * optimized set:
753 759 <baseset [2, 2]>
754 760 --- analyzed
755 761 +++ optimized
756 762 2
757 763 +2
758 764 [1]
759 765
760 766 $ hg debugrevspec --no-optimized --verify-optimized '0'
761 767 abort: cannot use --verify-optimized with --no-optimized
762 768 [255]
763 769
764 770 Test that symbols only get parsed as functions if there's an opening
765 771 parenthesis.
766 772
767 773 $ hg book only -r 9
768 774 $ log 'only(only)' # Outer "only" is a function, inner "only" is the bookmark
769 775 8
770 776 9
771 777
772 778 ':y' behaves like '0:y', but can't be rewritten as such since the revision '0'
773 779 may be hidden (issue5385)
774 780
775 781 $ try -p parsed -p analyzed ':'
776 782 * parsed:
777 783 (rangeall
778 784 None)
779 785 * analyzed:
780 786 (rangeall
781 787 None)
782 788 * set:
783 789 <spanset+ 0:10>
784 790 0
785 791 1
786 792 2
787 793 3
788 794 4
789 795 5
790 796 6
791 797 7
792 798 8
793 799 9
794 800 $ try -p analyzed ':1'
795 801 * analyzed:
796 802 (rangepre
797 803 (symbol '1'))
798 804 * set:
799 805 <spanset+ 0:2>
800 806 0
801 807 1
802 808 $ try -p analyzed ':(1|2)'
803 809 * analyzed:
804 810 (rangepre
805 811 (or
806 812 (list
807 813 (symbol '1')
808 814 (symbol '2'))))
809 815 * set:
810 816 <spanset+ 0:3>
811 817 0
812 818 1
813 819 2
814 820 $ try -p analyzed ':(1&2)'
815 821 * analyzed:
816 822 (rangepre
817 823 (and
818 824 (symbol '1')
819 825 (symbol '2')))
820 826 * set:
821 827 <baseset []>
822 828
823 829 infix/suffix resolution of ^ operator (issue2884, issue5764):
824 830
825 831 x^:y means (x^):y
826 832
827 833 $ try '1^:2'
828 834 (range
829 835 (parentpost
830 836 (symbol '1'))
831 837 (symbol '2'))
832 838 * set:
833 839 <spanset+ 0:3>
834 840 0
835 841 1
836 842 2
837 843
838 844 $ try '1^::2'
839 845 (dagrange
840 846 (parentpost
841 847 (symbol '1'))
842 848 (symbol '2'))
843 849 * set:
844 850 <baseset+ [0, 1, 2]>
845 851 0
846 852 1
847 853 2
848 854
849 855 $ try '1^..2'
850 856 (dagrange
851 857 (parentpost
852 858 (symbol '1'))
853 859 (symbol '2'))
854 860 * set:
855 861 <baseset+ [0, 1, 2]>
856 862 0
857 863 1
858 864 2
859 865
860 866 $ try '9^:'
861 867 (rangepost
862 868 (parentpost
863 869 (symbol '9')))
864 870 * set:
865 871 <spanset+ 8:10>
866 872 8
867 873 9
868 874
869 875 $ try '9^::'
870 876 (dagrangepost
871 877 (parentpost
872 878 (symbol '9')))
873 879 * set:
874 880 <generatorsetasc+>
875 881 8
876 882 9
877 883
878 884 $ try '9^..'
879 885 (dagrangepost
880 886 (parentpost
881 887 (symbol '9')))
882 888 * set:
883 889 <generatorsetasc+>
884 890 8
885 891 9
886 892
887 893 x^:y should be resolved before omitting group operators
888 894
889 895 $ try '1^(:2)'
890 896 (parent
891 897 (symbol '1')
892 898 (group
893 899 (rangepre
894 900 (symbol '2'))))
895 901 hg: parse error: ^ expects a number 0, 1, or 2
896 902 [255]
897 903
898 904 x^:y should be resolved recursively
899 905
900 906 $ try 'sort(1^:2)'
901 907 (func
902 908 (symbol 'sort')
903 909 (range
904 910 (parentpost
905 911 (symbol '1'))
906 912 (symbol '2')))
907 913 * set:
908 914 <spanset+ 0:3>
909 915 0
910 916 1
911 917 2
912 918
913 919 $ try '(3^:4)^:2'
914 920 (range
915 921 (parentpost
916 922 (group
917 923 (range
918 924 (parentpost
919 925 (symbol '3'))
920 926 (symbol '4'))))
921 927 (symbol '2'))
922 928 * set:
923 929 <spanset+ 0:3>
924 930 0
925 931 1
926 932 2
927 933
928 934 $ try '(3^::4)^::2'
929 935 (dagrange
930 936 (parentpost
931 937 (group
932 938 (dagrange
933 939 (parentpost
934 940 (symbol '3'))
935 941 (symbol '4'))))
936 942 (symbol '2'))
937 943 * set:
938 944 <baseset+ [0, 1, 2]>
939 945 0
940 946 1
941 947 2
942 948
943 949 $ try '(9^:)^:'
944 950 (rangepost
945 951 (parentpost
946 952 (group
947 953 (rangepost
948 954 (parentpost
949 955 (symbol '9'))))))
950 956 * set:
951 957 <spanset+ 4:10>
952 958 4
953 959 5
954 960 6
955 961 7
956 962 8
957 963 9
958 964
959 965 x^ in alias should also be resolved
960 966
961 967 $ try 'A' --config 'revsetalias.A=1^:2'
962 968 (symbol 'A')
963 969 * expanded:
964 970 (range
965 971 (parentpost
966 972 (symbol '1'))
967 973 (symbol '2'))
968 974 * set:
969 975 <spanset+ 0:3>
970 976 0
971 977 1
972 978 2
973 979
974 980 $ try 'A:2' --config 'revsetalias.A=1^'
975 981 (range
976 982 (symbol 'A')
977 983 (symbol '2'))
978 984 * expanded:
979 985 (range
980 986 (parentpost
981 987 (symbol '1'))
982 988 (symbol '2'))
983 989 * set:
984 990 <spanset+ 0:3>
985 991 0
986 992 1
987 993 2
988 994
989 995 but not beyond the boundary of alias expansion, because the resolution should
990 996 be made at the parsing stage
991 997
992 998 $ try '1^A' --config 'revsetalias.A=:2'
993 999 (parent
994 1000 (symbol '1')
995 1001 (symbol 'A'))
996 1002 * expanded:
997 1003 (parent
998 1004 (symbol '1')
999 1005 (rangepre
1000 1006 (symbol '2')))
1001 1007 hg: parse error: ^ expects a number 0, 1, or 2
1002 1008 [255]
1003 1009
1004 1010 '::' itself isn't a valid expression
1005 1011
1006 1012 $ try '::'
1007 1013 (dagrangeall
1008 1014 None)
1009 1015 hg: parse error: can't use '::' in this context
1010 1016 [255]
1011 1017
1012 1018 ancestor can accept 0 or more arguments
1013 1019
1014 1020 $ log 'ancestor()'
1015 1021 $ log 'ancestor(1)'
1016 1022 1
1017 1023 $ log 'ancestor(4,5)'
1018 1024 1
1019 1025 $ log 'ancestor(4,5) and 4'
1020 1026 $ log 'ancestor(0,0,1,3)'
1021 1027 0
1022 1028 $ log 'ancestor(3,1,5,3,5,1)'
1023 1029 1
1024 1030 $ log 'ancestor(0,1,3,5)'
1025 1031 0
1026 1032 $ log 'ancestor(1,2,3,4,5)'
1027 1033 1
1028 1034
1029 1035 test ancestors
1030 1036
1031 1037 $ hg log -G -T '{rev}\n' --config experimental.graphshorten=True
1032 1038 @ 9
1033 1039 o 8
1034 1040 | o 7
1035 1041 | o 6
1036 1042 |/|
1037 1043 | o 5
1038 1044 o | 4
1039 1045 | o 3
1040 1046 o | 2
1041 1047 |/
1042 1048 o 1
1043 1049 o 0
1044 1050
1045 1051 $ log 'ancestors(5)'
1046 1052 0
1047 1053 1
1048 1054 3
1049 1055 5
1050 1056 $ log 'ancestor(ancestors(5))'
1051 1057 0
1052 1058 $ log '::r3232()'
1053 1059 0
1054 1060 1
1055 1061 2
1056 1062 3
1057 1063
1058 1064 test common ancestors
1059 1065
1060 1066 $ hg log -T '{rev}\n' -r 'commonancestors(7 + 9)'
1061 1067 0
1062 1068 1
1063 1069 2
1064 1070 4
1065 1071
1066 1072 $ hg log -T '{rev}\n' -r 'commonancestors(heads(all()))'
1067 1073 0
1068 1074 1
1069 1075 2
1070 1076 4
1071 1077
1072 1078 $ hg log -T '{rev}\n' -r 'commonancestors(9)'
1073 1079 0
1074 1080 1
1075 1081 2
1076 1082 4
1077 1083 8
1078 1084 9
1079 1085
1080 1086 $ hg log -T '{rev}\n' -r 'commonancestors(8 + 9)'
1081 1087 0
1082 1088 1
1083 1089 2
1084 1090 4
1085 1091 8
1086 1092
1087 1093 test the specialized implementation of heads(commonancestors(..))
1088 1094 (2 gcas is tested in test-merge-criss-cross.t)
1089 1095
1090 1096 $ hg log -T '{rev}\n' -r 'heads(commonancestors(7 + 9))'
1091 1097 4
1092 1098 $ hg log -T '{rev}\n' -r 'heads(commonancestors(heads(all())))'
1093 1099 4
1094 1100 $ hg log -T '{rev}\n' -r 'heads(commonancestors(9))'
1095 1101 9
1096 1102 $ hg log -T '{rev}\n' -r 'heads(commonancestors(8 + 9))'
1097 1103 8
1098 1104
1099 1105 test ancestor variants of empty revision
1100 1106
1101 1107 $ log 'ancestor(none())'
1102 1108 $ log 'ancestors(none())'
1103 1109 $ log 'commonancestors(none())'
1104 1110 $ log 'heads(commonancestors(none()))'
1105 1111
1106 1112 test ancestors with depth limit
1107 1113
1108 1114 (depth=0 selects the node itself)
1109 1115
1110 1116 $ log 'reverse(ancestors(9, depth=0))'
1111 1117 9
1112 1118
1113 1119 (interleaved: '4' would be missing if heap queue were higher depth first)
1114 1120
1115 1121 $ log 'reverse(ancestors(8:9, depth=1))'
1116 1122 9
1117 1123 8
1118 1124 4
1119 1125
1120 1126 (interleaved: '2' would be missing if heap queue were higher depth first)
1121 1127
1122 1128 $ log 'reverse(ancestors(7+8, depth=2))'
1123 1129 8
1124 1130 7
1125 1131 6
1126 1132 5
1127 1133 4
1128 1134 2
1129 1135
1130 1136 (walk example above by separate queries)
1131 1137
1132 1138 $ log 'reverse(ancestors(8, depth=2)) + reverse(ancestors(7, depth=2))'
1133 1139 8
1134 1140 4
1135 1141 2
1136 1142 7
1137 1143 6
1138 1144 5
1139 1145
1140 1146 (walk 2nd and 3rd ancestors)
1141 1147
1142 1148 $ log 'reverse(ancestors(7, depth=3, startdepth=2))'
1143 1149 5
1144 1150 4
1145 1151 3
1146 1152 2
1147 1153
1148 1154 (interleaved: '4' would be missing if higher-depth ancestors weren't scanned)
1149 1155
1150 1156 $ log 'reverse(ancestors(7+8, depth=2, startdepth=2))'
1151 1157 5
1152 1158 4
1153 1159 2
1154 1160
1155 1161 (note that 'ancestors(x, depth=y, startdepth=z)' does not identical to
1156 1162 'ancestors(x, depth=y) - ancestors(x, depth=z-1)' because a node may have
1157 1163 multiple depths)
1158 1164
1159 1165 $ log 'reverse(ancestors(7+8, depth=2) - ancestors(7+8, depth=1))'
1160 1166 5
1161 1167 2
1162 1168
1163 1169 test bad arguments passed to ancestors()
1164 1170
1165 1171 $ log 'ancestors(., depth=-1)'
1166 1172 hg: parse error: negative depth
1167 1173 [255]
1168 1174 $ log 'ancestors(., depth=foo)'
1169 1175 hg: parse error: ancestors expects an integer depth
1170 1176 [255]
1171 1177
1172 1178 test descendants
1173 1179
1174 1180 $ hg log -G -T '{rev}\n' --config experimental.graphshorten=True
1175 1181 @ 9
1176 1182 o 8
1177 1183 | o 7
1178 1184 | o 6
1179 1185 |/|
1180 1186 | o 5
1181 1187 o | 4
1182 1188 | o 3
1183 1189 o | 2
1184 1190 |/
1185 1191 o 1
1186 1192 o 0
1187 1193
1188 1194 (null is ultimate root and has optimized path)
1189 1195
1190 1196 $ log 'null:4 & descendants(null)'
1191 1197 -1
1192 1198 0
1193 1199 1
1194 1200 2
1195 1201 3
1196 1202 4
1197 1203
1198 1204 (including merge)
1199 1205
1200 1206 $ log ':8 & descendants(2)'
1201 1207 2
1202 1208 4
1203 1209 6
1204 1210 7
1205 1211 8
1206 1212
1207 1213 (multiple roots)
1208 1214
1209 1215 $ log ':8 & descendants(2+5)'
1210 1216 2
1211 1217 4
1212 1218 5
1213 1219 6
1214 1220 7
1215 1221 8
1216 1222
1217 1223 test descendants with depth limit
1218 1224
1219 1225 (depth=0 selects the node itself)
1220 1226
1221 1227 $ log 'descendants(0, depth=0)'
1222 1228 0
1223 1229 $ log 'null: & descendants(null, depth=0)'
1224 1230 -1
1225 1231
1226 1232 (p2 = null should be ignored)
1227 1233
1228 1234 $ log 'null: & descendants(null, depth=2)'
1229 1235 -1
1230 1236 0
1231 1237 1
1232 1238
1233 1239 (multiple paths: depth(6) = (2, 3))
1234 1240
1235 1241 $ log 'descendants(1+3, depth=2)'
1236 1242 1
1237 1243 2
1238 1244 3
1239 1245 4
1240 1246 5
1241 1247 6
1242 1248
1243 1249 (multiple paths: depth(5) = (1, 2), depth(6) = (2, 3))
1244 1250
1245 1251 $ log 'descendants(3+1, depth=2, startdepth=2)'
1246 1252 4
1247 1253 5
1248 1254 6
1249 1255
1250 1256 (multiple depths: depth(6) = (0, 2, 4), search for depth=2)
1251 1257
1252 1258 $ log 'descendants(0+3+6, depth=3, startdepth=1)'
1253 1259 1
1254 1260 2
1255 1261 3
1256 1262 4
1257 1263 5
1258 1264 6
1259 1265 7
1260 1266
1261 1267 (multiple depths: depth(6) = (0, 4), no match)
1262 1268
1263 1269 $ log 'descendants(0+6, depth=3, startdepth=1)'
1264 1270 1
1265 1271 2
1266 1272 3
1267 1273 4
1268 1274 5
1269 1275 7
1270 1276
1271 1277 test ancestors/descendants relation subscript:
1272 1278
1273 1279 $ log 'tip#generations[0]'
1274 1280 9
1275 1281 $ log '.#generations[-1]'
1276 1282 8
1277 1283 $ log '.#g[(-1)]'
1278 1284 8
1279 1285
1280 1286 $ log '6#generations[0:1]'
1281 1287 6
1282 1288 7
1283 1289 $ log '6#generations[-1:1]'
1284 1290 4
1285 1291 5
1286 1292 6
1287 1293 7
1288 1294 $ log '6#generations[0:]'
1289 1295 6
1290 1296 7
1291 1297 $ log '5#generations[:0]'
1292 1298 0
1293 1299 1
1294 1300 3
1295 1301 5
1296 1302 $ log '3#generations[:]'
1297 1303 0
1298 1304 1
1299 1305 3
1300 1306 5
1301 1307 6
1302 1308 7
1303 1309 $ log 'tip#generations[1:-1]'
1304 1310
1305 1311 $ hg debugrevspec -p parsed 'roots(:)#g[2]'
1306 1312 * parsed:
1307 1313 (relsubscript
1308 1314 (func
1309 1315 (symbol 'roots')
1310 1316 (rangeall
1311 1317 None))
1312 1318 (symbol 'g')
1313 1319 (symbol '2'))
1314 1320 2
1315 1321 3
1316 1322
1317 1323 test author
1318 1324
1319 1325 $ log 'author(bob)'
1320 1326 2
1321 1327 $ log 'author("re:bob|test")'
1322 1328 0
1323 1329 1
1324 1330 2
1325 1331 3
1326 1332 4
1327 1333 5
1328 1334 6
1329 1335 7
1330 1336 8
1331 1337 9
1332 1338 $ log 'author(r"re:\S")'
1333 1339 0
1334 1340 1
1335 1341 2
1336 1342 3
1337 1343 4
1338 1344 5
1339 1345 6
1340 1346 7
1341 1347 8
1342 1348 9
1343 1349 $ log 'branch(Γ©)'
1344 1350 8
1345 1351 9
1346 1352 $ log 'branch(a)'
1347 1353 0
1348 1354 $ hg log -r 'branch("re:a")' --template '{rev} {branch}\n'
1349 1355 0 a
1350 1356 2 a-b-c-
1351 1357 3 +a+b+c+
1352 1358 4 -a-b-c-
1353 1359 5 !a/b/c/
1354 1360 6 _a_b_c_
1355 1361 7 .a.b.c.
1356 1362 $ log 'children(ancestor(4,5))'
1357 1363 2
1358 1364 3
1359 1365
1360 1366 $ log 'children(4)'
1361 1367 6
1362 1368 8
1363 1369 $ log 'children(null)'
1364 1370 0
1365 1371
1366 1372 $ log 'closed()'
1367 1373 $ log 'contains(a)'
1368 1374 0
1369 1375 1
1370 1376 3
1371 1377 5
1372 1378 $ log 'contains("../repo/a")'
1373 1379 0
1374 1380 1
1375 1381 3
1376 1382 5
1377 1383 $ log 'desc(B)'
1378 1384 5
1379 1385 $ hg log -r 'desc(r"re:S?u")' --template "{rev} {desc|firstline}\n"
1380 1386 5 5 bug
1381 1387 6 6 issue619
1382 1388 $ log 'descendants(2 or 3)'
1383 1389 2
1384 1390 3
1385 1391 4
1386 1392 5
1387 1393 6
1388 1394 7
1389 1395 8
1390 1396 9
1391 1397 $ log 'file("b*")'
1392 1398 1
1393 1399 4
1394 1400 $ log 'filelog("b")'
1395 1401 1
1396 1402 4
1397 1403 $ log 'filelog("../repo/b")'
1398 1404 1
1399 1405 4
1400 1406 $ log 'follow()'
1401 1407 0
1402 1408 1
1403 1409 2
1404 1410 4
1405 1411 8
1406 1412 9
1407 1413 $ log 'grep("issue\d+")'
1408 1414 6
1409 1415 $ try 'grep("(")' # invalid regular expression
1410 1416 (func
1411 1417 (symbol 'grep')
1412 1418 (string '('))
1413 1419 hg: parse error: invalid match pattern: (unbalanced parenthesis|missing \),.*) (re)
1414 1420 [255]
1415 1421 $ try 'grep("\bissue\d+")'
1416 1422 (func
1417 1423 (symbol 'grep')
1418 1424 (string '\x08issue\\d+'))
1419 1425 * set:
1420 1426 <filteredset
1421 1427 <fullreposet+ 0:10>,
1422 1428 <grep '\x08issue\\d+'>>
1423 1429 $ try 'grep(r"\bissue\d+")'
1424 1430 (func
1425 1431 (symbol 'grep')
1426 1432 (string '\\bissue\\d+'))
1427 1433 * set:
1428 1434 <filteredset
1429 1435 <fullreposet+ 0:10>,
1430 1436 <grep '\\bissue\\d+'>>
1431 1437 6
1432 1438 $ try 'grep(r"\")'
1433 1439 hg: parse error at 7: unterminated string
1434 1440 (grep(r"\")
1435 1441 ^ here)
1436 1442 [255]
1437 1443 $ log 'head()'
1438 1444 0
1439 1445 1
1440 1446 2
1441 1447 3
1442 1448 4
1443 1449 5
1444 1450 6
1445 1451 7
1446 1452 9
1447 1453
1448 1454 Test heads
1449 1455
1450 1456 $ log 'heads(6::)'
1451 1457 7
1452 1458
1453 1459 heads() can be computed in subset '9:'
1454 1460
1455 1461 $ hg debugrevspec -s '9: & heads(all())'
1456 1462 * set:
1457 1463 <filteredset
1458 1464 <baseset [9]>,
1459 1465 <baseset+ [7, 9]>>
1460 1466 9
1461 1467
1462 1468 but should follow the order of the subset
1463 1469
1464 1470 $ log 'heads(all())'
1465 1471 7
1466 1472 9
1467 1473 $ log 'heads(tip:0)'
1468 1474 7
1469 1475 9
1470 1476 $ log 'tip:0 & heads(all())'
1471 1477 9
1472 1478 7
1473 1479 $ log 'tip:0 & heads(0:tip)'
1474 1480 9
1475 1481 7
1476 1482
1477 1483 $ log 'keyword(issue)'
1478 1484 6
1479 1485 $ log 'keyword("test a")'
1480 1486
1481 1487 Test first (=limit) and last
1482 1488
1483 1489 $ log 'limit(head(), 1)'
1484 1490 0
1485 1491 $ log 'limit(author("re:bob|test"), 3, 5)'
1486 1492 5
1487 1493 6
1488 1494 7
1489 1495 $ log 'limit(author("re:bob|test"), offset=6)'
1490 1496 6
1491 1497 $ log 'limit(author("re:bob|test"), offset=10)'
1492 1498 $ log 'limit(all(), 1, -1)'
1493 1499 hg: parse error: negative offset
1494 1500 [255]
1495 1501 $ log 'limit(all(), -1)'
1496 1502 hg: parse error: negative number to select
1497 1503 [255]
1498 1504 $ log 'limit(all(), 0)'
1499 1505
1500 1506 $ log 'last(all(), -1)'
1501 1507 hg: parse error: negative number to select
1502 1508 [255]
1503 1509 $ log 'last(all(), 0)'
1504 1510 $ log 'last(all(), 1)'
1505 1511 9
1506 1512 $ log 'last(all(), 2)'
1507 1513 8
1508 1514 9
1509 1515
1510 1516 Test smartset.slice() by first/last()
1511 1517
1512 1518 (using unoptimized set, filteredset as example)
1513 1519
1514 1520 $ hg debugrevspec --no-show-revs -s '0:7 & branch("re:")'
1515 1521 * set:
1516 1522 <filteredset
1517 1523 <spanset+ 0:8>,
1518 1524 <branch 're:'>>
1519 1525 $ log 'limit(0:7 & branch("re:"), 3, 4)'
1520 1526 4
1521 1527 5
1522 1528 6
1523 1529 $ log 'limit(7:0 & branch("re:"), 3, 4)'
1524 1530 3
1525 1531 2
1526 1532 1
1527 1533 $ log 'last(0:7 & branch("re:"), 2)'
1528 1534 6
1529 1535 7
1530 1536
1531 1537 (using baseset)
1532 1538
1533 1539 $ hg debugrevspec --no-show-revs -s 0+1+2+3+4+5+6+7
1534 1540 * set:
1535 1541 <baseset [0, 1, 2, 3, 4, 5, 6, 7]>
1536 1542 $ hg debugrevspec --no-show-revs -s 0::7
1537 1543 * set:
1538 1544 <baseset+ [0, 1, 2, 3, 4, 5, 6, 7]>
1539 1545 $ log 'limit(0+1+2+3+4+5+6+7, 3, 4)'
1540 1546 4
1541 1547 5
1542 1548 6
1543 1549 $ log 'limit(sort(0::7, rev), 3, 4)'
1544 1550 4
1545 1551 5
1546 1552 6
1547 1553 $ log 'limit(sort(0::7, -rev), 3, 4)'
1548 1554 3
1549 1555 2
1550 1556 1
1551 1557 $ log 'last(sort(0::7, rev), 2)'
1552 1558 6
1553 1559 7
1554 1560 $ hg debugrevspec -s 'limit(sort(0::7, rev), 3, 6)'
1555 1561 * set:
1556 1562 <baseset+ [6, 7]>
1557 1563 6
1558 1564 7
1559 1565 $ hg debugrevspec -s 'limit(sort(0::7, rev), 3, 9)'
1560 1566 * set:
1561 1567 <baseset+ []>
1562 1568 $ hg debugrevspec -s 'limit(sort(0::7, -rev), 3, 6)'
1563 1569 * set:
1564 1570 <baseset- [0, 1]>
1565 1571 1
1566 1572 0
1567 1573 $ hg debugrevspec -s 'limit(sort(0::7, -rev), 3, 9)'
1568 1574 * set:
1569 1575 <baseset- []>
1570 1576 $ hg debugrevspec -s 'limit(0::7, 0)'
1571 1577 * set:
1572 1578 <baseset+ []>
1573 1579
1574 1580 (using spanset)
1575 1581
1576 1582 $ hg debugrevspec --no-show-revs -s 0:7
1577 1583 * set:
1578 1584 <spanset+ 0:8>
1579 1585 $ log 'limit(0:7, 3, 4)'
1580 1586 4
1581 1587 5
1582 1588 6
1583 1589 $ log 'limit(7:0, 3, 4)'
1584 1590 3
1585 1591 2
1586 1592 1
1587 1593 $ log 'limit(0:7, 3, 6)'
1588 1594 6
1589 1595 7
1590 1596 $ log 'limit(7:0, 3, 6)'
1591 1597 1
1592 1598 0
1593 1599 $ log 'last(0:7, 2)'
1594 1600 6
1595 1601 7
1596 1602 $ hg debugrevspec -s 'limit(0:7, 3, 6)'
1597 1603 * set:
1598 1604 <spanset+ 6:8>
1599 1605 6
1600 1606 7
1601 1607 $ hg debugrevspec -s 'limit(0:7, 3, 9)'
1602 1608 * set:
1603 1609 <spanset+ 8:8>
1604 1610 $ hg debugrevspec -s 'limit(7:0, 3, 6)'
1605 1611 * set:
1606 1612 <spanset- 0:2>
1607 1613 1
1608 1614 0
1609 1615 $ hg debugrevspec -s 'limit(7:0, 3, 9)'
1610 1616 * set:
1611 1617 <spanset- 0:0>
1612 1618 $ hg debugrevspec -s 'limit(0:7, 0)'
1613 1619 * set:
1614 1620 <spanset+ 0:0>
1615 1621
1616 1622 Test order of first/last revisions
1617 1623
1618 1624 $ hg debugrevspec -s 'first(4:0, 3) & 3:'
1619 1625 * set:
1620 1626 <filteredset
1621 1627 <spanset- 2:5>,
1622 1628 <spanset+ 3:10>>
1623 1629 4
1624 1630 3
1625 1631
1626 1632 $ hg debugrevspec -s '3: & first(4:0, 3)'
1627 1633 * set:
1628 1634 <filteredset
1629 1635 <spanset+ 3:10>,
1630 1636 <spanset- 2:5>>
1631 1637 3
1632 1638 4
1633 1639
1634 1640 $ hg debugrevspec -s 'last(4:0, 3) & :1'
1635 1641 * set:
1636 1642 <filteredset
1637 1643 <spanset- 0:3>,
1638 1644 <spanset+ 0:2>>
1639 1645 1
1640 1646 0
1641 1647
1642 1648 $ hg debugrevspec -s ':1 & last(4:0, 3)'
1643 1649 * set:
1644 1650 <filteredset
1645 1651 <spanset+ 0:2>,
1646 1652 <spanset+ 0:3>>
1647 1653 0
1648 1654 1
1649 1655
1650 1656 Test scmutil.revsingle() should return the last revision
1651 1657
1652 1658 $ hg debugrevspec -s 'last(0::)'
1653 1659 * set:
1654 1660 <baseset slice=0:1
1655 1661 <generatorsetasc->>
1656 1662 9
1657 1663 $ hg identify -r '0::' --num
1658 1664 9
1659 1665
1660 1666 Test matching
1661 1667
1662 1668 $ log 'matching(6)'
1663 1669 6
1664 1670 $ log 'matching(6:7, "phase parents user date branch summary files description substate")'
1665 1671 6
1666 1672 7
1667 1673
1668 1674 Testing min and max
1669 1675
1670 1676 max: simple
1671 1677
1672 1678 $ log 'max(contains(a))'
1673 1679 5
1674 1680
1675 1681 max: simple on unordered set)
1676 1682
1677 1683 $ log 'max((4+0+2+5+7) and contains(a))'
1678 1684 5
1679 1685
1680 1686 max: no result
1681 1687
1682 1688 $ log 'max(contains(stringthatdoesnotappearanywhere))'
1683 1689
1684 1690 max: no result on unordered set
1685 1691
1686 1692 $ log 'max((4+0+2+5+7) and contains(stringthatdoesnotappearanywhere))'
1687 1693
1688 1694 min: simple
1689 1695
1690 1696 $ log 'min(contains(a))'
1691 1697 0
1692 1698
1693 1699 min: simple on unordered set
1694 1700
1695 1701 $ log 'min((4+0+2+5+7) and contains(a))'
1696 1702 0
1697 1703
1698 1704 min: empty
1699 1705
1700 1706 $ log 'min(contains(stringthatdoesnotappearanywhere))'
1701 1707
1702 1708 min: empty on unordered set
1703 1709
1704 1710 $ log 'min((4+0+2+5+7) and contains(stringthatdoesnotappearanywhere))'
1705 1711
1706 1712
1707 1713 $ log 'merge()'
1708 1714 6
1709 1715 $ log 'branchpoint()'
1710 1716 1
1711 1717 4
1712 1718 $ log 'modifies(b)'
1713 1719 4
1714 1720 $ log 'modifies("path:b")'
1715 1721 4
1716 1722 $ log 'modifies("*")'
1717 1723 4
1718 1724 6
1719 1725 $ log 'modifies("set:modified()")'
1720 1726 4
1721 1727 $ log 'id(5)'
1722 1728 2
1723 1729 $ log 'only(9)'
1724 1730 8
1725 1731 9
1726 1732 $ log 'only(8)'
1727 1733 8
1728 1734 $ log 'only(9, 5)'
1729 1735 2
1730 1736 4
1731 1737 8
1732 1738 9
1733 1739 $ log 'only(7 + 9, 5 + 2)'
1734 1740 4
1735 1741 6
1736 1742 7
1737 1743 8
1738 1744 9
1739 1745
1740 1746 Test empty set input
1741 1747 $ log 'only(p2())'
1742 1748 $ log 'only(p1(), p2())'
1743 1749 0
1744 1750 1
1745 1751 2
1746 1752 4
1747 1753 8
1748 1754 9
1749 1755
1750 1756 Test '%' operator
1751 1757
1752 1758 $ log '9%'
1753 1759 8
1754 1760 9
1755 1761 $ log '9%5'
1756 1762 2
1757 1763 4
1758 1764 8
1759 1765 9
1760 1766 $ log '(7 + 9)%(5 + 2)'
1761 1767 4
1762 1768 6
1763 1769 7
1764 1770 8
1765 1771 9
1766 1772
1767 1773 Test operand of '%' is optimized recursively (issue4670)
1768 1774
1769 1775 $ try --optimize '8:9-8%'
1770 1776 (onlypost
1771 1777 (minus
1772 1778 (range
1773 1779 (symbol '8')
1774 1780 (symbol '9'))
1775 1781 (symbol '8')))
1776 1782 * optimized:
1777 1783 (func
1778 1784 (symbol 'only')
1779 1785 (difference
1780 1786 (range
1781 1787 (symbol '8')
1782 1788 (symbol '9'))
1783 1789 (symbol '8')))
1784 1790 * set:
1785 1791 <baseset+ [8, 9]>
1786 1792 8
1787 1793 9
1788 1794 $ try --optimize '(9)%(5)'
1789 1795 (only
1790 1796 (group
1791 1797 (symbol '9'))
1792 1798 (group
1793 1799 (symbol '5')))
1794 1800 * optimized:
1795 1801 (func
1796 1802 (symbol 'only')
1797 1803 (list
1798 1804 (symbol '9')
1799 1805 (symbol '5')))
1800 1806 * set:
1801 1807 <baseset+ [2, 4, 8, 9]>
1802 1808 2
1803 1809 4
1804 1810 8
1805 1811 9
1806 1812
1807 1813 Test the order of operations
1808 1814
1809 1815 $ log '7 + 9%5 + 2'
1810 1816 7
1811 1817 2
1812 1818 4
1813 1819 8
1814 1820 9
1815 1821
1816 1822 Test explicit numeric revision
1817 1823 $ log 'rev(-2)'
1818 1824 $ log 'rev(-1)'
1819 1825 -1
1820 1826 $ log 'rev(0)'
1821 1827 0
1822 1828 $ log 'rev(9)'
1823 1829 9
1824 1830 $ log 'rev(10)'
1825 1831 $ log 'rev(tip)'
1826 1832 hg: parse error: rev expects a number
1827 1833 [255]
1828 1834
1829 1835 Test hexadecimal revision
1830 1836 $ log 'id(2)'
1831 1837 $ log 'id(5)'
1832 1838 2
1833 1839 $ hg --config experimental.revisions.prefixhexnode=yes log --template '{rev}\n' -r 'id(x5)'
1834 1840 2
1835 1841 $ hg --config experimental.revisions.prefixhexnode=yes log --template '{rev}\n' -r 'x5'
1836 1842 2
1837 1843 $ hg --config experimental.revisions.prefixhexnode=yes log --template '{rev}\n' -r 'id(x)'
1838 1844 $ hg --config experimental.revisions.prefixhexnode=yes log --template '{rev}\n' -r 'x'
1839 1845 abort: 00changelog.i@: ambiguous identifier!
1840 1846 [255]
1841 1847 $ log 'id(23268)'
1842 1848 4
1843 1849 $ log 'id(2785f51eece)'
1844 1850 0
1845 1851 $ log 'id(d5d0dcbdc4d9ff5dbb2d336f32f0bb561c1a532c)'
1846 1852 8
1847 1853 $ log 'id(d5d0dcbdc4a)'
1848 1854 $ log 'id(d5d0dcbdc4w)'
1849 1855 $ log 'id(d5d0dcbdc4d9ff5dbb2d336f32f0bb561c1a532d)'
1850 1856 $ log 'id(d5d0dcbdc4d9ff5dbb2d336f32f0bb561c1a532q)'
1851 1857 $ log 'id(1.0)'
1852 1858 $ log 'id(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx)'
1853 1859
1854 1860 Test null revision
1855 1861 $ log '(null)'
1856 1862 -1
1857 1863 $ log '(null:0)'
1858 1864 -1
1859 1865 0
1860 1866 $ log '(0:null)'
1861 1867 0
1862 1868 -1
1863 1869 $ log 'null::0'
1864 1870 -1
1865 1871 0
1866 1872 $ log 'null:tip - 0:'
1867 1873 -1
1868 1874 $ log 'null: and null::' | head -1
1869 1875 -1
1870 1876 $ log 'null: or 0:' | head -2
1871 1877 -1
1872 1878 0
1873 1879 $ log 'ancestors(null)'
1874 1880 -1
1875 1881 $ log 'reverse(null:)' | tail -2
1876 1882 0
1877 1883 -1
1878 1884 $ log 'first(null:)'
1879 1885 -1
1880 1886 $ log 'min(null:)'
1881 1887 BROKEN: should be '-1'
1882 1888 $ log 'tip:null and all()' | tail -2
1883 1889 1
1884 1890 0
1885 1891
1886 1892 Test working-directory revision
1887 1893 $ hg debugrevspec 'wdir()'
1888 1894 2147483647
1889 1895 $ hg debugrevspec 'wdir()^'
1890 1896 9
1891 1897 $ hg up 7
1892 1898 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
1893 1899 $ hg debugrevspec 'wdir()^'
1894 1900 7
1895 1901 $ hg debugrevspec 'wdir()^0'
1896 1902 2147483647
1897 1903 $ hg debugrevspec 'wdir()~3'
1898 1904 5
1899 1905 $ hg debugrevspec 'ancestors(wdir())'
1900 1906 0
1901 1907 1
1902 1908 2
1903 1909 3
1904 1910 4
1905 1911 5
1906 1912 6
1907 1913 7
1908 1914 2147483647
1909 1915 $ hg debugrevspec '0:wdir() & ancestor(wdir())'
1910 1916 2147483647
1911 1917 $ hg debugrevspec '0:wdir() & ancestor(.:wdir())'
1912 1918 4
1913 1919 $ hg debugrevspec '0:wdir() & ancestor(wdir(), wdir())'
1914 1920 2147483647
1915 1921 $ hg debugrevspec '0:wdir() & ancestor(wdir(), tip)'
1916 1922 4
1917 1923 $ hg debugrevspec 'null:wdir() & ancestor(wdir(), null)'
1918 1924 -1
1919 1925 $ hg debugrevspec 'wdir()~0'
1920 1926 2147483647
1921 1927 $ hg debugrevspec 'p1(wdir())'
1922 1928 7
1923 1929 $ hg debugrevspec 'p2(wdir())'
1924 1930 $ hg debugrevspec 'parents(wdir())'
1925 1931 7
1926 1932 $ hg debugrevspec 'wdir()^1'
1927 1933 7
1928 1934 $ hg debugrevspec 'wdir()^2'
1929 1935 $ hg debugrevspec 'wdir()^3'
1930 1936 hg: parse error: ^ expects a number 0, 1, or 2
1931 1937 [255]
1932 1938 For tests consistency
1933 1939 $ hg up 9
1934 1940 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
1935 1941 $ hg debugrevspec 'tip or wdir()'
1936 1942 9
1937 1943 2147483647
1938 1944 $ hg debugrevspec '0:tip and wdir()'
1939 1945 $ log '0:wdir()' | tail -3
1940 1946 8
1941 1947 9
1942 1948 2147483647
1943 1949 $ log 'wdir():0' | head -3
1944 1950 2147483647
1945 1951 9
1946 1952 8
1947 1953 $ log 'wdir():wdir()'
1948 1954 2147483647
1949 1955 $ log '(all() + wdir()) & min(. + wdir())'
1950 1956 9
1951 1957 $ log '(all() + wdir()) & max(. + wdir())'
1952 1958 2147483647
1953 1959 $ log 'first(wdir() + .)'
1954 1960 2147483647
1955 1961 $ log 'last(. + wdir())'
1956 1962 2147483647
1957 1963
1958 1964 Test working-directory integer revision and node id
1959 1965
1960 1966 $ hg debugrevspec '2147483647'
1961 1967 2147483647
1962 1968 $ hg debugrevspec 'rev(2147483647)'
1963 1969 2147483647
1964 1970 $ hg debugrevspec 'ffffffffffffffffffffffffffffffffffffffff'
1965 1971 2147483647
1966 1972 $ hg debugrevspec 'ffffffffffff'
1967 1973 2147483647
1968 1974 $ hg debugrevspec 'id(ffffffffffffffffffffffffffffffffffffffff)'
1969 1975 2147483647
1970 1976 $ hg debugrevspec 'id(ffffffffffff)'
1971 1977 2147483647
1972 1978 $ hg debugrevspec 'ffffffffffff+000000000000'
1973 1979 2147483647
1974 1980 -1
1975 1981
1976 1982 $ cd ..
1977 1983
1978 1984 Test short 'ff...' hash collision
1979 1985
1980 1986 $ hg init wdir-hashcollision
1981 1987 $ cd wdir-hashcollision
1982 1988 $ cat <<EOF >> .hg/hgrc
1983 1989 > [experimental]
1984 1990 > evolution.createmarkers=True
1985 1991 > EOF
1986 1992 $ echo 0 > a
1987 1993 $ hg ci -qAm 0
1988 1994 $ for i in 2463 2961 6726 78127; do
1989 1995 > hg up -q 0
1990 1996 > echo $i > a
1991 1997 > hg ci -qm $i
1992 1998 > done
1993 1999 $ hg up -q null
1994 2000 $ hg log -r '0:wdir()' -T '{rev}:{node} {shortest(node, 3)}\n'
1995 2001 0:b4e73ffab476aa0ee32ed81ca51e07169844bc6a b4e
1996 2002 1:fffbae3886c8fbb2114296380d276fd37715d571 fffba
1997 2003 2:fffb6093b00943f91034b9bdad069402c834e572 fffb6
1998 2004 3:fff48a9b9de34a4d64120c29548214c67980ade3 fff4
1999 2005 4:ffff85cff0ff78504fcdc3c0bc10de0c65379249 ffff8
2000 2006 2147483647:ffffffffffffffffffffffffffffffffffffffff fffff
2001 2007 $ hg debugobsolete fffbae3886c8fbb2114296380d276fd37715d571
2002 2008 obsoleted 1 changesets
2003 2009
2004 2010 $ hg debugrevspec 'fff'
2005 2011 abort: 00changelog.i@fff: ambiguous identifier!
2006 2012 [255]
2007 2013 $ hg debugrevspec 'ffff'
2008 2014 abort: 00changelog.i@ffff: ambiguous identifier!
2009 2015 [255]
2010 2016 $ hg debugrevspec 'fffb'
2011 2017 abort: 00changelog.i@fffb: ambiguous identifier!
2012 2018 [255]
2013 2019 BROKEN should be '2' (node lookup uses unfiltered repo)
2014 2020 $ hg debugrevspec 'id(fffb)'
2015 2021 BROKEN should be '2' (node lookup uses unfiltered repo)
2016 2022 $ hg debugrevspec 'ffff8'
2017 2023 4
2018 2024 $ hg debugrevspec 'fffff'
2019 2025 2147483647
2020 2026
2021 2027 $ cd ..
2022 2028
2023 2029 Test branch() with wdir()
2024 2030
2025 2031 $ cd repo
2026 2032
2027 2033 $ log '0:wdir() & branch("literal:Γ©")'
2028 2034 8
2029 2035 9
2030 2036 2147483647
2031 2037 $ log '0:wdir() & branch("re:Γ©")'
2032 2038 8
2033 2039 9
2034 2040 2147483647
2035 2041 $ log '0:wdir() & branch("re:^a")'
2036 2042 0
2037 2043 2
2038 2044 $ log '0:wdir() & branch(8)'
2039 2045 8
2040 2046 9
2041 2047 2147483647
2042 2048
2043 2049 branch(wdir()) returns all revisions belonging to the working branch. The wdir
2044 2050 itself isn't returned unless it is explicitly populated.
2045 2051
2046 2052 $ log 'branch(wdir())'
2047 2053 8
2048 2054 9
2049 2055 $ log '0:wdir() & branch(wdir())'
2050 2056 8
2051 2057 9
2052 2058 2147483647
2053 2059
2054 2060 $ log 'outgoing()'
2055 2061 8
2056 2062 9
2057 2063 $ log 'outgoing("../remote1")'
2058 2064 8
2059 2065 9
2060 2066 $ log 'outgoing("../remote2")'
2061 2067 3
2062 2068 5
2063 2069 6
2064 2070 7
2065 2071 9
2066 2072 $ log 'p1(merge())'
2067 2073 5
2068 2074 $ log 'p2(merge())'
2069 2075 4
2070 2076 $ log 'parents(merge())'
2071 2077 4
2072 2078 5
2073 2079 $ log 'p1(branchpoint())'
2074 2080 0
2075 2081 2
2076 2082 $ log 'p2(branchpoint())'
2077 2083 $ log 'parents(branchpoint())'
2078 2084 0
2079 2085 2
2080 2086 $ log 'removes(a)'
2081 2087 2
2082 2088 6
2083 2089 $ log 'roots(all())'
2084 2090 0
2085 2091 $ log 'reverse(2 or 3 or 4 or 5)'
2086 2092 5
2087 2093 4
2088 2094 3
2089 2095 2
2090 2096 $ log 'reverse(all())'
2091 2097 9
2092 2098 8
2093 2099 7
2094 2100 6
2095 2101 5
2096 2102 4
2097 2103 3
2098 2104 2
2099 2105 1
2100 2106 0
2101 2107 $ log 'reverse(all()) & filelog(b)'
2102 2108 4
2103 2109 1
2104 2110 $ log 'rev(5)'
2105 2111 5
2106 2112 $ log 'sort(limit(reverse(all()), 3))'
2107 2113 7
2108 2114 8
2109 2115 9
2110 2116 $ log 'sort(2 or 3 or 4 or 5, date)'
2111 2117 2
2112 2118 3
2113 2119 5
2114 2120 4
2115 2121 $ log 'tagged()'
2116 2122 6
2117 2123 $ log 'tag()'
2118 2124 6
2119 2125 $ log 'tag(1.0)'
2120 2126 6
2121 2127 $ log 'tag(tip)'
2122 2128 9
2123 2129
2124 2130 Test order of revisions in compound expression
2125 2131 ----------------------------------------------
2126 2132
2127 2133 The general rule is that only the outermost (= leftmost) predicate can
2128 2134 enforce its ordering requirement. The other predicates should take the
2129 2135 ordering defined by it.
2130 2136
2131 2137 'A & B' should follow the order of 'A':
2132 2138
2133 2139 $ log '2:0 & 0::2'
2134 2140 2
2135 2141 1
2136 2142 0
2137 2143
2138 2144 'head()' combines sets in right order:
2139 2145
2140 2146 $ log '2:0 & head()'
2141 2147 2
2142 2148 1
2143 2149 0
2144 2150
2145 2151 'x:y' takes ordering parameter into account:
2146 2152
2147 2153 $ try -p optimized '3:0 & 0:3 & not 2:1'
2148 2154 * optimized:
2149 2155 (difference
2150 2156 (and
2151 2157 (range
2152 2158 (symbol '3')
2153 2159 (symbol '0'))
2154 2160 (range
2155 2161 (symbol '0')
2156 2162 (symbol '3')))
2157 2163 (range
2158 2164 (symbol '2')
2159 2165 (symbol '1')))
2160 2166 * set:
2161 2167 <filteredset
2162 2168 <filteredset
2163 2169 <spanset- 0:4>,
2164 2170 <spanset+ 0:4>>,
2165 2171 <not
2166 2172 <spanset+ 1:3>>>
2167 2173 3
2168 2174 0
2169 2175
2170 2176 'a + b', which is optimized to '_list(a b)', should take the ordering of
2171 2177 the left expression:
2172 2178
2173 2179 $ try --optimize '2:0 & (0 + 1 + 2)'
2174 2180 (and
2175 2181 (range
2176 2182 (symbol '2')
2177 2183 (symbol '0'))
2178 2184 (group
2179 2185 (or
2180 2186 (list
2181 2187 (symbol '0')
2182 2188 (symbol '1')
2183 2189 (symbol '2')))))
2184 2190 * optimized:
2185 2191 (and
2186 2192 (range
2187 2193 (symbol '2')
2188 2194 (symbol '0'))
2189 2195 (func
2190 2196 (symbol '_list')
2191 2197 (string '0\x001\x002')))
2192 2198 * set:
2193 2199 <filteredset
2194 2200 <spanset- 0:3>,
2195 2201 <baseset [0, 1, 2]>>
2196 2202 2
2197 2203 1
2198 2204 0
2199 2205
2200 2206 'A + B' should take the ordering of the left expression:
2201 2207
2202 2208 $ try --optimize '2:0 & (0:1 + 2)'
2203 2209 (and
2204 2210 (range
2205 2211 (symbol '2')
2206 2212 (symbol '0'))
2207 2213 (group
2208 2214 (or
2209 2215 (list
2210 2216 (range
2211 2217 (symbol '0')
2212 2218 (symbol '1'))
2213 2219 (symbol '2')))))
2214 2220 * optimized:
2215 2221 (and
2216 2222 (range
2217 2223 (symbol '2')
2218 2224 (symbol '0'))
2219 2225 (or
2220 2226 (list
2221 2227 (range
2222 2228 (symbol '0')
2223 2229 (symbol '1'))
2224 2230 (symbol '2'))))
2225 2231 * set:
2226 2232 <filteredset
2227 2233 <spanset- 0:3>,
2228 2234 <addset
2229 2235 <spanset+ 0:2>,
2230 2236 <baseset [2]>>>
2231 2237 2
2232 2238 1
2233 2239 0
2234 2240
2235 2241 '_intlist(a b)' should behave like 'a + b':
2236 2242
2237 2243 $ trylist --optimize '2:0 & %ld' 0 1 2
2238 2244 (and
2239 2245 (range
2240 2246 (symbol '2')
2241 2247 (symbol '0'))
2242 2248 (func
2243 2249 (symbol '_intlist')
2244 2250 (string '0\x001\x002')))
2245 2251 * optimized:
2246 2252 (andsmally
2247 2253 (range
2248 2254 (symbol '2')
2249 2255 (symbol '0'))
2250 2256 (func
2251 2257 (symbol '_intlist')
2252 2258 (string '0\x001\x002')))
2253 2259 * set:
2254 2260 <filteredset
2255 2261 <spanset- 0:3>,
2256 2262 <baseset+ [0, 1, 2]>>
2257 2263 2
2258 2264 1
2259 2265 0
2260 2266
2261 2267 $ trylist --optimize '%ld & 2:0' 0 2 1
2262 2268 (and
2263 2269 (func
2264 2270 (symbol '_intlist')
2265 2271 (string '0\x002\x001'))
2266 2272 (range
2267 2273 (symbol '2')
2268 2274 (symbol '0')))
2269 2275 * optimized:
2270 2276 (and
2271 2277 (func
2272 2278 (symbol '_intlist')
2273 2279 (string '0\x002\x001'))
2274 2280 (range
2275 2281 (symbol '2')
2276 2282 (symbol '0')))
2277 2283 * set:
2278 2284 <filteredset
2279 2285 <baseset [0, 2, 1]>,
2280 2286 <spanset- 0:3>>
2281 2287 0
2282 2288 2
2283 2289 1
2284 2290
2285 2291 '_hexlist(a b)' should behave like 'a + b':
2286 2292
2287 2293 $ trylist --optimize --bin '2:0 & %ln' `hg log -T '{node} ' -r0:2`
2288 2294 (and
2289 2295 (range
2290 2296 (symbol '2')
2291 2297 (symbol '0'))
2292 2298 (func
2293 2299 (symbol '_hexlist')
2294 2300 (string '*'))) (glob)
2295 2301 * optimized:
2296 2302 (and
2297 2303 (range
2298 2304 (symbol '2')
2299 2305 (symbol '0'))
2300 2306 (func
2301 2307 (symbol '_hexlist')
2302 2308 (string '*'))) (glob)
2303 2309 * set:
2304 2310 <filteredset
2305 2311 <spanset- 0:3>,
2306 2312 <baseset [0, 1, 2]>>
2307 2313 2
2308 2314 1
2309 2315 0
2310 2316
2311 2317 $ trylist --optimize --bin '%ln & 2:0' `hg log -T '{node} ' -r0+2+1`
2312 2318 (and
2313 2319 (func
2314 2320 (symbol '_hexlist')
2315 2321 (string '*')) (glob)
2316 2322 (range
2317 2323 (symbol '2')
2318 2324 (symbol '0')))
2319 2325 * optimized:
2320 2326 (andsmally
2321 2327 (func
2322 2328 (symbol '_hexlist')
2323 2329 (string '*')) (glob)
2324 2330 (range
2325 2331 (symbol '2')
2326 2332 (symbol '0')))
2327 2333 * set:
2328 2334 <baseset [0, 2, 1]>
2329 2335 0
2330 2336 2
2331 2337 1
2332 2338
2333 2339 '_list' should not go through the slow follow-order path if order doesn't
2334 2340 matter:
2335 2341
2336 2342 $ try -p optimized '2:0 & not (0 + 1)'
2337 2343 * optimized:
2338 2344 (difference
2339 2345 (range
2340 2346 (symbol '2')
2341 2347 (symbol '0'))
2342 2348 (func
2343 2349 (symbol '_list')
2344 2350 (string '0\x001')))
2345 2351 * set:
2346 2352 <filteredset
2347 2353 <spanset- 0:3>,
2348 2354 <not
2349 2355 <baseset [0, 1]>>>
2350 2356 2
2351 2357
2352 2358 $ try -p optimized '2:0 & not (0:2 & (0 + 1))'
2353 2359 * optimized:
2354 2360 (difference
2355 2361 (range
2356 2362 (symbol '2')
2357 2363 (symbol '0'))
2358 2364 (and
2359 2365 (range
2360 2366 (symbol '0')
2361 2367 (symbol '2'))
2362 2368 (func
2363 2369 (symbol '_list')
2364 2370 (string '0\x001'))))
2365 2371 * set:
2366 2372 <filteredset
2367 2373 <spanset- 0:3>,
2368 2374 <not
2369 2375 <baseset [0, 1]>>>
2370 2376 2
2371 2377
2372 2378 because 'present()' does nothing other than suppressing an error, the
2373 2379 ordering requirement should be forwarded to the nested expression
2374 2380
2375 2381 $ try -p optimized 'present(2 + 0 + 1)'
2376 2382 * optimized:
2377 2383 (func
2378 2384 (symbol 'present')
2379 2385 (func
2380 2386 (symbol '_list')
2381 2387 (string '2\x000\x001')))
2382 2388 * set:
2383 2389 <baseset [2, 0, 1]>
2384 2390 2
2385 2391 0
2386 2392 1
2387 2393
2388 2394 $ try --optimize '2:0 & present(0 + 1 + 2)'
2389 2395 (and
2390 2396 (range
2391 2397 (symbol '2')
2392 2398 (symbol '0'))
2393 2399 (func
2394 2400 (symbol 'present')
2395 2401 (or
2396 2402 (list
2397 2403 (symbol '0')
2398 2404 (symbol '1')
2399 2405 (symbol '2')))))
2400 2406 * optimized:
2401 2407 (and
2402 2408 (range
2403 2409 (symbol '2')
2404 2410 (symbol '0'))
2405 2411 (func
2406 2412 (symbol 'present')
2407 2413 (func
2408 2414 (symbol '_list')
2409 2415 (string '0\x001\x002'))))
2410 2416 * set:
2411 2417 <filteredset
2412 2418 <spanset- 0:3>,
2413 2419 <baseset [0, 1, 2]>>
2414 2420 2
2415 2421 1
2416 2422 0
2417 2423
2418 2424 'reverse()' should take effect only if it is the outermost expression:
2419 2425
2420 2426 $ try --optimize '0:2 & reverse(all())'
2421 2427 (and
2422 2428 (range
2423 2429 (symbol '0')
2424 2430 (symbol '2'))
2425 2431 (func
2426 2432 (symbol 'reverse')
2427 2433 (func
2428 2434 (symbol 'all')
2429 2435 None)))
2430 2436 * optimized:
2431 2437 (and
2432 2438 (range
2433 2439 (symbol '0')
2434 2440 (symbol '2'))
2435 2441 (func
2436 2442 (symbol 'reverse')
2437 2443 (func
2438 2444 (symbol 'all')
2439 2445 None)))
2440 2446 * set:
2441 2447 <filteredset
2442 2448 <spanset+ 0:3>,
2443 2449 <spanset+ 0:10>>
2444 2450 0
2445 2451 1
2446 2452 2
2447 2453
2448 2454 'sort()' should take effect only if it is the outermost expression:
2449 2455
2450 2456 $ try --optimize '0:2 & sort(all(), -rev)'
2451 2457 (and
2452 2458 (range
2453 2459 (symbol '0')
2454 2460 (symbol '2'))
2455 2461 (func
2456 2462 (symbol 'sort')
2457 2463 (list
2458 2464 (func
2459 2465 (symbol 'all')
2460 2466 None)
2461 2467 (negate
2462 2468 (symbol 'rev')))))
2463 2469 * optimized:
2464 2470 (and
2465 2471 (range
2466 2472 (symbol '0')
2467 2473 (symbol '2'))
2468 2474 (func
2469 2475 (symbol 'sort')
2470 2476 (list
2471 2477 (func
2472 2478 (symbol 'all')
2473 2479 None)
2474 2480 (string '-rev'))))
2475 2481 * set:
2476 2482 <filteredset
2477 2483 <spanset+ 0:3>,
2478 2484 <spanset+ 0:10>>
2479 2485 0
2480 2486 1
2481 2487 2
2482 2488
2483 2489 invalid argument passed to noop sort():
2484 2490
2485 2491 $ log '0:2 & sort()'
2486 2492 hg: parse error: sort requires one or two arguments
2487 2493 [255]
2488 2494 $ log '0:2 & sort(all(), -invalid)'
2489 2495 hg: parse error: unknown sort key '-invalid'
2490 2496 [255]
2491 2497
2492 2498 for 'A & f(B)', 'B' should not be affected by the order of 'A':
2493 2499
2494 2500 $ try --optimize '2:0 & first(1 + 0 + 2)'
2495 2501 (and
2496 2502 (range
2497 2503 (symbol '2')
2498 2504 (symbol '0'))
2499 2505 (func
2500 2506 (symbol 'first')
2501 2507 (or
2502 2508 (list
2503 2509 (symbol '1')
2504 2510 (symbol '0')
2505 2511 (symbol '2')))))
2506 2512 * optimized:
2507 2513 (and
2508 2514 (range
2509 2515 (symbol '2')
2510 2516 (symbol '0'))
2511 2517 (func
2512 2518 (symbol 'first')
2513 2519 (func
2514 2520 (symbol '_list')
2515 2521 (string '1\x000\x002'))))
2516 2522 * set:
2517 2523 <filteredset
2518 2524 <baseset [1]>,
2519 2525 <spanset- 0:3>>
2520 2526 1
2521 2527
2522 2528 $ try --optimize '2:0 & not last(0 + 2 + 1)'
2523 2529 (and
2524 2530 (range
2525 2531 (symbol '2')
2526 2532 (symbol '0'))
2527 2533 (not
2528 2534 (func
2529 2535 (symbol 'last')
2530 2536 (or
2531 2537 (list
2532 2538 (symbol '0')
2533 2539 (symbol '2')
2534 2540 (symbol '1'))))))
2535 2541 * optimized:
2536 2542 (difference
2537 2543 (range
2538 2544 (symbol '2')
2539 2545 (symbol '0'))
2540 2546 (func
2541 2547 (symbol 'last')
2542 2548 (func
2543 2549 (symbol '_list')
2544 2550 (string '0\x002\x001'))))
2545 2551 * set:
2546 2552 <filteredset
2547 2553 <spanset- 0:3>,
2548 2554 <not
2549 2555 <baseset [1]>>>
2550 2556 2
2551 2557 0
2552 2558
2553 2559 for 'A & (op)(B)', 'B' should not be affected by the order of 'A':
2554 2560
2555 2561 $ try --optimize '2:0 & (1 + 0 + 2):(0 + 2 + 1)'
2556 2562 (and
2557 2563 (range
2558 2564 (symbol '2')
2559 2565 (symbol '0'))
2560 2566 (range
2561 2567 (group
2562 2568 (or
2563 2569 (list
2564 2570 (symbol '1')
2565 2571 (symbol '0')
2566 2572 (symbol '2'))))
2567 2573 (group
2568 2574 (or
2569 2575 (list
2570 2576 (symbol '0')
2571 2577 (symbol '2')
2572 2578 (symbol '1'))))))
2573 2579 * optimized:
2574 2580 (and
2575 2581 (range
2576 2582 (symbol '2')
2577 2583 (symbol '0'))
2578 2584 (range
2579 2585 (func
2580 2586 (symbol '_list')
2581 2587 (string '1\x000\x002'))
2582 2588 (func
2583 2589 (symbol '_list')
2584 2590 (string '0\x002\x001'))))
2585 2591 * set:
2586 2592 <filteredset
2587 2593 <spanset- 0:3>,
2588 2594 <baseset [1]>>
2589 2595 1
2590 2596
2591 2597 'A & B' can be rewritten as 'flipand(B, A)' by weight.
2592 2598
2593 2599 $ try --optimize 'contains("glob:*") & (2 + 0 + 1)'
2594 2600 (and
2595 2601 (func
2596 2602 (symbol 'contains')
2597 2603 (string 'glob:*'))
2598 2604 (group
2599 2605 (or
2600 2606 (list
2601 2607 (symbol '2')
2602 2608 (symbol '0')
2603 2609 (symbol '1')))))
2604 2610 * optimized:
2605 2611 (andsmally
2606 2612 (func
2607 2613 (symbol 'contains')
2608 2614 (string 'glob:*'))
2609 2615 (func
2610 2616 (symbol '_list')
2611 2617 (string '2\x000\x001')))
2612 2618 * set:
2613 2619 <filteredset
2614 2620 <baseset+ [0, 1, 2]>,
2615 2621 <contains 'glob:*'>>
2616 2622 0
2617 2623 1
2618 2624 2
2619 2625
2620 2626 and in this example, 'A & B' is rewritten as 'B & A', but 'A' overrides
2621 2627 the order appropriately:
2622 2628
2623 2629 $ try --optimize 'reverse(contains("glob:*")) & (0 + 2 + 1)'
2624 2630 (and
2625 2631 (func
2626 2632 (symbol 'reverse')
2627 2633 (func
2628 2634 (symbol 'contains')
2629 2635 (string 'glob:*')))
2630 2636 (group
2631 2637 (or
2632 2638 (list
2633 2639 (symbol '0')
2634 2640 (symbol '2')
2635 2641 (symbol '1')))))
2636 2642 * optimized:
2637 2643 (andsmally
2638 2644 (func
2639 2645 (symbol 'reverse')
2640 2646 (func
2641 2647 (symbol 'contains')
2642 2648 (string 'glob:*')))
2643 2649 (func
2644 2650 (symbol '_list')
2645 2651 (string '0\x002\x001')))
2646 2652 * set:
2647 2653 <filteredset
2648 2654 <baseset- [0, 1, 2]>,
2649 2655 <contains 'glob:*'>>
2650 2656 2
2651 2657 1
2652 2658 0
2653 2659
2654 2660 test sort revset
2655 2661 --------------------------------------------
2656 2662
2657 2663 test when adding two unordered revsets
2658 2664
2659 2665 $ log 'sort(keyword(issue) or modifies(b))'
2660 2666 4
2661 2667 6
2662 2668
2663 2669 test when sorting a reversed collection in the same way it is
2664 2670
2665 2671 $ log 'sort(reverse(all()), -rev)'
2666 2672 9
2667 2673 8
2668 2674 7
2669 2675 6
2670 2676 5
2671 2677 4
2672 2678 3
2673 2679 2
2674 2680 1
2675 2681 0
2676 2682
2677 2683 test when sorting a reversed collection
2678 2684
2679 2685 $ log 'sort(reverse(all()), rev)'
2680 2686 0
2681 2687 1
2682 2688 2
2683 2689 3
2684 2690 4
2685 2691 5
2686 2692 6
2687 2693 7
2688 2694 8
2689 2695 9
2690 2696
2691 2697
2692 2698 test sorting two sorted collections in different orders
2693 2699
2694 2700 $ log 'sort(outgoing() or reverse(removes(a)), rev)'
2695 2701 2
2696 2702 6
2697 2703 8
2698 2704 9
2699 2705
2700 2706 test sorting two sorted collections in different orders backwards
2701 2707
2702 2708 $ log 'sort(outgoing() or reverse(removes(a)), -rev)'
2703 2709 9
2704 2710 8
2705 2711 6
2706 2712 2
2707 2713
2708 2714 test empty sort key which is noop
2709 2715
2710 2716 $ log 'sort(0 + 2 + 1, "")'
2711 2717 0
2712 2718 2
2713 2719 1
2714 2720
2715 2721 test invalid sort keys
2716 2722
2717 2723 $ log 'sort(all(), -invalid)'
2718 2724 hg: parse error: unknown sort key '-invalid'
2719 2725 [255]
2720 2726
2721 2727 $ cd ..
2722 2728
2723 2729 test sorting by multiple keys including variable-length strings
2724 2730
2725 2731 $ hg init sorting
2726 2732 $ cd sorting
2727 2733 $ cat <<EOF >> .hg/hgrc
2728 2734 > [ui]
2729 2735 > logtemplate = '{rev} {branch|p5}{desc|p5}{author|p5}{date|hgdate}\n'
2730 2736 > [templatealias]
2731 2737 > p5(s) = pad(s, 5)
2732 2738 > EOF
2733 2739 $ hg branch -qf b12
2734 2740 $ hg ci -m m111 -u u112 -d '111 10800'
2735 2741 $ hg branch -qf b11
2736 2742 $ hg ci -m m12 -u u111 -d '112 7200'
2737 2743 $ hg branch -qf b111
2738 2744 $ hg ci -m m11 -u u12 -d '111 3600'
2739 2745 $ hg branch -qf b112
2740 2746 $ hg ci -m m111 -u u11 -d '120 0'
2741 2747 $ hg branch -qf b111
2742 2748 $ hg ci -m m112 -u u111 -d '110 14400'
2743 2749 created new head
2744 2750
2745 2751 compare revisions (has fast path):
2746 2752
2747 2753 $ hg log -r 'sort(all(), rev)'
2748 2754 0 b12 m111 u112 111 10800
2749 2755 1 b11 m12 u111 112 7200
2750 2756 2 b111 m11 u12 111 3600
2751 2757 3 b112 m111 u11 120 0
2752 2758 4 b111 m112 u111 110 14400
2753 2759
2754 2760 $ hg log -r 'sort(all(), -rev)'
2755 2761 4 b111 m112 u111 110 14400
2756 2762 3 b112 m111 u11 120 0
2757 2763 2 b111 m11 u12 111 3600
2758 2764 1 b11 m12 u111 112 7200
2759 2765 0 b12 m111 u112 111 10800
2760 2766
2761 2767 compare variable-length strings (issue5218):
2762 2768
2763 2769 $ hg log -r 'sort(all(), branch)'
2764 2770 1 b11 m12 u111 112 7200
2765 2771 2 b111 m11 u12 111 3600
2766 2772 4 b111 m112 u111 110 14400
2767 2773 3 b112 m111 u11 120 0
2768 2774 0 b12 m111 u112 111 10800
2769 2775
2770 2776 $ hg log -r 'sort(all(), -branch)'
2771 2777 0 b12 m111 u112 111 10800
2772 2778 3 b112 m111 u11 120 0
2773 2779 2 b111 m11 u12 111 3600
2774 2780 4 b111 m112 u111 110 14400
2775 2781 1 b11 m12 u111 112 7200
2776 2782
2777 2783 $ hg log -r 'sort(all(), desc)'
2778 2784 2 b111 m11 u12 111 3600
2779 2785 0 b12 m111 u112 111 10800
2780 2786 3 b112 m111 u11 120 0
2781 2787 4 b111 m112 u111 110 14400
2782 2788 1 b11 m12 u111 112 7200
2783 2789
2784 2790 $ hg log -r 'sort(all(), -desc)'
2785 2791 1 b11 m12 u111 112 7200
2786 2792 4 b111 m112 u111 110 14400
2787 2793 0 b12 m111 u112 111 10800
2788 2794 3 b112 m111 u11 120 0
2789 2795 2 b111 m11 u12 111 3600
2790 2796
2791 2797 $ hg log -r 'sort(all(), user)'
2792 2798 3 b112 m111 u11 120 0
2793 2799 1 b11 m12 u111 112 7200
2794 2800 4 b111 m112 u111 110 14400
2795 2801 0 b12 m111 u112 111 10800
2796 2802 2 b111 m11 u12 111 3600
2797 2803
2798 2804 $ hg log -r 'sort(all(), -user)'
2799 2805 2 b111 m11 u12 111 3600
2800 2806 0 b12 m111 u112 111 10800
2801 2807 1 b11 m12 u111 112 7200
2802 2808 4 b111 m112 u111 110 14400
2803 2809 3 b112 m111 u11 120 0
2804 2810
2805 2811 compare dates (tz offset should have no effect):
2806 2812
2807 2813 $ hg log -r 'sort(all(), date)'
2808 2814 4 b111 m112 u111 110 14400
2809 2815 0 b12 m111 u112 111 10800
2810 2816 2 b111 m11 u12 111 3600
2811 2817 1 b11 m12 u111 112 7200
2812 2818 3 b112 m111 u11 120 0
2813 2819
2814 2820 $ hg log -r 'sort(all(), -date)'
2815 2821 3 b112 m111 u11 120 0
2816 2822 1 b11 m12 u111 112 7200
2817 2823 0 b12 m111 u112 111 10800
2818 2824 2 b111 m11 u12 111 3600
2819 2825 4 b111 m112 u111 110 14400
2820 2826
2821 2827 be aware that 'sort(x, -k)' is not exactly the same as 'reverse(sort(x, k))'
2822 2828 because '-k' reverses the comparison, not the list itself:
2823 2829
2824 2830 $ hg log -r 'sort(0 + 2, date)'
2825 2831 0 b12 m111 u112 111 10800
2826 2832 2 b111 m11 u12 111 3600
2827 2833
2828 2834 $ hg log -r 'sort(0 + 2, -date)'
2829 2835 0 b12 m111 u112 111 10800
2830 2836 2 b111 m11 u12 111 3600
2831 2837
2832 2838 $ hg log -r 'reverse(sort(0 + 2, date))'
2833 2839 2 b111 m11 u12 111 3600
2834 2840 0 b12 m111 u112 111 10800
2835 2841
2836 2842 sort by multiple keys:
2837 2843
2838 2844 $ hg log -r 'sort(all(), "branch -rev")'
2839 2845 1 b11 m12 u111 112 7200
2840 2846 4 b111 m112 u111 110 14400
2841 2847 2 b111 m11 u12 111 3600
2842 2848 3 b112 m111 u11 120 0
2843 2849 0 b12 m111 u112 111 10800
2844 2850
2845 2851 $ hg log -r 'sort(all(), "-desc -date")'
2846 2852 1 b11 m12 u111 112 7200
2847 2853 4 b111 m112 u111 110 14400
2848 2854 3 b112 m111 u11 120 0
2849 2855 0 b12 m111 u112 111 10800
2850 2856 2 b111 m11 u12 111 3600
2851 2857
2852 2858 $ hg log -r 'sort(all(), "user -branch date rev")'
2853 2859 3 b112 m111 u11 120 0
2854 2860 4 b111 m112 u111 110 14400
2855 2861 1 b11 m12 u111 112 7200
2856 2862 0 b12 m111 u112 111 10800
2857 2863 2 b111 m11 u12 111 3600
2858 2864
2859 2865 toposort prioritises graph branches
2860 2866
2861 2867 $ hg up 2
2862 2868 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
2863 2869 $ touch a
2864 2870 $ hg addremove
2865 2871 adding a
2866 2872 $ hg ci -m 't1' -u 'tu' -d '130 0'
2867 2873 created new head
2868 2874 $ echo 'a' >> a
2869 2875 $ hg ci -m 't2' -u 'tu' -d '130 0'
2870 2876 $ hg book book1
2871 2877 $ hg up 4
2872 2878 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
2873 2879 (leaving bookmark book1)
2874 2880 $ touch a
2875 2881 $ hg addremove
2876 2882 adding a
2877 2883 $ hg ci -m 't3' -u 'tu' -d '130 0'
2878 2884
2879 2885 $ hg log -r 'sort(all(), topo)'
2880 2886 7 b111 t3 tu 130 0
2881 2887 4 b111 m112 u111 110 14400
2882 2888 3 b112 m111 u11 120 0
2883 2889 6 b111 t2 tu 130 0
2884 2890 5 b111 t1 tu 130 0
2885 2891 2 b111 m11 u12 111 3600
2886 2892 1 b11 m12 u111 112 7200
2887 2893 0 b12 m111 u112 111 10800
2888 2894
2889 2895 $ hg log -r 'sort(all(), -topo)'
2890 2896 0 b12 m111 u112 111 10800
2891 2897 1 b11 m12 u111 112 7200
2892 2898 2 b111 m11 u12 111 3600
2893 2899 5 b111 t1 tu 130 0
2894 2900 6 b111 t2 tu 130 0
2895 2901 3 b112 m111 u11 120 0
2896 2902 4 b111 m112 u111 110 14400
2897 2903 7 b111 t3 tu 130 0
2898 2904
2899 2905 $ hg log -r 'sort(all(), topo, topo.firstbranch=book1)'
2900 2906 6 b111 t2 tu 130 0
2901 2907 5 b111 t1 tu 130 0
2902 2908 7 b111 t3 tu 130 0
2903 2909 4 b111 m112 u111 110 14400
2904 2910 3 b112 m111 u11 120 0
2905 2911 2 b111 m11 u12 111 3600
2906 2912 1 b11 m12 u111 112 7200
2907 2913 0 b12 m111 u112 111 10800
2908 2914
2909 2915 topographical sorting can't be combined with other sort keys, and you can't
2910 2916 use the topo.firstbranch option when topo sort is not active:
2911 2917
2912 2918 $ hg log -r 'sort(all(), "topo user")'
2913 2919 hg: parse error: topo sort order cannot be combined with other sort keys
2914 2920 [255]
2915 2921
2916 2922 $ hg log -r 'sort(all(), user, topo.firstbranch=book1)'
2917 2923 hg: parse error: topo.firstbranch can only be used when using the topo sort key
2918 2924 [255]
2919 2925
2920 2926 topo.firstbranch should accept any kind of expressions:
2921 2927
2922 2928 $ hg log -r 'sort(0, topo, topo.firstbranch=(book1))'
2923 2929 0 b12 m111 u112 111 10800
2924 2930
2925 2931 $ cd ..
2926 2932 $ cd repo
2927 2933
2928 2934 test multiline revset with errors
2929 2935
2930 2936 $ echo > multiline-revset
2931 2937 $ echo '. +' >> multiline-revset
2932 2938 $ echo '.^ +' >> multiline-revset
2933 2939 $ hg log -r "`cat multiline-revset`"
2934 2940 hg: parse error at 9: not a prefix: end
2935 2941 ( . + .^ +
2936 2942 ^ here)
2937 2943 [255]
2938 2944 $ hg debugrevspec -v 'revset(first(rev(0)))' -p all
2939 2945 * parsed:
2940 2946 (func
2941 2947 (symbol 'revset')
2942 2948 (func
2943 2949 (symbol 'first')
2944 2950 (func
2945 2951 (symbol 'rev')
2946 2952 (symbol '0'))))
2947 2953 * expanded:
2948 2954 (func
2949 2955 (symbol 'revset')
2950 2956 (func
2951 2957 (symbol 'first')
2952 2958 (func
2953 2959 (symbol 'rev')
2954 2960 (symbol '0'))))
2955 2961 * concatenated:
2956 2962 (func
2957 2963 (symbol 'revset')
2958 2964 (func
2959 2965 (symbol 'first')
2960 2966 (func
2961 2967 (symbol 'rev')
2962 2968 (symbol '0'))))
2963 2969 * analyzed:
2964 2970 (func
2965 2971 (symbol 'revset')
2966 2972 (func
2967 2973 (symbol 'first')
2968 2974 (func
2969 2975 (symbol 'rev')
2970 2976 (symbol '0'))))
2971 2977 * optimized:
2972 2978 (func
2973 2979 (symbol 'revset')
2974 2980 (func
2975 2981 (symbol 'first')
2976 2982 (func
2977 2983 (symbol 'rev')
2978 2984 (symbol '0'))))
2979 2985 * set:
2980 2986 <baseset+ [0]>
2981 2987 0
2982 2988
2983 2989 abort if the revset doesn't expect given size
2984 2990 $ log 'expectsize()'
2985 2991 hg: parse error: invalid set of arguments
2986 2992 [255]
2987 2993 $ log 'expectsize(0:2, a)'
2988 2994 hg: parse error: expectsize requires a size range or a positive integer
2989 2995 [255]
2990 2996 $ log 'expectsize(0:2, 3)'
2991 2997 0
2992 2998 1
2993 2999 2
2994 3000
2995 3001 $ log 'expectsize(2:0, 3)'
2996 3002 2
2997 3003 1
2998 3004 0
2999 3005 $ log 'expectsize(0:1, 1)'
3000 3006 abort: revset size mismatch. expected 1, got 2!
3001 3007 [255]
3002 3008 $ log 'expectsize(0:4, -1)'
3003 3009 hg: parse error: negative size
3004 3010 [255]
3005 3011 $ log 'expectsize(0:2, 2:4)'
3006 3012 0
3007 3013 1
3008 3014 2
3009 3015 $ log 'expectsize(0:1, 3:5)'
3010 3016 abort: revset size mismatch. expected between 3 and 5, got 2!
3011 3017 [255]
3012 3018 $ log 'expectsize(0:1, -1:2)'
3013 3019 hg: parse error: negative size
3014 3020 [255]
3015 3021 $ log 'expectsize(0:1, 1:-2)'
3016 3022 hg: parse error: negative size
3017 3023 [255]
3018 3024 $ log 'expectsize(0:2, a:4)'
3019 3025 hg: parse error: size range bounds must be integers
3020 3026 [255]
3021 3027 $ log 'expectsize(0:2, 2:b)'
3022 3028 hg: parse error: size range bounds must be integers
3023 3029 [255]
3024 3030 $ log 'expectsize(0:2, 2:)'
3025 3031 0
3026 3032 1
3027 3033 2
3028 3034 $ log 'expectsize(0:2, :5)'
3029 3035 0
3030 3036 1
3031 3037 2
3032 3038 $ log 'expectsize(0:2, :)'
3033 3039 0
3034 3040 1
3035 3041 2
3036 3042 $ log 'expectsize(0:2, 4:)'
3037 3043 abort: revset size mismatch. expected between 4 and 11, got 3!
3038 3044 [255]
3039 3045 $ log 'expectsize(0:2, :2)'
3040 3046 abort: revset size mismatch. expected between 0 and 2, got 3!
3041 3047 [255]
General Comments 0
You need to be logged in to leave comments. Login now