##// END OF EJS Templates
revset aliases
Alexander Solovyov -
r14098:9f5a0acb default
parent child Browse files
Show More

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

@@ -1,1398 +1,1398 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 node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, glob, tempfile
11 11 import util, scmutil, templater, patch, error, templatekw
12 12 import match as matchmod
13 13 import similar, revset, subrepo
14 14
15 15 revrangesep = ':'
16 16
17 17 def parsealiases(cmd):
18 18 return cmd.lstrip("^").split("|")
19 19
20 20 def findpossible(cmd, table, strict=False):
21 21 """
22 22 Return cmd -> (aliases, command table entry)
23 23 for each matching command.
24 24 Return debug commands (or their aliases) only if no normal command matches.
25 25 """
26 26 choice = {}
27 27 debugchoice = {}
28 28 for e in table.keys():
29 29 aliases = parsealiases(e)
30 30 found = None
31 31 if cmd in aliases:
32 32 found = cmd
33 33 elif not strict:
34 34 for a in aliases:
35 35 if a.startswith(cmd):
36 36 found = a
37 37 break
38 38 if found is not None:
39 39 if aliases[0].startswith("debug") or found.startswith("debug"):
40 40 debugchoice[found] = (aliases, table[e])
41 41 else:
42 42 choice[found] = (aliases, table[e])
43 43
44 44 if not choice and debugchoice:
45 45 choice = debugchoice
46 46
47 47 return choice
48 48
49 49 def findcmd(cmd, table, strict=True):
50 50 """Return (aliases, command table entry) for command string."""
51 51 choice = findpossible(cmd, table, strict)
52 52
53 53 if cmd in choice:
54 54 return choice[cmd]
55 55
56 56 if len(choice) > 1:
57 57 clist = choice.keys()
58 58 clist.sort()
59 59 raise error.AmbiguousCommand(cmd, clist)
60 60
61 61 if choice:
62 62 return choice.values()[0]
63 63
64 64 raise error.UnknownCommand(cmd)
65 65
66 66 def findrepo(p):
67 67 while not os.path.isdir(os.path.join(p, ".hg")):
68 68 oldp, p = p, os.path.dirname(p)
69 69 if p == oldp:
70 70 return None
71 71
72 72 return p
73 73
74 74 def bail_if_changed(repo):
75 75 if repo.dirstate.p2() != nullid:
76 76 raise util.Abort(_('outstanding uncommitted merge'))
77 77 modified, added, removed, deleted = repo.status()[:4]
78 78 if modified or added or removed or deleted:
79 79 raise util.Abort(_("outstanding uncommitted changes"))
80 80
81 81 def logmessage(opts):
82 82 """ get the log message according to -m and -l option """
83 83 message = opts.get('message')
84 84 logfile = opts.get('logfile')
85 85
86 86 if message and logfile:
87 87 raise util.Abort(_('options --message and --logfile are mutually '
88 88 'exclusive'))
89 89 if not message and logfile:
90 90 try:
91 91 if logfile == '-':
92 92 message = sys.stdin.read()
93 93 else:
94 94 message = open(logfile).read()
95 95 except IOError, inst:
96 96 raise util.Abort(_("can't read commit message '%s': %s") %
97 97 (logfile, inst.strerror))
98 98 return message
99 99
100 100 def loglimit(opts):
101 101 """get the log limit according to option -l/--limit"""
102 102 limit = opts.get('limit')
103 103 if limit:
104 104 try:
105 105 limit = int(limit)
106 106 except ValueError:
107 107 raise util.Abort(_('limit must be a positive integer'))
108 108 if limit <= 0:
109 109 raise util.Abort(_('limit must be positive'))
110 110 else:
111 111 limit = None
112 112 return limit
113 113
114 114 def revsingle(repo, revspec, default='.'):
115 115 if not revspec:
116 116 return repo[default]
117 117
118 118 l = revrange(repo, [revspec])
119 119 if len(l) < 1:
120 120 raise util.Abort(_('empty revision set'))
121 121 return repo[l[-1]]
122 122
123 123 def revpair(repo, revs):
124 124 if not revs:
125 125 return repo.dirstate.p1(), None
126 126
127 127 l = revrange(repo, revs)
128 128
129 129 if len(l) == 0:
130 130 return repo.dirstate.p1(), None
131 131
132 132 if len(l) == 1:
133 133 return repo.lookup(l[0]), None
134 134
135 135 return repo.lookup(l[0]), repo.lookup(l[-1])
136 136
137 137 def revrange(repo, revs):
138 138 """Yield revision as strings from a list of revision specifications."""
139 139
140 140 def revfix(repo, val, defval):
141 141 if not val and val != 0 and defval is not None:
142 142 return defval
143 143 return repo.changelog.rev(repo.lookup(val))
144 144
145 145 seen, l = set(), []
146 146 for spec in revs:
147 147 # attempt to parse old-style ranges first to deal with
148 148 # things like old-tag which contain query metacharacters
149 149 try:
150 150 if isinstance(spec, int):
151 151 seen.add(spec)
152 152 l.append(spec)
153 153 continue
154 154
155 155 if revrangesep in spec:
156 156 start, end = spec.split(revrangesep, 1)
157 157 start = revfix(repo, start, 0)
158 158 end = revfix(repo, end, len(repo) - 1)
159 159 step = start > end and -1 or 1
160 160 for rev in xrange(start, end + step, step):
161 161 if rev in seen:
162 162 continue
163 163 seen.add(rev)
164 164 l.append(rev)
165 165 continue
166 166 elif spec and spec in repo: # single unquoted rev
167 167 rev = revfix(repo, spec, None)
168 168 if rev in seen:
169 169 continue
170 170 seen.add(rev)
171 171 l.append(rev)
172 172 continue
173 173 except error.RepoLookupError:
174 174 pass
175 175
176 176 # fall through to new-style queries if old-style fails
177 m = revset.match(spec)
177 m = revset.match(repo.ui, spec)
178 178 for r in m(repo, range(len(repo))):
179 179 if r not in seen:
180 180 l.append(r)
181 181 seen.update(l)
182 182
183 183 return l
184 184
185 185 def make_filename(repo, pat, node,
186 186 total=None, seqno=None, revwidth=None, pathname=None):
187 187 node_expander = {
188 188 'H': lambda: hex(node),
189 189 'R': lambda: str(repo.changelog.rev(node)),
190 190 'h': lambda: short(node),
191 191 }
192 192 expander = {
193 193 '%': lambda: '%',
194 194 'b': lambda: os.path.basename(repo.root),
195 195 }
196 196
197 197 try:
198 198 if node:
199 199 expander.update(node_expander)
200 200 if node:
201 201 expander['r'] = (lambda:
202 202 str(repo.changelog.rev(node)).zfill(revwidth or 0))
203 203 if total is not None:
204 204 expander['N'] = lambda: str(total)
205 205 if seqno is not None:
206 206 expander['n'] = lambda: str(seqno)
207 207 if total is not None and seqno is not None:
208 208 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
209 209 if pathname is not None:
210 210 expander['s'] = lambda: os.path.basename(pathname)
211 211 expander['d'] = lambda: os.path.dirname(pathname) or '.'
212 212 expander['p'] = lambda: pathname
213 213
214 214 newname = []
215 215 patlen = len(pat)
216 216 i = 0
217 217 while i < patlen:
218 218 c = pat[i]
219 219 if c == '%':
220 220 i += 1
221 221 c = pat[i]
222 222 c = expander[c]()
223 223 newname.append(c)
224 224 i += 1
225 225 return ''.join(newname)
226 226 except KeyError, inst:
227 227 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
228 228 inst.args[0])
229 229
230 230 def make_file(repo, pat, node=None,
231 231 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
232 232
233 233 writable = mode not in ('r', 'rb')
234 234
235 235 if not pat or pat == '-':
236 236 fp = writable and sys.stdout or sys.stdin
237 237 return os.fdopen(os.dup(fp.fileno()), mode)
238 238 if hasattr(pat, 'write') and writable:
239 239 return pat
240 240 if hasattr(pat, 'read') and 'r' in mode:
241 241 return pat
242 242 return open(make_filename(repo, pat, node, total, seqno, revwidth,
243 243 pathname),
244 244 mode)
245 245
246 246 def expandpats(pats):
247 247 if not util.expandglobs:
248 248 return list(pats)
249 249 ret = []
250 250 for p in pats:
251 251 kind, name = matchmod._patsplit(p, None)
252 252 if kind is None:
253 253 try:
254 254 globbed = glob.glob(name)
255 255 except re.error:
256 256 globbed = [name]
257 257 if globbed:
258 258 ret.extend(globbed)
259 259 continue
260 260 ret.append(p)
261 261 return ret
262 262
263 263 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
264 264 if pats == ("",):
265 265 pats = []
266 266 if not globbed and default == 'relpath':
267 267 pats = expandpats(pats or [])
268 268 m = matchmod.match(repo.root, repo.getcwd(), pats,
269 269 opts.get('include'), opts.get('exclude'), default,
270 270 auditor=repo.auditor)
271 271 def badfn(f, msg):
272 272 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
273 273 m.bad = badfn
274 274 return m
275 275
276 276 def matchall(repo):
277 277 return matchmod.always(repo.root, repo.getcwd())
278 278
279 279 def matchfiles(repo, files):
280 280 return matchmod.exact(repo.root, repo.getcwd(), files)
281 281
282 282 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
283 283 if dry_run is None:
284 284 dry_run = opts.get('dry_run')
285 285 if similarity is None:
286 286 similarity = float(opts.get('similarity') or 0)
287 287 # we'd use status here, except handling of symlinks and ignore is tricky
288 288 added, unknown, deleted, removed = [], [], [], []
289 289 audit_path = scmutil.path_auditor(repo.root)
290 290 m = match(repo, pats, opts)
291 291 for abs in repo.walk(m):
292 292 target = repo.wjoin(abs)
293 293 good = True
294 294 try:
295 295 audit_path(abs)
296 296 except (OSError, util.Abort):
297 297 good = False
298 298 rel = m.rel(abs)
299 299 exact = m.exact(abs)
300 300 if good and abs not in repo.dirstate:
301 301 unknown.append(abs)
302 302 if repo.ui.verbose or not exact:
303 303 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
304 304 elif repo.dirstate[abs] != 'r' and (not good or not os.path.lexists(target)
305 305 or (os.path.isdir(target) and not os.path.islink(target))):
306 306 deleted.append(abs)
307 307 if repo.ui.verbose or not exact:
308 308 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
309 309 # for finding renames
310 310 elif repo.dirstate[abs] == 'r':
311 311 removed.append(abs)
312 312 elif repo.dirstate[abs] == 'a':
313 313 added.append(abs)
314 314 copies = {}
315 315 if similarity > 0:
316 316 for old, new, score in similar.findrenames(repo,
317 317 added + unknown, removed + deleted, similarity):
318 318 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
319 319 repo.ui.status(_('recording removal of %s as rename to %s '
320 320 '(%d%% similar)\n') %
321 321 (m.rel(old), m.rel(new), score * 100))
322 322 copies[new] = old
323 323
324 324 if not dry_run:
325 325 wctx = repo[None]
326 326 wlock = repo.wlock()
327 327 try:
328 328 wctx.remove(deleted)
329 329 wctx.add(unknown)
330 330 for new, old in copies.iteritems():
331 331 wctx.copy(old, new)
332 332 finally:
333 333 wlock.release()
334 334
335 335 def updatedir(ui, repo, patches, similarity=0):
336 336 '''Update dirstate after patch application according to metadata'''
337 337 if not patches:
338 338 return
339 339 copies = []
340 340 removes = set()
341 341 cfiles = patches.keys()
342 342 cwd = repo.getcwd()
343 343 if cwd:
344 344 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
345 345 for f in patches:
346 346 gp = patches[f]
347 347 if not gp:
348 348 continue
349 349 if gp.op == 'RENAME':
350 350 copies.append((gp.oldpath, gp.path))
351 351 removes.add(gp.oldpath)
352 352 elif gp.op == 'COPY':
353 353 copies.append((gp.oldpath, gp.path))
354 354 elif gp.op == 'DELETE':
355 355 removes.add(gp.path)
356 356
357 357 wctx = repo[None]
358 358 for src, dst in copies:
359 359 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
360 360 if (not similarity) and removes:
361 361 wctx.remove(sorted(removes), True)
362 362
363 363 for f in patches:
364 364 gp = patches[f]
365 365 if gp and gp.mode:
366 366 islink, isexec = gp.mode
367 367 dst = repo.wjoin(gp.path)
368 368 # patch won't create empty files
369 369 if gp.op == 'ADD' and not os.path.lexists(dst):
370 370 flags = (isexec and 'x' or '') + (islink and 'l' or '')
371 371 repo.wwrite(gp.path, '', flags)
372 372 util.set_flags(dst, islink, isexec)
373 373 addremove(repo, cfiles, similarity=similarity)
374 374 files = patches.keys()
375 375 files.extend([r for r in removes if r not in files])
376 376 return sorted(files)
377 377
378 378 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
379 379 """Update the dirstate to reflect the intent of copying src to dst. For
380 380 different reasons it might not end with dst being marked as copied from src.
381 381 """
382 382 origsrc = repo.dirstate.copied(src) or src
383 383 if dst == origsrc: # copying back a copy?
384 384 if repo.dirstate[dst] not in 'mn' and not dryrun:
385 385 repo.dirstate.normallookup(dst)
386 386 else:
387 387 if repo.dirstate[origsrc] == 'a' and origsrc == src:
388 388 if not ui.quiet:
389 389 ui.warn(_("%s has not been committed yet, so no copy "
390 390 "data will be stored for %s.\n")
391 391 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
392 392 if repo.dirstate[dst] in '?r' and not dryrun:
393 393 wctx.add([dst])
394 394 elif not dryrun:
395 395 wctx.copy(origsrc, dst)
396 396
397 397 def copy(ui, repo, pats, opts, rename=False):
398 398 # called with the repo lock held
399 399 #
400 400 # hgsep => pathname that uses "/" to separate directories
401 401 # ossep => pathname that uses os.sep to separate directories
402 402 cwd = repo.getcwd()
403 403 targets = {}
404 404 after = opts.get("after")
405 405 dryrun = opts.get("dry_run")
406 406 wctx = repo[None]
407 407
408 408 def walkpat(pat):
409 409 srcs = []
410 410 badstates = after and '?' or '?r'
411 411 m = match(repo, [pat], opts, globbed=True)
412 412 for abs in repo.walk(m):
413 413 state = repo.dirstate[abs]
414 414 rel = m.rel(abs)
415 415 exact = m.exact(abs)
416 416 if state in badstates:
417 417 if exact and state == '?':
418 418 ui.warn(_('%s: not copying - file is not managed\n') % rel)
419 419 if exact and state == 'r':
420 420 ui.warn(_('%s: not copying - file has been marked for'
421 421 ' remove\n') % rel)
422 422 continue
423 423 # abs: hgsep
424 424 # rel: ossep
425 425 srcs.append((abs, rel, exact))
426 426 return srcs
427 427
428 428 # abssrc: hgsep
429 429 # relsrc: ossep
430 430 # otarget: ossep
431 431 def copyfile(abssrc, relsrc, otarget, exact):
432 432 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
433 433 reltarget = repo.pathto(abstarget, cwd)
434 434 target = repo.wjoin(abstarget)
435 435 src = repo.wjoin(abssrc)
436 436 state = repo.dirstate[abstarget]
437 437
438 438 scmutil.checkportable(ui, abstarget)
439 439
440 440 # check for collisions
441 441 prevsrc = targets.get(abstarget)
442 442 if prevsrc is not None:
443 443 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
444 444 (reltarget, repo.pathto(abssrc, cwd),
445 445 repo.pathto(prevsrc, cwd)))
446 446 return
447 447
448 448 # check for overwrites
449 449 exists = os.path.lexists(target)
450 450 if not after and exists or after and state in 'mn':
451 451 if not opts['force']:
452 452 ui.warn(_('%s: not overwriting - file exists\n') %
453 453 reltarget)
454 454 return
455 455
456 456 if after:
457 457 if not exists:
458 458 if rename:
459 459 ui.warn(_('%s: not recording move - %s does not exist\n') %
460 460 (relsrc, reltarget))
461 461 else:
462 462 ui.warn(_('%s: not recording copy - %s does not exist\n') %
463 463 (relsrc, reltarget))
464 464 return
465 465 elif not dryrun:
466 466 try:
467 467 if exists:
468 468 os.unlink(target)
469 469 targetdir = os.path.dirname(target) or '.'
470 470 if not os.path.isdir(targetdir):
471 471 os.makedirs(targetdir)
472 472 util.copyfile(src, target)
473 473 except IOError, inst:
474 474 if inst.errno == errno.ENOENT:
475 475 ui.warn(_('%s: deleted in working copy\n') % relsrc)
476 476 else:
477 477 ui.warn(_('%s: cannot copy - %s\n') %
478 478 (relsrc, inst.strerror))
479 479 return True # report a failure
480 480
481 481 if ui.verbose or not exact:
482 482 if rename:
483 483 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
484 484 else:
485 485 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
486 486
487 487 targets[abstarget] = abssrc
488 488
489 489 # fix up dirstate
490 490 dirstatecopy(ui, repo, wctx, abssrc, abstarget, dryrun=dryrun, cwd=cwd)
491 491 if rename and not dryrun:
492 492 wctx.remove([abssrc], not after)
493 493
494 494 # pat: ossep
495 495 # dest ossep
496 496 # srcs: list of (hgsep, hgsep, ossep, bool)
497 497 # return: function that takes hgsep and returns ossep
498 498 def targetpathfn(pat, dest, srcs):
499 499 if os.path.isdir(pat):
500 500 abspfx = scmutil.canonpath(repo.root, cwd, pat)
501 501 abspfx = util.localpath(abspfx)
502 502 if destdirexists:
503 503 striplen = len(os.path.split(abspfx)[0])
504 504 else:
505 505 striplen = len(abspfx)
506 506 if striplen:
507 507 striplen += len(os.sep)
508 508 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
509 509 elif destdirexists:
510 510 res = lambda p: os.path.join(dest,
511 511 os.path.basename(util.localpath(p)))
512 512 else:
513 513 res = lambda p: dest
514 514 return res
515 515
516 516 # pat: ossep
517 517 # dest ossep
518 518 # srcs: list of (hgsep, hgsep, ossep, bool)
519 519 # return: function that takes hgsep and returns ossep
520 520 def targetpathafterfn(pat, dest, srcs):
521 521 if matchmod.patkind(pat):
522 522 # a mercurial pattern
523 523 res = lambda p: os.path.join(dest,
524 524 os.path.basename(util.localpath(p)))
525 525 else:
526 526 abspfx = scmutil.canonpath(repo.root, cwd, pat)
527 527 if len(abspfx) < len(srcs[0][0]):
528 528 # A directory. Either the target path contains the last
529 529 # component of the source path or it does not.
530 530 def evalpath(striplen):
531 531 score = 0
532 532 for s in srcs:
533 533 t = os.path.join(dest, util.localpath(s[0])[striplen:])
534 534 if os.path.lexists(t):
535 535 score += 1
536 536 return score
537 537
538 538 abspfx = util.localpath(abspfx)
539 539 striplen = len(abspfx)
540 540 if striplen:
541 541 striplen += len(os.sep)
542 542 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
543 543 score = evalpath(striplen)
544 544 striplen1 = len(os.path.split(abspfx)[0])
545 545 if striplen1:
546 546 striplen1 += len(os.sep)
547 547 if evalpath(striplen1) > score:
548 548 striplen = striplen1
549 549 res = lambda p: os.path.join(dest,
550 550 util.localpath(p)[striplen:])
551 551 else:
552 552 # a file
553 553 if destdirexists:
554 554 res = lambda p: os.path.join(dest,
555 555 os.path.basename(util.localpath(p)))
556 556 else:
557 557 res = lambda p: dest
558 558 return res
559 559
560 560
561 561 pats = expandpats(pats)
562 562 if not pats:
563 563 raise util.Abort(_('no source or destination specified'))
564 564 if len(pats) == 1:
565 565 raise util.Abort(_('no destination specified'))
566 566 dest = pats.pop()
567 567 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
568 568 if not destdirexists:
569 569 if len(pats) > 1 or matchmod.patkind(pats[0]):
570 570 raise util.Abort(_('with multiple sources, destination must be an '
571 571 'existing directory'))
572 572 if util.endswithsep(dest):
573 573 raise util.Abort(_('destination %s is not a directory') % dest)
574 574
575 575 tfn = targetpathfn
576 576 if after:
577 577 tfn = targetpathafterfn
578 578 copylist = []
579 579 for pat in pats:
580 580 srcs = walkpat(pat)
581 581 if not srcs:
582 582 continue
583 583 copylist.append((tfn(pat, dest, srcs), srcs))
584 584 if not copylist:
585 585 raise util.Abort(_('no files to copy'))
586 586
587 587 errors = 0
588 588 for targetpath, srcs in copylist:
589 589 for abssrc, relsrc, exact in srcs:
590 590 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
591 591 errors += 1
592 592
593 593 if errors:
594 594 ui.warn(_('(consider using --after)\n'))
595 595
596 596 return errors != 0
597 597
598 598 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
599 599 runargs=None, appendpid=False):
600 600 '''Run a command as a service.'''
601 601
602 602 if opts['daemon'] and not opts['daemon_pipefds']:
603 603 # Signal child process startup with file removal
604 604 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
605 605 os.close(lockfd)
606 606 try:
607 607 if not runargs:
608 608 runargs = util.hgcmd() + sys.argv[1:]
609 609 runargs.append('--daemon-pipefds=%s' % lockpath)
610 610 # Don't pass --cwd to the child process, because we've already
611 611 # changed directory.
612 612 for i in xrange(1, len(runargs)):
613 613 if runargs[i].startswith('--cwd='):
614 614 del runargs[i]
615 615 break
616 616 elif runargs[i].startswith('--cwd'):
617 617 del runargs[i:i + 2]
618 618 break
619 619 def condfn():
620 620 return not os.path.exists(lockpath)
621 621 pid = util.rundetached(runargs, condfn)
622 622 if pid < 0:
623 623 raise util.Abort(_('child process failed to start'))
624 624 finally:
625 625 try:
626 626 os.unlink(lockpath)
627 627 except OSError, e:
628 628 if e.errno != errno.ENOENT:
629 629 raise
630 630 if parentfn:
631 631 return parentfn(pid)
632 632 else:
633 633 return
634 634
635 635 if initfn:
636 636 initfn()
637 637
638 638 if opts['pid_file']:
639 639 mode = appendpid and 'a' or 'w'
640 640 fp = open(opts['pid_file'], mode)
641 641 fp.write(str(os.getpid()) + '\n')
642 642 fp.close()
643 643
644 644 if opts['daemon_pipefds']:
645 645 lockpath = opts['daemon_pipefds']
646 646 try:
647 647 os.setsid()
648 648 except AttributeError:
649 649 pass
650 650 os.unlink(lockpath)
651 651 util.hidewindow()
652 652 sys.stdout.flush()
653 653 sys.stderr.flush()
654 654
655 655 nullfd = os.open(util.nulldev, os.O_RDWR)
656 656 logfilefd = nullfd
657 657 if logfile:
658 658 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
659 659 os.dup2(nullfd, 0)
660 660 os.dup2(logfilefd, 1)
661 661 os.dup2(logfilefd, 2)
662 662 if nullfd not in (0, 1, 2):
663 663 os.close(nullfd)
664 664 if logfile and logfilefd not in (0, 1, 2):
665 665 os.close(logfilefd)
666 666
667 667 if runfn:
668 668 return runfn()
669 669
670 670 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
671 671 opts=None):
672 672 '''export changesets as hg patches.'''
673 673
674 674 total = len(revs)
675 675 revwidth = max([len(str(rev)) for rev in revs])
676 676
677 677 def single(rev, seqno, fp):
678 678 ctx = repo[rev]
679 679 node = ctx.node()
680 680 parents = [p.node() for p in ctx.parents() if p]
681 681 branch = ctx.branch()
682 682 if switch_parent:
683 683 parents.reverse()
684 684 prev = (parents and parents[0]) or nullid
685 685
686 686 shouldclose = False
687 687 if not fp:
688 688 fp = make_file(repo, template, node, total=total, seqno=seqno,
689 689 revwidth=revwidth, mode='ab')
690 690 if fp != template:
691 691 shouldclose = True
692 692 if fp != sys.stdout and hasattr(fp, 'name'):
693 693 repo.ui.note("%s\n" % fp.name)
694 694
695 695 fp.write("# HG changeset patch\n")
696 696 fp.write("# User %s\n" % ctx.user())
697 697 fp.write("# Date %d %d\n" % ctx.date())
698 698 if branch and branch != 'default':
699 699 fp.write("# Branch %s\n" % branch)
700 700 fp.write("# Node ID %s\n" % hex(node))
701 701 fp.write("# Parent %s\n" % hex(prev))
702 702 if len(parents) > 1:
703 703 fp.write("# Parent %s\n" % hex(parents[1]))
704 704 fp.write(ctx.description().rstrip())
705 705 fp.write("\n\n")
706 706
707 707 for chunk in patch.diff(repo, prev, node, opts=opts):
708 708 fp.write(chunk)
709 709
710 710 if shouldclose:
711 711 fp.close()
712 712
713 713 for seqno, rev in enumerate(revs):
714 714 single(rev, seqno + 1, fp)
715 715
716 716 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
717 717 changes=None, stat=False, fp=None, prefix='',
718 718 listsubrepos=False):
719 719 '''show diff or diffstat.'''
720 720 if fp is None:
721 721 write = ui.write
722 722 else:
723 723 def write(s, **kw):
724 724 fp.write(s)
725 725
726 726 if stat:
727 727 diffopts = diffopts.copy(context=0)
728 728 width = 80
729 729 if not ui.plain():
730 730 width = ui.termwidth()
731 731 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
732 732 prefix=prefix)
733 733 for chunk, label in patch.diffstatui(util.iterlines(chunks),
734 734 width=width,
735 735 git=diffopts.git):
736 736 write(chunk, label=label)
737 737 else:
738 738 for chunk, label in patch.diffui(repo, node1, node2, match,
739 739 changes, diffopts, prefix=prefix):
740 740 write(chunk, label=label)
741 741
742 742 if listsubrepos:
743 743 ctx1 = repo[node1]
744 744 ctx2 = repo[node2]
745 745 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
746 746 if node2 is not None:
747 747 node2 = ctx2.substate[subpath][1]
748 748 submatch = matchmod.narrowmatcher(subpath, match)
749 749 sub.diff(diffopts, node2, submatch, changes=changes,
750 750 stat=stat, fp=fp, prefix=prefix)
751 751
752 752 class changeset_printer(object):
753 753 '''show changeset information when templating not requested.'''
754 754
755 755 def __init__(self, ui, repo, patch, diffopts, buffered):
756 756 self.ui = ui
757 757 self.repo = repo
758 758 self.buffered = buffered
759 759 self.patch = patch
760 760 self.diffopts = diffopts
761 761 self.header = {}
762 762 self.hunk = {}
763 763 self.lastheader = None
764 764 self.footer = None
765 765
766 766 def flush(self, rev):
767 767 if rev in self.header:
768 768 h = self.header[rev]
769 769 if h != self.lastheader:
770 770 self.lastheader = h
771 771 self.ui.write(h)
772 772 del self.header[rev]
773 773 if rev in self.hunk:
774 774 self.ui.write(self.hunk[rev])
775 775 del self.hunk[rev]
776 776 return 1
777 777 return 0
778 778
779 779 def close(self):
780 780 if self.footer:
781 781 self.ui.write(self.footer)
782 782
783 783 def show(self, ctx, copies=None, matchfn=None, **props):
784 784 if self.buffered:
785 785 self.ui.pushbuffer()
786 786 self._show(ctx, copies, matchfn, props)
787 787 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
788 788 else:
789 789 self._show(ctx, copies, matchfn, props)
790 790
791 791 def _show(self, ctx, copies, matchfn, props):
792 792 '''show a single changeset or file revision'''
793 793 changenode = ctx.node()
794 794 rev = ctx.rev()
795 795
796 796 if self.ui.quiet:
797 797 self.ui.write("%d:%s\n" % (rev, short(changenode)),
798 798 label='log.node')
799 799 return
800 800
801 801 log = self.repo.changelog
802 802 date = util.datestr(ctx.date())
803 803
804 804 hexfunc = self.ui.debugflag and hex or short
805 805
806 806 parents = [(p, hexfunc(log.node(p)))
807 807 for p in self._meaningful_parentrevs(log, rev)]
808 808
809 809 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
810 810 label='log.changeset')
811 811
812 812 branch = ctx.branch()
813 813 # don't show the default branch name
814 814 if branch != 'default':
815 815 self.ui.write(_("branch: %s\n") % branch,
816 816 label='log.branch')
817 817 for bookmark in self.repo.nodebookmarks(changenode):
818 818 self.ui.write(_("bookmark: %s\n") % bookmark,
819 819 label='log.bookmark')
820 820 for tag in self.repo.nodetags(changenode):
821 821 self.ui.write(_("tag: %s\n") % tag,
822 822 label='log.tag')
823 823 for parent in parents:
824 824 self.ui.write(_("parent: %d:%s\n") % parent,
825 825 label='log.parent')
826 826
827 827 if self.ui.debugflag:
828 828 mnode = ctx.manifestnode()
829 829 self.ui.write(_("manifest: %d:%s\n") %
830 830 (self.repo.manifest.rev(mnode), hex(mnode)),
831 831 label='ui.debug log.manifest')
832 832 self.ui.write(_("user: %s\n") % ctx.user(),
833 833 label='log.user')
834 834 self.ui.write(_("date: %s\n") % date,
835 835 label='log.date')
836 836
837 837 if self.ui.debugflag:
838 838 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
839 839 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
840 840 files):
841 841 if value:
842 842 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
843 843 label='ui.debug log.files')
844 844 elif ctx.files() and self.ui.verbose:
845 845 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
846 846 label='ui.note log.files')
847 847 if copies and self.ui.verbose:
848 848 copies = ['%s (%s)' % c for c in copies]
849 849 self.ui.write(_("copies: %s\n") % ' '.join(copies),
850 850 label='ui.note log.copies')
851 851
852 852 extra = ctx.extra()
853 853 if extra and self.ui.debugflag:
854 854 for key, value in sorted(extra.items()):
855 855 self.ui.write(_("extra: %s=%s\n")
856 856 % (key, value.encode('string_escape')),
857 857 label='ui.debug log.extra')
858 858
859 859 description = ctx.description().strip()
860 860 if description:
861 861 if self.ui.verbose:
862 862 self.ui.write(_("description:\n"),
863 863 label='ui.note log.description')
864 864 self.ui.write(description,
865 865 label='ui.note log.description')
866 866 self.ui.write("\n\n")
867 867 else:
868 868 self.ui.write(_("summary: %s\n") %
869 869 description.splitlines()[0],
870 870 label='log.summary')
871 871 self.ui.write("\n")
872 872
873 873 self.showpatch(changenode, matchfn)
874 874
875 875 def showpatch(self, node, matchfn):
876 876 if not matchfn:
877 877 matchfn = self.patch
878 878 if matchfn:
879 879 stat = self.diffopts.get('stat')
880 880 diff = self.diffopts.get('patch')
881 881 diffopts = patch.diffopts(self.ui, self.diffopts)
882 882 prev = self.repo.changelog.parents(node)[0]
883 883 if stat:
884 884 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
885 885 match=matchfn, stat=True)
886 886 if diff:
887 887 if stat:
888 888 self.ui.write("\n")
889 889 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
890 890 match=matchfn, stat=False)
891 891 self.ui.write("\n")
892 892
893 893 def _meaningful_parentrevs(self, log, rev):
894 894 """Return list of meaningful (or all if debug) parentrevs for rev.
895 895
896 896 For merges (two non-nullrev revisions) both parents are meaningful.
897 897 Otherwise the first parent revision is considered meaningful if it
898 898 is not the preceding revision.
899 899 """
900 900 parents = log.parentrevs(rev)
901 901 if not self.ui.debugflag and parents[1] == nullrev:
902 902 if parents[0] >= rev - 1:
903 903 parents = []
904 904 else:
905 905 parents = [parents[0]]
906 906 return parents
907 907
908 908
909 909 class changeset_templater(changeset_printer):
910 910 '''format changeset information.'''
911 911
912 912 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
913 913 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
914 914 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
915 915 defaulttempl = {
916 916 'parent': '{rev}:{node|formatnode} ',
917 917 'manifest': '{rev}:{node|formatnode}',
918 918 'file_copy': '{name} ({source})',
919 919 'extra': '{key}={value|stringescape}'
920 920 }
921 921 # filecopy is preserved for compatibility reasons
922 922 defaulttempl['filecopy'] = defaulttempl['file_copy']
923 923 self.t = templater.templater(mapfile, {'formatnode': formatnode},
924 924 cache=defaulttempl)
925 925 self.cache = {}
926 926
927 927 def use_template(self, t):
928 928 '''set template string to use'''
929 929 self.t.cache['changeset'] = t
930 930
931 931 def _meaningful_parentrevs(self, ctx):
932 932 """Return list of meaningful (or all if debug) parentrevs for rev.
933 933 """
934 934 parents = ctx.parents()
935 935 if len(parents) > 1:
936 936 return parents
937 937 if self.ui.debugflag:
938 938 return [parents[0], self.repo['null']]
939 939 if parents[0].rev() >= ctx.rev() - 1:
940 940 return []
941 941 return parents
942 942
943 943 def _show(self, ctx, copies, matchfn, props):
944 944 '''show a single changeset or file revision'''
945 945
946 946 showlist = templatekw.showlist
947 947
948 948 # showparents() behaviour depends on ui trace level which
949 949 # causes unexpected behaviours at templating level and makes
950 950 # it harder to extract it in a standalone function. Its
951 951 # behaviour cannot be changed so leave it here for now.
952 952 def showparents(**args):
953 953 ctx = args['ctx']
954 954 parents = [[('rev', p.rev()), ('node', p.hex())]
955 955 for p in self._meaningful_parentrevs(ctx)]
956 956 return showlist('parent', parents, **args)
957 957
958 958 props = props.copy()
959 959 props.update(templatekw.keywords)
960 960 props['parents'] = showparents
961 961 props['templ'] = self.t
962 962 props['ctx'] = ctx
963 963 props['repo'] = self.repo
964 964 props['revcache'] = {'copies': copies}
965 965 props['cache'] = self.cache
966 966
967 967 # find correct templates for current mode
968 968
969 969 tmplmodes = [
970 970 (True, None),
971 971 (self.ui.verbose, 'verbose'),
972 972 (self.ui.quiet, 'quiet'),
973 973 (self.ui.debugflag, 'debug'),
974 974 ]
975 975
976 976 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
977 977 for mode, postfix in tmplmodes:
978 978 for type in types:
979 979 cur = postfix and ('%s_%s' % (type, postfix)) or type
980 980 if mode and cur in self.t:
981 981 types[type] = cur
982 982
983 983 try:
984 984
985 985 # write header
986 986 if types['header']:
987 987 h = templater.stringify(self.t(types['header'], **props))
988 988 if self.buffered:
989 989 self.header[ctx.rev()] = h
990 990 else:
991 991 if self.lastheader != h:
992 992 self.lastheader = h
993 993 self.ui.write(h)
994 994
995 995 # write changeset metadata, then patch if requested
996 996 key = types['changeset']
997 997 self.ui.write(templater.stringify(self.t(key, **props)))
998 998 self.showpatch(ctx.node(), matchfn)
999 999
1000 1000 if types['footer']:
1001 1001 if not self.footer:
1002 1002 self.footer = templater.stringify(self.t(types['footer'],
1003 1003 **props))
1004 1004
1005 1005 except KeyError, inst:
1006 1006 msg = _("%s: no key named '%s'")
1007 1007 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
1008 1008 except SyntaxError, inst:
1009 1009 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
1010 1010
1011 1011 def show_changeset(ui, repo, opts, buffered=False):
1012 1012 """show one changeset using template or regular display.
1013 1013
1014 1014 Display format will be the first non-empty hit of:
1015 1015 1. option 'template'
1016 1016 2. option 'style'
1017 1017 3. [ui] setting 'logtemplate'
1018 1018 4. [ui] setting 'style'
1019 1019 If all of these values are either the unset or the empty string,
1020 1020 regular display via changeset_printer() is done.
1021 1021 """
1022 1022 # options
1023 1023 patch = False
1024 1024 if opts.get('patch') or opts.get('stat'):
1025 1025 patch = matchall(repo)
1026 1026
1027 1027 tmpl = opts.get('template')
1028 1028 style = None
1029 1029 if tmpl:
1030 1030 tmpl = templater.parsestring(tmpl, quoted=False)
1031 1031 else:
1032 1032 style = opts.get('style')
1033 1033
1034 1034 # ui settings
1035 1035 if not (tmpl or style):
1036 1036 tmpl = ui.config('ui', 'logtemplate')
1037 1037 if tmpl:
1038 1038 tmpl = templater.parsestring(tmpl)
1039 1039 else:
1040 1040 style = util.expandpath(ui.config('ui', 'style', ''))
1041 1041
1042 1042 if not (tmpl or style):
1043 1043 return changeset_printer(ui, repo, patch, opts, buffered)
1044 1044
1045 1045 mapfile = None
1046 1046 if style and not tmpl:
1047 1047 mapfile = style
1048 1048 if not os.path.split(mapfile)[0]:
1049 1049 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1050 1050 or templater.templatepath(mapfile))
1051 1051 if mapname:
1052 1052 mapfile = mapname
1053 1053
1054 1054 try:
1055 1055 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
1056 1056 except SyntaxError, inst:
1057 1057 raise util.Abort(inst.args[0])
1058 1058 if tmpl:
1059 1059 t.use_template(tmpl)
1060 1060 return t
1061 1061
1062 1062 def finddate(ui, repo, date):
1063 1063 """Find the tipmost changeset that matches the given date spec"""
1064 1064
1065 1065 df = util.matchdate(date)
1066 1066 m = matchall(repo)
1067 1067 results = {}
1068 1068
1069 1069 def prep(ctx, fns):
1070 1070 d = ctx.date()
1071 1071 if df(d[0]):
1072 1072 results[ctx.rev()] = d
1073 1073
1074 1074 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1075 1075 rev = ctx.rev()
1076 1076 if rev in results:
1077 1077 ui.status(_("Found revision %s from %s\n") %
1078 1078 (rev, util.datestr(results[rev])))
1079 1079 return str(rev)
1080 1080
1081 1081 raise util.Abort(_("revision matching date not found"))
1082 1082
1083 1083 def walkchangerevs(repo, match, opts, prepare):
1084 1084 '''Iterate over files and the revs in which they changed.
1085 1085
1086 1086 Callers most commonly need to iterate backwards over the history
1087 1087 in which they are interested. Doing so has awful (quadratic-looking)
1088 1088 performance, so we use iterators in a "windowed" way.
1089 1089
1090 1090 We walk a window of revisions in the desired order. Within the
1091 1091 window, we first walk forwards to gather data, then in the desired
1092 1092 order (usually backwards) to display it.
1093 1093
1094 1094 This function returns an iterator yielding contexts. Before
1095 1095 yielding each context, the iterator will first call the prepare
1096 1096 function on each context in the window in forward order.'''
1097 1097
1098 1098 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1099 1099 if start < end:
1100 1100 while start < end:
1101 1101 yield start, min(windowsize, end - start)
1102 1102 start += windowsize
1103 1103 if windowsize < sizelimit:
1104 1104 windowsize *= 2
1105 1105 else:
1106 1106 while start > end:
1107 1107 yield start, min(windowsize, start - end - 1)
1108 1108 start -= windowsize
1109 1109 if windowsize < sizelimit:
1110 1110 windowsize *= 2
1111 1111
1112 1112 follow = opts.get('follow') or opts.get('follow_first')
1113 1113
1114 1114 if not len(repo):
1115 1115 return []
1116 1116
1117 1117 if follow:
1118 1118 defrange = '%s:0' % repo['.'].rev()
1119 1119 else:
1120 1120 defrange = '-1:0'
1121 1121 revs = revrange(repo, opts['rev'] or [defrange])
1122 1122 if not revs:
1123 1123 return []
1124 1124 wanted = set()
1125 1125 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1126 1126 fncache = {}
1127 1127 change = util.cachefunc(repo.changectx)
1128 1128
1129 1129 # First step is to fill wanted, the set of revisions that we want to yield.
1130 1130 # When it does not induce extra cost, we also fill fncache for revisions in
1131 1131 # wanted: a cache of filenames that were changed (ctx.files()) and that
1132 1132 # match the file filtering conditions.
1133 1133
1134 1134 if not slowpath and not match.files():
1135 1135 # No files, no patterns. Display all revs.
1136 1136 wanted = set(revs)
1137 1137 copies = []
1138 1138
1139 1139 if not slowpath:
1140 1140 # We only have to read through the filelog to find wanted revisions
1141 1141
1142 1142 minrev, maxrev = min(revs), max(revs)
1143 1143 def filerevgen(filelog, last):
1144 1144 """
1145 1145 Only files, no patterns. Check the history of each file.
1146 1146
1147 1147 Examines filelog entries within minrev, maxrev linkrev range
1148 1148 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1149 1149 tuples in backwards order
1150 1150 """
1151 1151 cl_count = len(repo)
1152 1152 revs = []
1153 1153 for j in xrange(0, last + 1):
1154 1154 linkrev = filelog.linkrev(j)
1155 1155 if linkrev < minrev:
1156 1156 continue
1157 1157 # only yield rev for which we have the changelog, it can
1158 1158 # happen while doing "hg log" during a pull or commit
1159 1159 if linkrev >= cl_count:
1160 1160 break
1161 1161
1162 1162 parentlinkrevs = []
1163 1163 for p in filelog.parentrevs(j):
1164 1164 if p != nullrev:
1165 1165 parentlinkrevs.append(filelog.linkrev(p))
1166 1166 n = filelog.node(j)
1167 1167 revs.append((linkrev, parentlinkrevs,
1168 1168 follow and filelog.renamed(n)))
1169 1169
1170 1170 return reversed(revs)
1171 1171 def iterfiles():
1172 1172 for filename in match.files():
1173 1173 yield filename, None
1174 1174 for filename_node in copies:
1175 1175 yield filename_node
1176 1176 for file_, node in iterfiles():
1177 1177 filelog = repo.file(file_)
1178 1178 if not len(filelog):
1179 1179 if node is None:
1180 1180 # A zero count may be a directory or deleted file, so
1181 1181 # try to find matching entries on the slow path.
1182 1182 if follow:
1183 1183 raise util.Abort(
1184 1184 _('cannot follow nonexistent file: "%s"') % file_)
1185 1185 slowpath = True
1186 1186 break
1187 1187 else:
1188 1188 continue
1189 1189
1190 1190 if node is None:
1191 1191 last = len(filelog) - 1
1192 1192 else:
1193 1193 last = filelog.rev(node)
1194 1194
1195 1195
1196 1196 # keep track of all ancestors of the file
1197 1197 ancestors = set([filelog.linkrev(last)])
1198 1198
1199 1199 # iterate from latest to oldest revision
1200 1200 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1201 1201 if not follow:
1202 1202 if rev > maxrev:
1203 1203 continue
1204 1204 else:
1205 1205 # Note that last might not be the first interesting
1206 1206 # rev to us:
1207 1207 # if the file has been changed after maxrev, we'll
1208 1208 # have linkrev(last) > maxrev, and we still need
1209 1209 # to explore the file graph
1210 1210 if rev not in ancestors:
1211 1211 continue
1212 1212 # XXX insert 1327 fix here
1213 1213 if flparentlinkrevs:
1214 1214 ancestors.update(flparentlinkrevs)
1215 1215
1216 1216 fncache.setdefault(rev, []).append(file_)
1217 1217 wanted.add(rev)
1218 1218 if copied:
1219 1219 copies.append(copied)
1220 1220 if slowpath:
1221 1221 # We have to read the changelog to match filenames against
1222 1222 # changed files
1223 1223
1224 1224 if follow:
1225 1225 raise util.Abort(_('can only follow copies/renames for explicit '
1226 1226 'filenames'))
1227 1227
1228 1228 # The slow path checks files modified in every changeset.
1229 1229 for i in sorted(revs):
1230 1230 ctx = change(i)
1231 1231 matches = filter(match, ctx.files())
1232 1232 if matches:
1233 1233 fncache[i] = matches
1234 1234 wanted.add(i)
1235 1235
1236 1236 class followfilter(object):
1237 1237 def __init__(self, onlyfirst=False):
1238 1238 self.startrev = nullrev
1239 1239 self.roots = set()
1240 1240 self.onlyfirst = onlyfirst
1241 1241
1242 1242 def match(self, rev):
1243 1243 def realparents(rev):
1244 1244 if self.onlyfirst:
1245 1245 return repo.changelog.parentrevs(rev)[0:1]
1246 1246 else:
1247 1247 return filter(lambda x: x != nullrev,
1248 1248 repo.changelog.parentrevs(rev))
1249 1249
1250 1250 if self.startrev == nullrev:
1251 1251 self.startrev = rev
1252 1252 return True
1253 1253
1254 1254 if rev > self.startrev:
1255 1255 # forward: all descendants
1256 1256 if not self.roots:
1257 1257 self.roots.add(self.startrev)
1258 1258 for parent in realparents(rev):
1259 1259 if parent in self.roots:
1260 1260 self.roots.add(rev)
1261 1261 return True
1262 1262 else:
1263 1263 # backwards: all parents
1264 1264 if not self.roots:
1265 1265 self.roots.update(realparents(self.startrev))
1266 1266 if rev in self.roots:
1267 1267 self.roots.remove(rev)
1268 1268 self.roots.update(realparents(rev))
1269 1269 return True
1270 1270
1271 1271 return False
1272 1272
1273 1273 # it might be worthwhile to do this in the iterator if the rev range
1274 1274 # is descending and the prune args are all within that range
1275 1275 for rev in opts.get('prune', ()):
1276 1276 rev = repo.changelog.rev(repo.lookup(rev))
1277 1277 ff = followfilter()
1278 1278 stop = min(revs[0], revs[-1])
1279 1279 for x in xrange(rev, stop - 1, -1):
1280 1280 if ff.match(x):
1281 1281 wanted.discard(x)
1282 1282
1283 1283 # Now that wanted is correctly initialized, we can iterate over the
1284 1284 # revision range, yielding only revisions in wanted.
1285 1285 def iterate():
1286 1286 if follow and not match.files():
1287 1287 ff = followfilter(onlyfirst=opts.get('follow_first'))
1288 1288 def want(rev):
1289 1289 return ff.match(rev) and rev in wanted
1290 1290 else:
1291 1291 def want(rev):
1292 1292 return rev in wanted
1293 1293
1294 1294 for i, window in increasing_windows(0, len(revs)):
1295 1295 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1296 1296 for rev in sorted(nrevs):
1297 1297 fns = fncache.get(rev)
1298 1298 ctx = change(rev)
1299 1299 if not fns:
1300 1300 def fns_generator():
1301 1301 for f in ctx.files():
1302 1302 if match(f):
1303 1303 yield f
1304 1304 fns = fns_generator()
1305 1305 prepare(ctx, fns)
1306 1306 for rev in nrevs:
1307 1307 yield change(rev)
1308 1308 return iterate()
1309 1309
1310 1310 def add(ui, repo, match, dryrun, listsubrepos, prefix):
1311 1311 join = lambda f: os.path.join(prefix, f)
1312 1312 bad = []
1313 1313 oldbad = match.bad
1314 1314 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1315 1315 names = []
1316 1316 wctx = repo[None]
1317 1317 wctx.status(clean=True)
1318 1318 existing = None
1319 1319 if scmutil.showportabilityalert(ui):
1320 1320 existing = dict([(fn.lower(), fn) for fn in
1321 1321 wctx.added() + wctx.clean() + wctx.modified()])
1322 1322 for f in repo.walk(match):
1323 1323 exact = match.exact(f)
1324 1324 if exact or f not in repo.dirstate:
1325 1325 if existing:
1326 1326 scmutil.checkcasecollision(ui, f, existing)
1327 1327 names.append(f)
1328 1328 if ui.verbose or not exact:
1329 1329 ui.status(_('adding %s\n') % match.rel(join(f)))
1330 1330
1331 1331 if listsubrepos:
1332 1332 for subpath in wctx.substate:
1333 1333 sub = wctx.sub(subpath)
1334 1334 try:
1335 1335 submatch = matchmod.narrowmatcher(subpath, match)
1336 1336 bad.extend(sub.add(ui, submatch, dryrun, prefix))
1337 1337 except error.LookupError:
1338 1338 ui.status(_("skipping missing subrepository: %s\n")
1339 1339 % join(subpath))
1340 1340
1341 1341 if not dryrun:
1342 1342 rejected = wctx.add(names, prefix)
1343 1343 bad.extend(f for f in rejected if f in match.files())
1344 1344 return bad
1345 1345
1346 1346 def commit(ui, repo, commitfunc, pats, opts):
1347 1347 '''commit the specified files or all outstanding changes'''
1348 1348 date = opts.get('date')
1349 1349 if date:
1350 1350 opts['date'] = util.parsedate(date)
1351 1351 message = logmessage(opts)
1352 1352
1353 1353 # extract addremove carefully -- this function can be called from a command
1354 1354 # that doesn't support addremove
1355 1355 if opts.get('addremove'):
1356 1356 addremove(repo, pats, opts)
1357 1357
1358 1358 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1359 1359
1360 1360 def commiteditor(repo, ctx, subs):
1361 1361 if ctx.description():
1362 1362 return ctx.description()
1363 1363 return commitforceeditor(repo, ctx, subs)
1364 1364
1365 1365 def commitforceeditor(repo, ctx, subs):
1366 1366 edittext = []
1367 1367 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1368 1368 if ctx.description():
1369 1369 edittext.append(ctx.description())
1370 1370 edittext.append("")
1371 1371 edittext.append("") # Empty line between message and comments.
1372 1372 edittext.append(_("HG: Enter commit message."
1373 1373 " Lines beginning with 'HG:' are removed."))
1374 1374 edittext.append(_("HG: Leave message empty to abort commit."))
1375 1375 edittext.append("HG: --")
1376 1376 edittext.append(_("HG: user: %s") % ctx.user())
1377 1377 if ctx.p2():
1378 1378 edittext.append(_("HG: branch merge"))
1379 1379 if ctx.branch():
1380 1380 edittext.append(_("HG: branch '%s'") % ctx.branch())
1381 1381 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1382 1382 edittext.extend([_("HG: added %s") % f for f in added])
1383 1383 edittext.extend([_("HG: changed %s") % f for f in modified])
1384 1384 edittext.extend([_("HG: removed %s") % f for f in removed])
1385 1385 if not added and not modified and not removed:
1386 1386 edittext.append(_("HG: no files changed"))
1387 1387 edittext.append("")
1388 1388 # run editor in the repository root
1389 1389 olddir = os.getcwd()
1390 1390 os.chdir(repo.root)
1391 1391 text = repo.ui.edit("\n".join(edittext), ctx.user())
1392 1392 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1393 1393 os.chdir(olddir)
1394 1394
1395 1395 if not text.strip():
1396 1396 raise util.Abort(_("empty commit message"))
1397 1397
1398 1398 return text
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,101 +1,121 b''
1 1 Mercurial supports a functional language for selecting a set of
2 2 revisions.
3 3
4 4 The language supports a number of predicates which are joined by infix
5 5 operators. Parenthesis can be used for grouping.
6 6
7 7 Identifiers such as branch names must be quoted with single or double
8 8 quotes if they contain characters outside of
9 9 ``[._a-zA-Z0-9\x80-\xff]`` or if they match one of the predefined
10 10 predicates.
11 11
12 12 Special characters can be used in quoted identifiers by escaping them,
13 13 e.g., ``\n`` is interpreted as a newline. To prevent them from being
14 14 interpreted, strings can be prefixed with ``r``, e.g. ``r'...'``.
15 15
16 16 There is a single prefix operator:
17 17
18 18 ``not x``
19 19 Changesets not in x. Short form is ``! x``.
20 20
21 21 These are the supported infix operators:
22 22
23 23 ``x::y``
24 24 A DAG range, meaning all changesets that are descendants of x and
25 25 ancestors of y, including x and y themselves. If the first endpoint
26 26 is left out, this is equivalent to ``ancestors(y)``, if the second
27 27 is left out it is equivalent to ``descendants(x)``.
28 28
29 29 An alternative syntax is ``x..y``.
30 30
31 31 ``x:y``
32 32 All changesets with revision numbers between x and y, both
33 33 inclusive. Either endpoint can be left out, they default to 0 and
34 34 tip.
35 35
36 36 ``x and y``
37 37 The intersection of changesets in x and y. Short form is ``x & y``.
38 38
39 39 ``x or y``
40 40 The union of changesets in x and y. There are two alternative short
41 41 forms: ``x | y`` and ``x + y``.
42 42
43 43 ``x - y``
44 44 Changesets in x but not in y.
45 45
46 46 ``x^n``
47 47 The nth parent of x, n == 0, 1, or 2.
48 48 For n == 0, x; for n == 1, the first parent of each changeset in x;
49 49 for n == 2, the second parent of changeset in x.
50 50
51 51 ``x~n``
52 52 The nth first ancestor of x; ``x~0`` is x; ``x~3`` is ``x^^^``.
53 53
54 54 There is a single postfix operator:
55 55
56 56 ``x^``
57 57 Equivalent to ``x^1``, the first parent of each changeset in x.
58 58
59 59
60 60 The following predicates are supported:
61 61
62 62 .. predicatesmarker
63 63
64 New predicates (known as "aliases") can be defined, using any combination of
65 existing predicates or other aliases. An alias definition looks like::
66
67 <alias> = <definition>
68
69 in the ``revsetalias`` section of ``.hgrc``. Arguments of the form `$1`, `$2`,
70 etc. are substituted from the alias into the definition.
71
72 For example,
73
74 ::
75
76 [revsetalias]
77 h = heads()
78 d($1) = sort($1, date)
79 rs($1, $2) = reverse(sort($1, $2))
80
81 defines three aliases, ``h``, ``d``, and ``rs``. ``rs(0:tip, author)`` is
82 exactly equivalent to ``reverse(sort(0:tip, author))``.
83
64 84 Command line equivalents for :hg:`log`::
65 85
66 86 -f -> ::.
67 87 -d x -> date(x)
68 88 -k x -> keyword(x)
69 89 -m -> merge()
70 90 -u x -> user(x)
71 91 -b x -> branch(x)
72 92 -P x -> !::x
73 93 -l x -> limit(expr, x)
74 94
75 95 Some sample queries:
76 96
77 97 - Changesets on the default branch::
78 98
79 99 hg log -r "branch(default)"
80 100
81 101 - Changesets on the default branch since tag 1.5 (excluding merges)::
82 102
83 103 hg log -r "branch(default) and 1.5:: and not merge()"
84 104
85 105 - Open branch heads::
86 106
87 107 hg log -r "head() and not closed()"
88 108
89 109 - Changesets between tags 1.3 and 1.5 mentioning "bug" that affect
90 110 ``hgext/*``::
91 111
92 112 hg log -r "1.3::1.5 and keyword(bug) and file('hgext/*')"
93 113
94 114 - Changesets committed in May 2008, sorted by user::
95 115
96 116 hg log -r "sort(date('May 2008'), user)"
97 117
98 118 - Changesets mentioning "bug" or "issue" that are not in a tagged
99 119 release::
100 120
101 121 hg log -r "(keyword(bug) or keyword(issue)) and not ancestors(tagged())"
@@ -1,909 +1,984 b''
1 1 # revset.py - revision set queries 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 import re
9 9 import parser, util, error, discovery, help, hbisect
10 10 import bookmarks as bookmarksmod
11 11 import match as matchmod
12 12 from i18n import _
13 13
14 14 elements = {
15 15 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
16 16 "~": (18, None, ("ancestor", 18)),
17 17 "^": (18, None, ("parent", 18), ("parentpost", 18)),
18 18 "-": (5, ("negate", 19), ("minus", 5)),
19 19 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
20 20 ("dagrangepost", 17)),
21 21 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
22 22 ("dagrangepost", 17)),
23 23 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
24 24 "not": (10, ("not", 10)),
25 25 "!": (10, ("not", 10)),
26 26 "and": (5, None, ("and", 5)),
27 27 "&": (5, None, ("and", 5)),
28 28 "or": (4, None, ("or", 4)),
29 29 "|": (4, None, ("or", 4)),
30 30 "+": (4, None, ("or", 4)),
31 31 ",": (2, None, ("list", 2)),
32 32 ")": (0, None, None),
33 33 "symbol": (0, ("symbol",), None),
34 34 "string": (0, ("string",), None),
35 35 "end": (0, None, None),
36 36 }
37 37
38 38 keywords = set(['and', 'or', 'not'])
39 39
40 40 def tokenize(program):
41 41 pos, l = 0, len(program)
42 42 while pos < l:
43 43 c = program[pos]
44 44 if c.isspace(): # skip inter-token whitespace
45 45 pass
46 46 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
47 47 yield ('::', None, pos)
48 48 pos += 1 # skip ahead
49 49 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
50 50 yield ('..', None, pos)
51 51 pos += 1 # skip ahead
52 52 elif c in "():,-|&+!~^": # handle simple operators
53 53 yield (c, None, pos)
54 54 elif (c in '"\'' or c == 'r' and
55 55 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
56 56 if c == 'r':
57 57 pos += 1
58 58 c = program[pos]
59 59 decode = lambda x: x
60 60 else:
61 61 decode = lambda x: x.decode('string-escape')
62 62 pos += 1
63 63 s = pos
64 64 while pos < l: # find closing quote
65 65 d = program[pos]
66 66 if d == '\\': # skip over escaped characters
67 67 pos += 2
68 68 continue
69 69 if d == c:
70 70 yield ('string', decode(program[s:pos]), s)
71 71 break
72 72 pos += 1
73 73 else:
74 74 raise error.ParseError(_("unterminated string"), s)
75 75 elif c.isalnum() or c in '._' or ord(c) > 127: # gather up a symbol/keyword
76 76 s = pos
77 77 pos += 1
78 78 while pos < l: # find end of symbol
79 79 d = program[pos]
80 80 if not (d.isalnum() or d in "._" or ord(d) > 127):
81 81 break
82 82 if d == '.' and program[pos - 1] == '.': # special case for ..
83 83 pos -= 1
84 84 break
85 85 pos += 1
86 86 sym = program[s:pos]
87 87 if sym in keywords: # operator keywords
88 88 yield (sym, None, s)
89 89 else:
90 90 yield ('symbol', sym, s)
91 91 pos -= 1
92 92 else:
93 93 raise error.ParseError(_("syntax error"), pos)
94 94 pos += 1
95 95 yield ('end', None, pos)
96 96
97 97 # helpers
98 98
99 99 def getstring(x, err):
100 100 if x and (x[0] == 'string' or x[0] == 'symbol'):
101 101 return x[1]
102 102 raise error.ParseError(err)
103 103
104 104 def getlist(x):
105 105 if not x:
106 106 return []
107 107 if x[0] == 'list':
108 108 return getlist(x[1]) + [x[2]]
109 109 return [x]
110 110
111 111 def getargs(x, min, max, err):
112 112 l = getlist(x)
113 113 if len(l) < min or len(l) > max:
114 114 raise error.ParseError(err)
115 115 return l
116 116
117 117 def getset(repo, subset, x):
118 118 if not x:
119 119 raise error.ParseError(_("missing argument"))
120 120 return methods[x[0]](repo, subset, *x[1:])
121 121
122 122 # operator methods
123 123
124 124 def stringset(repo, subset, x):
125 125 x = repo[x].rev()
126 126 if x == -1 and len(subset) == len(repo):
127 127 return [-1]
128 128 if len(subset) == len(repo) or x in subset:
129 129 return [x]
130 130 return []
131 131
132 132 def symbolset(repo, subset, x):
133 133 if x in symbols:
134 134 raise error.ParseError(_("can't use %s here") % x)
135 135 return stringset(repo, subset, x)
136 136
137 137 def rangeset(repo, subset, x, y):
138 138 m = getset(repo, subset, x)
139 139 if not m:
140 140 m = getset(repo, range(len(repo)), x)
141 141
142 142 n = getset(repo, subset, y)
143 143 if not n:
144 144 n = getset(repo, range(len(repo)), y)
145 145
146 146 if not m or not n:
147 147 return []
148 148 m, n = m[0], n[-1]
149 149
150 150 if m < n:
151 151 r = range(m, n + 1)
152 152 else:
153 153 r = range(m, n - 1, -1)
154 154 s = set(subset)
155 155 return [x for x in r if x in s]
156 156
157 157 def andset(repo, subset, x, y):
158 158 return getset(repo, getset(repo, subset, x), y)
159 159
160 160 def orset(repo, subset, x, y):
161 161 xl = getset(repo, subset, x)
162 162 s = set(xl)
163 163 yl = getset(repo, [r for r in subset if r not in s], y)
164 164 return xl + yl
165 165
166 166 def notset(repo, subset, x):
167 167 s = set(getset(repo, subset, x))
168 168 return [r for r in subset if r not in s]
169 169
170 170 def listset(repo, subset, a, b):
171 171 raise error.ParseError(_("can't use a list in this context"))
172 172
173 173 def func(repo, subset, a, b):
174 174 if a[0] == 'symbol' and a[1] in symbols:
175 175 return symbols[a[1]](repo, subset, b)
176 176 raise error.ParseError(_("not a function: %s") % a[1])
177 177
178 178 # functions
179 179
180 180 def adds(repo, subset, x):
181 181 """``adds(pattern)``
182 182 Changesets that add a file matching pattern.
183 183 """
184 184 # i18n: "adds" is a keyword
185 185 pat = getstring(x, _("adds requires a pattern"))
186 186 return checkstatus(repo, subset, pat, 1)
187 187
188 188 def ancestor(repo, subset, x):
189 189 """``ancestor(single, single)``
190 190 Greatest common ancestor of the two changesets.
191 191 """
192 192 # i18n: "ancestor" is a keyword
193 193 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
194 194 r = range(len(repo))
195 195 a = getset(repo, r, l[0])
196 196 b = getset(repo, r, l[1])
197 197 if len(a) != 1 or len(b) != 1:
198 198 # i18n: "ancestor" is a keyword
199 199 raise error.ParseError(_("ancestor arguments must be single revisions"))
200 200 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
201 201
202 202 return [r for r in an if r in subset]
203 203
204 204 def ancestors(repo, subset, x):
205 205 """``ancestors(set)``
206 206 Changesets that are ancestors of a changeset in set.
207 207 """
208 208 args = getset(repo, range(len(repo)), x)
209 209 if not args:
210 210 return []
211 211 s = set(repo.changelog.ancestors(*args)) | set(args)
212 212 return [r for r in subset if r in s]
213 213
214 214 def ancestorspec(repo, subset, x, n):
215 215 """``set~n``
216 216 Changesets that are the Nth ancestor (first parents only) of a changeset in set.
217 217 """
218 218 try:
219 219 n = int(n[1])
220 220 except ValueError:
221 221 raise error.ParseError(_("~ expects a number"))
222 222 ps = set()
223 223 cl = repo.changelog
224 224 for r in getset(repo, subset, x):
225 225 for i in range(n):
226 226 r = cl.parentrevs(r)[0]
227 227 ps.add(r)
228 228 return [r for r in subset if r in ps]
229 229
230 230 def author(repo, subset, x):
231 231 """``author(string)``
232 232 Alias for ``user(string)``.
233 233 """
234 234 # i18n: "author" is a keyword
235 235 n = getstring(x, _("author requires a string")).lower()
236 236 return [r for r in subset if n in repo[r].user().lower()]
237 237
238 238 def bisected(repo, subset, x):
239 239 """``bisected(string)``
240 240 Changesets marked in the specified bisect state (good, bad, skip).
241 241 """
242 242 state = getstring(x, _("bisect requires a string")).lower()
243 243 if state not in ('good', 'bad', 'skip', 'unknown'):
244 244 raise error.ParseError(_('invalid bisect state'))
245 245 marked = set(repo.changelog.rev(n) for n in hbisect.load_state(repo)[state])
246 246 return [r for r in subset if r in marked]
247 247
248 248 def bookmark(repo, subset, x):
249 249 """``bookmark([name])``
250 250 The named bookmark or all bookmarks.
251 251 """
252 252 # i18n: "bookmark" is a keyword
253 253 args = getargs(x, 0, 1, _('bookmark takes one or no arguments'))
254 254 if args:
255 255 bm = getstring(args[0],
256 256 # i18n: "bookmark" is a keyword
257 257 _('the argument to bookmark must be a string'))
258 258 bmrev = bookmarksmod.listbookmarks(repo).get(bm, None)
259 259 if not bmrev:
260 260 raise util.Abort(_("bookmark '%s' does not exist") % bm)
261 261 bmrev = repo[bmrev].rev()
262 262 return [r for r in subset if r == bmrev]
263 263 bms = set([repo[r].rev()
264 264 for r in bookmarksmod.listbookmarks(repo).values()])
265 265 return [r for r in subset if r in bms]
266 266
267 267 def branch(repo, subset, x):
268 268 """``branch(string or set)``
269 269 All changesets belonging to the given branch or the branches of the given
270 270 changesets.
271 271 """
272 272 try:
273 273 b = getstring(x, '')
274 274 if b in repo.branchmap():
275 275 return [r for r in subset if repo[r].branch() == b]
276 276 except error.ParseError:
277 277 # not a string, but another revspec, e.g. tip()
278 278 pass
279 279
280 280 s = getset(repo, range(len(repo)), x)
281 281 b = set()
282 282 for r in s:
283 283 b.add(repo[r].branch())
284 284 s = set(s)
285 285 return [r for r in subset if r in s or repo[r].branch() in b]
286 286
287 287 def checkstatus(repo, subset, pat, field):
288 288 m = matchmod.match(repo.root, repo.getcwd(), [pat])
289 289 s = []
290 290 fast = (m.files() == [pat])
291 291 for r in subset:
292 292 c = repo[r]
293 293 if fast:
294 294 if pat not in c.files():
295 295 continue
296 296 else:
297 297 for f in c.files():
298 298 if m(f):
299 299 break
300 300 else:
301 301 continue
302 302 files = repo.status(c.p1().node(), c.node())[field]
303 303 if fast:
304 304 if pat in files:
305 305 s.append(r)
306 306 else:
307 307 for f in files:
308 308 if m(f):
309 309 s.append(r)
310 310 break
311 311 return s
312 312
313 313 def children(repo, subset, x):
314 314 """``children(set)``
315 315 Child changesets of changesets in set.
316 316 """
317 317 cs = set()
318 318 cl = repo.changelog
319 319 s = set(getset(repo, range(len(repo)), x))
320 320 for r in xrange(0, len(repo)):
321 321 for p in cl.parentrevs(r):
322 322 if p in s:
323 323 cs.add(r)
324 324 return [r for r in subset if r in cs]
325 325
326 326 def closed(repo, subset, x):
327 327 """``closed()``
328 328 Changeset is closed.
329 329 """
330 330 # i18n: "closed" is a keyword
331 331 getargs(x, 0, 0, _("closed takes no arguments"))
332 332 return [r for r in subset if repo[r].extra().get('close')]
333 333
334 334 def contains(repo, subset, x):
335 335 """``contains(pattern)``
336 336 Revision contains pattern.
337 337 """
338 338 # i18n: "contains" is a keyword
339 339 pat = getstring(x, _("contains requires a pattern"))
340 340 m = matchmod.match(repo.root, repo.getcwd(), [pat])
341 341 s = []
342 342 if m.files() == [pat]:
343 343 for r in subset:
344 344 if pat in repo[r]:
345 345 s.append(r)
346 346 else:
347 347 for r in subset:
348 348 for f in repo[r].manifest():
349 349 if m(f):
350 350 s.append(r)
351 351 break
352 352 return s
353 353
354 354 def date(repo, subset, x):
355 355 """``date(interval)``
356 356 Changesets within the interval, see :hg:`help dates`.
357 357 """
358 358 # i18n: "date" is a keyword
359 359 ds = getstring(x, _("date requires a string"))
360 360 dm = util.matchdate(ds)
361 361 return [r for r in subset if dm(repo[r].date()[0])]
362 362
363 363 def descendants(repo, subset, x):
364 364 """``descendants(set)``
365 365 Changesets which are descendants of changesets in set.
366 366 """
367 367 args = getset(repo, range(len(repo)), x)
368 368 if not args:
369 369 return []
370 370 s = set(repo.changelog.descendants(*args)) | set(args)
371 371 return [r for r in subset if r in s]
372 372
373 373 def follow(repo, subset, x):
374 374 """``follow()``
375 375 An alias for ``::.`` (ancestors of the working copy's first parent).
376 376 """
377 377 # i18n: "follow" is a keyword
378 378 getargs(x, 0, 0, _("follow takes no arguments"))
379 379 p = repo['.'].rev()
380 380 s = set(repo.changelog.ancestors(p)) | set([p])
381 381 return [r for r in subset if r in s]
382 382
383 383 def getall(repo, subset, x):
384 384 """``all()``
385 385 All changesets, the same as ``0:tip``.
386 386 """
387 387 # i18n: "all" is a keyword
388 388 getargs(x, 0, 0, _("all takes no arguments"))
389 389 return subset
390 390
391 391 def grep(repo, subset, x):
392 392 """``grep(regex)``
393 393 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
394 394 to ensure special escape characters are handled correctly.
395 395 """
396 396 try:
397 397 # i18n: "grep" is a keyword
398 398 gr = re.compile(getstring(x, _("grep requires a string")))
399 399 except re.error, e:
400 400 raise error.ParseError(_('invalid match pattern: %s') % e)
401 401 l = []
402 402 for r in subset:
403 403 c = repo[r]
404 404 for e in c.files() + [c.user(), c.description()]:
405 405 if gr.search(e):
406 406 l.append(r)
407 407 break
408 408 return l
409 409
410 410 def hasfile(repo, subset, x):
411 411 """``file(pattern)``
412 412 Changesets affecting files matched by pattern.
413 413 """
414 414 # i18n: "file" is a keyword
415 415 pat = getstring(x, _("file requires a pattern"))
416 416 m = matchmod.match(repo.root, repo.getcwd(), [pat])
417 417 s = []
418 418 for r in subset:
419 419 for f in repo[r].files():
420 420 if m(f):
421 421 s.append(r)
422 422 break
423 423 return s
424 424
425 425 def head(repo, subset, x):
426 426 """``head()``
427 427 Changeset is a named branch head.
428 428 """
429 429 # i18n: "head" is a keyword
430 430 getargs(x, 0, 0, _("head takes no arguments"))
431 431 hs = set()
432 432 for b, ls in repo.branchmap().iteritems():
433 433 hs.update(repo[h].rev() for h in ls)
434 434 return [r for r in subset if r in hs]
435 435
436 436 def heads(repo, subset, x):
437 437 """``heads(set)``
438 438 Members of set with no children in set.
439 439 """
440 440 s = getset(repo, subset, x)
441 441 ps = set(parents(repo, subset, x))
442 442 return [r for r in s if r not in ps]
443 443
444 444 def keyword(repo, subset, x):
445 445 """``keyword(string)``
446 446 Search commit message, user name, and names of changed files for
447 447 string.
448 448 """
449 449 # i18n: "keyword" is a keyword
450 450 kw = getstring(x, _("keyword requires a string")).lower()
451 451 l = []
452 452 for r in subset:
453 453 c = repo[r]
454 454 t = " ".join(c.files() + [c.user(), c.description()])
455 455 if kw in t.lower():
456 456 l.append(r)
457 457 return l
458 458
459 459 def limit(repo, subset, x):
460 460 """``limit(set, n)``
461 461 First n members of set.
462 462 """
463 463 # i18n: "limit" is a keyword
464 464 l = getargs(x, 2, 2, _("limit requires two arguments"))
465 465 try:
466 466 # i18n: "limit" is a keyword
467 467 lim = int(getstring(l[1], _("limit requires a number")))
468 468 except ValueError:
469 469 # i18n: "limit" is a keyword
470 470 raise error.ParseError(_("limit expects a number"))
471 471 return getset(repo, subset, l[0])[:lim]
472 472
473 473 def last(repo, subset, x):
474 474 """``last(set, n)``
475 475 Last n members of set.
476 476 """
477 477 # i18n: "last" is a keyword
478 478 l = getargs(x, 2, 2, _("last requires two arguments"))
479 479 try:
480 480 # i18n: "last" is a keyword
481 481 lim = int(getstring(l[1], _("last requires a number")))
482 482 except ValueError:
483 483 # i18n: "last" is a keyword
484 484 raise error.ParseError(_("last expects a number"))
485 485 return getset(repo, subset, l[0])[-lim:]
486 486
487 487 def maxrev(repo, subset, x):
488 488 """``max(set)``
489 489 Changeset with highest revision number in set.
490 490 """
491 491 s = getset(repo, subset, x)
492 492 if s:
493 493 m = max(s)
494 494 if m in subset:
495 495 return [m]
496 496 return []
497 497
498 498 def merge(repo, subset, x):
499 499 """``merge()``
500 500 Changeset is a merge changeset.
501 501 """
502 502 # i18n: "merge" is a keyword
503 503 getargs(x, 0, 0, _("merge takes no arguments"))
504 504 cl = repo.changelog
505 505 return [r for r in subset if cl.parentrevs(r)[1] != -1]
506 506
507 507 def minrev(repo, subset, x):
508 508 """``min(set)``
509 509 Changeset with lowest revision number in set.
510 510 """
511 511 s = getset(repo, subset, x)
512 512 if s:
513 513 m = min(s)
514 514 if m in subset:
515 515 return [m]
516 516 return []
517 517
518 518 def modifies(repo, subset, x):
519 519 """``modifies(pattern)``
520 520 Changesets modifying files matched by pattern.
521 521 """
522 522 # i18n: "modifies" is a keyword
523 523 pat = getstring(x, _("modifies requires a pattern"))
524 524 return checkstatus(repo, subset, pat, 0)
525 525
526 526 def node(repo, subset, x):
527 527 """``id(string)``
528 528 Revision non-ambiguously specified by the given hex string prefix.
529 529 """
530 530 # i18n: "id" is a keyword
531 531 l = getargs(x, 1, 1, _("id requires one argument"))
532 532 # i18n: "id" is a keyword
533 533 n = getstring(l[0], _("id requires a string"))
534 534 if len(n) == 40:
535 535 rn = repo[n].rev()
536 536 else:
537 537 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
538 538 return [r for r in subset if r == rn]
539 539
540 540 def outgoing(repo, subset, x):
541 541 """``outgoing([path])``
542 542 Changesets not found in the specified destination repository, or the
543 543 default push location.
544 544 """
545 545 import hg # avoid start-up nasties
546 546 # i18n: "outgoing" is a keyword
547 547 l = getargs(x, 0, 1, _("outgoing requires a repository path"))
548 548 # i18n: "outgoing" is a keyword
549 549 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
550 550 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
551 551 dest, branches = hg.parseurl(dest)
552 552 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
553 553 if revs:
554 554 revs = [repo.lookup(rev) for rev in revs]
555 555 other = hg.repository(hg.remoteui(repo, {}), dest)
556 556 repo.ui.pushbuffer()
557 557 common, _anyinc, _heads = discovery.findcommonincoming(repo, other)
558 558 repo.ui.popbuffer()
559 559 cl = repo.changelog
560 560 o = set([cl.rev(r) for r in repo.changelog.findmissing(common, revs)])
561 561 return [r for r in subset if r in o]
562 562
563 563 def p1(repo, subset, x):
564 564 """``p1([set])``
565 565 First parent of changesets in set, or the working directory.
566 566 """
567 567 if x is None:
568 568 p = repo[x].p1().rev()
569 569 return [r for r in subset if r == p]
570 570
571 571 ps = set()
572 572 cl = repo.changelog
573 573 for r in getset(repo, range(len(repo)), x):
574 574 ps.add(cl.parentrevs(r)[0])
575 575 return [r for r in subset if r in ps]
576 576
577 577 def p2(repo, subset, x):
578 578 """``p2([set])``
579 579 Second parent of changesets in set, or the working directory.
580 580 """
581 581 if x is None:
582 582 ps = repo[x].parents()
583 583 try:
584 584 p = ps[1].rev()
585 585 return [r for r in subset if r == p]
586 586 except IndexError:
587 587 return []
588 588
589 589 ps = set()
590 590 cl = repo.changelog
591 591 for r in getset(repo, range(len(repo)), x):
592 592 ps.add(cl.parentrevs(r)[1])
593 593 return [r for r in subset if r in ps]
594 594
595 595 def parents(repo, subset, x):
596 596 """``parents([set])``
597 597 The set of all parents for all changesets in set, or the working directory.
598 598 """
599 599 if x is None:
600 600 ps = tuple(p.rev() for p in repo[x].parents())
601 601 return [r for r in subset if r in ps]
602 602
603 603 ps = set()
604 604 cl = repo.changelog
605 605 for r in getset(repo, range(len(repo)), x):
606 606 ps.update(cl.parentrevs(r))
607 607 return [r for r in subset if r in ps]
608 608
609 609 def parentspec(repo, subset, x, n):
610 610 """``set^0``
611 611 The set.
612 612 ``set^1`` (or ``set^``), ``set^2``
613 613 First or second parent, respectively, of all changesets in set.
614 614 """
615 615 try:
616 616 n = int(n[1])
617 617 if n not in (0, 1, 2):
618 618 raise ValueError
619 619 except ValueError:
620 620 raise error.ParseError(_("^ expects a number 0, 1, or 2"))
621 621 ps = set()
622 622 cl = repo.changelog
623 623 for r in getset(repo, subset, x):
624 624 if n == 0:
625 625 ps.add(r)
626 626 elif n == 1:
627 627 ps.add(cl.parentrevs(r)[0])
628 628 elif n == 2:
629 629 parents = cl.parentrevs(r)
630 630 if len(parents) > 1:
631 631 ps.add(parents[1])
632 632 return [r for r in subset if r in ps]
633 633
634 634 def present(repo, subset, x):
635 635 """``present(set)``
636 636 An empty set, if any revision in set isn't found; otherwise,
637 637 all revisions in set.
638 638 """
639 639 try:
640 640 return getset(repo, subset, x)
641 641 except error.RepoLookupError:
642 642 return []
643 643
644 644 def removes(repo, subset, x):
645 645 """``removes(pattern)``
646 646 Changesets which remove files matching pattern.
647 647 """
648 648 # i18n: "removes" is a keyword
649 649 pat = getstring(x, _("removes requires a pattern"))
650 650 return checkstatus(repo, subset, pat, 2)
651 651
652 652 def rev(repo, subset, x):
653 653 """``rev(number)``
654 654 Revision with the given numeric identifier.
655 655 """
656 656 # i18n: "rev" is a keyword
657 657 l = getargs(x, 1, 1, _("rev requires one argument"))
658 658 try:
659 659 # i18n: "rev" is a keyword
660 660 l = int(getstring(l[0], _("rev requires a number")))
661 661 except ValueError:
662 662 # i18n: "rev" is a keyword
663 663 raise error.ParseError(_("rev expects a number"))
664 664 return [r for r in subset if r == l]
665 665
666 666 def reverse(repo, subset, x):
667 667 """``reverse(set)``
668 668 Reverse order of set.
669 669 """
670 670 l = getset(repo, subset, x)
671 671 l.reverse()
672 672 return l
673 673
674 674 def roots(repo, subset, x):
675 675 """``roots(set)``
676 676 Changesets with no parent changeset in set.
677 677 """
678 678 s = getset(repo, subset, x)
679 679 cs = set(children(repo, subset, x))
680 680 return [r for r in s if r not in cs]
681 681
682 682 def sort(repo, subset, x):
683 683 """``sort(set[, [-]key...])``
684 684 Sort set by keys. The default sort order is ascending, specify a key
685 685 as ``-key`` to sort in descending order.
686 686
687 687 The keys can be:
688 688
689 689 - ``rev`` for the revision number,
690 690 - ``branch`` for the branch name,
691 691 - ``desc`` for the commit message (description),
692 692 - ``user`` for user name (``author`` can be used as an alias),
693 693 - ``date`` for the commit date
694 694 """
695 695 # i18n: "sort" is a keyword
696 696 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
697 697 keys = "rev"
698 698 if len(l) == 2:
699 699 keys = getstring(l[1], _("sort spec must be a string"))
700 700
701 701 s = l[0]
702 702 keys = keys.split()
703 703 l = []
704 704 def invert(s):
705 705 return "".join(chr(255 - ord(c)) for c in s)
706 706 for r in getset(repo, subset, s):
707 707 c = repo[r]
708 708 e = []
709 709 for k in keys:
710 710 if k == 'rev':
711 711 e.append(r)
712 712 elif k == '-rev':
713 713 e.append(-r)
714 714 elif k == 'branch':
715 715 e.append(c.branch())
716 716 elif k == '-branch':
717 717 e.append(invert(c.branch()))
718 718 elif k == 'desc':
719 719 e.append(c.description())
720 720 elif k == '-desc':
721 721 e.append(invert(c.description()))
722 722 elif k in 'user author':
723 723 e.append(c.user())
724 724 elif k in '-user -author':
725 725 e.append(invert(c.user()))
726 726 elif k == 'date':
727 727 e.append(c.date()[0])
728 728 elif k == '-date':
729 729 e.append(-c.date()[0])
730 730 else:
731 731 raise error.ParseError(_("unknown sort key %r") % k)
732 732 e.append(r)
733 733 l.append(e)
734 734 l.sort()
735 735 return [e[-1] for e in l]
736 736
737 737 def tag(repo, subset, x):
738 738 """``tag(name)``
739 739 The specified tag by name, or all tagged revisions if no name is given.
740 740 """
741 741 # i18n: "tag" is a keyword
742 742 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
743 743 cl = repo.changelog
744 744 if args:
745 745 tn = getstring(args[0],
746 746 # i18n: "tag" is a keyword
747 747 _('the argument to tag must be a string'))
748 748 if not repo.tags().get(tn, None):
749 749 raise util.Abort(_("tag '%s' does not exist") % tn)
750 750 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
751 751 else:
752 752 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
753 753 return [r for r in subset if r in s]
754 754
755 755 def tagged(repo, subset, x):
756 756 return tag(repo, subset, x)
757 757
758 758 def user(repo, subset, x):
759 759 """``user(string)``
760 760 User name is string.
761 761 """
762 762 return author(repo, subset, x)
763 763
764 764 symbols = {
765 765 "adds": adds,
766 766 "all": getall,
767 767 "ancestor": ancestor,
768 768 "ancestors": ancestors,
769 769 "author": author,
770 770 "bisected": bisected,
771 771 "bookmark": bookmark,
772 772 "branch": branch,
773 773 "children": children,
774 774 "closed": closed,
775 775 "contains": contains,
776 776 "date": date,
777 777 "descendants": descendants,
778 778 "file": hasfile,
779 779 "follow": follow,
780 780 "grep": grep,
781 781 "head": head,
782 782 "heads": heads,
783 783 "keyword": keyword,
784 784 "last": last,
785 785 "limit": limit,
786 786 "max": maxrev,
787 787 "min": minrev,
788 788 "merge": merge,
789 789 "modifies": modifies,
790 790 "id": node,
791 791 "outgoing": outgoing,
792 792 "p1": p1,
793 793 "p2": p2,
794 794 "parents": parents,
795 795 "present": present,
796 796 "removes": removes,
797 797 "reverse": reverse,
798 798 "rev": rev,
799 799 "roots": roots,
800 800 "sort": sort,
801 801 "tag": tag,
802 802 "tagged": tagged,
803 803 "user": user,
804 804 }
805 805
806 806 methods = {
807 807 "range": rangeset,
808 808 "string": stringset,
809 809 "symbol": symbolset,
810 810 "and": andset,
811 811 "or": orset,
812 812 "not": notset,
813 813 "list": listset,
814 814 "func": func,
815 815 "ancestor": ancestorspec,
816 816 "parent": parentspec,
817 817 "parentpost": p1,
818 818 }
819 819
820 820 def optimize(x, small):
821 821 if x is None:
822 822 return 0, x
823 823
824 824 smallbonus = 1
825 825 if small:
826 826 smallbonus = .5
827 827
828 828 op = x[0]
829 829 if op == 'minus':
830 830 return optimize(('and', x[1], ('not', x[2])), small)
831 831 elif op == 'dagrange':
832 832 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
833 833 ('func', ('symbol', 'ancestors'), x[2])), small)
834 834 elif op == 'dagrangepre':
835 835 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
836 836 elif op == 'dagrangepost':
837 837 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
838 838 elif op == 'rangepre':
839 839 return optimize(('range', ('string', '0'), x[1]), small)
840 840 elif op == 'rangepost':
841 841 return optimize(('range', x[1], ('string', 'tip')), small)
842 842 elif op == 'negate':
843 843 return optimize(('string',
844 844 '-' + getstring(x[1], _("can't negate that"))), small)
845 845 elif op in 'string symbol negate':
846 846 return smallbonus, x # single revisions are small
847 847 elif op == 'and' or op == 'dagrange':
848 848 wa, ta = optimize(x[1], True)
849 849 wb, tb = optimize(x[2], True)
850 850 w = min(wa, wb)
851 851 if wa > wb:
852 852 return w, (op, tb, ta)
853 853 return w, (op, ta, tb)
854 854 elif op == 'or':
855 855 wa, ta = optimize(x[1], False)
856 856 wb, tb = optimize(x[2], False)
857 857 if wb < wa:
858 858 wb, wa = wa, wb
859 859 return max(wa, wb), (op, ta, tb)
860 860 elif op == 'not':
861 861 o = optimize(x[1], not small)
862 862 return o[0], (op, o[1])
863 863 elif op == 'parentpost':
864 864 o = optimize(x[1], small)
865 865 return o[0], (op, o[1])
866 866 elif op == 'group':
867 867 return optimize(x[1], small)
868 868 elif op in 'range list parent ancestorspec':
869 869 wa, ta = optimize(x[1], small)
870 870 wb, tb = optimize(x[2], small)
871 871 return wa + wb, (op, ta, tb)
872 872 elif op == 'func':
873 873 f = getstring(x[1], _("not a symbol"))
874 874 wa, ta = optimize(x[2], small)
875 875 if f in "grep date user author keyword branch file outgoing closed":
876 876 w = 10 # slow
877 877 elif f in "modifies adds removes":
878 878 w = 30 # slower
879 879 elif f == "contains":
880 880 w = 100 # very slow
881 881 elif f == "ancestor":
882 882 w = 1 * smallbonus
883 883 elif f in "reverse limit":
884 884 w = 0
885 885 elif f in "sort":
886 886 w = 10 # assume most sorts look at changelog
887 887 else:
888 888 w = 1
889 889 return w + wa, (op, x[1], ta)
890 890 return 1, x
891 891
892 class revsetalias(object):
893 funcre = re.compile('^([^(]+)\(([^)]+)\)$')
894 args = ()
895
896 def __init__(self, token, value):
897 '''Aliases like:
898
899 h = heads(default)
900 b($1) = ancestors($1) - ancestors(default)
901 '''
902 if isinstance(token, tuple):
903 self.type, self.name = token
904 else:
905 m = self.funcre.search(token)
906 if m:
907 self.type = 'func'
908 self.name = m.group(1)
909 self.args = [x.strip() for x in m.group(2).split(',')]
910 else:
911 self.type = 'symbol'
912 self.name = token
913
914 if isinstance(value, str):
915 for arg in self.args:
916 value = value.replace(arg, repr(arg))
917 self.replacement, pos = parse(value)
918 if pos != len(value):
919 raise error.ParseError('invalid token', pos)
920 else:
921 self.replacement = value
922
923 def match(self, tree):
924 if not tree:
925 return False
926 if tree == (self.type, self.name):
927 return True
928 if tree[0] != self.type:
929 return False
930 if len(tree) > 1 and tree[1] != ('symbol', self.name):
931 return False
932 # 'func' + funcname + args
933 if ((self.args and len(tree) != 3) or
934 (len(self.args) == 1 and tree[2][0] == 'list') or
935 (len(self.args) > 1 and (tree[2][0] != 'list' or
936 len(tree[2]) - 1 != len(self.args)))):
937 raise error.ParseError('invalid amount of arguments', len(tree) - 2)
938 return True
939
940 def replace(self, tree):
941 if tree == (self.type, self.name):
942 return self.replacement
943 result = self.replacement
944 def getsubtree(i):
945 if tree[2][0] == 'list':
946 return tree[2][i + 1]
947 return tree[i + 2]
948 for i, v in enumerate(self.args):
949 valalias = revsetalias(('string', v), getsubtree(i))
950 result = valalias.process(result)
951 return result
952
953 def process(self, tree):
954 if self.match(tree):
955 return self.replace(tree)
956 if isinstance(tree, tuple):
957 return tuple(map(self.process, tree))
958 return tree
959
960 def findaliases(ui, tree):
961 for k, v in ui.configitems('revsetalias'):
962 alias = revsetalias(k, v)
963 tree = alias.process(tree)
964 return tree
965
892 966 parse = parser.parser(tokenize, elements).parse
893 967
894 def match(spec):
968 def match(ui, spec):
895 969 if not spec:
896 970 raise error.ParseError(_("empty query"))
897 971 tree, pos = parse(spec)
898 972 if (pos != len(spec)):
899 973 raise error.ParseError("invalid token", pos)
974 tree = findaliases(ui, tree)
900 975 weight, tree = optimize(tree, True)
901 976 def mfunc(repo, subset):
902 977 return getset(repo, subset, tree)
903 978 return mfunc
904 979
905 980 def makedoc(topic, doc):
906 981 return help.makeitemsdoc(topic, doc, '.. predicatesmarker', symbols)
907 982
908 983 # tell hggettext to extract docstrings from these functions:
909 984 i18nfunctions = symbols.values()
@@ -1,413 +1,437 b''
1 1 $ HGENCODING=utf-8
2 2 $ export HGENCODING
3 3
4 4 $ try() {
5 > hg debugrevspec --debug $@
5 > hg debugrevspec --debug "$@"
6 6 > }
7 7
8 8 $ log() {
9 9 > hg log --template '{rev}\n' -r "$1"
10 10 > }
11 11
12 12 $ hg init repo
13 13 $ cd repo
14 14
15 15 $ echo a > a
16 16 $ hg branch a
17 17 marked working directory as branch a
18 18 $ hg ci -Aqm0
19 19
20 20 $ echo b > b
21 21 $ hg branch b
22 22 marked working directory as branch b
23 23 $ hg ci -Aqm1
24 24
25 25 $ rm a
26 26 $ hg branch a-b-c-
27 27 marked working directory as branch a-b-c-
28 28 $ hg ci -Aqm2 -u Bob
29 29
30 30 $ hg co 1
31 31 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
32 32 $ hg branch +a+b+c+
33 33 marked working directory as branch +a+b+c+
34 34 $ hg ci -Aqm3
35 35
36 36 $ hg co 2 # interleave
37 37 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
38 38 $ echo bb > b
39 39 $ hg branch -- -a-b-c-
40 40 marked working directory as branch -a-b-c-
41 41 $ hg ci -Aqm4 -d "May 12 2005"
42 42
43 43 $ hg co 3
44 44 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
45 45 $ hg branch /a/b/c/
46 46 marked working directory as branch /a/b/c/
47 47 $ hg ci -Aqm"5 bug"
48 48
49 49 $ hg merge 4
50 50 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
51 51 (branch merge, don't forget to commit)
52 52 $ hg branch _a_b_c_
53 53 marked working directory as branch _a_b_c_
54 54 $ hg ci -Aqm"6 issue619"
55 55
56 56 $ hg branch .a.b.c.
57 57 marked working directory as branch .a.b.c.
58 58 $ hg ci -Aqm7
59 59
60 60 $ hg branch all
61 61 marked working directory as branch all
62 62 $ hg ci --close-branch -Aqm8
63 63 abort: can only close branch heads
64 64 [255]
65 65
66 66 $ hg co 4
67 67 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
68 68 $ hg branch Γ©
69 69 marked working directory as branch \xc3\xa9 (esc)
70 70 $ hg ci -Aqm9
71 71
72 72 $ hg tag -r6 1.0
73 73
74 74 $ hg clone --quiet -U -r 7 . ../remote1
75 75 $ hg clone --quiet -U -r 8 . ../remote2
76 76 $ echo "[paths]" >> .hg/hgrc
77 77 $ echo "default = ../remote1" >> .hg/hgrc
78 78
79 79 names that should work without quoting
80 80
81 81 $ try a
82 82 ('symbol', 'a')
83 83 0
84 84 $ try b-a
85 85 ('minus', ('symbol', 'b'), ('symbol', 'a'))
86 86 1
87 87 $ try _a_b_c_
88 88 ('symbol', '_a_b_c_')
89 89 6
90 90 $ try _a_b_c_-a
91 91 ('minus', ('symbol', '_a_b_c_'), ('symbol', 'a'))
92 92 6
93 93 $ try .a.b.c.
94 94 ('symbol', '.a.b.c.')
95 95 7
96 96 $ try .a.b.c.-a
97 97 ('minus', ('symbol', '.a.b.c.'), ('symbol', 'a'))
98 98 7
99 99 $ try -- '-a-b-c-' # complains
100 100 hg: parse error at 7: not a prefix: end
101 101 [255]
102 102 $ log -a-b-c- # succeeds with fallback
103 103 4
104 104 $ try -- -a-b-c--a # complains
105 105 ('minus', ('minus', ('minus', ('negate', ('symbol', 'a')), ('symbol', 'b')), ('symbol', 'c')), ('negate', ('symbol', 'a')))
106 106 abort: unknown revision '-a'!
107 107 [255]
108 108 $ try Γ©
109 109 ('symbol', '\xc3\xa9')
110 110 9
111 111
112 112 quoting needed
113 113
114 114 $ try '"-a-b-c-"-a'
115 115 ('minus', ('string', '-a-b-c-'), ('symbol', 'a'))
116 116 4
117 117
118 118 $ log '1 or 2'
119 119 1
120 120 2
121 121 $ log '1|2'
122 122 1
123 123 2
124 124 $ log '1 and 2'
125 125 $ log '1&2'
126 126 $ try '1&2|3' # precedence - and is higher
127 127 ('or', ('and', ('symbol', '1'), ('symbol', '2')), ('symbol', '3'))
128 128 3
129 129 $ try '1|2&3'
130 130 ('or', ('symbol', '1'), ('and', ('symbol', '2'), ('symbol', '3')))
131 131 1
132 132 $ try '1&2&3' # associativity
133 133 ('and', ('and', ('symbol', '1'), ('symbol', '2')), ('symbol', '3'))
134 134 $ try '1|(2|3)'
135 135 ('or', ('symbol', '1'), ('group', ('or', ('symbol', '2'), ('symbol', '3'))))
136 136 1
137 137 2
138 138 3
139 139 $ log '1.0' # tag
140 140 6
141 141 $ log 'a' # branch
142 142 0
143 143 $ log '2785f51ee'
144 144 0
145 145 $ log 'date(2005)'
146 146 4
147 147 $ log 'date(this is a test)'
148 148 hg: parse error at 10: unexpected token: symbol
149 149 [255]
150 150 $ log 'date()'
151 151 hg: parse error: date requires a string
152 152 [255]
153 153 $ log 'date'
154 154 hg: parse error: can't use date here
155 155 [255]
156 156 $ log 'date('
157 157 hg: parse error at 5: not a prefix: end
158 158 [255]
159 159 $ log 'date(tip)'
160 160 abort: invalid date: 'tip'
161 161 [255]
162 162 $ log '"date"'
163 163 abort: unknown revision 'date'!
164 164 [255]
165 165 $ log 'date(2005) and 1::'
166 166 4
167 167
168 168 $ log 'ancestor(1)'
169 169 hg: parse error: ancestor requires two arguments
170 170 [255]
171 171 $ log 'ancestor(4,5)'
172 172 1
173 173 $ log 'ancestor(4,5) and 4'
174 174 $ log 'ancestors(5)'
175 175 0
176 176 1
177 177 3
178 178 5
179 179 $ log 'author(bob)'
180 180 2
181 181 $ log 'branch(Γ©)'
182 182 8
183 183 9
184 184 $ log 'children(ancestor(4,5))'
185 185 2
186 186 3
187 187 $ log 'closed()'
188 188 $ log 'contains(a)'
189 189 0
190 190 1
191 191 3
192 192 5
193 193 $ log 'descendants(2 or 3)'
194 194 2
195 195 3
196 196 4
197 197 5
198 198 6
199 199 7
200 200 8
201 201 9
202 202 $ log 'file(b)'
203 203 1
204 204 4
205 205 $ log 'follow()'
206 206 0
207 207 1
208 208 2
209 209 4
210 210 8
211 211 9
212 212 $ log 'grep("issue\d+")'
213 213 6
214 214 $ try 'grep("(")' # invalid regular expression
215 215 ('func', ('symbol', 'grep'), ('string', '('))
216 216 hg: parse error: invalid match pattern: unbalanced parenthesis
217 217 [255]
218 218 $ try 'grep("\bissue\d+")'
219 219 ('func', ('symbol', 'grep'), ('string', '\x08issue\\d+'))
220 220 $ try 'grep(r"\bissue\d+")'
221 221 ('func', ('symbol', 'grep'), ('string', '\\bissue\\d+'))
222 222 6
223 223 $ try 'grep(r"\")'
224 224 hg: parse error at 7: unterminated string
225 225 [255]
226 226 $ log 'head()'
227 227 0
228 228 1
229 229 2
230 230 3
231 231 4
232 232 5
233 233 6
234 234 7
235 235 9
236 236 $ log 'heads(6::)'
237 237 7
238 238 $ log 'keyword(issue)'
239 239 6
240 240 $ log 'limit(head(), 1)'
241 241 0
242 242 $ log 'max(contains(a))'
243 243 5
244 244 $ log 'min(contains(a))'
245 245 0
246 246 $ log 'merge()'
247 247 6
248 248 $ log 'modifies(b)'
249 249 4
250 250 $ log 'id(5)'
251 251 2
252 252 $ log 'outgoing()'
253 253 8
254 254 9
255 255 $ log 'outgoing("../remote1")'
256 256 8
257 257 9
258 258 $ log 'outgoing("../remote2")'
259 259 3
260 260 5
261 261 6
262 262 7
263 263 9
264 264 $ log 'p1(merge())'
265 265 5
266 266 $ log 'p2(merge())'
267 267 4
268 268 $ log 'parents(merge())'
269 269 4
270 270 5
271 271 $ log 'removes(a)'
272 272 2
273 273 6
274 274 $ log 'roots(all())'
275 275 0
276 276 $ log 'reverse(2 or 3 or 4 or 5)'
277 277 5
278 278 4
279 279 3
280 280 2
281 281 $ log 'rev(5)'
282 282 5
283 283 $ log 'sort(limit(reverse(all()), 3))'
284 284 7
285 285 8
286 286 9
287 287 $ log 'sort(2 or 3 or 4 or 5, date)'
288 288 2
289 289 3
290 290 5
291 291 4
292 292 $ log 'tagged()'
293 293 6
294 294 $ log 'tag()'
295 295 6
296 296 $ log 'tag(1.0)'
297 297 6
298 298 $ log 'tag(tip)'
299 299 9
300 300 $ log 'tag(unknown)'
301 301 abort: tag 'unknown' does not exist
302 302 [255]
303 303 $ log 'branch(unknown)'
304 304 abort: unknown revision 'unknown'!
305 305 [255]
306 306 $ log 'user(bob)'
307 307 2
308 308
309 309 $ log '4::8'
310 310 4
311 311 8
312 312 $ log '4:8'
313 313 4
314 314 5
315 315 6
316 316 7
317 317 8
318 318
319 319 $ log 'sort(!merge() & (modifies(b) | user(bob) | keyword(bug) | keyword(issue) & 1::9), "-date")'
320 320 4
321 321 2
322 322 5
323 323
324 324 $ log 'not 0 and 0:2'
325 325 1
326 326 2
327 327 $ log 'not 1 and 0:2'
328 328 0
329 329 2
330 330 $ log 'not 2 and 0:2'
331 331 0
332 332 1
333 333 $ log '(1 and 2)::'
334 334 $ log '(1 and 2):'
335 335 $ log '(1 and 2):3'
336 336 $ log 'sort(head(), -rev)'
337 337 9
338 338 7
339 339 6
340 340 5
341 341 4
342 342 3
343 343 2
344 344 1
345 345 0
346 346 $ log '4::8 - 8'
347 347 4
348 348
349 349 issue2437
350 350
351 351 $ log '3 and p1(5)'
352 352 3
353 353 $ log '4 and p2(6)'
354 354 4
355 355 $ log '1 and parents(:2)'
356 356 1
357 357 $ log '2 and children(1:)'
358 358 2
359 359 $ log 'roots(all()) or roots(all())'
360 360 0
361 361 $ log 'heads(branch(Γ©)) or heads(branch(Γ©))'
362 362 9
363 363 $ log 'ancestors(8) and (heads(branch("-a-b-c-")) or heads(branch(Γ©)))'
364 364 4
365 365
366 366 issue2654: report a parse error if the revset was not completely parsed
367 367
368 368 $ log '1 OR 2'
369 369 hg: parse error at 2: invalid token
370 370 [255]
371 371
372 372 or operator should preserve ordering:
373 373 $ log 'reverse(2::4) or tip'
374 374 4
375 375 2
376 376 9
377 377
378 378 parentrevspec
379 379
380 380 $ log 'merge()^0'
381 381 6
382 382 $ log 'merge()^'
383 383 5
384 384 $ log 'merge()^1'
385 385 5
386 386 $ log 'merge()^2'
387 387 4
388 388 $ log 'merge()^^'
389 389 3
390 390 $ log 'merge()^1^'
391 391 3
392 392 $ log 'merge()^^^'
393 393 1
394 394
395 395 $ log 'merge()~0'
396 396 6
397 397 $ log 'merge()~1'
398 398 5
399 399 $ log 'merge()~2'
400 400 3
401 401 $ log 'merge()~2^1'
402 402 1
403 403 $ log 'merge()~3'
404 404 1
405 405
406 406 $ log '(-3:tip)^'
407 407 4
408 408 6
409 409 8
410 410
411 411 $ log 'tip^foo'
412 412 hg: parse error: ^ expects a number 0, 1, or 2
413 413 [255]
414
415 aliases:
416
417 $ echo '[revsetalias]' >> .hg/hgrc
418 $ echo 'm = merge()' >> .hg/hgrc
419 $ echo 'd($1) = reverse(sort($1, date))' >> .hg/hgrc
420 $ echo 'rs(ARG1, ARG2) = reverse(sort(ARG1, ARG2))' >> .hg/hgrc
421
422 $ try m
423 ('symbol', 'm')
424 ('func', ('symbol', 'merge'), None)
425 6
426 $ try 'd(2:5)'
427 ('func', ('symbol', 'd'), ('range', ('symbol', '2'), ('symbol', '5')))
428 ('func', ('symbol', 'reverse'), ('func', ('symbol', 'sort'), ('list', ('range', ('symbol', '2'), ('symbol', '5')), ('symbol', 'date'))))
429 4
430 5
431 3
432 2
433 $ try 'rs(2 or 3, date)'
434 ('func', ('symbol', 'rs'), ('list', ('or', ('symbol', '2'), ('symbol', '3')), ('symbol', 'date')))
435 ('func', ('symbol', 'reverse'), ('func', ('symbol', 'sort'), ('list', ('or', ('symbol', '2'), ('symbol', '3')), ('symbol', 'date'))))
436 3
437 2
General Comments 0
You need to be logged in to leave comments. Login now