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