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