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