##// END OF EJS Templates
histedit: add support to output nodechanges using formatter...
Pulkit Goyal -
r35124:a9cc233d default
parent child Browse files
Show More
@@ -0,0 +1,54 b''
1 Testing templating for histedit command
2
3 Setup
4
5 $ cat >> $HGRCPATH <<EOF
6 > [extensions]
7 > histedit=
8 > [experimental]
9 > evolution=createmarkers
10 > EOF
11
12 $ hg init repo
13 $ cd repo
14 $ for ch in a b c d; do echo foo > $ch; hg commit -Aqm "Added "$ch; done
15
16 $ hg log -G -T "{rev}:{node|short} {desc}"
17 @ 3:62615734edd5 Added d
18 |
19 o 2:28ad74487de9 Added c
20 |
21 o 1:29becc82797a Added b
22 |
23 o 0:18d04c59bb5d Added a
24
25 Getting the JSON output for nodechanges
26
27 $ hg histedit -Tjson --commands - 2>&1 <<EOF
28 > pick 28ad74487de9 Added c
29 > pick 62615734edd5 Added d
30 > pick 18d04c59bb5d Added a
31 > pick 29becc82797a Added b
32 > EOF
33 [
34 {
35 "nodechanges": {"18d04c59bb5d2d4090ad9a5b59bd6274adb63add": ["109f8ec895447f81b380ba8d4d8b66539ccdcb94"], "28ad74487de9599d00d81085be739c61fc340652": ["bff9e07c1807942b161dab768aa793b48e9a7f9d"], "29becc82797a4bc11ec8880b58eaecd2ab3e7760": ["f5dcf3b4db23f31f1aacf46c33d1393de303d26f"], "62615734edd52f06b6fb9c2beb429e4fe30d57b8": ["201423b441c84d9e6858daed653e0d22485c1cfa"]}
36 }
37 ]
38
39 $ hg log -G -T "{rev}:{node|short} {desc}"
40 @ 7:f5dcf3b4db23 Added b
41 |
42 o 6:109f8ec89544 Added a
43 |
44 o 5:201423b441c8 Added d
45 |
46 o 4:bff9e07c1807 Added c
47
48 $ hg histedit -T "{nodechanges|json}" --commands - 2>&1 <<EOF
49 > pick bff9e07c1807 Added c
50 > pick 201423b441c8 Added d
51 > pick 109f8ec89544 Added a
52 > roll f5dcf3b4db23 Added b
53 > EOF
54 {"109f8ec895447f81b380ba8d4d8b66539ccdcb94": ["8d01470bfeab64d3de13c49adb79d88790d38396"], "f3ec56a374bdbdf1953cacca505161442c6f3a3e": [], "f5dcf3b4db23f31f1aacf46c33d1393de303d26f": ["8d01470bfeab64d3de13c49adb79d88790d38396"]} (no-eol)
@@ -1,1636 +1,1647 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 import errno
187 187 import os
188 188
189 189 from mercurial.i18n import _
190 190 from mercurial import (
191 191 bundle2,
192 192 cmdutil,
193 193 context,
194 194 copies,
195 195 destutil,
196 196 discovery,
197 197 error,
198 198 exchange,
199 199 extensions,
200 200 hg,
201 201 lock,
202 202 merge as mergemod,
203 203 mergeutil,
204 204 node,
205 205 obsolete,
206 206 pycompat,
207 207 registrar,
208 208 repair,
209 209 scmutil,
210 210 util,
211 211 )
212 212
213 213 pickle = util.pickle
214 214 release = lock.release
215 215 cmdtable = {}
216 216 command = registrar.command(cmdtable)
217 217
218 218 configtable = {}
219 219 configitem = registrar.configitem(configtable)
220 220 configitem('experimental', 'histedit.autoverb',
221 221 default=False,
222 222 )
223 223 configitem('histedit', 'defaultrev',
224 224 default=configitem.dynamicdefault,
225 225 )
226 226 configitem('histedit', 'dropmissing',
227 227 default=False,
228 228 )
229 229 configitem('histedit', 'linelen',
230 230 default=80,
231 231 )
232 232 configitem('histedit', 'singletransaction',
233 233 default=False,
234 234 )
235 235
236 236 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
237 237 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
238 238 # be specifying the version(s) of Mercurial they are tested with, or
239 239 # leave the attribute unspecified.
240 240 testedwith = 'ships-with-hg-core'
241 241
242 242 actiontable = {}
243 243 primaryactions = set()
244 244 secondaryactions = set()
245 245 tertiaryactions = set()
246 246 internalactions = set()
247 247
248 248 def geteditcomment(ui, first, last):
249 249 """ construct the editor comment
250 250 The comment includes::
251 251 - an intro
252 252 - sorted primary commands
253 253 - sorted short commands
254 254 - sorted long commands
255 255 - additional hints
256 256
257 257 Commands are only included once.
258 258 """
259 259 intro = _("""Edit history between %s and %s
260 260
261 261 Commits are listed from least to most recent
262 262
263 263 You can reorder changesets by reordering the lines
264 264
265 265 Commands:
266 266 """)
267 267 actions = []
268 268 def addverb(v):
269 269 a = actiontable[v]
270 270 lines = a.message.split("\n")
271 271 if len(a.verbs):
272 272 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
273 273 actions.append(" %s = %s" % (v, lines[0]))
274 274 actions.extend([' %s' for l in lines[1:]])
275 275
276 276 for v in (
277 277 sorted(primaryactions) +
278 278 sorted(secondaryactions) +
279 279 sorted(tertiaryactions)
280 280 ):
281 281 addverb(v)
282 282 actions.append('')
283 283
284 284 hints = []
285 285 if ui.configbool('histedit', 'dropmissing'):
286 286 hints.append("Deleting a changeset from the list "
287 287 "will DISCARD it from the edited history!")
288 288
289 289 lines = (intro % (first, last)).split('\n') + actions + hints
290 290
291 291 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
292 292
293 293 class histeditstate(object):
294 294 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
295 295 topmost=None, replacements=None, lock=None, wlock=None):
296 296 self.repo = repo
297 297 self.actions = actions
298 298 self.keep = keep
299 299 self.topmost = topmost
300 300 self.parentctxnode = parentctxnode
301 301 self.lock = lock
302 302 self.wlock = wlock
303 303 self.backupfile = None
304 304 if replacements is None:
305 305 self.replacements = []
306 306 else:
307 307 self.replacements = replacements
308 308
309 309 def read(self):
310 310 """Load histedit state from disk and set fields appropriately."""
311 311 try:
312 312 state = self.repo.vfs.read('histedit-state')
313 313 except IOError as err:
314 314 if err.errno != errno.ENOENT:
315 315 raise
316 316 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
317 317
318 318 if state.startswith('v1\n'):
319 319 data = self._load()
320 320 parentctxnode, rules, keep, topmost, replacements, backupfile = data
321 321 else:
322 322 data = pickle.loads(state)
323 323 parentctxnode, rules, keep, topmost, replacements = data
324 324 backupfile = None
325 325
326 326 self.parentctxnode = parentctxnode
327 327 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
328 328 actions = parserules(rules, self)
329 329 self.actions = actions
330 330 self.keep = keep
331 331 self.topmost = topmost
332 332 self.replacements = replacements
333 333 self.backupfile = backupfile
334 334
335 335 def write(self, tr=None):
336 336 if tr:
337 337 tr.addfilegenerator('histedit-state', ('histedit-state',),
338 338 self._write, location='plain')
339 339 else:
340 340 with self.repo.vfs("histedit-state", "w") as f:
341 341 self._write(f)
342 342
343 343 def _write(self, fp):
344 344 fp.write('v1\n')
345 345 fp.write('%s\n' % node.hex(self.parentctxnode))
346 346 fp.write('%s\n' % node.hex(self.topmost))
347 347 fp.write('%s\n' % self.keep)
348 348 fp.write('%d\n' % len(self.actions))
349 349 for action in self.actions:
350 350 fp.write('%s\n' % action.tostate())
351 351 fp.write('%d\n' % len(self.replacements))
352 352 for replacement in self.replacements:
353 353 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
354 354 for r in replacement[1])))
355 355 backupfile = self.backupfile
356 356 if not backupfile:
357 357 backupfile = ''
358 358 fp.write('%s\n' % backupfile)
359 359
360 360 def _load(self):
361 361 fp = self.repo.vfs('histedit-state', 'r')
362 362 lines = [l[:-1] for l in fp.readlines()]
363 363
364 364 index = 0
365 365 lines[index] # version number
366 366 index += 1
367 367
368 368 parentctxnode = node.bin(lines[index])
369 369 index += 1
370 370
371 371 topmost = node.bin(lines[index])
372 372 index += 1
373 373
374 374 keep = lines[index] == 'True'
375 375 index += 1
376 376
377 377 # Rules
378 378 rules = []
379 379 rulelen = int(lines[index])
380 380 index += 1
381 381 for i in xrange(rulelen):
382 382 ruleaction = lines[index]
383 383 index += 1
384 384 rule = lines[index]
385 385 index += 1
386 386 rules.append((ruleaction, rule))
387 387
388 388 # Replacements
389 389 replacements = []
390 390 replacementlen = int(lines[index])
391 391 index += 1
392 392 for i in xrange(replacementlen):
393 393 replacement = lines[index]
394 394 original = node.bin(replacement[:40])
395 395 succ = [node.bin(replacement[i:i + 40]) for i in
396 396 range(40, len(replacement), 40)]
397 397 replacements.append((original, succ))
398 398 index += 1
399 399
400 400 backupfile = lines[index]
401 401 index += 1
402 402
403 403 fp.close()
404 404
405 405 return parentctxnode, rules, keep, topmost, replacements, backupfile
406 406
407 407 def clear(self):
408 408 if self.inprogress():
409 409 self.repo.vfs.unlink('histedit-state')
410 410
411 411 def inprogress(self):
412 412 return self.repo.vfs.exists('histedit-state')
413 413
414 414
415 415 class histeditaction(object):
416 416 def __init__(self, state, node):
417 417 self.state = state
418 418 self.repo = state.repo
419 419 self.node = node
420 420
421 421 @classmethod
422 422 def fromrule(cls, state, rule):
423 423 """Parses the given rule, returning an instance of the histeditaction.
424 424 """
425 425 rulehash = rule.strip().split(' ', 1)[0]
426 426 try:
427 427 rev = node.bin(rulehash)
428 428 except TypeError:
429 429 raise error.ParseError("invalid changeset %s" % rulehash)
430 430 return cls(state, rev)
431 431
432 432 def verify(self, prev, expected, seen):
433 433 """ Verifies semantic correctness of the rule"""
434 434 repo = self.repo
435 435 ha = node.hex(self.node)
436 436 try:
437 437 self.node = repo[ha].node()
438 438 except error.RepoError:
439 439 raise error.ParseError(_('unknown changeset %s listed')
440 440 % ha[:12])
441 441 if self.node is not None:
442 442 self._verifynodeconstraints(prev, expected, seen)
443 443
444 444 def _verifynodeconstraints(self, prev, expected, seen):
445 445 # by default command need a node in the edited list
446 446 if self.node not in expected:
447 447 raise error.ParseError(_('%s "%s" changeset was not a candidate')
448 448 % (self.verb, node.short(self.node)),
449 449 hint=_('only use listed changesets'))
450 450 # and only one command per node
451 451 if self.node in seen:
452 452 raise error.ParseError(_('duplicated command for changeset %s') %
453 453 node.short(self.node))
454 454
455 455 def torule(self):
456 456 """build a histedit rule line for an action
457 457
458 458 by default lines are in the form:
459 459 <hash> <rev> <summary>
460 460 """
461 461 ctx = self.repo[self.node]
462 462 summary = _getsummary(ctx)
463 463 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
464 464 # trim to 75 columns by default so it's not stupidly wide in my editor
465 465 # (the 5 more are left for verb)
466 466 maxlen = self.repo.ui.configint('histedit', 'linelen')
467 467 maxlen = max(maxlen, 22) # avoid truncating hash
468 468 return util.ellipsis(line, maxlen)
469 469
470 470 def tostate(self):
471 471 """Print an action in format used by histedit state files
472 472 (the first line is a verb, the remainder is the second)
473 473 """
474 474 return "%s\n%s" % (self.verb, node.hex(self.node))
475 475
476 476 def run(self):
477 477 """Runs the action. The default behavior is simply apply the action's
478 478 rulectx onto the current parentctx."""
479 479 self.applychange()
480 480 self.continuedirty()
481 481 return self.continueclean()
482 482
483 483 def applychange(self):
484 484 """Applies the changes from this action's rulectx onto the current
485 485 parentctx, but does not commit them."""
486 486 repo = self.repo
487 487 rulectx = repo[self.node]
488 488 repo.ui.pushbuffer(error=True, labeled=True)
489 489 hg.update(repo, self.state.parentctxnode, quietempty=True)
490 490 stats = applychanges(repo.ui, repo, rulectx, {})
491 491 if stats and stats[3] > 0:
492 492 buf = repo.ui.popbuffer()
493 493 repo.ui.write(*buf)
494 494 raise error.InterventionRequired(
495 495 _('Fix up the change (%s %s)') %
496 496 (self.verb, node.short(self.node)),
497 497 hint=_('hg histedit --continue to resume'))
498 498 else:
499 499 repo.ui.popbuffer()
500 500
501 501 def continuedirty(self):
502 502 """Continues the action when changes have been applied to the working
503 503 copy. The default behavior is to commit the dirty changes."""
504 504 repo = self.repo
505 505 rulectx = repo[self.node]
506 506
507 507 editor = self.commiteditor()
508 508 commit = commitfuncfor(repo, rulectx)
509 509
510 510 commit(text=rulectx.description(), user=rulectx.user(),
511 511 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
512 512
513 513 def commiteditor(self):
514 514 """The editor to be used to edit the commit message."""
515 515 return False
516 516
517 517 def continueclean(self):
518 518 """Continues the action when the working copy is clean. The default
519 519 behavior is to accept the current commit as the new version of the
520 520 rulectx."""
521 521 ctx = self.repo['.']
522 522 if ctx.node() == self.state.parentctxnode:
523 523 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
524 524 node.short(self.node))
525 525 return ctx, [(self.node, tuple())]
526 526 if ctx.node() == self.node:
527 527 # Nothing changed
528 528 return ctx, []
529 529 return ctx, [(self.node, (ctx.node(),))]
530 530
531 531 def commitfuncfor(repo, src):
532 532 """Build a commit function for the replacement of <src>
533 533
534 534 This function ensure we apply the same treatment to all changesets.
535 535
536 536 - Add a 'histedit_source' entry in extra.
537 537
538 538 Note that fold has its own separated logic because its handling is a bit
539 539 different and not easily factored out of the fold method.
540 540 """
541 541 phasemin = src.phase()
542 542 def commitfunc(**kwargs):
543 543 overrides = {('phases', 'new-commit'): phasemin}
544 544 with repo.ui.configoverride(overrides, 'histedit'):
545 545 extra = kwargs.get(r'extra', {}).copy()
546 546 extra['histedit_source'] = src.hex()
547 547 kwargs[r'extra'] = extra
548 548 return repo.commit(**kwargs)
549 549 return commitfunc
550 550
551 551 def applychanges(ui, repo, ctx, opts):
552 552 """Merge changeset from ctx (only) in the current working directory"""
553 553 wcpar = repo.dirstate.parents()[0]
554 554 if ctx.p1().node() == wcpar:
555 555 # edits are "in place" we do not need to make any merge,
556 556 # just applies changes on parent for editing
557 557 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
558 558 stats = None
559 559 else:
560 560 try:
561 561 # ui.forcemerge is an internal variable, do not document
562 562 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
563 563 'histedit')
564 564 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
565 565 finally:
566 566 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
567 567 return stats
568 568
569 569 def collapse(repo, first, last, commitopts, skipprompt=False):
570 570 """collapse the set of revisions from first to last as new one.
571 571
572 572 Expected commit options are:
573 573 - message
574 574 - date
575 575 - username
576 576 Commit message is edited in all cases.
577 577
578 578 This function works in memory."""
579 579 ctxs = list(repo.set('%d::%d', first, last))
580 580 if not ctxs:
581 581 return None
582 582 for c in ctxs:
583 583 if not c.mutable():
584 584 raise error.ParseError(
585 585 _("cannot fold into public change %s") % node.short(c.node()))
586 586 base = first.parents()[0]
587 587
588 588 # commit a new version of the old changeset, including the update
589 589 # collect all files which might be affected
590 590 files = set()
591 591 for ctx in ctxs:
592 592 files.update(ctx.files())
593 593
594 594 # Recompute copies (avoid recording a -> b -> a)
595 595 copied = copies.pathcopies(base, last)
596 596
597 597 # prune files which were reverted by the updates
598 598 files = [f for f in files if not cmdutil.samefile(f, last, base)]
599 599 # commit version of these files as defined by head
600 600 headmf = last.manifest()
601 601 def filectxfn(repo, ctx, path):
602 602 if path in headmf:
603 603 fctx = last[path]
604 604 flags = fctx.flags()
605 605 mctx = context.memfilectx(repo,
606 606 fctx.path(), fctx.data(),
607 607 islink='l' in flags,
608 608 isexec='x' in flags,
609 609 copied=copied.get(path))
610 610 return mctx
611 611 return None
612 612
613 613 if commitopts.get('message'):
614 614 message = commitopts['message']
615 615 else:
616 616 message = first.description()
617 617 user = commitopts.get('user')
618 618 date = commitopts.get('date')
619 619 extra = commitopts.get('extra')
620 620
621 621 parents = (first.p1().node(), first.p2().node())
622 622 editor = None
623 623 if not skipprompt:
624 624 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
625 625 new = context.memctx(repo,
626 626 parents=parents,
627 627 text=message,
628 628 files=files,
629 629 filectxfn=filectxfn,
630 630 user=user,
631 631 date=date,
632 632 extra=extra,
633 633 editor=editor)
634 634 return repo.commitctx(new)
635 635
636 636 def _isdirtywc(repo):
637 637 return repo[None].dirty(missing=True)
638 638
639 639 def abortdirty():
640 640 raise error.Abort(_('working copy has pending changes'),
641 641 hint=_('amend, commit, or revert them and run histedit '
642 642 '--continue, or abort with histedit --abort'))
643 643
644 644 def action(verbs, message, priority=False, internal=False):
645 645 def wrap(cls):
646 646 assert not priority or not internal
647 647 verb = verbs[0]
648 648 if priority:
649 649 primaryactions.add(verb)
650 650 elif internal:
651 651 internalactions.add(verb)
652 652 elif len(verbs) > 1:
653 653 secondaryactions.add(verb)
654 654 else:
655 655 tertiaryactions.add(verb)
656 656
657 657 cls.verb = verb
658 658 cls.verbs = verbs
659 659 cls.message = message
660 660 for verb in verbs:
661 661 actiontable[verb] = cls
662 662 return cls
663 663 return wrap
664 664
665 665 @action(['pick', 'p'],
666 666 _('use commit'),
667 667 priority=True)
668 668 class pick(histeditaction):
669 669 def run(self):
670 670 rulectx = self.repo[self.node]
671 671 if rulectx.parents()[0].node() == self.state.parentctxnode:
672 672 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
673 673 return rulectx, []
674 674
675 675 return super(pick, self).run()
676 676
677 677 @action(['edit', 'e'],
678 678 _('use commit, but stop for amending'),
679 679 priority=True)
680 680 class edit(histeditaction):
681 681 def run(self):
682 682 repo = self.repo
683 683 rulectx = repo[self.node]
684 684 hg.update(repo, self.state.parentctxnode, quietempty=True)
685 685 applychanges(repo.ui, repo, rulectx, {})
686 686 raise error.InterventionRequired(
687 687 _('Editing (%s), you may commit or record as needed now.')
688 688 % node.short(self.node),
689 689 hint=_('hg histedit --continue to resume'))
690 690
691 691 def commiteditor(self):
692 692 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
693 693
694 694 @action(['fold', 'f'],
695 695 _('use commit, but combine it with the one above'))
696 696 class fold(histeditaction):
697 697 def verify(self, prev, expected, seen):
698 698 """ Verifies semantic correctness of the fold rule"""
699 699 super(fold, self).verify(prev, expected, seen)
700 700 repo = self.repo
701 701 if not prev:
702 702 c = repo[self.node].parents()[0]
703 703 elif not prev.verb in ('pick', 'base'):
704 704 return
705 705 else:
706 706 c = repo[prev.node]
707 707 if not c.mutable():
708 708 raise error.ParseError(
709 709 _("cannot fold into public change %s") % node.short(c.node()))
710 710
711 711
712 712 def continuedirty(self):
713 713 repo = self.repo
714 714 rulectx = repo[self.node]
715 715
716 716 commit = commitfuncfor(repo, rulectx)
717 717 commit(text='fold-temp-revision %s' % node.short(self.node),
718 718 user=rulectx.user(), date=rulectx.date(),
719 719 extra=rulectx.extra())
720 720
721 721 def continueclean(self):
722 722 repo = self.repo
723 723 ctx = repo['.']
724 724 rulectx = repo[self.node]
725 725 parentctxnode = self.state.parentctxnode
726 726 if ctx.node() == parentctxnode:
727 727 repo.ui.warn(_('%s: empty changeset\n') %
728 728 node.short(self.node))
729 729 return ctx, [(self.node, (parentctxnode,))]
730 730
731 731 parentctx = repo[parentctxnode]
732 732 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
733 733 parentctx))
734 734 if not newcommits:
735 735 repo.ui.warn(_('%s: cannot fold - working copy is not a '
736 736 'descendant of previous commit %s\n') %
737 737 (node.short(self.node), node.short(parentctxnode)))
738 738 return ctx, [(self.node, (ctx.node(),))]
739 739
740 740 middlecommits = newcommits.copy()
741 741 middlecommits.discard(ctx.node())
742 742
743 743 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
744 744 middlecommits)
745 745
746 746 def skipprompt(self):
747 747 """Returns true if the rule should skip the message editor.
748 748
749 749 For example, 'fold' wants to show an editor, but 'rollup'
750 750 doesn't want to.
751 751 """
752 752 return False
753 753
754 754 def mergedescs(self):
755 755 """Returns true if the rule should merge messages of multiple changes.
756 756
757 757 This exists mainly so that 'rollup' rules can be a subclass of
758 758 'fold'.
759 759 """
760 760 return True
761 761
762 762 def firstdate(self):
763 763 """Returns true if the rule should preserve the date of the first
764 764 change.
765 765
766 766 This exists mainly so that 'rollup' rules can be a subclass of
767 767 'fold'.
768 768 """
769 769 return False
770 770
771 771 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
772 772 parent = ctx.parents()[0].node()
773 773 repo.ui.pushbuffer()
774 774 hg.update(repo, parent)
775 775 repo.ui.popbuffer()
776 776 ### prepare new commit data
777 777 commitopts = {}
778 778 commitopts['user'] = ctx.user()
779 779 # commit message
780 780 if not self.mergedescs():
781 781 newmessage = ctx.description()
782 782 else:
783 783 newmessage = '\n***\n'.join(
784 784 [ctx.description()] +
785 785 [repo[r].description() for r in internalchanges] +
786 786 [oldctx.description()]) + '\n'
787 787 commitopts['message'] = newmessage
788 788 # date
789 789 if self.firstdate():
790 790 commitopts['date'] = ctx.date()
791 791 else:
792 792 commitopts['date'] = max(ctx.date(), oldctx.date())
793 793 extra = ctx.extra().copy()
794 794 # histedit_source
795 795 # note: ctx is likely a temporary commit but that the best we can do
796 796 # here. This is sufficient to solve issue3681 anyway.
797 797 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
798 798 commitopts['extra'] = extra
799 799 phasemin = max(ctx.phase(), oldctx.phase())
800 800 overrides = {('phases', 'new-commit'): phasemin}
801 801 with repo.ui.configoverride(overrides, 'histedit'):
802 802 n = collapse(repo, ctx, repo[newnode], commitopts,
803 803 skipprompt=self.skipprompt())
804 804 if n is None:
805 805 return ctx, []
806 806 repo.ui.pushbuffer()
807 807 hg.update(repo, n)
808 808 repo.ui.popbuffer()
809 809 replacements = [(oldctx.node(), (newnode,)),
810 810 (ctx.node(), (n,)),
811 811 (newnode, (n,)),
812 812 ]
813 813 for ich in internalchanges:
814 814 replacements.append((ich, (n,)))
815 815 return repo[n], replacements
816 816
817 817 @action(['base', 'b'],
818 818 _('checkout changeset and apply further changesets from there'))
819 819 class base(histeditaction):
820 820
821 821 def run(self):
822 822 if self.repo['.'].node() != self.node:
823 823 mergemod.update(self.repo, self.node, False, True)
824 824 # branchmerge, force)
825 825 return self.continueclean()
826 826
827 827 def continuedirty(self):
828 828 abortdirty()
829 829
830 830 def continueclean(self):
831 831 basectx = self.repo['.']
832 832 return basectx, []
833 833
834 834 def _verifynodeconstraints(self, prev, expected, seen):
835 835 # base can only be use with a node not in the edited set
836 836 if self.node in expected:
837 837 msg = _('%s "%s" changeset was an edited list candidate')
838 838 raise error.ParseError(
839 839 msg % (self.verb, node.short(self.node)),
840 840 hint=_('base must only use unlisted changesets'))
841 841
842 842 @action(['_multifold'],
843 843 _(
844 844 """fold subclass used for when multiple folds happen in a row
845 845
846 846 We only want to fire the editor for the folded message once when
847 847 (say) four changes are folded down into a single change. This is
848 848 similar to rollup, but we should preserve both messages so that
849 849 when the last fold operation runs we can show the user all the
850 850 commit messages in their editor.
851 851 """),
852 852 internal=True)
853 853 class _multifold(fold):
854 854 def skipprompt(self):
855 855 return True
856 856
857 857 @action(["roll", "r"],
858 858 _("like fold, but discard this commit's description and date"))
859 859 class rollup(fold):
860 860 def mergedescs(self):
861 861 return False
862 862
863 863 def skipprompt(self):
864 864 return True
865 865
866 866 def firstdate(self):
867 867 return True
868 868
869 869 @action(["drop", "d"],
870 870 _('remove commit from history'))
871 871 class drop(histeditaction):
872 872 def run(self):
873 873 parentctx = self.repo[self.state.parentctxnode]
874 874 return parentctx, [(self.node, tuple())]
875 875
876 876 @action(["mess", "m"],
877 877 _('edit commit message without changing commit content'),
878 878 priority=True)
879 879 class message(histeditaction):
880 880 def commiteditor(self):
881 881 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
882 882
883 883 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
884 884 """utility function to find the first outgoing changeset
885 885
886 886 Used by initialization code"""
887 887 if opts is None:
888 888 opts = {}
889 889 dest = ui.expandpath(remote or 'default-push', remote or 'default')
890 890 dest, revs = hg.parseurl(dest, None)[:2]
891 891 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
892 892
893 893 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
894 894 other = hg.peer(repo, opts, dest)
895 895
896 896 if revs:
897 897 revs = [repo.lookup(rev) for rev in revs]
898 898
899 899 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
900 900 if not outgoing.missing:
901 901 raise error.Abort(_('no outgoing ancestors'))
902 902 roots = list(repo.revs("roots(%ln)", outgoing.missing))
903 903 if 1 < len(roots):
904 904 msg = _('there are ambiguous outgoing revisions')
905 905 hint = _("see 'hg help histedit' for more detail")
906 906 raise error.Abort(msg, hint=hint)
907 907 return repo.lookup(roots[0])
908 908
909 909 @command('histedit',
910 910 [('', 'commands', '',
911 911 _('read history edits from the specified file'), _('FILE')),
912 912 ('c', 'continue', False, _('continue an edit already in progress')),
913 913 ('', 'edit-plan', False, _('edit remaining actions list')),
914 914 ('k', 'keep', False,
915 915 _("don't strip old nodes after edit is complete")),
916 916 ('', 'abort', False, _('abort an edit in progress')),
917 917 ('o', 'outgoing', False, _('changesets not found in destination')),
918 918 ('f', 'force', False,
919 919 _('force outgoing even for unrelated repositories')),
920 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
920 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
921 cmdutil.formatteropts,
921 922 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
922 923 def histedit(ui, repo, *freeargs, **opts):
923 924 """interactively edit changeset history
924 925
925 926 This command lets you edit a linear series of changesets (up to
926 927 and including the working directory, which should be clean).
927 928 You can:
928 929
929 930 - `pick` to [re]order a changeset
930 931
931 932 - `drop` to omit changeset
932 933
933 934 - `mess` to reword the changeset commit message
934 935
935 936 - `fold` to combine it with the preceding changeset (using the later date)
936 937
937 938 - `roll` like fold, but discarding this commit's description and date
938 939
939 940 - `edit` to edit this changeset (preserving date)
940 941
941 942 - `base` to checkout changeset and apply further changesets from there
942 943
943 944 There are a number of ways to select the root changeset:
944 945
945 946 - Specify ANCESTOR directly
946 947
947 948 - Use --outgoing -- it will be the first linear changeset not
948 949 included in destination. (See :hg:`help config.paths.default-push`)
949 950
950 951 - Otherwise, the value from the "histedit.defaultrev" config option
951 952 is used as a revset to select the base revision when ANCESTOR is not
952 953 specified. The first revision returned by the revset is used. By
953 954 default, this selects the editable history that is unique to the
954 955 ancestry of the working directory.
955 956
956 957 .. container:: verbose
957 958
958 959 If you use --outgoing, this command will abort if there are ambiguous
959 960 outgoing revisions. For example, if there are multiple branches
960 961 containing outgoing revisions.
961 962
962 963 Use "min(outgoing() and ::.)" or similar revset specification
963 964 instead of --outgoing to specify edit target revision exactly in
964 965 such ambiguous situation. See :hg:`help revsets` for detail about
965 966 selecting revisions.
966 967
967 968 .. container:: verbose
968 969
969 970 Examples:
970 971
971 972 - A number of changes have been made.
972 973 Revision 3 is no longer needed.
973 974
974 975 Start history editing from revision 3::
975 976
976 977 hg histedit -r 3
977 978
978 979 An editor opens, containing the list of revisions,
979 980 with specific actions specified::
980 981
981 982 pick 5339bf82f0ca 3 Zworgle the foobar
982 983 pick 8ef592ce7cc4 4 Bedazzle the zerlog
983 984 pick 0a9639fcda9d 5 Morgify the cromulancy
984 985
985 986 Additional information about the possible actions
986 987 to take appears below the list of revisions.
987 988
988 989 To remove revision 3 from the history,
989 990 its action (at the beginning of the relevant line)
990 991 is changed to 'drop'::
991 992
992 993 drop 5339bf82f0ca 3 Zworgle the foobar
993 994 pick 8ef592ce7cc4 4 Bedazzle the zerlog
994 995 pick 0a9639fcda9d 5 Morgify the cromulancy
995 996
996 997 - A number of changes have been made.
997 998 Revision 2 and 4 need to be swapped.
998 999
999 1000 Start history editing from revision 2::
1000 1001
1001 1002 hg histedit -r 2
1002 1003
1003 1004 An editor opens, containing the list of revisions,
1004 1005 with specific actions specified::
1005 1006
1006 1007 pick 252a1af424ad 2 Blorb a morgwazzle
1007 1008 pick 5339bf82f0ca 3 Zworgle the foobar
1008 1009 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1009 1010
1010 1011 To swap revision 2 and 4, its lines are swapped
1011 1012 in the editor::
1012 1013
1013 1014 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1014 1015 pick 5339bf82f0ca 3 Zworgle the foobar
1015 1016 pick 252a1af424ad 2 Blorb a morgwazzle
1016 1017
1017 1018 Returns 0 on success, 1 if user intervention is required (not only
1018 1019 for intentional "edit" command, but also for resolving unexpected
1019 1020 conflicts).
1020 1021 """
1021 1022 state = histeditstate(repo)
1022 1023 try:
1023 1024 state.wlock = repo.wlock()
1024 1025 state.lock = repo.lock()
1025 1026 _histedit(ui, repo, state, *freeargs, **opts)
1026 1027 finally:
1027 1028 release(state.lock, state.wlock)
1028 1029
1029 1030 goalcontinue = 'continue'
1030 1031 goalabort = 'abort'
1031 1032 goaleditplan = 'edit-plan'
1032 1033 goalnew = 'new'
1033 1034
1034 1035 def _getgoal(opts):
1035 1036 if opts.get('continue'):
1036 1037 return goalcontinue
1037 1038 if opts.get('abort'):
1038 1039 return goalabort
1039 1040 if opts.get('edit_plan'):
1040 1041 return goaleditplan
1041 1042 return goalnew
1042 1043
1043 1044 def _readfile(ui, path):
1044 1045 if path == '-':
1045 1046 with ui.timeblockedsection('histedit'):
1046 1047 return ui.fin.read()
1047 1048 else:
1048 1049 with open(path, 'rb') as f:
1049 1050 return f.read()
1050 1051
1051 1052 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1052 1053 # TODO only abort if we try to histedit mq patches, not just
1053 1054 # blanket if mq patches are applied somewhere
1054 1055 mq = getattr(repo, 'mq', None)
1055 1056 if mq and mq.applied:
1056 1057 raise error.Abort(_('source has mq patches applied'))
1057 1058
1058 1059 # basic argument incompatibility processing
1059 1060 outg = opts.get('outgoing')
1060 1061 editplan = opts.get('edit_plan')
1061 1062 abort = opts.get('abort')
1062 1063 force = opts.get('force')
1063 1064 if force and not outg:
1064 1065 raise error.Abort(_('--force only allowed with --outgoing'))
1065 1066 if goal == 'continue':
1066 1067 if any((outg, abort, revs, freeargs, rules, editplan)):
1067 1068 raise error.Abort(_('no arguments allowed with --continue'))
1068 1069 elif goal == 'abort':
1069 1070 if any((outg, revs, freeargs, rules, editplan)):
1070 1071 raise error.Abort(_('no arguments allowed with --abort'))
1071 1072 elif goal == 'edit-plan':
1072 1073 if any((outg, revs, freeargs)):
1073 1074 raise error.Abort(_('only --commands argument allowed with '
1074 1075 '--edit-plan'))
1075 1076 else:
1076 1077 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1077 1078 raise error.Abort(_('history edit already in progress, try '
1078 1079 '--continue or --abort'))
1079 1080 if outg:
1080 1081 if revs:
1081 1082 raise error.Abort(_('no revisions allowed with --outgoing'))
1082 1083 if len(freeargs) > 1:
1083 1084 raise error.Abort(
1084 1085 _('only one repo argument allowed with --outgoing'))
1085 1086 else:
1086 1087 revs.extend(freeargs)
1087 1088 if len(revs) == 0:
1088 1089 defaultrev = destutil.desthistedit(ui, repo)
1089 1090 if defaultrev is not None:
1090 1091 revs.append(defaultrev)
1091 1092
1092 1093 if len(revs) != 1:
1093 1094 raise error.Abort(
1094 1095 _('histedit requires exactly one ancestor revision'))
1095 1096
1096 1097 def _histedit(ui, repo, state, *freeargs, **opts):
1097 1098 opts = pycompat.byteskwargs(opts)
1099 fm = ui.formatter('histedit', opts)
1100 fm.startitem()
1098 1101 goal = _getgoal(opts)
1099 1102 revs = opts.get('rev', [])
1100 1103 rules = opts.get('commands', '')
1101 1104 state.keep = opts.get('keep', False)
1102 1105
1103 1106 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1104 1107
1105 1108 # rebuild state
1106 1109 if goal == goalcontinue:
1107 1110 state.read()
1108 1111 state = bootstrapcontinue(ui, state, opts)
1109 1112 elif goal == goaleditplan:
1110 1113 _edithisteditplan(ui, repo, state, rules)
1111 1114 return
1112 1115 elif goal == goalabort:
1113 1116 _aborthistedit(ui, repo, state)
1114 1117 return
1115 1118 else:
1116 1119 # goal == goalnew
1117 1120 _newhistedit(ui, repo, state, revs, freeargs, opts)
1118 1121
1119 1122 _continuehistedit(ui, repo, state)
1120 _finishhistedit(ui, repo, state)
1123 _finishhistedit(ui, repo, state, fm)
1124 fm.end()
1121 1125
1122 1126 def _continuehistedit(ui, repo, state):
1123 1127 """This function runs after either:
1124 1128 - bootstrapcontinue (if the goal is 'continue')
1125 1129 - _newhistedit (if the goal is 'new')
1126 1130 """
1127 1131 # preprocess rules so that we can hide inner folds from the user
1128 1132 # and only show one editor
1129 1133 actions = state.actions[:]
1130 1134 for idx, (action, nextact) in enumerate(
1131 1135 zip(actions, actions[1:] + [None])):
1132 1136 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1133 1137 state.actions[idx].__class__ = _multifold
1134 1138
1135 1139 # Force an initial state file write, so the user can run --abort/continue
1136 1140 # even if there's an exception before the first transaction serialize.
1137 1141 state.write()
1138 1142
1139 1143 total = len(state.actions)
1140 1144 pos = 0
1141 1145 tr = None
1142 1146 # Don't use singletransaction by default since it rolls the entire
1143 1147 # transaction back if an unexpected exception happens (like a
1144 1148 # pretxncommit hook throws, or the user aborts the commit msg editor).
1145 1149 if ui.configbool("histedit", "singletransaction"):
1146 1150 # Don't use a 'with' for the transaction, since actions may close
1147 1151 # and reopen a transaction. For example, if the action executes an
1148 1152 # external process it may choose to commit the transaction first.
1149 1153 tr = repo.transaction('histedit')
1150 1154 with util.acceptintervention(tr):
1151 1155 while state.actions:
1152 1156 state.write(tr=tr)
1153 1157 actobj = state.actions[0]
1154 1158 pos += 1
1155 1159 ui.progress(_("editing"), pos, actobj.torule(),
1156 1160 _('changes'), total)
1157 1161 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1158 1162 actobj.torule()))
1159 1163 parentctx, replacement_ = actobj.run()
1160 1164 state.parentctxnode = parentctx.node()
1161 1165 state.replacements.extend(replacement_)
1162 1166 state.actions.pop(0)
1163 1167
1164 1168 state.write()
1165 1169 ui.progress(_("editing"), None)
1166 1170
1167 def _finishhistedit(ui, repo, state):
1171 def _finishhistedit(ui, repo, state, fm):
1168 1172 """This action runs when histedit is finishing its session"""
1169 1173 repo.ui.pushbuffer()
1170 1174 hg.update(repo, state.parentctxnode, quietempty=True)
1171 1175 repo.ui.popbuffer()
1172 1176
1173 1177 mapping, tmpnodes, created, ntm = processreplacement(state)
1174 1178 if mapping:
1175 1179 for prec, succs in mapping.iteritems():
1176 1180 if not succs:
1177 1181 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1178 1182 else:
1179 1183 ui.debug('histedit: %s is replaced by %s\n' % (
1180 1184 node.short(prec), node.short(succs[0])))
1181 1185 if len(succs) > 1:
1182 1186 m = 'histedit: %s'
1183 1187 for n in succs[1:]:
1184 1188 ui.debug(m % node.short(n))
1185 1189
1186 1190 if not state.keep:
1187 1191 if mapping:
1188 1192 movetopmostbookmarks(repo, state.topmost, ntm)
1189 1193 # TODO update mq state
1190 1194 else:
1191 1195 mapping = {}
1192 1196
1193 1197 for n in tmpnodes:
1194 1198 mapping[n] = ()
1195 1199
1196 1200 # remove entries about unknown nodes
1197 1201 nodemap = repo.unfiltered().changelog.nodemap
1198 1202 mapping = {k: v for k, v in mapping.items()
1199 1203 if k in nodemap and all(n in nodemap for n in v)}
1200 1204 scmutil.cleanupnodes(repo, mapping, 'histedit')
1205 hf = fm.hexfunc
1206 fl = fm.formatlist
1207 fd = fm.formatdict
1208 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1209 for oldn, newn in mapping.iteritems()},
1210 key="oldnode", value="newnodes")
1211 fm.data(nodechanges=nodechanges)
1201 1212
1202 1213 state.clear()
1203 1214 if os.path.exists(repo.sjoin('undo')):
1204 1215 os.unlink(repo.sjoin('undo'))
1205 1216 if repo.vfs.exists('histedit-last-edit.txt'):
1206 1217 repo.vfs.unlink('histedit-last-edit.txt')
1207 1218
1208 1219 def _aborthistedit(ui, repo, state):
1209 1220 try:
1210 1221 state.read()
1211 1222 __, leafs, tmpnodes, __ = processreplacement(state)
1212 1223 ui.debug('restore wc to old parent %s\n'
1213 1224 % node.short(state.topmost))
1214 1225
1215 1226 # Recover our old commits if necessary
1216 1227 if not state.topmost in repo and state.backupfile:
1217 1228 backupfile = repo.vfs.join(state.backupfile)
1218 1229 f = hg.openpath(ui, backupfile)
1219 1230 gen = exchange.readbundle(ui, f, backupfile)
1220 1231 with repo.transaction('histedit.abort') as tr:
1221 1232 bundle2.applybundle(repo, gen, tr, source='histedit',
1222 1233 url='bundle:' + backupfile)
1223 1234
1224 1235 os.remove(backupfile)
1225 1236
1226 1237 # check whether we should update away
1227 1238 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1228 1239 state.parentctxnode, leafs | tmpnodes):
1229 1240 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1230 1241 cleanupnode(ui, repo, tmpnodes)
1231 1242 cleanupnode(ui, repo, leafs)
1232 1243 except Exception:
1233 1244 if state.inprogress():
1234 1245 ui.warn(_('warning: encountered an exception during histedit '
1235 1246 '--abort; the repository may not have been completely '
1236 1247 'cleaned up\n'))
1237 1248 raise
1238 1249 finally:
1239 1250 state.clear()
1240 1251
1241 1252 def _edithisteditplan(ui, repo, state, rules):
1242 1253 state.read()
1243 1254 if not rules:
1244 1255 comment = geteditcomment(ui,
1245 1256 node.short(state.parentctxnode),
1246 1257 node.short(state.topmost))
1247 1258 rules = ruleeditor(repo, ui, state.actions, comment)
1248 1259 else:
1249 1260 rules = _readfile(ui, rules)
1250 1261 actions = parserules(rules, state)
1251 1262 ctxs = [repo[act.node] \
1252 1263 for act in state.actions if act.node]
1253 1264 warnverifyactions(ui, repo, actions, state, ctxs)
1254 1265 state.actions = actions
1255 1266 state.write()
1256 1267
1257 1268 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1258 1269 outg = opts.get('outgoing')
1259 1270 rules = opts.get('commands', '')
1260 1271 force = opts.get('force')
1261 1272
1262 1273 cmdutil.checkunfinished(repo)
1263 1274 cmdutil.bailifchanged(repo)
1264 1275
1265 1276 topmost, empty = repo.dirstate.parents()
1266 1277 if outg:
1267 1278 if freeargs:
1268 1279 remote = freeargs[0]
1269 1280 else:
1270 1281 remote = None
1271 1282 root = findoutgoing(ui, repo, remote, force, opts)
1272 1283 else:
1273 1284 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1274 1285 if len(rr) != 1:
1275 1286 raise error.Abort(_('The specified revisions must have '
1276 1287 'exactly one common root'))
1277 1288 root = rr[0].node()
1278 1289
1279 1290 revs = between(repo, root, topmost, state.keep)
1280 1291 if not revs:
1281 1292 raise error.Abort(_('%s is not an ancestor of working directory') %
1282 1293 node.short(root))
1283 1294
1284 1295 ctxs = [repo[r] for r in revs]
1285 1296 if not rules:
1286 1297 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1287 1298 actions = [pick(state, r) for r in revs]
1288 1299 rules = ruleeditor(repo, ui, actions, comment)
1289 1300 else:
1290 1301 rules = _readfile(ui, rules)
1291 1302 actions = parserules(rules, state)
1292 1303 warnverifyactions(ui, repo, actions, state, ctxs)
1293 1304
1294 1305 parentctxnode = repo[root].parents()[0].node()
1295 1306
1296 1307 state.parentctxnode = parentctxnode
1297 1308 state.actions = actions
1298 1309 state.topmost = topmost
1299 1310 state.replacements = []
1300 1311
1301 1312 # Create a backup so we can always abort completely.
1302 1313 backupfile = None
1303 1314 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1304 1315 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1305 1316 'histedit')
1306 1317 state.backupfile = backupfile
1307 1318
1308 1319 def _getsummary(ctx):
1309 1320 # a common pattern is to extract the summary but default to the empty
1310 1321 # string
1311 1322 summary = ctx.description() or ''
1312 1323 if summary:
1313 1324 summary = summary.splitlines()[0]
1314 1325 return summary
1315 1326
1316 1327 def bootstrapcontinue(ui, state, opts):
1317 1328 repo = state.repo
1318 1329
1319 1330 ms = mergemod.mergestate.read(repo)
1320 1331 mergeutil.checkunresolved(ms)
1321 1332
1322 1333 if state.actions:
1323 1334 actobj = state.actions.pop(0)
1324 1335
1325 1336 if _isdirtywc(repo):
1326 1337 actobj.continuedirty()
1327 1338 if _isdirtywc(repo):
1328 1339 abortdirty()
1329 1340
1330 1341 parentctx, replacements = actobj.continueclean()
1331 1342
1332 1343 state.parentctxnode = parentctx.node()
1333 1344 state.replacements.extend(replacements)
1334 1345
1335 1346 return state
1336 1347
1337 1348 def between(repo, old, new, keep):
1338 1349 """select and validate the set of revision to edit
1339 1350
1340 1351 When keep is false, the specified set can't have children."""
1341 1352 ctxs = list(repo.set('%n::%n', old, new))
1342 1353 if ctxs and not keep:
1343 1354 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1344 1355 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1345 1356 raise error.Abort(_('can only histedit a changeset together '
1346 1357 'with all its descendants'))
1347 1358 if repo.revs('(%ld) and merge()', ctxs):
1348 1359 raise error.Abort(_('cannot edit history that contains merges'))
1349 1360 root = ctxs[0] # list is already sorted by repo.set
1350 1361 if not root.mutable():
1351 1362 raise error.Abort(_('cannot edit public changeset: %s') % root,
1352 1363 hint=_("see 'hg help phases' for details"))
1353 1364 return [c.node() for c in ctxs]
1354 1365
1355 1366 def ruleeditor(repo, ui, actions, editcomment=""):
1356 1367 """open an editor to edit rules
1357 1368
1358 1369 rules are in the format [ [act, ctx], ...] like in state.rules
1359 1370 """
1360 1371 if repo.ui.configbool("experimental", "histedit.autoverb"):
1361 1372 newact = util.sortdict()
1362 1373 for act in actions:
1363 1374 ctx = repo[act.node]
1364 1375 summary = _getsummary(ctx)
1365 1376 fword = summary.split(' ', 1)[0].lower()
1366 1377 added = False
1367 1378
1368 1379 # if it doesn't end with the special character '!' just skip this
1369 1380 if fword.endswith('!'):
1370 1381 fword = fword[:-1]
1371 1382 if fword in primaryactions | secondaryactions | tertiaryactions:
1372 1383 act.verb = fword
1373 1384 # get the target summary
1374 1385 tsum = summary[len(fword) + 1:].lstrip()
1375 1386 # safe but slow: reverse iterate over the actions so we
1376 1387 # don't clash on two commits having the same summary
1377 1388 for na, l in reversed(list(newact.iteritems())):
1378 1389 actx = repo[na.node]
1379 1390 asum = _getsummary(actx)
1380 1391 if asum == tsum:
1381 1392 added = True
1382 1393 l.append(act)
1383 1394 break
1384 1395
1385 1396 if not added:
1386 1397 newact[act] = []
1387 1398
1388 1399 # copy over and flatten the new list
1389 1400 actions = []
1390 1401 for na, l in newact.iteritems():
1391 1402 actions.append(na)
1392 1403 actions += l
1393 1404
1394 1405 rules = '\n'.join([act.torule() for act in actions])
1395 1406 rules += '\n\n'
1396 1407 rules += editcomment
1397 1408 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1398 1409 repopath=repo.path, action='histedit')
1399 1410
1400 1411 # Save edit rules in .hg/histedit-last-edit.txt in case
1401 1412 # the user needs to ask for help after something
1402 1413 # surprising happens.
1403 1414 f = open(repo.vfs.join('histedit-last-edit.txt'), 'w')
1404 1415 f.write(rules)
1405 1416 f.close()
1406 1417
1407 1418 return rules
1408 1419
1409 1420 def parserules(rules, state):
1410 1421 """Read the histedit rules string and return list of action objects """
1411 1422 rules = [l for l in (r.strip() for r in rules.splitlines())
1412 1423 if l and not l.startswith('#')]
1413 1424 actions = []
1414 1425 for r in rules:
1415 1426 if ' ' not in r:
1416 1427 raise error.ParseError(_('malformed line "%s"') % r)
1417 1428 verb, rest = r.split(' ', 1)
1418 1429
1419 1430 if verb not in actiontable:
1420 1431 raise error.ParseError(_('unknown action "%s"') % verb)
1421 1432
1422 1433 action = actiontable[verb].fromrule(state, rest)
1423 1434 actions.append(action)
1424 1435 return actions
1425 1436
1426 1437 def warnverifyactions(ui, repo, actions, state, ctxs):
1427 1438 try:
1428 1439 verifyactions(actions, state, ctxs)
1429 1440 except error.ParseError:
1430 1441 if repo.vfs.exists('histedit-last-edit.txt'):
1431 1442 ui.warn(_('warning: histedit rules saved '
1432 1443 'to: .hg/histedit-last-edit.txt\n'))
1433 1444 raise
1434 1445
1435 1446 def verifyactions(actions, state, ctxs):
1436 1447 """Verify that there exists exactly one action per given changeset and
1437 1448 other constraints.
1438 1449
1439 1450 Will abort if there are to many or too few rules, a malformed rule,
1440 1451 or a rule on a changeset outside of the user-given range.
1441 1452 """
1442 1453 expected = set(c.node() for c in ctxs)
1443 1454 seen = set()
1444 1455 prev = None
1445 1456
1446 1457 if actions and actions[0].verb in ['roll', 'fold']:
1447 1458 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1448 1459 actions[0].verb)
1449 1460
1450 1461 for action in actions:
1451 1462 action.verify(prev, expected, seen)
1452 1463 prev = action
1453 1464 if action.node is not None:
1454 1465 seen.add(action.node)
1455 1466 missing = sorted(expected - seen) # sort to stabilize output
1456 1467
1457 1468 if state.repo.ui.configbool('histedit', 'dropmissing'):
1458 1469 if len(actions) == 0:
1459 1470 raise error.ParseError(_('no rules provided'),
1460 1471 hint=_('use strip extension to remove commits'))
1461 1472
1462 1473 drops = [drop(state, n) for n in missing]
1463 1474 # put the in the beginning so they execute immediately and
1464 1475 # don't show in the edit-plan in the future
1465 1476 actions[:0] = drops
1466 1477 elif missing:
1467 1478 raise error.ParseError(_('missing rules for changeset %s') %
1468 1479 node.short(missing[0]),
1469 1480 hint=_('use "drop %s" to discard, see also: '
1470 1481 "'hg help -e histedit.config'")
1471 1482 % node.short(missing[0]))
1472 1483
1473 1484 def adjustreplacementsfrommarkers(repo, oldreplacements):
1474 1485 """Adjust replacements from obsolescence markers
1475 1486
1476 1487 Replacements structure is originally generated based on
1477 1488 histedit's state and does not account for changes that are
1478 1489 not recorded there. This function fixes that by adding
1479 1490 data read from obsolescence markers"""
1480 1491 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1481 1492 return oldreplacements
1482 1493
1483 1494 unfi = repo.unfiltered()
1484 1495 nm = unfi.changelog.nodemap
1485 1496 obsstore = repo.obsstore
1486 1497 newreplacements = list(oldreplacements)
1487 1498 oldsuccs = [r[1] for r in oldreplacements]
1488 1499 # successors that have already been added to succstocheck once
1489 1500 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1490 1501 succstocheck = list(seensuccs)
1491 1502 while succstocheck:
1492 1503 n = succstocheck.pop()
1493 1504 missing = nm.get(n) is None
1494 1505 markers = obsstore.successors.get(n, ())
1495 1506 if missing and not markers:
1496 1507 # dead end, mark it as such
1497 1508 newreplacements.append((n, ()))
1498 1509 for marker in markers:
1499 1510 nsuccs = marker[1]
1500 1511 newreplacements.append((n, nsuccs))
1501 1512 for nsucc in nsuccs:
1502 1513 if nsucc not in seensuccs:
1503 1514 seensuccs.add(nsucc)
1504 1515 succstocheck.append(nsucc)
1505 1516
1506 1517 return newreplacements
1507 1518
1508 1519 def processreplacement(state):
1509 1520 """process the list of replacements to return
1510 1521
1511 1522 1) the final mapping between original and created nodes
1512 1523 2) the list of temporary node created by histedit
1513 1524 3) the list of new commit created by histedit"""
1514 1525 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1515 1526 allsuccs = set()
1516 1527 replaced = set()
1517 1528 fullmapping = {}
1518 1529 # initialize basic set
1519 1530 # fullmapping records all operations recorded in replacement
1520 1531 for rep in replacements:
1521 1532 allsuccs.update(rep[1])
1522 1533 replaced.add(rep[0])
1523 1534 fullmapping.setdefault(rep[0], set()).update(rep[1])
1524 1535 new = allsuccs - replaced
1525 1536 tmpnodes = allsuccs & replaced
1526 1537 # Reduce content fullmapping into direct relation between original nodes
1527 1538 # and final node created during history edition
1528 1539 # Dropped changeset are replaced by an empty list
1529 1540 toproceed = set(fullmapping)
1530 1541 final = {}
1531 1542 while toproceed:
1532 1543 for x in list(toproceed):
1533 1544 succs = fullmapping[x]
1534 1545 for s in list(succs):
1535 1546 if s in toproceed:
1536 1547 # non final node with unknown closure
1537 1548 # We can't process this now
1538 1549 break
1539 1550 elif s in final:
1540 1551 # non final node, replace with closure
1541 1552 succs.remove(s)
1542 1553 succs.update(final[s])
1543 1554 else:
1544 1555 final[x] = succs
1545 1556 toproceed.remove(x)
1546 1557 # remove tmpnodes from final mapping
1547 1558 for n in tmpnodes:
1548 1559 del final[n]
1549 1560 # we expect all changes involved in final to exist in the repo
1550 1561 # turn `final` into list (topologically sorted)
1551 1562 nm = state.repo.changelog.nodemap
1552 1563 for prec, succs in final.items():
1553 1564 final[prec] = sorted(succs, key=nm.get)
1554 1565
1555 1566 # computed topmost element (necessary for bookmark)
1556 1567 if new:
1557 1568 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1558 1569 elif not final:
1559 1570 # Nothing rewritten at all. we won't need `newtopmost`
1560 1571 # It is the same as `oldtopmost` and `processreplacement` know it
1561 1572 newtopmost = None
1562 1573 else:
1563 1574 # every body died. The newtopmost is the parent of the root.
1564 1575 r = state.repo.changelog.rev
1565 1576 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1566 1577
1567 1578 return final, tmpnodes, new, newtopmost
1568 1579
1569 1580 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1570 1581 """Move bookmark from oldtopmost to newly created topmost
1571 1582
1572 1583 This is arguably a feature and we may only want that for the active
1573 1584 bookmark. But the behavior is kept compatible with the old version for now.
1574 1585 """
1575 1586 if not oldtopmost or not newtopmost:
1576 1587 return
1577 1588 oldbmarks = repo.nodebookmarks(oldtopmost)
1578 1589 if oldbmarks:
1579 1590 with repo.lock(), repo.transaction('histedit') as tr:
1580 1591 marks = repo._bookmarks
1581 1592 changes = []
1582 1593 for name in oldbmarks:
1583 1594 changes.append((name, newtopmost))
1584 1595 marks.applychanges(repo, tr, changes)
1585 1596
1586 1597 def cleanupnode(ui, repo, nodes):
1587 1598 """strip a group of nodes from the repository
1588 1599
1589 1600 The set of node to strip may contains unknown nodes."""
1590 1601 with repo.lock():
1591 1602 # do not let filtering get in the way of the cleanse
1592 1603 # we should probably get rid of obsolescence marker created during the
1593 1604 # histedit, but we currently do not have such information.
1594 1605 repo = repo.unfiltered()
1595 1606 # Find all nodes that need to be stripped
1596 1607 # (we use %lr instead of %ln to silently ignore unknown items)
1597 1608 nm = repo.changelog.nodemap
1598 1609 nodes = sorted(n for n in nodes if n in nm)
1599 1610 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1600 1611 if roots:
1601 1612 repair.strip(ui, repo, roots)
1602 1613
1603 1614 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1604 1615 if isinstance(nodelist, str):
1605 1616 nodelist = [nodelist]
1606 1617 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1607 1618 state = histeditstate(repo)
1608 1619 state.read()
1609 1620 histedit_nodes = {action.node for action
1610 1621 in state.actions if action.node}
1611 1622 common_nodes = histedit_nodes & set(nodelist)
1612 1623 if common_nodes:
1613 1624 raise error.Abort(_("histedit in progress, can't strip %s")
1614 1625 % ', '.join(node.short(x) for x in common_nodes))
1615 1626 return orig(ui, repo, nodelist, *args, **kwargs)
1616 1627
1617 1628 extensions.wrapfunction(repair, 'strip', stripwrapper)
1618 1629
1619 1630 def summaryhook(ui, repo):
1620 1631 if not os.path.exists(repo.vfs.join('histedit-state')):
1621 1632 return
1622 1633 state = histeditstate(repo)
1623 1634 state.read()
1624 1635 if state.actions:
1625 1636 # i18n: column positioning for "hg summary"
1626 1637 ui.write(_('hist: %s (histedit --continue)\n') %
1627 1638 (ui.label(_('%d remaining'), 'histedit.remaining') %
1628 1639 len(state.actions)))
1629 1640
1630 1641 def extsetup(ui):
1631 1642 cmdutil.summaryhooks.add('histedit', summaryhook)
1632 1643 cmdutil.unfinishedstates.append(
1633 1644 ['histedit-state', False, True, _('histedit in progress'),
1634 1645 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1635 1646 cmdutil.afterresolvedstates.append(
1636 1647 ['histedit-state', _('hg histedit --continue')])
General Comments 0
You need to be logged in to leave comments. Login now