##// END OF EJS Templates
shelve: add a shelve extension to save/restore working changes...
David Soria Parra -
r19854:49d4919d default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (607 lines changed) Show them Hide them
@@ -0,0 +1,607
1 # shelve.py - save/restore working directory state
2 #
3 # Copyright 2013 Facebook, Inc.
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 """save and restore changes to the working directory
9
10 The "hg shelve" command saves changes made to the working directory
11 and reverts those changes, resetting the working directory to a clean
12 state.
13
14 Later on, the "hg unshelve" command restores the changes saved by "hg
15 shelve". Changes can be restored even after updating to a different
16 parent, in which case Mercurial's merge machinery will resolve any
17 conflicts if necessary.
18
19 You can have more than one shelved change outstanding at a time; each
20 shelved change has a distinct name. For details, see the help for "hg
21 shelve".
22 """
23
24 try:
25 import cPickle as pickle
26 pickle.dump # import now
27 except ImportError:
28 import pickle
29 from mercurial.i18n import _
30 from mercurial.node import nullid
31 from mercurial import changegroup, cmdutil, scmutil, phases
32 from mercurial import error, hg, mdiff, merge, patch, repair, util
33 from mercurial import templatefilters
34 from mercurial import lock as lockmod
35 import errno
36
37 cmdtable = {}
38 command = cmdutil.command(cmdtable)
39 testedwith = 'internal'
40
41 class shelvedfile(object):
42 """Handles common functions on shelve files (.hg/.files/.patch) using
43 the vfs layer"""
44 def __init__(self, repo, name, filetype=None):
45 self.repo = repo
46 self.name = name
47 self.vfs = scmutil.vfs(repo.join('shelved'))
48 if filetype:
49 self.fname = name + '.' + filetype
50 else:
51 self.fname = name
52
53 def exists(self):
54 return self.vfs.exists(self.fname)
55
56 def filename(self):
57 return self.vfs.join(self.fname)
58
59 def unlink(self):
60 util.unlink(self.filename())
61
62 def stat(self):
63 return self.vfs.stat(self.fname)
64
65 def opener(self, mode='rb'):
66 try:
67 return self.vfs(self.fname, mode)
68 except IOError, err:
69 if err.errno != errno.ENOENT:
70 raise
71 if mode[0] in 'wa':
72 try:
73 self.vfs.mkdir()
74 return self.vfs(self.fname, mode)
75 except IOError, err:
76 if err.errno != errno.EEXIST:
77 raise
78 elif mode[0] == 'r':
79 raise util.Abort(_("shelved change '%s' not found") %
80 self.name)
81
82 class shelvedstate(object):
83 """Handles saving and restoring a shelved state. Ensures that different
84 versions of a shelved state are possible and handles them appropriate"""
85 _version = 1
86 _filename = 'shelvedstate'
87
88 @classmethod
89 def load(cls, repo):
90 fp = repo.opener(cls._filename)
91 (version, name, parents, stripnodes) = pickle.load(fp)
92
93 if version != cls._version:
94 raise util.Abort(_('this version of shelve is incompatible '
95 'with the version used in this repo'))
96
97 obj = cls()
98 obj.name = name
99 obj.parents = parents
100 obj.stripnodes = stripnodes
101
102 return obj
103
104 @classmethod
105 def save(cls, repo, name, stripnodes):
106 fp = repo.opener(cls._filename, 'wb')
107 pickle.dump((cls._version, name,
108 repo.dirstate.parents(),
109 stripnodes), fp)
110 fp.close()
111
112 @staticmethod
113 def clear(repo):
114 util.unlinkpath(repo.join('shelvedstate'), ignoremissing=True)
115
116 def createcmd(ui, repo, pats, opts):
117 def publicancestors(ctx):
118 """Compute the heads of the public ancestors of a commit.
119
120 Much faster than the revset heads(ancestors(ctx) - draft())"""
121 seen = set()
122 visit = util.deque()
123 visit.append(ctx)
124 while visit:
125 ctx = visit.popleft()
126 for parent in ctx.parents():
127 rev = parent.rev()
128 if rev not in seen:
129 seen.add(rev)
130 if parent.mutable():
131 visit.append(parent)
132 else:
133 yield parent.node()
134
135 wctx = repo[None]
136 parents = wctx.parents()
137 if len(parents) > 1:
138 raise util.Abort(_('cannot shelve while merging'))
139 parent = parents[0]
140
141 # we never need the user, so we use a generic user for all shelve operations
142 user = 'shelve@localhost'
143 label = repo._bookmarkcurrent or parent.branch() or 'default'
144
145 # slashes aren't allowed in filenames, therefore we rename it
146 origlabel, label = label, label.replace('/', '_')
147
148 def gennames():
149 yield label
150 for i in xrange(1, 100):
151 yield '%s-%02d' % (label, i)
152
153 shelvedfiles = []
154
155 def commitfunc(ui, repo, message, match, opts):
156 # check modified, added, removed, deleted only
157 for flist in repo.status(match=match)[:4]:
158 shelvedfiles.extend(flist)
159 return repo.commit(message, user, opts.get('date'), match)
160
161 if parent.node() != nullid:
162 desc = parent.description().split('\n', 1)[0]
163 desc = _('shelved from %s (%s): %s') % (label, str(parent)[:8], desc)
164 else:
165 desc = '(empty repository)'
166
167 if not opts['message']:
168 opts['message'] = desc
169
170 name = opts['name']
171
172 wlock = lock = tr = None
173 try:
174 wlock = repo.wlock()
175 lock = repo.lock()
176
177 # use an uncommited transaction to generate the bundle to avoid
178 # pull races. ensure we don't print the abort message to stderr.
179 tr = repo.transaction('commit', report=lambda x: None)
180
181 if name:
182 if shelvedfile(repo, name, 'hg').exists():
183 raise util.Abort(_("a shelved change named '%s' already exists")
184 % name)
185 else:
186 for n in gennames():
187 if not shelvedfile(repo, n, 'hg').exists():
188 name = n
189 break
190 else:
191 raise util.Abort(_("too many shelved changes named '%s'") %
192 label)
193
194 # ensure we are not creating a subdirectory or a hidden file
195 if '/' in name or '\\' in name:
196 raise util.Abort(_('shelved change names may not contain slashes'))
197 if name.startswith('.'):
198 raise util.Abort(_("shelved change names may not start with '.'"))
199
200 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
201
202 if not node:
203 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
204 if stat[3]:
205 ui.status(_("nothing changed (%d missing files, see "
206 "'hg status')\n") % len(stat[3]))
207 else:
208 ui.status(_("nothing changed\n"))
209 return 1
210
211 phases.retractboundary(repo, phases.secret, [node])
212
213 fp = shelvedfile(repo, name, 'files').opener('wb')
214 fp.write('\0'.join(shelvedfiles))
215
216 bases = list(publicancestors(repo[node]))
217 cg = repo.changegroupsubset(bases, [node], 'shelve')
218 changegroup.writebundle(cg, shelvedfile(repo, name, 'hg').filename(),
219 'HG10UN')
220 cmdutil.export(repo, [node],
221 fp=shelvedfile(repo, name, 'patch').opener('wb'),
222 opts=mdiff.diffopts(git=True))
223
224 if ui.formatted():
225 desc = util.ellipsis(desc, ui.termwidth())
226 ui.status(desc + '\n')
227 ui.status(_('shelved as %s\n') % name)
228 hg.update(repo, parent.node())
229 finally:
230 if tr:
231 tr.abort()
232 lockmod.release(lock, wlock)
233
234 def cleanupcmd(ui, repo):
235 wlock = None
236 try:
237 wlock = repo.wlock()
238 for (name, _) in repo.vfs.readdir('shelved'):
239 suffix = name.rsplit('.', 1)[-1]
240 if suffix in ('hg', 'files', 'patch'):
241 shelvedfile(repo, name).unlink()
242 finally:
243 lockmod.release(wlock)
244
245 def deletecmd(ui, repo, pats):
246 if not pats:
247 raise util.Abort(_('no shelved changes specified!'))
248 wlock = None
249 try:
250 wlock = repo.wlock()
251 try:
252 for name in pats:
253 for suffix in 'hg files patch'.split():
254 shelvedfile(repo, name, suffix).unlink()
255 except OSError, err:
256 if err.errno != errno.ENOENT:
257 raise
258 raise util.Abort(_("shelved change '%s' not found") % name)
259 finally:
260 lockmod.release(wlock)
261
262 def listshelves(repo):
263 try:
264 names = repo.vfs.readdir('shelved')
265 except OSError, err:
266 if err.errno != errno.ENOENT:
267 raise
268 return []
269 info = []
270 for (name, _) in names:
271 pfx, sfx = name.rsplit('.', 1)
272 if not pfx or sfx != 'patch':
273 continue
274 st = shelvedfile(repo, name).stat()
275 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
276 return sorted(info, reverse=True)
277
278 def listcmd(ui, repo, pats, opts):
279 pats = set(pats)
280 width = 80
281 if not ui.plain():
282 width = ui.termwidth()
283 namelabel = 'shelve.newest'
284 for mtime, name in listshelves(repo):
285 sname = util.split(name)[1]
286 if pats and sname not in pats:
287 continue
288 ui.write(sname, label=namelabel)
289 namelabel = 'shelve.name'
290 if ui.quiet:
291 ui.write('\n')
292 continue
293 ui.write(' ' * (16 - len(sname)))
294 used = 16
295 age = '[%s]' % templatefilters.age(util.makedate(mtime))
296 ui.write(age, label='shelve.age')
297 ui.write(' ' * (18 - len(age)))
298 used += 18
299 fp = open(name + '.patch', 'rb')
300 try:
301 while True:
302 line = fp.readline()
303 if not line:
304 break
305 if not line.startswith('#'):
306 desc = line.rstrip()
307 if ui.formatted():
308 desc = util.ellipsis(desc, width - used)
309 ui.write(desc)
310 break
311 ui.write('\n')
312 if not (opts['patch'] or opts['stat']):
313 continue
314 difflines = fp.readlines()
315 if opts['patch']:
316 for chunk, label in patch.difflabel(iter, difflines):
317 ui.write(chunk, label=label)
318 if opts['stat']:
319 for chunk, label in patch.diffstatui(difflines, width=width,
320 git=True):
321 ui.write(chunk, label=label)
322 finally:
323 fp.close()
324
325 def readshelvedfiles(repo, basename):
326 fp = shelvedfile(repo, basename, 'files').opener()
327 return fp.read().split('\0')
328
329 def checkparents(repo, state):
330 if state.parents != repo.dirstate.parents():
331 raise util.Abort(_('working directory parents do not match unshelve '
332 'state'))
333
334 def unshelveabort(ui, repo, state, opts):
335 wlock = repo.wlock()
336 lock = None
337 try:
338 checkparents(repo, state)
339 lock = repo.lock()
340 merge.mergestate(repo).reset()
341 if opts['keep']:
342 repo.setparents(repo.dirstate.parents()[0])
343 else:
344 revertfiles = readshelvedfiles(repo, state.name)
345 wctx = repo.parents()[0]
346 cmdutil.revert(ui, repo, wctx, [wctx.node(), nullid],
347 *revertfiles, no_backup=True)
348 # fix up the weird dirstate states the merge left behind
349 mf = wctx.manifest()
350 dirstate = repo.dirstate
351 for f in revertfiles:
352 if f in mf:
353 dirstate.normallookup(f)
354 else:
355 dirstate.drop(f)
356 dirstate._pl = (wctx.node(), nullid)
357 dirstate._dirty = True
358 repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
359 shelvedstate.clear(repo)
360 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
361 finally:
362 lockmod.release(lock, wlock)
363
364 def unshelvecleanup(ui, repo, name, opts):
365 if not opts['keep']:
366 for filetype in 'hg files patch'.split():
367 shelvedfile(repo, name, filetype).unlink()
368
369 def finishmerge(ui, repo, ms, stripnodes, name, opts):
370 # Reset the working dir so it's no longer in a merge state.
371 dirstate = repo.dirstate
372 for f in ms:
373 if dirstate[f] == 'm':
374 dirstate.normallookup(f)
375 dirstate._pl = (dirstate._pl[0], nullid)
376 dirstate._dirty = dirstate._dirtypl = True
377 shelvedstate.clear(repo)
378
379 def unshelvecontinue(ui, repo, state, opts):
380 # We're finishing off a merge. First parent is our original
381 # parent, second is the temporary "fake" commit we're unshelving.
382 wlock = repo.wlock()
383 lock = None
384 try:
385 checkparents(repo, state)
386 ms = merge.mergestate(repo)
387 if [f for f in ms if ms[f] == 'u']:
388 raise util.Abort(
389 _("unresolved conflicts, can't continue"),
390 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
391 finishmerge(ui, repo, ms, state.stripnodes, state.name, opts)
392 lock = repo.lock()
393 repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
394 unshelvecleanup(ui, repo, state.name, opts)
395 ui.status(_("unshelve of '%s' complete\n") % state.name)
396 finally:
397 lockmod.release(lock, wlock)
398
399 @command('unshelve',
400 [('a', 'abort', None,
401 _('abort an incomplete unshelve operation')),
402 ('c', 'continue', None,
403 _('continue an incomplete unshelve operation')),
404 ('', 'keep', None,
405 _('keep shelve after unshelving'))],
406 _('hg unshelve [SHELVED]'))
407 def unshelve(ui, repo, *shelved, **opts):
408 """restore a shelved change to the working directory
409
410 This command accepts an optional name of a shelved change to
411 restore. If none is given, the most recent shelved change is used.
412
413 If a shelved change is applied successfully, the bundle that
414 contains the shelved changes is deleted afterwards.
415
416 Since you can restore a shelved change on top of an arbitrary
417 commit, it is possible that unshelving will result in a conflict
418 between your changes and the commits you are unshelving onto. If
419 this occurs, you must resolve the conflict, then use
420 ``--continue`` to complete the unshelve operation. (The bundle
421 will not be deleted until you successfully complete the unshelve.)
422
423 (Alternatively, you can use ``--abort`` to abandon an unshelve
424 that causes a conflict. This reverts the unshelved changes, and
425 does not delete the bundle.)
426 """
427 abortf = opts['abort']
428 continuef = opts['continue']
429 if not abortf and not continuef:
430 cmdutil.checkunfinished(repo)
431
432 if abortf or continuef:
433 if abortf and continuef:
434 raise util.Abort(_('cannot use both abort and continue'))
435 if shelved:
436 raise util.Abort(_('cannot combine abort/continue with '
437 'naming a shelved change'))
438
439 try:
440 state = shelvedstate.load(repo)
441 except IOError, err:
442 if err.errno != errno.ENOENT:
443 raise
444 raise util.Abort(_('no unshelve operation underway'))
445
446 if abortf:
447 return unshelveabort(ui, repo, state, opts)
448 elif continuef:
449 return unshelvecontinue(ui, repo, state, opts)
450 elif len(shelved) > 1:
451 raise util.Abort(_('can only unshelve one change at a time'))
452 elif not shelved:
453 shelved = listshelves(repo)
454 if not shelved:
455 raise util.Abort(_('no shelved changes to apply!'))
456 basename = util.split(shelved[0][1])[1]
457 ui.status(_("unshelving change '%s'\n") % basename)
458 else:
459 basename = shelved[0]
460
461 shelvedfiles = readshelvedfiles(repo, basename)
462
463 m, a, r, d = repo.status()[:4]
464 unsafe = set(m + a + r + d).intersection(shelvedfiles)
465 if unsafe:
466 ui.warn(_('the following shelved files have been modified:\n'))
467 for f in sorted(unsafe):
468 ui.warn(' %s\n' % f)
469 ui.warn(_('you must commit, revert, or shelve your changes before you '
470 'can proceed\n'))
471 raise util.Abort(_('cannot unshelve due to local changes\n'))
472
473 wlock = lock = tr = None
474 try:
475 lock = repo.lock()
476
477 tr = repo.transaction('unshelve', report=lambda x: None)
478 oldtiprev = len(repo)
479 try:
480 fp = shelvedfile(repo, basename, 'hg').opener()
481 gen = changegroup.readbundle(fp, fp.name)
482 repo.addchangegroup(gen, 'unshelve', 'bundle:' + fp.name)
483 nodes = [ctx.node() for ctx in repo.set('%d:', oldtiprev)]
484 phases.retractboundary(repo, phases.secret, nodes)
485 tr.close()
486 finally:
487 fp.close()
488
489 tip = repo['tip']
490 wctx = repo['.']
491 ancestor = tip.ancestor(wctx)
492
493 wlock = repo.wlock()
494
495 if ancestor.node() != wctx.node():
496 conflicts = hg.merge(repo, tip.node(), force=True, remind=False)
497 ms = merge.mergestate(repo)
498 stripnodes = [repo.changelog.node(rev)
499 for rev in xrange(oldtiprev, len(repo))]
500 if conflicts:
501 shelvedstate.save(repo, basename, stripnodes)
502 # Fix up the dirstate entries of files from the second
503 # parent as if we were not merging, except for those
504 # with unresolved conflicts.
505 parents = repo.parents()
506 revertfiles = set(parents[1].files()).difference(ms)
507 cmdutil.revert(ui, repo, parents[1],
508 (parents[0].node(), nullid),
509 *revertfiles, no_backup=True)
510 raise error.InterventionRequired(
511 _("unresolved conflicts (see 'hg resolve', then "
512 "'hg unshelve --continue')"))
513 finishmerge(ui, repo, ms, stripnodes, basename, opts)
514 else:
515 parent = tip.parents()[0]
516 hg.update(repo, parent.node())
517 cmdutil.revert(ui, repo, tip, repo.dirstate.parents(), *tip.files(),
518 no_backup=True)
519
520 prevquiet = ui.quiet
521 ui.quiet = True
522 try:
523 repo.rollback(force=True)
524 finally:
525 ui.quiet = prevquiet
526
527 unshelvecleanup(ui, repo, basename, opts)
528 finally:
529 if tr:
530 tr.release()
531 lockmod.release(lock, wlock)
532
533 @command('shelve',
534 [('A', 'addremove', None,
535 _('mark new/missing files as added/removed before shelving')),
536 ('', 'cleanup', None,
537 _('delete all shelved changes')),
538 ('', 'date', '',
539 _('shelve with the specified commit date'), _('DATE')),
540 ('d', 'delete', None,
541 _('delete the named shelved change(s)')),
542 ('l', 'list', None,
543 _('list current shelves')),
544 ('m', 'message', '',
545 _('use text as shelve message'), _('TEXT')),
546 ('n', 'name', '',
547 _('use the given name for the shelved commit'), _('NAME')),
548 ('p', 'patch', None,
549 _('show patch')),
550 ('', 'stat', None,
551 _('output diffstat-style summary of changes'))],
552 _('hg shelve'))
553 def shelvecmd(ui, repo, *pats, **opts):
554 '''save and set aside changes from the working directory
555
556 Shelving takes files that "hg status" reports as not clean, saves
557 the modifications to a bundle (a shelved change), and reverts the
558 files so that their state in the working directory becomes clean.
559
560 To restore these changes to the working directory, using "hg
561 unshelve"; this will work even if you switch to a different
562 commit.
563
564 When no files are specified, "hg shelve" saves all not-clean
565 files. If specific files or directories are named, only changes to
566 those files are shelved.
567
568 Each shelved change has a name that makes it easier to find later.
569 The name of a shelved change defaults to being based on the active
570 bookmark, or if there is no active bookmark, the current named
571 branch. To specify a different name, use ``--name``.
572
573 To see a list of existing shelved changes, use the ``--list``
574 option. For each shelved change, this will print its name, age,
575 and description; use ``--patch`` or ``--stat`` for more details.
576
577 To delete specific shelved changes, use ``--delete``. To delete
578 all shelved changes, use ``--cleanup``.
579 '''
580 cmdutil.checkunfinished(repo)
581
582 def checkopt(opt, incompatible):
583 if opts[opt]:
584 for i in incompatible.split():
585 if opts[i]:
586 raise util.Abort(_("options '--%s' and '--%s' may not be "
587 "used together") % (opt, i))
588 return True
589 if checkopt('cleanup', 'addremove delete list message name patch stat'):
590 if pats:
591 raise util.Abort(_("cannot specify names when using '--cleanup'"))
592 return cleanupcmd(ui, repo)
593 elif checkopt('delete', 'addremove cleanup list message name patch stat'):
594 return deletecmd(ui, repo, pats)
595 elif checkopt('list', 'addremove cleanup delete message name'):
596 return listcmd(ui, repo, pats, opts)
597 else:
598 for i in ('patch', 'stat'):
599 if opts[i]:
600 raise util.Abort(_("option '--%s' may not be "
601 "used when shelving a change") % (i,))
602 return createcmd(ui, repo, pats, opts)
603
604 def extsetup(ui):
605 cmdutil.unfinishedstates.append(
606 [shelvedstate._filename, False, True, _('unshelve already in progress'),
607 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
@@ -0,0 +1,420
1 $ echo "[extensions]" >> $HGRCPATH
2 $ echo "shelve=" >> $HGRCPATH
3 $ echo "[defaults]" >> $HGRCPATH
4 $ echo "diff = --nodates --git" >> $HGRCPATH
5
6 $ hg init repo
7 $ cd repo
8 $ mkdir a b
9 $ echo a > a/a
10 $ echo b > b/b
11 $ echo c > c
12 $ echo d > d
13 $ echo x > x
14 $ hg addremove -q
15
16 shelving in an empty repo should be possible
17
18 $ hg shelve
19 (empty repository)
20 shelved as default
21 0 files updated, 0 files merged, 5 files removed, 0 files unresolved
22
23 $ hg unshelve
24 unshelving change 'default'
25 adding changesets
26 adding manifests
27 adding file changes
28 added 1 changesets with 5 changes to 5 files
29 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
30
31 $ hg commit -q -m 'initial commit'
32
33 $ hg shelve
34 nothing changed
35 [1]
36
37 create another commit
38
39 $ echo n > n
40 $ hg add n
41 $ hg commit n -m second
42
43 shelve a change that we will delete later
44
45 $ echo a >> a/a
46 $ hg shelve
47 shelved from default (bb4fec6d): second
48 shelved as default
49 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
50
51 set up some more complex changes to shelve
52
53 $ echo a >> a/a
54 $ hg mv b b.rename
55 moving b/b to b.rename/b (glob)
56 $ hg cp c c.copy
57 $ hg status -C
58 M a/a
59 A b.rename/b
60 b/b
61 A c.copy
62 c
63 R b/b
64
65 prevent some foot-shooting
66
67 $ hg shelve -n foo/bar
68 abort: shelved change names may not contain slashes
69 [255]
70 $ hg shelve -n .baz
71 abort: shelved change names may not start with '.'
72 [255]
73
74 the common case - no options or filenames
75
76 $ hg shelve
77 shelved from default (bb4fec6d): second
78 shelved as default-01
79 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
80 $ hg status -C
81
82 ensure that our shelved changes exist
83
84 $ hg shelve -l
85 default-01 [*] shelved from default (bb4fec6d): second (glob)
86 default [*] shelved from default (bb4fec6d): second (glob)
87
88 $ hg shelve -l -p default
89 default [*] shelved from default (bb4fec6d): second (glob)
90
91 diff --git a/a/a b/a/a
92 --- a/a/a
93 +++ b/a/a
94 @@ -1,1 +1,2 @@
95 a
96 +a
97
98 delete our older shelved change
99
100 $ hg shelve -d default
101
102 local edits should prevent a shelved change from applying
103
104 $ echo e>>a/a
105 $ hg unshelve
106 unshelving change 'default-01'
107 the following shelved files have been modified:
108 a/a
109 you must commit, revert, or shelve your changes before you can proceed
110 abort: cannot unshelve due to local changes
111
112 [255]
113
114 $ hg revert -C a/a
115
116 apply it and make sure our state is as expected
117
118 $ hg unshelve
119 unshelving change 'default-01'
120 adding changesets
121 adding manifests
122 adding file changes
123 added 1 changesets with 3 changes to 8 files
124 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
125 $ hg status -C
126 M a/a
127 A b.rename/b
128 b/b
129 A c.copy
130 c
131 R b/b
132 $ hg shelve -l
133
134 $ hg unshelve
135 abort: no shelved changes to apply!
136 [255]
137 $ hg unshelve foo
138 abort: shelved change 'foo' not found
139 [255]
140
141 named shelves, specific filenames, and "commit messages" should all work
142
143 $ hg status -C
144 M a/a
145 A b.rename/b
146 b/b
147 A c.copy
148 c
149 R b/b
150 $ hg shelve -q -n wibble -m wat a
151
152 expect "a" to no longer be present, but status otherwise unchanged
153
154 $ hg status -C
155 A b.rename/b
156 b/b
157 A c.copy
158 c
159 R b/b
160 $ hg shelve -l --stat
161 wibble [*] wat (glob)
162 a/a | 1 +
163 1 files changed, 1 insertions(+), 0 deletions(-)
164
165 and now "a/a" should reappear
166
167 $ hg unshelve -q wibble
168 $ hg status -C
169 M a/a
170 A b.rename/b
171 b/b
172 A c.copy
173 c
174 R b/b
175
176 cause unshelving to result in a merge with 'a' conflicting
177
178 $ hg shelve -q
179 $ echo c>>a/a
180 $ hg commit -m second
181 $ hg tip --template '{files}\n'
182 a/a
183
184 add an unrelated change that should be preserved
185
186 $ mkdir foo
187 $ echo foo > foo/foo
188 $ hg add foo/foo
189
190 force a conflicted merge to occur
191
192 $ hg unshelve
193 unshelving change 'default'
194 adding changesets
195 adding manifests
196 adding file changes
197 added 1 changesets with 3 changes to 8 files (+1 heads)
198 merging a/a
199 warning: conflicts during merge.
200 merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
201 2 files updated, 0 files merged, 1 files removed, 1 files unresolved
202 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
203 unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
204 [1]
205
206 ensure that we have a merge with unresolved conflicts
207
208 $ hg heads -q
209 3:7ec047b69dc0
210 2:ceefc37abe1e
211 $ hg parents -q
212 2:ceefc37abe1e
213 3:7ec047b69dc0
214 $ hg status
215 M a/a
216 M b.rename/b
217 M c.copy
218 A foo/foo
219 R b/b
220 ? a/a.orig
221 $ hg diff
222 diff --git a/a/a b/a/a
223 --- a/a/a
224 +++ b/a/a
225 @@ -1,2 +1,6 @@
226 a
227 +<<<<<<< local
228 c
229 +=======
230 +a
231 +>>>>>>> other
232 diff --git a/b.rename/b b/b.rename/b
233 --- /dev/null
234 +++ b/b.rename/b
235 @@ -0,0 +1,1 @@
236 +b
237 diff --git a/b/b b/b/b
238 deleted file mode 100644
239 --- a/b/b
240 +++ /dev/null
241 @@ -1,1 +0,0 @@
242 -b
243 diff --git a/c.copy b/c.copy
244 --- /dev/null
245 +++ b/c.copy
246 @@ -0,0 +1,1 @@
247 +c
248 diff --git a/foo/foo b/foo/foo
249 new file mode 100644
250 --- /dev/null
251 +++ b/foo/foo
252 @@ -0,0 +1,1 @@
253 +foo
254 $ hg resolve -l
255 U a/a
256
257 $ hg shelve
258 abort: unshelve already in progress
259 (use 'hg unshelve --continue' or 'hg unshelve --abort')
260 [255]
261
262 abort the unshelve and be happy
263
264 $ hg status
265 M a/a
266 M b.rename/b
267 M c.copy
268 A foo/foo
269 R b/b
270 ? a/a.orig
271 $ hg unshelve -a
272 unshelve of 'default' aborted
273 $ hg heads -q
274 2:ceefc37abe1e
275 $ hg parents
276 changeset: 2:ceefc37abe1e
277 tag: tip
278 user: test
279 date: Thu Jan 01 00:00:00 1970 +0000
280 summary: second
281
282 $ hg resolve -l
283 $ hg status
284 A foo/foo
285 ? a/a.orig
286
287 try to continue with no unshelve underway
288
289 $ hg unshelve -c
290 abort: no unshelve operation underway
291 [255]
292 $ hg status
293 A foo/foo
294 ? a/a.orig
295
296 redo the unshelve to get a conflict
297
298 $ hg unshelve -q
299 warning: conflicts during merge.
300 merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
301 unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
302 [1]
303
304 attempt to continue
305
306 $ hg unshelve -c
307 abort: unresolved conflicts, can't continue
308 (see 'hg resolve', then 'hg unshelve --continue')
309 [255]
310
311 $ hg revert -r . a/a
312 $ hg resolve -m a/a
313
314 $ hg unshelve -c
315 unshelve of 'default' complete
316
317 ensure the repo is as we hope
318
319 $ hg parents
320 changeset: 2:ceefc37abe1e
321 tag: tip
322 user: test
323 date: Thu Jan 01 00:00:00 1970 +0000
324 summary: second
325
326 $ hg heads -q
327 2:ceefc37abe1e
328
329 $ hg status -C
330 M a/a
331 M b.rename/b
332 b/b
333 M c.copy
334 c
335 A foo/foo
336 R b/b
337 ? a/a.orig
338
339 there should be no shelves left
340
341 $ hg shelve -l
342
343 $ hg commit -m whee a/a
344
345 #if execbit
346
347 ensure that metadata-only changes are shelved
348
349 $ chmod +x a/a
350 $ hg shelve -q -n execbit a/a
351 $ hg status a/a
352 $ hg unshelve -q execbit
353 $ hg status a/a
354 M a/a
355 $ hg revert a/a
356
357 #endif
358
359 #if symlink
360
361 $ rm a/a
362 $ ln -s foo a/a
363 $ hg shelve -q -n symlink a/a
364 $ hg status a/a
365 $ hg unshelve -q symlink
366 $ hg status a/a
367 M a/a
368 $ hg revert a/a
369
370 #endif
371
372 set up another conflict between a commit and a shelved change
373
374 $ hg revert -q -C -a
375 $ echo a >> a/a
376 $ hg shelve -q
377 $ echo x >> a/a
378 $ hg ci -m 'create conflict'
379 $ hg add foo/foo
380
381 if we resolve a conflict while unshelving, the unshelve should succeed
382
383 $ HGMERGE=true hg unshelve
384 unshelving change 'default'
385 adding changesets
386 adding manifests
387 adding file changes
388 added 1 changesets with 1 changes to 6 files (+1 heads)
389 merging a/a
390 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
391 $ hg parents -q
392 4:be7e79683c99
393 $ hg shelve -l
394 $ hg status
395 M a/a
396 A foo/foo
397 $ cat a/a
398 a
399 c
400 x
401
402 test keep and cleanup
403
404 $ hg shelve
405 shelved from default (be7e7968): create conflict
406 shelved as default
407 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
408 $ hg shelve --list
409 default [*] shelved from default (be7e7968): create conflict (glob)
410 $ hg unshelve --keep
411 unshelving change 'default'
412 adding changesets
413 adding manifests
414 adding file changes
415 added 1 changesets with 1 changes to 7 files
416 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
417 $ hg shelve --list
418 default [*] shelved from default (be7e7968): create conflict (glob)
419 $ hg shelve --cleanup
420 $ hg shelve --list
@@ -1,552 +1,559
1 # color.py color output for the status and qseries commands
1 # color.py color output for the status and qseries commands
2 #
2 #
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''colorize output from some commands
8 '''colorize output from some commands
9
9
10 This extension modifies the status and resolve commands to add color
10 This extension modifies the status and resolve commands to add color
11 to their output to reflect file status, the qseries command to add
11 to their output to reflect file status, the qseries command to add
12 color to reflect patch status (applied, unapplied, missing), and to
12 color to reflect patch status (applied, unapplied, missing), and to
13 diff-related commands to highlight additions, removals, diff headers,
13 diff-related commands to highlight additions, removals, diff headers,
14 and trailing whitespace.
14 and trailing whitespace.
15
15
16 Other effects in addition to color, like bold and underlined text, are
16 Other effects in addition to color, like bold and underlined text, are
17 also available. By default, the terminfo database is used to find the
17 also available. By default, the terminfo database is used to find the
18 terminal codes used to change color and effect. If terminfo is not
18 terminal codes used to change color and effect. If terminfo is not
19 available, then effects are rendered with the ECMA-48 SGR control
19 available, then effects are rendered with the ECMA-48 SGR control
20 function (aka ANSI escape codes).
20 function (aka ANSI escape codes).
21
21
22 Default effects may be overridden from your configuration file::
22 Default effects may be overridden from your configuration file::
23
23
24 [color]
24 [color]
25 status.modified = blue bold underline red_background
25 status.modified = blue bold underline red_background
26 status.added = green bold
26 status.added = green bold
27 status.removed = red bold blue_background
27 status.removed = red bold blue_background
28 status.deleted = cyan bold underline
28 status.deleted = cyan bold underline
29 status.unknown = magenta bold underline
29 status.unknown = magenta bold underline
30 status.ignored = black bold
30 status.ignored = black bold
31
31
32 # 'none' turns off all effects
32 # 'none' turns off all effects
33 status.clean = none
33 status.clean = none
34 status.copied = none
34 status.copied = none
35
35
36 qseries.applied = blue bold underline
36 qseries.applied = blue bold underline
37 qseries.unapplied = black bold
37 qseries.unapplied = black bold
38 qseries.missing = red bold
38 qseries.missing = red bold
39
39
40 diff.diffline = bold
40 diff.diffline = bold
41 diff.extended = cyan bold
41 diff.extended = cyan bold
42 diff.file_a = red bold
42 diff.file_a = red bold
43 diff.file_b = green bold
43 diff.file_b = green bold
44 diff.hunk = magenta
44 diff.hunk = magenta
45 diff.deleted = red
45 diff.deleted = red
46 diff.inserted = green
46 diff.inserted = green
47 diff.changed = white
47 diff.changed = white
48 diff.trailingwhitespace = bold red_background
48 diff.trailingwhitespace = bold red_background
49
49
50 resolve.unresolved = red bold
50 resolve.unresolved = red bold
51 resolve.resolved = green bold
51 resolve.resolved = green bold
52
52
53 bookmarks.current = green
53 bookmarks.current = green
54
54
55 branches.active = none
55 branches.active = none
56 branches.closed = black bold
56 branches.closed = black bold
57 branches.current = green
57 branches.current = green
58 branches.inactive = none
58 branches.inactive = none
59
59
60 tags.normal = green
60 tags.normal = green
61 tags.local = black bold
61 tags.local = black bold
62
62
63 rebase.rebased = blue
63 rebase.rebased = blue
64 rebase.remaining = red bold
64 rebase.remaining = red bold
65
65
66 shelve.age = cyan
67 shelve.newest = green bold
68 shelve.name = blue bold
69
66 histedit.remaining = red bold
70 histedit.remaining = red bold
67
71
68 The available effects in terminfo mode are 'blink', 'bold', 'dim',
72 The available effects in terminfo mode are 'blink', 'bold', 'dim',
69 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
73 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
70 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
74 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
71 'underline'. How each is rendered depends on the terminal emulator.
75 'underline'. How each is rendered depends on the terminal emulator.
72 Some may not be available for a given terminal type, and will be
76 Some may not be available for a given terminal type, and will be
73 silently ignored.
77 silently ignored.
74
78
75 Note that on some systems, terminfo mode may cause problems when using
79 Note that on some systems, terminfo mode may cause problems when using
76 color with the pager extension and less -R. less with the -R option
80 color with the pager extension and less -R. less with the -R option
77 will only display ECMA-48 color codes, and terminfo mode may sometimes
81 will only display ECMA-48 color codes, and terminfo mode may sometimes
78 emit codes that less doesn't understand. You can work around this by
82 emit codes that less doesn't understand. You can work around this by
79 either using ansi mode (or auto mode), or by using less -r (which will
83 either using ansi mode (or auto mode), or by using less -r (which will
80 pass through all terminal control codes, not just color control
84 pass through all terminal control codes, not just color control
81 codes).
85 codes).
82
86
83 Because there are only eight standard colors, this module allows you
87 Because there are only eight standard colors, this module allows you
84 to define color names for other color slots which might be available
88 to define color names for other color slots which might be available
85 for your terminal type, assuming terminfo mode. For instance::
89 for your terminal type, assuming terminfo mode. For instance::
86
90
87 color.brightblue = 12
91 color.brightblue = 12
88 color.pink = 207
92 color.pink = 207
89 color.orange = 202
93 color.orange = 202
90
94
91 to set 'brightblue' to color slot 12 (useful for 16 color terminals
95 to set 'brightblue' to color slot 12 (useful for 16 color terminals
92 that have brighter colors defined in the upper eight) and, 'pink' and
96 that have brighter colors defined in the upper eight) and, 'pink' and
93 'orange' to colors in 256-color xterm's default color cube. These
97 'orange' to colors in 256-color xterm's default color cube. These
94 defined colors may then be used as any of the pre-defined eight,
98 defined colors may then be used as any of the pre-defined eight,
95 including appending '_background' to set the background to that color.
99 including appending '_background' to set the background to that color.
96
100
97 By default, the color extension will use ANSI mode (or win32 mode on
101 By default, the color extension will use ANSI mode (or win32 mode on
98 Windows) if it detects a terminal. To override auto mode (to enable
102 Windows) if it detects a terminal. To override auto mode (to enable
99 terminfo mode, for example), set the following configuration option::
103 terminfo mode, for example), set the following configuration option::
100
104
101 [color]
105 [color]
102 mode = terminfo
106 mode = terminfo
103
107
104 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
108 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
105 disable color.
109 disable color.
106 '''
110 '''
107
111
108 import os
112 import os
109
113
110 from mercurial import commands, dispatch, extensions, ui as uimod, util
114 from mercurial import commands, dispatch, extensions, ui as uimod, util
111 from mercurial import templater, error
115 from mercurial import templater, error
112 from mercurial.i18n import _
116 from mercurial.i18n import _
113
117
114 testedwith = 'internal'
118 testedwith = 'internal'
115
119
116 # start and stop parameters for effects
120 # start and stop parameters for effects
117 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
121 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
118 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
122 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
119 'italic': 3, 'underline': 4, 'inverse': 7,
123 'italic': 3, 'underline': 4, 'inverse': 7,
120 'black_background': 40, 'red_background': 41,
124 'black_background': 40, 'red_background': 41,
121 'green_background': 42, 'yellow_background': 43,
125 'green_background': 42, 'yellow_background': 43,
122 'blue_background': 44, 'purple_background': 45,
126 'blue_background': 44, 'purple_background': 45,
123 'cyan_background': 46, 'white_background': 47}
127 'cyan_background': 46, 'white_background': 47}
124
128
125 def _terminfosetup(ui, mode):
129 def _terminfosetup(ui, mode):
126 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
130 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
127
131
128 global _terminfo_params
132 global _terminfo_params
129 # If we failed to load curses, we go ahead and return.
133 # If we failed to load curses, we go ahead and return.
130 if not _terminfo_params:
134 if not _terminfo_params:
131 return
135 return
132 # Otherwise, see what the config file says.
136 # Otherwise, see what the config file says.
133 if mode not in ('auto', 'terminfo'):
137 if mode not in ('auto', 'terminfo'):
134 return
138 return
135
139
136 _terminfo_params.update((key[6:], (False, int(val)))
140 _terminfo_params.update((key[6:], (False, int(val)))
137 for key, val in ui.configitems('color')
141 for key, val in ui.configitems('color')
138 if key.startswith('color.'))
142 if key.startswith('color.'))
139
143
140 try:
144 try:
141 curses.setupterm()
145 curses.setupterm()
142 except curses.error, e:
146 except curses.error, e:
143 _terminfo_params = {}
147 _terminfo_params = {}
144 return
148 return
145
149
146 for key, (b, e) in _terminfo_params.items():
150 for key, (b, e) in _terminfo_params.items():
147 if not b:
151 if not b:
148 continue
152 continue
149 if not curses.tigetstr(e):
153 if not curses.tigetstr(e):
150 # Most terminals don't support dim, invis, etc, so don't be
154 # Most terminals don't support dim, invis, etc, so don't be
151 # noisy and use ui.debug().
155 # noisy and use ui.debug().
152 ui.debug("no terminfo entry for %s\n" % e)
156 ui.debug("no terminfo entry for %s\n" % e)
153 del _terminfo_params[key]
157 del _terminfo_params[key]
154 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
158 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
155 # Only warn about missing terminfo entries if we explicitly asked for
159 # Only warn about missing terminfo entries if we explicitly asked for
156 # terminfo mode.
160 # terminfo mode.
157 if mode == "terminfo":
161 if mode == "terminfo":
158 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
162 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
159 "ECMA-48 color\n"))
163 "ECMA-48 color\n"))
160 _terminfo_params = {}
164 _terminfo_params = {}
161
165
162 def _modesetup(ui, coloropt):
166 def _modesetup(ui, coloropt):
163 global _terminfo_params
167 global _terminfo_params
164
168
165 auto = coloropt == 'auto'
169 auto = coloropt == 'auto'
166 always = not auto and util.parsebool(coloropt)
170 always = not auto and util.parsebool(coloropt)
167 if not always and not auto:
171 if not always and not auto:
168 return None
172 return None
169
173
170 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
174 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
171
175
172 mode = ui.config('color', 'mode', 'auto')
176 mode = ui.config('color', 'mode', 'auto')
173 realmode = mode
177 realmode = mode
174 if mode == 'auto':
178 if mode == 'auto':
175 if os.name == 'nt' and 'TERM' not in os.environ:
179 if os.name == 'nt' and 'TERM' not in os.environ:
176 # looks line a cmd.exe console, use win32 API or nothing
180 # looks line a cmd.exe console, use win32 API or nothing
177 realmode = 'win32'
181 realmode = 'win32'
178 else:
182 else:
179 realmode = 'ansi'
183 realmode = 'ansi'
180
184
181 if realmode == 'win32':
185 if realmode == 'win32':
182 _terminfo_params = {}
186 _terminfo_params = {}
183 if not w32effects:
187 if not w32effects:
184 if mode == 'win32':
188 if mode == 'win32':
185 # only warn if color.mode is explicitly set to win32
189 # only warn if color.mode is explicitly set to win32
186 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
190 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
187 return None
191 return None
188 _effects.update(w32effects)
192 _effects.update(w32effects)
189 elif realmode == 'ansi':
193 elif realmode == 'ansi':
190 _terminfo_params = {}
194 _terminfo_params = {}
191 elif realmode == 'terminfo':
195 elif realmode == 'terminfo':
192 _terminfosetup(ui, mode)
196 _terminfosetup(ui, mode)
193 if not _terminfo_params:
197 if not _terminfo_params:
194 if mode == 'terminfo':
198 if mode == 'terminfo':
195 ## FIXME Shouldn't we return None in this case too?
199 ## FIXME Shouldn't we return None in this case too?
196 # only warn if color.mode is explicitly set to win32
200 # only warn if color.mode is explicitly set to win32
197 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
201 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
198 realmode = 'ansi'
202 realmode = 'ansi'
199 else:
203 else:
200 return None
204 return None
201
205
202 if always or (auto and formatted):
206 if always or (auto and formatted):
203 return realmode
207 return realmode
204 return None
208 return None
205
209
206 try:
210 try:
207 import curses
211 import curses
208 # Mapping from effect name to terminfo attribute name or color number.
212 # Mapping from effect name to terminfo attribute name or color number.
209 # This will also force-load the curses module.
213 # This will also force-load the curses module.
210 _terminfo_params = {'none': (True, 'sgr0'),
214 _terminfo_params = {'none': (True, 'sgr0'),
211 'standout': (True, 'smso'),
215 'standout': (True, 'smso'),
212 'underline': (True, 'smul'),
216 'underline': (True, 'smul'),
213 'reverse': (True, 'rev'),
217 'reverse': (True, 'rev'),
214 'inverse': (True, 'rev'),
218 'inverse': (True, 'rev'),
215 'blink': (True, 'blink'),
219 'blink': (True, 'blink'),
216 'dim': (True, 'dim'),
220 'dim': (True, 'dim'),
217 'bold': (True, 'bold'),
221 'bold': (True, 'bold'),
218 'invisible': (True, 'invis'),
222 'invisible': (True, 'invis'),
219 'italic': (True, 'sitm'),
223 'italic': (True, 'sitm'),
220 'black': (False, curses.COLOR_BLACK),
224 'black': (False, curses.COLOR_BLACK),
221 'red': (False, curses.COLOR_RED),
225 'red': (False, curses.COLOR_RED),
222 'green': (False, curses.COLOR_GREEN),
226 'green': (False, curses.COLOR_GREEN),
223 'yellow': (False, curses.COLOR_YELLOW),
227 'yellow': (False, curses.COLOR_YELLOW),
224 'blue': (False, curses.COLOR_BLUE),
228 'blue': (False, curses.COLOR_BLUE),
225 'magenta': (False, curses.COLOR_MAGENTA),
229 'magenta': (False, curses.COLOR_MAGENTA),
226 'cyan': (False, curses.COLOR_CYAN),
230 'cyan': (False, curses.COLOR_CYAN),
227 'white': (False, curses.COLOR_WHITE)}
231 'white': (False, curses.COLOR_WHITE)}
228 except ImportError:
232 except ImportError:
229 _terminfo_params = False
233 _terminfo_params = False
230
234
231 _styles = {'grep.match': 'red bold',
235 _styles = {'grep.match': 'red bold',
232 'grep.linenumber': 'green',
236 'grep.linenumber': 'green',
233 'grep.rev': 'green',
237 'grep.rev': 'green',
234 'grep.change': 'green',
238 'grep.change': 'green',
235 'grep.sep': 'cyan',
239 'grep.sep': 'cyan',
236 'grep.filename': 'magenta',
240 'grep.filename': 'magenta',
237 'grep.user': 'magenta',
241 'grep.user': 'magenta',
238 'grep.date': 'magenta',
242 'grep.date': 'magenta',
239 'bookmarks.current': 'green',
243 'bookmarks.current': 'green',
240 'branches.active': 'none',
244 'branches.active': 'none',
241 'branches.closed': 'black bold',
245 'branches.closed': 'black bold',
242 'branches.current': 'green',
246 'branches.current': 'green',
243 'branches.inactive': 'none',
247 'branches.inactive': 'none',
244 'diff.changed': 'white',
248 'diff.changed': 'white',
245 'diff.deleted': 'red',
249 'diff.deleted': 'red',
246 'diff.diffline': 'bold',
250 'diff.diffline': 'bold',
247 'diff.extended': 'cyan bold',
251 'diff.extended': 'cyan bold',
248 'diff.file_a': 'red bold',
252 'diff.file_a': 'red bold',
249 'diff.file_b': 'green bold',
253 'diff.file_b': 'green bold',
250 'diff.hunk': 'magenta',
254 'diff.hunk': 'magenta',
251 'diff.inserted': 'green',
255 'diff.inserted': 'green',
252 'diff.trailingwhitespace': 'bold red_background',
256 'diff.trailingwhitespace': 'bold red_background',
253 'diffstat.deleted': 'red',
257 'diffstat.deleted': 'red',
254 'diffstat.inserted': 'green',
258 'diffstat.inserted': 'green',
255 'histedit.remaining': 'red bold',
259 'histedit.remaining': 'red bold',
256 'ui.prompt': 'yellow',
260 'ui.prompt': 'yellow',
257 'log.changeset': 'yellow',
261 'log.changeset': 'yellow',
258 'rebase.rebased': 'blue',
262 'rebase.rebased': 'blue',
259 'rebase.remaining': 'red bold',
263 'rebase.remaining': 'red bold',
260 'resolve.resolved': 'green bold',
264 'resolve.resolved': 'green bold',
261 'resolve.unresolved': 'red bold',
265 'resolve.unresolved': 'red bold',
266 'shelve.age': 'cyan',
267 'shelve.newest': 'green bold',
268 'shelve.name': 'blue bold',
262 'status.added': 'green bold',
269 'status.added': 'green bold',
263 'status.clean': 'none',
270 'status.clean': 'none',
264 'status.copied': 'none',
271 'status.copied': 'none',
265 'status.deleted': 'cyan bold underline',
272 'status.deleted': 'cyan bold underline',
266 'status.ignored': 'black bold',
273 'status.ignored': 'black bold',
267 'status.modified': 'blue bold',
274 'status.modified': 'blue bold',
268 'status.removed': 'red bold',
275 'status.removed': 'red bold',
269 'status.unknown': 'magenta bold underline',
276 'status.unknown': 'magenta bold underline',
270 'tags.normal': 'green',
277 'tags.normal': 'green',
271 'tags.local': 'black bold'}
278 'tags.local': 'black bold'}
272
279
273
280
274 def _effect_str(effect):
281 def _effect_str(effect):
275 '''Helper function for render_effects().'''
282 '''Helper function for render_effects().'''
276
283
277 bg = False
284 bg = False
278 if effect.endswith('_background'):
285 if effect.endswith('_background'):
279 bg = True
286 bg = True
280 effect = effect[:-11]
287 effect = effect[:-11]
281 attr, val = _terminfo_params[effect]
288 attr, val = _terminfo_params[effect]
282 if attr:
289 if attr:
283 return curses.tigetstr(val)
290 return curses.tigetstr(val)
284 elif bg:
291 elif bg:
285 return curses.tparm(curses.tigetstr('setab'), val)
292 return curses.tparm(curses.tigetstr('setab'), val)
286 else:
293 else:
287 return curses.tparm(curses.tigetstr('setaf'), val)
294 return curses.tparm(curses.tigetstr('setaf'), val)
288
295
289 def render_effects(text, effects):
296 def render_effects(text, effects):
290 'Wrap text in commands to turn on each effect.'
297 'Wrap text in commands to turn on each effect.'
291 if not text:
298 if not text:
292 return text
299 return text
293 if not _terminfo_params:
300 if not _terminfo_params:
294 start = [str(_effects[e]) for e in ['none'] + effects.split()]
301 start = [str(_effects[e]) for e in ['none'] + effects.split()]
295 start = '\033[' + ';'.join(start) + 'm'
302 start = '\033[' + ';'.join(start) + 'm'
296 stop = '\033[' + str(_effects['none']) + 'm'
303 stop = '\033[' + str(_effects['none']) + 'm'
297 else:
304 else:
298 start = ''.join(_effect_str(effect)
305 start = ''.join(_effect_str(effect)
299 for effect in ['none'] + effects.split())
306 for effect in ['none'] + effects.split())
300 stop = _effect_str('none')
307 stop = _effect_str('none')
301 return ''.join([start, text, stop])
308 return ''.join([start, text, stop])
302
309
303 def extstyles():
310 def extstyles():
304 for name, ext in extensions.extensions():
311 for name, ext in extensions.extensions():
305 _styles.update(getattr(ext, 'colortable', {}))
312 _styles.update(getattr(ext, 'colortable', {}))
306
313
307 def configstyles(ui):
314 def configstyles(ui):
308 for status, cfgeffects in ui.configitems('color'):
315 for status, cfgeffects in ui.configitems('color'):
309 if '.' not in status or status.startswith('color.'):
316 if '.' not in status or status.startswith('color.'):
310 continue
317 continue
311 cfgeffects = ui.configlist('color', status)
318 cfgeffects = ui.configlist('color', status)
312 if cfgeffects:
319 if cfgeffects:
313 good = []
320 good = []
314 for e in cfgeffects:
321 for e in cfgeffects:
315 if not _terminfo_params and e in _effects:
322 if not _terminfo_params and e in _effects:
316 good.append(e)
323 good.append(e)
317 elif e in _terminfo_params or e[:-11] in _terminfo_params:
324 elif e in _terminfo_params or e[:-11] in _terminfo_params:
318 good.append(e)
325 good.append(e)
319 else:
326 else:
320 ui.warn(_("ignoring unknown color/effect %r "
327 ui.warn(_("ignoring unknown color/effect %r "
321 "(configured in color.%s)\n")
328 "(configured in color.%s)\n")
322 % (e, status))
329 % (e, status))
323 _styles[status] = ' '.join(good)
330 _styles[status] = ' '.join(good)
324
331
325 class colorui(uimod.ui):
332 class colorui(uimod.ui):
326 def popbuffer(self, labeled=False):
333 def popbuffer(self, labeled=False):
327 if self._colormode is None:
334 if self._colormode is None:
328 return super(colorui, self).popbuffer(labeled)
335 return super(colorui, self).popbuffer(labeled)
329
336
330 if labeled:
337 if labeled:
331 return ''.join(self.label(a, label) for a, label
338 return ''.join(self.label(a, label) for a, label
332 in self._buffers.pop())
339 in self._buffers.pop())
333 return ''.join(a for a, label in self._buffers.pop())
340 return ''.join(a for a, label in self._buffers.pop())
334
341
335 _colormode = 'ansi'
342 _colormode = 'ansi'
336 def write(self, *args, **opts):
343 def write(self, *args, **opts):
337 if self._colormode is None:
344 if self._colormode is None:
338 return super(colorui, self).write(*args, **opts)
345 return super(colorui, self).write(*args, **opts)
339
346
340 label = opts.get('label', '')
347 label = opts.get('label', '')
341 if self._buffers:
348 if self._buffers:
342 self._buffers[-1].extend([(str(a), label) for a in args])
349 self._buffers[-1].extend([(str(a), label) for a in args])
343 elif self._colormode == 'win32':
350 elif self._colormode == 'win32':
344 for a in args:
351 for a in args:
345 win32print(a, super(colorui, self).write, **opts)
352 win32print(a, super(colorui, self).write, **opts)
346 else:
353 else:
347 return super(colorui, self).write(
354 return super(colorui, self).write(
348 *[self.label(str(a), label) for a in args], **opts)
355 *[self.label(str(a), label) for a in args], **opts)
349
356
350 def write_err(self, *args, **opts):
357 def write_err(self, *args, **opts):
351 if self._colormode is None:
358 if self._colormode is None:
352 return super(colorui, self).write_err(*args, **opts)
359 return super(colorui, self).write_err(*args, **opts)
353
360
354 label = opts.get('label', '')
361 label = opts.get('label', '')
355 if self._colormode == 'win32':
362 if self._colormode == 'win32':
356 for a in args:
363 for a in args:
357 win32print(a, super(colorui, self).write_err, **opts)
364 win32print(a, super(colorui, self).write_err, **opts)
358 else:
365 else:
359 return super(colorui, self).write_err(
366 return super(colorui, self).write_err(
360 *[self.label(str(a), label) for a in args], **opts)
367 *[self.label(str(a), label) for a in args], **opts)
361
368
362 def label(self, msg, label):
369 def label(self, msg, label):
363 if self._colormode is None:
370 if self._colormode is None:
364 return super(colorui, self).label(msg, label)
371 return super(colorui, self).label(msg, label)
365
372
366 effects = []
373 effects = []
367 for l in label.split():
374 for l in label.split():
368 s = _styles.get(l, '')
375 s = _styles.get(l, '')
369 if s:
376 if s:
370 effects.append(s)
377 effects.append(s)
371 effects = ' '.join(effects)
378 effects = ' '.join(effects)
372 if effects:
379 if effects:
373 return '\n'.join([render_effects(s, effects)
380 return '\n'.join([render_effects(s, effects)
374 for s in msg.split('\n')])
381 for s in msg.split('\n')])
375 return msg
382 return msg
376
383
377 def templatelabel(context, mapping, args):
384 def templatelabel(context, mapping, args):
378 if len(args) != 2:
385 if len(args) != 2:
379 # i18n: "label" is a keyword
386 # i18n: "label" is a keyword
380 raise error.ParseError(_("label expects two arguments"))
387 raise error.ParseError(_("label expects two arguments"))
381
388
382 thing = templater.stringify(args[1][0](context, mapping, args[1][1]))
389 thing = templater.stringify(args[1][0](context, mapping, args[1][1]))
383 thing = templater.runtemplate(context, mapping,
390 thing = templater.runtemplate(context, mapping,
384 templater.compiletemplate(thing, context))
391 templater.compiletemplate(thing, context))
385
392
386 # apparently, repo could be a string that is the favicon?
393 # apparently, repo could be a string that is the favicon?
387 repo = mapping.get('repo', '')
394 repo = mapping.get('repo', '')
388 if isinstance(repo, str):
395 if isinstance(repo, str):
389 return thing
396 return thing
390
397
391 label = templater.stringify(args[0][0](context, mapping, args[0][1]))
398 label = templater.stringify(args[0][0](context, mapping, args[0][1]))
392 label = templater.runtemplate(context, mapping,
399 label = templater.runtemplate(context, mapping,
393 templater.compiletemplate(label, context))
400 templater.compiletemplate(label, context))
394
401
395 thing = templater.stringify(thing)
402 thing = templater.stringify(thing)
396 label = templater.stringify(label)
403 label = templater.stringify(label)
397
404
398 return repo.ui.label(thing, label)
405 return repo.ui.label(thing, label)
399
406
400 def uisetup(ui):
407 def uisetup(ui):
401 if ui.plain():
408 if ui.plain():
402 return
409 return
403 if not isinstance(ui, colorui):
410 if not isinstance(ui, colorui):
404 colorui.__bases__ = (ui.__class__,)
411 colorui.__bases__ = (ui.__class__,)
405 ui.__class__ = colorui
412 ui.__class__ = colorui
406 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
413 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
407 mode = _modesetup(ui_, opts['color'])
414 mode = _modesetup(ui_, opts['color'])
408 colorui._colormode = mode
415 colorui._colormode = mode
409 if mode:
416 if mode:
410 extstyles()
417 extstyles()
411 configstyles(ui_)
418 configstyles(ui_)
412 return orig(ui_, opts, cmd, cmdfunc)
419 return orig(ui_, opts, cmd, cmdfunc)
413 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
420 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
414 templater.funcs['label'] = templatelabel
421 templater.funcs['label'] = templatelabel
415
422
416 def extsetup(ui):
423 def extsetup(ui):
417 commands.globalopts.append(
424 commands.globalopts.append(
418 ('', 'color', 'auto',
425 ('', 'color', 'auto',
419 # i18n: 'always', 'auto', and 'never' are keywords and should
426 # i18n: 'always', 'auto', and 'never' are keywords and should
420 # not be translated
427 # not be translated
421 _("when to colorize (boolean, always, auto, or never)"),
428 _("when to colorize (boolean, always, auto, or never)"),
422 _('TYPE')))
429 _('TYPE')))
423
430
424 if os.name != 'nt':
431 if os.name != 'nt':
425 w32effects = None
432 w32effects = None
426 else:
433 else:
427 import re, ctypes
434 import re, ctypes
428
435
429 _kernel32 = ctypes.windll.kernel32
436 _kernel32 = ctypes.windll.kernel32
430
437
431 _WORD = ctypes.c_ushort
438 _WORD = ctypes.c_ushort
432
439
433 _INVALID_HANDLE_VALUE = -1
440 _INVALID_HANDLE_VALUE = -1
434
441
435 class _COORD(ctypes.Structure):
442 class _COORD(ctypes.Structure):
436 _fields_ = [('X', ctypes.c_short),
443 _fields_ = [('X', ctypes.c_short),
437 ('Y', ctypes.c_short)]
444 ('Y', ctypes.c_short)]
438
445
439 class _SMALL_RECT(ctypes.Structure):
446 class _SMALL_RECT(ctypes.Structure):
440 _fields_ = [('Left', ctypes.c_short),
447 _fields_ = [('Left', ctypes.c_short),
441 ('Top', ctypes.c_short),
448 ('Top', ctypes.c_short),
442 ('Right', ctypes.c_short),
449 ('Right', ctypes.c_short),
443 ('Bottom', ctypes.c_short)]
450 ('Bottom', ctypes.c_short)]
444
451
445 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
452 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
446 _fields_ = [('dwSize', _COORD),
453 _fields_ = [('dwSize', _COORD),
447 ('dwCursorPosition', _COORD),
454 ('dwCursorPosition', _COORD),
448 ('wAttributes', _WORD),
455 ('wAttributes', _WORD),
449 ('srWindow', _SMALL_RECT),
456 ('srWindow', _SMALL_RECT),
450 ('dwMaximumWindowSize', _COORD)]
457 ('dwMaximumWindowSize', _COORD)]
451
458
452 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
459 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
453 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
460 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
454
461
455 _FOREGROUND_BLUE = 0x0001
462 _FOREGROUND_BLUE = 0x0001
456 _FOREGROUND_GREEN = 0x0002
463 _FOREGROUND_GREEN = 0x0002
457 _FOREGROUND_RED = 0x0004
464 _FOREGROUND_RED = 0x0004
458 _FOREGROUND_INTENSITY = 0x0008
465 _FOREGROUND_INTENSITY = 0x0008
459
466
460 _BACKGROUND_BLUE = 0x0010
467 _BACKGROUND_BLUE = 0x0010
461 _BACKGROUND_GREEN = 0x0020
468 _BACKGROUND_GREEN = 0x0020
462 _BACKGROUND_RED = 0x0040
469 _BACKGROUND_RED = 0x0040
463 _BACKGROUND_INTENSITY = 0x0080
470 _BACKGROUND_INTENSITY = 0x0080
464
471
465 _COMMON_LVB_REVERSE_VIDEO = 0x4000
472 _COMMON_LVB_REVERSE_VIDEO = 0x4000
466 _COMMON_LVB_UNDERSCORE = 0x8000
473 _COMMON_LVB_UNDERSCORE = 0x8000
467
474
468 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
475 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
469 w32effects = {
476 w32effects = {
470 'none': -1,
477 'none': -1,
471 'black': 0,
478 'black': 0,
472 'red': _FOREGROUND_RED,
479 'red': _FOREGROUND_RED,
473 'green': _FOREGROUND_GREEN,
480 'green': _FOREGROUND_GREEN,
474 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
481 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
475 'blue': _FOREGROUND_BLUE,
482 'blue': _FOREGROUND_BLUE,
476 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
483 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
477 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
484 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
478 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
485 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
479 'bold': _FOREGROUND_INTENSITY,
486 'bold': _FOREGROUND_INTENSITY,
480 'black_background': 0x100, # unused value > 0x0f
487 'black_background': 0x100, # unused value > 0x0f
481 'red_background': _BACKGROUND_RED,
488 'red_background': _BACKGROUND_RED,
482 'green_background': _BACKGROUND_GREEN,
489 'green_background': _BACKGROUND_GREEN,
483 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
490 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
484 'blue_background': _BACKGROUND_BLUE,
491 'blue_background': _BACKGROUND_BLUE,
485 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
492 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
486 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
493 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
487 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
494 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
488 _BACKGROUND_BLUE),
495 _BACKGROUND_BLUE),
489 'bold_background': _BACKGROUND_INTENSITY,
496 'bold_background': _BACKGROUND_INTENSITY,
490 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
497 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
491 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
498 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
492 }
499 }
493
500
494 passthrough = set([_FOREGROUND_INTENSITY,
501 passthrough = set([_FOREGROUND_INTENSITY,
495 _BACKGROUND_INTENSITY,
502 _BACKGROUND_INTENSITY,
496 _COMMON_LVB_UNDERSCORE,
503 _COMMON_LVB_UNDERSCORE,
497 _COMMON_LVB_REVERSE_VIDEO])
504 _COMMON_LVB_REVERSE_VIDEO])
498
505
499 stdout = _kernel32.GetStdHandle(
506 stdout = _kernel32.GetStdHandle(
500 _STD_OUTPUT_HANDLE) # don't close the handle returned
507 _STD_OUTPUT_HANDLE) # don't close the handle returned
501 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
508 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
502 w32effects = None
509 w32effects = None
503 else:
510 else:
504 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
511 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
505 if not _kernel32.GetConsoleScreenBufferInfo(
512 if not _kernel32.GetConsoleScreenBufferInfo(
506 stdout, ctypes.byref(csbi)):
513 stdout, ctypes.byref(csbi)):
507 # stdout may not support GetConsoleScreenBufferInfo()
514 # stdout may not support GetConsoleScreenBufferInfo()
508 # when called from subprocess or redirected
515 # when called from subprocess or redirected
509 w32effects = None
516 w32effects = None
510 else:
517 else:
511 origattr = csbi.wAttributes
518 origattr = csbi.wAttributes
512 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
519 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
513 re.MULTILINE | re.DOTALL)
520 re.MULTILINE | re.DOTALL)
514
521
515 def win32print(text, orig, **opts):
522 def win32print(text, orig, **opts):
516 label = opts.get('label', '')
523 label = opts.get('label', '')
517 attr = origattr
524 attr = origattr
518
525
519 def mapcolor(val, attr):
526 def mapcolor(val, attr):
520 if val == -1:
527 if val == -1:
521 return origattr
528 return origattr
522 elif val in passthrough:
529 elif val in passthrough:
523 return attr | val
530 return attr | val
524 elif val > 0x0f:
531 elif val > 0x0f:
525 return (val & 0x70) | (attr & 0x8f)
532 return (val & 0x70) | (attr & 0x8f)
526 else:
533 else:
527 return (val & 0x07) | (attr & 0xf8)
534 return (val & 0x07) | (attr & 0xf8)
528
535
529 # determine console attributes based on labels
536 # determine console attributes based on labels
530 for l in label.split():
537 for l in label.split():
531 style = _styles.get(l, '')
538 style = _styles.get(l, '')
532 for effect in style.split():
539 for effect in style.split():
533 attr = mapcolor(w32effects[effect], attr)
540 attr = mapcolor(w32effects[effect], attr)
534
541
535 # hack to ensure regexp finds data
542 # hack to ensure regexp finds data
536 if not text.startswith('\033['):
543 if not text.startswith('\033['):
537 text = '\033[m' + text
544 text = '\033[m' + text
538
545
539 # Look for ANSI-like codes embedded in text
546 # Look for ANSI-like codes embedded in text
540 m = re.match(ansire, text)
547 m = re.match(ansire, text)
541
548
542 try:
549 try:
543 while m:
550 while m:
544 for sattr in m.group(1).split(';'):
551 for sattr in m.group(1).split(';'):
545 if sattr:
552 if sattr:
546 attr = mapcolor(int(sattr), attr)
553 attr = mapcolor(int(sattr), attr)
547 _kernel32.SetConsoleTextAttribute(stdout, attr)
554 _kernel32.SetConsoleTextAttribute(stdout, attr)
548 orig(m.group(2), **opts)
555 orig(m.group(2), **opts)
549 m = re.match(ansire, m.group(3))
556 m = re.match(ansire, m.group(3))
550 finally:
557 finally:
551 # Explicitly reset original attributes
558 # Explicitly reset original attributes
552 _kernel32.SetConsoleTextAttribute(stdout, origattr)
559 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,1270 +1,1271
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # run-tests.py - Run a set of tests on Mercurial
3 # run-tests.py - Run a set of tests on Mercurial
4 #
4 #
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 # Modifying this script is tricky because it has many modes:
10 # Modifying this script is tricky because it has many modes:
11 # - serial (default) vs parallel (-jN, N > 1)
11 # - serial (default) vs parallel (-jN, N > 1)
12 # - no coverage (default) vs coverage (-c, -C, -s)
12 # - no coverage (default) vs coverage (-c, -C, -s)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 # - tests are a mix of shell scripts and Python scripts
14 # - tests are a mix of shell scripts and Python scripts
15 #
15 #
16 # If you change this script, it is recommended that you ensure you
16 # If you change this script, it is recommended that you ensure you
17 # haven't broken it by running it in various modes with a representative
17 # haven't broken it by running it in various modes with a representative
18 # sample of test scripts. For example:
18 # sample of test scripts. For example:
19 #
19 #
20 # 1) serial, no coverage, temp install:
20 # 1) serial, no coverage, temp install:
21 # ./run-tests.py test-s*
21 # ./run-tests.py test-s*
22 # 2) serial, no coverage, local hg:
22 # 2) serial, no coverage, local hg:
23 # ./run-tests.py --local test-s*
23 # ./run-tests.py --local test-s*
24 # 3) serial, coverage, temp install:
24 # 3) serial, coverage, temp install:
25 # ./run-tests.py -c test-s*
25 # ./run-tests.py -c test-s*
26 # 4) serial, coverage, local hg:
26 # 4) serial, coverage, local hg:
27 # ./run-tests.py -c --local test-s* # unsupported
27 # ./run-tests.py -c --local test-s* # unsupported
28 # 5) parallel, no coverage, temp install:
28 # 5) parallel, no coverage, temp install:
29 # ./run-tests.py -j2 test-s*
29 # ./run-tests.py -j2 test-s*
30 # 6) parallel, no coverage, local hg:
30 # 6) parallel, no coverage, local hg:
31 # ./run-tests.py -j2 --local test-s*
31 # ./run-tests.py -j2 --local test-s*
32 # 7) parallel, coverage, temp install:
32 # 7) parallel, coverage, temp install:
33 # ./run-tests.py -j2 -c test-s* # currently broken
33 # ./run-tests.py -j2 -c test-s* # currently broken
34 # 8) parallel, coverage, local install:
34 # 8) parallel, coverage, local install:
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 # 9) parallel, custom tmp dir:
36 # 9) parallel, custom tmp dir:
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 #
38 #
39 # (You could use any subset of the tests: test-s* happens to match
39 # (You could use any subset of the tests: test-s* happens to match
40 # enough that it's worth doing parallel runs, few enough that it
40 # enough that it's worth doing parallel runs, few enough that it
41 # completes fairly quickly, includes both shell and Python scripts, and
41 # completes fairly quickly, includes both shell and Python scripts, and
42 # includes some scripts that run daemon processes.)
42 # includes some scripts that run daemon processes.)
43
43
44 from distutils import version
44 from distutils import version
45 import difflib
45 import difflib
46 import errno
46 import errno
47 import optparse
47 import optparse
48 import os
48 import os
49 import shutil
49 import shutil
50 import subprocess
50 import subprocess
51 import signal
51 import signal
52 import sys
52 import sys
53 import tempfile
53 import tempfile
54 import time
54 import time
55 import random
55 import random
56 import re
56 import re
57 import threading
57 import threading
58 import killdaemons as killmod
58 import killdaemons as killmod
59 import Queue as queue
59 import Queue as queue
60
60
61 processlock = threading.Lock()
61 processlock = threading.Lock()
62
62
63 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
63 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
64 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
64 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
65 # zombies but it's pretty harmless even if we do.
65 # zombies but it's pretty harmless even if we do.
66 if sys.version_info[1] < 5:
66 if sys.version_info[1] < 5:
67 subprocess._cleanup = lambda: None
67 subprocess._cleanup = lambda: None
68
68
69 closefds = os.name == 'posix'
69 closefds = os.name == 'posix'
70 def Popen4(cmd, wd, timeout, env=None):
70 def Popen4(cmd, wd, timeout, env=None):
71 processlock.acquire()
71 processlock.acquire()
72 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
72 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
73 close_fds=closefds,
73 close_fds=closefds,
74 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
74 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
75 stderr=subprocess.STDOUT)
75 stderr=subprocess.STDOUT)
76 processlock.release()
76 processlock.release()
77
77
78 p.fromchild = p.stdout
78 p.fromchild = p.stdout
79 p.tochild = p.stdin
79 p.tochild = p.stdin
80 p.childerr = p.stderr
80 p.childerr = p.stderr
81
81
82 p.timeout = False
82 p.timeout = False
83 if timeout:
83 if timeout:
84 def t():
84 def t():
85 start = time.time()
85 start = time.time()
86 while time.time() - start < timeout and p.returncode is None:
86 while time.time() - start < timeout and p.returncode is None:
87 time.sleep(.1)
87 time.sleep(.1)
88 p.timeout = True
88 p.timeout = True
89 if p.returncode is None:
89 if p.returncode is None:
90 terminate(p)
90 terminate(p)
91 threading.Thread(target=t).start()
91 threading.Thread(target=t).start()
92
92
93 return p
93 return p
94
94
95 # reserved exit code to skip test (used by hghave)
95 # reserved exit code to skip test (used by hghave)
96 SKIPPED_STATUS = 80
96 SKIPPED_STATUS = 80
97 SKIPPED_PREFIX = 'skipped: '
97 SKIPPED_PREFIX = 'skipped: '
98 FAILED_PREFIX = 'hghave check failed: '
98 FAILED_PREFIX = 'hghave check failed: '
99 PYTHON = sys.executable.replace('\\', '/')
99 PYTHON = sys.executable.replace('\\', '/')
100 IMPL_PATH = 'PYTHONPATH'
100 IMPL_PATH = 'PYTHONPATH'
101 if 'java' in sys.platform:
101 if 'java' in sys.platform:
102 IMPL_PATH = 'JYTHONPATH'
102 IMPL_PATH = 'JYTHONPATH'
103
103
104 requiredtools = [os.path.basename(sys.executable), "diff", "grep", "unzip",
104 requiredtools = [os.path.basename(sys.executable), "diff", "grep", "unzip",
105 "gunzip", "bunzip2", "sed"]
105 "gunzip", "bunzip2", "sed"]
106
106
107 defaults = {
107 defaults = {
108 'jobs': ('HGTEST_JOBS', 1),
108 'jobs': ('HGTEST_JOBS', 1),
109 'timeout': ('HGTEST_TIMEOUT', 180),
109 'timeout': ('HGTEST_TIMEOUT', 180),
110 'port': ('HGTEST_PORT', 20059),
110 'port': ('HGTEST_PORT', 20059),
111 'shell': ('HGTEST_SHELL', 'sh'),
111 'shell': ('HGTEST_SHELL', 'sh'),
112 }
112 }
113
113
114 def parselistfiles(files, listtype, warn=True):
114 def parselistfiles(files, listtype, warn=True):
115 entries = dict()
115 entries = dict()
116 for filename in files:
116 for filename in files:
117 try:
117 try:
118 path = os.path.expanduser(os.path.expandvars(filename))
118 path = os.path.expanduser(os.path.expandvars(filename))
119 f = open(path, "r")
119 f = open(path, "r")
120 except IOError, err:
120 except IOError, err:
121 if err.errno != errno.ENOENT:
121 if err.errno != errno.ENOENT:
122 raise
122 raise
123 if warn:
123 if warn:
124 print "warning: no such %s file: %s" % (listtype, filename)
124 print "warning: no such %s file: %s" % (listtype, filename)
125 continue
125 continue
126
126
127 for line in f.readlines():
127 for line in f.readlines():
128 line = line.split('#', 1)[0].strip()
128 line = line.split('#', 1)[0].strip()
129 if line:
129 if line:
130 entries[line] = filename
130 entries[line] = filename
131
131
132 f.close()
132 f.close()
133 return entries
133 return entries
134
134
135 def parseargs():
135 def parseargs():
136 parser = optparse.OptionParser("%prog [options] [tests]")
136 parser = optparse.OptionParser("%prog [options] [tests]")
137
137
138 # keep these sorted
138 # keep these sorted
139 parser.add_option("--blacklist", action="append",
139 parser.add_option("--blacklist", action="append",
140 help="skip tests listed in the specified blacklist file")
140 help="skip tests listed in the specified blacklist file")
141 parser.add_option("--whitelist", action="append",
141 parser.add_option("--whitelist", action="append",
142 help="always run tests listed in the specified whitelist file")
142 help="always run tests listed in the specified whitelist file")
143 parser.add_option("-C", "--annotate", action="store_true",
143 parser.add_option("-C", "--annotate", action="store_true",
144 help="output files annotated with coverage")
144 help="output files annotated with coverage")
145 parser.add_option("-c", "--cover", action="store_true",
145 parser.add_option("-c", "--cover", action="store_true",
146 help="print a test coverage report")
146 help="print a test coverage report")
147 parser.add_option("-d", "--debug", action="store_true",
147 parser.add_option("-d", "--debug", action="store_true",
148 help="debug mode: write output of test scripts to console"
148 help="debug mode: write output of test scripts to console"
149 " rather than capturing and diff'ing it (disables timeout)")
149 " rather than capturing and diff'ing it (disables timeout)")
150 parser.add_option("-f", "--first", action="store_true",
150 parser.add_option("-f", "--first", action="store_true",
151 help="exit on the first test failure")
151 help="exit on the first test failure")
152 parser.add_option("-H", "--htmlcov", action="store_true",
152 parser.add_option("-H", "--htmlcov", action="store_true",
153 help="create an HTML report of the coverage of the files")
153 help="create an HTML report of the coverage of the files")
154 parser.add_option("--inotify", action="store_true",
154 parser.add_option("--inotify", action="store_true",
155 help="enable inotify extension when running tests")
155 help="enable inotify extension when running tests")
156 parser.add_option("-i", "--interactive", action="store_true",
156 parser.add_option("-i", "--interactive", action="store_true",
157 help="prompt to accept changed output")
157 help="prompt to accept changed output")
158 parser.add_option("-j", "--jobs", type="int",
158 parser.add_option("-j", "--jobs", type="int",
159 help="number of jobs to run in parallel"
159 help="number of jobs to run in parallel"
160 " (default: $%s or %d)" % defaults['jobs'])
160 " (default: $%s or %d)" % defaults['jobs'])
161 parser.add_option("--keep-tmpdir", action="store_true",
161 parser.add_option("--keep-tmpdir", action="store_true",
162 help="keep temporary directory after running tests")
162 help="keep temporary directory after running tests")
163 parser.add_option("-k", "--keywords",
163 parser.add_option("-k", "--keywords",
164 help="run tests matching keywords")
164 help="run tests matching keywords")
165 parser.add_option("-l", "--local", action="store_true",
165 parser.add_option("-l", "--local", action="store_true",
166 help="shortcut for --with-hg=<testdir>/../hg")
166 help="shortcut for --with-hg=<testdir>/../hg")
167 parser.add_option("--loop", action="store_true",
167 parser.add_option("--loop", action="store_true",
168 help="loop tests repeatedly")
168 help="loop tests repeatedly")
169 parser.add_option("-n", "--nodiff", action="store_true",
169 parser.add_option("-n", "--nodiff", action="store_true",
170 help="skip showing test changes")
170 help="skip showing test changes")
171 parser.add_option("-p", "--port", type="int",
171 parser.add_option("-p", "--port", type="int",
172 help="port on which servers should listen"
172 help="port on which servers should listen"
173 " (default: $%s or %d)" % defaults['port'])
173 " (default: $%s or %d)" % defaults['port'])
174 parser.add_option("--compiler", type="string",
174 parser.add_option("--compiler", type="string",
175 help="compiler to build with")
175 help="compiler to build with")
176 parser.add_option("--pure", action="store_true",
176 parser.add_option("--pure", action="store_true",
177 help="use pure Python code instead of C extensions")
177 help="use pure Python code instead of C extensions")
178 parser.add_option("-R", "--restart", action="store_true",
178 parser.add_option("-R", "--restart", action="store_true",
179 help="restart at last error")
179 help="restart at last error")
180 parser.add_option("-r", "--retest", action="store_true",
180 parser.add_option("-r", "--retest", action="store_true",
181 help="retest failed tests")
181 help="retest failed tests")
182 parser.add_option("-S", "--noskips", action="store_true",
182 parser.add_option("-S", "--noskips", action="store_true",
183 help="don't report skip tests verbosely")
183 help="don't report skip tests verbosely")
184 parser.add_option("--shell", type="string",
184 parser.add_option("--shell", type="string",
185 help="shell to use (default: $%s or %s)" % defaults['shell'])
185 help="shell to use (default: $%s or %s)" % defaults['shell'])
186 parser.add_option("-t", "--timeout", type="int",
186 parser.add_option("-t", "--timeout", type="int",
187 help="kill errant tests after TIMEOUT seconds"
187 help="kill errant tests after TIMEOUT seconds"
188 " (default: $%s or %d)" % defaults['timeout'])
188 " (default: $%s or %d)" % defaults['timeout'])
189 parser.add_option("--time", action="store_true",
189 parser.add_option("--time", action="store_true",
190 help="time how long each test takes")
190 help="time how long each test takes")
191 parser.add_option("--tmpdir", type="string",
191 parser.add_option("--tmpdir", type="string",
192 help="run tests in the given temporary directory"
192 help="run tests in the given temporary directory"
193 " (implies --keep-tmpdir)")
193 " (implies --keep-tmpdir)")
194 parser.add_option("-v", "--verbose", action="store_true",
194 parser.add_option("-v", "--verbose", action="store_true",
195 help="output verbose messages")
195 help="output verbose messages")
196 parser.add_option("--view", type="string",
196 parser.add_option("--view", type="string",
197 help="external diff viewer")
197 help="external diff viewer")
198 parser.add_option("--with-hg", type="string",
198 parser.add_option("--with-hg", type="string",
199 metavar="HG",
199 metavar="HG",
200 help="test using specified hg script rather than a "
200 help="test using specified hg script rather than a "
201 "temporary installation")
201 "temporary installation")
202 parser.add_option("-3", "--py3k-warnings", action="store_true",
202 parser.add_option("-3", "--py3k-warnings", action="store_true",
203 help="enable Py3k warnings on Python 2.6+")
203 help="enable Py3k warnings on Python 2.6+")
204 parser.add_option('--extra-config-opt', action="append",
204 parser.add_option('--extra-config-opt', action="append",
205 help='set the given config opt in the test hgrc')
205 help='set the given config opt in the test hgrc')
206 parser.add_option('--random', action="store_true",
206 parser.add_option('--random', action="store_true",
207 help='run tests in random order')
207 help='run tests in random order')
208
208
209 for option, (envvar, default) in defaults.items():
209 for option, (envvar, default) in defaults.items():
210 defaults[option] = type(default)(os.environ.get(envvar, default))
210 defaults[option] = type(default)(os.environ.get(envvar, default))
211 parser.set_defaults(**defaults)
211 parser.set_defaults(**defaults)
212 (options, args) = parser.parse_args()
212 (options, args) = parser.parse_args()
213
213
214 # jython is always pure
214 # jython is always pure
215 if 'java' in sys.platform or '__pypy__' in sys.modules:
215 if 'java' in sys.platform or '__pypy__' in sys.modules:
216 options.pure = True
216 options.pure = True
217
217
218 if options.with_hg:
218 if options.with_hg:
219 options.with_hg = os.path.expanduser(options.with_hg)
219 options.with_hg = os.path.expanduser(options.with_hg)
220 if not (os.path.isfile(options.with_hg) and
220 if not (os.path.isfile(options.with_hg) and
221 os.access(options.with_hg, os.X_OK)):
221 os.access(options.with_hg, os.X_OK)):
222 parser.error('--with-hg must specify an executable hg script')
222 parser.error('--with-hg must specify an executable hg script')
223 if not os.path.basename(options.with_hg) == 'hg':
223 if not os.path.basename(options.with_hg) == 'hg':
224 sys.stderr.write('warning: --with-hg should specify an hg script\n')
224 sys.stderr.write('warning: --with-hg should specify an hg script\n')
225 if options.local:
225 if options.local:
226 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
226 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
227 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
227 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
228 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
228 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
229 parser.error('--local specified, but %r not found or not executable'
229 parser.error('--local specified, but %r not found or not executable'
230 % hgbin)
230 % hgbin)
231 options.with_hg = hgbin
231 options.with_hg = hgbin
232
232
233 options.anycoverage = options.cover or options.annotate or options.htmlcov
233 options.anycoverage = options.cover or options.annotate or options.htmlcov
234 if options.anycoverage:
234 if options.anycoverage:
235 try:
235 try:
236 import coverage
236 import coverage
237 covver = version.StrictVersion(coverage.__version__).version
237 covver = version.StrictVersion(coverage.__version__).version
238 if covver < (3, 3):
238 if covver < (3, 3):
239 parser.error('coverage options require coverage 3.3 or later')
239 parser.error('coverage options require coverage 3.3 or later')
240 except ImportError:
240 except ImportError:
241 parser.error('coverage options now require the coverage package')
241 parser.error('coverage options now require the coverage package')
242
242
243 if options.anycoverage and options.local:
243 if options.anycoverage and options.local:
244 # this needs some path mangling somewhere, I guess
244 # this needs some path mangling somewhere, I guess
245 parser.error("sorry, coverage options do not work when --local "
245 parser.error("sorry, coverage options do not work when --local "
246 "is specified")
246 "is specified")
247
247
248 global verbose
248 global verbose
249 if options.verbose:
249 if options.verbose:
250 verbose = ''
250 verbose = ''
251
251
252 if options.tmpdir:
252 if options.tmpdir:
253 options.tmpdir = os.path.expanduser(options.tmpdir)
253 options.tmpdir = os.path.expanduser(options.tmpdir)
254
254
255 if options.jobs < 1:
255 if options.jobs < 1:
256 parser.error('--jobs must be positive')
256 parser.error('--jobs must be positive')
257 if options.interactive and options.debug:
257 if options.interactive and options.debug:
258 parser.error("-i/--interactive and -d/--debug are incompatible")
258 parser.error("-i/--interactive and -d/--debug are incompatible")
259 if options.debug:
259 if options.debug:
260 if options.timeout != defaults['timeout']:
260 if options.timeout != defaults['timeout']:
261 sys.stderr.write(
261 sys.stderr.write(
262 'warning: --timeout option ignored with --debug\n')
262 'warning: --timeout option ignored with --debug\n')
263 options.timeout = 0
263 options.timeout = 0
264 if options.py3k_warnings:
264 if options.py3k_warnings:
265 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
265 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
266 parser.error('--py3k-warnings can only be used on Python 2.6+')
266 parser.error('--py3k-warnings can only be used on Python 2.6+')
267 if options.blacklist:
267 if options.blacklist:
268 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
268 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
269 if options.whitelist:
269 if options.whitelist:
270 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
270 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
271 else:
271 else:
272 options.whitelisted = {}
272 options.whitelisted = {}
273
273
274 return (options, args)
274 return (options, args)
275
275
276 def rename(src, dst):
276 def rename(src, dst):
277 """Like os.rename(), trade atomicity and opened files friendliness
277 """Like os.rename(), trade atomicity and opened files friendliness
278 for existing destination support.
278 for existing destination support.
279 """
279 """
280 shutil.copy(src, dst)
280 shutil.copy(src, dst)
281 os.remove(src)
281 os.remove(src)
282
282
283 def parsehghaveoutput(lines):
283 def parsehghaveoutput(lines):
284 '''Parse hghave log lines.
284 '''Parse hghave log lines.
285 Return tuple of lists (missing, failed):
285 Return tuple of lists (missing, failed):
286 * the missing/unknown features
286 * the missing/unknown features
287 * the features for which existence check failed'''
287 * the features for which existence check failed'''
288 missing = []
288 missing = []
289 failed = []
289 failed = []
290 for line in lines:
290 for line in lines:
291 if line.startswith(SKIPPED_PREFIX):
291 if line.startswith(SKIPPED_PREFIX):
292 line = line.splitlines()[0]
292 line = line.splitlines()[0]
293 missing.append(line[len(SKIPPED_PREFIX):])
293 missing.append(line[len(SKIPPED_PREFIX):])
294 elif line.startswith(FAILED_PREFIX):
294 elif line.startswith(FAILED_PREFIX):
295 line = line.splitlines()[0]
295 line = line.splitlines()[0]
296 failed.append(line[len(FAILED_PREFIX):])
296 failed.append(line[len(FAILED_PREFIX):])
297
297
298 return missing, failed
298 return missing, failed
299
299
300 def showdiff(expected, output, ref, err):
300 def showdiff(expected, output, ref, err):
301 print
301 print
302 for line in difflib.unified_diff(expected, output, ref, err):
302 for line in difflib.unified_diff(expected, output, ref, err):
303 sys.stdout.write(line)
303 sys.stdout.write(line)
304
304
305 verbose = False
305 verbose = False
306 def vlog(*msg):
306 def vlog(*msg):
307 if verbose is not False:
307 if verbose is not False:
308 iolock.acquire()
308 iolock.acquire()
309 if verbose:
309 if verbose:
310 print verbose,
310 print verbose,
311 for m in msg:
311 for m in msg:
312 print m,
312 print m,
313 print
313 print
314 sys.stdout.flush()
314 sys.stdout.flush()
315 iolock.release()
315 iolock.release()
316
316
317 def log(*msg):
317 def log(*msg):
318 iolock.acquire()
318 iolock.acquire()
319 if verbose:
319 if verbose:
320 print verbose,
320 print verbose,
321 for m in msg:
321 for m in msg:
322 print m,
322 print m,
323 print
323 print
324 sys.stdout.flush()
324 sys.stdout.flush()
325 iolock.release()
325 iolock.release()
326
326
327 def findprogram(program):
327 def findprogram(program):
328 """Search PATH for a executable program"""
328 """Search PATH for a executable program"""
329 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
329 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
330 name = os.path.join(p, program)
330 name = os.path.join(p, program)
331 if os.name == 'nt' or os.access(name, os.X_OK):
331 if os.name == 'nt' or os.access(name, os.X_OK):
332 return name
332 return name
333 return None
333 return None
334
334
335 def createhgrc(path, options):
335 def createhgrc(path, options):
336 # create a fresh hgrc
336 # create a fresh hgrc
337 hgrc = open(path, 'w')
337 hgrc = open(path, 'w')
338 hgrc.write('[ui]\n')
338 hgrc.write('[ui]\n')
339 hgrc.write('slash = True\n')
339 hgrc.write('slash = True\n')
340 hgrc.write('interactive = False\n')
340 hgrc.write('interactive = False\n')
341 hgrc.write('[defaults]\n')
341 hgrc.write('[defaults]\n')
342 hgrc.write('backout = -d "0 0"\n')
342 hgrc.write('backout = -d "0 0"\n')
343 hgrc.write('commit = -d "0 0"\n')
343 hgrc.write('commit = -d "0 0"\n')
344 hgrc.write('shelve = --date "0 0"\n')
344 hgrc.write('tag = -d "0 0"\n')
345 hgrc.write('tag = -d "0 0"\n')
345 if options.inotify:
346 if options.inotify:
346 hgrc.write('[extensions]\n')
347 hgrc.write('[extensions]\n')
347 hgrc.write('inotify=\n')
348 hgrc.write('inotify=\n')
348 hgrc.write('[inotify]\n')
349 hgrc.write('[inotify]\n')
349 hgrc.write('pidfile=daemon.pids')
350 hgrc.write('pidfile=daemon.pids')
350 hgrc.write('appendpid=True\n')
351 hgrc.write('appendpid=True\n')
351 if options.extra_config_opt:
352 if options.extra_config_opt:
352 for opt in options.extra_config_opt:
353 for opt in options.extra_config_opt:
353 section, key = opt.split('.', 1)
354 section, key = opt.split('.', 1)
354 assert '=' in key, ('extra config opt %s must '
355 assert '=' in key, ('extra config opt %s must '
355 'have an = for assignment' % opt)
356 'have an = for assignment' % opt)
356 hgrc.write('[%s]\n%s\n' % (section, key))
357 hgrc.write('[%s]\n%s\n' % (section, key))
357 hgrc.close()
358 hgrc.close()
358
359
359 def createenv(options, testtmp, threadtmp, port):
360 def createenv(options, testtmp, threadtmp, port):
360 env = os.environ.copy()
361 env = os.environ.copy()
361 env['TESTTMP'] = testtmp
362 env['TESTTMP'] = testtmp
362 env['HOME'] = testtmp
363 env['HOME'] = testtmp
363 env["HGPORT"] = str(port)
364 env["HGPORT"] = str(port)
364 env["HGPORT1"] = str(port + 1)
365 env["HGPORT1"] = str(port + 1)
365 env["HGPORT2"] = str(port + 2)
366 env["HGPORT2"] = str(port + 2)
366 env["HGRCPATH"] = os.path.join(threadtmp, '.hgrc')
367 env["HGRCPATH"] = os.path.join(threadtmp, '.hgrc')
367 env["DAEMON_PIDS"] = os.path.join(threadtmp, 'daemon.pids')
368 env["DAEMON_PIDS"] = os.path.join(threadtmp, 'daemon.pids')
368 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
369 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
369 env["HGMERGE"] = "internal:merge"
370 env["HGMERGE"] = "internal:merge"
370 env["HGUSER"] = "test"
371 env["HGUSER"] = "test"
371 env["HGENCODING"] = "ascii"
372 env["HGENCODING"] = "ascii"
372 env["HGENCODINGMODE"] = "strict"
373 env["HGENCODINGMODE"] = "strict"
373
374
374 # Reset some environment variables to well-known values so that
375 # Reset some environment variables to well-known values so that
375 # the tests produce repeatable output.
376 # the tests produce repeatable output.
376 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
377 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
377 env['TZ'] = 'GMT'
378 env['TZ'] = 'GMT'
378 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
379 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
379 env['COLUMNS'] = '80'
380 env['COLUMNS'] = '80'
380 env['TERM'] = 'xterm'
381 env['TERM'] = 'xterm'
381
382
382 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
383 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
383 'NO_PROXY').split():
384 'NO_PROXY').split():
384 if k in env:
385 if k in env:
385 del env[k]
386 del env[k]
386
387
387 # unset env related to hooks
388 # unset env related to hooks
388 for k in env.keys():
389 for k in env.keys():
389 if k.startswith('HG_'):
390 if k.startswith('HG_'):
390 del env[k]
391 del env[k]
391
392
392 return env
393 return env
393
394
394 def checktools():
395 def checktools():
395 # Before we go any further, check for pre-requisite tools
396 # Before we go any further, check for pre-requisite tools
396 # stuff from coreutils (cat, rm, etc) are not tested
397 # stuff from coreutils (cat, rm, etc) are not tested
397 for p in requiredtools:
398 for p in requiredtools:
398 if os.name == 'nt' and not p.endswith('.exe'):
399 if os.name == 'nt' and not p.endswith('.exe'):
399 p += '.exe'
400 p += '.exe'
400 found = findprogram(p)
401 found = findprogram(p)
401 if found:
402 if found:
402 vlog("# Found prerequisite", p, "at", found)
403 vlog("# Found prerequisite", p, "at", found)
403 else:
404 else:
404 print "WARNING: Did not find prerequisite tool: "+p
405 print "WARNING: Did not find prerequisite tool: "+p
405
406
406 def terminate(proc):
407 def terminate(proc):
407 """Terminate subprocess (with fallback for Python versions < 2.6)"""
408 """Terminate subprocess (with fallback for Python versions < 2.6)"""
408 vlog('# Terminating process %d' % proc.pid)
409 vlog('# Terminating process %d' % proc.pid)
409 try:
410 try:
410 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
411 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
411 except OSError:
412 except OSError:
412 pass
413 pass
413
414
414 def killdaemons(pidfile):
415 def killdaemons(pidfile):
415 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
416 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
416 logfn=vlog)
417 logfn=vlog)
417
418
418 def cleanup(options):
419 def cleanup(options):
419 if not options.keep_tmpdir:
420 if not options.keep_tmpdir:
420 vlog("# Cleaning up HGTMP", HGTMP)
421 vlog("# Cleaning up HGTMP", HGTMP)
421 shutil.rmtree(HGTMP, True)
422 shutil.rmtree(HGTMP, True)
422
423
423 def usecorrectpython():
424 def usecorrectpython():
424 # some tests run python interpreter. they must use same
425 # some tests run python interpreter. they must use same
425 # interpreter we use or bad things will happen.
426 # interpreter we use or bad things will happen.
426 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
427 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
427 if getattr(os, 'symlink', None):
428 if getattr(os, 'symlink', None):
428 vlog("# Making python executable in test path a symlink to '%s'" %
429 vlog("# Making python executable in test path a symlink to '%s'" %
429 sys.executable)
430 sys.executable)
430 mypython = os.path.join(BINDIR, pyexename)
431 mypython = os.path.join(BINDIR, pyexename)
431 try:
432 try:
432 if os.readlink(mypython) == sys.executable:
433 if os.readlink(mypython) == sys.executable:
433 return
434 return
434 os.unlink(mypython)
435 os.unlink(mypython)
435 except OSError, err:
436 except OSError, err:
436 if err.errno != errno.ENOENT:
437 if err.errno != errno.ENOENT:
437 raise
438 raise
438 if findprogram(pyexename) != sys.executable:
439 if findprogram(pyexename) != sys.executable:
439 try:
440 try:
440 os.symlink(sys.executable, mypython)
441 os.symlink(sys.executable, mypython)
441 except OSError, err:
442 except OSError, err:
442 # child processes may race, which is harmless
443 # child processes may race, which is harmless
443 if err.errno != errno.EEXIST:
444 if err.errno != errno.EEXIST:
444 raise
445 raise
445 else:
446 else:
446 exedir, exename = os.path.split(sys.executable)
447 exedir, exename = os.path.split(sys.executable)
447 vlog("# Modifying search path to find %s as %s in '%s'" %
448 vlog("# Modifying search path to find %s as %s in '%s'" %
448 (exename, pyexename, exedir))
449 (exename, pyexename, exedir))
449 path = os.environ['PATH'].split(os.pathsep)
450 path = os.environ['PATH'].split(os.pathsep)
450 while exedir in path:
451 while exedir in path:
451 path.remove(exedir)
452 path.remove(exedir)
452 os.environ['PATH'] = os.pathsep.join([exedir] + path)
453 os.environ['PATH'] = os.pathsep.join([exedir] + path)
453 if not findprogram(pyexename):
454 if not findprogram(pyexename):
454 print "WARNING: Cannot find %s in search path" % pyexename
455 print "WARNING: Cannot find %s in search path" % pyexename
455
456
456 def installhg(options):
457 def installhg(options):
457 vlog("# Performing temporary installation of HG")
458 vlog("# Performing temporary installation of HG")
458 installerrs = os.path.join("tests", "install.err")
459 installerrs = os.path.join("tests", "install.err")
459 compiler = ''
460 compiler = ''
460 if options.compiler:
461 if options.compiler:
461 compiler = '--compiler ' + options.compiler
462 compiler = '--compiler ' + options.compiler
462 pure = options.pure and "--pure" or ""
463 pure = options.pure and "--pure" or ""
463 py3 = ''
464 py3 = ''
464 if sys.version_info[0] == 3:
465 if sys.version_info[0] == 3:
465 py3 = '--c2to3'
466 py3 = '--c2to3'
466
467
467 # Run installer in hg root
468 # Run installer in hg root
468 script = os.path.realpath(sys.argv[0])
469 script = os.path.realpath(sys.argv[0])
469 hgroot = os.path.dirname(os.path.dirname(script))
470 hgroot = os.path.dirname(os.path.dirname(script))
470 os.chdir(hgroot)
471 os.chdir(hgroot)
471 nohome = '--home=""'
472 nohome = '--home=""'
472 if os.name == 'nt':
473 if os.name == 'nt':
473 # The --home="" trick works only on OS where os.sep == '/'
474 # The --home="" trick works only on OS where os.sep == '/'
474 # because of a distutils convert_path() fast-path. Avoid it at
475 # because of a distutils convert_path() fast-path. Avoid it at
475 # least on Windows for now, deal with .pydistutils.cfg bugs
476 # least on Windows for now, deal with .pydistutils.cfg bugs
476 # when they happen.
477 # when they happen.
477 nohome = ''
478 nohome = ''
478 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
479 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
479 ' build %(compiler)s --build-base="%(base)s"'
480 ' build %(compiler)s --build-base="%(base)s"'
480 ' install --force --prefix="%(prefix)s" --install-lib="%(libdir)s"'
481 ' install --force --prefix="%(prefix)s" --install-lib="%(libdir)s"'
481 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
482 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
482 % dict(exe=sys.executable, py3=py3, pure=pure, compiler=compiler,
483 % dict(exe=sys.executable, py3=py3, pure=pure, compiler=compiler,
483 base=os.path.join(HGTMP, "build"),
484 base=os.path.join(HGTMP, "build"),
484 prefix=INST, libdir=PYTHONDIR, bindir=BINDIR,
485 prefix=INST, libdir=PYTHONDIR, bindir=BINDIR,
485 nohome=nohome, logfile=installerrs))
486 nohome=nohome, logfile=installerrs))
486 vlog("# Running", cmd)
487 vlog("# Running", cmd)
487 if os.system(cmd) == 0:
488 if os.system(cmd) == 0:
488 if not options.verbose:
489 if not options.verbose:
489 os.remove(installerrs)
490 os.remove(installerrs)
490 else:
491 else:
491 f = open(installerrs)
492 f = open(installerrs)
492 for line in f:
493 for line in f:
493 print line,
494 print line,
494 f.close()
495 f.close()
495 sys.exit(1)
496 sys.exit(1)
496 os.chdir(TESTDIR)
497 os.chdir(TESTDIR)
497
498
498 usecorrectpython()
499 usecorrectpython()
499
500
500 vlog("# Installing dummy diffstat")
501 vlog("# Installing dummy diffstat")
501 f = open(os.path.join(BINDIR, 'diffstat'), 'w')
502 f = open(os.path.join(BINDIR, 'diffstat'), 'w')
502 f.write('#!' + sys.executable + '\n'
503 f.write('#!' + sys.executable + '\n'
503 'import sys\n'
504 'import sys\n'
504 'files = 0\n'
505 'files = 0\n'
505 'for line in sys.stdin:\n'
506 'for line in sys.stdin:\n'
506 ' if line.startswith("diff "):\n'
507 ' if line.startswith("diff "):\n'
507 ' files += 1\n'
508 ' files += 1\n'
508 'sys.stdout.write("files patched: %d\\n" % files)\n')
509 'sys.stdout.write("files patched: %d\\n" % files)\n')
509 f.close()
510 f.close()
510 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
511 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
511
512
512 if options.py3k_warnings and not options.anycoverage:
513 if options.py3k_warnings and not options.anycoverage:
513 vlog("# Updating hg command to enable Py3k Warnings switch")
514 vlog("# Updating hg command to enable Py3k Warnings switch")
514 f = open(os.path.join(BINDIR, 'hg'), 'r')
515 f = open(os.path.join(BINDIR, 'hg'), 'r')
515 lines = [line.rstrip() for line in f]
516 lines = [line.rstrip() for line in f]
516 lines[0] += ' -3'
517 lines[0] += ' -3'
517 f.close()
518 f.close()
518 f = open(os.path.join(BINDIR, 'hg'), 'w')
519 f = open(os.path.join(BINDIR, 'hg'), 'w')
519 for line in lines:
520 for line in lines:
520 f.write(line + '\n')
521 f.write(line + '\n')
521 f.close()
522 f.close()
522
523
523 hgbat = os.path.join(BINDIR, 'hg.bat')
524 hgbat = os.path.join(BINDIR, 'hg.bat')
524 if os.path.isfile(hgbat):
525 if os.path.isfile(hgbat):
525 # hg.bat expects to be put in bin/scripts while run-tests.py
526 # hg.bat expects to be put in bin/scripts while run-tests.py
526 # installation layout put it in bin/ directly. Fix it
527 # installation layout put it in bin/ directly. Fix it
527 f = open(hgbat, 'rb')
528 f = open(hgbat, 'rb')
528 data = f.read()
529 data = f.read()
529 f.close()
530 f.close()
530 if '"%~dp0..\python" "%~dp0hg" %*' in data:
531 if '"%~dp0..\python" "%~dp0hg" %*' in data:
531 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
532 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
532 '"%~dp0python" "%~dp0hg" %*')
533 '"%~dp0python" "%~dp0hg" %*')
533 f = open(hgbat, 'wb')
534 f = open(hgbat, 'wb')
534 f.write(data)
535 f.write(data)
535 f.close()
536 f.close()
536 else:
537 else:
537 print 'WARNING: cannot fix hg.bat reference to python.exe'
538 print 'WARNING: cannot fix hg.bat reference to python.exe'
538
539
539 if options.anycoverage:
540 if options.anycoverage:
540 custom = os.path.join(TESTDIR, 'sitecustomize.py')
541 custom = os.path.join(TESTDIR, 'sitecustomize.py')
541 target = os.path.join(PYTHONDIR, 'sitecustomize.py')
542 target = os.path.join(PYTHONDIR, 'sitecustomize.py')
542 vlog('# Installing coverage trigger to %s' % target)
543 vlog('# Installing coverage trigger to %s' % target)
543 shutil.copyfile(custom, target)
544 shutil.copyfile(custom, target)
544 rc = os.path.join(TESTDIR, '.coveragerc')
545 rc = os.path.join(TESTDIR, '.coveragerc')
545 vlog('# Installing coverage rc to %s' % rc)
546 vlog('# Installing coverage rc to %s' % rc)
546 os.environ['COVERAGE_PROCESS_START'] = rc
547 os.environ['COVERAGE_PROCESS_START'] = rc
547 fn = os.path.join(INST, '..', '.coverage')
548 fn = os.path.join(INST, '..', '.coverage')
548 os.environ['COVERAGE_FILE'] = fn
549 os.environ['COVERAGE_FILE'] = fn
549
550
550 def outputtimes(options):
551 def outputtimes(options):
551 vlog('# Producing time report')
552 vlog('# Producing time report')
552 times.sort(key=lambda t: (t[1], t[0]), reverse=True)
553 times.sort(key=lambda t: (t[1], t[0]), reverse=True)
553 cols = '%7.3f %s'
554 cols = '%7.3f %s'
554 print '\n%-7s %s' % ('Time', 'Test')
555 print '\n%-7s %s' % ('Time', 'Test')
555 for test, timetaken in times:
556 for test, timetaken in times:
556 print cols % (timetaken, test)
557 print cols % (timetaken, test)
557
558
558 def outputcoverage(options):
559 def outputcoverage(options):
559
560
560 vlog('# Producing coverage report')
561 vlog('# Producing coverage report')
561 os.chdir(PYTHONDIR)
562 os.chdir(PYTHONDIR)
562
563
563 def covrun(*args):
564 def covrun(*args):
564 cmd = 'coverage %s' % ' '.join(args)
565 cmd = 'coverage %s' % ' '.join(args)
565 vlog('# Running: %s' % cmd)
566 vlog('# Running: %s' % cmd)
566 os.system(cmd)
567 os.system(cmd)
567
568
568 covrun('-c')
569 covrun('-c')
569 omit = ','.join(os.path.join(x, '*') for x in [BINDIR, TESTDIR])
570 omit = ','.join(os.path.join(x, '*') for x in [BINDIR, TESTDIR])
570 covrun('-i', '-r', '"--omit=%s"' % omit) # report
571 covrun('-i', '-r', '"--omit=%s"' % omit) # report
571 if options.htmlcov:
572 if options.htmlcov:
572 htmldir = os.path.join(TESTDIR, 'htmlcov')
573 htmldir = os.path.join(TESTDIR, 'htmlcov')
573 covrun('-i', '-b', '"--directory=%s"' % htmldir, '"--omit=%s"' % omit)
574 covrun('-i', '-b', '"--directory=%s"' % htmldir, '"--omit=%s"' % omit)
574 if options.annotate:
575 if options.annotate:
575 adir = os.path.join(TESTDIR, 'annotated')
576 adir = os.path.join(TESTDIR, 'annotated')
576 if not os.path.isdir(adir):
577 if not os.path.isdir(adir):
577 os.mkdir(adir)
578 os.mkdir(adir)
578 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
579 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
579
580
580 def pytest(test, wd, options, replacements, env):
581 def pytest(test, wd, options, replacements, env):
581 py3kswitch = options.py3k_warnings and ' -3' or ''
582 py3kswitch = options.py3k_warnings and ' -3' or ''
582 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
583 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
583 vlog("# Running", cmd)
584 vlog("# Running", cmd)
584 if os.name == 'nt':
585 if os.name == 'nt':
585 replacements.append((r'\r\n', '\n'))
586 replacements.append((r'\r\n', '\n'))
586 return run(cmd, wd, options, replacements, env)
587 return run(cmd, wd, options, replacements, env)
587
588
588 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
589 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
589 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
590 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
590 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
591 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
591 escapemap.update({'\\': '\\\\', '\r': r'\r'})
592 escapemap.update({'\\': '\\\\', '\r': r'\r'})
592 def escapef(m):
593 def escapef(m):
593 return escapemap[m.group(0)]
594 return escapemap[m.group(0)]
594 def stringescape(s):
595 def stringescape(s):
595 return escapesub(escapef, s)
596 return escapesub(escapef, s)
596
597
597 def rematch(el, l):
598 def rematch(el, l):
598 try:
599 try:
599 # use \Z to ensure that the regex matches to the end of the string
600 # use \Z to ensure that the regex matches to the end of the string
600 if os.name == 'nt':
601 if os.name == 'nt':
601 return re.match(el + r'\r?\n\Z', l)
602 return re.match(el + r'\r?\n\Z', l)
602 return re.match(el + r'\n\Z', l)
603 return re.match(el + r'\n\Z', l)
603 except re.error:
604 except re.error:
604 # el is an invalid regex
605 # el is an invalid regex
605 return False
606 return False
606
607
607 def globmatch(el, l):
608 def globmatch(el, l):
608 # The only supported special characters are * and ? plus / which also
609 # The only supported special characters are * and ? plus / which also
609 # matches \ on windows. Escaping of these caracters is supported.
610 # matches \ on windows. Escaping of these caracters is supported.
610 if el + '\n' == l:
611 if el + '\n' == l:
611 if os.altsep:
612 if os.altsep:
612 # matching on "/" is not needed for this line
613 # matching on "/" is not needed for this line
613 log("\nInfo, unnecessary glob: %s (glob)" % el)
614 log("\nInfo, unnecessary glob: %s (glob)" % el)
614 return True
615 return True
615 i, n = 0, len(el)
616 i, n = 0, len(el)
616 res = ''
617 res = ''
617 while i < n:
618 while i < n:
618 c = el[i]
619 c = el[i]
619 i += 1
620 i += 1
620 if c == '\\' and el[i] in '*?\\/':
621 if c == '\\' and el[i] in '*?\\/':
621 res += el[i - 1:i + 1]
622 res += el[i - 1:i + 1]
622 i += 1
623 i += 1
623 elif c == '*':
624 elif c == '*':
624 res += '.*'
625 res += '.*'
625 elif c == '?':
626 elif c == '?':
626 res += '.'
627 res += '.'
627 elif c == '/' and os.altsep:
628 elif c == '/' and os.altsep:
628 res += '[/\\\\]'
629 res += '[/\\\\]'
629 else:
630 else:
630 res += re.escape(c)
631 res += re.escape(c)
631 return rematch(res, l)
632 return rematch(res, l)
632
633
633 def linematch(el, l):
634 def linematch(el, l):
634 if el == l: # perfect match (fast)
635 if el == l: # perfect match (fast)
635 return True
636 return True
636 if el:
637 if el:
637 if el.endswith(" (esc)\n"):
638 if el.endswith(" (esc)\n"):
638 el = el[:-7].decode('string-escape') + '\n'
639 el = el[:-7].decode('string-escape') + '\n'
639 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
640 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
640 return True
641 return True
641 if (el.endswith(" (re)\n") and rematch(el[:-6], l) or
642 if (el.endswith(" (re)\n") and rematch(el[:-6], l) or
642 el.endswith(" (glob)\n") and globmatch(el[:-8], l)):
643 el.endswith(" (glob)\n") and globmatch(el[:-8], l)):
643 return True
644 return True
644 return False
645 return False
645
646
646 def tsttest(test, wd, options, replacements, env):
647 def tsttest(test, wd, options, replacements, env):
647 # We generate a shell script which outputs unique markers to line
648 # We generate a shell script which outputs unique markers to line
648 # up script results with our source. These markers include input
649 # up script results with our source. These markers include input
649 # line number and the last return code
650 # line number and the last return code
650 salt = "SALT" + str(time.time())
651 salt = "SALT" + str(time.time())
651 def addsalt(line, inpython):
652 def addsalt(line, inpython):
652 if inpython:
653 if inpython:
653 script.append('%s %d 0\n' % (salt, line))
654 script.append('%s %d 0\n' % (salt, line))
654 else:
655 else:
655 script.append('echo %s %s $?\n' % (salt, line))
656 script.append('echo %s %s $?\n' % (salt, line))
656
657
657 # After we run the shell script, we re-unify the script output
658 # After we run the shell script, we re-unify the script output
658 # with non-active parts of the source, with synchronization by our
659 # with non-active parts of the source, with synchronization by our
659 # SALT line number markers. The after table contains the
660 # SALT line number markers. The after table contains the
660 # non-active components, ordered by line number
661 # non-active components, ordered by line number
661 after = {}
662 after = {}
662 pos = prepos = -1
663 pos = prepos = -1
663
664
664 # Expected shellscript output
665 # Expected shellscript output
665 expected = {}
666 expected = {}
666
667
667 # We keep track of whether or not we're in a Python block so we
668 # We keep track of whether or not we're in a Python block so we
668 # can generate the surrounding doctest magic
669 # can generate the surrounding doctest magic
669 inpython = False
670 inpython = False
670
671
671 # True or False when in a true or false conditional section
672 # True or False when in a true or false conditional section
672 skipping = None
673 skipping = None
673
674
674 def hghave(reqs):
675 def hghave(reqs):
675 # TODO: do something smarter when all other uses of hghave is gone
676 # TODO: do something smarter when all other uses of hghave is gone
676 tdir = TESTDIR.replace('\\', '/')
677 tdir = TESTDIR.replace('\\', '/')
677 proc = Popen4('%s -c "%s/hghave %s"' %
678 proc = Popen4('%s -c "%s/hghave %s"' %
678 (options.shell, tdir, ' '.join(reqs)), wd, 0)
679 (options.shell, tdir, ' '.join(reqs)), wd, 0)
679 stdout, stderr = proc.communicate()
680 stdout, stderr = proc.communicate()
680 ret = proc.wait()
681 ret = proc.wait()
681 if wifexited(ret):
682 if wifexited(ret):
682 ret = os.WEXITSTATUS(ret)
683 ret = os.WEXITSTATUS(ret)
683 if ret == 2:
684 if ret == 2:
684 print stdout
685 print stdout
685 sys.exit(1)
686 sys.exit(1)
686 return ret == 0
687 return ret == 0
687
688
688 f = open(test)
689 f = open(test)
689 t = f.readlines()
690 t = f.readlines()
690 f.close()
691 f.close()
691
692
692 script = []
693 script = []
693 if options.debug:
694 if options.debug:
694 script.append('set -x\n')
695 script.append('set -x\n')
695 if os.getenv('MSYSTEM'):
696 if os.getenv('MSYSTEM'):
696 script.append('alias pwd="pwd -W"\n')
697 script.append('alias pwd="pwd -W"\n')
697 n = 0
698 n = 0
698 for n, l in enumerate(t):
699 for n, l in enumerate(t):
699 if not l.endswith('\n'):
700 if not l.endswith('\n'):
700 l += '\n'
701 l += '\n'
701 if l.startswith('#if'):
702 if l.startswith('#if'):
702 if skipping is not None:
703 if skipping is not None:
703 after.setdefault(pos, []).append(' !!! nested #if\n')
704 after.setdefault(pos, []).append(' !!! nested #if\n')
704 skipping = not hghave(l.split()[1:])
705 skipping = not hghave(l.split()[1:])
705 after.setdefault(pos, []).append(l)
706 after.setdefault(pos, []).append(l)
706 elif l.startswith('#else'):
707 elif l.startswith('#else'):
707 if skipping is None:
708 if skipping is None:
708 after.setdefault(pos, []).append(' !!! missing #if\n')
709 after.setdefault(pos, []).append(' !!! missing #if\n')
709 skipping = not skipping
710 skipping = not skipping
710 after.setdefault(pos, []).append(l)
711 after.setdefault(pos, []).append(l)
711 elif l.startswith('#endif'):
712 elif l.startswith('#endif'):
712 if skipping is None:
713 if skipping is None:
713 after.setdefault(pos, []).append(' !!! missing #if\n')
714 after.setdefault(pos, []).append(' !!! missing #if\n')
714 skipping = None
715 skipping = None
715 after.setdefault(pos, []).append(l)
716 after.setdefault(pos, []).append(l)
716 elif skipping:
717 elif skipping:
717 after.setdefault(pos, []).append(l)
718 after.setdefault(pos, []).append(l)
718 elif l.startswith(' >>> '): # python inlines
719 elif l.startswith(' >>> '): # python inlines
719 after.setdefault(pos, []).append(l)
720 after.setdefault(pos, []).append(l)
720 prepos = pos
721 prepos = pos
721 pos = n
722 pos = n
722 if not inpython:
723 if not inpython:
723 # we've just entered a Python block, add the header
724 # we've just entered a Python block, add the header
724 inpython = True
725 inpython = True
725 addsalt(prepos, False) # make sure we report the exit code
726 addsalt(prepos, False) # make sure we report the exit code
726 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
727 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
727 addsalt(n, True)
728 addsalt(n, True)
728 script.append(l[2:])
729 script.append(l[2:])
729 elif l.startswith(' ... '): # python inlines
730 elif l.startswith(' ... '): # python inlines
730 after.setdefault(prepos, []).append(l)
731 after.setdefault(prepos, []).append(l)
731 script.append(l[2:])
732 script.append(l[2:])
732 elif l.startswith(' $ '): # commands
733 elif l.startswith(' $ '): # commands
733 if inpython:
734 if inpython:
734 script.append("EOF\n")
735 script.append("EOF\n")
735 inpython = False
736 inpython = False
736 after.setdefault(pos, []).append(l)
737 after.setdefault(pos, []).append(l)
737 prepos = pos
738 prepos = pos
738 pos = n
739 pos = n
739 addsalt(n, False)
740 addsalt(n, False)
740 cmd = l[4:].split()
741 cmd = l[4:].split()
741 if len(cmd) == 2 and cmd[0] == 'cd':
742 if len(cmd) == 2 and cmd[0] == 'cd':
742 l = ' $ cd %s || exit 1\n' % cmd[1]
743 l = ' $ cd %s || exit 1\n' % cmd[1]
743 script.append(l[4:])
744 script.append(l[4:])
744 elif l.startswith(' > '): # continuations
745 elif l.startswith(' > '): # continuations
745 after.setdefault(prepos, []).append(l)
746 after.setdefault(prepos, []).append(l)
746 script.append(l[4:])
747 script.append(l[4:])
747 elif l.startswith(' '): # results
748 elif l.startswith(' '): # results
748 # queue up a list of expected results
749 # queue up a list of expected results
749 expected.setdefault(pos, []).append(l[2:])
750 expected.setdefault(pos, []).append(l[2:])
750 else:
751 else:
751 if inpython:
752 if inpython:
752 script.append("EOF\n")
753 script.append("EOF\n")
753 inpython = False
754 inpython = False
754 # non-command/result - queue up for merged output
755 # non-command/result - queue up for merged output
755 after.setdefault(pos, []).append(l)
756 after.setdefault(pos, []).append(l)
756
757
757 if inpython:
758 if inpython:
758 script.append("EOF\n")
759 script.append("EOF\n")
759 if skipping is not None:
760 if skipping is not None:
760 after.setdefault(pos, []).append(' !!! missing #endif\n')
761 after.setdefault(pos, []).append(' !!! missing #endif\n')
761 addsalt(n + 1, False)
762 addsalt(n + 1, False)
762
763
763 # Write out the script and execute it
764 # Write out the script and execute it
764 fd, name = tempfile.mkstemp(suffix='hg-tst')
765 fd, name = tempfile.mkstemp(suffix='hg-tst')
765 try:
766 try:
766 for l in script:
767 for l in script:
767 os.write(fd, l)
768 os.write(fd, l)
768 os.close(fd)
769 os.close(fd)
769
770
770 cmd = '%s "%s"' % (options.shell, name)
771 cmd = '%s "%s"' % (options.shell, name)
771 vlog("# Running", cmd)
772 vlog("# Running", cmd)
772 exitcode, output = run(cmd, wd, options, replacements, env)
773 exitcode, output = run(cmd, wd, options, replacements, env)
773 # do not merge output if skipped, return hghave message instead
774 # do not merge output if skipped, return hghave message instead
774 # similarly, with --debug, output is None
775 # similarly, with --debug, output is None
775 if exitcode == SKIPPED_STATUS or output is None:
776 if exitcode == SKIPPED_STATUS or output is None:
776 return exitcode, output
777 return exitcode, output
777 finally:
778 finally:
778 os.remove(name)
779 os.remove(name)
779
780
780 # Merge the script output back into a unified test
781 # Merge the script output back into a unified test
781
782
782 pos = -1
783 pos = -1
783 postout = []
784 postout = []
784 ret = 0
785 ret = 0
785 for l in output:
786 for l in output:
786 lout, lcmd = l, None
787 lout, lcmd = l, None
787 if salt in l:
788 if salt in l:
788 lout, lcmd = l.split(salt, 1)
789 lout, lcmd = l.split(salt, 1)
789
790
790 if lout:
791 if lout:
791 if not lout.endswith('\n'):
792 if not lout.endswith('\n'):
792 lout += ' (no-eol)\n'
793 lout += ' (no-eol)\n'
793
794
794 # find the expected output at the current position
795 # find the expected output at the current position
795 el = None
796 el = None
796 if pos in expected and expected[pos]:
797 if pos in expected and expected[pos]:
797 el = expected[pos].pop(0)
798 el = expected[pos].pop(0)
798
799
799 if linematch(el, lout):
800 if linematch(el, lout):
800 postout.append(" " + el)
801 postout.append(" " + el)
801 else:
802 else:
802 if needescape(lout):
803 if needescape(lout):
803 lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
804 lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
804 postout.append(" " + lout) # let diff deal with it
805 postout.append(" " + lout) # let diff deal with it
805
806
806 if lcmd:
807 if lcmd:
807 # add on last return code
808 # add on last return code
808 ret = int(lcmd.split()[1])
809 ret = int(lcmd.split()[1])
809 if ret != 0:
810 if ret != 0:
810 postout.append(" [%s]\n" % ret)
811 postout.append(" [%s]\n" % ret)
811 if pos in after:
812 if pos in after:
812 # merge in non-active test bits
813 # merge in non-active test bits
813 postout += after.pop(pos)
814 postout += after.pop(pos)
814 pos = int(lcmd.split()[0])
815 pos = int(lcmd.split()[0])
815
816
816 if pos in after:
817 if pos in after:
817 postout += after.pop(pos)
818 postout += after.pop(pos)
818
819
819 return exitcode, postout
820 return exitcode, postout
820
821
821 wifexited = getattr(os, "WIFEXITED", lambda x: False)
822 wifexited = getattr(os, "WIFEXITED", lambda x: False)
822 def run(cmd, wd, options, replacements, env):
823 def run(cmd, wd, options, replacements, env):
823 """Run command in a sub-process, capturing the output (stdout and stderr).
824 """Run command in a sub-process, capturing the output (stdout and stderr).
824 Return a tuple (exitcode, output). output is None in debug mode."""
825 Return a tuple (exitcode, output). output is None in debug mode."""
825 # TODO: Use subprocess.Popen if we're running on Python 2.4
826 # TODO: Use subprocess.Popen if we're running on Python 2.4
826 if options.debug:
827 if options.debug:
827 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
828 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
828 ret = proc.wait()
829 ret = proc.wait()
829 return (ret, None)
830 return (ret, None)
830
831
831 proc = Popen4(cmd, wd, options.timeout, env)
832 proc = Popen4(cmd, wd, options.timeout, env)
832 def cleanup():
833 def cleanup():
833 terminate(proc)
834 terminate(proc)
834 ret = proc.wait()
835 ret = proc.wait()
835 if ret == 0:
836 if ret == 0:
836 ret = signal.SIGTERM << 8
837 ret = signal.SIGTERM << 8
837 killdaemons(env['DAEMON_PIDS'])
838 killdaemons(env['DAEMON_PIDS'])
838 return ret
839 return ret
839
840
840 output = ''
841 output = ''
841 proc.tochild.close()
842 proc.tochild.close()
842
843
843 try:
844 try:
844 output = proc.fromchild.read()
845 output = proc.fromchild.read()
845 except KeyboardInterrupt:
846 except KeyboardInterrupt:
846 vlog('# Handling keyboard interrupt')
847 vlog('# Handling keyboard interrupt')
847 cleanup()
848 cleanup()
848 raise
849 raise
849
850
850 ret = proc.wait()
851 ret = proc.wait()
851 if wifexited(ret):
852 if wifexited(ret):
852 ret = os.WEXITSTATUS(ret)
853 ret = os.WEXITSTATUS(ret)
853
854
854 if proc.timeout:
855 if proc.timeout:
855 ret = 'timeout'
856 ret = 'timeout'
856
857
857 if ret:
858 if ret:
858 killdaemons(env['DAEMON_PIDS'])
859 killdaemons(env['DAEMON_PIDS'])
859
860
860 if abort:
861 if abort:
861 raise KeyboardInterrupt()
862 raise KeyboardInterrupt()
862
863
863 for s, r in replacements:
864 for s, r in replacements:
864 output = re.sub(s, r, output)
865 output = re.sub(s, r, output)
865 return ret, output.splitlines(True)
866 return ret, output.splitlines(True)
866
867
867 def runone(options, test, count):
868 def runone(options, test, count):
868 '''returns a result element: (code, test, msg)'''
869 '''returns a result element: (code, test, msg)'''
869
870
870 def skip(msg):
871 def skip(msg):
871 if options.verbose:
872 if options.verbose:
872 log("\nSkipping %s: %s" % (testpath, msg))
873 log("\nSkipping %s: %s" % (testpath, msg))
873 return 's', test, msg
874 return 's', test, msg
874
875
875 def fail(msg, ret):
876 def fail(msg, ret):
876 if not options.nodiff:
877 if not options.nodiff:
877 log("\nERROR: %s %s" % (testpath, msg))
878 log("\nERROR: %s %s" % (testpath, msg))
878 if (not ret and options.interactive
879 if (not ret and options.interactive
879 and os.path.exists(testpath + ".err")):
880 and os.path.exists(testpath + ".err")):
880 iolock.acquire()
881 iolock.acquire()
881 print "Accept this change? [n] ",
882 print "Accept this change? [n] ",
882 answer = sys.stdin.readline().strip()
883 answer = sys.stdin.readline().strip()
883 iolock.release()
884 iolock.release()
884 if answer.lower() in "y yes".split():
885 if answer.lower() in "y yes".split():
885 if test.endswith(".t"):
886 if test.endswith(".t"):
886 rename(testpath + ".err", testpath)
887 rename(testpath + ".err", testpath)
887 else:
888 else:
888 rename(testpath + ".err", testpath + ".out")
889 rename(testpath + ".err", testpath + ".out")
889 return '.', test, ''
890 return '.', test, ''
890 return '!', test, msg
891 return '!', test, msg
891
892
892 def success():
893 def success():
893 return '.', test, ''
894 return '.', test, ''
894
895
895 def ignore(msg):
896 def ignore(msg):
896 return 'i', test, msg
897 return 'i', test, msg
897
898
898 def describe(ret):
899 def describe(ret):
899 if ret < 0:
900 if ret < 0:
900 return 'killed by signal %d' % -ret
901 return 'killed by signal %d' % -ret
901 return 'returned error code %d' % ret
902 return 'returned error code %d' % ret
902
903
903 testpath = os.path.join(TESTDIR, test)
904 testpath = os.path.join(TESTDIR, test)
904 err = os.path.join(TESTDIR, test + ".err")
905 err = os.path.join(TESTDIR, test + ".err")
905 lctest = test.lower()
906 lctest = test.lower()
906
907
907 if not os.path.exists(testpath):
908 if not os.path.exists(testpath):
908 return skip("doesn't exist")
909 return skip("doesn't exist")
909
910
910 if not (options.whitelisted and test in options.whitelisted):
911 if not (options.whitelisted and test in options.whitelisted):
911 if options.blacklist and test in options.blacklist:
912 if options.blacklist and test in options.blacklist:
912 return skip("blacklisted")
913 return skip("blacklisted")
913
914
914 if options.retest and not os.path.exists(test + ".err"):
915 if options.retest and not os.path.exists(test + ".err"):
915 return ignore("not retesting")
916 return ignore("not retesting")
916
917
917 if options.keywords:
918 if options.keywords:
918 fp = open(test)
919 fp = open(test)
919 t = fp.read().lower() + test.lower()
920 t = fp.read().lower() + test.lower()
920 fp.close()
921 fp.close()
921 for k in options.keywords.lower().split():
922 for k in options.keywords.lower().split():
922 if k in t:
923 if k in t:
923 break
924 break
924 else:
925 else:
925 return ignore("doesn't match keyword")
926 return ignore("doesn't match keyword")
926
927
927 if not lctest.startswith("test-"):
928 if not lctest.startswith("test-"):
928 return skip("not a test file")
929 return skip("not a test file")
929 for ext, func, out in testtypes:
930 for ext, func, out in testtypes:
930 if lctest.endswith(ext):
931 if lctest.endswith(ext):
931 runner = func
932 runner = func
932 ref = os.path.join(TESTDIR, test + out)
933 ref = os.path.join(TESTDIR, test + out)
933 break
934 break
934 else:
935 else:
935 return skip("unknown test type")
936 return skip("unknown test type")
936
937
937 vlog("# Test", test)
938 vlog("# Test", test)
938
939
939 if os.path.exists(err):
940 if os.path.exists(err):
940 os.remove(err) # Remove any previous output files
941 os.remove(err) # Remove any previous output files
941
942
942 # Make a tmp subdirectory to work in
943 # Make a tmp subdirectory to work in
943 threadtmp = os.path.join(HGTMP, "child%d" % count)
944 threadtmp = os.path.join(HGTMP, "child%d" % count)
944 testtmp = os.path.join(threadtmp, os.path.basename(test))
945 testtmp = os.path.join(threadtmp, os.path.basename(test))
945 os.mkdir(threadtmp)
946 os.mkdir(threadtmp)
946 os.mkdir(testtmp)
947 os.mkdir(testtmp)
947
948
948 port = options.port + count * 3
949 port = options.port + count * 3
949 replacements = [
950 replacements = [
950 (r':%s\b' % port, ':$HGPORT'),
951 (r':%s\b' % port, ':$HGPORT'),
951 (r':%s\b' % (port + 1), ':$HGPORT1'),
952 (r':%s\b' % (port + 1), ':$HGPORT1'),
952 (r':%s\b' % (port + 2), ':$HGPORT2'),
953 (r':%s\b' % (port + 2), ':$HGPORT2'),
953 ]
954 ]
954 if os.name == 'nt':
955 if os.name == 'nt':
955 replacements.append(
956 replacements.append(
956 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
957 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
957 c in '/\\' and r'[/\\]' or
958 c in '/\\' and r'[/\\]' or
958 c.isdigit() and c or
959 c.isdigit() and c or
959 '\\' + c
960 '\\' + c
960 for c in testtmp), '$TESTTMP'))
961 for c in testtmp), '$TESTTMP'))
961 else:
962 else:
962 replacements.append((re.escape(testtmp), '$TESTTMP'))
963 replacements.append((re.escape(testtmp), '$TESTTMP'))
963
964
964 env = createenv(options, testtmp, threadtmp, port)
965 env = createenv(options, testtmp, threadtmp, port)
965 createhgrc(env['HGRCPATH'], options)
966 createhgrc(env['HGRCPATH'], options)
966
967
967 starttime = time.time()
968 starttime = time.time()
968 try:
969 try:
969 ret, out = runner(testpath, testtmp, options, replacements, env)
970 ret, out = runner(testpath, testtmp, options, replacements, env)
970 except KeyboardInterrupt:
971 except KeyboardInterrupt:
971 endtime = time.time()
972 endtime = time.time()
972 log('INTERRUPTED: %s (after %d seconds)' % (test, endtime - starttime))
973 log('INTERRUPTED: %s (after %d seconds)' % (test, endtime - starttime))
973 raise
974 raise
974 endtime = time.time()
975 endtime = time.time()
975 times.append((test, endtime - starttime))
976 times.append((test, endtime - starttime))
976 vlog("# Ret was:", ret)
977 vlog("# Ret was:", ret)
977
978
978 killdaemons(env['DAEMON_PIDS'])
979 killdaemons(env['DAEMON_PIDS'])
979
980
980 skipped = (ret == SKIPPED_STATUS)
981 skipped = (ret == SKIPPED_STATUS)
981
982
982 # If we're not in --debug mode and reference output file exists,
983 # If we're not in --debug mode and reference output file exists,
983 # check test output against it.
984 # check test output against it.
984 if options.debug:
985 if options.debug:
985 refout = None # to match "out is None"
986 refout = None # to match "out is None"
986 elif os.path.exists(ref):
987 elif os.path.exists(ref):
987 f = open(ref, "r")
988 f = open(ref, "r")
988 refout = f.read().splitlines(True)
989 refout = f.read().splitlines(True)
989 f.close()
990 f.close()
990 else:
991 else:
991 refout = []
992 refout = []
992
993
993 if (ret != 0 or out != refout) and not skipped and not options.debug:
994 if (ret != 0 or out != refout) and not skipped and not options.debug:
994 # Save errors to a file for diagnosis
995 # Save errors to a file for diagnosis
995 f = open(err, "wb")
996 f = open(err, "wb")
996 for line in out:
997 for line in out:
997 f.write(line)
998 f.write(line)
998 f.close()
999 f.close()
999
1000
1000 if skipped:
1001 if skipped:
1001 if out is None: # debug mode: nothing to parse
1002 if out is None: # debug mode: nothing to parse
1002 missing = ['unknown']
1003 missing = ['unknown']
1003 failed = None
1004 failed = None
1004 else:
1005 else:
1005 missing, failed = parsehghaveoutput(out)
1006 missing, failed = parsehghaveoutput(out)
1006 if not missing:
1007 if not missing:
1007 missing = ['irrelevant']
1008 missing = ['irrelevant']
1008 if failed:
1009 if failed:
1009 result = fail("hghave failed checking for %s" % failed[-1], ret)
1010 result = fail("hghave failed checking for %s" % failed[-1], ret)
1010 skipped = False
1011 skipped = False
1011 else:
1012 else:
1012 result = skip(missing[-1])
1013 result = skip(missing[-1])
1013 elif ret == 'timeout':
1014 elif ret == 'timeout':
1014 result = fail("timed out", ret)
1015 result = fail("timed out", ret)
1015 elif out != refout:
1016 elif out != refout:
1016 if not options.nodiff:
1017 if not options.nodiff:
1017 iolock.acquire()
1018 iolock.acquire()
1018 if options.view:
1019 if options.view:
1019 os.system("%s %s %s" % (options.view, ref, err))
1020 os.system("%s %s %s" % (options.view, ref, err))
1020 else:
1021 else:
1021 showdiff(refout, out, ref, err)
1022 showdiff(refout, out, ref, err)
1022 iolock.release()
1023 iolock.release()
1023 if ret:
1024 if ret:
1024 result = fail("output changed and " + describe(ret), ret)
1025 result = fail("output changed and " + describe(ret), ret)
1025 else:
1026 else:
1026 result = fail("output changed", ret)
1027 result = fail("output changed", ret)
1027 elif ret:
1028 elif ret:
1028 result = fail(describe(ret), ret)
1029 result = fail(describe(ret), ret)
1029 else:
1030 else:
1030 result = success()
1031 result = success()
1031
1032
1032 if not options.verbose:
1033 if not options.verbose:
1033 iolock.acquire()
1034 iolock.acquire()
1034 sys.stdout.write(result[0])
1035 sys.stdout.write(result[0])
1035 sys.stdout.flush()
1036 sys.stdout.flush()
1036 iolock.release()
1037 iolock.release()
1037
1038
1038 if not options.keep_tmpdir:
1039 if not options.keep_tmpdir:
1039 shutil.rmtree(threadtmp, True)
1040 shutil.rmtree(threadtmp, True)
1040 return result
1041 return result
1041
1042
1042 _hgpath = None
1043 _hgpath = None
1043
1044
1044 def _gethgpath():
1045 def _gethgpath():
1045 """Return the path to the mercurial package that is actually found by
1046 """Return the path to the mercurial package that is actually found by
1046 the current Python interpreter."""
1047 the current Python interpreter."""
1047 global _hgpath
1048 global _hgpath
1048 if _hgpath is not None:
1049 if _hgpath is not None:
1049 return _hgpath
1050 return _hgpath
1050
1051
1051 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1052 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1052 pipe = os.popen(cmd % PYTHON)
1053 pipe = os.popen(cmd % PYTHON)
1053 try:
1054 try:
1054 _hgpath = pipe.read().strip()
1055 _hgpath = pipe.read().strip()
1055 finally:
1056 finally:
1056 pipe.close()
1057 pipe.close()
1057 return _hgpath
1058 return _hgpath
1058
1059
1059 def _checkhglib(verb):
1060 def _checkhglib(verb):
1060 """Ensure that the 'mercurial' package imported by python is
1061 """Ensure that the 'mercurial' package imported by python is
1061 the one we expect it to be. If not, print a warning to stderr."""
1062 the one we expect it to be. If not, print a warning to stderr."""
1062 expecthg = os.path.join(PYTHONDIR, 'mercurial')
1063 expecthg = os.path.join(PYTHONDIR, 'mercurial')
1063 actualhg = _gethgpath()
1064 actualhg = _gethgpath()
1064 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1065 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1065 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1066 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1066 ' (expected %s)\n'
1067 ' (expected %s)\n'
1067 % (verb, actualhg, expecthg))
1068 % (verb, actualhg, expecthg))
1068
1069
1069 results = {'.':[], '!':[], 's':[], 'i':[]}
1070 results = {'.':[], '!':[], 's':[], 'i':[]}
1070 times = []
1071 times = []
1071 iolock = threading.Lock()
1072 iolock = threading.Lock()
1072 abort = False
1073 abort = False
1073
1074
1074 def scheduletests(options, tests):
1075 def scheduletests(options, tests):
1075 jobs = options.jobs
1076 jobs = options.jobs
1076 done = queue.Queue()
1077 done = queue.Queue()
1077 running = 0
1078 running = 0
1078 count = 0
1079 count = 0
1079 global abort
1080 global abort
1080
1081
1081 def job(test, count):
1082 def job(test, count):
1082 try:
1083 try:
1083 done.put(runone(options, test, count))
1084 done.put(runone(options, test, count))
1084 except KeyboardInterrupt:
1085 except KeyboardInterrupt:
1085 pass
1086 pass
1086
1087
1087 try:
1088 try:
1088 while tests or running:
1089 while tests or running:
1089 if not done.empty() or running == jobs or not tests:
1090 if not done.empty() or running == jobs or not tests:
1090 try:
1091 try:
1091 code, test, msg = done.get(True, 1)
1092 code, test, msg = done.get(True, 1)
1092 results[code].append((test, msg))
1093 results[code].append((test, msg))
1093 if options.first and code not in '.si':
1094 if options.first and code not in '.si':
1094 break
1095 break
1095 except queue.Empty:
1096 except queue.Empty:
1096 continue
1097 continue
1097 running -= 1
1098 running -= 1
1098 if tests and not running == jobs:
1099 if tests and not running == jobs:
1099 test = tests.pop(0)
1100 test = tests.pop(0)
1100 if options.loop:
1101 if options.loop:
1101 tests.append(test)
1102 tests.append(test)
1102 t = threading.Thread(target=job, args=(test, count))
1103 t = threading.Thread(target=job, args=(test, count))
1103 t.start()
1104 t.start()
1104 running += 1
1105 running += 1
1105 count += 1
1106 count += 1
1106 except KeyboardInterrupt:
1107 except KeyboardInterrupt:
1107 abort = True
1108 abort = True
1108
1109
1109 def runtests(options, tests):
1110 def runtests(options, tests):
1110 try:
1111 try:
1111 if INST:
1112 if INST:
1112 installhg(options)
1113 installhg(options)
1113 _checkhglib("Testing")
1114 _checkhglib("Testing")
1114 else:
1115 else:
1115 usecorrectpython()
1116 usecorrectpython()
1116
1117
1117 if options.restart:
1118 if options.restart:
1118 orig = list(tests)
1119 orig = list(tests)
1119 while tests:
1120 while tests:
1120 if os.path.exists(tests[0] + ".err"):
1121 if os.path.exists(tests[0] + ".err"):
1121 break
1122 break
1122 tests.pop(0)
1123 tests.pop(0)
1123 if not tests:
1124 if not tests:
1124 print "running all tests"
1125 print "running all tests"
1125 tests = orig
1126 tests = orig
1126
1127
1127 scheduletests(options, tests)
1128 scheduletests(options, tests)
1128
1129
1129 failed = len(results['!'])
1130 failed = len(results['!'])
1130 tested = len(results['.']) + failed
1131 tested = len(results['.']) + failed
1131 skipped = len(results['s'])
1132 skipped = len(results['s'])
1132 ignored = len(results['i'])
1133 ignored = len(results['i'])
1133
1134
1134 print
1135 print
1135 if not options.noskips:
1136 if not options.noskips:
1136 for s in results['s']:
1137 for s in results['s']:
1137 print "Skipped %s: %s" % s
1138 print "Skipped %s: %s" % s
1138 for s in results['!']:
1139 for s in results['!']:
1139 print "Failed %s: %s" % s
1140 print "Failed %s: %s" % s
1140 _checkhglib("Tested")
1141 _checkhglib("Tested")
1141 print "# Ran %d tests, %d skipped, %d failed." % (
1142 print "# Ran %d tests, %d skipped, %d failed." % (
1142 tested, skipped + ignored, failed)
1143 tested, skipped + ignored, failed)
1143 if options.time:
1144 if options.time:
1144 outputtimes(options)
1145 outputtimes(options)
1145
1146
1146 if options.anycoverage:
1147 if options.anycoverage:
1147 outputcoverage(options)
1148 outputcoverage(options)
1148 except KeyboardInterrupt:
1149 except KeyboardInterrupt:
1149 failed = True
1150 failed = True
1150 print "\ninterrupted!"
1151 print "\ninterrupted!"
1151
1152
1152 if failed:
1153 if failed:
1153 sys.exit(1)
1154 sys.exit(1)
1154
1155
1155 testtypes = [('.py', pytest, '.out'),
1156 testtypes = [('.py', pytest, '.out'),
1156 ('.t', tsttest, '')]
1157 ('.t', tsttest, '')]
1157
1158
1158 def main():
1159 def main():
1159 (options, args) = parseargs()
1160 (options, args) = parseargs()
1160 os.umask(022)
1161 os.umask(022)
1161
1162
1162 checktools()
1163 checktools()
1163
1164
1164 if len(args) == 0:
1165 if len(args) == 0:
1165 args = [t for t in os.listdir(".")
1166 args = [t for t in os.listdir(".")
1166 if t.startswith("test-")
1167 if t.startswith("test-")
1167 and (t.endswith(".py") or t.endswith(".t"))]
1168 and (t.endswith(".py") or t.endswith(".t"))]
1168
1169
1169 tests = args
1170 tests = args
1170
1171
1171 if options.random:
1172 if options.random:
1172 random.shuffle(tests)
1173 random.shuffle(tests)
1173 else:
1174 else:
1174 # keywords for slow tests
1175 # keywords for slow tests
1175 slow = 'svn gendoc check-code-hg'.split()
1176 slow = 'svn gendoc check-code-hg'.split()
1176 def sortkey(f):
1177 def sortkey(f):
1177 # run largest tests first, as they tend to take the longest
1178 # run largest tests first, as they tend to take the longest
1178 try:
1179 try:
1179 val = -os.stat(f).st_size
1180 val = -os.stat(f).st_size
1180 except OSError, e:
1181 except OSError, e:
1181 if e.errno != errno.ENOENT:
1182 if e.errno != errno.ENOENT:
1182 raise
1183 raise
1183 return -1e9 # file does not exist, tell early
1184 return -1e9 # file does not exist, tell early
1184 for kw in slow:
1185 for kw in slow:
1185 if kw in f:
1186 if kw in f:
1186 val *= 10
1187 val *= 10
1187 return val
1188 return val
1188 tests.sort(key=sortkey)
1189 tests.sort(key=sortkey)
1189
1190
1190 if 'PYTHONHASHSEED' not in os.environ:
1191 if 'PYTHONHASHSEED' not in os.environ:
1191 # use a random python hash seed all the time
1192 # use a random python hash seed all the time
1192 # we do the randomness ourself to know what seed is used
1193 # we do the randomness ourself to know what seed is used
1193 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1194 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1194 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1195 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1195
1196
1196 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
1197 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
1197 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1198 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1198 if options.tmpdir:
1199 if options.tmpdir:
1199 options.keep_tmpdir = True
1200 options.keep_tmpdir = True
1200 tmpdir = options.tmpdir
1201 tmpdir = options.tmpdir
1201 if os.path.exists(tmpdir):
1202 if os.path.exists(tmpdir):
1202 # Meaning of tmpdir has changed since 1.3: we used to create
1203 # Meaning of tmpdir has changed since 1.3: we used to create
1203 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1204 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1204 # tmpdir already exists.
1205 # tmpdir already exists.
1205 sys.exit("error: temp dir %r already exists" % tmpdir)
1206 sys.exit("error: temp dir %r already exists" % tmpdir)
1206
1207
1207 # Automatically removing tmpdir sounds convenient, but could
1208 # Automatically removing tmpdir sounds convenient, but could
1208 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1209 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1209 # or "--tmpdir=$HOME".
1210 # or "--tmpdir=$HOME".
1210 #vlog("# Removing temp dir", tmpdir)
1211 #vlog("# Removing temp dir", tmpdir)
1211 #shutil.rmtree(tmpdir)
1212 #shutil.rmtree(tmpdir)
1212 os.makedirs(tmpdir)
1213 os.makedirs(tmpdir)
1213 else:
1214 else:
1214 d = None
1215 d = None
1215 if os.name == 'nt':
1216 if os.name == 'nt':
1216 # without this, we get the default temp dir location, but
1217 # without this, we get the default temp dir location, but
1217 # in all lowercase, which causes troubles with paths (issue3490)
1218 # in all lowercase, which causes troubles with paths (issue3490)
1218 d = os.getenv('TMP')
1219 d = os.getenv('TMP')
1219 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1220 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1220 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1221 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1221
1222
1222 if options.with_hg:
1223 if options.with_hg:
1223 INST = None
1224 INST = None
1224 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1225 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1225
1226
1226 # This looks redundant with how Python initializes sys.path from
1227 # This looks redundant with how Python initializes sys.path from
1227 # the location of the script being executed. Needed because the
1228 # the location of the script being executed. Needed because the
1228 # "hg" specified by --with-hg is not the only Python script
1229 # "hg" specified by --with-hg is not the only Python script
1229 # executed in the test suite that needs to import 'mercurial'
1230 # executed in the test suite that needs to import 'mercurial'
1230 # ... which means it's not really redundant at all.
1231 # ... which means it's not really redundant at all.
1231 PYTHONDIR = BINDIR
1232 PYTHONDIR = BINDIR
1232 else:
1233 else:
1233 INST = os.path.join(HGTMP, "install")
1234 INST = os.path.join(HGTMP, "install")
1234 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1235 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1235 PYTHONDIR = os.path.join(INST, "lib", "python")
1236 PYTHONDIR = os.path.join(INST, "lib", "python")
1236
1237
1237 os.environ["BINDIR"] = BINDIR
1238 os.environ["BINDIR"] = BINDIR
1238 os.environ["PYTHON"] = PYTHON
1239 os.environ["PYTHON"] = PYTHON
1239
1240
1240 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1241 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1241 os.environ["PATH"] = os.pathsep.join(path)
1242 os.environ["PATH"] = os.pathsep.join(path)
1242
1243
1243 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1244 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1244 # can run .../tests/run-tests.py test-foo where test-foo
1245 # can run .../tests/run-tests.py test-foo where test-foo
1245 # adds an extension to HGRC
1246 # adds an extension to HGRC
1246 pypath = [PYTHONDIR, TESTDIR]
1247 pypath = [PYTHONDIR, TESTDIR]
1247 # We have to augment PYTHONPATH, rather than simply replacing
1248 # We have to augment PYTHONPATH, rather than simply replacing
1248 # it, in case external libraries are only available via current
1249 # it, in case external libraries are only available via current
1249 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1250 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1250 # are in /opt/subversion.)
1251 # are in /opt/subversion.)
1251 oldpypath = os.environ.get(IMPL_PATH)
1252 oldpypath = os.environ.get(IMPL_PATH)
1252 if oldpypath:
1253 if oldpypath:
1253 pypath.append(oldpypath)
1254 pypath.append(oldpypath)
1254 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1255 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1255
1256
1256 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1257 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1257
1258
1258 vlog("# Using TESTDIR", TESTDIR)
1259 vlog("# Using TESTDIR", TESTDIR)
1259 vlog("# Using HGTMP", HGTMP)
1260 vlog("# Using HGTMP", HGTMP)
1260 vlog("# Using PATH", os.environ["PATH"])
1261 vlog("# Using PATH", os.environ["PATH"])
1261 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1262 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1262
1263
1263 try:
1264 try:
1264 runtests(options, tests)
1265 runtests(options, tests)
1265 finally:
1266 finally:
1266 time.sleep(.1)
1267 time.sleep(.1)
1267 cleanup(options)
1268 cleanup(options)
1268
1269
1269 if __name__ == '__main__':
1270 if __name__ == '__main__':
1270 main()
1271 main()
@@ -1,191 +1,193
1
1
2 testing hellomessage:
2 testing hellomessage:
3
3
4 o, 'capabilities: getencoding runcommand\nencoding: ***'
4 o, 'capabilities: getencoding runcommand\nencoding: ***'
5 runcommand id
5 runcommand id
6 000000000000 tip
6 000000000000 tip
7
7
8 testing unknowncommand:
8 testing unknowncommand:
9
9
10 abort: unknown command unknowncommand
10 abort: unknown command unknowncommand
11
11
12 testing checkruncommand:
12 testing checkruncommand:
13
13
14 runcommand
14 runcommand
15 Mercurial Distributed SCM
15 Mercurial Distributed SCM
16
16
17 basic commands:
17 basic commands:
18
18
19 add add the specified files on the next commit
19 add add the specified files on the next commit
20 annotate show changeset information by line for each file
20 annotate show changeset information by line for each file
21 clone make a copy of an existing repository
21 clone make a copy of an existing repository
22 commit commit the specified files or all outstanding changes
22 commit commit the specified files or all outstanding changes
23 diff diff repository (or selected files)
23 diff diff repository (or selected files)
24 export dump the header and diffs for one or more changesets
24 export dump the header and diffs for one or more changesets
25 forget forget the specified files on the next commit
25 forget forget the specified files on the next commit
26 init create a new repository in the given directory
26 init create a new repository in the given directory
27 log show revision history of entire repository or files
27 log show revision history of entire repository or files
28 merge merge working directory with another revision
28 merge merge working directory with another revision
29 pull pull changes from the specified source
29 pull pull changes from the specified source
30 push push changes to the specified destination
30 push push changes to the specified destination
31 remove remove the specified files on the next commit
31 remove remove the specified files on the next commit
32 serve start stand-alone webserver
32 serve start stand-alone webserver
33 status show changed files in the working directory
33 status show changed files in the working directory
34 summary summarize working directory state
34 summary summarize working directory state
35 update update working directory (or switch revisions)
35 update update working directory (or switch revisions)
36
36
37 use "hg help" for the full list of commands or "hg -v" for details
37 use "hg help" for the full list of commands or "hg -v" for details
38 runcommand id --quiet
38 runcommand id --quiet
39 000000000000
39 000000000000
40 runcommand id
40 runcommand id
41 000000000000 tip
41 000000000000 tip
42 runcommand id --config ui.quiet=True
42 runcommand id --config ui.quiet=True
43 000000000000
43 000000000000
44 runcommand id
44 runcommand id
45 000000000000 tip
45 000000000000 tip
46
46
47 testing inputeof:
47 testing inputeof:
48
48
49 server exit code = 1
49 server exit code = 1
50
50
51 testing serverinput:
51 testing serverinput:
52
52
53 runcommand import -
53 runcommand import -
54 applying patch from stdin
54 applying patch from stdin
55 runcommand log
55 runcommand log
56 changeset: 0:eff892de26ec
56 changeset: 0:eff892de26ec
57 tag: tip
57 tag: tip
58 user: test
58 user: test
59 date: Thu Jan 01 00:00:00 1970 +0000
59 date: Thu Jan 01 00:00:00 1970 +0000
60 summary: 1
60 summary: 1
61
61
62
62
63 testing cwd:
63 testing cwd:
64
64
65 runcommand --cwd foo st bar
65 runcommand --cwd foo st bar
66 ? bar
66 ? bar
67 runcommand st foo/bar
67 runcommand st foo/bar
68 ? foo/bar
68 ? foo/bar
69
69
70 testing localhgrc:
70 testing localhgrc:
71
71
72 runcommand showconfig
72 runcommand showconfig
73 bundle.mainreporoot=$TESTTMP
73 bundle.mainreporoot=$TESTTMP
74 defaults.backout=-d "0 0"
74 defaults.backout=-d "0 0"
75 defaults.commit=-d "0 0"
75 defaults.commit=-d "0 0"
76 defaults.shelve=--date "0 0"
76 defaults.tag=-d "0 0"
77 defaults.tag=-d "0 0"
77 ui.slash=True
78 ui.slash=True
78 ui.interactive=False
79 ui.interactive=False
79 ui.foo=bar
80 ui.foo=bar
80 runcommand init foo
81 runcommand init foo
81 runcommand -R foo showconfig ui defaults
82 runcommand -R foo showconfig ui defaults
82 defaults.backout=-d "0 0"
83 defaults.backout=-d "0 0"
83 defaults.commit=-d "0 0"
84 defaults.commit=-d "0 0"
85 defaults.shelve=--date "0 0"
84 defaults.tag=-d "0 0"
86 defaults.tag=-d "0 0"
85 ui.slash=True
87 ui.slash=True
86 ui.interactive=False
88 ui.interactive=False
87
89
88 testing hookoutput:
90 testing hookoutput:
89
91
90 runcommand --config hooks.pre-identify=python:test-commandserver.hook id
92 runcommand --config hooks.pre-identify=python:test-commandserver.hook id
91 hook talking
93 hook talking
92 now try to read something: 'some input'
94 now try to read something: 'some input'
93 eff892de26ec tip
95 eff892de26ec tip
94
96
95 testing outsidechanges:
97 testing outsidechanges:
96
98
97 runcommand status
99 runcommand status
98 M a
100 M a
99 runcommand tip
101 runcommand tip
100 changeset: 1:d3a0a68be6de
102 changeset: 1:d3a0a68be6de
101 tag: tip
103 tag: tip
102 user: test
104 user: test
103 date: Thu Jan 01 00:00:00 1970 +0000
105 date: Thu Jan 01 00:00:00 1970 +0000
104 summary: 2
106 summary: 2
105
107
106 runcommand status
108 runcommand status
107
109
108 testing bookmarks:
110 testing bookmarks:
109
111
110 runcommand bookmarks
112 runcommand bookmarks
111 no bookmarks set
113 no bookmarks set
112 runcommand bookmarks
114 runcommand bookmarks
113 bm1 1:d3a0a68be6de
115 bm1 1:d3a0a68be6de
114 bm2 1:d3a0a68be6de
116 bm2 1:d3a0a68be6de
115 runcommand bookmarks
117 runcommand bookmarks
116 * bm1 1:d3a0a68be6de
118 * bm1 1:d3a0a68be6de
117 bm2 1:d3a0a68be6de
119 bm2 1:d3a0a68be6de
118 runcommand bookmarks bm3
120 runcommand bookmarks bm3
119 runcommand commit -Amm
121 runcommand commit -Amm
120 runcommand bookmarks
122 runcommand bookmarks
121 bm1 1:d3a0a68be6de
123 bm1 1:d3a0a68be6de
122 bm2 1:d3a0a68be6de
124 bm2 1:d3a0a68be6de
123 * bm3 2:aef17e88f5f0
125 * bm3 2:aef17e88f5f0
124
126
125 testing tagscache:
127 testing tagscache:
126
128
127 runcommand id -t -r 0
129 runcommand id -t -r 0
128
130
129 runcommand id -t -r 0
131 runcommand id -t -r 0
130 foo
132 foo
131
133
132 testing setphase:
134 testing setphase:
133
135
134 runcommand phase -r .
136 runcommand phase -r .
135 3: draft
137 3: draft
136 runcommand phase -r .
138 runcommand phase -r .
137 3: public
139 3: public
138
140
139 testing rollback:
141 testing rollback:
140
142
141 runcommand phase -r . -p
143 runcommand phase -r . -p
142 no phases changed
144 no phases changed
143 runcommand commit -Am.
145 runcommand commit -Am.
144 runcommand rollback
146 runcommand rollback
145 repository tip rolled back to revision 3 (undo commit)
147 repository tip rolled back to revision 3 (undo commit)
146 working directory now based on revision 3
148 working directory now based on revision 3
147 runcommand phase -r .
149 runcommand phase -r .
148 3: public
150 3: public
149
151
150 testing branch:
152 testing branch:
151
153
152 runcommand branch
154 runcommand branch
153 default
155 default
154 marked working directory as branch foo
156 marked working directory as branch foo
155 (branches are permanent and global, did you want a bookmark?)
157 (branches are permanent and global, did you want a bookmark?)
156 runcommand branch
158 runcommand branch
157 foo
159 foo
158 marked working directory as branch default
160 marked working directory as branch default
159 (branches are permanent and global, did you want a bookmark?)
161 (branches are permanent and global, did you want a bookmark?)
160
162
161 testing hgignore:
163 testing hgignore:
162
164
163 runcommand commit -Am.
165 runcommand commit -Am.
164 adding .hgignore
166 adding .hgignore
165 runcommand status -i -u
167 runcommand status -i -u
166 I ignored-file
168 I ignored-file
167
169
168 testing phasecacheafterstrip:
170 testing phasecacheafterstrip:
169
171
170 runcommand update -C 0
172 runcommand update -C 0
171 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
173 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
172 runcommand commit -Am. a
174 runcommand commit -Am. a
173 created new head
175 created new head
174 runcommand log -Gq
176 runcommand log -Gq
175 @ 5:731265503d86
177 @ 5:731265503d86
176 |
178 |
177 | o 4:7966c8e3734d
179 | o 4:7966c8e3734d
178 | |
180 | |
179 | o 3:b9b85890c400
181 | o 3:b9b85890c400
180 | |
182 | |
181 | o 2:aef17e88f5f0
183 | o 2:aef17e88f5f0
182 | |
184 | |
183 | o 1:d3a0a68be6de
185 | o 1:d3a0a68be6de
184 |/
186 |/
185 o 0:eff892de26ec
187 o 0:eff892de26ec
186
188
187 runcommand phase -p .
189 runcommand phase -p .
188 runcommand phase .
190 runcommand phase .
189 5: public
191 5: public
190 runcommand branches
192 runcommand branches
191 default 1:731265503d86
193 default 1:731265503d86
General Comments 0
You need to be logged in to leave comments. Login now