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