##// END OF EJS Templates
merge with stable
Matt Mackall -
r24990:015adbcd merge default
parent child Browse files
Show More
@@ -1,1147 +1,1151
1 1 # histedit.py - interactive history editing for mercurial
2 2 #
3 3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7 """interactive history editing
8 8
9 9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 10 is as follows, assuming the following history::
11 11
12 12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 13 | Add delta
14 14 |
15 15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 16 | Add gamma
17 17 |
18 18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 19 | Add beta
20 20 |
21 21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 22 Add alpha
23 23
24 24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 25 file open in your editor::
26 26
27 27 pick c561b4e977df Add beta
28 28 pick 030b686bedc4 Add gamma
29 29 pick 7c2fd3b9020c Add delta
30 30
31 31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 32 #
33 33 # Commits are listed from least to most recent
34 34 #
35 35 # Commands:
36 36 # p, pick = use commit
37 37 # e, edit = use commit, but stop for amending
38 38 # f, fold = use commit, but combine it with the one above
39 39 # r, roll = like fold, but discard this commit's description
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit message without changing commit content
42 42 #
43 43
44 44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 45 for each revision in your history. For example, if you had meant to add gamma
46 46 before beta, and then wanted to add delta in the same revision as beta, you
47 47 would reorganize the file to look like this::
48 48
49 49 pick 030b686bedc4 Add gamma
50 50 pick c561b4e977df Add beta
51 51 fold 7c2fd3b9020c Add delta
52 52
53 53 # Edit history between c561b4e977df and 7c2fd3b9020c
54 54 #
55 55 # Commits are listed from least to most recent
56 56 #
57 57 # Commands:
58 58 # p, pick = use commit
59 59 # e, edit = use commit, but stop for amending
60 60 # f, fold = use commit, but combine it with the one above
61 61 # r, roll = like fold, but discard this commit's description
62 62 # d, drop = remove commit from history
63 63 # m, mess = edit message without changing commit content
64 64 #
65 65
66 66 At which point you close the editor and ``histedit`` starts working. When you
67 67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 68 those revisions together, offering you a chance to clean up the commit message::
69 69
70 70 Add beta
71 71 ***
72 72 Add delta
73 73
74 74 Edit the commit message to your liking, then close the editor. For
75 75 this example, let's assume that the commit message was changed to
76 76 ``Add beta and delta.`` After histedit has run and had a chance to
77 77 remove any old or temporary revisions it needed, the history looks
78 78 like this::
79 79
80 80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 81 | Add beta and delta.
82 82 |
83 83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 84 | Add gamma
85 85 |
86 86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 87 Add alpha
88 88
89 89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 90 ones) until after it has completed all the editing operations, so it will
91 91 probably perform several strip operations when it's done. For the above example,
92 92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 93 so you might need to be a little patient. You can choose to keep the original
94 94 revisions by passing the ``--keep`` flag.
95 95
96 96 The ``edit`` operation will drop you back to a command prompt,
97 97 allowing you to edit files freely, or even use ``hg record`` to commit
98 98 some changes as a separate commit. When you're done, any remaining
99 99 uncommitted changes will be committed as well. When done, run ``hg
100 100 histedit --continue`` to finish this step. You'll be prompted for a
101 101 new commit message, but the default commit message will be the
102 102 original message for the ``edit`` ed revision.
103 103
104 104 The ``message`` operation will give you a chance to revise a commit
105 105 message without changing the contents. It's a shortcut for doing
106 106 ``edit`` immediately followed by `hg histedit --continue``.
107 107
108 108 If ``histedit`` encounters a conflict when moving a revision (while
109 109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 110 ``edit`` with the difference that it won't prompt you for a commit
111 111 message when done. If you decide at this point that you don't like how
112 112 much work it will be to rearrange history, or that you made a mistake,
113 113 you can use ``hg histedit --abort`` to abandon the new changes you
114 114 have made and return to the state before you attempted to edit your
115 115 history.
116 116
117 117 If we clone the histedit-ed example repository above and add four more
118 118 changes, such that we have the following history::
119 119
120 120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 121 | Add theta
122 122 |
123 123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 124 | Add eta
125 125 |
126 126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 127 | Add zeta
128 128 |
129 129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 130 | Add epsilon
131 131 |
132 132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 133 | Add beta and delta.
134 134 |
135 135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 136 | Add gamma
137 137 |
138 138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 139 Add alpha
140 140
141 141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 143 repository that Mercurial does not detect to be related to the source
144 144 repo, you can add a ``--force`` option.
145 145
146 146 Histedit rule lines are truncated to 80 characters by default. You
147 147 can customise this behaviour by setting a different length in your
148 148 configuration file::
149 149
150 150 [histedit]
151 151 linelen = 120 # truncate rule lines at 120 characters
152 152 """
153 153
154 154 try:
155 155 import cPickle as pickle
156 156 pickle.dump # import now
157 157 except ImportError:
158 158 import pickle
159 159 import errno
160 160 import os
161 161 import sys
162 162
163 163 from mercurial import cmdutil
164 164 from mercurial import discovery
165 165 from mercurial import error
166 166 from mercurial import changegroup
167 167 from mercurial import copies
168 168 from mercurial import context
169 169 from mercurial import exchange
170 170 from mercurial import extensions
171 171 from mercurial import hg
172 172 from mercurial import node
173 173 from mercurial import repair
174 174 from mercurial import scmutil
175 175 from mercurial import util
176 176 from mercurial import obsolete
177 177 from mercurial import merge as mergemod
178 178 from mercurial.lock import release
179 179 from mercurial.i18n import _
180 180
181 181 cmdtable = {}
182 182 command = cmdutil.command(cmdtable)
183 183
184 184 testedwith = 'internal'
185 185
186 186 # i18n: command names and abbreviations must remain untranslated
187 187 editcomment = _("""# Edit history between %s and %s
188 188 #
189 189 # Commits are listed from least to most recent
190 190 #
191 191 # Commands:
192 192 # p, pick = use commit
193 193 # e, edit = use commit, but stop for amending
194 194 # f, fold = use commit, but combine it with the one above
195 195 # r, roll = like fold, but discard this commit's description
196 196 # d, drop = remove commit from history
197 197 # m, mess = edit message without changing commit content
198 198 #
199 199 """)
200 200
201 201 class histeditstate(object):
202 202 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
203 203 topmost=None, replacements=None, lock=None, wlock=None):
204 204 self.repo = repo
205 205 self.rules = rules
206 206 self.keep = keep
207 207 self.topmost = topmost
208 208 self.parentctxnode = parentctxnode
209 209 self.lock = lock
210 210 self.wlock = wlock
211 211 self.backupfile = None
212 212 if replacements is None:
213 213 self.replacements = []
214 214 else:
215 215 self.replacements = replacements
216 216
217 217 def read(self):
218 218 """Load histedit state from disk and set fields appropriately."""
219 219 try:
220 220 fp = self.repo.vfs('histedit-state', 'r')
221 221 except IOError, err:
222 222 if err.errno != errno.ENOENT:
223 223 raise
224 224 raise util.Abort(_('no histedit in progress'))
225 225
226 226 try:
227 227 data = pickle.load(fp)
228 228 parentctxnode, rules, keep, topmost, replacements = data
229 229 backupfile = None
230 230 except pickle.UnpicklingError:
231 231 data = self._load()
232 232 parentctxnode, rules, keep, topmost, replacements, backupfile = data
233 233
234 234 self.parentctxnode = parentctxnode
235 235 self.rules = rules
236 236 self.keep = keep
237 237 self.topmost = topmost
238 238 self.replacements = replacements
239 239 self.backupfile = backupfile
240 240
241 241 def write(self):
242 242 fp = self.repo.vfs('histedit-state', 'w')
243 243 fp.write('v1\n')
244 244 fp.write('%s\n' % node.hex(self.parentctxnode))
245 245 fp.write('%s\n' % node.hex(self.topmost))
246 246 fp.write('%s\n' % self.keep)
247 247 fp.write('%d\n' % len(self.rules))
248 248 for rule in self.rules:
249 249 fp.write('%s\n' % rule[0]) # action
250 250 fp.write('%s\n' % rule[1]) # remainder
251 251 fp.write('%d\n' % len(self.replacements))
252 252 for replacement in self.replacements:
253 253 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
254 254 for r in replacement[1])))
255 fp.write('%s\n' % self.backupfile)
255 backupfile = self.backupfile
256 if not backupfile:
257 backupfile = ''
258 fp.write('%s\n' % backupfile)
256 259 fp.close()
257 260
258 261 def _load(self):
259 262 fp = self.repo.vfs('histedit-state', 'r')
260 263 lines = [l[:-1] for l in fp.readlines()]
261 264
262 265 index = 0
263 266 lines[index] # version number
264 267 index += 1
265 268
266 269 parentctxnode = node.bin(lines[index])
267 270 index += 1
268 271
269 272 topmost = node.bin(lines[index])
270 273 index += 1
271 274
272 275 keep = lines[index] == 'True'
273 276 index += 1
274 277
275 278 # Rules
276 279 rules = []
277 280 rulelen = int(lines[index])
278 281 index += 1
279 282 for i in xrange(rulelen):
280 283 ruleaction = lines[index]
281 284 index += 1
282 285 rule = lines[index]
283 286 index += 1
284 287 rules.append((ruleaction, rule))
285 288
286 289 # Replacements
287 290 replacements = []
288 291 replacementlen = int(lines[index])
289 292 index += 1
290 293 for i in xrange(replacementlen):
291 294 replacement = lines[index]
292 295 original = node.bin(replacement[:40])
293 296 succ = [node.bin(replacement[i:i + 40]) for i in
294 297 range(40, len(replacement), 40)]
295 298 replacements.append((original, succ))
296 299 index += 1
297 300
298 301 backupfile = lines[index]
299 302 index += 1
300 303
301 304 fp.close()
302 305
303 306 return parentctxnode, rules, keep, topmost, replacements, backupfile
304 307
305 308 def clear(self):
306 309 self.repo.vfs.unlink('histedit-state')
307 310
308 311 class histeditaction(object):
309 312 def __init__(self, state, node):
310 313 self.state = state
311 314 self.repo = state.repo
312 315 self.node = node
313 316
314 317 @classmethod
315 318 def fromrule(cls, state, rule):
316 319 """Parses the given rule, returning an instance of the histeditaction.
317 320 """
318 321 repo = state.repo
319 322 rulehash = rule.strip().split(' ', 1)[0]
320 323 try:
321 324 node = repo[rulehash].node()
322 325 except error.RepoError:
323 326 raise util.Abort(_('unknown changeset %s listed') % rulehash[:12])
324 327 return cls(state, node)
325 328
326 329 def run(self):
327 330 """Runs the action. The default behavior is simply apply the action's
328 331 rulectx onto the current parentctx."""
329 332 self.applychange()
330 333 self.continuedirty()
331 334 return self.continueclean()
332 335
333 336 def applychange(self):
334 337 """Applies the changes from this action's rulectx onto the current
335 338 parentctx, but does not commit them."""
336 339 repo = self.repo
337 340 rulectx = repo[self.node]
338 341 hg.update(repo, self.state.parentctxnode)
339 342 stats = applychanges(repo.ui, repo, rulectx, {})
340 343 if stats and stats[3] > 0:
341 344 raise error.InterventionRequired(_('Fix up the change and run '
342 345 'hg histedit --continue'))
343 346
344 347 def continuedirty(self):
345 348 """Continues the action when changes have been applied to the working
346 349 copy. The default behavior is to commit the dirty changes."""
347 350 repo = self.repo
348 351 rulectx = repo[self.node]
349 352
350 353 editor = self.commiteditor()
351 354 commit = commitfuncfor(repo, rulectx)
352 355
353 356 commit(text=rulectx.description(), user=rulectx.user(),
354 357 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
355 358
356 359 def commiteditor(self):
357 360 """The editor to be used to edit the commit message."""
358 361 return False
359 362
360 363 def continueclean(self):
361 364 """Continues the action when the working copy is clean. The default
362 365 behavior is to accept the current commit as the new version of the
363 366 rulectx."""
364 367 ctx = self.repo['.']
365 368 if ctx.node() == self.state.parentctxnode:
366 369 self.repo.ui.warn(_('%s: empty changeset\n') %
367 370 node.short(self.node))
368 371 return ctx, [(self.node, tuple())]
369 372 if ctx.node() == self.node:
370 373 # Nothing changed
371 374 return ctx, []
372 375 return ctx, [(self.node, (ctx.node(),))]
373 376
374 377 def commitfuncfor(repo, src):
375 378 """Build a commit function for the replacement of <src>
376 379
377 380 This function ensure we apply the same treatment to all changesets.
378 381
379 382 - Add a 'histedit_source' entry in extra.
380 383
381 384 Note that fold have its own separated logic because its handling is a bit
382 385 different and not easily factored out of the fold method.
383 386 """
384 387 phasemin = src.phase()
385 388 def commitfunc(**kwargs):
386 389 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
387 390 try:
388 391 repo.ui.setconfig('phases', 'new-commit', phasemin,
389 392 'histedit')
390 393 extra = kwargs.get('extra', {}).copy()
391 394 extra['histedit_source'] = src.hex()
392 395 kwargs['extra'] = extra
393 396 return repo.commit(**kwargs)
394 397 finally:
395 398 repo.ui.restoreconfig(phasebackup)
396 399 return commitfunc
397 400
398 401 def applychanges(ui, repo, ctx, opts):
399 402 """Merge changeset from ctx (only) in the current working directory"""
400 403 wcpar = repo.dirstate.parents()[0]
401 404 if ctx.p1().node() == wcpar:
402 405 # edition ar "in place" we do not need to make any merge,
403 406 # just applies changes on parent for edition
404 407 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
405 408 stats = None
406 409 else:
407 410 try:
408 411 # ui.forcemerge is an internal variable, do not document
409 412 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
410 413 'histedit')
411 414 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
412 415 finally:
413 416 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
414 417 return stats
415 418
416 419 def collapse(repo, first, last, commitopts, skipprompt=False):
417 420 """collapse the set of revisions from first to last as new one.
418 421
419 422 Expected commit options are:
420 423 - message
421 424 - date
422 425 - username
423 426 Commit message is edited in all cases.
424 427
425 428 This function works in memory."""
426 429 ctxs = list(repo.set('%d::%d', first, last))
427 430 if not ctxs:
428 431 return None
429 432 base = first.parents()[0]
430 433
431 434 # commit a new version of the old changeset, including the update
432 435 # collect all files which might be affected
433 436 files = set()
434 437 for ctx in ctxs:
435 438 files.update(ctx.files())
436 439
437 440 # Recompute copies (avoid recording a -> b -> a)
438 441 copied = copies.pathcopies(base, last)
439 442
440 443 # prune files which were reverted by the updates
441 444 def samefile(f):
442 445 if f in last.manifest():
443 446 a = last.filectx(f)
444 447 if f in base.manifest():
445 448 b = base.filectx(f)
446 449 return (a.data() == b.data()
447 450 and a.flags() == b.flags())
448 451 else:
449 452 return False
450 453 else:
451 454 return f not in base.manifest()
452 455 files = [f for f in files if not samefile(f)]
453 456 # commit version of these files as defined by head
454 457 headmf = last.manifest()
455 458 def filectxfn(repo, ctx, path):
456 459 if path in headmf:
457 460 fctx = last[path]
458 461 flags = fctx.flags()
459 462 mctx = context.memfilectx(repo,
460 463 fctx.path(), fctx.data(),
461 464 islink='l' in flags,
462 465 isexec='x' in flags,
463 466 copied=copied.get(path))
464 467 return mctx
465 468 return None
466 469
467 470 if commitopts.get('message'):
468 471 message = commitopts['message']
469 472 else:
470 473 message = first.description()
471 474 user = commitopts.get('user')
472 475 date = commitopts.get('date')
473 476 extra = commitopts.get('extra')
474 477
475 478 parents = (first.p1().node(), first.p2().node())
476 479 editor = None
477 480 if not skipprompt:
478 481 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
479 482 new = context.memctx(repo,
480 483 parents=parents,
481 484 text=message,
482 485 files=files,
483 486 filectxfn=filectxfn,
484 487 user=user,
485 488 date=date,
486 489 extra=extra,
487 490 editor=editor)
488 491 return repo.commitctx(new)
489 492
490 493 class pick(histeditaction):
491 494 def run(self):
492 495 rulectx = self.repo[self.node]
493 496 if rulectx.parents()[0].node() == self.state.parentctxnode:
494 497 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
495 498 return rulectx, []
496 499
497 500 return super(pick, self).run()
498 501
499 502 class edit(histeditaction):
500 503 def run(self):
501 504 repo = self.repo
502 505 rulectx = repo[self.node]
503 506 hg.update(repo, self.state.parentctxnode)
504 507 applychanges(repo.ui, repo, rulectx, {})
505 508 raise error.InterventionRequired(
506 509 _('Make changes as needed, you may commit or record as needed '
507 510 'now.\nWhen you are finished, run hg histedit --continue to '
508 511 'resume.'))
509 512
510 513 def commiteditor(self):
511 514 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
512 515
513 516 class fold(histeditaction):
514 517 def continuedirty(self):
515 518 repo = self.repo
516 519 rulectx = repo[self.node]
517 520
518 521 commit = commitfuncfor(repo, rulectx)
519 522 commit(text='fold-temp-revision %s' % node.short(self.node),
520 523 user=rulectx.user(), date=rulectx.date(),
521 524 extra=rulectx.extra())
522 525
523 526 def continueclean(self):
524 527 repo = self.repo
525 528 ctx = repo['.']
526 529 rulectx = repo[self.node]
527 530 parentctxnode = self.state.parentctxnode
528 531 if ctx.node() == parentctxnode:
529 532 repo.ui.warn(_('%s: empty changeset\n') %
530 533 node.short(self.node))
531 534 return ctx, [(self.node, (parentctxnode,))]
532 535
533 536 parentctx = repo[parentctxnode]
534 537 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
535 538 parentctx))
536 539 if not newcommits:
537 540 repo.ui.warn(_('%s: cannot fold - working copy is not a '
538 541 'descendant of previous commit %s\n') %
539 542 (node.short(self.node), node.short(parentctxnode)))
540 543 return ctx, [(self.node, (ctx.node(),))]
541 544
542 545 middlecommits = newcommits.copy()
543 546 middlecommits.discard(ctx.node())
544 547
545 548 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
546 549 middlecommits)
547 550
548 551 def skipprompt(self):
549 552 return False
550 553
551 554 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
552 555 parent = ctx.parents()[0].node()
553 556 hg.update(repo, parent)
554 557 ### prepare new commit data
555 558 commitopts = {}
556 559 commitopts['user'] = ctx.user()
557 560 # commit message
558 561 if self.skipprompt():
559 562 newmessage = ctx.description()
560 563 else:
561 564 newmessage = '\n***\n'.join(
562 565 [ctx.description()] +
563 566 [repo[r].description() for r in internalchanges] +
564 567 [oldctx.description()]) + '\n'
565 568 commitopts['message'] = newmessage
566 569 # date
567 570 commitopts['date'] = max(ctx.date(), oldctx.date())
568 571 extra = ctx.extra().copy()
569 572 # histedit_source
570 573 # note: ctx is likely a temporary commit but that the best we can do
571 574 # here. This is sufficient to solve issue3681 anyway.
572 575 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
573 576 commitopts['extra'] = extra
574 577 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
575 578 try:
576 579 phasemin = max(ctx.phase(), oldctx.phase())
577 580 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
578 581 n = collapse(repo, ctx, repo[newnode], commitopts,
579 582 skipprompt=self.skipprompt())
580 583 finally:
581 584 repo.ui.restoreconfig(phasebackup)
582 585 if n is None:
583 586 return ctx, []
584 587 hg.update(repo, n)
585 588 replacements = [(oldctx.node(), (newnode,)),
586 589 (ctx.node(), (n,)),
587 590 (newnode, (n,)),
588 591 ]
589 592 for ich in internalchanges:
590 593 replacements.append((ich, (n,)))
591 594 return repo[n], replacements
592 595
593 596 class rollup(fold):
594 597 def skipprompt(self):
595 598 return True
596 599
597 600 class drop(histeditaction):
598 601 def run(self):
599 602 parentctx = self.repo[self.state.parentctxnode]
600 603 return parentctx, [(self.node, tuple())]
601 604
602 605 class message(histeditaction):
603 606 def commiteditor(self):
604 607 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
605 608
606 609 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
607 610 """utility function to find the first outgoing changeset
608 611
609 612 Used by initialisation code"""
610 613 dest = ui.expandpath(remote or 'default-push', remote or 'default')
611 614 dest, revs = hg.parseurl(dest, None)[:2]
612 615 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
613 616
614 617 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
615 618 other = hg.peer(repo, opts, dest)
616 619
617 620 if revs:
618 621 revs = [repo.lookup(rev) for rev in revs]
619 622
620 623 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
621 624 if not outgoing.missing:
622 625 raise util.Abort(_('no outgoing ancestors'))
623 626 roots = list(repo.revs("roots(%ln)", outgoing.missing))
624 627 if 1 < len(roots):
625 628 msg = _('there are ambiguous outgoing revisions')
626 629 hint = _('see "hg help histedit" for more detail')
627 630 raise util.Abort(msg, hint=hint)
628 631 return repo.lookup(roots[0])
629 632
630 633 actiontable = {'p': pick,
631 634 'pick': pick,
632 635 'e': edit,
633 636 'edit': edit,
634 637 'f': fold,
635 638 'fold': fold,
636 639 'r': rollup,
637 640 'roll': rollup,
638 641 'd': drop,
639 642 'drop': drop,
640 643 'm': message,
641 644 'mess': message,
642 645 }
643 646
644 647 @command('histedit',
645 648 [('', 'commands', '',
646 649 _('read history edits from the specified file'), _('FILE')),
647 650 ('c', 'continue', False, _('continue an edit already in progress')),
648 651 ('', 'edit-plan', False, _('edit remaining actions list')),
649 652 ('k', 'keep', False,
650 653 _("don't strip old nodes after edit is complete")),
651 654 ('', 'abort', False, _('abort an edit in progress')),
652 655 ('o', 'outgoing', False, _('changesets not found in destination')),
653 656 ('f', 'force', False,
654 657 _('force outgoing even for unrelated repositories')),
655 658 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
656 659 _("ANCESTOR | --outgoing [URL]"))
657 660 def histedit(ui, repo, *freeargs, **opts):
658 661 """interactively edit changeset history
659 662
660 663 This command edits changesets between ANCESTOR and the parent of
661 664 the working directory.
662 665
663 666 With --outgoing, this edits changesets not found in the
664 667 destination repository. If URL of the destination is omitted, the
665 668 'default-push' (or 'default') path will be used.
666 669
667 670 For safety, this command is aborted, also if there are ambiguous
668 671 outgoing revisions which may confuse users: for example, there are
669 672 multiple branches containing outgoing revisions.
670 673
671 674 Use "min(outgoing() and ::.)" or similar revset specification
672 675 instead of --outgoing to specify edit target revision exactly in
673 676 such ambiguous situation. See :hg:`help revsets` for detail about
674 677 selecting revisions.
675 678
676 679 Returns 0 on success, 1 if user intervention is required (not only
677 680 for intentional "edit" command, but also for resolving unexpected
678 681 conflicts).
679 682 """
680 683 state = histeditstate(repo)
681 684 try:
682 685 state.wlock = repo.wlock()
683 686 state.lock = repo.lock()
684 687 _histedit(ui, repo, state, *freeargs, **opts)
685 688 finally:
686 689 release(state.lock, state.wlock)
687 690
688 691 def _histedit(ui, repo, state, *freeargs, **opts):
689 692 # TODO only abort if we try and histedit mq patches, not just
690 693 # blanket if mq patches are applied somewhere
691 694 mq = getattr(repo, 'mq', None)
692 695 if mq and mq.applied:
693 696 raise util.Abort(_('source has mq patches applied'))
694 697
695 698 # basic argument incompatibility processing
696 699 outg = opts.get('outgoing')
697 700 cont = opts.get('continue')
698 701 editplan = opts.get('edit_plan')
699 702 abort = opts.get('abort')
700 703 force = opts.get('force')
701 704 rules = opts.get('commands', '')
702 705 revs = opts.get('rev', [])
703 706 goal = 'new' # This invocation goal, in new, continue, abort
704 707 if force and not outg:
705 708 raise util.Abort(_('--force only allowed with --outgoing'))
706 709 if cont:
707 710 if util.any((outg, abort, revs, freeargs, rules, editplan)):
708 711 raise util.Abort(_('no arguments allowed with --continue'))
709 712 goal = 'continue'
710 713 elif abort:
711 714 if util.any((outg, revs, freeargs, rules, editplan)):
712 715 raise util.Abort(_('no arguments allowed with --abort'))
713 716 goal = 'abort'
714 717 elif editplan:
715 718 if util.any((outg, revs, freeargs)):
716 719 raise util.Abort(_('only --commands argument allowed with '
717 720 '--edit-plan'))
718 721 goal = 'edit-plan'
719 722 else:
720 723 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
721 724 raise util.Abort(_('history edit already in progress, try '
722 725 '--continue or --abort'))
723 726 if outg:
724 727 if revs:
725 728 raise util.Abort(_('no revisions allowed with --outgoing'))
726 729 if len(freeargs) > 1:
727 730 raise util.Abort(
728 731 _('only one repo argument allowed with --outgoing'))
729 732 else:
730 733 revs.extend(freeargs)
731 734 if len(revs) == 0:
732 735 histeditdefault = ui.config('histedit', 'defaultrev')
733 736 if histeditdefault:
734 737 revs.append(histeditdefault)
735 738 if len(revs) != 1:
736 739 raise util.Abort(
737 740 _('histedit requires exactly one ancestor revision'))
738 741
739 742
740 743 replacements = []
741 744 keep = opts.get('keep', False)
742 745
743 746 # rebuild state
744 747 if goal == 'continue':
745 748 state.read()
746 749 state = bootstrapcontinue(ui, state, opts)
747 750 elif goal == 'edit-plan':
748 751 state.read()
749 752 if not rules:
750 753 comment = editcomment % (node.short(state.parentctxnode),
751 754 node.short(state.topmost))
752 755 rules = ruleeditor(repo, ui, state.rules, comment)
753 756 else:
754 757 if rules == '-':
755 758 f = sys.stdin
756 759 else:
757 760 f = open(rules)
758 761 rules = f.read()
759 762 f.close()
760 763 rules = [l for l in (r.strip() for r in rules.splitlines())
761 764 if l and not l.startswith('#')]
762 765 rules = verifyrules(rules, repo, [repo[c] for [_a, c] in state.rules])
763 766 state.rules = rules
764 767 state.write()
765 768 return
766 769 elif goal == 'abort':
767 770 state.read()
768 771 mapping, tmpnodes, leafs, _ntm = processreplacement(state)
769 772 ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
770 773
771 774 # Recover our old commits if necessary
772 775 if not state.topmost in repo and state.backupfile:
773 776 backupfile = repo.join(state.backupfile)
774 777 f = hg.openpath(ui, backupfile)
775 778 gen = exchange.readbundle(ui, f, backupfile)
776 779 changegroup.addchangegroup(repo, gen, 'histedit',
777 780 'bundle:' + backupfile)
778 781 os.remove(backupfile)
779 782
780 783 # check whether we should update away
781 784 parentnodes = [c.node() for c in repo[None].parents()]
782 785 for n in leafs | set([state.parentctxnode]):
783 786 if n in parentnodes:
784 787 hg.clean(repo, state.topmost)
785 788 break
786 789 else:
787 790 pass
788 791 cleanupnode(ui, repo, 'created', tmpnodes)
789 792 cleanupnode(ui, repo, 'temp', leafs)
790 793 state.clear()
791 794 return
792 795 else:
793 796 cmdutil.checkunfinished(repo)
794 797 cmdutil.bailifchanged(repo)
795 798
796 799 topmost, empty = repo.dirstate.parents()
797 800 if outg:
798 801 if freeargs:
799 802 remote = freeargs[0]
800 803 else:
801 804 remote = None
802 805 root = findoutgoing(ui, repo, remote, force, opts)
803 806 else:
804 807 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
805 808 if len(rr) != 1:
806 809 raise util.Abort(_('The specified revisions must have '
807 810 'exactly one common root'))
808 811 root = rr[0].node()
809 812
810 813 revs = between(repo, root, topmost, keep)
811 814 if not revs:
812 815 raise util.Abort(_('%s is not an ancestor of working directory') %
813 816 node.short(root))
814 817
815 818 ctxs = [repo[r] for r in revs]
816 819 if not rules:
817 820 comment = editcomment % (node.short(root), node.short(topmost))
818 821 rules = ruleeditor(repo, ui, [['pick', c] for c in ctxs], comment)
819 822 else:
820 823 if rules == '-':
821 824 f = sys.stdin
822 825 else:
823 826 f = open(rules)
824 827 rules = f.read()
825 828 f.close()
826 829 rules = [l for l in (r.strip() for r in rules.splitlines())
827 830 if l and not l.startswith('#')]
828 831 rules = verifyrules(rules, repo, ctxs)
829 832
830 833 parentctxnode = repo[root].parents()[0].node()
831 834
832 835 state.parentctxnode = parentctxnode
833 836 state.rules = rules
834 837 state.keep = keep
835 838 state.topmost = topmost
836 839 state.replacements = replacements
837 840
838 841 # Create a backup so we can always abort completely.
839 842 backupfile = None
840 843 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
841 844 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
842 845 'histedit')
843 846 state.backupfile = backupfile
844 847
845 848 while state.rules:
846 849 state.write()
847 850 action, ha = state.rules.pop(0)
848 851 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
849 852 actobj = actiontable[action].fromrule(state, ha)
850 853 parentctx, replacement_ = actobj.run()
851 854 state.parentctxnode = parentctx.node()
852 855 state.replacements.extend(replacement_)
853 856 state.write()
854 857
855 858 hg.update(repo, state.parentctxnode)
856 859
857 860 mapping, tmpnodes, created, ntm = processreplacement(state)
858 861 if mapping:
859 862 for prec, succs in mapping.iteritems():
860 863 if not succs:
861 864 ui.debug('histedit: %s is dropped\n' % node.short(prec))
862 865 else:
863 866 ui.debug('histedit: %s is replaced by %s\n' % (
864 867 node.short(prec), node.short(succs[0])))
865 868 if len(succs) > 1:
866 869 m = 'histedit: %s'
867 870 for n in succs[1:]:
868 871 ui.debug(m % node.short(n))
869 872
870 873 if not keep:
871 874 if mapping:
872 875 movebookmarks(ui, repo, mapping, state.topmost, ntm)
873 876 # TODO update mq state
874 877 if obsolete.isenabled(repo, obsolete.createmarkersopt):
875 878 markers = []
876 879 # sort by revision number because it sound "right"
877 880 for prec in sorted(mapping, key=repo.changelog.rev):
878 881 succs = mapping[prec]
879 882 markers.append((repo[prec],
880 883 tuple(repo[s] for s in succs)))
881 884 if markers:
882 885 obsolete.createmarkers(repo, markers)
883 886 else:
884 887 cleanupnode(ui, repo, 'replaced', mapping)
885 888
886 889 cleanupnode(ui, repo, 'temp', tmpnodes)
887 890 state.clear()
888 891 if os.path.exists(repo.sjoin('undo')):
889 892 os.unlink(repo.sjoin('undo'))
890 893
891 894 def bootstrapcontinue(ui, state, opts):
892 895 repo = state.repo
896 if state.rules:
893 897 action, currentnode = state.rules.pop(0)
894 898
895 899 actobj = actiontable[action].fromrule(state, currentnode)
896 900
897 901 s = repo.status()
898 902 if s.modified or s.added or s.removed or s.deleted:
899 903 actobj.continuedirty()
900 904 s = repo.status()
901 905 if s.modified or s.added or s.removed or s.deleted:
902 906 raise util.Abort(_("working copy still dirty"))
903 907
904 908 parentctx, replacements = actobj.continueclean()
905 909
906 910 state.parentctxnode = parentctx.node()
907 911 state.replacements.extend(replacements)
908 912
909 913 return state
910 914
911 915 def between(repo, old, new, keep):
912 916 """select and validate the set of revision to edit
913 917
914 918 When keep is false, the specified set can't have children."""
915 919 ctxs = list(repo.set('%n::%n', old, new))
916 920 if ctxs and not keep:
917 921 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
918 922 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
919 923 raise util.Abort(_('cannot edit history that would orphan nodes'))
920 924 if repo.revs('(%ld) and merge()', ctxs):
921 925 raise util.Abort(_('cannot edit history that contains merges'))
922 926 root = ctxs[0] # list is already sorted by repo.set
923 927 if not root.mutable():
924 928 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
925 929 return [c.node() for c in ctxs]
926 930
927 931 def makedesc(repo, action, rev):
928 932 """build a initial action line for a ctx
929 933
930 934 line are in the form:
931 935
932 936 <action> <hash> <rev> <summary>
933 937 """
934 938 ctx = repo[rev]
935 939 summary = ''
936 940 if ctx.description():
937 941 summary = ctx.description().splitlines()[0]
938 942 line = '%s %s %d %s' % (action, ctx, ctx.rev(), summary)
939 943 # trim to 80 columns so it's not stupidly wide in my editor
940 944 maxlen = repo.ui.configint('histedit', 'linelen', default=80)
941 945 maxlen = max(maxlen, 22) # avoid truncating hash
942 946 return util.ellipsis(line, maxlen)
943 947
944 948 def ruleeditor(repo, ui, rules, editcomment=""):
945 949 """open an editor to edit rules
946 950
947 951 rules are in the format [ [act, ctx], ...] like in state.rules
948 952 """
949 953 rules = '\n'.join([makedesc(repo, act, rev) for [act, rev] in rules])
950 954 rules += '\n\n'
951 955 rules += editcomment
952 956 rules = ui.edit(rules, ui.username())
953 957
954 958 # Save edit rules in .hg/histedit-last-edit.txt in case
955 959 # the user needs to ask for help after something
956 960 # surprising happens.
957 961 f = open(repo.join('histedit-last-edit.txt'), 'w')
958 962 f.write(rules)
959 963 f.close()
960 964
961 965 return rules
962 966
963 967 def verifyrules(rules, repo, ctxs):
964 968 """Verify that there exists exactly one edit rule per given changeset.
965 969
966 970 Will abort if there are to many or too few rules, a malformed rule,
967 971 or a rule on a changeset outside of the user-given range.
968 972 """
969 973 parsed = []
970 974 expected = set(c.hex() for c in ctxs)
971 975 seen = set()
972 976 for r in rules:
973 977 if ' ' not in r:
974 978 raise util.Abort(_('malformed line "%s"') % r)
975 979 action, rest = r.split(' ', 1)
976 980 ha = rest.strip().split(' ', 1)[0]
977 981 try:
978 982 ha = repo[ha].hex()
979 983 except error.RepoError:
980 984 raise util.Abort(_('unknown changeset %s listed') % ha[:12])
981 985 if ha not in expected:
982 986 raise util.Abort(
983 987 _('may not use changesets other than the ones listed'))
984 988 if ha in seen:
985 989 raise util.Abort(_('duplicated command for changeset %s') %
986 990 ha[:12])
987 991 seen.add(ha)
988 992 if action not in actiontable:
989 993 raise util.Abort(_('unknown action "%s"') % action)
990 994 parsed.append([action, ha])
991 995 missing = sorted(expected - seen) # sort to stabilize output
992 996 if missing:
993 997 raise util.Abort(_('missing rules for changeset %s') %
994 998 missing[0][:12],
995 999 hint=_('do you want to use the drop action?'))
996 1000 return parsed
997 1001
998 1002 def processreplacement(state):
999 1003 """process the list of replacements to return
1000 1004
1001 1005 1) the final mapping between original and created nodes
1002 1006 2) the list of temporary node created by histedit
1003 1007 3) the list of new commit created by histedit"""
1004 1008 replacements = state.replacements
1005 1009 allsuccs = set()
1006 1010 replaced = set()
1007 1011 fullmapping = {}
1008 1012 # initialise basic set
1009 1013 # fullmapping record all operation recorded in replacement
1010 1014 for rep in replacements:
1011 1015 allsuccs.update(rep[1])
1012 1016 replaced.add(rep[0])
1013 1017 fullmapping.setdefault(rep[0], set()).update(rep[1])
1014 1018 new = allsuccs - replaced
1015 1019 tmpnodes = allsuccs & replaced
1016 1020 # Reduce content fullmapping into direct relation between original nodes
1017 1021 # and final node created during history edition
1018 1022 # Dropped changeset are replaced by an empty list
1019 1023 toproceed = set(fullmapping)
1020 1024 final = {}
1021 1025 while toproceed:
1022 1026 for x in list(toproceed):
1023 1027 succs = fullmapping[x]
1024 1028 for s in list(succs):
1025 1029 if s in toproceed:
1026 1030 # non final node with unknown closure
1027 1031 # We can't process this now
1028 1032 break
1029 1033 elif s in final:
1030 1034 # non final node, replace with closure
1031 1035 succs.remove(s)
1032 1036 succs.update(final[s])
1033 1037 else:
1034 1038 final[x] = succs
1035 1039 toproceed.remove(x)
1036 1040 # remove tmpnodes from final mapping
1037 1041 for n in tmpnodes:
1038 1042 del final[n]
1039 1043 # we expect all changes involved in final to exist in the repo
1040 1044 # turn `final` into list (topologically sorted)
1041 1045 nm = state.repo.changelog.nodemap
1042 1046 for prec, succs in final.items():
1043 1047 final[prec] = sorted(succs, key=nm.get)
1044 1048
1045 1049 # computed topmost element (necessary for bookmark)
1046 1050 if new:
1047 1051 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1048 1052 elif not final:
1049 1053 # Nothing rewritten at all. we won't need `newtopmost`
1050 1054 # It is the same as `oldtopmost` and `processreplacement` know it
1051 1055 newtopmost = None
1052 1056 else:
1053 1057 # every body died. The newtopmost is the parent of the root.
1054 1058 r = state.repo.changelog.rev
1055 1059 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1056 1060
1057 1061 return final, tmpnodes, new, newtopmost
1058 1062
1059 1063 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1060 1064 """Move bookmark from old to newly created node"""
1061 1065 if not mapping:
1062 1066 # if nothing got rewritten there is not purpose for this function
1063 1067 return
1064 1068 moves = []
1065 1069 for bk, old in sorted(repo._bookmarks.iteritems()):
1066 1070 if old == oldtopmost:
1067 1071 # special case ensure bookmark stay on tip.
1068 1072 #
1069 1073 # This is arguably a feature and we may only want that for the
1070 1074 # active bookmark. But the behavior is kept compatible with the old
1071 1075 # version for now.
1072 1076 moves.append((bk, newtopmost))
1073 1077 continue
1074 1078 base = old
1075 1079 new = mapping.get(base, None)
1076 1080 if new is None:
1077 1081 continue
1078 1082 while not new:
1079 1083 # base is killed, trying with parent
1080 1084 base = repo[base].p1().node()
1081 1085 new = mapping.get(base, (base,))
1082 1086 # nothing to move
1083 1087 moves.append((bk, new[-1]))
1084 1088 if moves:
1085 1089 marks = repo._bookmarks
1086 1090 for mark, new in moves:
1087 1091 old = marks[mark]
1088 1092 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1089 1093 % (mark, node.short(old), node.short(new)))
1090 1094 marks[mark] = new
1091 1095 marks.write()
1092 1096
1093 1097 def cleanupnode(ui, repo, name, nodes):
1094 1098 """strip a group of nodes from the repository
1095 1099
1096 1100 The set of node to strip may contains unknown nodes."""
1097 1101 ui.debug('should strip %s nodes %s\n' %
1098 1102 (name, ', '.join([node.short(n) for n in nodes])))
1099 1103 lock = None
1100 1104 try:
1101 1105 lock = repo.lock()
1102 1106 # Find all node that need to be stripped
1103 1107 # (we hg %lr instead of %ln to silently ignore unknown item
1104 1108 nm = repo.changelog.nodemap
1105 1109 nodes = sorted(n for n in nodes if n in nm)
1106 1110 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1107 1111 for c in roots:
1108 1112 # We should process node in reverse order to strip tip most first.
1109 1113 # but this trigger a bug in changegroup hook.
1110 1114 # This would reduce bundle overhead
1111 1115 repair.strip(ui, repo, c)
1112 1116 finally:
1113 1117 release(lock)
1114 1118
1115 1119 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1116 1120 if isinstance(nodelist, str):
1117 1121 nodelist = [nodelist]
1118 1122 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1119 1123 state = histeditstate(repo)
1120 1124 state.read()
1121 1125 histedit_nodes = set([repo[rulehash].node() for (action, rulehash)
1122 1126 in state.rules if rulehash in repo])
1123 1127 strip_nodes = set([repo[n].node() for n in nodelist])
1124 1128 common_nodes = histedit_nodes & strip_nodes
1125 1129 if common_nodes:
1126 1130 raise util.Abort(_("histedit in progress, can't strip %s")
1127 1131 % ', '.join(node.short(x) for x in common_nodes))
1128 1132 return orig(ui, repo, nodelist, *args, **kwargs)
1129 1133
1130 1134 extensions.wrapfunction(repair, 'strip', stripwrapper)
1131 1135
1132 1136 def summaryhook(ui, repo):
1133 1137 if not os.path.exists(repo.join('histedit-state')):
1134 1138 return
1135 1139 state = histeditstate(repo)
1136 1140 state.read()
1137 1141 if state.rules:
1138 1142 # i18n: column positioning for "hg summary"
1139 1143 ui.write(_('hist: %s (histedit --continue)\n') %
1140 1144 (ui.label(_('%d remaining'), 'histedit.remaining') %
1141 1145 len(state.rules)))
1142 1146
1143 1147 def extsetup(ui):
1144 1148 cmdutil.summaryhooks.add('histedit', summaryhook)
1145 1149 cmdutil.unfinishedstates.append(
1146 1150 ['histedit-state', False, True, _('histedit in progress'),
1147 1151 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,547 +1,554
1 1 This file tests the behavior of run-tests.py itself.
2 2
3 Avoid interference from actual test env:
4
5 $ unset HGTEST_JOBS
6 $ unset HGTEST_TIMEOUT
7 $ unset HGTEST_PORT
8 $ unset HGTEST_SHELL
9
3 10 Smoke test
4 11 ============
5 12
6 13 $ $TESTDIR/run-tests.py $HGTEST_RUN_TESTS_PURE
7 14
8 15 # Ran 0 tests, 0 skipped, 0 warned, 0 failed.
9 16
10 17 a succesful test
11 18 =======================
12 19
13 20 $ cat > test-success.t << EOF
14 21 > $ echo babar
15 22 > babar
16 23 > $ echo xyzzy
17 24 > xyzzy
18 25 > EOF
19 26
20 27 $ $TESTDIR/run-tests.py --with-hg=`which hg`
21 28 .
22 29 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
23 30
24 31 failing test
25 32 ==================
26 33
27 34 $ cat > test-failure.t << EOF
28 35 > $ echo babar
29 36 > rataxes
30 37 > This is a noop statement so that
31 38 > this test is still more bytes than success.
32 39 > EOF
33 40
34 41 >>> fh = open('test-failure-unicode.t', 'wb')
35 42 >>> fh.write(u' $ echo babar\u03b1\n'.encode('utf-8'))
36 43 >>> fh.write(u' l\u03b5\u03b5t\n'.encode('utf-8'))
37 44
38 45 $ $TESTDIR/run-tests.py --with-hg=`which hg`
39 46
40 47 --- $TESTTMP/test-failure.t
41 48 +++ $TESTTMP/test-failure.t.err
42 49 @@ -1,4 +1,4 @@
43 50 $ echo babar
44 51 - rataxes
45 52 + babar
46 53 This is a noop statement so that
47 54 this test is still more bytes than success.
48 55
49 56 ERROR: test-failure.t output changed
50 57 !.
51 58 --- $TESTTMP/test-failure-unicode.t
52 59 +++ $TESTTMP/test-failure-unicode.t.err
53 60 @@ -1,2 +1,2 @@
54 61 $ echo babar\xce\xb1 (esc)
55 62 - l\xce\xb5\xce\xb5t (esc)
56 63 + babar\xce\xb1 (esc)
57 64
58 65 ERROR: test-failure-unicode.t output changed
59 66 !
60 67 Failed test-failure.t: output changed
61 68 Failed test-failure-unicode.t: output changed
62 69 # Ran 3 tests, 0 skipped, 0 warned, 2 failed.
63 70 python hash seed: * (glob)
64 71 [1]
65 72
66 73 test --xunit support
67 74 $ $TESTDIR/run-tests.py --with-hg=`which hg` --xunit=xunit.xml
68 75
69 76 --- $TESTTMP/test-failure.t
70 77 +++ $TESTTMP/test-failure.t.err
71 78 @@ -1,4 +1,4 @@
72 79 $ echo babar
73 80 - rataxes
74 81 + babar
75 82 This is a noop statement so that
76 83 this test is still more bytes than success.
77 84
78 85 ERROR: test-failure.t output changed
79 86 !.
80 87 --- $TESTTMP/test-failure-unicode.t
81 88 +++ $TESTTMP/test-failure-unicode.t.err
82 89 @@ -1,2 +1,2 @@
83 90 $ echo babar\xce\xb1 (esc)
84 91 - l\xce\xb5\xce\xb5t (esc)
85 92 + babar\xce\xb1 (esc)
86 93
87 94 ERROR: test-failure-unicode.t output changed
88 95 !
89 96 Failed test-failure.t: output changed
90 97 Failed test-failure-unicode.t: output changed
91 98 # Ran 3 tests, 0 skipped, 0 warned, 2 failed.
92 99 python hash seed: * (glob)
93 100 [1]
94 101 $ cat xunit.xml
95 102 <?xml version="1.0" encoding="utf-8"?>
96 103 <testsuite errors="0" failures="2" name="run-tests" skipped="0" tests="3">
97 104 <testcase name="test-success.t" time="*"/> (glob)
98 105 <testcase name="test-failure-unicode.t" time="*"> (glob)
99 106 <![CDATA[--- $TESTTMP/test-failure-unicode.t
100 107 +++ $TESTTMP/test-failure-unicode.t.err
101 108 @@ -1,2 +1,2 @@
102 109 $ echo babar\xce\xb1 (esc)
103 110 - l\xce\xb5\xce\xb5t (esc)
104 111 + babar\xce\xb1 (esc)
105 112 ]]> </testcase>
106 113 <testcase name="test-failure.t" time="*"> (glob)
107 114 <![CDATA[--- $TESTTMP/test-failure.t
108 115 +++ $TESTTMP/test-failure.t.err
109 116 @@ -1,4 +1,4 @@
110 117 $ echo babar
111 118 - rataxes
112 119 + babar
113 120 This is a noop statement so that
114 121 this test is still more bytes than success.
115 122 ]]> </testcase>
116 123 </testsuite>
117 124
118 125 $ rm test-failure-unicode.t
119 126
120 127 test for --retest
121 128 ====================
122 129
123 130 $ $TESTDIR/run-tests.py --with-hg=`which hg` --retest
124 131
125 132 --- $TESTTMP/test-failure.t
126 133 +++ $TESTTMP/test-failure.t.err
127 134 @@ -1,4 +1,4 @@
128 135 $ echo babar
129 136 - rataxes
130 137 + babar
131 138 This is a noop statement so that
132 139 this test is still more bytes than success.
133 140
134 141 ERROR: test-failure.t output changed
135 142 !
136 143 Failed test-failure.t: output changed
137 144 # Ran 2 tests, 1 skipped, 0 warned, 1 failed.
138 145 python hash seed: * (glob)
139 146 [1]
140 147
141 148 Selecting Tests To Run
142 149 ======================
143 150
144 151 successful
145 152
146 153 $ $TESTDIR/run-tests.py --with-hg=`which hg` test-success.t
147 154 .
148 155 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
149 156
150 157 success w/ keyword
151 158 $ $TESTDIR/run-tests.py --with-hg=`which hg` -k xyzzy
152 159 .
153 160 # Ran 2 tests, 1 skipped, 0 warned, 0 failed.
154 161
155 162 failed
156 163
157 164 $ $TESTDIR/run-tests.py --with-hg=`which hg` test-failure.t
158 165
159 166 --- $TESTTMP/test-failure.t
160 167 +++ $TESTTMP/test-failure.t.err
161 168 @@ -1,4 +1,4 @@
162 169 $ echo babar
163 170 - rataxes
164 171 + babar
165 172 This is a noop statement so that
166 173 this test is still more bytes than success.
167 174
168 175 ERROR: test-failure.t output changed
169 176 !
170 177 Failed test-failure.t: output changed
171 178 # Ran 1 tests, 0 skipped, 0 warned, 1 failed.
172 179 python hash seed: * (glob)
173 180 [1]
174 181
175 182 failure w/ keyword
176 183 $ $TESTDIR/run-tests.py --with-hg=`which hg` -k rataxes
177 184
178 185 --- $TESTTMP/test-failure.t
179 186 +++ $TESTTMP/test-failure.t.err
180 187 @@ -1,4 +1,4 @@
181 188 $ echo babar
182 189 - rataxes
183 190 + babar
184 191 This is a noop statement so that
185 192 this test is still more bytes than success.
186 193
187 194 ERROR: test-failure.t output changed
188 195 !
189 196 Failed test-failure.t: output changed
190 197 # Ran 2 tests, 1 skipped, 0 warned, 1 failed.
191 198 python hash seed: * (glob)
192 199 [1]
193 200
194 201 Verify that when a process fails to start we show a useful message
195 202 ==================================================================
196 203 NOTE: there is currently a bug where this shows "2 failed" even though
197 204 it's actually the same test being reported for failure twice.
198 205
199 206 $ cat > test-serve-fail.t <<EOF
200 207 > $ echo 'abort: child process failed to start blah'
201 208 > EOF
202 209 $ $TESTDIR/run-tests.py --with-hg=`which hg` test-serve-fail.t
203 210
204 211 ERROR: test-serve-fail.t output changed
205 212 !
206 213 ERROR: test-serve-fail.t output changed
207 214 !
208 215 Failed test-serve-fail.t: server failed to start (HGPORT=*) (glob)
209 216 Failed test-serve-fail.t: output changed
210 217 # Ran 1 tests, 0 skipped, 0 warned, 2 failed.
211 218 python hash seed: * (glob)
212 219 [1]
213 220 $ rm test-serve-fail.t
214 221
215 222 Running In Debug Mode
216 223 ======================
217 224
218 225 $ $TESTDIR/run-tests.py --with-hg=`which hg` --debug 2>&1 | grep -v pwd
219 226 + echo *SALT* 0 0 (glob)
220 227 *SALT* 0 0 (glob)
221 228 + echo babar
222 229 babar
223 230 + echo *SALT* 4 0 (glob)
224 231 *SALT* 4 0 (glob)
225 232 .+ echo *SALT* 0 0 (glob)
226 233 *SALT* 0 0 (glob)
227 234 + echo babar
228 235 babar
229 236 + echo *SALT* 2 0 (glob)
230 237 *SALT* 2 0 (glob)
231 238 + echo xyzzy
232 239 xyzzy
233 240 + echo *SALT* 4 0 (glob)
234 241 *SALT* 4 0 (glob)
235 242 .
236 243 # Ran 2 tests, 0 skipped, 0 warned, 0 failed.
237 244
238 245 Parallel runs
239 246 ==============
240 247
241 248 (duplicate the failing test to get predictable output)
242 249 $ cp test-failure.t test-failure-copy.t
243 250
244 251 $ $TESTDIR/run-tests.py --with-hg=`which hg` --jobs 2 test-failure*.t -n
245 252 !!
246 253 Failed test-failure*.t: output changed (glob)
247 254 Failed test-failure*.t: output changed (glob)
248 255 # Ran 2 tests, 0 skipped, 0 warned, 2 failed.
249 256 python hash seed: * (glob)
250 257 [1]
251 258
252 259 failures in parallel with --first should only print one failure
253 260 >>> f = open('test-nothing.t', 'w')
254 261 >>> f.write('foo\n' * 1024)
255 262 >>> f.write(' $ sleep 1')
256 263 $ $TESTDIR/run-tests.py --with-hg=`which hg` --jobs 2 --first
257 264
258 265 --- $TESTTMP/test-failure*.t (glob)
259 266 +++ $TESTTMP/test-failure*.t.err (glob)
260 267 @@ -1,4 +1,4 @@
261 268 $ echo babar
262 269 - rataxes
263 270 + babar
264 271 This is a noop statement so that
265 272 this test is still more bytes than success.
266 273
267 274 Failed test-failure*.t: output changed (glob)
268 275 Failed test-nothing.t: output changed
269 276 # Ran 2 tests, 0 skipped, 0 warned, 2 failed.
270 277 python hash seed: * (glob)
271 278 [1]
272 279
273 280
274 281 (delete the duplicated test file)
275 282 $ rm test-failure-copy.t test-nothing.t
276 283
277 284
278 285 Interactive run
279 286 ===============
280 287
281 288 (backup the failing test)
282 289 $ cp test-failure.t backup
283 290
284 291 Refuse the fix
285 292
286 293 $ echo 'n' | $TESTDIR/run-tests.py --with-hg=`which hg` -i
287 294
288 295 --- $TESTTMP/test-failure.t
289 296 +++ $TESTTMP/test-failure.t.err
290 297 @@ -1,4 +1,4 @@
291 298 $ echo babar
292 299 - rataxes
293 300 + babar
294 301 This is a noop statement so that
295 302 this test is still more bytes than success.
296 303 Accept this change? [n]
297 304 ERROR: test-failure.t output changed
298 305 !.
299 306 Failed test-failure.t: output changed
300 307 # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
301 308 python hash seed: * (glob)
302 309 [1]
303 310
304 311 $ cat test-failure.t
305 312 $ echo babar
306 313 rataxes
307 314 This is a noop statement so that
308 315 this test is still more bytes than success.
309 316
310 317 Interactive with custom view
311 318
312 319 $ echo 'n' | $TESTDIR/run-tests.py --with-hg=`which hg` -i --view echo
313 320 $TESTTMP/test-failure.t $TESTTMP/test-failure.t.err (glob)
314 321 Accept this change? [n]* (glob)
315 322 ERROR: test-failure.t output changed
316 323 !.
317 324 Failed test-failure.t: output changed
318 325 # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
319 326 python hash seed: * (glob)
320 327 [1]
321 328
322 329 View the fix
323 330
324 331 $ echo 'y' | $TESTDIR/run-tests.py --with-hg=`which hg` --view echo
325 332 $TESTTMP/test-failure.t $TESTTMP/test-failure.t.err (glob)
326 333
327 334 ERROR: test-failure.t output changed
328 335 !.
329 336 Failed test-failure.t: output changed
330 337 # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
331 338 python hash seed: * (glob)
332 339 [1]
333 340
334 341 Accept the fix
335 342
336 343 $ echo " $ echo 'saved backup bundle to \$TESTTMP/foo.hg'" >> test-failure.t
337 344 $ echo " saved backup bundle to \$TESTTMP/foo.hg" >> test-failure.t
338 345 $ echo " $ echo 'saved backup bundle to \$TESTTMP/foo.hg'" >> test-failure.t
339 346 $ echo " saved backup bundle to \$TESTTMP/foo.hg (glob)" >> test-failure.t
340 347 $ echo " $ echo 'saved backup bundle to \$TESTTMP/foo.hg'" >> test-failure.t
341 348 $ echo " saved backup bundle to \$TESTTMP/*.hg (glob)" >> test-failure.t
342 349 $ echo 'y' | $TESTDIR/run-tests.py --with-hg=`which hg` -i 2>&1 | \
343 350 > sed -e 's,(glob)$,&<,g'
344 351
345 352 --- $TESTTMP/test-failure.t
346 353 +++ $TESTTMP/test-failure.t.err
347 354 @@ -1,9 +1,9 @@
348 355 $ echo babar
349 356 - rataxes
350 357 + babar
351 358 This is a noop statement so that
352 359 this test is still more bytes than success.
353 360 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
354 361 - saved backup bundle to $TESTTMP/foo.hg
355 362 + saved backup bundle to $TESTTMP/foo.hg (glob)<
356 363 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
357 364 saved backup bundle to $TESTTMP/foo.hg (glob)<
358 365 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
359 366 Accept this change? [n] ..
360 367 # Ran 2 tests, 0 skipped, 0 warned, 0 failed.
361 368
362 369 $ sed -e 's,(glob)$,&<,g' test-failure.t
363 370 $ echo babar
364 371 babar
365 372 This is a noop statement so that
366 373 this test is still more bytes than success.
367 374 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
368 375 saved backup bundle to $TESTTMP/foo.hg (glob)<
369 376 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
370 377 saved backup bundle to $TESTTMP/foo.hg (glob)<
371 378 $ echo 'saved backup bundle to $TESTTMP/foo.hg'
372 379 saved backup bundle to $TESTTMP/*.hg (glob)<
373 380
374 381 (reinstall)
375 382 $ mv backup test-failure.t
376 383
377 384 No Diff
378 385 ===============
379 386
380 387 $ $TESTDIR/run-tests.py --with-hg=`which hg` --nodiff
381 388 !.
382 389 Failed test-failure.t: output changed
383 390 # Ran 2 tests, 0 skipped, 0 warned, 1 failed.
384 391 python hash seed: * (glob)
385 392 [1]
386 393
387 394 test for --time
388 395 ==================
389 396
390 397 $ $TESTDIR/run-tests.py --with-hg=`which hg` test-success.t --time
391 398 .
392 399 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
393 400 # Producing time report
394 401 cuser csys real Test
395 402 \s*[\d\.]{5} \s*[\d\.]{5} \s*[\d\.]{5} test-success.t (re)
396 403
397 404 test for --time with --job enabled
398 405 ====================================
399 406
400 407 $ $TESTDIR/run-tests.py --with-hg=`which hg` test-success.t --time --jobs 2
401 408 .
402 409 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
403 410 # Producing time report
404 411 cuser csys real Test
405 412 \s*[\d\.]{5} \s*[\d\.]{5} \s*[\d\.]{5} test-success.t (re)
406 413
407 414 Skips
408 415 ================
409 416 $ cat > test-skip.t <<EOF
410 417 > $ echo xyzzy
411 418 > #require false
412 419 > EOF
413 420 $ $TESTDIR/run-tests.py --with-hg=`which hg` --nodiff
414 421 !.s
415 422 Skipped test-skip.t: skipped
416 423 Failed test-failure.t: output changed
417 424 # Ran 2 tests, 1 skipped, 0 warned, 1 failed.
418 425 python hash seed: * (glob)
419 426 [1]
420 427
421 428 $ $TESTDIR/run-tests.py --with-hg=`which hg` --keyword xyzzy
422 429 .s
423 430 Skipped test-skip.t: skipped
424 431 # Ran 2 tests, 2 skipped, 0 warned, 0 failed.
425 432
426 433 Skips with xml
427 434 $ $TESTDIR/run-tests.py --with-hg=`which hg` --keyword xyzzy \
428 435 > --xunit=xunit.xml
429 436 .s
430 437 Skipped test-skip.t: skipped
431 438 # Ran 2 tests, 2 skipped, 0 warned, 0 failed.
432 439 $ cat xunit.xml
433 440 <?xml version="1.0" encoding="utf-8"?>
434 441 <testsuite errors="0" failures="0" name="run-tests" skipped="2" tests="2">
435 442 <testcase name="test-success.t" time="*"/> (glob)
436 443 </testsuite>
437 444
438 445 Missing skips or blacklisted skips don't count as executed:
439 446 $ echo test-failure.t > blacklist
440 447 $ $TESTDIR/run-tests.py --with-hg=`which hg` --blacklist=blacklist \
441 448 > test-failure.t test-bogus.t
442 449 ss
443 450 Skipped test-bogus.t: Doesn't exist
444 451 Skipped test-failure.t: blacklisted
445 452 # Ran 0 tests, 2 skipped, 0 warned, 0 failed.
446 453
447 454 #if json
448 455
449 456 test for --json
450 457 ==================
451 458
452 459 $ $TESTDIR/run-tests.py --with-hg=`which hg` --json
453 460
454 461 --- $TESTTMP/test-failure.t
455 462 +++ $TESTTMP/test-failure.t.err
456 463 @@ -1,4 +1,4 @@
457 464 $ echo babar
458 465 - rataxes
459 466 + babar
460 467 This is a noop statement so that
461 468 this test is still more bytes than success.
462 469
463 470 ERROR: test-failure.t output changed
464 471 !.s
465 472 Skipped test-skip.t: skipped
466 473 Failed test-failure.t: output changed
467 474 # Ran 2 tests, 1 skipped, 0 warned, 1 failed.
468 475 python hash seed: * (glob)
469 476 [1]
470 477
471 478 $ cat report.json
472 479 testreport ={
473 480 "test-failure.t": [\{] (re)
474 481 "csys": "\s*[\d\.]{4,5}", ? (re)
475 482 "cuser": "\s*[\d\.]{4,5}", ? (re)
476 483 "result": "failure", ? (re)
477 484 "time": "\s*[\d\.]{4,5}" (re)
478 485 }, ? (re)
479 486 "test-skip.t": {
480 487 "csys": "\s*[\d\.]{4,5}", ? (re)
481 488 "cuser": "\s*[\d\.]{4,5}", ? (re)
482 489 "result": "skip", ? (re)
483 490 "time": "\s*[\d\.]{4,5}" (re)
484 491 }, ? (re)
485 492 "test-success.t": [\{] (re)
486 493 "csys": "\s*[\d\.]{4,5}", ? (re)
487 494 "cuser": "\s*[\d\.]{4,5}", ? (re)
488 495 "result": "success", ? (re)
489 496 "time": "\s*[\d\.]{4,5}" (re)
490 497 }
491 498 } (no-eol)
492 499
493 500 Test that failed test accepted through interactive are properly reported:
494 501
495 502 $ cp test-failure.t backup
496 503 $ echo y | $TESTDIR/run-tests.py --with-hg=`which hg` --json -i
497 504
498 505 --- $TESTTMP/test-failure.t
499 506 +++ $TESTTMP/test-failure.t.err
500 507 @@ -1,4 +1,4 @@
501 508 $ echo babar
502 509 - rataxes
503 510 + babar
504 511 This is a noop statement so that
505 512 this test is still more bytes than success.
506 513 Accept this change? [n] ..s
507 514 Skipped test-skip.t: skipped
508 515 # Ran 2 tests, 1 skipped, 0 warned, 0 failed.
509 516
510 517 $ cat report.json
511 518 testreport ={
512 519 "test-failure.t": [\{] (re)
513 520 "csys": "\s*[\d\.]{4,5}", ? (re)
514 521 "cuser": "\s*[\d\.]{4,5}", ? (re)
515 522 "result": "success", ? (re)
516 523 "time": "\s*[\d\.]{4,5}" (re)
517 524 }, ? (re)
518 525 "test-skip.t": {
519 526 "csys": "\s*[\d\.]{4,5}", ? (re)
520 527 "cuser": "\s*[\d\.]{4,5}", ? (re)
521 528 "result": "skip", ? (re)
522 529 "time": "\s*[\d\.]{4,5}" (re)
523 530 }, ? (re)
524 531 "test-success.t": [\{] (re)
525 532 "csys": "\s*[\d\.]{4,5}", ? (re)
526 533 "cuser": "\s*[\d\.]{4,5}", ? (re)
527 534 "result": "success", ? (re)
528 535 "time": "\s*[\d\.]{4,5}" (re)
529 536 }
530 537 } (no-eol)
531 538 $ mv backup test-failure.t
532 539
533 540 #endif
534 541
535 542 backslash on end of line with glob matching is handled properly
536 543
537 544 $ cat > test-glob-backslash.t << EOF
538 545 > $ echo 'foo bar \\'
539 546 > foo * \ (glob)
540 547 > EOF
541 548
542 549 $ $TESTDIR/run-tests.py --with-hg=`which hg` test-glob-backslash.t
543 550 .
544 551 # Ran 1 tests, 0 skipped, 0 warned, 0 failed.
545 552
546 553 $ rm -f test-glob-backslash.t
547 554
General Comments 0
You need to be logged in to leave comments. Login now