##// END OF EJS Templates
run-tests: add --inotify option to test runner...
Nicolas Dumazet -
r9958:777c1df7 default
parent child Browse files
Show More
@@ -1,1293 +1,1293 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, incorporated herein by reference.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, glob
11 11 import mdiff, bdiff, util, templater, patch, error, encoding
12 12 import match as _match
13 13
14 14 revrangesep = ':'
15 15
16 16 def findpossible(cmd, table, strict=False):
17 17 """
18 18 Return cmd -> (aliases, command table entry)
19 19 for each matching command.
20 20 Return debug commands (or their aliases) only if no normal command matches.
21 21 """
22 22 choice = {}
23 23 debugchoice = {}
24 24 for e in table.keys():
25 25 aliases = e.lstrip("^").split("|")
26 26 found = None
27 27 if cmd in aliases:
28 28 found = cmd
29 29 elif not strict:
30 30 for a in aliases:
31 31 if a.startswith(cmd):
32 32 found = a
33 33 break
34 34 if found is not None:
35 35 if aliases[0].startswith("debug") or found.startswith("debug"):
36 36 debugchoice[found] = (aliases, table[e])
37 37 else:
38 38 choice[found] = (aliases, table[e])
39 39
40 40 if not choice and debugchoice:
41 41 choice = debugchoice
42 42
43 43 return choice
44 44
45 45 def findcmd(cmd, table, strict=True):
46 46 """Return (aliases, command table entry) for command string."""
47 47 choice = findpossible(cmd, table, strict)
48 48
49 49 if cmd in choice:
50 50 return choice[cmd]
51 51
52 52 if len(choice) > 1:
53 53 clist = choice.keys()
54 54 clist.sort()
55 55 raise error.AmbiguousCommand(cmd, clist)
56 56
57 57 if choice:
58 58 return choice.values()[0]
59 59
60 60 raise error.UnknownCommand(cmd)
61 61
62 62 def bail_if_changed(repo):
63 63 if repo.dirstate.parents()[1] != nullid:
64 64 raise util.Abort(_('outstanding uncommitted merge'))
65 65 modified, added, removed, deleted = repo.status()[:4]
66 66 if modified or added or removed or deleted:
67 67 raise util.Abort(_("outstanding uncommitted changes"))
68 68
69 69 def logmessage(opts):
70 70 """ get the log message according to -m and -l option """
71 71 message = opts.get('message')
72 72 logfile = opts.get('logfile')
73 73
74 74 if message and logfile:
75 75 raise util.Abort(_('options --message and --logfile are mutually '
76 76 'exclusive'))
77 77 if not message and logfile:
78 78 try:
79 79 if logfile == '-':
80 80 message = sys.stdin.read()
81 81 else:
82 82 message = open(logfile).read()
83 83 except IOError, inst:
84 84 raise util.Abort(_("can't read commit message '%s': %s") %
85 85 (logfile, inst.strerror))
86 86 return message
87 87
88 88 def loglimit(opts):
89 89 """get the log limit according to option -l/--limit"""
90 90 limit = opts.get('limit')
91 91 if limit:
92 92 try:
93 93 limit = int(limit)
94 94 except ValueError:
95 95 raise util.Abort(_('limit must be a positive integer'))
96 96 if limit <= 0: raise util.Abort(_('limit must be positive'))
97 97 else:
98 98 limit = sys.maxint
99 99 return limit
100 100
101 101 def remoteui(src, opts):
102 102 'build a remote ui from ui or repo and opts'
103 103 if hasattr(src, 'baseui'): # looks like a repository
104 104 dst = src.baseui.copy() # drop repo-specific config
105 105 src = src.ui # copy target options from repo
106 106 else: # assume it's a global ui object
107 107 dst = src.copy() # keep all global options
108 108
109 109 # copy ssh-specific options
110 110 for o in 'ssh', 'remotecmd':
111 111 v = opts.get(o) or src.config('ui', o)
112 112 if v:
113 113 dst.setconfig("ui", o, v)
114 114 # copy bundle-specific options
115 115 r = src.config('bundle', 'mainreporoot')
116 116 if r:
117 117 dst.setconfig('bundle', 'mainreporoot', r)
118 118
119 119 return dst
120 120
121 121 def revpair(repo, revs):
122 122 '''return pair of nodes, given list of revisions. second item can
123 123 be None, meaning use working dir.'''
124 124
125 125 def revfix(repo, val, defval):
126 126 if not val and val != 0 and defval is not None:
127 127 val = defval
128 128 return repo.lookup(val)
129 129
130 130 if not revs:
131 131 return repo.dirstate.parents()[0], None
132 132 end = None
133 133 if len(revs) == 1:
134 134 if revrangesep in revs[0]:
135 135 start, end = revs[0].split(revrangesep, 1)
136 136 start = revfix(repo, start, 0)
137 137 end = revfix(repo, end, len(repo) - 1)
138 138 else:
139 139 start = revfix(repo, revs[0], None)
140 140 elif len(revs) == 2:
141 141 if revrangesep in revs[0] or revrangesep in revs[1]:
142 142 raise util.Abort(_('too many revisions specified'))
143 143 start = revfix(repo, revs[0], None)
144 144 end = revfix(repo, revs[1], None)
145 145 else:
146 146 raise util.Abort(_('too many revisions specified'))
147 147 return start, end
148 148
149 149 def revrange(repo, revs):
150 150 """Yield revision as strings from a list of revision specifications."""
151 151
152 152 def revfix(repo, val, defval):
153 153 if not val and val != 0 and defval is not None:
154 154 return defval
155 155 return repo.changelog.rev(repo.lookup(val))
156 156
157 157 seen, l = set(), []
158 158 for spec in revs:
159 159 if revrangesep in spec:
160 160 start, end = spec.split(revrangesep, 1)
161 161 start = revfix(repo, start, 0)
162 162 end = revfix(repo, end, len(repo) - 1)
163 163 step = start > end and -1 or 1
164 164 for rev in xrange(start, end+step, step):
165 165 if rev in seen:
166 166 continue
167 167 seen.add(rev)
168 168 l.append(rev)
169 169 else:
170 170 rev = revfix(repo, spec, None)
171 171 if rev in seen:
172 172 continue
173 173 seen.add(rev)
174 174 l.append(rev)
175 175
176 176 return l
177 177
178 178 def make_filename(repo, pat, node,
179 179 total=None, seqno=None, revwidth=None, pathname=None):
180 180 node_expander = {
181 181 'H': lambda: hex(node),
182 182 'R': lambda: str(repo.changelog.rev(node)),
183 183 'h': lambda: short(node),
184 184 }
185 185 expander = {
186 186 '%': lambda: '%',
187 187 'b': lambda: os.path.basename(repo.root),
188 188 }
189 189
190 190 try:
191 191 if node:
192 192 expander.update(node_expander)
193 193 if node:
194 194 expander['r'] = (lambda:
195 195 str(repo.changelog.rev(node)).zfill(revwidth or 0))
196 196 if total is not None:
197 197 expander['N'] = lambda: str(total)
198 198 if seqno is not None:
199 199 expander['n'] = lambda: str(seqno)
200 200 if total is not None and seqno is not None:
201 201 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
202 202 if pathname is not None:
203 203 expander['s'] = lambda: os.path.basename(pathname)
204 204 expander['d'] = lambda: os.path.dirname(pathname) or '.'
205 205 expander['p'] = lambda: pathname
206 206
207 207 newname = []
208 208 patlen = len(pat)
209 209 i = 0
210 210 while i < patlen:
211 211 c = pat[i]
212 212 if c == '%':
213 213 i += 1
214 214 c = pat[i]
215 215 c = expander[c]()
216 216 newname.append(c)
217 217 i += 1
218 218 return ''.join(newname)
219 219 except KeyError, inst:
220 220 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
221 221 inst.args[0])
222 222
223 223 def make_file(repo, pat, node=None,
224 224 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
225 225
226 226 writable = 'w' in mode or 'a' in mode
227 227
228 228 if not pat or pat == '-':
229 229 return writable and sys.stdout or sys.stdin
230 230 if hasattr(pat, 'write') and writable:
231 231 return pat
232 232 if hasattr(pat, 'read') and 'r' in mode:
233 233 return pat
234 234 return open(make_filename(repo, pat, node, total, seqno, revwidth,
235 235 pathname),
236 236 mode)
237 237
238 238 def expandpats(pats):
239 239 if not util.expandglobs:
240 240 return list(pats)
241 241 ret = []
242 242 for p in pats:
243 243 kind, name = _match._patsplit(p, None)
244 244 if kind is None:
245 245 try:
246 246 globbed = glob.glob(name)
247 247 except re.error:
248 248 globbed = [name]
249 249 if globbed:
250 250 ret.extend(globbed)
251 251 continue
252 252 ret.append(p)
253 253 return ret
254 254
255 255 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
256 256 if not globbed and default == 'relpath':
257 257 pats = expandpats(pats or [])
258 258 m = _match.match(repo.root, repo.getcwd(), pats,
259 259 opts.get('include'), opts.get('exclude'), default)
260 260 def badfn(f, msg):
261 261 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
262 262 m.bad = badfn
263 263 return m
264 264
265 265 def matchall(repo):
266 266 return _match.always(repo.root, repo.getcwd())
267 267
268 268 def matchfiles(repo, files):
269 269 return _match.exact(repo.root, repo.getcwd(), files)
270 270
271 271 def findrenames(repo, added, removed, threshold):
272 272 '''find renamed files -- yields (before, after, score) tuples'''
273 273 copies = {}
274 274 ctx = repo['.']
275 275 for r in removed:
276 276 if r not in ctx:
277 277 continue
278 278 fctx = ctx.filectx(r)
279 279
280 280 def score(text):
281 281 if not len(text):
282 282 return 0.0
283 283 if not fctx.cmp(text):
284 284 return 1.0
285 285 if threshold == 1.0:
286 286 return 0.0
287 287 orig = fctx.data()
288 288 # bdiff.blocks() returns blocks of matching lines
289 289 # count the number of bytes in each
290 290 equal = 0
291 291 alines = mdiff.splitnewlines(text)
292 292 matches = bdiff.blocks(text, orig)
293 293 for x1, x2, y1, y2 in matches:
294 294 for line in alines[x1:x2]:
295 295 equal += len(line)
296 296
297 297 lengths = len(text) + len(orig)
298 298 return equal * 2.0 / lengths
299 299
300 300 for a in added:
301 301 bestscore = copies.get(a, (None, threshold))[1]
302 302 myscore = score(repo.wread(a))
303 303 if myscore >= bestscore:
304 304 copies[a] = (r, myscore)
305 305
306 306 for dest, v in copies.iteritems():
307 307 source, score = v
308 308 yield source, dest, score
309 309
310 310 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
311 311 if dry_run is None:
312 312 dry_run = opts.get('dry_run')
313 313 if similarity is None:
314 314 similarity = float(opts.get('similarity') or 0)
315 315 # we'd use status here, except handling of symlinks and ignore is tricky
316 316 added, unknown, deleted, removed = [], [], [], []
317 317 audit_path = util.path_auditor(repo.root)
318 318 m = match(repo, pats, opts)
319 319 for abs in repo.walk(m):
320 320 target = repo.wjoin(abs)
321 321 good = True
322 322 try:
323 323 audit_path(abs)
324 324 except:
325 325 good = False
326 326 rel = m.rel(abs)
327 327 exact = m.exact(abs)
328 328 if good and abs not in repo.dirstate:
329 329 unknown.append(abs)
330 330 if repo.ui.verbose or not exact:
331 331 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
332 332 elif repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
333 333 or (os.path.isdir(target) and not os.path.islink(target))):
334 334 deleted.append(abs)
335 335 if repo.ui.verbose or not exact:
336 336 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
337 337 # for finding renames
338 338 elif repo.dirstate[abs] == 'r':
339 339 removed.append(abs)
340 340 elif repo.dirstate[abs] == 'a':
341 341 added.append(abs)
342 342 if not dry_run:
343 343 repo.remove(deleted)
344 344 repo.add(unknown)
345 345 if similarity > 0:
346 346 for old, new, score in findrenames(repo, added + unknown,
347 347 removed + deleted, similarity):
348 348 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
349 349 repo.ui.status(_('recording removal of %s as rename to %s '
350 350 '(%d%% similar)\n') %
351 351 (m.rel(old), m.rel(new), score * 100))
352 352 if not dry_run:
353 353 repo.copy(old, new)
354 354
355 355 def copy(ui, repo, pats, opts, rename=False):
356 356 # called with the repo lock held
357 357 #
358 358 # hgsep => pathname that uses "/" to separate directories
359 359 # ossep => pathname that uses os.sep to separate directories
360 360 cwd = repo.getcwd()
361 361 targets = {}
362 362 after = opts.get("after")
363 363 dryrun = opts.get("dry_run")
364 364
365 365 def walkpat(pat):
366 366 srcs = []
367 367 m = match(repo, [pat], opts, globbed=True)
368 368 for abs in repo.walk(m):
369 369 state = repo.dirstate[abs]
370 370 rel = m.rel(abs)
371 371 exact = m.exact(abs)
372 372 if state in '?r':
373 373 if exact and state == '?':
374 374 ui.warn(_('%s: not copying - file is not managed\n') % rel)
375 375 if exact and state == 'r':
376 376 ui.warn(_('%s: not copying - file has been marked for'
377 377 ' remove\n') % rel)
378 378 continue
379 379 # abs: hgsep
380 380 # rel: ossep
381 381 srcs.append((abs, rel, exact))
382 382 return srcs
383 383
384 384 # abssrc: hgsep
385 385 # relsrc: ossep
386 386 # otarget: ossep
387 387 def copyfile(abssrc, relsrc, otarget, exact):
388 388 abstarget = util.canonpath(repo.root, cwd, otarget)
389 389 reltarget = repo.pathto(abstarget, cwd)
390 390 target = repo.wjoin(abstarget)
391 391 src = repo.wjoin(abssrc)
392 392 state = repo.dirstate[abstarget]
393 393
394 394 # check for collisions
395 395 prevsrc = targets.get(abstarget)
396 396 if prevsrc is not None:
397 397 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
398 398 (reltarget, repo.pathto(abssrc, cwd),
399 399 repo.pathto(prevsrc, cwd)))
400 400 return
401 401
402 402 # check for overwrites
403 403 exists = os.path.exists(target)
404 404 if not after and exists or after and state in 'mn':
405 405 if not opts['force']:
406 406 ui.warn(_('%s: not overwriting - file exists\n') %
407 407 reltarget)
408 408 return
409 409
410 410 if after:
411 411 if not exists:
412 412 return
413 413 elif not dryrun:
414 414 try:
415 415 if exists:
416 416 os.unlink(target)
417 417 targetdir = os.path.dirname(target) or '.'
418 418 if not os.path.isdir(targetdir):
419 419 os.makedirs(targetdir)
420 420 util.copyfile(src, target)
421 421 except IOError, inst:
422 422 if inst.errno == errno.ENOENT:
423 423 ui.warn(_('%s: deleted in working copy\n') % relsrc)
424 424 else:
425 425 ui.warn(_('%s: cannot copy - %s\n') %
426 426 (relsrc, inst.strerror))
427 427 return True # report a failure
428 428
429 429 if ui.verbose or not exact:
430 430 if rename:
431 431 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
432 432 else:
433 433 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
434 434
435 435 targets[abstarget] = abssrc
436 436
437 437 # fix up dirstate
438 438 origsrc = repo.dirstate.copied(abssrc) or abssrc
439 439 if abstarget == origsrc: # copying back a copy?
440 440 if state not in 'mn' and not dryrun:
441 441 repo.dirstate.normallookup(abstarget)
442 442 else:
443 443 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
444 444 if not ui.quiet:
445 445 ui.warn(_("%s has not been committed yet, so no copy "
446 446 "data will be stored for %s.\n")
447 447 % (repo.pathto(origsrc, cwd), reltarget))
448 448 if repo.dirstate[abstarget] in '?r' and not dryrun:
449 449 repo.add([abstarget])
450 450 elif not dryrun:
451 451 repo.copy(origsrc, abstarget)
452 452
453 453 if rename and not dryrun:
454 454 repo.remove([abssrc], not after)
455 455
456 456 # pat: ossep
457 457 # dest ossep
458 458 # srcs: list of (hgsep, hgsep, ossep, bool)
459 459 # return: function that takes hgsep and returns ossep
460 460 def targetpathfn(pat, dest, srcs):
461 461 if os.path.isdir(pat):
462 462 abspfx = util.canonpath(repo.root, cwd, pat)
463 463 abspfx = util.localpath(abspfx)
464 464 if destdirexists:
465 465 striplen = len(os.path.split(abspfx)[0])
466 466 else:
467 467 striplen = len(abspfx)
468 468 if striplen:
469 469 striplen += len(os.sep)
470 470 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
471 471 elif destdirexists:
472 472 res = lambda p: os.path.join(dest,
473 473 os.path.basename(util.localpath(p)))
474 474 else:
475 475 res = lambda p: dest
476 476 return res
477 477
478 478 # pat: ossep
479 479 # dest ossep
480 480 # srcs: list of (hgsep, hgsep, ossep, bool)
481 481 # return: function that takes hgsep and returns ossep
482 482 def targetpathafterfn(pat, dest, srcs):
483 483 if _match.patkind(pat):
484 484 # a mercurial pattern
485 485 res = lambda p: os.path.join(dest,
486 486 os.path.basename(util.localpath(p)))
487 487 else:
488 488 abspfx = util.canonpath(repo.root, cwd, pat)
489 489 if len(abspfx) < len(srcs[0][0]):
490 490 # A directory. Either the target path contains the last
491 491 # component of the source path or it does not.
492 492 def evalpath(striplen):
493 493 score = 0
494 494 for s in srcs:
495 495 t = os.path.join(dest, util.localpath(s[0])[striplen:])
496 496 if os.path.exists(t):
497 497 score += 1
498 498 return score
499 499
500 500 abspfx = util.localpath(abspfx)
501 501 striplen = len(abspfx)
502 502 if striplen:
503 503 striplen += len(os.sep)
504 504 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
505 505 score = evalpath(striplen)
506 506 striplen1 = len(os.path.split(abspfx)[0])
507 507 if striplen1:
508 508 striplen1 += len(os.sep)
509 509 if evalpath(striplen1) > score:
510 510 striplen = striplen1
511 511 res = lambda p: os.path.join(dest,
512 512 util.localpath(p)[striplen:])
513 513 else:
514 514 # a file
515 515 if destdirexists:
516 516 res = lambda p: os.path.join(dest,
517 517 os.path.basename(util.localpath(p)))
518 518 else:
519 519 res = lambda p: dest
520 520 return res
521 521
522 522
523 523 pats = expandpats(pats)
524 524 if not pats:
525 525 raise util.Abort(_('no source or destination specified'))
526 526 if len(pats) == 1:
527 527 raise util.Abort(_('no destination specified'))
528 528 dest = pats.pop()
529 529 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
530 530 if not destdirexists:
531 531 if len(pats) > 1 or _match.patkind(pats[0]):
532 532 raise util.Abort(_('with multiple sources, destination must be an '
533 533 'existing directory'))
534 534 if util.endswithsep(dest):
535 535 raise util.Abort(_('destination %s is not a directory') % dest)
536 536
537 537 tfn = targetpathfn
538 538 if after:
539 539 tfn = targetpathafterfn
540 540 copylist = []
541 541 for pat in pats:
542 542 srcs = walkpat(pat)
543 543 if not srcs:
544 544 continue
545 545 copylist.append((tfn(pat, dest, srcs), srcs))
546 546 if not copylist:
547 547 raise util.Abort(_('no files to copy'))
548 548
549 549 errors = 0
550 550 for targetpath, srcs in copylist:
551 551 for abssrc, relsrc, exact in srcs:
552 552 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
553 553 errors += 1
554 554
555 555 if errors:
556 556 ui.warn(_('(consider using --after)\n'))
557 557
558 558 return errors
559 559
560 560 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
561 561 runargs=None):
562 562 '''Run a command as a service.'''
563 563
564 564 if opts['daemon'] and not opts['daemon_pipefds']:
565 565 rfd, wfd = os.pipe()
566 566 if not runargs:
567 567 runargs = sys.argv[:]
568 568 runargs.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
569 569 # Don't pass --cwd to the child process, because we've already
570 570 # changed directory.
571 571 for i in xrange(1,len(runargs)):
572 572 if runargs[i].startswith('--cwd='):
573 573 del runargs[i]
574 574 break
575 575 elif runargs[i].startswith('--cwd'):
576 576 del runargs[i:i+2]
577 577 break
578 578 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
579 579 runargs[0], runargs)
580 580 os.close(wfd)
581 581 os.read(rfd, 1)
582 582 if parentfn:
583 583 return parentfn(pid)
584 584 else:
585 585 return
586 586
587 587 if initfn:
588 588 initfn()
589 589
590 590 if opts['pid_file']:
591 fp = open(opts['pid_file'], 'w')
591 fp = open(opts['pid_file'], 'a')
592 592 fp.write(str(os.getpid()) + '\n')
593 593 fp.close()
594 594
595 595 if opts['daemon_pipefds']:
596 596 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
597 597 os.close(rfd)
598 598 try:
599 599 os.setsid()
600 600 except AttributeError:
601 601 pass
602 602 os.write(wfd, 'y')
603 603 os.close(wfd)
604 604 sys.stdout.flush()
605 605 sys.stderr.flush()
606 606
607 607 nullfd = os.open(util.nulldev, os.O_RDWR)
608 608 logfilefd = nullfd
609 609 if logfile:
610 610 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
611 611 os.dup2(nullfd, 0)
612 612 os.dup2(logfilefd, 1)
613 613 os.dup2(logfilefd, 2)
614 614 if nullfd not in (0, 1, 2):
615 615 os.close(nullfd)
616 616 if logfile and logfilefd not in (0, 1, 2):
617 617 os.close(logfilefd)
618 618
619 619 if runfn:
620 620 return runfn()
621 621
622 622 class changeset_printer(object):
623 623 '''show changeset information when templating not requested.'''
624 624
625 625 def __init__(self, ui, repo, patch, diffopts, buffered):
626 626 self.ui = ui
627 627 self.repo = repo
628 628 self.buffered = buffered
629 629 self.patch = patch
630 630 self.diffopts = diffopts
631 631 self.header = {}
632 632 self.hunk = {}
633 633 self.lastheader = None
634 634
635 635 def flush(self, rev):
636 636 if rev in self.header:
637 637 h = self.header[rev]
638 638 if h != self.lastheader:
639 639 self.lastheader = h
640 640 self.ui.write(h)
641 641 del self.header[rev]
642 642 if rev in self.hunk:
643 643 self.ui.write(self.hunk[rev])
644 644 del self.hunk[rev]
645 645 return 1
646 646 return 0
647 647
648 648 def show(self, ctx, copies=(), **props):
649 649 if self.buffered:
650 650 self.ui.pushbuffer()
651 651 self._show(ctx, copies, props)
652 652 self.hunk[ctx.rev()] = self.ui.popbuffer()
653 653 else:
654 654 self._show(ctx, copies, props)
655 655
656 656 def _show(self, ctx, copies, props):
657 657 '''show a single changeset or file revision'''
658 658 changenode = ctx.node()
659 659 rev = ctx.rev()
660 660
661 661 if self.ui.quiet:
662 662 self.ui.write("%d:%s\n" % (rev, short(changenode)))
663 663 return
664 664
665 665 log = self.repo.changelog
666 666 date = util.datestr(ctx.date())
667 667
668 668 hexfunc = self.ui.debugflag and hex or short
669 669
670 670 parents = [(p, hexfunc(log.node(p)))
671 671 for p in self._meaningful_parentrevs(log, rev)]
672 672
673 673 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
674 674
675 675 branch = ctx.branch()
676 676 # don't show the default branch name
677 677 if branch != 'default':
678 678 branch = encoding.tolocal(branch)
679 679 self.ui.write(_("branch: %s\n") % branch)
680 680 for tag in self.repo.nodetags(changenode):
681 681 self.ui.write(_("tag: %s\n") % tag)
682 682 for parent in parents:
683 683 self.ui.write(_("parent: %d:%s\n") % parent)
684 684
685 685 if self.ui.debugflag:
686 686 mnode = ctx.manifestnode()
687 687 self.ui.write(_("manifest: %d:%s\n") %
688 688 (self.repo.manifest.rev(mnode), hex(mnode)))
689 689 self.ui.write(_("user: %s\n") % ctx.user())
690 690 self.ui.write(_("date: %s\n") % date)
691 691
692 692 if self.ui.debugflag:
693 693 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
694 694 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
695 695 files):
696 696 if value:
697 697 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
698 698 elif ctx.files() and self.ui.verbose:
699 699 self.ui.write(_("files: %s\n") % " ".join(ctx.files()))
700 700 if copies and self.ui.verbose:
701 701 copies = ['%s (%s)' % c for c in copies]
702 702 self.ui.write(_("copies: %s\n") % ' '.join(copies))
703 703
704 704 extra = ctx.extra()
705 705 if extra and self.ui.debugflag:
706 706 for key, value in sorted(extra.items()):
707 707 self.ui.write(_("extra: %s=%s\n")
708 708 % (key, value.encode('string_escape')))
709 709
710 710 description = ctx.description().strip()
711 711 if description:
712 712 if self.ui.verbose:
713 713 self.ui.write(_("description:\n"))
714 714 self.ui.write(description)
715 715 self.ui.write("\n\n")
716 716 else:
717 717 self.ui.write(_("summary: %s\n") %
718 718 description.splitlines()[0])
719 719 self.ui.write("\n")
720 720
721 721 self.showpatch(changenode)
722 722
723 723 def showpatch(self, node):
724 724 if self.patch:
725 725 prev = self.repo.changelog.parents(node)[0]
726 726 chunks = patch.diff(self.repo, prev, node, match=self.patch,
727 727 opts=patch.diffopts(self.ui, self.diffopts))
728 728 for chunk in chunks:
729 729 self.ui.write(chunk)
730 730 self.ui.write("\n")
731 731
732 732 def _meaningful_parentrevs(self, log, rev):
733 733 """Return list of meaningful (or all if debug) parentrevs for rev.
734 734
735 735 For merges (two non-nullrev revisions) both parents are meaningful.
736 736 Otherwise the first parent revision is considered meaningful if it
737 737 is not the preceding revision.
738 738 """
739 739 parents = log.parentrevs(rev)
740 740 if not self.ui.debugflag and parents[1] == nullrev:
741 741 if parents[0] >= rev - 1:
742 742 parents = []
743 743 else:
744 744 parents = [parents[0]]
745 745 return parents
746 746
747 747
748 748 class changeset_templater(changeset_printer):
749 749 '''format changeset information.'''
750 750
751 751 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
752 752 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
753 753 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
754 754 self.t = templater.templater(mapfile, {'formatnode': formatnode},
755 755 cache={
756 756 'parent': '{rev}:{node|formatnode} ',
757 757 'manifest': '{rev}:{node|formatnode}',
758 758 'filecopy': '{name} ({source})'})
759 759 # Cache mapping from rev to a tuple with tag date, tag
760 760 # distance and tag name
761 761 self._latesttagcache = {-1: (0, 0, 'null')}
762 762
763 763 def use_template(self, t):
764 764 '''set template string to use'''
765 765 self.t.cache['changeset'] = t
766 766
767 767 def _meaningful_parentrevs(self, ctx):
768 768 """Return list of meaningful (or all if debug) parentrevs for rev.
769 769 """
770 770 parents = ctx.parents()
771 771 if len(parents) > 1:
772 772 return parents
773 773 if self.ui.debugflag:
774 774 return [parents[0], self.repo['null']]
775 775 if parents[0].rev() >= ctx.rev() - 1:
776 776 return []
777 777 return parents
778 778
779 779 def _latesttaginfo(self, rev):
780 780 '''return date, distance and name for the latest tag of rev'''
781 781 todo = [rev]
782 782 while todo:
783 783 rev = todo.pop()
784 784 if rev in self._latesttagcache:
785 785 continue
786 786 ctx = self.repo[rev]
787 787 tags = [t for t in ctx.tags() if self.repo.tagtype(t) == 'global']
788 788 if tags:
789 789 self._latesttagcache[rev] = ctx.date()[0], 0, ':'.join(sorted(tags))
790 790 continue
791 791 try:
792 792 # The tuples are laid out so the right one can be found by comparison.
793 793 pdate, pdist, ptag = max(
794 794 self._latesttagcache[p.rev()] for p in ctx.parents())
795 795 except KeyError:
796 796 # Cache miss - recurse
797 797 todo.append(rev)
798 798 todo.extend(p.rev() for p in ctx.parents())
799 799 continue
800 800 self._latesttagcache[rev] = pdate, pdist + 1, ptag
801 801 return self._latesttagcache[rev]
802 802
803 803 def _show(self, ctx, copies, props):
804 804 '''show a single changeset or file revision'''
805 805
806 806 def showlist(name, values, plural=None, **args):
807 807 '''expand set of values.
808 808 name is name of key in template map.
809 809 values is list of strings or dicts.
810 810 plural is plural of name, if not simply name + 's'.
811 811
812 812 expansion works like this, given name 'foo'.
813 813
814 814 if values is empty, expand 'no_foos'.
815 815
816 816 if 'foo' not in template map, return values as a string,
817 817 joined by space.
818 818
819 819 expand 'start_foos'.
820 820
821 821 for each value, expand 'foo'. if 'last_foo' in template
822 822 map, expand it instead of 'foo' for last key.
823 823
824 824 expand 'end_foos'.
825 825 '''
826 826 if plural: names = plural
827 827 else: names = name + 's'
828 828 if not values:
829 829 noname = 'no_' + names
830 830 if noname in self.t:
831 831 yield self.t(noname, **args)
832 832 return
833 833 if name not in self.t:
834 834 if isinstance(values[0], str):
835 835 yield ' '.join(values)
836 836 else:
837 837 for v in values:
838 838 yield dict(v, **args)
839 839 return
840 840 startname = 'start_' + names
841 841 if startname in self.t:
842 842 yield self.t(startname, **args)
843 843 vargs = args.copy()
844 844 def one(v, tag=name):
845 845 try:
846 846 vargs.update(v)
847 847 except (AttributeError, ValueError):
848 848 try:
849 849 for a, b in v:
850 850 vargs[a] = b
851 851 except ValueError:
852 852 vargs[name] = v
853 853 return self.t(tag, **vargs)
854 854 lastname = 'last_' + name
855 855 if lastname in self.t:
856 856 last = values.pop()
857 857 else:
858 858 last = None
859 859 for v in values:
860 860 yield one(v)
861 861 if last is not None:
862 862 yield one(last, tag=lastname)
863 863 endname = 'end_' + names
864 864 if endname in self.t:
865 865 yield self.t(endname, **args)
866 866
867 867 def showbranches(**args):
868 868 branch = ctx.branch()
869 869 if branch != 'default':
870 870 branch = encoding.tolocal(branch)
871 871 return showlist('branch', [branch], plural='branches', **args)
872 872
873 873 def showparents(**args):
874 874 parents = [[('rev', p.rev()), ('node', p.hex())]
875 875 for p in self._meaningful_parentrevs(ctx)]
876 876 return showlist('parent', parents, **args)
877 877
878 878 def showtags(**args):
879 879 return showlist('tag', ctx.tags(), **args)
880 880
881 881 def showextras(**args):
882 882 for key, value in sorted(ctx.extra().items()):
883 883 args = args.copy()
884 884 args.update(dict(key=key, value=value))
885 885 yield self.t('extra', **args)
886 886
887 887 def showcopies(**args):
888 888 c = [{'name': x[0], 'source': x[1]} for x in copies]
889 889 return showlist('file_copy', c, plural='file_copies', **args)
890 890
891 891 files = []
892 892 def getfiles():
893 893 if not files:
894 894 files[:] = self.repo.status(ctx.parents()[0].node(),
895 895 ctx.node())[:3]
896 896 return files
897 897 def showfiles(**args):
898 898 return showlist('file', ctx.files(), **args)
899 899 def showmods(**args):
900 900 return showlist('file_mod', getfiles()[0], **args)
901 901 def showadds(**args):
902 902 return showlist('file_add', getfiles()[1], **args)
903 903 def showdels(**args):
904 904 return showlist('file_del', getfiles()[2], **args)
905 905 def showmanifest(**args):
906 906 args = args.copy()
907 907 args.update(dict(rev=self.repo.manifest.rev(ctx.changeset()[0]),
908 908 node=hex(ctx.changeset()[0])))
909 909 return self.t('manifest', **args)
910 910
911 911 def showdiffstat(**args):
912 912 diff = patch.diff(self.repo, ctx.parents()[0].node(), ctx.node())
913 913 files, adds, removes = 0, 0, 0
914 914 for i in patch.diffstatdata(util.iterlines(diff)):
915 915 files += 1
916 916 adds += i[1]
917 917 removes += i[2]
918 918 return '%s: +%s/-%s' % (files, adds, removes)
919 919
920 920 def showlatesttag(**args):
921 921 return self._latesttaginfo(ctx.rev())[2]
922 922 def showlatesttagdistance(**args):
923 923 return self._latesttaginfo(ctx.rev())[1]
924 924
925 925 defprops = {
926 926 'author': ctx.user(),
927 927 'branches': showbranches,
928 928 'date': ctx.date(),
929 929 'desc': ctx.description().strip(),
930 930 'file_adds': showadds,
931 931 'file_dels': showdels,
932 932 'file_mods': showmods,
933 933 'files': showfiles,
934 934 'file_copies': showcopies,
935 935 'manifest': showmanifest,
936 936 'node': ctx.hex(),
937 937 'parents': showparents,
938 938 'rev': ctx.rev(),
939 939 'tags': showtags,
940 940 'extras': showextras,
941 941 'diffstat': showdiffstat,
942 942 'latesttag': showlatesttag,
943 943 'latesttagdistance': showlatesttagdistance,
944 944 }
945 945 props = props.copy()
946 946 props.update(defprops)
947 947
948 948 # find correct templates for current mode
949 949
950 950 tmplmodes = [
951 951 (True, None),
952 952 (self.ui.verbose, 'verbose'),
953 953 (self.ui.quiet, 'quiet'),
954 954 (self.ui.debugflag, 'debug'),
955 955 ]
956 956
957 957 types = {'header': '', 'changeset': 'changeset'}
958 958 for mode, postfix in tmplmodes:
959 959 for type in types:
960 960 cur = postfix and ('%s_%s' % (type, postfix)) or type
961 961 if mode and cur in self.t:
962 962 types[type] = cur
963 963
964 964 try:
965 965
966 966 # write header
967 967 if types['header']:
968 968 h = templater.stringify(self.t(types['header'], **props))
969 969 if self.buffered:
970 970 self.header[ctx.rev()] = h
971 971 else:
972 972 self.ui.write(h)
973 973
974 974 # write changeset metadata, then patch if requested
975 975 key = types['changeset']
976 976 self.ui.write(templater.stringify(self.t(key, **props)))
977 977 self.showpatch(ctx.node())
978 978
979 979 except KeyError, inst:
980 980 msg = _("%s: no key named '%s'")
981 981 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
982 982 except SyntaxError, inst:
983 983 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
984 984
985 985 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
986 986 """show one changeset using template or regular display.
987 987
988 988 Display format will be the first non-empty hit of:
989 989 1. option 'template'
990 990 2. option 'style'
991 991 3. [ui] setting 'logtemplate'
992 992 4. [ui] setting 'style'
993 993 If all of these values are either the unset or the empty string,
994 994 regular display via changeset_printer() is done.
995 995 """
996 996 # options
997 997 patch = False
998 998 if opts.get('patch'):
999 999 patch = matchfn or matchall(repo)
1000 1000
1001 1001 tmpl = opts.get('template')
1002 1002 style = None
1003 1003 if tmpl:
1004 1004 tmpl = templater.parsestring(tmpl, quoted=False)
1005 1005 else:
1006 1006 style = opts.get('style')
1007 1007
1008 1008 # ui settings
1009 1009 if not (tmpl or style):
1010 1010 tmpl = ui.config('ui', 'logtemplate')
1011 1011 if tmpl:
1012 1012 tmpl = templater.parsestring(tmpl)
1013 1013 else:
1014 1014 style = ui.config('ui', 'style')
1015 1015
1016 1016 if not (tmpl or style):
1017 1017 return changeset_printer(ui, repo, patch, opts, buffered)
1018 1018
1019 1019 mapfile = None
1020 1020 if style and not tmpl:
1021 1021 mapfile = style
1022 1022 if not os.path.split(mapfile)[0]:
1023 1023 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1024 1024 or templater.templatepath(mapfile))
1025 1025 if mapname: mapfile = mapname
1026 1026
1027 1027 try:
1028 1028 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
1029 1029 except SyntaxError, inst:
1030 1030 raise util.Abort(inst.args[0])
1031 1031 if tmpl: t.use_template(tmpl)
1032 1032 return t
1033 1033
1034 1034 def finddate(ui, repo, date):
1035 1035 """Find the tipmost changeset that matches the given date spec"""
1036 1036
1037 1037 df = util.matchdate(date)
1038 1038 m = matchall(repo)
1039 1039 results = {}
1040 1040
1041 1041 def prep(ctx, fns):
1042 1042 d = ctx.date()
1043 1043 if df(d[0]):
1044 1044 results[ctx.rev()] = d
1045 1045
1046 1046 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1047 1047 rev = ctx.rev()
1048 1048 if rev in results:
1049 1049 ui.status(_("Found revision %s from %s\n") %
1050 1050 (rev, util.datestr(results[rev])))
1051 1051 return str(rev)
1052 1052
1053 1053 raise util.Abort(_("revision matching date not found"))
1054 1054
1055 1055 def walkchangerevs(repo, match, opts, prepare):
1056 1056 '''Iterate over files and the revs in which they changed.
1057 1057
1058 1058 Callers most commonly need to iterate backwards over the history
1059 1059 in which they are interested. Doing so has awful (quadratic-looking)
1060 1060 performance, so we use iterators in a "windowed" way.
1061 1061
1062 1062 We walk a window of revisions in the desired order. Within the
1063 1063 window, we first walk forwards to gather data, then in the desired
1064 1064 order (usually backwards) to display it.
1065 1065
1066 1066 This function returns an iterator yielding contexts. Before
1067 1067 yielding each context, the iterator will first call the prepare
1068 1068 function on each context in the window in forward order.'''
1069 1069
1070 1070 def increasing_windows(start, end, windowsize=8, sizelimit=512):
1071 1071 if start < end:
1072 1072 while start < end:
1073 1073 yield start, min(windowsize, end-start)
1074 1074 start += windowsize
1075 1075 if windowsize < sizelimit:
1076 1076 windowsize *= 2
1077 1077 else:
1078 1078 while start > end:
1079 1079 yield start, min(windowsize, start-end-1)
1080 1080 start -= windowsize
1081 1081 if windowsize < sizelimit:
1082 1082 windowsize *= 2
1083 1083
1084 1084 follow = opts.get('follow') or opts.get('follow_first')
1085 1085
1086 1086 if not len(repo):
1087 1087 return []
1088 1088
1089 1089 if follow:
1090 1090 defrange = '%s:0' % repo['.'].rev()
1091 1091 else:
1092 1092 defrange = '-1:0'
1093 1093 revs = revrange(repo, opts['rev'] or [defrange])
1094 1094 wanted = set()
1095 1095 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1096 1096 fncache = {}
1097 1097 change = util.cachefunc(repo.changectx)
1098 1098
1099 1099 if not slowpath and not match.files():
1100 1100 # No files, no patterns. Display all revs.
1101 1101 wanted = set(revs)
1102 1102 copies = []
1103 1103
1104 1104 if not slowpath:
1105 1105 # Only files, no patterns. Check the history of each file.
1106 1106 def filerevgen(filelog, node):
1107 1107 cl_count = len(repo)
1108 1108 if node is None:
1109 1109 last = len(filelog) - 1
1110 1110 else:
1111 1111 last = filelog.rev(node)
1112 1112 for i, window in increasing_windows(last, nullrev):
1113 1113 revs = []
1114 1114 for j in xrange(i - window, i + 1):
1115 1115 n = filelog.node(j)
1116 1116 revs.append((filelog.linkrev(j),
1117 1117 follow and filelog.renamed(n)))
1118 1118 for rev in reversed(revs):
1119 1119 # only yield rev for which we have the changelog, it can
1120 1120 # happen while doing "hg log" during a pull or commit
1121 1121 if rev[0] < cl_count:
1122 1122 yield rev
1123 1123 def iterfiles():
1124 1124 for filename in match.files():
1125 1125 yield filename, None
1126 1126 for filename_node in copies:
1127 1127 yield filename_node
1128 1128 minrev, maxrev = min(revs), max(revs)
1129 1129 for file_, node in iterfiles():
1130 1130 filelog = repo.file(file_)
1131 1131 if not len(filelog):
1132 1132 if node is None:
1133 1133 # A zero count may be a directory or deleted file, so
1134 1134 # try to find matching entries on the slow path.
1135 1135 if follow:
1136 1136 raise util.Abort(_('cannot follow nonexistent file: "%s"') % file_)
1137 1137 slowpath = True
1138 1138 break
1139 1139 else:
1140 1140 continue
1141 1141 for rev, copied in filerevgen(filelog, node):
1142 1142 if rev <= maxrev:
1143 1143 if rev < minrev:
1144 1144 break
1145 1145 fncache.setdefault(rev, [])
1146 1146 fncache[rev].append(file_)
1147 1147 wanted.add(rev)
1148 1148 if follow and copied:
1149 1149 copies.append(copied)
1150 1150 if slowpath:
1151 1151 if follow:
1152 1152 raise util.Abort(_('can only follow copies/renames for explicit '
1153 1153 'filenames'))
1154 1154
1155 1155 # The slow path checks files modified in every changeset.
1156 1156 def changerevgen():
1157 1157 for i, window in increasing_windows(len(repo) - 1, nullrev):
1158 1158 for j in xrange(i - window, i + 1):
1159 1159 yield change(j)
1160 1160
1161 1161 for ctx in changerevgen():
1162 1162 matches = filter(match, ctx.files())
1163 1163 if matches:
1164 1164 fncache[ctx.rev()] = matches
1165 1165 wanted.add(ctx.rev())
1166 1166
1167 1167 class followfilter(object):
1168 1168 def __init__(self, onlyfirst=False):
1169 1169 self.startrev = nullrev
1170 1170 self.roots = []
1171 1171 self.onlyfirst = onlyfirst
1172 1172
1173 1173 def match(self, rev):
1174 1174 def realparents(rev):
1175 1175 if self.onlyfirst:
1176 1176 return repo.changelog.parentrevs(rev)[0:1]
1177 1177 else:
1178 1178 return filter(lambda x: x != nullrev,
1179 1179 repo.changelog.parentrevs(rev))
1180 1180
1181 1181 if self.startrev == nullrev:
1182 1182 self.startrev = rev
1183 1183 return True
1184 1184
1185 1185 if rev > self.startrev:
1186 1186 # forward: all descendants
1187 1187 if not self.roots:
1188 1188 self.roots.append(self.startrev)
1189 1189 for parent in realparents(rev):
1190 1190 if parent in self.roots:
1191 1191 self.roots.append(rev)
1192 1192 return True
1193 1193 else:
1194 1194 # backwards: all parents
1195 1195 if not self.roots:
1196 1196 self.roots.extend(realparents(self.startrev))
1197 1197 if rev in self.roots:
1198 1198 self.roots.remove(rev)
1199 1199 self.roots.extend(realparents(rev))
1200 1200 return True
1201 1201
1202 1202 return False
1203 1203
1204 1204 # it might be worthwhile to do this in the iterator if the rev range
1205 1205 # is descending and the prune args are all within that range
1206 1206 for rev in opts.get('prune', ()):
1207 1207 rev = repo.changelog.rev(repo.lookup(rev))
1208 1208 ff = followfilter()
1209 1209 stop = min(revs[0], revs[-1])
1210 1210 for x in xrange(rev, stop-1, -1):
1211 1211 if ff.match(x):
1212 1212 wanted.discard(x)
1213 1213
1214 1214 def iterate():
1215 1215 if follow and not match.files():
1216 1216 ff = followfilter(onlyfirst=opts.get('follow_first'))
1217 1217 def want(rev):
1218 1218 return ff.match(rev) and rev in wanted
1219 1219 else:
1220 1220 def want(rev):
1221 1221 return rev in wanted
1222 1222
1223 1223 for i, window in increasing_windows(0, len(revs)):
1224 1224 change = util.cachefunc(repo.changectx)
1225 1225 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1226 1226 for rev in sorted(nrevs):
1227 1227 fns = fncache.get(rev)
1228 1228 ctx = change(rev)
1229 1229 if not fns:
1230 1230 def fns_generator():
1231 1231 for f in ctx.files():
1232 1232 if match(f):
1233 1233 yield f
1234 1234 fns = fns_generator()
1235 1235 prepare(ctx, fns)
1236 1236 for rev in nrevs:
1237 1237 yield change(rev)
1238 1238 return iterate()
1239 1239
1240 1240 def commit(ui, repo, commitfunc, pats, opts):
1241 1241 '''commit the specified files or all outstanding changes'''
1242 1242 date = opts.get('date')
1243 1243 if date:
1244 1244 opts['date'] = util.parsedate(date)
1245 1245 message = logmessage(opts)
1246 1246
1247 1247 # extract addremove carefully -- this function can be called from a command
1248 1248 # that doesn't support addremove
1249 1249 if opts.get('addremove'):
1250 1250 addremove(repo, pats, opts)
1251 1251
1252 1252 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1253 1253
1254 1254 def commiteditor(repo, ctx, subs):
1255 1255 if ctx.description():
1256 1256 return ctx.description()
1257 1257 return commitforceeditor(repo, ctx, subs)
1258 1258
1259 1259 def commitforceeditor(repo, ctx, subs):
1260 1260 edittext = []
1261 1261 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1262 1262 if ctx.description():
1263 1263 edittext.append(ctx.description())
1264 1264 edittext.append("")
1265 1265 edittext.append("") # Empty line between message and comments.
1266 1266 edittext.append(_("HG: Enter commit message."
1267 1267 " Lines beginning with 'HG:' are removed."))
1268 1268 edittext.append(_("HG: Leave message empty to abort commit."))
1269 1269 edittext.append("HG: --")
1270 1270 edittext.append(_("HG: user: %s") % ctx.user())
1271 1271 if ctx.p2():
1272 1272 edittext.append(_("HG: branch merge"))
1273 1273 if ctx.branch():
1274 1274 edittext.append(_("HG: branch '%s'")
1275 1275 % encoding.tolocal(ctx.branch()))
1276 1276 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1277 1277 edittext.extend([_("HG: added %s") % f for f in added])
1278 1278 edittext.extend([_("HG: changed %s") % f for f in modified])
1279 1279 edittext.extend([_("HG: removed %s") % f for f in removed])
1280 1280 if not added and not modified and not removed:
1281 1281 edittext.append(_("HG: no files changed"))
1282 1282 edittext.append("")
1283 1283 # run editor in the repository root
1284 1284 olddir = os.getcwd()
1285 1285 os.chdir(repo.root)
1286 1286 text = repo.ui.edit("\n".join(edittext), ctx.user())
1287 1287 text = re.sub("(?m)^HG:.*\n", "", text)
1288 1288 os.chdir(olddir)
1289 1289
1290 1290 if not text.strip():
1291 1291 raise util.Abort(_("empty commit message"))
1292 1292
1293 1293 return text
@@ -1,894 +1,901 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # run-tests.py - Run a set of tests on Mercurial
4 4 #
5 5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2, incorporated herein by reference.
9 9
10 10 # Modifying this script is tricky because it has many modes:
11 11 # - serial (default) vs parallel (-jN, N > 1)
12 12 # - no coverage (default) vs coverage (-c, -C, -s)
13 13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 14 # - tests are a mix of shell scripts and Python scripts
15 15 #
16 16 # If you change this script, it is recommended that you ensure you
17 17 # haven't broken it by running it in various modes with a representative
18 18 # sample of test scripts. For example:
19 19 #
20 20 # 1) serial, no coverage, temp install:
21 21 # ./run-tests.py test-s*
22 22 # 2) serial, no coverage, local hg:
23 23 # ./run-tests.py --local test-s*
24 24 # 3) serial, coverage, temp install:
25 25 # ./run-tests.py -c test-s*
26 26 # 4) serial, coverage, local hg:
27 27 # ./run-tests.py -c --local test-s* # unsupported
28 28 # 5) parallel, no coverage, temp install:
29 29 # ./run-tests.py -j2 test-s*
30 30 # 6) parallel, no coverage, local hg:
31 31 # ./run-tests.py -j2 --local test-s*
32 32 # 7) parallel, coverage, temp install:
33 33 # ./run-tests.py -j2 -c test-s* # currently broken
34 34 # 8) parallel, coverage, local install:
35 35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 36 # 9) parallel, custom tmp dir:
37 37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 38 #
39 39 # (You could use any subset of the tests: test-s* happens to match
40 40 # enough that it's worth doing parallel runs, few enough that it
41 41 # completes fairly quickly, includes both shell and Python scripts, and
42 42 # includes some scripts that run daemon processes.)
43 43
44 44 import difflib
45 45 import errno
46 46 import optparse
47 47 import os
48 48 import subprocess
49 49 import shutil
50 50 import signal
51 51 import sys
52 52 import tempfile
53 53 import time
54 54
55 55 closefds = os.name == 'posix'
56 56 def Popen4(cmd, bufsize=-1):
57 57 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
58 58 close_fds=closefds,
59 59 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
60 60 stderr=subprocess.STDOUT)
61 61 p.fromchild = p.stdout
62 62 p.tochild = p.stdin
63 63 p.childerr = p.stderr
64 64 return p
65 65
66 66 # reserved exit code to skip test (used by hghave)
67 67 SKIPPED_STATUS = 80
68 68 SKIPPED_PREFIX = 'skipped: '
69 69 FAILED_PREFIX = 'hghave check failed: '
70 70 PYTHON = sys.executable
71 71
72 72 requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
73 73
74 74 defaults = {
75 75 'jobs': ('HGTEST_JOBS', 1),
76 76 'timeout': ('HGTEST_TIMEOUT', 180),
77 77 'port': ('HGTEST_PORT', 20059),
78 78 }
79 79
80 80 def parseargs():
81 81 parser = optparse.OptionParser("%prog [options] [tests]")
82 82 parser.add_option("-C", "--annotate", action="store_true",
83 83 help="output files annotated with coverage")
84 84 parser.add_option("--child", type="int",
85 85 help="run as child process, summary to given fd")
86 86 parser.add_option("-c", "--cover", action="store_true",
87 87 help="print a test coverage report")
88 88 parser.add_option("-f", "--first", action="store_true",
89 89 help="exit on the first test failure")
90 90 parser.add_option("-i", "--interactive", action="store_true",
91 91 help="prompt to accept changed output")
92 92 parser.add_option("-j", "--jobs", type="int",
93 93 help="number of jobs to run in parallel"
94 94 " (default: $%s or %d)" % defaults['jobs'])
95 95 parser.add_option("-k", "--keywords",
96 96 help="run tests matching keywords")
97 97 parser.add_option("--keep-tmpdir", action="store_true",
98 98 help="keep temporary directory after running tests")
99 99 parser.add_option("--tmpdir", type="string",
100 100 help="run tests in the given temporary directory"
101 101 " (implies --keep-tmpdir)")
102 102 parser.add_option("-d", "--debug", action="store_true",
103 103 help="debug mode: write output of test scripts to console"
104 104 " rather than capturing and diff'ing it (disables timeout)")
105 105 parser.add_option("-R", "--restart", action="store_true",
106 106 help="restart at last error")
107 107 parser.add_option("-p", "--port", type="int",
108 108 help="port on which servers should listen"
109 109 " (default: $%s or %d)" % defaults['port'])
110 110 parser.add_option("-r", "--retest", action="store_true",
111 111 help="retest failed tests")
112 112 parser.add_option("-s", "--cover_stdlib", action="store_true",
113 113 help="print a test coverage report inc. standard libraries")
114 114 parser.add_option("-S", "--noskips", action="store_true",
115 115 help="don't report skip tests verbosely")
116 116 parser.add_option("-t", "--timeout", type="int",
117 117 help="kill errant tests after TIMEOUT seconds"
118 118 " (default: $%s or %d)" % defaults['timeout'])
119 119 parser.add_option("-v", "--verbose", action="store_true",
120 120 help="output verbose messages")
121 121 parser.add_option("-n", "--nodiff", action="store_true",
122 122 help="skip showing test changes")
123 123 parser.add_option("--with-hg", type="string",
124 124 metavar="HG",
125 125 help="test using specified hg script rather than a "
126 126 "temporary installation")
127 127 parser.add_option("--local", action="store_true",
128 128 help="shortcut for --with-hg=<testdir>/../hg")
129 129 parser.add_option("--pure", action="store_true",
130 130 help="use pure Python code instead of C extensions")
131 131 parser.add_option("-3", "--py3k-warnings", action="store_true",
132 132 help="enable Py3k warnings on Python 2.6+")
133 parser.add_option("--inotify", action="store_true",
134 help="enable inotify extension when running tests")
133 135
134 136 for option, default in defaults.items():
135 137 defaults[option] = int(os.environ.get(*default))
136 138 parser.set_defaults(**defaults)
137 139 (options, args) = parser.parse_args()
138 140
139 141 if options.with_hg:
140 142 if not (os.path.isfile(options.with_hg) and
141 143 os.access(options.with_hg, os.X_OK)):
142 144 parser.error('--with-hg must specify an executable hg script')
143 145 if not os.path.basename(options.with_hg) == 'hg':
144 146 sys.stderr.write('warning: --with-hg should specify an hg script')
145 147 if options.local:
146 148 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
147 149 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
148 150 if not os.access(hgbin, os.X_OK):
149 151 parser.error('--local specified, but %r not found or not executable'
150 152 % hgbin)
151 153 options.with_hg = hgbin
152 154
153 155 options.anycoverage = (options.cover or
154 156 options.cover_stdlib or
155 157 options.annotate)
156 158
157 159 if options.anycoverage and options.with_hg:
158 160 # I'm not sure if this is a fundamental limitation or just a
159 161 # bug. But I don't want to waste people's time and energy doing
160 162 # test runs that don't give the results they want.
161 163 parser.error("sorry, coverage options do not work when --with-hg "
162 164 "or --local specified")
163 165
164 166 global vlog
165 167 if options.verbose:
166 168 if options.jobs > 1 or options.child is not None:
167 169 pid = "[%d]" % os.getpid()
168 170 else:
169 171 pid = None
170 172 def vlog(*msg):
171 173 if pid:
172 174 print pid,
173 175 for m in msg:
174 176 print m,
175 177 print
176 178 sys.stdout.flush()
177 179 else:
178 180 vlog = lambda *msg: None
179 181
180 182 if options.tmpdir:
181 183 options.tmpdir = os.path.expanduser(options.tmpdir)
182 184
183 185 if options.jobs < 1:
184 186 parser.error('--jobs must be positive')
185 187 if options.interactive and options.jobs > 1:
186 188 print '(--interactive overrides --jobs)'
187 189 options.jobs = 1
188 190 if options.interactive and options.debug:
189 191 parser.error("-i/--interactive and -d/--debug are incompatible")
190 192 if options.debug:
191 193 if options.timeout != defaults['timeout']:
192 194 sys.stderr.write(
193 195 'warning: --timeout option ignored with --debug\n')
194 196 options.timeout = 0
195 197 if options.py3k_warnings:
196 198 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
197 199 parser.error('--py3k-warnings can only be used on Python 2.6+')
198 200
199 201 return (options, args)
200 202
201 203 def rename(src, dst):
202 204 """Like os.rename(), trade atomicity and opened files friendliness
203 205 for existing destination support.
204 206 """
205 207 shutil.copy(src, dst)
206 208 os.remove(src)
207 209
208 210 def splitnewlines(text):
209 211 '''like str.splitlines, but only split on newlines.
210 212 keep line endings.'''
211 213 i = 0
212 214 lines = []
213 215 while True:
214 216 n = text.find('\n', i)
215 217 if n == -1:
216 218 last = text[i:]
217 219 if last:
218 220 lines.append(last)
219 221 return lines
220 222 lines.append(text[i:n+1])
221 223 i = n + 1
222 224
223 225 def parsehghaveoutput(lines):
224 226 '''Parse hghave log lines.
225 227 Return tuple of lists (missing, failed):
226 228 * the missing/unknown features
227 229 * the features for which existence check failed'''
228 230 missing = []
229 231 failed = []
230 232 for line in lines:
231 233 if line.startswith(SKIPPED_PREFIX):
232 234 line = line.splitlines()[0]
233 235 missing.append(line[len(SKIPPED_PREFIX):])
234 236 elif line.startswith(FAILED_PREFIX):
235 237 line = line.splitlines()[0]
236 238 failed.append(line[len(FAILED_PREFIX):])
237 239
238 240 return missing, failed
239 241
240 242 def showdiff(expected, output):
241 243 for line in difflib.unified_diff(expected, output,
242 244 "Expected output", "Test output"):
243 245 sys.stdout.write(line)
244 246
245 247 def findprogram(program):
246 248 """Search PATH for a executable program"""
247 249 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
248 250 name = os.path.join(p, program)
249 251 if os.access(name, os.X_OK):
250 252 return name
251 253 return None
252 254
253 255 def checktools():
254 256 # Before we go any further, check for pre-requisite tools
255 257 # stuff from coreutils (cat, rm, etc) are not tested
256 258 for p in requiredtools:
257 259 if os.name == 'nt':
258 260 p += '.exe'
259 261 found = findprogram(p)
260 262 if found:
261 263 vlog("# Found prerequisite", p, "at", found)
262 264 else:
263 265 print "WARNING: Did not find prerequisite tool: "+p
264 266
265 267 def cleanup(options):
266 268 if not options.keep_tmpdir:
267 269 vlog("# Cleaning up HGTMP", HGTMP)
268 270 shutil.rmtree(HGTMP, True)
269 271
270 272 def usecorrectpython():
271 273 # some tests run python interpreter. they must use same
272 274 # interpreter we use or bad things will happen.
273 275 exedir, exename = os.path.split(sys.executable)
274 276 if exename == 'python':
275 277 path = findprogram('python')
276 278 if os.path.dirname(path) == exedir:
277 279 return
278 280 vlog('# Making python executable in test path use correct Python')
279 281 mypython = os.path.join(BINDIR, 'python')
280 282 try:
281 283 os.symlink(sys.executable, mypython)
282 284 except AttributeError:
283 285 # windows fallback
284 286 shutil.copyfile(sys.executable, mypython)
285 287 shutil.copymode(sys.executable, mypython)
286 288
287 289 def installhg(options):
288 290 vlog("# Performing temporary installation of HG")
289 291 installerrs = os.path.join("tests", "install.err")
290 292 pure = options.pure and "--pure" or ""
291 293
292 294 # Run installer in hg root
293 295 script = os.path.realpath(sys.argv[0])
294 296 hgroot = os.path.dirname(os.path.dirname(script))
295 297 os.chdir(hgroot)
296 298 nohome = '--home=""'
297 299 if os.name == 'nt':
298 300 # The --home="" trick works only on OS where os.sep == '/'
299 301 # because of a distutils convert_path() fast-path. Avoid it at
300 302 # least on Windows for now, deal with .pydistutils.cfg bugs
301 303 # when they happen.
302 304 nohome = ''
303 305 cmd = ('%s setup.py %s clean --all'
304 306 ' install --force --prefix="%s" --install-lib="%s"'
305 307 ' --install-scripts="%s" %s >%s 2>&1'
306 308 % (sys.executable, pure, INST, PYTHONDIR, BINDIR, nohome,
307 309 installerrs))
308 310 vlog("# Running", cmd)
309 311 if os.system(cmd) == 0:
310 312 if not options.verbose:
311 313 os.remove(installerrs)
312 314 else:
313 315 f = open(installerrs)
314 316 for line in f:
315 317 print line,
316 318 f.close()
317 319 sys.exit(1)
318 320 os.chdir(TESTDIR)
319 321
320 322 usecorrectpython()
321 323
322 324 vlog("# Installing dummy diffstat")
323 325 f = open(os.path.join(BINDIR, 'diffstat'), 'w')
324 326 f.write('#!' + sys.executable + '\n'
325 327 'import sys\n'
326 328 'files = 0\n'
327 329 'for line in sys.stdin:\n'
328 330 ' if line.startswith("diff "):\n'
329 331 ' files += 1\n'
330 332 'sys.stdout.write("files patched: %d\\n" % files)\n')
331 333 f.close()
332 334 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
333 335
334 336 if options.py3k_warnings and not options.anycoverage:
335 337 vlog("# Updating hg command to enable Py3k Warnings switch")
336 338 f = open(os.path.join(BINDIR, 'hg'), 'r')
337 339 lines = [line.rstrip() for line in f]
338 340 lines[0] += ' -3'
339 341 f.close()
340 342 f = open(os.path.join(BINDIR, 'hg'), 'w')
341 343 for line in lines:
342 344 f.write(line + '\n')
343 345 f.close()
344 346
345 347 if options.anycoverage:
346 348 vlog("# Installing coverage wrapper")
347 349 os.environ['COVERAGE_FILE'] = COVERAGE_FILE
348 350 if os.path.exists(COVERAGE_FILE):
349 351 os.unlink(COVERAGE_FILE)
350 352 # Create a wrapper script to invoke hg via coverage.py
351 353 os.rename(os.path.join(BINDIR, "hg"), os.path.join(BINDIR, "_hg.py"))
352 354 f = open(os.path.join(BINDIR, 'hg'), 'w')
353 355 f.write('#!' + sys.executable + '\n')
354 356 f.write('import sys, os; os.execv(sys.executable, [sys.executable, '
355 357 '"%s", "-x", "-p", "%s"] + sys.argv[1:])\n' %
356 358 (os.path.join(TESTDIR, 'coverage.py'),
357 359 os.path.join(BINDIR, '_hg.py')))
358 360 f.close()
359 361 os.chmod(os.path.join(BINDIR, 'hg'), 0700)
360 362
361 363 def outputcoverage(options):
362 364
363 365 vlog('# Producing coverage report')
364 366 os.chdir(PYTHONDIR)
365 367
366 368 def covrun(*args):
367 369 start = sys.executable, os.path.join(TESTDIR, 'coverage.py')
368 370 cmd = '"%s" "%s" %s' % (start[0], start[1], ' '.join(args))
369 371 vlog('# Running: %s' % cmd)
370 372 os.system(cmd)
371 373
372 374 omit = [BINDIR, TESTDIR, PYTHONDIR]
373 375 if not options.cover_stdlib:
374 376 # Exclude as system paths (ignoring empty strings seen on win)
375 377 omit += [x for x in sys.path if x != '']
376 378 omit = ','.join(omit)
377 379
378 380 covrun('-c') # combine from parallel processes
379 381 for fn in os.listdir(TESTDIR):
380 382 if fn.startswith('.coverage.'):
381 383 os.unlink(os.path.join(TESTDIR, fn))
382 384
383 385 covrun('-i', '-r', '"--omit=%s"' % omit) # report
384 386 if options.annotate:
385 387 adir = os.path.join(TESTDIR, 'annotated')
386 388 if not os.path.isdir(adir):
387 389 os.mkdir(adir)
388 390 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
389 391
390 392 class Timeout(Exception):
391 393 pass
392 394
393 395 def alarmed(signum, frame):
394 396 raise Timeout
395 397
396 398 def run(cmd, options):
397 399 """Run command in a sub-process, capturing the output (stdout and stderr).
398 400 Return a tuple (exitcode, output). output is None in debug mode."""
399 401 # TODO: Use subprocess.Popen if we're running on Python 2.4
400 402 if options.debug:
401 403 proc = subprocess.Popen(cmd, shell=True)
402 404 ret = proc.wait()
403 405 return (ret, None)
404 406
405 407 if os.name == 'nt' or sys.platform.startswith('java'):
406 408 tochild, fromchild = os.popen4(cmd)
407 409 tochild.close()
408 410 output = fromchild.read()
409 411 ret = fromchild.close()
410 412 if ret == None:
411 413 ret = 0
412 414 else:
413 415 proc = Popen4(cmd)
414 416 try:
415 417 output = ''
416 418 proc.tochild.close()
417 419 output = proc.fromchild.read()
418 420 ret = proc.wait()
419 421 if os.WIFEXITED(ret):
420 422 ret = os.WEXITSTATUS(ret)
421 423 except Timeout:
422 424 vlog('# Process %d timed out - killing it' % proc.pid)
423 425 os.kill(proc.pid, signal.SIGTERM)
424 426 ret = proc.wait()
425 427 if ret == 0:
426 428 ret = signal.SIGTERM << 8
427 429 output += ("\n### Abort: timeout after %d seconds.\n"
428 430 % options.timeout)
429 431 return ret, splitnewlines(output)
430 432
431 433 def runone(options, test, skips, fails):
432 434 '''tristate output:
433 435 None -> skipped
434 436 True -> passed
435 437 False -> failed'''
436 438
437 439 def skip(msg):
438 440 if not options.verbose:
439 441 skips.append((test, msg))
440 442 else:
441 443 print "\nSkipping %s: %s" % (test, msg)
442 444 return None
443 445
444 446 def fail(msg):
445 447 fails.append((test, msg))
446 448 if not options.nodiff:
447 449 print "\nERROR: %s %s" % (test, msg)
448 450 return None
449 451
450 452 vlog("# Test", test)
451 453
452 454 # create a fresh hgrc
453 455 hgrc = open(HGRCPATH, 'w+')
454 456 hgrc.write('[ui]\n')
455 457 hgrc.write('slash = True\n')
456 458 hgrc.write('[defaults]\n')
457 459 hgrc.write('backout = -d "0 0"\n')
458 460 hgrc.write('commit = -d "0 0"\n')
459 461 hgrc.write('tag = -d "0 0"\n')
462 if options.inotify:
463 hgrc.write('[extensions]\n')
464 hgrc.write('inotify=\n')
465 hgrc.write('[inotify]\n')
466 hgrc.write('pidfile=%s\n' % DAEMON_PIDS)
460 467 hgrc.close()
461 468
462 469 err = os.path.join(TESTDIR, test+".err")
463 470 ref = os.path.join(TESTDIR, test+".out")
464 471 testpath = os.path.join(TESTDIR, test)
465 472
466 473 if os.path.exists(err):
467 474 os.remove(err) # Remove any previous output files
468 475
469 476 # Make a tmp subdirectory to work in
470 477 tmpd = os.path.join(HGTMP, test)
471 478 os.mkdir(tmpd)
472 479 os.chdir(tmpd)
473 480
474 481 try:
475 482 tf = open(testpath)
476 483 firstline = tf.readline().rstrip()
477 484 tf.close()
478 485 except:
479 486 firstline = ''
480 487 lctest = test.lower()
481 488
482 489 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
483 490 py3kswitch = options.py3k_warnings and ' -3' or ''
484 491 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, testpath)
485 492 elif lctest.endswith('.bat'):
486 493 # do not run batch scripts on non-windows
487 494 if os.name != 'nt':
488 495 return skip("batch script")
489 496 # To reliably get the error code from batch files on WinXP,
490 497 # the "cmd /c call" prefix is needed. Grrr
491 498 cmd = 'cmd /c call "%s"' % testpath
492 499 else:
493 500 # do not run shell scripts on windows
494 501 if os.name == 'nt':
495 502 return skip("shell script")
496 503 # do not try to run non-executable programs
497 504 if not os.path.exists(testpath):
498 505 return fail("does not exist")
499 506 elif not os.access(testpath, os.X_OK):
500 507 return skip("not executable")
501 508 cmd = '"%s"' % testpath
502 509
503 510 if options.timeout > 0:
504 511 signal.alarm(options.timeout)
505 512
506 513 vlog("# Running", cmd)
507 514 ret, out = run(cmd, options)
508 515 vlog("# Ret was:", ret)
509 516
510 517 if options.timeout > 0:
511 518 signal.alarm(0)
512 519
513 520 mark = '.'
514 521
515 522 skipped = (ret == SKIPPED_STATUS)
516 523 # If we're not in --debug mode and reference output file exists,
517 524 # check test output against it.
518 525 if options.debug:
519 526 refout = None # to match out == None
520 527 elif os.path.exists(ref):
521 528 f = open(ref, "r")
522 529 refout = splitnewlines(f.read())
523 530 f.close()
524 531 else:
525 532 refout = []
526 533
527 534 if skipped:
528 535 mark = 's'
529 536 if out is None: # debug mode: nothing to parse
530 537 missing = ['unknown']
531 538 failed = None
532 539 else:
533 540 missing, failed = parsehghaveoutput(out)
534 541 if not missing:
535 542 missing = ['irrelevant']
536 543 if failed:
537 544 fail("hghave failed checking for %s" % failed[-1])
538 545 skipped = False
539 546 else:
540 547 skip(missing[-1])
541 548 elif out != refout:
542 549 mark = '!'
543 550 if ret:
544 551 fail("output changed and returned error code %d" % ret)
545 552 else:
546 553 fail("output changed")
547 554 if not options.nodiff:
548 555 showdiff(refout, out)
549 556 ret = 1
550 557 elif ret:
551 558 mark = '!'
552 559 fail("returned error code %d" % ret)
553 560
554 561 if not options.verbose:
555 562 sys.stdout.write(mark)
556 563 sys.stdout.flush()
557 564
558 565 if ret != 0 and not skipped and not options.debug:
559 566 # Save errors to a file for diagnosis
560 567 f = open(err, "wb")
561 568 for line in out:
562 569 f.write(line)
563 570 f.close()
564 571
565 572 # Kill off any leftover daemon processes
566 573 try:
567 574 fp = open(DAEMON_PIDS)
568 575 for line in fp:
569 576 try:
570 577 pid = int(line)
571 578 except ValueError:
572 579 continue
573 580 try:
574 581 os.kill(pid, 0)
575 582 vlog('# Killing daemon process %d' % pid)
576 583 os.kill(pid, signal.SIGTERM)
577 584 time.sleep(0.25)
578 585 os.kill(pid, 0)
579 586 vlog('# Daemon process %d is stuck - really killing it' % pid)
580 587 os.kill(pid, signal.SIGKILL)
581 588 except OSError, err:
582 589 if err.errno != errno.ESRCH:
583 590 raise
584 591 fp.close()
585 592 os.unlink(DAEMON_PIDS)
586 593 except IOError:
587 594 pass
588 595
589 596 os.chdir(TESTDIR)
590 597 if not options.keep_tmpdir:
591 598 shutil.rmtree(tmpd, True)
592 599 if skipped:
593 600 return None
594 601 return ret == 0
595 602
596 603 _hgpath = None
597 604
598 605 def _gethgpath():
599 606 """Return the path to the mercurial package that is actually found by
600 607 the current Python interpreter."""
601 608 global _hgpath
602 609 if _hgpath is not None:
603 610 return _hgpath
604 611
605 612 cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
606 613 pipe = os.popen(cmd % PYTHON)
607 614 try:
608 615 _hgpath = pipe.read().strip()
609 616 finally:
610 617 pipe.close()
611 618 return _hgpath
612 619
613 620 def _checkhglib(verb):
614 621 """Ensure that the 'mercurial' package imported by python is
615 622 the one we expect it to be. If not, print a warning to stderr."""
616 623 expecthg = os.path.join(PYTHONDIR, 'mercurial')
617 624 actualhg = _gethgpath()
618 625 if actualhg != expecthg:
619 626 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
620 627 ' (expected %s)\n'
621 628 % (verb, actualhg, expecthg))
622 629
623 630 def runchildren(options, tests):
624 631 if INST:
625 632 installhg(options)
626 633 _checkhglib("Testing")
627 634
628 635 optcopy = dict(options.__dict__)
629 636 optcopy['jobs'] = 1
630 637 if optcopy['with_hg'] is None:
631 638 optcopy['with_hg'] = os.path.join(BINDIR, "hg")
632 639 opts = []
633 640 for opt, value in optcopy.iteritems():
634 641 name = '--' + opt.replace('_', '-')
635 642 if value is True:
636 643 opts.append(name)
637 644 elif value is not None:
638 645 opts.append(name + '=' + str(value))
639 646
640 647 tests.reverse()
641 648 jobs = [[] for j in xrange(options.jobs)]
642 649 while tests:
643 650 for job in jobs:
644 651 if not tests: break
645 652 job.append(tests.pop())
646 653 fps = {}
647 654 for j, job in enumerate(jobs):
648 655 if not job:
649 656 continue
650 657 rfd, wfd = os.pipe()
651 658 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
652 659 childtmp = os.path.join(HGTMP, 'child%d' % j)
653 660 childopts += ['--tmpdir', childtmp]
654 661 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
655 662 vlog(' '.join(cmdline))
656 663 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
657 664 os.close(wfd)
658 665 failures = 0
659 666 tested, skipped, failed = 0, 0, 0
660 667 skips = []
661 668 fails = []
662 669 while fps:
663 670 pid, status = os.wait()
664 671 fp = fps.pop(pid)
665 672 l = fp.read().splitlines()
666 673 test, skip, fail = map(int, l[:3])
667 674 split = -fail or len(l)
668 675 for s in l[3:split]:
669 676 skips.append(s.split(" ", 1))
670 677 for s in l[split:]:
671 678 fails.append(s.split(" ", 1))
672 679 tested += test
673 680 skipped += skip
674 681 failed += fail
675 682 vlog('pid %d exited, status %d' % (pid, status))
676 683 failures |= status
677 684 print
678 685 if not options.noskips:
679 686 for s in skips:
680 687 print "Skipped %s: %s" % (s[0], s[1])
681 688 for s in fails:
682 689 print "Failed %s: %s" % (s[0], s[1])
683 690
684 691 _checkhglib("Tested")
685 692 print "# Ran %d tests, %d skipped, %d failed." % (
686 693 tested, skipped, failed)
687 694 sys.exit(failures != 0)
688 695
689 696 def runtests(options, tests):
690 697 global DAEMON_PIDS, HGRCPATH
691 698 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
692 699 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
693 700
694 701 try:
695 702 if INST:
696 703 installhg(options)
697 704 _checkhglib("Testing")
698 705
699 706 if options.timeout > 0:
700 707 try:
701 708 signal.signal(signal.SIGALRM, alarmed)
702 709 vlog('# Running each test with %d second timeout' %
703 710 options.timeout)
704 711 except AttributeError:
705 712 print 'WARNING: cannot run tests with timeouts'
706 713 options.timeout = 0
707 714
708 715 tested = 0
709 716 failed = 0
710 717 skipped = 0
711 718
712 719 if options.restart:
713 720 orig = list(tests)
714 721 while tests:
715 722 if os.path.exists(tests[0] + ".err"):
716 723 break
717 724 tests.pop(0)
718 725 if not tests:
719 726 print "running all tests"
720 727 tests = orig
721 728
722 729 skips = []
723 730 fails = []
724 731
725 732 for test in tests:
726 733 if options.retest and not os.path.exists(test + ".err"):
727 734 skipped += 1
728 735 continue
729 736
730 737 if options.keywords:
731 738 t = open(test).read().lower() + test.lower()
732 739 for k in options.keywords.lower().split():
733 740 if k in t:
734 741 break
735 742 else:
736 743 skipped +=1
737 744 continue
738 745
739 746 ret = runone(options, test, skips, fails)
740 747 if ret is None:
741 748 skipped += 1
742 749 elif not ret:
743 750 if options.interactive:
744 751 print "Accept this change? [n] ",
745 752 answer = sys.stdin.readline().strip()
746 753 if answer.lower() in "y yes".split():
747 754 rename(test + ".err", test + ".out")
748 755 tested += 1
749 756 fails.pop()
750 757 continue
751 758 failed += 1
752 759 if options.first:
753 760 break
754 761 tested += 1
755 762
756 763 if options.child:
757 764 fp = os.fdopen(options.child, 'w')
758 765 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
759 766 for s in skips:
760 767 fp.write("%s %s\n" % s)
761 768 for s in fails:
762 769 fp.write("%s %s\n" % s)
763 770 fp.close()
764 771 else:
765 772 print
766 773 for s in skips:
767 774 print "Skipped %s: %s" % s
768 775 for s in fails:
769 776 print "Failed %s: %s" % s
770 777 _checkhglib("Tested")
771 778 print "# Ran %d tests, %d skipped, %d failed." % (
772 779 tested, skipped, failed)
773 780
774 781 if options.anycoverage:
775 782 outputcoverage(options)
776 783 except KeyboardInterrupt:
777 784 failed = True
778 785 print "\ninterrupted!"
779 786
780 787 if failed:
781 788 sys.exit(1)
782 789
783 790 def main():
784 791 (options, args) = parseargs()
785 792 if not options.child:
786 793 os.umask(022)
787 794
788 795 checktools()
789 796
790 797 # Reset some environment variables to well-known values so that
791 798 # the tests produce repeatable output.
792 799 os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C'
793 800 os.environ['TZ'] = 'GMT'
794 801 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
795 802 os.environ['CDPATH'] = ''
796 803 os.environ['COLUMNS'] = '80'
797 804
798 805 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
799 806 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
800 807 if options.tmpdir:
801 808 options.keep_tmpdir = True
802 809 tmpdir = options.tmpdir
803 810 if os.path.exists(tmpdir):
804 811 # Meaning of tmpdir has changed since 1.3: we used to create
805 812 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
806 813 # tmpdir already exists.
807 814 sys.exit("error: temp dir %r already exists" % tmpdir)
808 815
809 816 # Automatically removing tmpdir sounds convenient, but could
810 817 # really annoy anyone in the habit of using "--tmpdir=/tmp"
811 818 # or "--tmpdir=$HOME".
812 819 #vlog("# Removing temp dir", tmpdir)
813 820 #shutil.rmtree(tmpdir)
814 821 os.makedirs(tmpdir)
815 822 else:
816 823 tmpdir = tempfile.mkdtemp('', 'hgtests.')
817 824 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
818 825 DAEMON_PIDS = None
819 826 HGRCPATH = None
820 827
821 828 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
822 829 os.environ["HGMERGE"] = "internal:merge"
823 830 os.environ["HGUSER"] = "test"
824 831 os.environ["HGENCODING"] = "ascii"
825 832 os.environ["HGENCODINGMODE"] = "strict"
826 833 os.environ["HGPORT"] = str(options.port)
827 834 os.environ["HGPORT1"] = str(options.port + 1)
828 835 os.environ["HGPORT2"] = str(options.port + 2)
829 836
830 837 if options.with_hg:
831 838 INST = None
832 839 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
833 840
834 841 # This looks redundant with how Python initializes sys.path from
835 842 # the location of the script being executed. Needed because the
836 843 # "hg" specified by --with-hg is not the only Python script
837 844 # executed in the test suite that needs to import 'mercurial'
838 845 # ... which means it's not really redundant at all.
839 846 PYTHONDIR = BINDIR
840 847 else:
841 848 INST = os.path.join(HGTMP, "install")
842 849 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
843 850 PYTHONDIR = os.path.join(INST, "lib", "python")
844 851
845 852 os.environ["BINDIR"] = BINDIR
846 853 os.environ["PYTHON"] = PYTHON
847 854
848 855 if not options.child:
849 856 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
850 857 os.environ["PATH"] = os.pathsep.join(path)
851 858
852 859 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
853 860 # can run .../tests/run-tests.py test-foo where test-foo
854 861 # adds an extension to HGRC
855 862 pypath = [PYTHONDIR, TESTDIR]
856 863 # We have to augment PYTHONPATH, rather than simply replacing
857 864 # it, in case external libraries are only available via current
858 865 # PYTHONPATH. (In particular, the Subversion bindings on OS X
859 866 # are in /opt/subversion.)
860 867 oldpypath = os.environ.get('PYTHONPATH')
861 868 if oldpypath:
862 869 pypath.append(oldpypath)
863 870 os.environ['PYTHONPATH'] = os.pathsep.join(pypath)
864 871
865 872 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
866 873
867 874 if len(args) == 0:
868 875 args = os.listdir(".")
869 876 args.sort()
870 877
871 878 tests = []
872 879 for test in args:
873 880 if (test.startswith("test-") and '~' not in test and
874 881 ('.' not in test or test.endswith('.py') or
875 882 test.endswith('.bat'))):
876 883 tests.append(test)
877 884 if not tests:
878 885 print "# Ran 0 tests, 0 skipped, 0 failed."
879 886 return
880 887
881 888 vlog("# Using TESTDIR", TESTDIR)
882 889 vlog("# Using HGTMP", HGTMP)
883 890 vlog("# Using PATH", os.environ["PATH"])
884 891 vlog("# Using PYTHONPATH", os.environ["PYTHONPATH"])
885 892
886 893 try:
887 894 if len(tests) > 1 and options.jobs > 1:
888 895 runchildren(options, tests)
889 896 else:
890 897 runtests(options, tests)
891 898 finally:
892 899 cleanup(options)
893 900
894 901 main()
General Comments 0
You need to be logged in to leave comments. Login now