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