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