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