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