##// END OF EJS Templates
histedit: add rewrite.update-timestamp support to fold and mess...
Taapas Agrawal -
r41249:704a3aa3 default
parent child Browse files
Show More
@@ -0,0 +1,21 b''
1 # mock out util.makedate() to supply testable values
2
3 from __future__ import absolute_import
4
5 import os
6
7 from mercurial import pycompat
8 from mercurial.utils import dateutil
9
10 def mockmakedate():
11 filename = os.path.join(os.environ['TESTTMP'], 'testtime')
12 try:
13 with open(filename, 'rb') as timef:
14 time = float(timef.read()) + 1
15 except IOError:
16 time = 0.0
17 with open(filename, 'wb') as timef:
18 timef.write(pycompat.bytestr(time))
19 return (time, 0)
20
21 dateutil.makedate = mockmakedate
@@ -1,2241 +1,2249 b''
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 and date
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit commit message without changing commit content
42 42 # b, base = checkout changeset and apply further changesets from there
43 43 #
44 44
45 45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 46 for each revision in your history. For example, if you had meant to add gamma
47 47 before beta, and then wanted to add delta in the same revision as beta, you
48 48 would reorganize the file to look like this::
49 49
50 50 pick 030b686bedc4 Add gamma
51 51 pick c561b4e977df Add beta
52 52 fold 7c2fd3b9020c Add delta
53 53
54 54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 55 #
56 56 # Commits are listed from least to most recent
57 57 #
58 58 # Commands:
59 59 # p, pick = use commit
60 60 # e, edit = use commit, but stop for amending
61 61 # f, fold = use commit, but combine it with the one above
62 62 # r, roll = like fold, but discard this commit's description and date
63 63 # d, drop = remove commit from history
64 64 # m, mess = edit commit message without changing commit content
65 65 # b, base = checkout changeset and apply further changesets from there
66 66 #
67 67
68 68 At which point you close the editor and ``histedit`` starts working. When you
69 69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 70 those revisions together, offering you a chance to clean up the commit message::
71 71
72 72 Add beta
73 73 ***
74 74 Add delta
75 75
76 76 Edit the commit message to your liking, then close the editor. The date used
77 77 for the commit will be the later of the two commits' dates. For this example,
78 78 let's assume that the commit message was changed to ``Add beta and delta.``
79 79 After histedit has run and had a chance to remove any old or temporary
80 80 revisions it needed, the history looks like this::
81 81
82 82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 83 | Add beta and delta.
84 84 |
85 85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 86 | Add gamma
87 87 |
88 88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 89 Add alpha
90 90
91 91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 92 ones) until after it has completed all the editing operations, so it will
93 93 probably perform several strip operations when it's done. For the above example,
94 94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 95 so you might need to be a little patient. You can choose to keep the original
96 96 revisions by passing the ``--keep`` flag.
97 97
98 98 The ``edit`` operation will drop you back to a command prompt,
99 99 allowing you to edit files freely, or even use ``hg record`` to commit
100 100 some changes as a separate commit. When you're done, any remaining
101 101 uncommitted changes will be committed as well. When done, run ``hg
102 102 histedit --continue`` to finish this step. If there are uncommitted
103 103 changes, you'll be prompted for a new commit message, but the default
104 104 commit message will be the original message for the ``edit`` ed
105 105 revision, and the date of the original commit will be preserved.
106 106
107 107 The ``message`` operation will give you a chance to revise a commit
108 108 message without changing the contents. It's a shortcut for doing
109 109 ``edit`` immediately followed by `hg histedit --continue``.
110 110
111 111 If ``histedit`` encounters a conflict when moving a revision (while
112 112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 113 ``edit`` with the difference that it won't prompt you for a commit
114 114 message when done. If you decide at this point that you don't like how
115 115 much work it will be to rearrange history, or that you made a mistake,
116 116 you can use ``hg histedit --abort`` to abandon the new changes you
117 117 have made and return to the state before you attempted to edit your
118 118 history.
119 119
120 120 If we clone the histedit-ed example repository above and add four more
121 121 changes, such that we have the following history::
122 122
123 123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 124 | Add theta
125 125 |
126 126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 127 | Add eta
128 128 |
129 129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 130 | Add zeta
131 131 |
132 132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 133 | Add epsilon
134 134 |
135 135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 136 | Add beta and delta.
137 137 |
138 138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 139 | Add gamma
140 140 |
141 141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 142 Add alpha
143 143
144 144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 146 repository that Mercurial does not detect to be related to the source
147 147 repo, you can add a ``--force`` option.
148 148
149 149 Config
150 150 ------
151 151
152 152 Histedit rule lines are truncated to 80 characters by default. You
153 153 can customize this behavior by setting a different length in your
154 154 configuration file::
155 155
156 156 [histedit]
157 157 linelen = 120 # truncate rule lines at 120 characters
158 158
159 159 ``hg histedit`` attempts to automatically choose an appropriate base
160 160 revision to use. To change which base revision is used, define a
161 161 revset in your configuration file::
162 162
163 163 [histedit]
164 164 defaultrev = only(.) & draft()
165 165
166 166 By default each edited revision needs to be present in histedit commands.
167 167 To remove revision you need to use ``drop`` operation. You can configure
168 168 the drop to be implicit for missing commits by adding::
169 169
170 170 [histedit]
171 171 dropmissing = True
172 172
173 173 By default, histedit will close the transaction after each action. For
174 174 performance purposes, you can configure histedit to use a single transaction
175 175 across the entire histedit. WARNING: This setting introduces a significant risk
176 176 of losing the work you've done in a histedit if the histedit aborts
177 177 unexpectedly::
178 178
179 179 [histedit]
180 180 singletransaction = True
181 181
182 182 """
183 183
184 184 from __future__ import absolute_import
185 185
186 186 # chistedit dependencies that are not available everywhere
187 187 try:
188 188 import fcntl
189 189 import termios
190 190 except ImportError:
191 191 fcntl = None
192 192 termios = None
193 193
194 194 import functools
195 195 import os
196 196 import struct
197 197
198 198 from mercurial.i18n import _
199 199 from mercurial import (
200 200 bundle2,
201 201 cmdutil,
202 202 context,
203 203 copies,
204 204 destutil,
205 205 discovery,
206 206 error,
207 207 exchange,
208 208 extensions,
209 209 hg,
210 210 logcmdutil,
211 211 merge as mergemod,
212 212 mergeutil,
213 213 node,
214 214 obsolete,
215 215 pycompat,
216 216 registrar,
217 217 repair,
218 218 scmutil,
219 219 state as statemod,
220 220 util,
221 221 )
222 222 from mercurial.utils import (
223 dateutil,
223 224 stringutil,
224 225 )
225 226
226 227 pickle = util.pickle
227 228 cmdtable = {}
228 229 command = registrar.command(cmdtable)
229 230
230 231 configtable = {}
231 232 configitem = registrar.configitem(configtable)
232 233 configitem('experimental', 'histedit.autoverb',
233 234 default=False,
234 235 )
235 236 configitem('histedit', 'defaultrev',
236 237 default=None,
237 238 )
238 239 configitem('histedit', 'dropmissing',
239 240 default=False,
240 241 )
241 242 configitem('histedit', 'linelen',
242 243 default=80,
243 244 )
244 245 configitem('histedit', 'singletransaction',
245 246 default=False,
246 247 )
247 248 configitem('ui', 'interface.histedit',
248 249 default=None,
249 250 )
250 251
251 252 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
252 253 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
253 254 # be specifying the version(s) of Mercurial they are tested with, or
254 255 # leave the attribute unspecified.
255 256 testedwith = 'ships-with-hg-core'
256 257
257 258 actiontable = {}
258 259 primaryactions = set()
259 260 secondaryactions = set()
260 261 tertiaryactions = set()
261 262 internalactions = set()
262 263
263 264 def geteditcomment(ui, first, last):
264 265 """ construct the editor comment
265 266 The comment includes::
266 267 - an intro
267 268 - sorted primary commands
268 269 - sorted short commands
269 270 - sorted long commands
270 271 - additional hints
271 272
272 273 Commands are only included once.
273 274 """
274 275 intro = _("""Edit history between %s and %s
275 276
276 277 Commits are listed from least to most recent
277 278
278 279 You can reorder changesets by reordering the lines
279 280
280 281 Commands:
281 282 """)
282 283 actions = []
283 284 def addverb(v):
284 285 a = actiontable[v]
285 286 lines = a.message.split("\n")
286 287 if len(a.verbs):
287 288 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
288 289 actions.append(" %s = %s" % (v, lines[0]))
289 290 actions.extend([' %s' for l in lines[1:]])
290 291
291 292 for v in (
292 293 sorted(primaryactions) +
293 294 sorted(secondaryactions) +
294 295 sorted(tertiaryactions)
295 296 ):
296 297 addverb(v)
297 298 actions.append('')
298 299
299 300 hints = []
300 301 if ui.configbool('histedit', 'dropmissing'):
301 302 hints.append("Deleting a changeset from the list "
302 303 "will DISCARD it from the edited history!")
303 304
304 305 lines = (intro % (first, last)).split('\n') + actions + hints
305 306
306 307 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
307 308
308 309 class histeditstate(object):
309 310 def __init__(self, repo):
310 311 self.repo = repo
311 312 self.actions = None
312 313 self.keep = None
313 314 self.topmost = None
314 315 self.parentctxnode = None
315 316 self.lock = None
316 317 self.wlock = None
317 318 self.backupfile = None
318 319 self.stateobj = statemod.cmdstate(repo, 'histedit-state')
319 320 self.replacements = []
320 321
321 322 def read(self):
322 323 """Load histedit state from disk and set fields appropriately."""
323 324 if not self.stateobj.exists():
324 325 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
325 326
326 327 data = self._read()
327 328
328 329 self.parentctxnode = data['parentctxnode']
329 330 actions = parserules(data['rules'], self)
330 331 self.actions = actions
331 332 self.keep = data['keep']
332 333 self.topmost = data['topmost']
333 334 self.replacements = data['replacements']
334 335 self.backupfile = data['backupfile']
335 336
336 337 def _read(self):
337 338 fp = self.repo.vfs.read('histedit-state')
338 339 if fp.startswith('v1\n'):
339 340 data = self._load()
340 341 parentctxnode, rules, keep, topmost, replacements, backupfile = data
341 342 else:
342 343 data = pickle.loads(fp)
343 344 parentctxnode, rules, keep, topmost, replacements = data
344 345 backupfile = None
345 346 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
346 347
347 348 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
348 349 "topmost": topmost, "replacements": replacements,
349 350 "backupfile": backupfile}
350 351
351 352 def write(self, tr=None):
352 353 if tr:
353 354 tr.addfilegenerator('histedit-state', ('histedit-state',),
354 355 self._write, location='plain')
355 356 else:
356 357 with self.repo.vfs("histedit-state", "w") as f:
357 358 self._write(f)
358 359
359 360 def _write(self, fp):
360 361 fp.write('v1\n')
361 362 fp.write('%s\n' % node.hex(self.parentctxnode))
362 363 fp.write('%s\n' % node.hex(self.topmost))
363 364 fp.write('%s\n' % ('True' if self.keep else 'False'))
364 365 fp.write('%d\n' % len(self.actions))
365 366 for action in self.actions:
366 367 fp.write('%s\n' % action.tostate())
367 368 fp.write('%d\n' % len(self.replacements))
368 369 for replacement in self.replacements:
369 370 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
370 371 for r in replacement[1])))
371 372 backupfile = self.backupfile
372 373 if not backupfile:
373 374 backupfile = ''
374 375 fp.write('%s\n' % backupfile)
375 376
376 377 def _load(self):
377 378 fp = self.repo.vfs('histedit-state', 'r')
378 379 lines = [l[:-1] for l in fp.readlines()]
379 380
380 381 index = 0
381 382 lines[index] # version number
382 383 index += 1
383 384
384 385 parentctxnode = node.bin(lines[index])
385 386 index += 1
386 387
387 388 topmost = node.bin(lines[index])
388 389 index += 1
389 390
390 391 keep = lines[index] == 'True'
391 392 index += 1
392 393
393 394 # Rules
394 395 rules = []
395 396 rulelen = int(lines[index])
396 397 index += 1
397 398 for i in pycompat.xrange(rulelen):
398 399 ruleaction = lines[index]
399 400 index += 1
400 401 rule = lines[index]
401 402 index += 1
402 403 rules.append((ruleaction, rule))
403 404
404 405 # Replacements
405 406 replacements = []
406 407 replacementlen = int(lines[index])
407 408 index += 1
408 409 for i in pycompat.xrange(replacementlen):
409 410 replacement = lines[index]
410 411 original = node.bin(replacement[:40])
411 412 succ = [node.bin(replacement[i:i + 40]) for i in
412 413 range(40, len(replacement), 40)]
413 414 replacements.append((original, succ))
414 415 index += 1
415 416
416 417 backupfile = lines[index]
417 418 index += 1
418 419
419 420 fp.close()
420 421
421 422 return parentctxnode, rules, keep, topmost, replacements, backupfile
422 423
423 424 def clear(self):
424 425 if self.inprogress():
425 426 self.repo.vfs.unlink('histedit-state')
426 427
427 428 def inprogress(self):
428 429 return self.repo.vfs.exists('histedit-state')
429 430
430 431
431 432 class histeditaction(object):
432 433 def __init__(self, state, node):
433 434 self.state = state
434 435 self.repo = state.repo
435 436 self.node = node
436 437
437 438 @classmethod
438 439 def fromrule(cls, state, rule):
439 440 """Parses the given rule, returning an instance of the histeditaction.
440 441 """
441 442 ruleid = rule.strip().split(' ', 1)[0]
442 443 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
443 444 # Check for validation of rule ids and get the rulehash
444 445 try:
445 446 rev = node.bin(ruleid)
446 447 except TypeError:
447 448 try:
448 449 _ctx = scmutil.revsingle(state.repo, ruleid)
449 450 rulehash = _ctx.hex()
450 451 rev = node.bin(rulehash)
451 452 except error.RepoLookupError:
452 453 raise error.ParseError(_("invalid changeset %s") % ruleid)
453 454 return cls(state, rev)
454 455
455 456 def verify(self, prev, expected, seen):
456 457 """ Verifies semantic correctness of the rule"""
457 458 repo = self.repo
458 459 ha = node.hex(self.node)
459 460 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
460 461 if self.node is None:
461 462 raise error.ParseError(_('unknown changeset %s listed') % ha[:12])
462 463 self._verifynodeconstraints(prev, expected, seen)
463 464
464 465 def _verifynodeconstraints(self, prev, expected, seen):
465 466 # by default command need a node in the edited list
466 467 if self.node not in expected:
467 468 raise error.ParseError(_('%s "%s" changeset was not a candidate')
468 469 % (self.verb, node.short(self.node)),
469 470 hint=_('only use listed changesets'))
470 471 # and only one command per node
471 472 if self.node in seen:
472 473 raise error.ParseError(_('duplicated command for changeset %s') %
473 474 node.short(self.node))
474 475
475 476 def torule(self):
476 477 """build a histedit rule line for an action
477 478
478 479 by default lines are in the form:
479 480 <hash> <rev> <summary>
480 481 """
481 482 ctx = self.repo[self.node]
482 483 summary = _getsummary(ctx)
483 484 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
484 485 # trim to 75 columns by default so it's not stupidly wide in my editor
485 486 # (the 5 more are left for verb)
486 487 maxlen = self.repo.ui.configint('histedit', 'linelen')
487 488 maxlen = max(maxlen, 22) # avoid truncating hash
488 489 return stringutil.ellipsis(line, maxlen)
489 490
490 491 def tostate(self):
491 492 """Print an action in format used by histedit state files
492 493 (the first line is a verb, the remainder is the second)
493 494 """
494 495 return "%s\n%s" % (self.verb, node.hex(self.node))
495 496
496 497 def run(self):
497 498 """Runs the action. The default behavior is simply apply the action's
498 499 rulectx onto the current parentctx."""
499 500 self.applychange()
500 501 self.continuedirty()
501 502 return self.continueclean()
502 503
503 504 def applychange(self):
504 505 """Applies the changes from this action's rulectx onto the current
505 506 parentctx, but does not commit them."""
506 507 repo = self.repo
507 508 rulectx = repo[self.node]
508 509 repo.ui.pushbuffer(error=True, labeled=True)
509 510 hg.update(repo, self.state.parentctxnode, quietempty=True)
510 511 stats = applychanges(repo.ui, repo, rulectx, {})
511 512 repo.dirstate.setbranch(rulectx.branch())
512 513 if stats.unresolvedcount:
513 514 buf = repo.ui.popbuffer()
514 515 repo.ui.write(buf)
515 516 raise error.InterventionRequired(
516 517 _('Fix up the change (%s %s)') %
517 518 (self.verb, node.short(self.node)),
518 519 hint=_('hg histedit --continue to resume'))
519 520 else:
520 521 repo.ui.popbuffer()
521 522
522 523 def continuedirty(self):
523 524 """Continues the action when changes have been applied to the working
524 525 copy. The default behavior is to commit the dirty changes."""
525 526 repo = self.repo
526 527 rulectx = repo[self.node]
527 528
528 529 editor = self.commiteditor()
529 530 commit = commitfuncfor(repo, rulectx)
530
531 if repo.ui.configbool('rewrite', 'update-timestamp'):
532 date = dateutil.makedate()
533 else:
534 date = rulectx.date()
531 535 commit(text=rulectx.description(), user=rulectx.user(),
532 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
536 date=date, extra=rulectx.extra(), editor=editor)
533 537
534 538 def commiteditor(self):
535 539 """The editor to be used to edit the commit message."""
536 540 return False
537 541
538 542 def continueclean(self):
539 543 """Continues the action when the working copy is clean. The default
540 544 behavior is to accept the current commit as the new version of the
541 545 rulectx."""
542 546 ctx = self.repo['.']
543 547 if ctx.node() == self.state.parentctxnode:
544 548 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
545 549 node.short(self.node))
546 550 return ctx, [(self.node, tuple())]
547 551 if ctx.node() == self.node:
548 552 # Nothing changed
549 553 return ctx, []
550 554 return ctx, [(self.node, (ctx.node(),))]
551 555
552 556 def commitfuncfor(repo, src):
553 557 """Build a commit function for the replacement of <src>
554 558
555 559 This function ensure we apply the same treatment to all changesets.
556 560
557 561 - Add a 'histedit_source' entry in extra.
558 562
559 563 Note that fold has its own separated logic because its handling is a bit
560 564 different and not easily factored out of the fold method.
561 565 """
562 566 phasemin = src.phase()
563 567 def commitfunc(**kwargs):
564 568 overrides = {('phases', 'new-commit'): phasemin}
565 569 with repo.ui.configoverride(overrides, 'histedit'):
566 570 extra = kwargs.get(r'extra', {}).copy()
567 571 extra['histedit_source'] = src.hex()
568 572 kwargs[r'extra'] = extra
569 573 return repo.commit(**kwargs)
570 574 return commitfunc
571 575
572 576 def applychanges(ui, repo, ctx, opts):
573 577 """Merge changeset from ctx (only) in the current working directory"""
574 578 wcpar = repo.dirstate.parents()[0]
575 579 if ctx.p1().node() == wcpar:
576 580 # edits are "in place" we do not need to make any merge,
577 581 # just applies changes on parent for editing
578 582 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
579 583 stats = mergemod.updateresult(0, 0, 0, 0)
580 584 else:
581 585 try:
582 586 # ui.forcemerge is an internal variable, do not document
583 587 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
584 588 'histedit')
585 589 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
586 590 finally:
587 591 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
588 592 return stats
589 593
590 594 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
591 595 """collapse the set of revisions from first to last as new one.
592 596
593 597 Expected commit options are:
594 598 - message
595 599 - date
596 600 - username
597 601 Commit message is edited in all cases.
598 602
599 603 This function works in memory."""
600 604 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
601 605 if not ctxs:
602 606 return None
603 607 for c in ctxs:
604 608 if not c.mutable():
605 609 raise error.ParseError(
606 610 _("cannot fold into public change %s") % node.short(c.node()))
607 611 base = firstctx.parents()[0]
608 612
609 613 # commit a new version of the old changeset, including the update
610 614 # collect all files which might be affected
611 615 files = set()
612 616 for ctx in ctxs:
613 617 files.update(ctx.files())
614 618
615 619 # Recompute copies (avoid recording a -> b -> a)
616 620 copied = copies.pathcopies(base, lastctx)
617 621
618 622 # prune files which were reverted by the updates
619 623 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
620 624 # commit version of these files as defined by head
621 625 headmf = lastctx.manifest()
622 626 def filectxfn(repo, ctx, path):
623 627 if path in headmf:
624 628 fctx = lastctx[path]
625 629 flags = fctx.flags()
626 630 mctx = context.memfilectx(repo, ctx,
627 631 fctx.path(), fctx.data(),
628 632 islink='l' in flags,
629 633 isexec='x' in flags,
630 634 copied=copied.get(path))
631 635 return mctx
632 636 return None
633 637
634 638 if commitopts.get('message'):
635 639 message = commitopts['message']
636 640 else:
637 641 message = firstctx.description()
638 642 user = commitopts.get('user')
639 643 date = commitopts.get('date')
640 644 extra = commitopts.get('extra')
641 645
642 646 parents = (firstctx.p1().node(), firstctx.p2().node())
643 647 editor = None
644 648 if not skipprompt:
645 649 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
646 650 new = context.memctx(repo,
647 651 parents=parents,
648 652 text=message,
649 653 files=files,
650 654 filectxfn=filectxfn,
651 655 user=user,
652 656 date=date,
653 657 extra=extra,
654 658 editor=editor)
655 659 return repo.commitctx(new)
656 660
657 661 def _isdirtywc(repo):
658 662 return repo[None].dirty(missing=True)
659 663
660 664 def abortdirty():
661 665 raise error.Abort(_('working copy has pending changes'),
662 666 hint=_('amend, commit, or revert them and run histedit '
663 667 '--continue, or abort with histedit --abort'))
664 668
665 669 def action(verbs, message, priority=False, internal=False):
666 670 def wrap(cls):
667 671 assert not priority or not internal
668 672 verb = verbs[0]
669 673 if priority:
670 674 primaryactions.add(verb)
671 675 elif internal:
672 676 internalactions.add(verb)
673 677 elif len(verbs) > 1:
674 678 secondaryactions.add(verb)
675 679 else:
676 680 tertiaryactions.add(verb)
677 681
678 682 cls.verb = verb
679 683 cls.verbs = verbs
680 684 cls.message = message
681 685 for verb in verbs:
682 686 actiontable[verb] = cls
683 687 return cls
684 688 return wrap
685 689
686 690 @action(['pick', 'p'],
687 691 _('use commit'),
688 692 priority=True)
689 693 class pick(histeditaction):
690 694 def run(self):
691 695 rulectx = self.repo[self.node]
692 696 if rulectx.parents()[0].node() == self.state.parentctxnode:
693 697 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
694 698 return rulectx, []
695 699
696 700 return super(pick, self).run()
697 701
698 702 @action(['edit', 'e'],
699 703 _('use commit, but stop for amending'),
700 704 priority=True)
701 705 class edit(histeditaction):
702 706 def run(self):
703 707 repo = self.repo
704 708 rulectx = repo[self.node]
705 709 hg.update(repo, self.state.parentctxnode, quietempty=True)
706 710 applychanges(repo.ui, repo, rulectx, {})
707 711 raise error.InterventionRequired(
708 712 _('Editing (%s), you may commit or record as needed now.')
709 713 % node.short(self.node),
710 714 hint=_('hg histedit --continue to resume'))
711 715
712 716 def commiteditor(self):
713 717 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
714 718
715 719 @action(['fold', 'f'],
716 720 _('use commit, but combine it with the one above'))
717 721 class fold(histeditaction):
718 722 def verify(self, prev, expected, seen):
719 723 """ Verifies semantic correctness of the fold rule"""
720 724 super(fold, self).verify(prev, expected, seen)
721 725 repo = self.repo
722 726 if not prev:
723 727 c = repo[self.node].parents()[0]
724 728 elif not prev.verb in ('pick', 'base'):
725 729 return
726 730 else:
727 731 c = repo[prev.node]
728 732 if not c.mutable():
729 733 raise error.ParseError(
730 734 _("cannot fold into public change %s") % node.short(c.node()))
731 735
732 736
733 737 def continuedirty(self):
734 738 repo = self.repo
735 739 rulectx = repo[self.node]
736 740
737 741 commit = commitfuncfor(repo, rulectx)
738 742 commit(text='fold-temp-revision %s' % node.short(self.node),
739 743 user=rulectx.user(), date=rulectx.date(),
740 744 extra=rulectx.extra())
741 745
742 746 def continueclean(self):
743 747 repo = self.repo
744 748 ctx = repo['.']
745 749 rulectx = repo[self.node]
746 750 parentctxnode = self.state.parentctxnode
747 751 if ctx.node() == parentctxnode:
748 752 repo.ui.warn(_('%s: empty changeset\n') %
749 753 node.short(self.node))
750 754 return ctx, [(self.node, (parentctxnode,))]
751 755
752 756 parentctx = repo[parentctxnode]
753 757 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
754 758 parentctx.rev(),
755 759 parentctx.rev()))
756 760 if not newcommits:
757 761 repo.ui.warn(_('%s: cannot fold - working copy is not a '
758 762 'descendant of previous commit %s\n') %
759 763 (node.short(self.node), node.short(parentctxnode)))
760 764 return ctx, [(self.node, (ctx.node(),))]
761 765
762 766 middlecommits = newcommits.copy()
763 767 middlecommits.discard(ctx.node())
764 768
765 769 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
766 770 middlecommits)
767 771
768 772 def skipprompt(self):
769 773 """Returns true if the rule should skip the message editor.
770 774
771 775 For example, 'fold' wants to show an editor, but 'rollup'
772 776 doesn't want to.
773 777 """
774 778 return False
775 779
776 780 def mergedescs(self):
777 781 """Returns true if the rule should merge messages of multiple changes.
778 782
779 783 This exists mainly so that 'rollup' rules can be a subclass of
780 784 'fold'.
781 785 """
782 786 return True
783 787
784 788 def firstdate(self):
785 789 """Returns true if the rule should preserve the date of the first
786 790 change.
787 791
788 792 This exists mainly so that 'rollup' rules can be a subclass of
789 793 'fold'.
790 794 """
791 795 return False
792 796
793 797 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
794 798 parent = ctx.parents()[0].node()
795 799 hg.updaterepo(repo, parent, overwrite=False)
796 800 ### prepare new commit data
797 801 commitopts = {}
798 802 commitopts['user'] = ctx.user()
799 803 # commit message
800 804 if not self.mergedescs():
801 805 newmessage = ctx.description()
802 806 else:
803 807 newmessage = '\n***\n'.join(
804 808 [ctx.description()] +
805 809 [repo[r].description() for r in internalchanges] +
806 810 [oldctx.description()]) + '\n'
807 811 commitopts['message'] = newmessage
808 812 # date
809 813 if self.firstdate():
810 814 commitopts['date'] = ctx.date()
811 815 else:
812 816 commitopts['date'] = max(ctx.date(), oldctx.date())
817 # if date is to be updated to current
818 if ui.configbool('rewrite', 'update-timestamp'):
819 commitopts['date'] = dateutil.makedate()
820
813 821 extra = ctx.extra().copy()
814 822 # histedit_source
815 823 # note: ctx is likely a temporary commit but that the best we can do
816 824 # here. This is sufficient to solve issue3681 anyway.
817 825 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
818 826 commitopts['extra'] = extra
819 827 phasemin = max(ctx.phase(), oldctx.phase())
820 828 overrides = {('phases', 'new-commit'): phasemin}
821 829 with repo.ui.configoverride(overrides, 'histedit'):
822 830 n = collapse(repo, ctx, repo[newnode], commitopts,
823 831 skipprompt=self.skipprompt())
824 832 if n is None:
825 833 return ctx, []
826 834 hg.updaterepo(repo, n, overwrite=False)
827 835 replacements = [(oldctx.node(), (newnode,)),
828 836 (ctx.node(), (n,)),
829 837 (newnode, (n,)),
830 838 ]
831 839 for ich in internalchanges:
832 840 replacements.append((ich, (n,)))
833 841 return repo[n], replacements
834 842
835 843 @action(['base', 'b'],
836 844 _('checkout changeset and apply further changesets from there'))
837 845 class base(histeditaction):
838 846
839 847 def run(self):
840 848 if self.repo['.'].node() != self.node:
841 849 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
842 850 return self.continueclean()
843 851
844 852 def continuedirty(self):
845 853 abortdirty()
846 854
847 855 def continueclean(self):
848 856 basectx = self.repo['.']
849 857 return basectx, []
850 858
851 859 def _verifynodeconstraints(self, prev, expected, seen):
852 860 # base can only be use with a node not in the edited set
853 861 if self.node in expected:
854 862 msg = _('%s "%s" changeset was an edited list candidate')
855 863 raise error.ParseError(
856 864 msg % (self.verb, node.short(self.node)),
857 865 hint=_('base must only use unlisted changesets'))
858 866
859 867 @action(['_multifold'],
860 868 _(
861 869 """fold subclass used for when multiple folds happen in a row
862 870
863 871 We only want to fire the editor for the folded message once when
864 872 (say) four changes are folded down into a single change. This is
865 873 similar to rollup, but we should preserve both messages so that
866 874 when the last fold operation runs we can show the user all the
867 875 commit messages in their editor.
868 876 """),
869 877 internal=True)
870 878 class _multifold(fold):
871 879 def skipprompt(self):
872 880 return True
873 881
874 882 @action(["roll", "r"],
875 883 _("like fold, but discard this commit's description and date"))
876 884 class rollup(fold):
877 885 def mergedescs(self):
878 886 return False
879 887
880 888 def skipprompt(self):
881 889 return True
882 890
883 891 def firstdate(self):
884 892 return True
885 893
886 894 @action(["drop", "d"],
887 895 _('remove commit from history'))
888 896 class drop(histeditaction):
889 897 def run(self):
890 898 parentctx = self.repo[self.state.parentctxnode]
891 899 return parentctx, [(self.node, tuple())]
892 900
893 901 @action(["mess", "m"],
894 902 _('edit commit message without changing commit content'),
895 903 priority=True)
896 904 class message(histeditaction):
897 905 def commiteditor(self):
898 906 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
899 907
900 908 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
901 909 """utility function to find the first outgoing changeset
902 910
903 911 Used by initialization code"""
904 912 if opts is None:
905 913 opts = {}
906 914 dest = ui.expandpath(remote or 'default-push', remote or 'default')
907 915 dest, branches = hg.parseurl(dest, None)[:2]
908 916 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
909 917
910 918 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
911 919 other = hg.peer(repo, opts, dest)
912 920
913 921 if revs:
914 922 revs = [repo.lookup(rev) for rev in revs]
915 923
916 924 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
917 925 if not outgoing.missing:
918 926 raise error.Abort(_('no outgoing ancestors'))
919 927 roots = list(repo.revs("roots(%ln)", outgoing.missing))
920 928 if len(roots) > 1:
921 929 msg = _('there are ambiguous outgoing revisions')
922 930 hint = _("see 'hg help histedit' for more detail")
923 931 raise error.Abort(msg, hint=hint)
924 932 return repo[roots[0]].node()
925 933
926 934 # Curses Support
927 935 try:
928 936 import curses
929 937 except ImportError:
930 938 curses = None
931 939
932 940 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
933 941 ACTION_LABELS = {
934 942 'fold': '^fold',
935 943 'roll': '^roll',
936 944 }
937 945
938 946 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN = 1, 2, 3, 4
939 947
940 948 E_QUIT, E_HISTEDIT = 1, 2
941 949 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
942 950 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
943 951
944 952 KEYTABLE = {
945 953 'global': {
946 954 'h': 'next-action',
947 955 'KEY_RIGHT': 'next-action',
948 956 'l': 'prev-action',
949 957 'KEY_LEFT': 'prev-action',
950 958 'q': 'quit',
951 959 'c': 'histedit',
952 960 'C': 'histedit',
953 961 'v': 'showpatch',
954 962 '?': 'help',
955 963 },
956 964 MODE_RULES: {
957 965 'd': 'action-drop',
958 966 'e': 'action-edit',
959 967 'f': 'action-fold',
960 968 'm': 'action-mess',
961 969 'p': 'action-pick',
962 970 'r': 'action-roll',
963 971 ' ': 'select',
964 972 'j': 'down',
965 973 'k': 'up',
966 974 'KEY_DOWN': 'down',
967 975 'KEY_UP': 'up',
968 976 'J': 'move-down',
969 977 'K': 'move-up',
970 978 'KEY_NPAGE': 'move-down',
971 979 'KEY_PPAGE': 'move-up',
972 980 '0': 'goto', # Used for 0..9
973 981 },
974 982 MODE_PATCH: {
975 983 ' ': 'page-down',
976 984 'KEY_NPAGE': 'page-down',
977 985 'KEY_PPAGE': 'page-up',
978 986 'j': 'line-down',
979 987 'k': 'line-up',
980 988 'KEY_DOWN': 'line-down',
981 989 'KEY_UP': 'line-up',
982 990 'J': 'down',
983 991 'K': 'up',
984 992 },
985 993 MODE_HELP: {
986 994 },
987 995 }
988 996
989 997 def screen_size():
990 998 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' '))
991 999
992 1000 class histeditrule(object):
993 1001 def __init__(self, ctx, pos, action='pick'):
994 1002 self.ctx = ctx
995 1003 self.action = action
996 1004 self.origpos = pos
997 1005 self.pos = pos
998 1006 self.conflicts = []
999 1007
1000 1008 def __str__(self):
1001 1009 # Some actions ('fold' and 'roll') combine a patch with a previous one.
1002 1010 # Add a marker showing which patch they apply to, and also omit the
1003 1011 # description for 'roll' (since it will get discarded). Example display:
1004 1012 #
1005 1013 # #10 pick 316392:06a16c25c053 add option to skip tests
1006 1014 # #11 ^roll 316393:71313c964cc5
1007 1015 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1008 1016 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1009 1017 #
1010 1018 # The carets point to the changeset being folded into ("roll this
1011 1019 # changeset into the changeset above").
1012 1020 action = ACTION_LABELS.get(self.action, self.action)
1013 1021 h = self.ctx.hex()[0:12]
1014 1022 r = self.ctx.rev()
1015 1023 desc = self.ctx.description().splitlines()[0].strip()
1016 1024 if self.action == 'roll':
1017 1025 desc = ''
1018 1026 return "#{0:<2} {1:<6} {2}:{3} {4}".format(
1019 1027 self.origpos, action, r, h, desc)
1020 1028
1021 1029 def checkconflicts(self, other):
1022 1030 if other.pos > self.pos and other.origpos <= self.origpos:
1023 1031 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1024 1032 self.conflicts.append(other)
1025 1033 return self.conflicts
1026 1034
1027 1035 if other in self.conflicts:
1028 1036 self.conflicts.remove(other)
1029 1037 return self.conflicts
1030 1038
1031 1039 # ============ EVENTS ===============
1032 1040 def movecursor(state, oldpos, newpos):
1033 1041 '''Change the rule/changeset that the cursor is pointing to, regardless of
1034 1042 current mode (you can switch between patches from the view patch window).'''
1035 1043 state['pos'] = newpos
1036 1044
1037 1045 mode, _ = state['mode']
1038 1046 if mode == MODE_RULES:
1039 1047 # Scroll through the list by updating the view for MODE_RULES, so that
1040 1048 # even if we are not currently viewing the rules, switching back will
1041 1049 # result in the cursor's rule being visible.
1042 1050 modestate = state['modes'][MODE_RULES]
1043 1051 if newpos < modestate['line_offset']:
1044 1052 modestate['line_offset'] = newpos
1045 1053 elif newpos > modestate['line_offset'] + state['page_height'] - 1:
1046 1054 modestate['line_offset'] = newpos - state['page_height'] + 1
1047 1055
1048 1056 # Reset the patch view region to the top of the new patch.
1049 1057 state['modes'][MODE_PATCH]['line_offset'] = 0
1050 1058
1051 1059 def changemode(state, mode):
1052 1060 curmode, _ = state['mode']
1053 1061 state['mode'] = (mode, curmode)
1054 1062
1055 1063 def makeselection(state, pos):
1056 1064 state['selected'] = pos
1057 1065
1058 1066 def swap(state, oldpos, newpos):
1059 1067 """Swap two positions and calculate necessary conflicts in
1060 1068 O(|newpos-oldpos|) time"""
1061 1069
1062 1070 rules = state['rules']
1063 1071 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1064 1072
1065 1073 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1066 1074
1067 1075 # TODO: swap should not know about histeditrule's internals
1068 1076 rules[newpos].pos = newpos
1069 1077 rules[oldpos].pos = oldpos
1070 1078
1071 1079 start = min(oldpos, newpos)
1072 1080 end = max(oldpos, newpos)
1073 1081 for r in pycompat.xrange(start, end + 1):
1074 1082 rules[newpos].checkconflicts(rules[r])
1075 1083 rules[oldpos].checkconflicts(rules[r])
1076 1084
1077 1085 if state['selected']:
1078 1086 makeselection(state, newpos)
1079 1087
1080 1088 def changeaction(state, pos, action):
1081 1089 """Change the action state on the given position to the new action"""
1082 1090 rules = state['rules']
1083 1091 assert 0 <= pos < len(rules)
1084 1092 rules[pos].action = action
1085 1093
1086 1094 def cycleaction(state, pos, next=False):
1087 1095 """Changes the action state the next or the previous action from
1088 1096 the action list"""
1089 1097 rules = state['rules']
1090 1098 assert 0 <= pos < len(rules)
1091 1099 current = rules[pos].action
1092 1100
1093 1101 assert current in KEY_LIST
1094 1102
1095 1103 index = KEY_LIST.index(current)
1096 1104 if next:
1097 1105 index += 1
1098 1106 else:
1099 1107 index -= 1
1100 1108 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1101 1109
1102 1110 def changeview(state, delta, unit):
1103 1111 '''Change the region of whatever is being viewed (a patch or the list of
1104 1112 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1105 1113 mode, _ = state['mode']
1106 1114 if mode != MODE_PATCH:
1107 1115 return
1108 1116 mode_state = state['modes'][mode]
1109 1117 num_lines = len(patchcontents(state))
1110 1118 page_height = state['page_height']
1111 1119 unit = page_height if unit == 'page' else 1
1112 1120 num_pages = 1 + (num_lines - 1) / page_height
1113 1121 max_offset = (num_pages - 1) * page_height
1114 1122 newline = mode_state['line_offset'] + delta * unit
1115 1123 mode_state['line_offset'] = max(0, min(max_offset, newline))
1116 1124
1117 1125 def event(state, ch):
1118 1126 """Change state based on the current character input
1119 1127
1120 1128 This takes the current state and based on the current character input from
1121 1129 the user we change the state.
1122 1130 """
1123 1131 selected = state['selected']
1124 1132 oldpos = state['pos']
1125 1133 rules = state['rules']
1126 1134
1127 1135 if ch in (curses.KEY_RESIZE, "KEY_RESIZE"):
1128 1136 return E_RESIZE
1129 1137
1130 1138 lookup_ch = ch
1131 1139 if '0' <= ch <= '9':
1132 1140 lookup_ch = '0'
1133 1141
1134 1142 curmode, prevmode = state['mode']
1135 1143 action = KEYTABLE[curmode].get(lookup_ch, KEYTABLE['global'].get(lookup_ch))
1136 1144 if action is None:
1137 1145 return
1138 1146 if action in ('down', 'move-down'):
1139 1147 newpos = min(oldpos + 1, len(rules) - 1)
1140 1148 movecursor(state, oldpos, newpos)
1141 1149 if selected is not None or action == 'move-down':
1142 1150 swap(state, oldpos, newpos)
1143 1151 elif action in ('up', 'move-up'):
1144 1152 newpos = max(0, oldpos - 1)
1145 1153 movecursor(state, oldpos, newpos)
1146 1154 if selected is not None or action == 'move-up':
1147 1155 swap(state, oldpos, newpos)
1148 1156 elif action == 'next-action':
1149 1157 cycleaction(state, oldpos, next=True)
1150 1158 elif action == 'prev-action':
1151 1159 cycleaction(state, oldpos, next=False)
1152 1160 elif action == 'select':
1153 1161 selected = oldpos if selected is None else None
1154 1162 makeselection(state, selected)
1155 1163 elif action == 'goto' and int(ch) < len(rules) and len(rules) <= 10:
1156 1164 newrule = next((r for r in rules if r.origpos == int(ch)))
1157 1165 movecursor(state, oldpos, newrule.pos)
1158 1166 if selected is not None:
1159 1167 swap(state, oldpos, newrule.pos)
1160 1168 elif action.startswith('action-'):
1161 1169 changeaction(state, oldpos, action[7:])
1162 1170 elif action == 'showpatch':
1163 1171 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1164 1172 elif action == 'help':
1165 1173 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1166 1174 elif action == 'quit':
1167 1175 return E_QUIT
1168 1176 elif action == 'histedit':
1169 1177 return E_HISTEDIT
1170 1178 elif action == 'page-down':
1171 1179 return E_PAGEDOWN
1172 1180 elif action == 'page-up':
1173 1181 return E_PAGEUP
1174 1182 elif action == 'line-down':
1175 1183 return E_LINEDOWN
1176 1184 elif action == 'line-up':
1177 1185 return E_LINEUP
1178 1186
1179 1187 def makecommands(rules):
1180 1188 """Returns a list of commands consumable by histedit --commands based on
1181 1189 our list of rules"""
1182 1190 commands = []
1183 1191 for rules in rules:
1184 1192 commands.append("{0} {1}\n".format(rules.action, rules.ctx))
1185 1193 return commands
1186 1194
1187 1195 def addln(win, y, x, line, color=None):
1188 1196 """Add a line to the given window left padding but 100% filled with
1189 1197 whitespace characters, so that the color appears on the whole line"""
1190 1198 maxy, maxx = win.getmaxyx()
1191 1199 length = maxx - 1 - x
1192 1200 line = ("{0:<%d}" % length).format(str(line).strip())[:length]
1193 1201 if y < 0:
1194 1202 y = maxy + y
1195 1203 if x < 0:
1196 1204 x = maxx + x
1197 1205 if color:
1198 1206 win.addstr(y, x, line, color)
1199 1207 else:
1200 1208 win.addstr(y, x, line)
1201 1209
1202 1210 def patchcontents(state):
1203 1211 repo = state['repo']
1204 1212 rule = state['rules'][state['pos']]
1205 1213 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1206 1214 'patch': True, 'verbose': True
1207 1215 }, buffered=True)
1208 1216 displayer.show(rule.ctx)
1209 1217 displayer.close()
1210 1218 return displayer.hunk[rule.ctx.rev()].splitlines()
1211 1219
1212 1220 def _chisteditmain(repo, rules, stdscr):
1213 1221 # initialize color pattern
1214 1222 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1215 1223 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1216 1224 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1217 1225 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1218 1226
1219 1227 # don't display the cursor
1220 1228 try:
1221 1229 curses.curs_set(0)
1222 1230 except curses.error:
1223 1231 pass
1224 1232
1225 1233 def rendercommit(win, state):
1226 1234 """Renders the commit window that shows the log of the current selected
1227 1235 commit"""
1228 1236 pos = state['pos']
1229 1237 rules = state['rules']
1230 1238 rule = rules[pos]
1231 1239
1232 1240 ctx = rule.ctx
1233 1241 win.box()
1234 1242
1235 1243 maxy, maxx = win.getmaxyx()
1236 1244 length = maxx - 3
1237 1245
1238 1246 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1239 1247 win.addstr(1, 1, line[:length])
1240 1248
1241 1249 line = "user: {0}".format(stringutil.shortuser(ctx.user()))
1242 1250 win.addstr(2, 1, line[:length])
1243 1251
1244 1252 bms = repo.nodebookmarks(ctx.node())
1245 1253 line = "bookmark: {0}".format(' '.join(bms))
1246 1254 win.addstr(3, 1, line[:length])
1247 1255
1248 1256 line = "files: {0}".format(','.join(ctx.files()))
1249 1257 win.addstr(4, 1, line[:length])
1250 1258
1251 1259 line = "summary: {0}".format(ctx.description().splitlines()[0])
1252 1260 win.addstr(5, 1, line[:length])
1253 1261
1254 1262 conflicts = rule.conflicts
1255 1263 if len(conflicts) > 0:
1256 1264 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1257 1265 conflictstr = "changed files overlap with {0}".format(conflictstr)
1258 1266 else:
1259 1267 conflictstr = 'no overlap'
1260 1268
1261 1269 win.addstr(6, 1, conflictstr[:length])
1262 1270 win.noutrefresh()
1263 1271
1264 1272 def helplines(mode):
1265 1273 if mode == MODE_PATCH:
1266 1274 help = """\
1267 1275 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1268 1276 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1269 1277 """
1270 1278 else:
1271 1279 help = """\
1272 1280 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1273 1281 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1274 1282 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1275 1283 """
1276 1284 return help.splitlines()
1277 1285
1278 1286 def renderhelp(win, state):
1279 1287 maxy, maxx = win.getmaxyx()
1280 1288 mode, _ = state['mode']
1281 1289 for y, line in enumerate(helplines(mode)):
1282 1290 if y >= maxy:
1283 1291 break
1284 1292 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1285 1293 win.noutrefresh()
1286 1294
1287 1295 def renderrules(rulesscr, state):
1288 1296 rules = state['rules']
1289 1297 pos = state['pos']
1290 1298 selected = state['selected']
1291 1299 start = state['modes'][MODE_RULES]['line_offset']
1292 1300
1293 1301 conflicts = [r.ctx for r in rules if r.conflicts]
1294 1302 if len(conflicts) > 0:
1295 1303 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1296 1304 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1297 1305
1298 1306 for y, rule in enumerate(rules[start:]):
1299 1307 if y >= state['page_height']:
1300 1308 break
1301 1309 if len(rule.conflicts) > 0:
1302 1310 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1303 1311 else:
1304 1312 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1305 1313 if y + start == selected:
1306 1314 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1307 1315 elif y + start == pos:
1308 1316 addln(rulesscr, y, 2, rule, curses.A_BOLD)
1309 1317 else:
1310 1318 addln(rulesscr, y, 2, rule)
1311 1319 rulesscr.noutrefresh()
1312 1320
1313 1321 def renderstring(win, state, output):
1314 1322 maxy, maxx = win.getmaxyx()
1315 1323 length = min(maxy - 1, len(output))
1316 1324 for y in range(0, length):
1317 1325 win.addstr(y, 0, output[y])
1318 1326 win.noutrefresh()
1319 1327
1320 1328 def renderpatch(win, state):
1321 1329 start = state['modes'][MODE_PATCH]['line_offset']
1322 1330 renderstring(win, state, patchcontents(state)[start:])
1323 1331
1324 1332 def layout(mode):
1325 1333 maxy, maxx = stdscr.getmaxyx()
1326 1334 helplen = len(helplines(mode))
1327 1335 return {
1328 1336 'commit': (8, maxx),
1329 1337 'help': (helplen, maxx),
1330 1338 'main': (maxy - helplen - 8, maxx),
1331 1339 }
1332 1340
1333 1341 def drawvertwin(size, y, x):
1334 1342 win = curses.newwin(size[0], size[1], y, x)
1335 1343 y += size[0]
1336 1344 return win, y, x
1337 1345
1338 1346 state = {
1339 1347 'pos': 0,
1340 1348 'rules': rules,
1341 1349 'selected': None,
1342 1350 'mode': (MODE_INIT, MODE_INIT),
1343 1351 'page_height': None,
1344 1352 'modes': {
1345 1353 MODE_RULES: {
1346 1354 'line_offset': 0,
1347 1355 },
1348 1356 MODE_PATCH: {
1349 1357 'line_offset': 0,
1350 1358 }
1351 1359 },
1352 1360 'repo': repo,
1353 1361 }
1354 1362
1355 1363 # eventloop
1356 1364 ch = None
1357 1365 stdscr.clear()
1358 1366 stdscr.refresh()
1359 1367 while True:
1360 1368 try:
1361 1369 oldmode, _ = state['mode']
1362 1370 if oldmode == MODE_INIT:
1363 1371 changemode(state, MODE_RULES)
1364 1372 e = event(state, ch)
1365 1373
1366 1374 if e == E_QUIT:
1367 1375 return False
1368 1376 if e == E_HISTEDIT:
1369 1377 return state['rules']
1370 1378 else:
1371 1379 if e == E_RESIZE:
1372 1380 size = screen_size()
1373 1381 if size != stdscr.getmaxyx():
1374 1382 curses.resizeterm(*size)
1375 1383
1376 1384 curmode, _ = state['mode']
1377 1385 sizes = layout(curmode)
1378 1386 if curmode != oldmode:
1379 1387 state['page_height'] = sizes['main'][0]
1380 1388 # Adjust the view to fit the current screen size.
1381 1389 movecursor(state, state['pos'], state['pos'])
1382 1390
1383 1391 # Pack the windows against the top, each pane spread across the
1384 1392 # full width of the screen.
1385 1393 y, x = (0, 0)
1386 1394 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1387 1395 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1388 1396 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1389 1397
1390 1398 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1391 1399 if e == E_PAGEDOWN:
1392 1400 changeview(state, +1, 'page')
1393 1401 elif e == E_PAGEUP:
1394 1402 changeview(state, -1, 'page')
1395 1403 elif e == E_LINEDOWN:
1396 1404 changeview(state, +1, 'line')
1397 1405 elif e == E_LINEUP:
1398 1406 changeview(state, -1, 'line')
1399 1407
1400 1408 # start rendering
1401 1409 commitwin.erase()
1402 1410 helpwin.erase()
1403 1411 mainwin.erase()
1404 1412 if curmode == MODE_PATCH:
1405 1413 renderpatch(mainwin, state)
1406 1414 elif curmode == MODE_HELP:
1407 1415 renderstring(mainwin, state, __doc__.strip().splitlines())
1408 1416 else:
1409 1417 renderrules(mainwin, state)
1410 1418 rendercommit(commitwin, state)
1411 1419 renderhelp(helpwin, state)
1412 1420 curses.doupdate()
1413 1421 # done rendering
1414 1422 ch = stdscr.getkey()
1415 1423 except curses.error:
1416 1424 pass
1417 1425
1418 1426 def _chistedit(ui, repo, *freeargs, **opts):
1419 1427 """interactively edit changeset history via a curses interface
1420 1428
1421 1429 Provides a ncurses interface to histedit. Press ? in chistedit mode
1422 1430 to see an extensive help. Requires python-curses to be installed."""
1423 1431
1424 1432 if curses is None:
1425 1433 raise error.Abort(_("Python curses library required"))
1426 1434
1427 1435 # disable color
1428 1436 ui._colormode = None
1429 1437
1430 1438 try:
1431 1439 keep = opts.get('keep')
1432 1440 revs = opts.get('rev', [])[:]
1433 1441 cmdutil.checkunfinished(repo)
1434 1442 cmdutil.bailifchanged(repo)
1435 1443
1436 1444 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1437 1445 raise error.Abort(_('history edit already in progress, try '
1438 1446 '--continue or --abort'))
1439 1447 revs.extend(freeargs)
1440 1448 if not revs:
1441 1449 defaultrev = destutil.desthistedit(ui, repo)
1442 1450 if defaultrev is not None:
1443 1451 revs.append(defaultrev)
1444 1452 if len(revs) != 1:
1445 1453 raise error.Abort(
1446 1454 _('histedit requires exactly one ancestor revision'))
1447 1455
1448 1456 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1449 1457 if len(rr) != 1:
1450 1458 raise error.Abort(_('The specified revisions must have '
1451 1459 'exactly one common root'))
1452 1460 root = rr[0].node()
1453 1461
1454 1462 topmost, empty = repo.dirstate.parents()
1455 1463 revs = between(repo, root, topmost, keep)
1456 1464 if not revs:
1457 1465 raise error.Abort(_('%s is not an ancestor of working directory') %
1458 1466 node.short(root))
1459 1467
1460 1468 ctxs = []
1461 1469 for i, r in enumerate(revs):
1462 1470 ctxs.append(histeditrule(repo[r], i))
1463 1471 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1464 1472 curses.echo()
1465 1473 curses.endwin()
1466 1474 if rc is False:
1467 1475 ui.write(_("chistedit aborted\n"))
1468 1476 return 0
1469 1477 if type(rc) is list:
1470 1478 ui.status(_("running histedit\n"))
1471 1479 rules = makecommands(rc)
1472 1480 filename = repo.vfs.join('chistedit')
1473 1481 with open(filename, 'w+') as fp:
1474 1482 for r in rules:
1475 1483 fp.write(r)
1476 1484 opts['commands'] = filename
1477 1485 return _texthistedit(ui, repo, *freeargs, **opts)
1478 1486 except KeyboardInterrupt:
1479 1487 pass
1480 1488 return -1
1481 1489
1482 1490 @command('histedit',
1483 1491 [('', 'commands', '',
1484 1492 _('read history edits from the specified file'), _('FILE')),
1485 1493 ('c', 'continue', False, _('continue an edit already in progress')),
1486 1494 ('', 'edit-plan', False, _('edit remaining actions list')),
1487 1495 ('k', 'keep', False,
1488 1496 _("don't strip old nodes after edit is complete")),
1489 1497 ('', 'abort', False, _('abort an edit in progress')),
1490 1498 ('o', 'outgoing', False, _('changesets not found in destination')),
1491 1499 ('f', 'force', False,
1492 1500 _('force outgoing even for unrelated repositories')),
1493 1501 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1494 1502 cmdutil.formatteropts,
1495 1503 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1496 1504 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1497 1505 def histedit(ui, repo, *freeargs, **opts):
1498 1506 """interactively edit changeset history
1499 1507
1500 1508 This command lets you edit a linear series of changesets (up to
1501 1509 and including the working directory, which should be clean).
1502 1510 You can:
1503 1511
1504 1512 - `pick` to [re]order a changeset
1505 1513
1506 1514 - `drop` to omit changeset
1507 1515
1508 1516 - `mess` to reword the changeset commit message
1509 1517
1510 1518 - `fold` to combine it with the preceding changeset (using the later date)
1511 1519
1512 1520 - `roll` like fold, but discarding this commit's description and date
1513 1521
1514 1522 - `edit` to edit this changeset (preserving date)
1515 1523
1516 1524 - `base` to checkout changeset and apply further changesets from there
1517 1525
1518 1526 There are a number of ways to select the root changeset:
1519 1527
1520 1528 - Specify ANCESTOR directly
1521 1529
1522 1530 - Use --outgoing -- it will be the first linear changeset not
1523 1531 included in destination. (See :hg:`help config.paths.default-push`)
1524 1532
1525 1533 - Otherwise, the value from the "histedit.defaultrev" config option
1526 1534 is used as a revset to select the base revision when ANCESTOR is not
1527 1535 specified. The first revision returned by the revset is used. By
1528 1536 default, this selects the editable history that is unique to the
1529 1537 ancestry of the working directory.
1530 1538
1531 1539 .. container:: verbose
1532 1540
1533 1541 If you use --outgoing, this command will abort if there are ambiguous
1534 1542 outgoing revisions. For example, if there are multiple branches
1535 1543 containing outgoing revisions.
1536 1544
1537 1545 Use "min(outgoing() and ::.)" or similar revset specification
1538 1546 instead of --outgoing to specify edit target revision exactly in
1539 1547 such ambiguous situation. See :hg:`help revsets` for detail about
1540 1548 selecting revisions.
1541 1549
1542 1550 .. container:: verbose
1543 1551
1544 1552 Examples:
1545 1553
1546 1554 - A number of changes have been made.
1547 1555 Revision 3 is no longer needed.
1548 1556
1549 1557 Start history editing from revision 3::
1550 1558
1551 1559 hg histedit -r 3
1552 1560
1553 1561 An editor opens, containing the list of revisions,
1554 1562 with specific actions specified::
1555 1563
1556 1564 pick 5339bf82f0ca 3 Zworgle the foobar
1557 1565 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1558 1566 pick 0a9639fcda9d 5 Morgify the cromulancy
1559 1567
1560 1568 Additional information about the possible actions
1561 1569 to take appears below the list of revisions.
1562 1570
1563 1571 To remove revision 3 from the history,
1564 1572 its action (at the beginning of the relevant line)
1565 1573 is changed to 'drop'::
1566 1574
1567 1575 drop 5339bf82f0ca 3 Zworgle the foobar
1568 1576 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1569 1577 pick 0a9639fcda9d 5 Morgify the cromulancy
1570 1578
1571 1579 - A number of changes have been made.
1572 1580 Revision 2 and 4 need to be swapped.
1573 1581
1574 1582 Start history editing from revision 2::
1575 1583
1576 1584 hg histedit -r 2
1577 1585
1578 1586 An editor opens, containing the list of revisions,
1579 1587 with specific actions specified::
1580 1588
1581 1589 pick 252a1af424ad 2 Blorb a morgwazzle
1582 1590 pick 5339bf82f0ca 3 Zworgle the foobar
1583 1591 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1584 1592
1585 1593 To swap revision 2 and 4, its lines are swapped
1586 1594 in the editor::
1587 1595
1588 1596 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1589 1597 pick 5339bf82f0ca 3 Zworgle the foobar
1590 1598 pick 252a1af424ad 2 Blorb a morgwazzle
1591 1599
1592 1600 Returns 0 on success, 1 if user intervention is required (not only
1593 1601 for intentional "edit" command, but also for resolving unexpected
1594 1602 conflicts).
1595 1603 """
1596 1604 # kludge: _chistedit only works for starting an edit, not aborting
1597 1605 # or continuing, so fall back to regular _texthistedit for those
1598 1606 # operations.
1599 1607 if ui.interface('histedit') == 'curses' and _getgoal(opts) == goalnew:
1600 1608 return _chistedit(ui, repo, *freeargs, **opts)
1601 1609 return _texthistedit(ui, repo, *freeargs, **opts)
1602 1610
1603 1611 def _texthistedit(ui, repo, *freeargs, **opts):
1604 1612 state = histeditstate(repo)
1605 1613 with repo.wlock() as wlock, repo.lock() as lock:
1606 1614 state.wlock = wlock
1607 1615 state.lock = lock
1608 1616 _histedit(ui, repo, state, *freeargs, **opts)
1609 1617
1610 1618 goalcontinue = 'continue'
1611 1619 goalabort = 'abort'
1612 1620 goaleditplan = 'edit-plan'
1613 1621 goalnew = 'new'
1614 1622
1615 1623 def _getgoal(opts):
1616 1624 if opts.get('continue'):
1617 1625 return goalcontinue
1618 1626 if opts.get('abort'):
1619 1627 return goalabort
1620 1628 if opts.get('edit_plan'):
1621 1629 return goaleditplan
1622 1630 return goalnew
1623 1631
1624 1632 def _readfile(ui, path):
1625 1633 if path == '-':
1626 1634 with ui.timeblockedsection('histedit'):
1627 1635 return ui.fin.read()
1628 1636 else:
1629 1637 with open(path, 'rb') as f:
1630 1638 return f.read()
1631 1639
1632 1640 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1633 1641 # TODO only abort if we try to histedit mq patches, not just
1634 1642 # blanket if mq patches are applied somewhere
1635 1643 mq = getattr(repo, 'mq', None)
1636 1644 if mq and mq.applied:
1637 1645 raise error.Abort(_('source has mq patches applied'))
1638 1646
1639 1647 # basic argument incompatibility processing
1640 1648 outg = opts.get('outgoing')
1641 1649 editplan = opts.get('edit_plan')
1642 1650 abort = opts.get('abort')
1643 1651 force = opts.get('force')
1644 1652 if force and not outg:
1645 1653 raise error.Abort(_('--force only allowed with --outgoing'))
1646 1654 if goal == 'continue':
1647 1655 if any((outg, abort, revs, freeargs, rules, editplan)):
1648 1656 raise error.Abort(_('no arguments allowed with --continue'))
1649 1657 elif goal == 'abort':
1650 1658 if any((outg, revs, freeargs, rules, editplan)):
1651 1659 raise error.Abort(_('no arguments allowed with --abort'))
1652 1660 elif goal == 'edit-plan':
1653 1661 if any((outg, revs, freeargs)):
1654 1662 raise error.Abort(_('only --commands argument allowed with '
1655 1663 '--edit-plan'))
1656 1664 else:
1657 1665 if state.inprogress():
1658 1666 raise error.Abort(_('history edit already in progress, try '
1659 1667 '--continue or --abort'))
1660 1668 if outg:
1661 1669 if revs:
1662 1670 raise error.Abort(_('no revisions allowed with --outgoing'))
1663 1671 if len(freeargs) > 1:
1664 1672 raise error.Abort(
1665 1673 _('only one repo argument allowed with --outgoing'))
1666 1674 else:
1667 1675 revs.extend(freeargs)
1668 1676 if len(revs) == 0:
1669 1677 defaultrev = destutil.desthistedit(ui, repo)
1670 1678 if defaultrev is not None:
1671 1679 revs.append(defaultrev)
1672 1680
1673 1681 if len(revs) != 1:
1674 1682 raise error.Abort(
1675 1683 _('histedit requires exactly one ancestor revision'))
1676 1684
1677 1685 def _histedit(ui, repo, state, *freeargs, **opts):
1678 1686 opts = pycompat.byteskwargs(opts)
1679 1687 fm = ui.formatter('histedit', opts)
1680 1688 fm.startitem()
1681 1689 goal = _getgoal(opts)
1682 1690 revs = opts.get('rev', [])
1683 1691 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1684 1692 rules = opts.get('commands', '')
1685 1693 state.keep = opts.get('keep', False)
1686 1694
1687 1695 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1688 1696
1689 1697 hastags = False
1690 1698 if revs:
1691 1699 revs = scmutil.revrange(repo, revs)
1692 1700 ctxs = [repo[rev] for rev in revs]
1693 1701 for ctx in ctxs:
1694 1702 tags = [tag for tag in ctx.tags() if tag != 'tip']
1695 1703 if not hastags:
1696 1704 hastags = len(tags)
1697 1705 if hastags:
1698 1706 if ui.promptchoice(_('warning: tags associated with the given'
1699 1707 ' changeset will be lost after histedit.\n'
1700 1708 'do you want to continue (yN)? $$ &Yes $$ &No'),
1701 1709 default=1):
1702 1710 raise error.Abort(_('histedit cancelled\n'))
1703 1711 # rebuild state
1704 1712 if goal == goalcontinue:
1705 1713 state.read()
1706 1714 state = bootstrapcontinue(ui, state, opts)
1707 1715 elif goal == goaleditplan:
1708 1716 _edithisteditplan(ui, repo, state, rules)
1709 1717 return
1710 1718 elif goal == goalabort:
1711 1719 _aborthistedit(ui, repo, state, nobackup=nobackup)
1712 1720 return
1713 1721 else:
1714 1722 # goal == goalnew
1715 1723 _newhistedit(ui, repo, state, revs, freeargs, opts)
1716 1724
1717 1725 _continuehistedit(ui, repo, state)
1718 1726 _finishhistedit(ui, repo, state, fm)
1719 1727 fm.end()
1720 1728
1721 1729 def _continuehistedit(ui, repo, state):
1722 1730 """This function runs after either:
1723 1731 - bootstrapcontinue (if the goal is 'continue')
1724 1732 - _newhistedit (if the goal is 'new')
1725 1733 """
1726 1734 # preprocess rules so that we can hide inner folds from the user
1727 1735 # and only show one editor
1728 1736 actions = state.actions[:]
1729 1737 for idx, (action, nextact) in enumerate(
1730 1738 zip(actions, actions[1:] + [None])):
1731 1739 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1732 1740 state.actions[idx].__class__ = _multifold
1733 1741
1734 1742 # Force an initial state file write, so the user can run --abort/continue
1735 1743 # even if there's an exception before the first transaction serialize.
1736 1744 state.write()
1737 1745
1738 1746 tr = None
1739 1747 # Don't use singletransaction by default since it rolls the entire
1740 1748 # transaction back if an unexpected exception happens (like a
1741 1749 # pretxncommit hook throws, or the user aborts the commit msg editor).
1742 1750 if ui.configbool("histedit", "singletransaction"):
1743 1751 # Don't use a 'with' for the transaction, since actions may close
1744 1752 # and reopen a transaction. For example, if the action executes an
1745 1753 # external process it may choose to commit the transaction first.
1746 1754 tr = repo.transaction('histedit')
1747 1755 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1748 1756 total=len(state.actions))
1749 1757 with progress, util.acceptintervention(tr):
1750 1758 while state.actions:
1751 1759 state.write(tr=tr)
1752 1760 actobj = state.actions[0]
1753 1761 progress.increment(item=actobj.torule())
1754 1762 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1755 1763 actobj.torule()))
1756 1764 parentctx, replacement_ = actobj.run()
1757 1765 state.parentctxnode = parentctx.node()
1758 1766 state.replacements.extend(replacement_)
1759 1767 state.actions.pop(0)
1760 1768
1761 1769 state.write()
1762 1770
1763 1771 def _finishhistedit(ui, repo, state, fm):
1764 1772 """This action runs when histedit is finishing its session"""
1765 1773 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1766 1774
1767 1775 mapping, tmpnodes, created, ntm = processreplacement(state)
1768 1776 if mapping:
1769 1777 for prec, succs in mapping.iteritems():
1770 1778 if not succs:
1771 1779 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1772 1780 else:
1773 1781 ui.debug('histedit: %s is replaced by %s\n' % (
1774 1782 node.short(prec), node.short(succs[0])))
1775 1783 if len(succs) > 1:
1776 1784 m = 'histedit: %s'
1777 1785 for n in succs[1:]:
1778 1786 ui.debug(m % node.short(n))
1779 1787
1780 1788 if not state.keep:
1781 1789 if mapping:
1782 1790 movetopmostbookmarks(repo, state.topmost, ntm)
1783 1791 # TODO update mq state
1784 1792 else:
1785 1793 mapping = {}
1786 1794
1787 1795 for n in tmpnodes:
1788 1796 if n in repo:
1789 1797 mapping[n] = ()
1790 1798
1791 1799 # remove entries about unknown nodes
1792 1800 nodemap = repo.unfiltered().changelog.nodemap
1793 1801 mapping = {k: v for k, v in mapping.items()
1794 1802 if k in nodemap and all(n in nodemap for n in v)}
1795 1803 scmutil.cleanupnodes(repo, mapping, 'histedit')
1796 1804 hf = fm.hexfunc
1797 1805 fl = fm.formatlist
1798 1806 fd = fm.formatdict
1799 1807 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1800 1808 for oldn, newn in mapping.iteritems()},
1801 1809 key="oldnode", value="newnodes")
1802 1810 fm.data(nodechanges=nodechanges)
1803 1811
1804 1812 state.clear()
1805 1813 if os.path.exists(repo.sjoin('undo')):
1806 1814 os.unlink(repo.sjoin('undo'))
1807 1815 if repo.vfs.exists('histedit-last-edit.txt'):
1808 1816 repo.vfs.unlink('histedit-last-edit.txt')
1809 1817
1810 1818 def _aborthistedit(ui, repo, state, nobackup=False):
1811 1819 try:
1812 1820 state.read()
1813 1821 __, leafs, tmpnodes, __ = processreplacement(state)
1814 1822 ui.debug('restore wc to old parent %s\n'
1815 1823 % node.short(state.topmost))
1816 1824
1817 1825 # Recover our old commits if necessary
1818 1826 if not state.topmost in repo and state.backupfile:
1819 1827 backupfile = repo.vfs.join(state.backupfile)
1820 1828 f = hg.openpath(ui, backupfile)
1821 1829 gen = exchange.readbundle(ui, f, backupfile)
1822 1830 with repo.transaction('histedit.abort') as tr:
1823 1831 bundle2.applybundle(repo, gen, tr, source='histedit',
1824 1832 url='bundle:' + backupfile)
1825 1833
1826 1834 os.remove(backupfile)
1827 1835
1828 1836 # check whether we should update away
1829 1837 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1830 1838 state.parentctxnode, leafs | tmpnodes):
1831 1839 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1832 1840 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1833 1841 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1834 1842 except Exception:
1835 1843 if state.inprogress():
1836 1844 ui.warn(_('warning: encountered an exception during histedit '
1837 1845 '--abort; the repository may not have been completely '
1838 1846 'cleaned up\n'))
1839 1847 raise
1840 1848 finally:
1841 1849 state.clear()
1842 1850
1843 1851 def _edithisteditplan(ui, repo, state, rules):
1844 1852 state.read()
1845 1853 if not rules:
1846 1854 comment = geteditcomment(ui,
1847 1855 node.short(state.parentctxnode),
1848 1856 node.short(state.topmost))
1849 1857 rules = ruleeditor(repo, ui, state.actions, comment)
1850 1858 else:
1851 1859 rules = _readfile(ui, rules)
1852 1860 actions = parserules(rules, state)
1853 1861 ctxs = [repo[act.node] \
1854 1862 for act in state.actions if act.node]
1855 1863 warnverifyactions(ui, repo, actions, state, ctxs)
1856 1864 state.actions = actions
1857 1865 state.write()
1858 1866
1859 1867 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1860 1868 outg = opts.get('outgoing')
1861 1869 rules = opts.get('commands', '')
1862 1870 force = opts.get('force')
1863 1871
1864 1872 cmdutil.checkunfinished(repo)
1865 1873 cmdutil.bailifchanged(repo)
1866 1874
1867 1875 topmost, empty = repo.dirstate.parents()
1868 1876 if outg:
1869 1877 if freeargs:
1870 1878 remote = freeargs[0]
1871 1879 else:
1872 1880 remote = None
1873 1881 root = findoutgoing(ui, repo, remote, force, opts)
1874 1882 else:
1875 1883 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1876 1884 if len(rr) != 1:
1877 1885 raise error.Abort(_('The specified revisions must have '
1878 1886 'exactly one common root'))
1879 1887 root = rr[0].node()
1880 1888
1881 1889 revs = between(repo, root, topmost, state.keep)
1882 1890 if not revs:
1883 1891 raise error.Abort(_('%s is not an ancestor of working directory') %
1884 1892 node.short(root))
1885 1893
1886 1894 ctxs = [repo[r] for r in revs]
1887 1895 if not rules:
1888 1896 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1889 1897 actions = [pick(state, r) for r in revs]
1890 1898 rules = ruleeditor(repo, ui, actions, comment)
1891 1899 else:
1892 1900 rules = _readfile(ui, rules)
1893 1901 actions = parserules(rules, state)
1894 1902 warnverifyactions(ui, repo, actions, state, ctxs)
1895 1903
1896 1904 parentctxnode = repo[root].parents()[0].node()
1897 1905
1898 1906 state.parentctxnode = parentctxnode
1899 1907 state.actions = actions
1900 1908 state.topmost = topmost
1901 1909 state.replacements = []
1902 1910
1903 1911 ui.log("histedit", "%d actions to histedit\n", len(actions),
1904 1912 histedit_num_actions=len(actions))
1905 1913
1906 1914 # Create a backup so we can always abort completely.
1907 1915 backupfile = None
1908 1916 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1909 1917 backupfile = repair.backupbundle(repo, [parentctxnode],
1910 1918 [topmost], root, 'histedit')
1911 1919 state.backupfile = backupfile
1912 1920
1913 1921 def _getsummary(ctx):
1914 1922 # a common pattern is to extract the summary but default to the empty
1915 1923 # string
1916 1924 summary = ctx.description() or ''
1917 1925 if summary:
1918 1926 summary = summary.splitlines()[0]
1919 1927 return summary
1920 1928
1921 1929 def bootstrapcontinue(ui, state, opts):
1922 1930 repo = state.repo
1923 1931
1924 1932 ms = mergemod.mergestate.read(repo)
1925 1933 mergeutil.checkunresolved(ms)
1926 1934
1927 1935 if state.actions:
1928 1936 actobj = state.actions.pop(0)
1929 1937
1930 1938 if _isdirtywc(repo):
1931 1939 actobj.continuedirty()
1932 1940 if _isdirtywc(repo):
1933 1941 abortdirty()
1934 1942
1935 1943 parentctx, replacements = actobj.continueclean()
1936 1944
1937 1945 state.parentctxnode = parentctx.node()
1938 1946 state.replacements.extend(replacements)
1939 1947
1940 1948 return state
1941 1949
1942 1950 def between(repo, old, new, keep):
1943 1951 """select and validate the set of revision to edit
1944 1952
1945 1953 When keep is false, the specified set can't have children."""
1946 1954 revs = repo.revs('%n::%n', old, new)
1947 1955 if revs and not keep:
1948 1956 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1949 1957 repo.revs('(%ld::) - (%ld)', revs, revs)):
1950 1958 raise error.Abort(_('can only histedit a changeset together '
1951 1959 'with all its descendants'))
1952 1960 if repo.revs('(%ld) and merge()', revs):
1953 1961 raise error.Abort(_('cannot edit history that contains merges'))
1954 1962 root = repo[revs.first()] # list is already sorted by repo.revs()
1955 1963 if not root.mutable():
1956 1964 raise error.Abort(_('cannot edit public changeset: %s') % root,
1957 1965 hint=_("see 'hg help phases' for details"))
1958 1966 return pycompat.maplist(repo.changelog.node, revs)
1959 1967
1960 1968 def ruleeditor(repo, ui, actions, editcomment=""):
1961 1969 """open an editor to edit rules
1962 1970
1963 1971 rules are in the format [ [act, ctx], ...] like in state.rules
1964 1972 """
1965 1973 if repo.ui.configbool("experimental", "histedit.autoverb"):
1966 1974 newact = util.sortdict()
1967 1975 for act in actions:
1968 1976 ctx = repo[act.node]
1969 1977 summary = _getsummary(ctx)
1970 1978 fword = summary.split(' ', 1)[0].lower()
1971 1979 added = False
1972 1980
1973 1981 # if it doesn't end with the special character '!' just skip this
1974 1982 if fword.endswith('!'):
1975 1983 fword = fword[:-1]
1976 1984 if fword in primaryactions | secondaryactions | tertiaryactions:
1977 1985 act.verb = fword
1978 1986 # get the target summary
1979 1987 tsum = summary[len(fword) + 1:].lstrip()
1980 1988 # safe but slow: reverse iterate over the actions so we
1981 1989 # don't clash on two commits having the same summary
1982 1990 for na, l in reversed(list(newact.iteritems())):
1983 1991 actx = repo[na.node]
1984 1992 asum = _getsummary(actx)
1985 1993 if asum == tsum:
1986 1994 added = True
1987 1995 l.append(act)
1988 1996 break
1989 1997
1990 1998 if not added:
1991 1999 newact[act] = []
1992 2000
1993 2001 # copy over and flatten the new list
1994 2002 actions = []
1995 2003 for na, l in newact.iteritems():
1996 2004 actions.append(na)
1997 2005 actions += l
1998 2006
1999 2007 rules = '\n'.join([act.torule() for act in actions])
2000 2008 rules += '\n\n'
2001 2009 rules += editcomment
2002 2010 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2003 2011 repopath=repo.path, action='histedit')
2004 2012
2005 2013 # Save edit rules in .hg/histedit-last-edit.txt in case
2006 2014 # the user needs to ask for help after something
2007 2015 # surprising happens.
2008 2016 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2009 2017 f.write(rules)
2010 2018
2011 2019 return rules
2012 2020
2013 2021 def parserules(rules, state):
2014 2022 """Read the histedit rules string and return list of action objects """
2015 2023 rules = [l for l in (r.strip() for r in rules.splitlines())
2016 2024 if l and not l.startswith('#')]
2017 2025 actions = []
2018 2026 for r in rules:
2019 2027 if ' ' not in r:
2020 2028 raise error.ParseError(_('malformed line "%s"') % r)
2021 2029 verb, rest = r.split(' ', 1)
2022 2030
2023 2031 if verb not in actiontable:
2024 2032 raise error.ParseError(_('unknown action "%s"') % verb)
2025 2033
2026 2034 action = actiontable[verb].fromrule(state, rest)
2027 2035 actions.append(action)
2028 2036 return actions
2029 2037
2030 2038 def warnverifyactions(ui, repo, actions, state, ctxs):
2031 2039 try:
2032 2040 verifyactions(actions, state, ctxs)
2033 2041 except error.ParseError:
2034 2042 if repo.vfs.exists('histedit-last-edit.txt'):
2035 2043 ui.warn(_('warning: histedit rules saved '
2036 2044 'to: .hg/histedit-last-edit.txt\n'))
2037 2045 raise
2038 2046
2039 2047 def verifyactions(actions, state, ctxs):
2040 2048 """Verify that there exists exactly one action per given changeset and
2041 2049 other constraints.
2042 2050
2043 2051 Will abort if there are to many or too few rules, a malformed rule,
2044 2052 or a rule on a changeset outside of the user-given range.
2045 2053 """
2046 2054 expected = set(c.node() for c in ctxs)
2047 2055 seen = set()
2048 2056 prev = None
2049 2057
2050 2058 if actions and actions[0].verb in ['roll', 'fold']:
2051 2059 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2052 2060 actions[0].verb)
2053 2061
2054 2062 for action in actions:
2055 2063 action.verify(prev, expected, seen)
2056 2064 prev = action
2057 2065 if action.node is not None:
2058 2066 seen.add(action.node)
2059 2067 missing = sorted(expected - seen) # sort to stabilize output
2060 2068
2061 2069 if state.repo.ui.configbool('histedit', 'dropmissing'):
2062 2070 if len(actions) == 0:
2063 2071 raise error.ParseError(_('no rules provided'),
2064 2072 hint=_('use strip extension to remove commits'))
2065 2073
2066 2074 drops = [drop(state, n) for n in missing]
2067 2075 # put the in the beginning so they execute immediately and
2068 2076 # don't show in the edit-plan in the future
2069 2077 actions[:0] = drops
2070 2078 elif missing:
2071 2079 raise error.ParseError(_('missing rules for changeset %s') %
2072 2080 node.short(missing[0]),
2073 2081 hint=_('use "drop %s" to discard, see also: '
2074 2082 "'hg help -e histedit.config'")
2075 2083 % node.short(missing[0]))
2076 2084
2077 2085 def adjustreplacementsfrommarkers(repo, oldreplacements):
2078 2086 """Adjust replacements from obsolescence markers
2079 2087
2080 2088 Replacements structure is originally generated based on
2081 2089 histedit's state and does not account for changes that are
2082 2090 not recorded there. This function fixes that by adding
2083 2091 data read from obsolescence markers"""
2084 2092 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2085 2093 return oldreplacements
2086 2094
2087 2095 unfi = repo.unfiltered()
2088 2096 nm = unfi.changelog.nodemap
2089 2097 obsstore = repo.obsstore
2090 2098 newreplacements = list(oldreplacements)
2091 2099 oldsuccs = [r[1] for r in oldreplacements]
2092 2100 # successors that have already been added to succstocheck once
2093 2101 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2094 2102 succstocheck = list(seensuccs)
2095 2103 while succstocheck:
2096 2104 n = succstocheck.pop()
2097 2105 missing = nm.get(n) is None
2098 2106 markers = obsstore.successors.get(n, ())
2099 2107 if missing and not markers:
2100 2108 # dead end, mark it as such
2101 2109 newreplacements.append((n, ()))
2102 2110 for marker in markers:
2103 2111 nsuccs = marker[1]
2104 2112 newreplacements.append((n, nsuccs))
2105 2113 for nsucc in nsuccs:
2106 2114 if nsucc not in seensuccs:
2107 2115 seensuccs.add(nsucc)
2108 2116 succstocheck.append(nsucc)
2109 2117
2110 2118 return newreplacements
2111 2119
2112 2120 def processreplacement(state):
2113 2121 """process the list of replacements to return
2114 2122
2115 2123 1) the final mapping between original and created nodes
2116 2124 2) the list of temporary node created by histedit
2117 2125 3) the list of new commit created by histedit"""
2118 2126 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2119 2127 allsuccs = set()
2120 2128 replaced = set()
2121 2129 fullmapping = {}
2122 2130 # initialize basic set
2123 2131 # fullmapping records all operations recorded in replacement
2124 2132 for rep in replacements:
2125 2133 allsuccs.update(rep[1])
2126 2134 replaced.add(rep[0])
2127 2135 fullmapping.setdefault(rep[0], set()).update(rep[1])
2128 2136 new = allsuccs - replaced
2129 2137 tmpnodes = allsuccs & replaced
2130 2138 # Reduce content fullmapping into direct relation between original nodes
2131 2139 # and final node created during history edition
2132 2140 # Dropped changeset are replaced by an empty list
2133 2141 toproceed = set(fullmapping)
2134 2142 final = {}
2135 2143 while toproceed:
2136 2144 for x in list(toproceed):
2137 2145 succs = fullmapping[x]
2138 2146 for s in list(succs):
2139 2147 if s in toproceed:
2140 2148 # non final node with unknown closure
2141 2149 # We can't process this now
2142 2150 break
2143 2151 elif s in final:
2144 2152 # non final node, replace with closure
2145 2153 succs.remove(s)
2146 2154 succs.update(final[s])
2147 2155 else:
2148 2156 final[x] = succs
2149 2157 toproceed.remove(x)
2150 2158 # remove tmpnodes from final mapping
2151 2159 for n in tmpnodes:
2152 2160 del final[n]
2153 2161 # we expect all changes involved in final to exist in the repo
2154 2162 # turn `final` into list (topologically sorted)
2155 2163 nm = state.repo.changelog.nodemap
2156 2164 for prec, succs in final.items():
2157 2165 final[prec] = sorted(succs, key=nm.get)
2158 2166
2159 2167 # computed topmost element (necessary for bookmark)
2160 2168 if new:
2161 2169 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2162 2170 elif not final:
2163 2171 # Nothing rewritten at all. we won't need `newtopmost`
2164 2172 # It is the same as `oldtopmost` and `processreplacement` know it
2165 2173 newtopmost = None
2166 2174 else:
2167 2175 # every body died. The newtopmost is the parent of the root.
2168 2176 r = state.repo.changelog.rev
2169 2177 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2170 2178
2171 2179 return final, tmpnodes, new, newtopmost
2172 2180
2173 2181 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2174 2182 """Move bookmark from oldtopmost to newly created topmost
2175 2183
2176 2184 This is arguably a feature and we may only want that for the active
2177 2185 bookmark. But the behavior is kept compatible with the old version for now.
2178 2186 """
2179 2187 if not oldtopmost or not newtopmost:
2180 2188 return
2181 2189 oldbmarks = repo.nodebookmarks(oldtopmost)
2182 2190 if oldbmarks:
2183 2191 with repo.lock(), repo.transaction('histedit') as tr:
2184 2192 marks = repo._bookmarks
2185 2193 changes = []
2186 2194 for name in oldbmarks:
2187 2195 changes.append((name, newtopmost))
2188 2196 marks.applychanges(repo, tr, changes)
2189 2197
2190 2198 def cleanupnode(ui, repo, nodes, nobackup=False):
2191 2199 """strip a group of nodes from the repository
2192 2200
2193 2201 The set of node to strip may contains unknown nodes."""
2194 2202 with repo.lock():
2195 2203 # do not let filtering get in the way of the cleanse
2196 2204 # we should probably get rid of obsolescence marker created during the
2197 2205 # histedit, but we currently do not have such information.
2198 2206 repo = repo.unfiltered()
2199 2207 # Find all nodes that need to be stripped
2200 2208 # (we use %lr instead of %ln to silently ignore unknown items)
2201 2209 nm = repo.changelog.nodemap
2202 2210 nodes = sorted(n for n in nodes if n in nm)
2203 2211 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2204 2212 if roots:
2205 2213 backup = not nobackup
2206 2214 repair.strip(ui, repo, roots, backup=backup)
2207 2215
2208 2216 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2209 2217 if isinstance(nodelist, str):
2210 2218 nodelist = [nodelist]
2211 2219 state = histeditstate(repo)
2212 2220 if state.inprogress():
2213 2221 state.read()
2214 2222 histedit_nodes = {action.node for action
2215 2223 in state.actions if action.node}
2216 2224 common_nodes = histedit_nodes & set(nodelist)
2217 2225 if common_nodes:
2218 2226 raise error.Abort(_("histedit in progress, can't strip %s")
2219 2227 % ', '.join(node.short(x) for x in common_nodes))
2220 2228 return orig(ui, repo, nodelist, *args, **kwargs)
2221 2229
2222 2230 extensions.wrapfunction(repair, 'strip', stripwrapper)
2223 2231
2224 2232 def summaryhook(ui, repo):
2225 2233 state = histeditstate(repo)
2226 2234 if not state.inprogress():
2227 2235 return
2228 2236 state.read()
2229 2237 if state.actions:
2230 2238 # i18n: column positioning for "hg summary"
2231 2239 ui.write(_('hist: %s (histedit --continue)\n') %
2232 2240 (ui.label(_('%d remaining'), 'histedit.remaining') %
2233 2241 len(state.actions)))
2234 2242
2235 2243 def extsetup(ui):
2236 2244 cmdutil.summaryhooks.add('histedit', summaryhook)
2237 2245 cmdutil.unfinishedstates.append(
2238 2246 ['histedit-state', False, True, _('histedit in progress'),
2239 2247 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2240 2248 cmdutil.afterresolvedstates.append(
2241 2249 ['histedit-state', _('hg histedit --continue')])
@@ -1,504 +1,555 b''
1 1 $ . "$TESTDIR/histedit-helpers.sh"
2 2
3 3 $ cat >> $HGRCPATH <<EOF
4 4 > [extensions]
5 5 > histedit=
6 6 > strip=
7 > mockmakedate = $TESTDIR/mockmakedate.py
7 8 > EOF
8 9
9 10 $ initrepo ()
10 11 > {
11 12 > hg init r
12 13 > cd r
13 14 > for x in a b c d e f g; do
14 15 > echo $x > $x
15 16 > hg add $x
16 17 > hg ci -m $x
17 18 > done
18 19 > }
19 20
20 21 $ initrepo
21 22
22 23 log before edit
23 24 $ hg log --graph
24 25 @ changeset: 6:3c6a8ed2ebe8
25 26 | tag: tip
26 27 | user: test
27 28 | date: Thu Jan 01 00:00:00 1970 +0000
28 29 | summary: g
29 30 |
30 31 o changeset: 5:652413bf663e
31 32 | user: test
32 33 | date: Thu Jan 01 00:00:00 1970 +0000
33 34 | summary: f
34 35 |
35 36 o changeset: 4:e860deea161a
36 37 | user: test
37 38 | date: Thu Jan 01 00:00:00 1970 +0000
38 39 | summary: e
39 40 |
40 41 o changeset: 3:055a42cdd887
41 42 | user: test
42 43 | date: Thu Jan 01 00:00:00 1970 +0000
43 44 | summary: d
44 45 |
45 46 o changeset: 2:177f92b77385
46 47 | user: test
47 48 | date: Thu Jan 01 00:00:00 1970 +0000
48 49 | summary: c
49 50 |
50 51 o changeset: 1:d2ae7f538514
51 52 | user: test
52 53 | date: Thu Jan 01 00:00:00 1970 +0000
53 54 | summary: b
54 55 |
55 56 o changeset: 0:cb9a9f314b8b
56 57 user: test
57 58 date: Thu Jan 01 00:00:00 1970 +0000
58 59 summary: a
59 60
60 61 dirty a file
61 62 $ echo a > g
62 63 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF
63 64 > EOF
64 65 abort: uncommitted changes
65 66 [255]
66 67 $ echo g > g
67 68
68 69 edit the history
69 70 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
70 71 > pick 177f92b77385 c
71 72 > pick 055a42cdd887 d
72 73 > edit e860deea161a e
73 74 > pick 652413bf663e f
74 75 > pick 3c6a8ed2ebe8 g
75 76 > EOF
76 77 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
77 78 Editing (e860deea161a), you may commit or record as needed now.
78 79 (hg histedit --continue to resume)
79 80
80 81 try to update and get an error
81 82 $ hg update tip
82 83 abort: histedit in progress
83 84 (use 'hg histedit --continue' or 'hg histedit --abort')
84 85 [255]
85 86
86 87 edit the plan via the editor
87 88 $ cat >> $TESTTMP/editplan.sh <<EOF
88 89 > cat > \$1 <<EOF2
89 90 > drop e860deea161a e
90 91 > drop 652413bf663e f
91 92 > drop 3c6a8ed2ebe8 g
92 93 > EOF2
93 94 > EOF
94 95 $ HGEDITOR="sh $TESTTMP/editplan.sh" hg histedit --edit-plan
95 96 $ cat .hg/histedit-state
96 97 v1
97 98 055a42cdd88768532f9cf79daa407fc8d138de9b
98 99 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
99 100 False
100 101 3
101 102 drop
102 103 e860deea161a2f77de56603b340ebbb4536308ae
103 104 drop
104 105 652413bf663ef2a641cab26574e46d5f5a64a55a
105 106 drop
106 107 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
107 108 0
108 109 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
109 110
110 111 edit the plan via --commands
111 112 $ hg histedit --edit-plan --commands - 2>&1 << EOF
112 113 > edit e860deea161a e
113 114 > pick 652413bf663e f
114 115 > drop 3c6a8ed2ebe8 g
115 116 > EOF
116 117 $ cat .hg/histedit-state
117 118 v1
118 119 055a42cdd88768532f9cf79daa407fc8d138de9b
119 120 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
120 121 False
121 122 3
122 123 edit
123 124 e860deea161a2f77de56603b340ebbb4536308ae
124 125 pick
125 126 652413bf663ef2a641cab26574e46d5f5a64a55a
126 127 drop
127 128 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
128 129 0
129 130 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
130 131
131 132 Go at a random point and try to continue
132 133
133 134 $ hg id -n
134 135 3+
135 136 $ hg up 0
136 137 abort: histedit in progress
137 138 (use 'hg histedit --continue' or 'hg histedit --abort')
138 139 [255]
139 140
140 141 Try to delete necessary commit
141 142 $ hg strip -r 652413b
142 143 abort: histedit in progress, can't strip 652413bf663e
143 144 [255]
144 145
145 146 commit, then edit the revision
146 147 $ hg ci -m 'wat'
147 148 created new head
148 149 $ echo a > e
149 150
150 151 qnew should fail while we're in the middle of the edit step
151 152
152 153 $ hg --config extensions.mq= qnew please-fail
153 154 abort: histedit in progress
154 155 (use 'hg histedit --continue' or 'hg histedit --abort')
155 156 [255]
156 157 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
157 158
158 159 $ hg log --graph
159 160 @ changeset: 6:b5f70786f9b0
160 161 | tag: tip
161 162 | user: test
162 163 | date: Thu Jan 01 00:00:00 1970 +0000
163 164 | summary: f
164 165 |
165 166 o changeset: 5:a5e1ba2f7afb
166 167 | user: test
167 168 | date: Thu Jan 01 00:00:00 1970 +0000
168 169 | summary: foobaz
169 170 |
170 171 o changeset: 4:1a60820cd1f6
171 172 | user: test
172 173 | date: Thu Jan 01 00:00:00 1970 +0000
173 174 | summary: wat
174 175 |
175 176 o changeset: 3:055a42cdd887
176 177 | user: test
177 178 | date: Thu Jan 01 00:00:00 1970 +0000
178 179 | summary: d
179 180 |
180 181 o changeset: 2:177f92b77385
181 182 | user: test
182 183 | date: Thu Jan 01 00:00:00 1970 +0000
183 184 | summary: c
184 185 |
185 186 o changeset: 1:d2ae7f538514
186 187 | user: test
187 188 | date: Thu Jan 01 00:00:00 1970 +0000
188 189 | summary: b
189 190 |
190 191 o changeset: 0:cb9a9f314b8b
191 192 user: test
192 193 date: Thu Jan 01 00:00:00 1970 +0000
193 194 summary: a
194 195
195 196
196 197 $ hg cat e
197 198 a
198 199
199 200 Stripping necessary commits should not break --abort
200 201
201 202 $ hg histedit 1a60820cd1f6 --commands - 2>&1 << EOF| fixbundle
202 203 > edit 1a60820cd1f6 wat
203 204 > pick a5e1ba2f7afb foobaz
204 205 > pick b5f70786f9b0 g
205 206 > EOF
206 207 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
207 208 Editing (1a60820cd1f6), you may commit or record as needed now.
208 209 (hg histedit --continue to resume)
209 210
210 211 $ mv .hg/histedit-state .hg/histedit-state.bak
211 212 $ hg strip -q -r b5f70786f9b0
212 213 $ mv .hg/histedit-state.bak .hg/histedit-state
213 214 $ hg histedit --abort
214 215 adding changesets
215 216 adding manifests
216 217 adding file changes
217 218 added 1 changesets with 1 changes to 3 files
218 219 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
219 220 $ hg log -r .
220 221 changeset: 6:b5f70786f9b0
221 222 tag: tip
222 223 user: test
223 224 date: Thu Jan 01 00:00:00 1970 +0000
224 225 summary: f
225 226
226 227
227 228 check histedit_source
228 229
229 230 $ hg log --debug --rev 5
230 231 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
231 232 phase: draft
232 233 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
233 234 parent: -1:0000000000000000000000000000000000000000
234 235 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
235 236 user: test
236 237 date: Thu Jan 01 00:00:00 1970 +0000
237 238 files: e
238 239 extra: branch=default
239 240 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
240 241 description:
241 242 foobaz
242 243
243 244
244 245
245 246 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
246 247 > edit b5f70786f9b0 f
247 248 > EOF
248 249 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
249 250 Editing (b5f70786f9b0), you may commit or record as needed now.
250 251 (hg histedit --continue to resume)
251 252 $ hg status
252 253 A f
253 254
254 255 $ hg summary
255 256 parent: 5:a5e1ba2f7afb
256 257 foobaz
257 258 branch: default
258 259 commit: 1 added (new branch head)
259 260 update: 1 new changesets (update)
260 261 phases: 7 draft
261 262 hist: 1 remaining (histedit --continue)
262 263
263 264 (test also that editor is invoked if histedit is continued for
264 265 "edit" action)
265 266
266 267 $ HGEDITOR='cat' hg histedit --continue
267 268 f
268 269
269 270
270 271 HG: Enter commit message. Lines beginning with 'HG:' are removed.
271 272 HG: Leave message empty to abort commit.
272 273 HG: --
273 274 HG: user: test
274 275 HG: branch 'default'
275 276 HG: added f
276 277 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-histedit.hg
277 278
278 279 $ hg status
279 280
280 281 log after edit
281 282 $ hg log --limit 1
282 283 changeset: 6:a107ee126658
283 284 tag: tip
284 285 user: test
285 286 date: Thu Jan 01 00:00:00 1970 +0000
286 287 summary: f
287 288
288 289
289 290 say we'll change the message, but don't.
290 291 $ cat > ../edit.sh <<EOF
291 292 > cat "\$1" | sed s/pick/mess/ > tmp
292 293 > mv tmp "\$1"
293 294 > EOF
294 295 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
295 296 $ hg status
296 297 $ hg log --limit 1
297 298 changeset: 6:1fd3b2fe7754
298 299 tag: tip
299 300 user: test
300 301 date: Thu Jan 01 00:00:00 1970 +0000
301 302 summary: f
302 303
303 304
304 305 modify the message
305 306
306 307 check saving last-message.txt, at first
307 308
308 309 $ cat > $TESTTMP/commitfailure.py <<EOF
309 310 > from mercurial import error
310 311 > def reposetup(ui, repo):
311 312 > class commitfailure(repo.__class__):
312 313 > def commit(self, *args, **kwargs):
313 314 > raise error.Abort('emulating unexpected abort')
314 315 > repo.__class__ = commitfailure
315 316 > EOF
316 317 $ cat >> .hg/hgrc <<EOF
317 318 > [extensions]
318 319 > # this failure occurs before editor invocation
319 320 > commitfailure = $TESTTMP/commitfailure.py
320 321 > EOF
321 322
322 323 $ cat > $TESTTMP/editor.sh <<EOF
323 324 > echo "==== before editing"
324 325 > cat \$1
325 326 > echo "===="
326 327 > echo "check saving last-message.txt" >> \$1
327 328 > EOF
328 329
329 330 (test that editor is not invoked before transaction starting)
330 331
331 332 $ rm -f .hg/last-message.txt
332 333 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
333 334 > mess 1fd3b2fe7754 f
334 335 > EOF
335 336 abort: emulating unexpected abort
336 337 $ test -f .hg/last-message.txt
337 338 [1]
338 339
339 340 $ cat >> .hg/hgrc <<EOF
340 341 > [extensions]
341 342 > commitfailure = !
342 343 > EOF
343 344 $ hg histedit --abort -q
344 345
345 346 (test that editor is invoked and commit message is saved into
346 347 "last-message.txt")
347 348
348 349 $ cat >> .hg/hgrc <<EOF
349 350 > [hooks]
350 351 > # this failure occurs after editor invocation
351 352 > pretxncommit.unexpectedabort = false
352 353 > EOF
353 354
354 355 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
355 356 A f
356 357
357 358 $ rm -f .hg/last-message.txt
358 359 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
359 360 > mess 1fd3b2fe7754 f
360 361 > EOF
361 362 ==== before editing
362 363 f
363 364
364 365
365 366 HG: Enter commit message. Lines beginning with 'HG:' are removed.
366 367 HG: Leave message empty to abort commit.
367 368 HG: --
368 369 HG: user: test
369 370 HG: branch 'default'
370 371 HG: added f
371 372 ====
372 373 note: commit message saved in .hg/last-message.txt
373 374 transaction abort!
374 375 rollback completed
375 376 abort: pretxncommit.unexpectedabort hook exited with status 1
376 377 [255]
377 378 $ cat .hg/last-message.txt
378 379 f
379 380
380 381
381 382 check saving last-message.txt
382 383
383 384 (test also that editor is invoked if histedit is continued for "message"
384 385 action)
385 386
386 387 $ HGEDITOR=cat hg histedit --continue
387 388 f
388 389
389 390
390 391 HG: Enter commit message. Lines beginning with 'HG:' are removed.
391 392 HG: Leave message empty to abort commit.
392 393 HG: --
393 394 HG: user: test
394 395 HG: branch 'default'
395 396 HG: added f
396 397 note: commit message saved in .hg/last-message.txt
397 398 transaction abort!
398 399 rollback completed
399 400 abort: pretxncommit.unexpectedabort hook exited with status 1
400 401 [255]
401 402
402 403 $ cat >> .hg/hgrc <<EOF
403 404 > [hooks]
404 405 > pretxncommit.unexpectedabort =
405 406 > EOF
406 407 $ hg histedit --abort -q
407 408
408 409 then, check "modify the message" itself
409 410
410 411 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
411 412 > mess 1fd3b2fe7754 f
412 413 > EOF
413 414 $ hg status
414 415 $ hg log --limit 1
415 416 changeset: 6:62feedb1200e
416 417 tag: tip
417 418 user: test
418 419 date: Thu Jan 01 00:00:00 1970 +0000
419 420 summary: f
420 421
421 422
422 423 rollback should not work after a histedit
423 424 $ hg rollback
424 425 no rollback information available
425 426 [1]
426 427
427 428 $ cd ..
428 429 $ hg clone -qr0 r r0
429 430 $ cd r0
430 431 $ hg phase -fdr0
431 432 $ hg histedit --commands - 0 2>&1 << EOF
432 433 > edit cb9a9f314b8b a > $EDITED
433 434 > EOF
434 435 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
435 436 adding a
436 437 Editing (cb9a9f314b8b), you may commit or record as needed now.
437 438 (hg histedit --continue to resume)
438 439 [1]
439 440 $ HGEDITOR=true hg histedit --continue
440 441 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-histedit.hg
441 442
442 443 $ hg log -G
443 444 @ changeset: 0:0efcea34f18a
444 445 tag: tip
445 446 user: test
446 447 date: Thu Jan 01 00:00:00 1970 +0000
447 448 summary: a
448 449
449 450 $ echo foo >> b
450 451 $ hg addr
451 452 adding b
452 453 $ hg ci -m 'add b'
453 454 $ echo foo >> a
454 455 $ hg ci -m 'extend a'
455 456 $ hg phase --public 1
456 457 Attempting to fold a change into a public change should not work:
457 458 $ cat > ../edit.sh <<EOF
458 459 > cat "\$1" | sed s/pick/fold/ > tmp
459 460 > mv tmp "\$1"
460 461 > EOF
461 462 $ HGEDITOR="sh ../edit.sh" hg histedit 2
462 463 warning: histedit rules saved to: .hg/histedit-last-edit.txt
463 464 hg: parse error: first changeset cannot use verb "fold"
464 465 [255]
465 466 $ cat .hg/histedit-last-edit.txt
466 467 fold 0012be4a27ea 2 extend a
467 468
468 469 # Edit history between 0012be4a27ea and 0012be4a27ea
469 470 #
470 471 # Commits are listed from least to most recent
471 472 #
472 473 # You can reorder changesets by reordering the lines
473 474 #
474 475 # Commands:
475 476 #
476 477 # e, edit = use commit, but stop for amending
477 478 # m, mess = edit commit message without changing commit content
478 479 # p, fold = use commit
479 480 # b, base = checkout changeset and apply further changesets from there
480 481 # d, drop = remove commit from history
481 482 # f, fold = use commit, but combine it with the one above
482 483 # r, roll = like fold, but discard this commit's description and date
483 484 #
484 485
485 486 $ cd ..
486 487
488 ============================================
489 Test update-timestamp config option in mess|
490 ============================================
491
492 $ addwithdate ()
493 > {
494 > echo $1 > $1
495 > hg add $1
496 > hg ci -m $1 -d "$2 0"
497 > }
498
499 $ initrepo ()
500 > {
501 > hg init r2
502 > cd r2
503 > addwithdate a 1
504 > addwithdate b 2
505 > addwithdate c 3
506 > addwithdate d 4
507 > addwithdate e 5
508 > addwithdate f 6
509 > }
510
511 $ initrepo
512
513 log before edit
514
515 $ hg log --limit 1
516 changeset: 5:178e35e0ce73
517 tag: tip
518 user: test
519 date: Thu Jan 01 00:00:06 1970 +0000
520 summary: f
521
522 $ hg histedit tip --commands - 2>&1 --config rewrite.update-timestamp=True << EOF | fixbundle
523 > mess 178e35e0ce73 f
524 > EOF
525
526 log after edit
527
528 $ hg log --limit 1
529 changeset: 5:98bf456d476b
530 tag: tip
531 user: test
532 date: Thu Jan 01 00:00:00 1970 +0000
533 summary: f
534
535
536 $ cd ..
537
487 538 warn the user on editing tagged commits
488 539
489 540 $ hg init issue4017
490 541 $ cd issue4017
491 542 $ echo > a
492 543 $ hg ci -Am 'add a'
493 544 adding a
494 545 $ hg tag a
495 546 $ hg tags
496 547 tip 1:bd7ee4f3939b
497 548 a 0:a8a82d372bb3
498 549 $ hg histedit
499 550 warning: tags associated with the given changeset will be lost after histedit.
500 551 do you want to continue (yN)? n
501 552 abort: histedit cancelled
502 553
503 554 [255]
504 555 $ cd ..
@@ -1,599 +1,707 b''
1 1 Test histedit extension: Fold commands
2 2 ======================================
3 3
4 4 This test file is dedicated to testing the fold command in non conflicting
5 5 case.
6 6
7 7 Initialization
8 8 ---------------
9 9
10 10
11 11 $ . "$TESTDIR/histedit-helpers.sh"
12 12
13 13 $ cat >> $HGRCPATH <<EOF
14 14 > [alias]
15 15 > logt = log --template '{rev}:{node|short} {desc|firstline}\n'
16 16 > [extensions]
17 17 > histedit=
18 > mockmakedate = $TESTDIR/mockmakedate.py
18 19 > EOF
19 20
20 21
21 22 Simple folding
22 23 --------------------
23 24 $ addwithdate ()
24 25 > {
25 26 > echo $1 > $1
26 27 > hg add $1
27 28 > hg ci -m $1 -d "$2 0"
28 29 > }
29 30
30 31 $ initrepo ()
31 32 > {
32 33 > hg init r
33 34 > cd r
34 35 > addwithdate a 1
35 36 > addwithdate b 2
36 37 > addwithdate c 3
37 38 > addwithdate d 4
38 39 > addwithdate e 5
39 40 > addwithdate f 6
40 41 > }
41 42
42 43 $ initrepo
43 44
44 45 log before edit
45 46 $ hg logt --graph
46 47 @ 5:178e35e0ce73 f
47 48 |
48 49 o 4:1ddb6c90f2ee e
49 50 |
50 51 o 3:532247a8969b d
51 52 |
52 53 o 2:ff2c9fa2018b c
53 54 |
54 55 o 1:97d72e5f12c7 b
55 56 |
56 57 o 0:8580ff50825a a
57 58
58 59
59 60 $ hg histedit ff2c9fa2018b --commands - 2>&1 <<EOF | fixbundle
60 61 > pick 1ddb6c90f2ee e
61 62 > pick 178e35e0ce73 f
62 63 > fold ff2c9fa2018b c
63 64 > pick 532247a8969b d
64 65 > EOF
65 66
66 67 log after edit
67 68 $ hg logt --graph
68 69 @ 4:c4d7f3def76d d
69 70 |
70 71 o 3:575228819b7e f
71 72 |
72 73 o 2:505a591af19e e
73 74 |
74 75 o 1:97d72e5f12c7 b
75 76 |
76 77 o 0:8580ff50825a a
77 78
78 79
79 80 post-fold manifest
80 81 $ hg manifest
81 82 a
82 83 b
83 84 c
84 85 d
85 86 e
86 87 f
87 88
88 89
89 90 check histedit_source, including that it uses the later date, from the first changeset
90 91
91 92 $ hg log --debug --rev 3
92 93 changeset: 3:575228819b7e6ed69e8c0a6a383ee59a80db7358
93 94 phase: draft
94 95 parent: 2:505a591af19eed18f560af827b9e03d2076773dc
95 96 parent: -1:0000000000000000000000000000000000000000
96 97 manifest: 3:81eede616954057198ead0b2c73b41d1f392829a
97 98 user: test
98 99 date: Thu Jan 01 00:00:06 1970 +0000
99 100 files+: c f
100 101 extra: branch=default
101 102 extra: histedit_source=7cad1d7030207872dfd1c3a7cb430f24f2884086,ff2c9fa2018b15fa74b33363bda9527323e2a99f
102 103 description:
103 104 f
104 105 ***
105 106 c
106 107
107 108
108 109
109 110 rollup will fold without preserving the folded commit's message or date
110 111
111 112 $ OLDHGEDITOR=$HGEDITOR
112 113 $ HGEDITOR=false
113 114 $ hg histedit 97d72e5f12c7 --commands - 2>&1 <<EOF | fixbundle
114 115 > pick 97d72e5f12c7 b
115 116 > roll 505a591af19e e
116 117 > pick 575228819b7e f
117 118 > pick c4d7f3def76d d
118 119 > EOF
119 120
120 121 $ HGEDITOR=$OLDHGEDITOR
121 122
122 123 log after edit
123 124 $ hg logt --graph
124 125 @ 3:bab801520cec d
125 126 |
126 127 o 2:58c8f2bfc151 f
127 128 |
128 129 o 1:5d939c56c72e b
129 130 |
130 131 o 0:8580ff50825a a
131 132
132 133
133 134 description is taken from rollup target commit
134 135
135 136 $ hg log --debug --rev 1
136 137 changeset: 1:5d939c56c72e77e29f5167696218e2131a40f5cf
137 138 phase: draft
138 139 parent: 0:8580ff50825a50c8f716709acdf8de0deddcd6ab
139 140 parent: -1:0000000000000000000000000000000000000000
140 141 manifest: 1:b5e112a3a8354e269b1524729f0918662d847c38
141 142 user: test
142 143 date: Thu Jan 01 00:00:02 1970 +0000
143 144 files+: b e
144 145 extra: branch=default
145 146 extra: histedit_source=97d72e5f12c7e84f85064aa72e5a297142c36ed9,505a591af19eed18f560af827b9e03d2076773dc
146 147 description:
147 148 b
148 149
149 150
150 151
151 152 check saving last-message.txt
152 153
153 154 $ cat > $TESTTMP/abortfolding.py <<EOF
154 155 > from mercurial import util
155 156 > def abortfolding(ui, repo, hooktype, **kwargs):
156 157 > ctx = repo[kwargs.get('node')]
157 158 > if set(ctx.files()) == {b'c', b'd', b'f'}:
158 159 > return True # abort folding commit only
159 160 > ui.warn(b'allow non-folding commit\\n')
160 161 > EOF
161 162 $ cat > .hg/hgrc <<EOF
162 163 > [hooks]
163 164 > pretxncommit.abortfolding = python:$TESTTMP/abortfolding.py:abortfolding
164 165 > EOF
165 166
166 167 $ cat > $TESTTMP/editor.sh << EOF
167 168 > echo "==== before editing"
168 169 > cat \$1
169 170 > echo "===="
170 171 > echo "check saving last-message.txt" >> \$1
171 172 > EOF
172 173
173 174 $ rm -f .hg/last-message.txt
174 175 $ hg status --rev '58c8f2bfc151^1::bab801520cec'
175 176 A c
176 177 A d
177 178 A f
178 179 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit 58c8f2bfc151 --commands - 2>&1 <<EOF
179 180 > pick 58c8f2bfc151 f
180 181 > fold bab801520cec d
181 182 > EOF
182 183 allow non-folding commit
183 184 ==== before editing
184 185 f
185 186 ***
186 187 c
187 188 ***
188 189 d
189 190
190 191
191 192
192 193 HG: Enter commit message. Lines beginning with 'HG:' are removed.
193 194 HG: Leave message empty to abort commit.
194 195 HG: --
195 196 HG: user: test
196 197 HG: branch 'default'
197 198 HG: added c
198 199 HG: added d
199 200 HG: added f
200 201 ====
201 202 transaction abort!
202 203 rollback completed
203 204 abort: pretxncommit.abortfolding hook failed
204 205 [255]
205 206
206 207 $ cat .hg/last-message.txt
207 208 f
208 209 ***
209 210 c
210 211 ***
211 212 d
212 213
213 214
214 215
215 216 check saving last-message.txt
216 217
217 218 $ cd ..
218 219 $ rm -r r
219 220
220 221 folding preserves initial author but uses later date
221 222 ----------------------------------------------------
222 223
223 224 $ initrepo
224 225
225 226 $ hg ci -d '7 0' --user "someone else" --amend --quiet
226 227
227 228 tip before edit
228 229 $ hg log --rev .
229 230 changeset: 5:10c36dd37515
230 231 tag: tip
231 232 user: someone else
232 233 date: Thu Jan 01 00:00:07 1970 +0000
233 234 summary: f
234 235
235 236
236 237 $ hg --config progress.debug=1 --debug \
237 238 > histedit 1ddb6c90f2ee --commands - 2>&1 <<EOF | \
238 239 > egrep 'editing|unresolved'
239 240 > pick 1ddb6c90f2ee e
240 241 > fold 10c36dd37515 f
241 242 > EOF
242 243 editing: pick 1ddb6c90f2ee 4 e 1/2 changes (50.00%)
243 244 editing: fold 10c36dd37515 5 f 2/2 changes (100.00%)
244 245
245 246 tip after edit, which should use the later date, from the second changeset
246 247 $ hg log --rev .
247 248 changeset: 4:e4f3ec5d0b40
248 249 tag: tip
249 250 user: test
250 251 date: Thu Jan 01 00:00:07 1970 +0000
251 252 summary: e
252 253
253 254
254 255 $ cd ..
255 256 $ rm -r r
256 257
257 258 folding and creating no new change doesn't break:
258 259 -------------------------------------------------
259 260
260 261 folded content is dropped during a merge. The folded commit should properly disappear.
261 262
262 263 $ mkdir fold-to-empty-test
263 264 $ cd fold-to-empty-test
264 265 $ hg init
265 266 $ printf "1\n2\n3\n" > file
266 267 $ hg add file
267 268 $ hg commit -m '1+2+3'
268 269 $ echo 4 >> file
269 270 $ hg commit -m '+4'
270 271 $ echo 5 >> file
271 272 $ hg commit -m '+5'
272 273 $ echo 6 >> file
273 274 $ hg commit -m '+6'
274 275 $ hg logt --graph
275 276 @ 3:251d831eeec5 +6
276 277 |
277 278 o 2:888f9082bf99 +5
278 279 |
279 280 o 1:617f94f13c0f +4
280 281 |
281 282 o 0:0189ba417d34 1+2+3
282 283
283 284
284 285 $ hg histedit 1 --commands - << EOF
285 286 > pick 617f94f13c0f 1 +4
286 287 > drop 888f9082bf99 2 +5
287 288 > fold 251d831eeec5 3 +6
288 289 > EOF
289 290 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
290 291 merging file
291 292 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
292 293 Fix up the change (fold 251d831eeec5)
293 294 (hg histedit --continue to resume)
294 295 [1]
295 296 There were conflicts, we keep P1 content. This
296 297 should effectively drop the changes from +6.
297 298
298 299 $ hg status -v
299 300 M file
300 301 ? file.orig
301 302 # The repository is in an unfinished *histedit* state.
302 303
303 304 # Unresolved merge conflicts:
304 305 #
305 306 # file
306 307 #
307 308 # To mark files as resolved: hg resolve --mark FILE
308 309
309 310 # To continue: hg histedit --continue
310 311 # To abort: hg histedit --abort
311 312
312 313 $ hg resolve -l
313 314 U file
314 315 $ hg revert -r 'p1()' file
315 316 $ hg resolve --mark file
316 317 (no more unresolved files)
317 318 continue: hg histedit --continue
318 319 $ hg histedit --continue
319 320 251d831eeec5: empty changeset
320 321 saved backup bundle to $TESTTMP/fold-to-empty-test/.hg/strip-backup/888f9082bf99-daa0b8b3-histedit.hg
321 322 $ hg logt --graph
322 323 @ 1:617f94f13c0f +4
323 324 |
324 325 o 0:0189ba417d34 1+2+3
325 326
326 327
327 328 $ cd ..
328 329
329 330
330 331 Test fold through dropped
331 332 -------------------------
332 333
333 334
334 335 Test corner case where folded revision is separated from its parent by a
335 336 dropped revision.
336 337
337 338
338 339 $ hg init fold-with-dropped
339 340 $ cd fold-with-dropped
340 341 $ printf "1\n2\n3\n" > file
341 342 $ hg commit -Am '1+2+3'
342 343 adding file
343 344 $ echo 4 >> file
344 345 $ hg commit -m '+4'
345 346 $ echo 5 >> file
346 347 $ hg commit -m '+5'
347 348 $ echo 6 >> file
348 349 $ hg commit -m '+6'
349 350 $ hg logt -G
350 351 @ 3:251d831eeec5 +6
351 352 |
352 353 o 2:888f9082bf99 +5
353 354 |
354 355 o 1:617f94f13c0f +4
355 356 |
356 357 o 0:0189ba417d34 1+2+3
357 358
358 359 $ hg histedit 1 --commands - << EOF
359 360 > pick 617f94f13c0f 1 +4
360 361 > drop 888f9082bf99 2 +5
361 362 > fold 251d831eeec5 3 +6
362 363 > EOF
363 364 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
364 365 merging file
365 366 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
366 367 Fix up the change (fold 251d831eeec5)
367 368 (hg histedit --continue to resume)
368 369 [1]
369 370 $ cat > file << EOF
370 371 > 1
371 372 > 2
372 373 > 3
373 374 > 4
374 375 > 5
375 376 > EOF
376 377 $ hg resolve --mark file
377 378 (no more unresolved files)
378 379 continue: hg histedit --continue
379 380 $ hg commit -m '+5.2'
380 381 created new head
381 382 $ echo 6 >> file
382 383 $ HGEDITOR=cat hg histedit --continue
383 384 +4
384 385 ***
385 386 +5.2
386 387 ***
387 388 +6
388 389
389 390
390 391
391 392 HG: Enter commit message. Lines beginning with 'HG:' are removed.
392 393 HG: Leave message empty to abort commit.
393 394 HG: --
394 395 HG: user: test
395 396 HG: branch 'default'
396 397 HG: changed file
397 398 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-3d69522c-histedit.hg
398 399 $ hg logt -G
399 400 @ 1:10c647b2cdd5 +4
400 401 |
401 402 o 0:0189ba417d34 1+2+3
402 403
403 404 $ hg export tip
404 405 # HG changeset patch
405 406 # User test
406 407 # Date 0 0
407 408 # Thu Jan 01 00:00:00 1970 +0000
408 409 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
409 410 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
410 411 +4
411 412 ***
412 413 +5.2
413 414 ***
414 415 +6
415 416
416 417 diff -r 0189ba417d34 -r 10c647b2cdd5 file
417 418 --- a/file Thu Jan 01 00:00:00 1970 +0000
418 419 +++ b/file Thu Jan 01 00:00:00 1970 +0000
419 420 @@ -1,3 +1,6 @@
420 421 1
421 422 2
422 423 3
423 424 +4
424 425 +5
425 426 +6
426 427 $ cd ..
427 428
428 429
429 430 Folding with initial rename (issue3729)
430 431 ---------------------------------------
431 432
432 433 $ hg init fold-rename
433 434 $ cd fold-rename
434 435 $ echo a > a.txt
435 436 $ hg add a.txt
436 437 $ hg commit -m a
437 438 $ hg rename a.txt b.txt
438 439 $ hg commit -m rename
439 440 $ echo b >> b.txt
440 441 $ hg commit -m b
441 442
442 443 $ hg logt --follow b.txt
443 444 2:e0371e0426bc b
444 445 1:1c4f440a8085 rename
445 446 0:6c795aa153cb a
446 447
447 448 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
448 449 > pick 1c4f440a8085 rename
449 450 > fold e0371e0426bc b
450 451 > EOF
451 452
452 453 $ hg logt --follow b.txt
453 454 1:cf858d235c76 rename
454 455 0:6c795aa153cb a
455 456
456 457 $ cd ..
457 458
458 459 Folding with swapping
459 460 ---------------------
460 461
461 462 This is an excuse to test hook with histedit temporary commit (issue4422)
462 463
463 464
464 465 $ hg init issue4422
465 466 $ cd issue4422
466 467 $ echo a > a.txt
467 468 $ hg add a.txt
468 469 $ hg commit -m a
469 470 $ echo b > b.txt
470 471 $ hg add b.txt
471 472 $ hg commit -m b
472 473 $ echo c > c.txt
473 474 $ hg add c.txt
474 475 $ hg commit -m c
475 476
476 477 $ hg logt
477 478 2:a1a953ffb4b0 c
478 479 1:199b6bb90248 b
479 480 0:6c795aa153cb a
480 481
481 482 $ hg histedit 6c795aa153cb --config hooks.commit='echo commit $HG_NODE' --config hooks.tonative.commit=True \
482 483 > --commands - 2>&1 << EOF | fixbundle
483 484 > pick 199b6bb90248 b
484 485 > fold a1a953ffb4b0 c
485 486 > pick 6c795aa153cb a
486 487 > EOF
487 488 commit 9599899f62c05f4377548c32bf1c9f1a39634b0c
488 489
489 490 $ hg logt
490 491 1:9599899f62c0 a
491 492 0:79b99e9c8e49 b
492 493
493 494 Test unix -> windows style variable substitution in external hooks.
494 495
495 496 $ cat > $TESTTMP/tmp.hgrc <<'EOF'
496 497 > [hooks]
497 498 > pre-add = echo no variables
498 499 > post-add = echo ran $HG_ARGS, literal \$non-var, 'also $non-var', $HG_RESULT
499 500 > tonative.post-add = True
500 501 > EOF
501 502
502 503 $ echo "foo" > amended.txt
503 504 $ HGRCPATH=$TESTTMP/tmp.hgrc hg add -v amended.txt
504 505 running hook pre-add: echo no variables
505 506 no variables
506 507 adding amended.txt
507 508 converting hook "post-add" to native (windows !)
508 509 running hook post-add: echo ran %HG_ARGS%, literal $non-var, "also $non-var", %HG_RESULT% (windows !)
509 510 running hook post-add: echo ran $HG_ARGS, literal \$non-var, 'also $non-var', $HG_RESULT (no-windows !)
510 511 ran add -v amended.txt, literal $non-var, "also $non-var", 0 (windows !)
511 512 ran add -v amended.txt, literal $non-var, also $non-var, 0 (no-windows !)
512 513 $ hg ci -q --config extensions.largefiles= --amend -I amended.txt
513 514 The fsmonitor extension is incompatible with the largefiles extension and has been disabled. (fsmonitor !)
514 515
515 516 Test that folding multiple changes in a row doesn't show multiple
516 517 editors.
517 518
518 519 $ echo foo >> foo
519 520 $ hg add foo
520 521 $ hg ci -m foo1
521 522 $ echo foo >> foo
522 523 $ hg ci -m foo2
523 524 $ echo foo >> foo
524 525 $ hg ci -m foo3
525 526 $ hg logt
526 527 4:21679ff7675c foo3
527 528 3:b7389cc4d66e foo2
528 529 2:0e01aeef5fa8 foo1
529 530 1:578c7455730c a
530 531 0:79b99e9c8e49 b
531 532 $ cat > "$TESTTMP/editor.sh" <<EOF
532 533 > echo ran editor >> "$TESTTMP/editorlog.txt"
533 534 > cat \$1 >> "$TESTTMP/editorlog.txt"
534 535 > echo END >> "$TESTTMP/editorlog.txt"
535 536 > echo merged foos > \$1
536 537 > EOF
537 538 $ HGEDITOR="sh \"$TESTTMP/editor.sh\"" hg histedit 1 --commands - 2>&1 <<EOF | fixbundle
538 539 > pick 578c7455730c 1 a
539 540 > pick 0e01aeef5fa8 2 foo1
540 541 > fold b7389cc4d66e 3 foo2
541 542 > fold 21679ff7675c 4 foo3
542 543 > EOF
543 544 $ hg logt
544 545 2:e8bedbda72c1 merged foos
545 546 1:578c7455730c a
546 547 0:79b99e9c8e49 b
547 548 Editor should have run only once
548 549 $ cat $TESTTMP/editorlog.txt
549 550 ran editor
550 551 foo1
551 552 ***
552 553 foo2
553 554 ***
554 555 foo3
555 556
556 557
557 558
558 559 HG: Enter commit message. Lines beginning with 'HG:' are removed.
559 560 HG: Leave message empty to abort commit.
560 561 HG: --
561 562 HG: user: test
562 563 HG: branch 'default'
563 564 HG: added foo
564 565 END
565 566
566 567 $ cd ..
567 568
568 569 Test rolling into a commit with multiple children (issue5498)
569 570
570 571 $ hg init roll
571 572 $ cd roll
572 573 $ echo a > a
573 574 $ hg commit -qAm aa
574 575 $ echo b > b
575 576 $ hg commit -qAm bb
576 577 $ hg up -q ".^"
577 578 $ echo c > c
578 579 $ hg commit -qAm cc
579 580 $ hg log -G -T '{node|short} {desc}'
580 581 @ 5db65b93a12b cc
581 582 |
582 583 | o 301d76bdc3ae bb
583 584 |/
584 585 o 8f0162e483d0 aa
585 586
586 587
587 588 $ hg histedit . --commands - << EOF
588 589 > r 5db65b93a12b
589 590 > EOF
590 591 hg: parse error: first changeset cannot use verb "roll"
591 592 [255]
592 593 $ hg log -G -T '{node|short} {desc}'
593 594 @ 5db65b93a12b cc
594 595 |
595 596 | o 301d76bdc3ae bb
596 597 |/
597 598 o 8f0162e483d0 aa
598 599
599 600
601 $ cd ..
602
603 ====================================
604 Test update-timestamp config option|
605 ====================================
606
607 $ addwithdate ()
608 > {
609 > echo $1 > $1
610 > hg add $1
611 > hg ci -m $1 -d "$2 0"
612 > }
613
614 $ initrepo ()
615 > {
616 > hg init r
617 > cd r
618 > addwithdate a 1
619 > addwithdate b 2
620 > addwithdate c 3
621 > addwithdate d 4
622 > addwithdate e 5
623 > addwithdate f 6
624 > }
625
626 $ initrepo
627
628 log before edit
629
630 $ hg log
631 changeset: 5:178e35e0ce73
632 tag: tip
633 user: test
634 date: Thu Jan 01 00:00:06 1970 +0000
635 summary: f
636
637 changeset: 4:1ddb6c90f2ee
638 user: test
639 date: Thu Jan 01 00:00:05 1970 +0000
640 summary: e
641
642 changeset: 3:532247a8969b
643 user: test
644 date: Thu Jan 01 00:00:04 1970 +0000
645 summary: d
646
647 changeset: 2:ff2c9fa2018b
648 user: test
649 date: Thu Jan 01 00:00:03 1970 +0000
650 summary: c
651
652 changeset: 1:97d72e5f12c7
653 user: test
654 date: Thu Jan 01 00:00:02 1970 +0000
655 summary: b
656
657 changeset: 0:8580ff50825a
658 user: test
659 date: Thu Jan 01 00:00:01 1970 +0000
660 summary: a
661
662
663 $ hg histedit 1ddb6c90f2ee --commands - 2>&1 --config rewrite.update-timestamp=True <<EOF | fixbundle
664 > pick 178e35e0ce73 f
665 > fold 1ddb6c90f2ee e
666 > EOF
667
668 log after edit
669 observe time from f is updated
670
671 $ hg log
672 changeset: 4:f7909b1863a2
673 tag: tip
674 user: test
675 date: Thu Jan 01 00:00:01 1970 +0000
676 summary: f
677
678 changeset: 3:532247a8969b
679 user: test
680 date: Thu Jan 01 00:00:04 1970 +0000
681 summary: d
682
683 changeset: 2:ff2c9fa2018b
684 user: test
685 date: Thu Jan 01 00:00:03 1970 +0000
686 summary: c
687
688 changeset: 1:97d72e5f12c7
689 user: test
690 date: Thu Jan 01 00:00:02 1970 +0000
691 summary: b
692
693 changeset: 0:8580ff50825a
694 user: test
695 date: Thu Jan 01 00:00:01 1970 +0000
696 summary: a
697
698 post-fold manifest
699 $ hg manifest
700 a
701 b
702 c
703 d
704 e
705 f
706
707 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now