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