##// END OF EJS Templates
chistedit: change in-progress message...
Jordi Gutiérrez Hermoso -
r42189:1f567a9e default
parent child Browse files
Show More
@@ -1,2272 +1,2272 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 The summary of a change can be customized as well::
159 The summary of a change can be customized as well::
160
160
161 [histedit]
161 [histedit]
162 summary-template = '{rev} {bookmarks} {desc|firstline}'
162 summary-template = '{rev} {bookmarks} {desc|firstline}'
163
163
164 The customized summary should be kept short enough that rule lines
164 The customized summary should be kept short enough that rule lines
165 will fit in the configured line length. See above if that requires
165 will fit in the configured line length. See above if that requires
166 customization.
166 customization.
167
167
168 ``hg histedit`` attempts to automatically choose an appropriate base
168 ``hg histedit`` attempts to automatically choose an appropriate base
169 revision to use. To change which base revision is used, define a
169 revision to use. To change which base revision is used, define a
170 revset in your configuration file::
170 revset in your configuration file::
171
171
172 [histedit]
172 [histedit]
173 defaultrev = only(.) & draft()
173 defaultrev = only(.) & draft()
174
174
175 By default each edited revision needs to be present in histedit commands.
175 By default each edited revision needs to be present in histedit commands.
176 To remove revision you need to use ``drop`` operation. You can configure
176 To remove revision you need to use ``drop`` operation. You can configure
177 the drop to be implicit for missing commits by adding::
177 the drop to be implicit for missing commits by adding::
178
178
179 [histedit]
179 [histedit]
180 dropmissing = True
180 dropmissing = True
181
181
182 By default, histedit will close the transaction after each action. For
182 By default, histedit will close the transaction after each action. For
183 performance purposes, you can configure histedit to use a single transaction
183 performance purposes, you can configure histedit to use a single transaction
184 across the entire histedit. WARNING: This setting introduces a significant risk
184 across the entire histedit. WARNING: This setting introduces a significant risk
185 of losing the work you've done in a histedit if the histedit aborts
185 of losing the work you've done in a histedit if the histedit aborts
186 unexpectedly::
186 unexpectedly::
187
187
188 [histedit]
188 [histedit]
189 singletransaction = True
189 singletransaction = True
190
190
191 """
191 """
192
192
193 from __future__ import absolute_import
193 from __future__ import absolute_import
194
194
195 # chistedit dependencies that are not available everywhere
195 # chistedit dependencies that are not available everywhere
196 try:
196 try:
197 import fcntl
197 import fcntl
198 import termios
198 import termios
199 except ImportError:
199 except ImportError:
200 fcntl = None
200 fcntl = None
201 termios = None
201 termios = None
202
202
203 import functools
203 import functools
204 import os
204 import os
205 import struct
205 import struct
206
206
207 from mercurial.i18n import _
207 from mercurial.i18n import _
208 from mercurial import (
208 from mercurial import (
209 bundle2,
209 bundle2,
210 cmdutil,
210 cmdutil,
211 context,
211 context,
212 copies,
212 copies,
213 destutil,
213 destutil,
214 discovery,
214 discovery,
215 error,
215 error,
216 exchange,
216 exchange,
217 extensions,
217 extensions,
218 hg,
218 hg,
219 logcmdutil,
219 logcmdutil,
220 merge as mergemod,
220 merge as mergemod,
221 mergeutil,
221 mergeutil,
222 node,
222 node,
223 obsolete,
223 obsolete,
224 pycompat,
224 pycompat,
225 registrar,
225 registrar,
226 repair,
226 repair,
227 scmutil,
227 scmutil,
228 state as statemod,
228 state as statemod,
229 util,
229 util,
230 )
230 )
231 from mercurial.utils import (
231 from mercurial.utils import (
232 dateutil,
232 dateutil,
233 stringutil,
233 stringutil,
234 )
234 )
235
235
236 pickle = util.pickle
236 pickle = util.pickle
237 cmdtable = {}
237 cmdtable = {}
238 command = registrar.command(cmdtable)
238 command = registrar.command(cmdtable)
239
239
240 configtable = {}
240 configtable = {}
241 configitem = registrar.configitem(configtable)
241 configitem = registrar.configitem(configtable)
242 configitem('experimental', 'histedit.autoverb',
242 configitem('experimental', 'histedit.autoverb',
243 default=False,
243 default=False,
244 )
244 )
245 configitem('histedit', 'defaultrev',
245 configitem('histedit', 'defaultrev',
246 default=None,
246 default=None,
247 )
247 )
248 configitem('histedit', 'dropmissing',
248 configitem('histedit', 'dropmissing',
249 default=False,
249 default=False,
250 )
250 )
251 configitem('histedit', 'linelen',
251 configitem('histedit', 'linelen',
252 default=80,
252 default=80,
253 )
253 )
254 configitem('histedit', 'singletransaction',
254 configitem('histedit', 'singletransaction',
255 default=False,
255 default=False,
256 )
256 )
257 configitem('ui', 'interface.histedit',
257 configitem('ui', 'interface.histedit',
258 default=None,
258 default=None,
259 )
259 )
260 configitem('histedit', 'summary-template',
260 configitem('histedit', 'summary-template',
261 default='{rev} {desc|firstline}')
261 default='{rev} {desc|firstline}')
262
262
263 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
263 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
264 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
264 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
265 # be specifying the version(s) of Mercurial they are tested with, or
265 # be specifying the version(s) of Mercurial they are tested with, or
266 # leave the attribute unspecified.
266 # leave the attribute unspecified.
267 testedwith = 'ships-with-hg-core'
267 testedwith = 'ships-with-hg-core'
268
268
269 actiontable = {}
269 actiontable = {}
270 primaryactions = set()
270 primaryactions = set()
271 secondaryactions = set()
271 secondaryactions = set()
272 tertiaryactions = set()
272 tertiaryactions = set()
273 internalactions = set()
273 internalactions = set()
274
274
275 def geteditcomment(ui, first, last):
275 def geteditcomment(ui, first, last):
276 """ construct the editor comment
276 """ construct the editor comment
277 The comment includes::
277 The comment includes::
278 - an intro
278 - an intro
279 - sorted primary commands
279 - sorted primary commands
280 - sorted short commands
280 - sorted short commands
281 - sorted long commands
281 - sorted long commands
282 - additional hints
282 - additional hints
283
283
284 Commands are only included once.
284 Commands are only included once.
285 """
285 """
286 intro = _("""Edit history between %s and %s
286 intro = _("""Edit history between %s and %s
287
287
288 Commits are listed from least to most recent
288 Commits are listed from least to most recent
289
289
290 You can reorder changesets by reordering the lines
290 You can reorder changesets by reordering the lines
291
291
292 Commands:
292 Commands:
293 """)
293 """)
294 actions = []
294 actions = []
295 def addverb(v):
295 def addverb(v):
296 a = actiontable[v]
296 a = actiontable[v]
297 lines = a.message.split("\n")
297 lines = a.message.split("\n")
298 if len(a.verbs):
298 if len(a.verbs):
299 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
299 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
300 actions.append(" %s = %s" % (v, lines[0]))
300 actions.append(" %s = %s" % (v, lines[0]))
301 actions.extend([' %s' for l in lines[1:]])
301 actions.extend([' %s' for l in lines[1:]])
302
302
303 for v in (
303 for v in (
304 sorted(primaryactions) +
304 sorted(primaryactions) +
305 sorted(secondaryactions) +
305 sorted(secondaryactions) +
306 sorted(tertiaryactions)
306 sorted(tertiaryactions)
307 ):
307 ):
308 addverb(v)
308 addverb(v)
309 actions.append('')
309 actions.append('')
310
310
311 hints = []
311 hints = []
312 if ui.configbool('histedit', 'dropmissing'):
312 if ui.configbool('histedit', 'dropmissing'):
313 hints.append("Deleting a changeset from the list "
313 hints.append("Deleting a changeset from the list "
314 "will DISCARD it from the edited history!")
314 "will DISCARD it from the edited history!")
315
315
316 lines = (intro % (first, last)).split('\n') + actions + hints
316 lines = (intro % (first, last)).split('\n') + actions + hints
317
317
318 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
318 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
319
319
320 class histeditstate(object):
320 class histeditstate(object):
321 def __init__(self, repo):
321 def __init__(self, repo):
322 self.repo = repo
322 self.repo = repo
323 self.actions = None
323 self.actions = None
324 self.keep = None
324 self.keep = None
325 self.topmost = None
325 self.topmost = None
326 self.parentctxnode = None
326 self.parentctxnode = None
327 self.lock = None
327 self.lock = None
328 self.wlock = None
328 self.wlock = None
329 self.backupfile = None
329 self.backupfile = None
330 self.stateobj = statemod.cmdstate(repo, 'histedit-state')
330 self.stateobj = statemod.cmdstate(repo, 'histedit-state')
331 self.replacements = []
331 self.replacements = []
332
332
333 def read(self):
333 def read(self):
334 """Load histedit state from disk and set fields appropriately."""
334 """Load histedit state from disk and set fields appropriately."""
335 if not self.stateobj.exists():
335 if not self.stateobj.exists():
336 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
336 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
337
337
338 data = self._read()
338 data = self._read()
339
339
340 self.parentctxnode = data['parentctxnode']
340 self.parentctxnode = data['parentctxnode']
341 actions = parserules(data['rules'], self)
341 actions = parserules(data['rules'], self)
342 self.actions = actions
342 self.actions = actions
343 self.keep = data['keep']
343 self.keep = data['keep']
344 self.topmost = data['topmost']
344 self.topmost = data['topmost']
345 self.replacements = data['replacements']
345 self.replacements = data['replacements']
346 self.backupfile = data['backupfile']
346 self.backupfile = data['backupfile']
347
347
348 def _read(self):
348 def _read(self):
349 fp = self.repo.vfs.read('histedit-state')
349 fp = self.repo.vfs.read('histedit-state')
350 if fp.startswith('v1\n'):
350 if fp.startswith('v1\n'):
351 data = self._load()
351 data = self._load()
352 parentctxnode, rules, keep, topmost, replacements, backupfile = data
352 parentctxnode, rules, keep, topmost, replacements, backupfile = data
353 else:
353 else:
354 data = pickle.loads(fp)
354 data = pickle.loads(fp)
355 parentctxnode, rules, keep, topmost, replacements = data
355 parentctxnode, rules, keep, topmost, replacements = data
356 backupfile = None
356 backupfile = None
357 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
357 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
358
358
359 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
359 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
360 "topmost": topmost, "replacements": replacements,
360 "topmost": topmost, "replacements": replacements,
361 "backupfile": backupfile}
361 "backupfile": backupfile}
362
362
363 def write(self, tr=None):
363 def write(self, tr=None):
364 if tr:
364 if tr:
365 tr.addfilegenerator('histedit-state', ('histedit-state',),
365 tr.addfilegenerator('histedit-state', ('histedit-state',),
366 self._write, location='plain')
366 self._write, location='plain')
367 else:
367 else:
368 with self.repo.vfs("histedit-state", "w") as f:
368 with self.repo.vfs("histedit-state", "w") as f:
369 self._write(f)
369 self._write(f)
370
370
371 def _write(self, fp):
371 def _write(self, fp):
372 fp.write('v1\n')
372 fp.write('v1\n')
373 fp.write('%s\n' % node.hex(self.parentctxnode))
373 fp.write('%s\n' % node.hex(self.parentctxnode))
374 fp.write('%s\n' % node.hex(self.topmost))
374 fp.write('%s\n' % node.hex(self.topmost))
375 fp.write('%s\n' % ('True' if self.keep else 'False'))
375 fp.write('%s\n' % ('True' if self.keep else 'False'))
376 fp.write('%d\n' % len(self.actions))
376 fp.write('%d\n' % len(self.actions))
377 for action in self.actions:
377 for action in self.actions:
378 fp.write('%s\n' % action.tostate())
378 fp.write('%s\n' % action.tostate())
379 fp.write('%d\n' % len(self.replacements))
379 fp.write('%d\n' % len(self.replacements))
380 for replacement in self.replacements:
380 for replacement in self.replacements:
381 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
381 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
382 for r in replacement[1])))
382 for r in replacement[1])))
383 backupfile = self.backupfile
383 backupfile = self.backupfile
384 if not backupfile:
384 if not backupfile:
385 backupfile = ''
385 backupfile = ''
386 fp.write('%s\n' % backupfile)
386 fp.write('%s\n' % backupfile)
387
387
388 def _load(self):
388 def _load(self):
389 fp = self.repo.vfs('histedit-state', 'r')
389 fp = self.repo.vfs('histedit-state', 'r')
390 lines = [l[:-1] for l in fp.readlines()]
390 lines = [l[:-1] for l in fp.readlines()]
391
391
392 index = 0
392 index = 0
393 lines[index] # version number
393 lines[index] # version number
394 index += 1
394 index += 1
395
395
396 parentctxnode = node.bin(lines[index])
396 parentctxnode = node.bin(lines[index])
397 index += 1
397 index += 1
398
398
399 topmost = node.bin(lines[index])
399 topmost = node.bin(lines[index])
400 index += 1
400 index += 1
401
401
402 keep = lines[index] == 'True'
402 keep = lines[index] == 'True'
403 index += 1
403 index += 1
404
404
405 # Rules
405 # Rules
406 rules = []
406 rules = []
407 rulelen = int(lines[index])
407 rulelen = int(lines[index])
408 index += 1
408 index += 1
409 for i in pycompat.xrange(rulelen):
409 for i in pycompat.xrange(rulelen):
410 ruleaction = lines[index]
410 ruleaction = lines[index]
411 index += 1
411 index += 1
412 rule = lines[index]
412 rule = lines[index]
413 index += 1
413 index += 1
414 rules.append((ruleaction, rule))
414 rules.append((ruleaction, rule))
415
415
416 # Replacements
416 # Replacements
417 replacements = []
417 replacements = []
418 replacementlen = int(lines[index])
418 replacementlen = int(lines[index])
419 index += 1
419 index += 1
420 for i in pycompat.xrange(replacementlen):
420 for i in pycompat.xrange(replacementlen):
421 replacement = lines[index]
421 replacement = lines[index]
422 original = node.bin(replacement[:40])
422 original = node.bin(replacement[:40])
423 succ = [node.bin(replacement[i:i + 40]) for i in
423 succ = [node.bin(replacement[i:i + 40]) for i in
424 range(40, len(replacement), 40)]
424 range(40, len(replacement), 40)]
425 replacements.append((original, succ))
425 replacements.append((original, succ))
426 index += 1
426 index += 1
427
427
428 backupfile = lines[index]
428 backupfile = lines[index]
429 index += 1
429 index += 1
430
430
431 fp.close()
431 fp.close()
432
432
433 return parentctxnode, rules, keep, topmost, replacements, backupfile
433 return parentctxnode, rules, keep, topmost, replacements, backupfile
434
434
435 def clear(self):
435 def clear(self):
436 if self.inprogress():
436 if self.inprogress():
437 self.repo.vfs.unlink('histedit-state')
437 self.repo.vfs.unlink('histedit-state')
438
438
439 def inprogress(self):
439 def inprogress(self):
440 return self.repo.vfs.exists('histedit-state')
440 return self.repo.vfs.exists('histedit-state')
441
441
442
442
443 class histeditaction(object):
443 class histeditaction(object):
444 def __init__(self, state, node):
444 def __init__(self, state, node):
445 self.state = state
445 self.state = state
446 self.repo = state.repo
446 self.repo = state.repo
447 self.node = node
447 self.node = node
448
448
449 @classmethod
449 @classmethod
450 def fromrule(cls, state, rule):
450 def fromrule(cls, state, rule):
451 """Parses the given rule, returning an instance of the histeditaction.
451 """Parses the given rule, returning an instance of the histeditaction.
452 """
452 """
453 ruleid = rule.strip().split(' ', 1)[0]
453 ruleid = rule.strip().split(' ', 1)[0]
454 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
454 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
455 # Check for validation of rule ids and get the rulehash
455 # Check for validation of rule ids and get the rulehash
456 try:
456 try:
457 rev = node.bin(ruleid)
457 rev = node.bin(ruleid)
458 except TypeError:
458 except TypeError:
459 try:
459 try:
460 _ctx = scmutil.revsingle(state.repo, ruleid)
460 _ctx = scmutil.revsingle(state.repo, ruleid)
461 rulehash = _ctx.hex()
461 rulehash = _ctx.hex()
462 rev = node.bin(rulehash)
462 rev = node.bin(rulehash)
463 except error.RepoLookupError:
463 except error.RepoLookupError:
464 raise error.ParseError(_("invalid changeset %s") % ruleid)
464 raise error.ParseError(_("invalid changeset %s") % ruleid)
465 return cls(state, rev)
465 return cls(state, rev)
466
466
467 def verify(self, prev, expected, seen):
467 def verify(self, prev, expected, seen):
468 """ Verifies semantic correctness of the rule"""
468 """ Verifies semantic correctness of the rule"""
469 repo = self.repo
469 repo = self.repo
470 ha = node.hex(self.node)
470 ha = node.hex(self.node)
471 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
471 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
472 if self.node is None:
472 if self.node is None:
473 raise error.ParseError(_('unknown changeset %s listed') % ha[:12])
473 raise error.ParseError(_('unknown changeset %s listed') % ha[:12])
474 self._verifynodeconstraints(prev, expected, seen)
474 self._verifynodeconstraints(prev, expected, seen)
475
475
476 def _verifynodeconstraints(self, prev, expected, seen):
476 def _verifynodeconstraints(self, prev, expected, seen):
477 # by default command need a node in the edited list
477 # by default command need a node in the edited list
478 if self.node not in expected:
478 if self.node not in expected:
479 raise error.ParseError(_('%s "%s" changeset was not a candidate')
479 raise error.ParseError(_('%s "%s" changeset was not a candidate')
480 % (self.verb, node.short(self.node)),
480 % (self.verb, node.short(self.node)),
481 hint=_('only use listed changesets'))
481 hint=_('only use listed changesets'))
482 # and only one command per node
482 # and only one command per node
483 if self.node in seen:
483 if self.node in seen:
484 raise error.ParseError(_('duplicated command for changeset %s') %
484 raise error.ParseError(_('duplicated command for changeset %s') %
485 node.short(self.node))
485 node.short(self.node))
486
486
487 def torule(self):
487 def torule(self):
488 """build a histedit rule line for an action
488 """build a histedit rule line for an action
489
489
490 by default lines are in the form:
490 by default lines are in the form:
491 <hash> <rev> <summary>
491 <hash> <rev> <summary>
492 """
492 """
493 ctx = self.repo[self.node]
493 ctx = self.repo[self.node]
494 ui = self.repo.ui
494 ui = self.repo.ui
495 summary = cmdutil.rendertemplate(
495 summary = cmdutil.rendertemplate(
496 ctx, ui.config('histedit', 'summary-template')) or ''
496 ctx, ui.config('histedit', 'summary-template')) or ''
497 summary = summary.splitlines()[0]
497 summary = summary.splitlines()[0]
498 line = '%s %s %s' % (self.verb, ctx, summary)
498 line = '%s %s %s' % (self.verb, ctx, summary)
499 # trim to 75 columns by default so it's not stupidly wide in my editor
499 # trim to 75 columns by default so it's not stupidly wide in my editor
500 # (the 5 more are left for verb)
500 # (the 5 more are left for verb)
501 maxlen = self.repo.ui.configint('histedit', 'linelen')
501 maxlen = self.repo.ui.configint('histedit', 'linelen')
502 maxlen = max(maxlen, 22) # avoid truncating hash
502 maxlen = max(maxlen, 22) # avoid truncating hash
503 return stringutil.ellipsis(line, maxlen)
503 return stringutil.ellipsis(line, maxlen)
504
504
505 def tostate(self):
505 def tostate(self):
506 """Print an action in format used by histedit state files
506 """Print an action in format used by histedit state files
507 (the first line is a verb, the remainder is the second)
507 (the first line is a verb, the remainder is the second)
508 """
508 """
509 return "%s\n%s" % (self.verb, node.hex(self.node))
509 return "%s\n%s" % (self.verb, node.hex(self.node))
510
510
511 def run(self):
511 def run(self):
512 """Runs the action. The default behavior is simply apply the action's
512 """Runs the action. The default behavior is simply apply the action's
513 rulectx onto the current parentctx."""
513 rulectx onto the current parentctx."""
514 self.applychange()
514 self.applychange()
515 self.continuedirty()
515 self.continuedirty()
516 return self.continueclean()
516 return self.continueclean()
517
517
518 def applychange(self):
518 def applychange(self):
519 """Applies the changes from this action's rulectx onto the current
519 """Applies the changes from this action's rulectx onto the current
520 parentctx, but does not commit them."""
520 parentctx, but does not commit them."""
521 repo = self.repo
521 repo = self.repo
522 rulectx = repo[self.node]
522 rulectx = repo[self.node]
523 repo.ui.pushbuffer(error=True, labeled=True)
523 repo.ui.pushbuffer(error=True, labeled=True)
524 hg.update(repo, self.state.parentctxnode, quietempty=True)
524 hg.update(repo, self.state.parentctxnode, quietempty=True)
525 stats = applychanges(repo.ui, repo, rulectx, {})
525 stats = applychanges(repo.ui, repo, rulectx, {})
526 repo.dirstate.setbranch(rulectx.branch())
526 repo.dirstate.setbranch(rulectx.branch())
527 if stats.unresolvedcount:
527 if stats.unresolvedcount:
528 buf = repo.ui.popbuffer()
528 buf = repo.ui.popbuffer()
529 repo.ui.write(buf)
529 repo.ui.write(buf)
530 raise error.InterventionRequired(
530 raise error.InterventionRequired(
531 _('Fix up the change (%s %s)') %
531 _('Fix up the change (%s %s)') %
532 (self.verb, node.short(self.node)),
532 (self.verb, node.short(self.node)),
533 hint=_('hg histedit --continue to resume'))
533 hint=_('hg histedit --continue to resume'))
534 else:
534 else:
535 repo.ui.popbuffer()
535 repo.ui.popbuffer()
536
536
537 def continuedirty(self):
537 def continuedirty(self):
538 """Continues the action when changes have been applied to the working
538 """Continues the action when changes have been applied to the working
539 copy. The default behavior is to commit the dirty changes."""
539 copy. The default behavior is to commit the dirty changes."""
540 repo = self.repo
540 repo = self.repo
541 rulectx = repo[self.node]
541 rulectx = repo[self.node]
542
542
543 editor = self.commiteditor()
543 editor = self.commiteditor()
544 commit = commitfuncfor(repo, rulectx)
544 commit = commitfuncfor(repo, rulectx)
545 if repo.ui.configbool('rewrite', 'update-timestamp'):
545 if repo.ui.configbool('rewrite', 'update-timestamp'):
546 date = dateutil.makedate()
546 date = dateutil.makedate()
547 else:
547 else:
548 date = rulectx.date()
548 date = rulectx.date()
549 commit(text=rulectx.description(), user=rulectx.user(),
549 commit(text=rulectx.description(), user=rulectx.user(),
550 date=date, extra=rulectx.extra(), editor=editor)
550 date=date, extra=rulectx.extra(), editor=editor)
551
551
552 def commiteditor(self):
552 def commiteditor(self):
553 """The editor to be used to edit the commit message."""
553 """The editor to be used to edit the commit message."""
554 return False
554 return False
555
555
556 def continueclean(self):
556 def continueclean(self):
557 """Continues the action when the working copy is clean. The default
557 """Continues the action when the working copy is clean. The default
558 behavior is to accept the current commit as the new version of the
558 behavior is to accept the current commit as the new version of the
559 rulectx."""
559 rulectx."""
560 ctx = self.repo['.']
560 ctx = self.repo['.']
561 if ctx.node() == self.state.parentctxnode:
561 if ctx.node() == self.state.parentctxnode:
562 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
562 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
563 node.short(self.node))
563 node.short(self.node))
564 return ctx, [(self.node, tuple())]
564 return ctx, [(self.node, tuple())]
565 if ctx.node() == self.node:
565 if ctx.node() == self.node:
566 # Nothing changed
566 # Nothing changed
567 return ctx, []
567 return ctx, []
568 return ctx, [(self.node, (ctx.node(),))]
568 return ctx, [(self.node, (ctx.node(),))]
569
569
570 def commitfuncfor(repo, src):
570 def commitfuncfor(repo, src):
571 """Build a commit function for the replacement of <src>
571 """Build a commit function for the replacement of <src>
572
572
573 This function ensure we apply the same treatment to all changesets.
573 This function ensure we apply the same treatment to all changesets.
574
574
575 - Add a 'histedit_source' entry in extra.
575 - Add a 'histedit_source' entry in extra.
576
576
577 Note that fold has its own separated logic because its handling is a bit
577 Note that fold has its own separated logic because its handling is a bit
578 different and not easily factored out of the fold method.
578 different and not easily factored out of the fold method.
579 """
579 """
580 phasemin = src.phase()
580 phasemin = src.phase()
581 def commitfunc(**kwargs):
581 def commitfunc(**kwargs):
582 overrides = {('phases', 'new-commit'): phasemin}
582 overrides = {('phases', 'new-commit'): phasemin}
583 with repo.ui.configoverride(overrides, 'histedit'):
583 with repo.ui.configoverride(overrides, 'histedit'):
584 extra = kwargs.get(r'extra', {}).copy()
584 extra = kwargs.get(r'extra', {}).copy()
585 extra['histedit_source'] = src.hex()
585 extra['histedit_source'] = src.hex()
586 kwargs[r'extra'] = extra
586 kwargs[r'extra'] = extra
587 return repo.commit(**kwargs)
587 return repo.commit(**kwargs)
588 return commitfunc
588 return commitfunc
589
589
590 def applychanges(ui, repo, ctx, opts):
590 def applychanges(ui, repo, ctx, opts):
591 """Merge changeset from ctx (only) in the current working directory"""
591 """Merge changeset from ctx (only) in the current working directory"""
592 wcpar = repo.dirstate.p1()
592 wcpar = repo.dirstate.p1()
593 if ctx.p1().node() == wcpar:
593 if ctx.p1().node() == wcpar:
594 # edits are "in place" we do not need to make any merge,
594 # edits are "in place" we do not need to make any merge,
595 # just applies changes on parent for editing
595 # just applies changes on parent for editing
596 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
596 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
597 stats = mergemod.updateresult(0, 0, 0, 0)
597 stats = mergemod.updateresult(0, 0, 0, 0)
598 else:
598 else:
599 try:
599 try:
600 # ui.forcemerge is an internal variable, do not document
600 # ui.forcemerge is an internal variable, do not document
601 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
601 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
602 'histedit')
602 'histedit')
603 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
603 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
604 finally:
604 finally:
605 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
605 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
606 return stats
606 return stats
607
607
608 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
608 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
609 """collapse the set of revisions from first to last as new one.
609 """collapse the set of revisions from first to last as new one.
610
610
611 Expected commit options are:
611 Expected commit options are:
612 - message
612 - message
613 - date
613 - date
614 - username
614 - username
615 Commit message is edited in all cases.
615 Commit message is edited in all cases.
616
616
617 This function works in memory."""
617 This function works in memory."""
618 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
618 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
619 if not ctxs:
619 if not ctxs:
620 return None
620 return None
621 for c in ctxs:
621 for c in ctxs:
622 if not c.mutable():
622 if not c.mutable():
623 raise error.ParseError(
623 raise error.ParseError(
624 _("cannot fold into public change %s") % node.short(c.node()))
624 _("cannot fold into public change %s") % node.short(c.node()))
625 base = firstctx.p1()
625 base = firstctx.p1()
626
626
627 # commit a new version of the old changeset, including the update
627 # commit a new version of the old changeset, including the update
628 # collect all files which might be affected
628 # collect all files which might be affected
629 files = set()
629 files = set()
630 for ctx in ctxs:
630 for ctx in ctxs:
631 files.update(ctx.files())
631 files.update(ctx.files())
632
632
633 # Recompute copies (avoid recording a -> b -> a)
633 # Recompute copies (avoid recording a -> b -> a)
634 copied = copies.pathcopies(base, lastctx)
634 copied = copies.pathcopies(base, lastctx)
635
635
636 # prune files which were reverted by the updates
636 # prune files which were reverted by the updates
637 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
637 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
638 # commit version of these files as defined by head
638 # commit version of these files as defined by head
639 headmf = lastctx.manifest()
639 headmf = lastctx.manifest()
640 def filectxfn(repo, ctx, path):
640 def filectxfn(repo, ctx, path):
641 if path in headmf:
641 if path in headmf:
642 fctx = lastctx[path]
642 fctx = lastctx[path]
643 flags = fctx.flags()
643 flags = fctx.flags()
644 mctx = context.memfilectx(repo, ctx,
644 mctx = context.memfilectx(repo, ctx,
645 fctx.path(), fctx.data(),
645 fctx.path(), fctx.data(),
646 islink='l' in flags,
646 islink='l' in flags,
647 isexec='x' in flags,
647 isexec='x' in flags,
648 copysource=copied.get(path))
648 copysource=copied.get(path))
649 return mctx
649 return mctx
650 return None
650 return None
651
651
652 if commitopts.get('message'):
652 if commitopts.get('message'):
653 message = commitopts['message']
653 message = commitopts['message']
654 else:
654 else:
655 message = firstctx.description()
655 message = firstctx.description()
656 user = commitopts.get('user')
656 user = commitopts.get('user')
657 date = commitopts.get('date')
657 date = commitopts.get('date')
658 extra = commitopts.get('extra')
658 extra = commitopts.get('extra')
659
659
660 parents = (firstctx.p1().node(), firstctx.p2().node())
660 parents = (firstctx.p1().node(), firstctx.p2().node())
661 editor = None
661 editor = None
662 if not skipprompt:
662 if not skipprompt:
663 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
663 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
664 new = context.memctx(repo,
664 new = context.memctx(repo,
665 parents=parents,
665 parents=parents,
666 text=message,
666 text=message,
667 files=files,
667 files=files,
668 filectxfn=filectxfn,
668 filectxfn=filectxfn,
669 user=user,
669 user=user,
670 date=date,
670 date=date,
671 extra=extra,
671 extra=extra,
672 editor=editor)
672 editor=editor)
673 return repo.commitctx(new)
673 return repo.commitctx(new)
674
674
675 def _isdirtywc(repo):
675 def _isdirtywc(repo):
676 return repo[None].dirty(missing=True)
676 return repo[None].dirty(missing=True)
677
677
678 def abortdirty():
678 def abortdirty():
679 raise error.Abort(_('working copy has pending changes'),
679 raise error.Abort(_('working copy has pending changes'),
680 hint=_('amend, commit, or revert them and run histedit '
680 hint=_('amend, commit, or revert them and run histedit '
681 '--continue, or abort with histedit --abort'))
681 '--continue, or abort with histedit --abort'))
682
682
683 def action(verbs, message, priority=False, internal=False):
683 def action(verbs, message, priority=False, internal=False):
684 def wrap(cls):
684 def wrap(cls):
685 assert not priority or not internal
685 assert not priority or not internal
686 verb = verbs[0]
686 verb = verbs[0]
687 if priority:
687 if priority:
688 primaryactions.add(verb)
688 primaryactions.add(verb)
689 elif internal:
689 elif internal:
690 internalactions.add(verb)
690 internalactions.add(verb)
691 elif len(verbs) > 1:
691 elif len(verbs) > 1:
692 secondaryactions.add(verb)
692 secondaryactions.add(verb)
693 else:
693 else:
694 tertiaryactions.add(verb)
694 tertiaryactions.add(verb)
695
695
696 cls.verb = verb
696 cls.verb = verb
697 cls.verbs = verbs
697 cls.verbs = verbs
698 cls.message = message
698 cls.message = message
699 for verb in verbs:
699 for verb in verbs:
700 actiontable[verb] = cls
700 actiontable[verb] = cls
701 return cls
701 return cls
702 return wrap
702 return wrap
703
703
704 @action(['pick', 'p'],
704 @action(['pick', 'p'],
705 _('use commit'),
705 _('use commit'),
706 priority=True)
706 priority=True)
707 class pick(histeditaction):
707 class pick(histeditaction):
708 def run(self):
708 def run(self):
709 rulectx = self.repo[self.node]
709 rulectx = self.repo[self.node]
710 if rulectx.p1().node() == self.state.parentctxnode:
710 if rulectx.p1().node() == self.state.parentctxnode:
711 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
711 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
712 return rulectx, []
712 return rulectx, []
713
713
714 return super(pick, self).run()
714 return super(pick, self).run()
715
715
716 @action(['edit', 'e'],
716 @action(['edit', 'e'],
717 _('use commit, but stop for amending'),
717 _('use commit, but stop for amending'),
718 priority=True)
718 priority=True)
719 class edit(histeditaction):
719 class edit(histeditaction):
720 def run(self):
720 def run(self):
721 repo = self.repo
721 repo = self.repo
722 rulectx = repo[self.node]
722 rulectx = repo[self.node]
723 hg.update(repo, self.state.parentctxnode, quietempty=True)
723 hg.update(repo, self.state.parentctxnode, quietempty=True)
724 applychanges(repo.ui, repo, rulectx, {})
724 applychanges(repo.ui, repo, rulectx, {})
725 raise error.InterventionRequired(
725 raise error.InterventionRequired(
726 _('Editing (%s), you may commit or record as needed now.')
726 _('Editing (%s), you may commit or record as needed now.')
727 % node.short(self.node),
727 % node.short(self.node),
728 hint=_('hg histedit --continue to resume'))
728 hint=_('hg histedit --continue to resume'))
729
729
730 def commiteditor(self):
730 def commiteditor(self):
731 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
731 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
732
732
733 @action(['fold', 'f'],
733 @action(['fold', 'f'],
734 _('use commit, but combine it with the one above'))
734 _('use commit, but combine it with the one above'))
735 class fold(histeditaction):
735 class fold(histeditaction):
736 def verify(self, prev, expected, seen):
736 def verify(self, prev, expected, seen):
737 """ Verifies semantic correctness of the fold rule"""
737 """ Verifies semantic correctness of the fold rule"""
738 super(fold, self).verify(prev, expected, seen)
738 super(fold, self).verify(prev, expected, seen)
739 repo = self.repo
739 repo = self.repo
740 if not prev:
740 if not prev:
741 c = repo[self.node].p1()
741 c = repo[self.node].p1()
742 elif not prev.verb in ('pick', 'base'):
742 elif not prev.verb in ('pick', 'base'):
743 return
743 return
744 else:
744 else:
745 c = repo[prev.node]
745 c = repo[prev.node]
746 if not c.mutable():
746 if not c.mutable():
747 raise error.ParseError(
747 raise error.ParseError(
748 _("cannot fold into public change %s") % node.short(c.node()))
748 _("cannot fold into public change %s") % node.short(c.node()))
749
749
750
750
751 def continuedirty(self):
751 def continuedirty(self):
752 repo = self.repo
752 repo = self.repo
753 rulectx = repo[self.node]
753 rulectx = repo[self.node]
754
754
755 commit = commitfuncfor(repo, rulectx)
755 commit = commitfuncfor(repo, rulectx)
756 commit(text='fold-temp-revision %s' % node.short(self.node),
756 commit(text='fold-temp-revision %s' % node.short(self.node),
757 user=rulectx.user(), date=rulectx.date(),
757 user=rulectx.user(), date=rulectx.date(),
758 extra=rulectx.extra())
758 extra=rulectx.extra())
759
759
760 def continueclean(self):
760 def continueclean(self):
761 repo = self.repo
761 repo = self.repo
762 ctx = repo['.']
762 ctx = repo['.']
763 rulectx = repo[self.node]
763 rulectx = repo[self.node]
764 parentctxnode = self.state.parentctxnode
764 parentctxnode = self.state.parentctxnode
765 if ctx.node() == parentctxnode:
765 if ctx.node() == parentctxnode:
766 repo.ui.warn(_('%s: empty changeset\n') %
766 repo.ui.warn(_('%s: empty changeset\n') %
767 node.short(self.node))
767 node.short(self.node))
768 return ctx, [(self.node, (parentctxnode,))]
768 return ctx, [(self.node, (parentctxnode,))]
769
769
770 parentctx = repo[parentctxnode]
770 parentctx = repo[parentctxnode]
771 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
771 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
772 parentctx.rev(),
772 parentctx.rev(),
773 parentctx.rev()))
773 parentctx.rev()))
774 if not newcommits:
774 if not newcommits:
775 repo.ui.warn(_('%s: cannot fold - working copy is not a '
775 repo.ui.warn(_('%s: cannot fold - working copy is not a '
776 'descendant of previous commit %s\n') %
776 'descendant of previous commit %s\n') %
777 (node.short(self.node), node.short(parentctxnode)))
777 (node.short(self.node), node.short(parentctxnode)))
778 return ctx, [(self.node, (ctx.node(),))]
778 return ctx, [(self.node, (ctx.node(),))]
779
779
780 middlecommits = newcommits.copy()
780 middlecommits = newcommits.copy()
781 middlecommits.discard(ctx.node())
781 middlecommits.discard(ctx.node())
782
782
783 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
783 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
784 middlecommits)
784 middlecommits)
785
785
786 def skipprompt(self):
786 def skipprompt(self):
787 """Returns true if the rule should skip the message editor.
787 """Returns true if the rule should skip the message editor.
788
788
789 For example, 'fold' wants to show an editor, but 'rollup'
789 For example, 'fold' wants to show an editor, but 'rollup'
790 doesn't want to.
790 doesn't want to.
791 """
791 """
792 return False
792 return False
793
793
794 def mergedescs(self):
794 def mergedescs(self):
795 """Returns true if the rule should merge messages of multiple changes.
795 """Returns true if the rule should merge messages of multiple changes.
796
796
797 This exists mainly so that 'rollup' rules can be a subclass of
797 This exists mainly so that 'rollup' rules can be a subclass of
798 'fold'.
798 'fold'.
799 """
799 """
800 return True
800 return True
801
801
802 def firstdate(self):
802 def firstdate(self):
803 """Returns true if the rule should preserve the date of the first
803 """Returns true if the rule should preserve the date of the first
804 change.
804 change.
805
805
806 This exists mainly so that 'rollup' rules can be a subclass of
806 This exists mainly so that 'rollup' rules can be a subclass of
807 'fold'.
807 'fold'.
808 """
808 """
809 return False
809 return False
810
810
811 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
811 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
812 parent = ctx.p1().node()
812 parent = ctx.p1().node()
813 hg.updaterepo(repo, parent, overwrite=False)
813 hg.updaterepo(repo, parent, overwrite=False)
814 ### prepare new commit data
814 ### prepare new commit data
815 commitopts = {}
815 commitopts = {}
816 commitopts['user'] = ctx.user()
816 commitopts['user'] = ctx.user()
817 # commit message
817 # commit message
818 if not self.mergedescs():
818 if not self.mergedescs():
819 newmessage = ctx.description()
819 newmessage = ctx.description()
820 else:
820 else:
821 newmessage = '\n***\n'.join(
821 newmessage = '\n***\n'.join(
822 [ctx.description()] +
822 [ctx.description()] +
823 [repo[r].description() for r in internalchanges] +
823 [repo[r].description() for r in internalchanges] +
824 [oldctx.description()]) + '\n'
824 [oldctx.description()]) + '\n'
825 commitopts['message'] = newmessage
825 commitopts['message'] = newmessage
826 # date
826 # date
827 if self.firstdate():
827 if self.firstdate():
828 commitopts['date'] = ctx.date()
828 commitopts['date'] = ctx.date()
829 else:
829 else:
830 commitopts['date'] = max(ctx.date(), oldctx.date())
830 commitopts['date'] = max(ctx.date(), oldctx.date())
831 # if date is to be updated to current
831 # if date is to be updated to current
832 if ui.configbool('rewrite', 'update-timestamp'):
832 if ui.configbool('rewrite', 'update-timestamp'):
833 commitopts['date'] = dateutil.makedate()
833 commitopts['date'] = dateutil.makedate()
834
834
835 extra = ctx.extra().copy()
835 extra = ctx.extra().copy()
836 # histedit_source
836 # histedit_source
837 # note: ctx is likely a temporary commit but that the best we can do
837 # note: ctx is likely a temporary commit but that the best we can do
838 # here. This is sufficient to solve issue3681 anyway.
838 # here. This is sufficient to solve issue3681 anyway.
839 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
839 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
840 commitopts['extra'] = extra
840 commitopts['extra'] = extra
841 phasemin = max(ctx.phase(), oldctx.phase())
841 phasemin = max(ctx.phase(), oldctx.phase())
842 overrides = {('phases', 'new-commit'): phasemin}
842 overrides = {('phases', 'new-commit'): phasemin}
843 with repo.ui.configoverride(overrides, 'histedit'):
843 with repo.ui.configoverride(overrides, 'histedit'):
844 n = collapse(repo, ctx, repo[newnode], commitopts,
844 n = collapse(repo, ctx, repo[newnode], commitopts,
845 skipprompt=self.skipprompt())
845 skipprompt=self.skipprompt())
846 if n is None:
846 if n is None:
847 return ctx, []
847 return ctx, []
848 hg.updaterepo(repo, n, overwrite=False)
848 hg.updaterepo(repo, n, overwrite=False)
849 replacements = [(oldctx.node(), (newnode,)),
849 replacements = [(oldctx.node(), (newnode,)),
850 (ctx.node(), (n,)),
850 (ctx.node(), (n,)),
851 (newnode, (n,)),
851 (newnode, (n,)),
852 ]
852 ]
853 for ich in internalchanges:
853 for ich in internalchanges:
854 replacements.append((ich, (n,)))
854 replacements.append((ich, (n,)))
855 return repo[n], replacements
855 return repo[n], replacements
856
856
857 @action(['base', 'b'],
857 @action(['base', 'b'],
858 _('checkout changeset and apply further changesets from there'))
858 _('checkout changeset and apply further changesets from there'))
859 class base(histeditaction):
859 class base(histeditaction):
860
860
861 def run(self):
861 def run(self):
862 if self.repo['.'].node() != self.node:
862 if self.repo['.'].node() != self.node:
863 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
863 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
864 return self.continueclean()
864 return self.continueclean()
865
865
866 def continuedirty(self):
866 def continuedirty(self):
867 abortdirty()
867 abortdirty()
868
868
869 def continueclean(self):
869 def continueclean(self):
870 basectx = self.repo['.']
870 basectx = self.repo['.']
871 return basectx, []
871 return basectx, []
872
872
873 def _verifynodeconstraints(self, prev, expected, seen):
873 def _verifynodeconstraints(self, prev, expected, seen):
874 # base can only be use with a node not in the edited set
874 # base can only be use with a node not in the edited set
875 if self.node in expected:
875 if self.node in expected:
876 msg = _('%s "%s" changeset was an edited list candidate')
876 msg = _('%s "%s" changeset was an edited list candidate')
877 raise error.ParseError(
877 raise error.ParseError(
878 msg % (self.verb, node.short(self.node)),
878 msg % (self.verb, node.short(self.node)),
879 hint=_('base must only use unlisted changesets'))
879 hint=_('base must only use unlisted changesets'))
880
880
881 @action(['_multifold'],
881 @action(['_multifold'],
882 _(
882 _(
883 """fold subclass used for when multiple folds happen in a row
883 """fold subclass used for when multiple folds happen in a row
884
884
885 We only want to fire the editor for the folded message once when
885 We only want to fire the editor for the folded message once when
886 (say) four changes are folded down into a single change. This is
886 (say) four changes are folded down into a single change. This is
887 similar to rollup, but we should preserve both messages so that
887 similar to rollup, but we should preserve both messages so that
888 when the last fold operation runs we can show the user all the
888 when the last fold operation runs we can show the user all the
889 commit messages in their editor.
889 commit messages in their editor.
890 """),
890 """),
891 internal=True)
891 internal=True)
892 class _multifold(fold):
892 class _multifold(fold):
893 def skipprompt(self):
893 def skipprompt(self):
894 return True
894 return True
895
895
896 @action(["roll", "r"],
896 @action(["roll", "r"],
897 _("like fold, but discard this commit's description and date"))
897 _("like fold, but discard this commit's description and date"))
898 class rollup(fold):
898 class rollup(fold):
899 def mergedescs(self):
899 def mergedescs(self):
900 return False
900 return False
901
901
902 def skipprompt(self):
902 def skipprompt(self):
903 return True
903 return True
904
904
905 def firstdate(self):
905 def firstdate(self):
906 return True
906 return True
907
907
908 @action(["drop", "d"],
908 @action(["drop", "d"],
909 _('remove commit from history'))
909 _('remove commit from history'))
910 class drop(histeditaction):
910 class drop(histeditaction):
911 def run(self):
911 def run(self):
912 parentctx = self.repo[self.state.parentctxnode]
912 parentctx = self.repo[self.state.parentctxnode]
913 return parentctx, [(self.node, tuple())]
913 return parentctx, [(self.node, tuple())]
914
914
915 @action(["mess", "m"],
915 @action(["mess", "m"],
916 _('edit commit message without changing commit content'),
916 _('edit commit message without changing commit content'),
917 priority=True)
917 priority=True)
918 class message(histeditaction):
918 class message(histeditaction):
919 def commiteditor(self):
919 def commiteditor(self):
920 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
920 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
921
921
922 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
922 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
923 """utility function to find the first outgoing changeset
923 """utility function to find the first outgoing changeset
924
924
925 Used by initialization code"""
925 Used by initialization code"""
926 if opts is None:
926 if opts is None:
927 opts = {}
927 opts = {}
928 dest = ui.expandpath(remote or 'default-push', remote or 'default')
928 dest = ui.expandpath(remote or 'default-push', remote or 'default')
929 dest, branches = hg.parseurl(dest, None)[:2]
929 dest, branches = hg.parseurl(dest, None)[:2]
930 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
930 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
931
931
932 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
932 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
933 other = hg.peer(repo, opts, dest)
933 other = hg.peer(repo, opts, dest)
934
934
935 if revs:
935 if revs:
936 revs = [repo.lookup(rev) for rev in revs]
936 revs = [repo.lookup(rev) for rev in revs]
937
937
938 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
938 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
939 if not outgoing.missing:
939 if not outgoing.missing:
940 raise error.Abort(_('no outgoing ancestors'))
940 raise error.Abort(_('no outgoing ancestors'))
941 roots = list(repo.revs("roots(%ln)", outgoing.missing))
941 roots = list(repo.revs("roots(%ln)", outgoing.missing))
942 if len(roots) > 1:
942 if len(roots) > 1:
943 msg = _('there are ambiguous outgoing revisions')
943 msg = _('there are ambiguous outgoing revisions')
944 hint = _("see 'hg help histedit' for more detail")
944 hint = _("see 'hg help histedit' for more detail")
945 raise error.Abort(msg, hint=hint)
945 raise error.Abort(msg, hint=hint)
946 return repo[roots[0]].node()
946 return repo[roots[0]].node()
947
947
948 # Curses Support
948 # Curses Support
949 try:
949 try:
950 import curses
950 import curses
951
951
952 # Curses requires setting the locale or it will default to the C
952 # Curses requires setting the locale or it will default to the C
953 # locale. This sets the locale to the user's default system
953 # locale. This sets the locale to the user's default system
954 # locale.
954 # locale.
955 import locale
955 import locale
956 locale.setlocale(locale.LC_ALL, r'')
956 locale.setlocale(locale.LC_ALL, r'')
957 except ImportError:
957 except ImportError:
958 curses = None
958 curses = None
959
959
960 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
960 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
961 ACTION_LABELS = {
961 ACTION_LABELS = {
962 'fold': '^fold',
962 'fold': '^fold',
963 'roll': '^roll',
963 'roll': '^roll',
964 }
964 }
965
965
966 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
966 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
967
967
968 E_QUIT, E_HISTEDIT = 1, 2
968 E_QUIT, E_HISTEDIT = 1, 2
969 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
969 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
970 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
970 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
971
971
972 KEYTABLE = {
972 KEYTABLE = {
973 'global': {
973 'global': {
974 'h': 'next-action',
974 'h': 'next-action',
975 'KEY_RIGHT': 'next-action',
975 'KEY_RIGHT': 'next-action',
976 'l': 'prev-action',
976 'l': 'prev-action',
977 'KEY_LEFT': 'prev-action',
977 'KEY_LEFT': 'prev-action',
978 'q': 'quit',
978 'q': 'quit',
979 'c': 'histedit',
979 'c': 'histedit',
980 'C': 'histedit',
980 'C': 'histedit',
981 'v': 'showpatch',
981 'v': 'showpatch',
982 '?': 'help',
982 '?': 'help',
983 },
983 },
984 MODE_RULES: {
984 MODE_RULES: {
985 'd': 'action-drop',
985 'd': 'action-drop',
986 'e': 'action-edit',
986 'e': 'action-edit',
987 'f': 'action-fold',
987 'f': 'action-fold',
988 'm': 'action-mess',
988 'm': 'action-mess',
989 'p': 'action-pick',
989 'p': 'action-pick',
990 'r': 'action-roll',
990 'r': 'action-roll',
991 ' ': 'select',
991 ' ': 'select',
992 'j': 'down',
992 'j': 'down',
993 'k': 'up',
993 'k': 'up',
994 'KEY_DOWN': 'down',
994 'KEY_DOWN': 'down',
995 'KEY_UP': 'up',
995 'KEY_UP': 'up',
996 'J': 'move-down',
996 'J': 'move-down',
997 'K': 'move-up',
997 'K': 'move-up',
998 'KEY_NPAGE': 'move-down',
998 'KEY_NPAGE': 'move-down',
999 'KEY_PPAGE': 'move-up',
999 'KEY_PPAGE': 'move-up',
1000 '0': 'goto', # Used for 0..9
1000 '0': 'goto', # Used for 0..9
1001 },
1001 },
1002 MODE_PATCH: {
1002 MODE_PATCH: {
1003 ' ': 'page-down',
1003 ' ': 'page-down',
1004 'KEY_NPAGE': 'page-down',
1004 'KEY_NPAGE': 'page-down',
1005 'KEY_PPAGE': 'page-up',
1005 'KEY_PPAGE': 'page-up',
1006 'j': 'line-down',
1006 'j': 'line-down',
1007 'k': 'line-up',
1007 'k': 'line-up',
1008 'KEY_DOWN': 'line-down',
1008 'KEY_DOWN': 'line-down',
1009 'KEY_UP': 'line-up',
1009 'KEY_UP': 'line-up',
1010 'J': 'down',
1010 'J': 'down',
1011 'K': 'up',
1011 'K': 'up',
1012 },
1012 },
1013 MODE_HELP: {
1013 MODE_HELP: {
1014 },
1014 },
1015 }
1015 }
1016
1016
1017 def screen_size():
1017 def screen_size():
1018 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' '))
1018 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' '))
1019
1019
1020 class histeditrule(object):
1020 class histeditrule(object):
1021 def __init__(self, ctx, pos, action='pick'):
1021 def __init__(self, ctx, pos, action='pick'):
1022 self.ctx = ctx
1022 self.ctx = ctx
1023 self.action = action
1023 self.action = action
1024 self.origpos = pos
1024 self.origpos = pos
1025 self.pos = pos
1025 self.pos = pos
1026 self.conflicts = []
1026 self.conflicts = []
1027
1027
1028 def __str__(self):
1028 def __str__(self):
1029 # Some actions ('fold' and 'roll') combine a patch with a previous one.
1029 # Some actions ('fold' and 'roll') combine a patch with a previous one.
1030 # Add a marker showing which patch they apply to, and also omit the
1030 # Add a marker showing which patch they apply to, and also omit the
1031 # description for 'roll' (since it will get discarded). Example display:
1031 # description for 'roll' (since it will get discarded). Example display:
1032 #
1032 #
1033 # #10 pick 316392:06a16c25c053 add option to skip tests
1033 # #10 pick 316392:06a16c25c053 add option to skip tests
1034 # #11 ^roll 316393:71313c964cc5
1034 # #11 ^roll 316393:71313c964cc5
1035 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1035 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1036 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1036 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1037 #
1037 #
1038 # The carets point to the changeset being folded into ("roll this
1038 # The carets point to the changeset being folded into ("roll this
1039 # changeset into the changeset above").
1039 # changeset into the changeset above").
1040 action = ACTION_LABELS.get(self.action, self.action)
1040 action = ACTION_LABELS.get(self.action, self.action)
1041 h = self.ctx.hex()[0:12]
1041 h = self.ctx.hex()[0:12]
1042 r = self.ctx.rev()
1042 r = self.ctx.rev()
1043 desc = self.ctx.description().splitlines()[0].strip()
1043 desc = self.ctx.description().splitlines()[0].strip()
1044 if self.action == 'roll':
1044 if self.action == 'roll':
1045 desc = ''
1045 desc = ''
1046 return "#{0:<2} {1:<6} {2}:{3} {4}".format(
1046 return "#{0:<2} {1:<6} {2}:{3} {4}".format(
1047 self.origpos, action, r, h, desc)
1047 self.origpos, action, r, h, desc)
1048
1048
1049 def checkconflicts(self, other):
1049 def checkconflicts(self, other):
1050 if other.pos > self.pos and other.origpos <= self.origpos:
1050 if other.pos > self.pos and other.origpos <= self.origpos:
1051 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1051 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1052 self.conflicts.append(other)
1052 self.conflicts.append(other)
1053 return self.conflicts
1053 return self.conflicts
1054
1054
1055 if other in self.conflicts:
1055 if other in self.conflicts:
1056 self.conflicts.remove(other)
1056 self.conflicts.remove(other)
1057 return self.conflicts
1057 return self.conflicts
1058
1058
1059 # ============ EVENTS ===============
1059 # ============ EVENTS ===============
1060 def movecursor(state, oldpos, newpos):
1060 def movecursor(state, oldpos, newpos):
1061 '''Change the rule/changeset that the cursor is pointing to, regardless of
1061 '''Change the rule/changeset that the cursor is pointing to, regardless of
1062 current mode (you can switch between patches from the view patch window).'''
1062 current mode (you can switch between patches from the view patch window).'''
1063 state['pos'] = newpos
1063 state['pos'] = newpos
1064
1064
1065 mode, _ = state['mode']
1065 mode, _ = state['mode']
1066 if mode == MODE_RULES:
1066 if mode == MODE_RULES:
1067 # Scroll through the list by updating the view for MODE_RULES, so that
1067 # Scroll through the list by updating the view for MODE_RULES, so that
1068 # even if we are not currently viewing the rules, switching back will
1068 # even if we are not currently viewing the rules, switching back will
1069 # result in the cursor's rule being visible.
1069 # result in the cursor's rule being visible.
1070 modestate = state['modes'][MODE_RULES]
1070 modestate = state['modes'][MODE_RULES]
1071 if newpos < modestate['line_offset']:
1071 if newpos < modestate['line_offset']:
1072 modestate['line_offset'] = newpos
1072 modestate['line_offset'] = newpos
1073 elif newpos > modestate['line_offset'] + state['page_height'] - 1:
1073 elif newpos > modestate['line_offset'] + state['page_height'] - 1:
1074 modestate['line_offset'] = newpos - state['page_height'] + 1
1074 modestate['line_offset'] = newpos - state['page_height'] + 1
1075
1075
1076 # Reset the patch view region to the top of the new patch.
1076 # Reset the patch view region to the top of the new patch.
1077 state['modes'][MODE_PATCH]['line_offset'] = 0
1077 state['modes'][MODE_PATCH]['line_offset'] = 0
1078
1078
1079 def changemode(state, mode):
1079 def changemode(state, mode):
1080 curmode, _ = state['mode']
1080 curmode, _ = state['mode']
1081 state['mode'] = (mode, curmode)
1081 state['mode'] = (mode, curmode)
1082
1082
1083 def makeselection(state, pos):
1083 def makeselection(state, pos):
1084 state['selected'] = pos
1084 state['selected'] = pos
1085
1085
1086 def swap(state, oldpos, newpos):
1086 def swap(state, oldpos, newpos):
1087 """Swap two positions and calculate necessary conflicts in
1087 """Swap two positions and calculate necessary conflicts in
1088 O(|newpos-oldpos|) time"""
1088 O(|newpos-oldpos|) time"""
1089
1089
1090 rules = state['rules']
1090 rules = state['rules']
1091 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1091 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1092
1092
1093 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1093 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1094
1094
1095 # TODO: swap should not know about histeditrule's internals
1095 # TODO: swap should not know about histeditrule's internals
1096 rules[newpos].pos = newpos
1096 rules[newpos].pos = newpos
1097 rules[oldpos].pos = oldpos
1097 rules[oldpos].pos = oldpos
1098
1098
1099 start = min(oldpos, newpos)
1099 start = min(oldpos, newpos)
1100 end = max(oldpos, newpos)
1100 end = max(oldpos, newpos)
1101 for r in pycompat.xrange(start, end + 1):
1101 for r in pycompat.xrange(start, end + 1):
1102 rules[newpos].checkconflicts(rules[r])
1102 rules[newpos].checkconflicts(rules[r])
1103 rules[oldpos].checkconflicts(rules[r])
1103 rules[oldpos].checkconflicts(rules[r])
1104
1104
1105 if state['selected']:
1105 if state['selected']:
1106 makeselection(state, newpos)
1106 makeselection(state, newpos)
1107
1107
1108 def changeaction(state, pos, action):
1108 def changeaction(state, pos, action):
1109 """Change the action state on the given position to the new action"""
1109 """Change the action state on the given position to the new action"""
1110 rules = state['rules']
1110 rules = state['rules']
1111 assert 0 <= pos < len(rules)
1111 assert 0 <= pos < len(rules)
1112 rules[pos].action = action
1112 rules[pos].action = action
1113
1113
1114 def cycleaction(state, pos, next=False):
1114 def cycleaction(state, pos, next=False):
1115 """Changes the action state the next or the previous action from
1115 """Changes the action state the next or the previous action from
1116 the action list"""
1116 the action list"""
1117 rules = state['rules']
1117 rules = state['rules']
1118 assert 0 <= pos < len(rules)
1118 assert 0 <= pos < len(rules)
1119 current = rules[pos].action
1119 current = rules[pos].action
1120
1120
1121 assert current in KEY_LIST
1121 assert current in KEY_LIST
1122
1122
1123 index = KEY_LIST.index(current)
1123 index = KEY_LIST.index(current)
1124 if next:
1124 if next:
1125 index += 1
1125 index += 1
1126 else:
1126 else:
1127 index -= 1
1127 index -= 1
1128 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1128 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1129
1129
1130 def changeview(state, delta, unit):
1130 def changeview(state, delta, unit):
1131 '''Change the region of whatever is being viewed (a patch or the list of
1131 '''Change the region of whatever is being viewed (a patch or the list of
1132 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1132 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1133 mode, _ = state['mode']
1133 mode, _ = state['mode']
1134 if mode != MODE_PATCH:
1134 if mode != MODE_PATCH:
1135 return
1135 return
1136 mode_state = state['modes'][mode]
1136 mode_state = state['modes'][mode]
1137 num_lines = len(patchcontents(state))
1137 num_lines = len(patchcontents(state))
1138 page_height = state['page_height']
1138 page_height = state['page_height']
1139 unit = page_height if unit == 'page' else 1
1139 unit = page_height if unit == 'page' else 1
1140 num_pages = 1 + (num_lines - 1) / page_height
1140 num_pages = 1 + (num_lines - 1) / page_height
1141 max_offset = (num_pages - 1) * page_height
1141 max_offset = (num_pages - 1) * page_height
1142 newline = mode_state['line_offset'] + delta * unit
1142 newline = mode_state['line_offset'] + delta * unit
1143 mode_state['line_offset'] = max(0, min(max_offset, newline))
1143 mode_state['line_offset'] = max(0, min(max_offset, newline))
1144
1144
1145 def event(state, ch):
1145 def event(state, ch):
1146 """Change state based on the current character input
1146 """Change state based on the current character input
1147
1147
1148 This takes the current state and based on the current character input from
1148 This takes the current state and based on the current character input from
1149 the user we change the state.
1149 the user we change the state.
1150 """
1150 """
1151 selected = state['selected']
1151 selected = state['selected']
1152 oldpos = state['pos']
1152 oldpos = state['pos']
1153 rules = state['rules']
1153 rules = state['rules']
1154
1154
1155 if ch in (curses.KEY_RESIZE, "KEY_RESIZE"):
1155 if ch in (curses.KEY_RESIZE, "KEY_RESIZE"):
1156 return E_RESIZE
1156 return E_RESIZE
1157
1157
1158 lookup_ch = ch
1158 lookup_ch = ch
1159 if '0' <= ch <= '9':
1159 if '0' <= ch <= '9':
1160 lookup_ch = '0'
1160 lookup_ch = '0'
1161
1161
1162 curmode, prevmode = state['mode']
1162 curmode, prevmode = state['mode']
1163 action = KEYTABLE[curmode].get(lookup_ch, KEYTABLE['global'].get(lookup_ch))
1163 action = KEYTABLE[curmode].get(lookup_ch, KEYTABLE['global'].get(lookup_ch))
1164 if action is None:
1164 if action is None:
1165 return
1165 return
1166 if action in ('down', 'move-down'):
1166 if action in ('down', 'move-down'):
1167 newpos = min(oldpos + 1, len(rules) - 1)
1167 newpos = min(oldpos + 1, len(rules) - 1)
1168 movecursor(state, oldpos, newpos)
1168 movecursor(state, oldpos, newpos)
1169 if selected is not None or action == 'move-down':
1169 if selected is not None or action == 'move-down':
1170 swap(state, oldpos, newpos)
1170 swap(state, oldpos, newpos)
1171 elif action in ('up', 'move-up'):
1171 elif action in ('up', 'move-up'):
1172 newpos = max(0, oldpos - 1)
1172 newpos = max(0, oldpos - 1)
1173 movecursor(state, oldpos, newpos)
1173 movecursor(state, oldpos, newpos)
1174 if selected is not None or action == 'move-up':
1174 if selected is not None or action == 'move-up':
1175 swap(state, oldpos, newpos)
1175 swap(state, oldpos, newpos)
1176 elif action == 'next-action':
1176 elif action == 'next-action':
1177 cycleaction(state, oldpos, next=True)
1177 cycleaction(state, oldpos, next=True)
1178 elif action == 'prev-action':
1178 elif action == 'prev-action':
1179 cycleaction(state, oldpos, next=False)
1179 cycleaction(state, oldpos, next=False)
1180 elif action == 'select':
1180 elif action == 'select':
1181 selected = oldpos if selected is None else None
1181 selected = oldpos if selected is None else None
1182 makeselection(state, selected)
1182 makeselection(state, selected)
1183 elif action == 'goto' and int(ch) < len(rules) and len(rules) <= 10:
1183 elif action == 'goto' and int(ch) < len(rules) and len(rules) <= 10:
1184 newrule = next((r for r in rules if r.origpos == int(ch)))
1184 newrule = next((r for r in rules if r.origpos == int(ch)))
1185 movecursor(state, oldpos, newrule.pos)
1185 movecursor(state, oldpos, newrule.pos)
1186 if selected is not None:
1186 if selected is not None:
1187 swap(state, oldpos, newrule.pos)
1187 swap(state, oldpos, newrule.pos)
1188 elif action.startswith('action-'):
1188 elif action.startswith('action-'):
1189 changeaction(state, oldpos, action[7:])
1189 changeaction(state, oldpos, action[7:])
1190 elif action == 'showpatch':
1190 elif action == 'showpatch':
1191 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1191 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1192 elif action == 'help':
1192 elif action == 'help':
1193 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1193 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1194 elif action == 'quit':
1194 elif action == 'quit':
1195 return E_QUIT
1195 return E_QUIT
1196 elif action == 'histedit':
1196 elif action == 'histedit':
1197 return E_HISTEDIT
1197 return E_HISTEDIT
1198 elif action == 'page-down':
1198 elif action == 'page-down':
1199 return E_PAGEDOWN
1199 return E_PAGEDOWN
1200 elif action == 'page-up':
1200 elif action == 'page-up':
1201 return E_PAGEUP
1201 return E_PAGEUP
1202 elif action == 'line-down':
1202 elif action == 'line-down':
1203 return E_LINEDOWN
1203 return E_LINEDOWN
1204 elif action == 'line-up':
1204 elif action == 'line-up':
1205 return E_LINEUP
1205 return E_LINEUP
1206
1206
1207 def makecommands(rules):
1207 def makecommands(rules):
1208 """Returns a list of commands consumable by histedit --commands based on
1208 """Returns a list of commands consumable by histedit --commands based on
1209 our list of rules"""
1209 our list of rules"""
1210 commands = []
1210 commands = []
1211 for rules in rules:
1211 for rules in rules:
1212 commands.append("{0} {1}\n".format(rules.action, rules.ctx))
1212 commands.append("{0} {1}\n".format(rules.action, rules.ctx))
1213 return commands
1213 return commands
1214
1214
1215 def addln(win, y, x, line, color=None):
1215 def addln(win, y, x, line, color=None):
1216 """Add a line to the given window left padding but 100% filled with
1216 """Add a line to the given window left padding but 100% filled with
1217 whitespace characters, so that the color appears on the whole line"""
1217 whitespace characters, so that the color appears on the whole line"""
1218 maxy, maxx = win.getmaxyx()
1218 maxy, maxx = win.getmaxyx()
1219 length = maxx - 1 - x
1219 length = maxx - 1 - x
1220 line = ("{0:<%d}" % length).format(str(line).strip())[:length]
1220 line = ("{0:<%d}" % length).format(str(line).strip())[:length]
1221 if y < 0:
1221 if y < 0:
1222 y = maxy + y
1222 y = maxy + y
1223 if x < 0:
1223 if x < 0:
1224 x = maxx + x
1224 x = maxx + x
1225 if color:
1225 if color:
1226 win.addstr(y, x, line, color)
1226 win.addstr(y, x, line, color)
1227 else:
1227 else:
1228 win.addstr(y, x, line)
1228 win.addstr(y, x, line)
1229
1229
1230 def patchcontents(state):
1230 def patchcontents(state):
1231 repo = state['repo']
1231 repo = state['repo']
1232 rule = state['rules'][state['pos']]
1232 rule = state['rules'][state['pos']]
1233 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1233 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1234 'patch': True, 'verbose': True
1234 'patch': True, 'verbose': True
1235 }, buffered=True)
1235 }, buffered=True)
1236 displayer.show(rule.ctx)
1236 displayer.show(rule.ctx)
1237 displayer.close()
1237 displayer.close()
1238 return displayer.hunk[rule.ctx.rev()].splitlines()
1238 return displayer.hunk[rule.ctx.rev()].splitlines()
1239
1239
1240 def _chisteditmain(repo, rules, stdscr):
1240 def _chisteditmain(repo, rules, stdscr):
1241 # initialize color pattern
1241 # initialize color pattern
1242 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1242 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1243 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1243 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1244 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1244 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1245 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1245 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1246 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1246 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1247
1247
1248 # don't display the cursor
1248 # don't display the cursor
1249 try:
1249 try:
1250 curses.curs_set(0)
1250 curses.curs_set(0)
1251 except curses.error:
1251 except curses.error:
1252 pass
1252 pass
1253
1253
1254 def rendercommit(win, state):
1254 def rendercommit(win, state):
1255 """Renders the commit window that shows the log of the current selected
1255 """Renders the commit window that shows the log of the current selected
1256 commit"""
1256 commit"""
1257 pos = state['pos']
1257 pos = state['pos']
1258 rules = state['rules']
1258 rules = state['rules']
1259 rule = rules[pos]
1259 rule = rules[pos]
1260
1260
1261 ctx = rule.ctx
1261 ctx = rule.ctx
1262 win.box()
1262 win.box()
1263
1263
1264 maxy, maxx = win.getmaxyx()
1264 maxy, maxx = win.getmaxyx()
1265 length = maxx - 3
1265 length = maxx - 3
1266
1266
1267 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1267 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1268 win.addstr(1, 1, line[:length])
1268 win.addstr(1, 1, line[:length])
1269
1269
1270 line = "user: {0}".format(ctx.user())
1270 line = "user: {0}".format(ctx.user())
1271 win.addstr(2, 1, line[:length])
1271 win.addstr(2, 1, line[:length])
1272
1272
1273 bms = repo.nodebookmarks(ctx.node())
1273 bms = repo.nodebookmarks(ctx.node())
1274 line = "bookmark: {0}".format(' '.join(bms))
1274 line = "bookmark: {0}".format(' '.join(bms))
1275 win.addstr(3, 1, line[:length])
1275 win.addstr(3, 1, line[:length])
1276
1276
1277 line = "files: {0}".format(','.join(ctx.files()))
1277 line = "files: {0}".format(','.join(ctx.files()))
1278 win.addstr(4, 1, line[:length])
1278 win.addstr(4, 1, line[:length])
1279
1279
1280 line = "summary: {0}".format(ctx.description().splitlines()[0])
1280 line = "summary: {0}".format(ctx.description().splitlines()[0])
1281 win.addstr(5, 1, line[:length])
1281 win.addstr(5, 1, line[:length])
1282
1282
1283 conflicts = rule.conflicts
1283 conflicts = rule.conflicts
1284 if len(conflicts) > 0:
1284 if len(conflicts) > 0:
1285 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1285 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1286 conflictstr = "changed files overlap with {0}".format(conflictstr)
1286 conflictstr = "changed files overlap with {0}".format(conflictstr)
1287 else:
1287 else:
1288 conflictstr = 'no overlap'
1288 conflictstr = 'no overlap'
1289
1289
1290 win.addstr(6, 1, conflictstr[:length])
1290 win.addstr(6, 1, conflictstr[:length])
1291 win.noutrefresh()
1291 win.noutrefresh()
1292
1292
1293 def helplines(mode):
1293 def helplines(mode):
1294 if mode == MODE_PATCH:
1294 if mode == MODE_PATCH:
1295 help = """\
1295 help = """\
1296 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1296 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1297 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1297 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1298 """
1298 """
1299 else:
1299 else:
1300 help = """\
1300 help = """\
1301 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1301 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1302 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1302 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1303 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1303 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1304 """
1304 """
1305 return help.splitlines()
1305 return help.splitlines()
1306
1306
1307 def renderhelp(win, state):
1307 def renderhelp(win, state):
1308 maxy, maxx = win.getmaxyx()
1308 maxy, maxx = win.getmaxyx()
1309 mode, _ = state['mode']
1309 mode, _ = state['mode']
1310 for y, line in enumerate(helplines(mode)):
1310 for y, line in enumerate(helplines(mode)):
1311 if y >= maxy:
1311 if y >= maxy:
1312 break
1312 break
1313 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1313 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1314 win.noutrefresh()
1314 win.noutrefresh()
1315
1315
1316 def renderrules(rulesscr, state):
1316 def renderrules(rulesscr, state):
1317 rules = state['rules']
1317 rules = state['rules']
1318 pos = state['pos']
1318 pos = state['pos']
1319 selected = state['selected']
1319 selected = state['selected']
1320 start = state['modes'][MODE_RULES]['line_offset']
1320 start = state['modes'][MODE_RULES]['line_offset']
1321
1321
1322 conflicts = [r.ctx for r in rules if r.conflicts]
1322 conflicts = [r.ctx for r in rules if r.conflicts]
1323 if len(conflicts) > 0:
1323 if len(conflicts) > 0:
1324 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1324 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1325 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1325 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1326
1326
1327 for y, rule in enumerate(rules[start:]):
1327 for y, rule in enumerate(rules[start:]):
1328 if y >= state['page_height']:
1328 if y >= state['page_height']:
1329 break
1329 break
1330 if len(rule.conflicts) > 0:
1330 if len(rule.conflicts) > 0:
1331 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1331 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1332 else:
1332 else:
1333 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1333 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1334 if y + start == selected:
1334 if y + start == selected:
1335 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1335 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1336 elif y + start == pos:
1336 elif y + start == pos:
1337 addln(rulesscr, y, 2, rule,
1337 addln(rulesscr, y, 2, rule,
1338 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD)
1338 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD)
1339 else:
1339 else:
1340 addln(rulesscr, y, 2, rule)
1340 addln(rulesscr, y, 2, rule)
1341 rulesscr.noutrefresh()
1341 rulesscr.noutrefresh()
1342
1342
1343 def renderstring(win, state, output):
1343 def renderstring(win, state, output):
1344 maxy, maxx = win.getmaxyx()
1344 maxy, maxx = win.getmaxyx()
1345 length = min(maxy - 1, len(output))
1345 length = min(maxy - 1, len(output))
1346 for y in range(0, length):
1346 for y in range(0, length):
1347 win.addstr(y, 0, output[y])
1347 win.addstr(y, 0, output[y])
1348 win.noutrefresh()
1348 win.noutrefresh()
1349
1349
1350 def renderpatch(win, state):
1350 def renderpatch(win, state):
1351 start = state['modes'][MODE_PATCH]['line_offset']
1351 start = state['modes'][MODE_PATCH]['line_offset']
1352 renderstring(win, state, patchcontents(state)[start:])
1352 renderstring(win, state, patchcontents(state)[start:])
1353
1353
1354 def layout(mode):
1354 def layout(mode):
1355 maxy, maxx = stdscr.getmaxyx()
1355 maxy, maxx = stdscr.getmaxyx()
1356 helplen = len(helplines(mode))
1356 helplen = len(helplines(mode))
1357 return {
1357 return {
1358 'commit': (8, maxx),
1358 'commit': (8, maxx),
1359 'help': (helplen, maxx),
1359 'help': (helplen, maxx),
1360 'main': (maxy - helplen - 8, maxx),
1360 'main': (maxy - helplen - 8, maxx),
1361 }
1361 }
1362
1362
1363 def drawvertwin(size, y, x):
1363 def drawvertwin(size, y, x):
1364 win = curses.newwin(size[0], size[1], y, x)
1364 win = curses.newwin(size[0], size[1], y, x)
1365 y += size[0]
1365 y += size[0]
1366 return win, y, x
1366 return win, y, x
1367
1367
1368 state = {
1368 state = {
1369 'pos': 0,
1369 'pos': 0,
1370 'rules': rules,
1370 'rules': rules,
1371 'selected': None,
1371 'selected': None,
1372 'mode': (MODE_INIT, MODE_INIT),
1372 'mode': (MODE_INIT, MODE_INIT),
1373 'page_height': None,
1373 'page_height': None,
1374 'modes': {
1374 'modes': {
1375 MODE_RULES: {
1375 MODE_RULES: {
1376 'line_offset': 0,
1376 'line_offset': 0,
1377 },
1377 },
1378 MODE_PATCH: {
1378 MODE_PATCH: {
1379 'line_offset': 0,
1379 'line_offset': 0,
1380 }
1380 }
1381 },
1381 },
1382 'repo': repo,
1382 'repo': repo,
1383 }
1383 }
1384
1384
1385 # eventloop
1385 # eventloop
1386 ch = None
1386 ch = None
1387 stdscr.clear()
1387 stdscr.clear()
1388 stdscr.refresh()
1388 stdscr.refresh()
1389 while True:
1389 while True:
1390 try:
1390 try:
1391 oldmode, _ = state['mode']
1391 oldmode, _ = state['mode']
1392 if oldmode == MODE_INIT:
1392 if oldmode == MODE_INIT:
1393 changemode(state, MODE_RULES)
1393 changemode(state, MODE_RULES)
1394 e = event(state, ch)
1394 e = event(state, ch)
1395
1395
1396 if e == E_QUIT:
1396 if e == E_QUIT:
1397 return False
1397 return False
1398 if e == E_HISTEDIT:
1398 if e == E_HISTEDIT:
1399 return state['rules']
1399 return state['rules']
1400 else:
1400 else:
1401 if e == E_RESIZE:
1401 if e == E_RESIZE:
1402 size = screen_size()
1402 size = screen_size()
1403 if size != stdscr.getmaxyx():
1403 if size != stdscr.getmaxyx():
1404 curses.resizeterm(*size)
1404 curses.resizeterm(*size)
1405
1405
1406 curmode, _ = state['mode']
1406 curmode, _ = state['mode']
1407 sizes = layout(curmode)
1407 sizes = layout(curmode)
1408 if curmode != oldmode:
1408 if curmode != oldmode:
1409 state['page_height'] = sizes['main'][0]
1409 state['page_height'] = sizes['main'][0]
1410 # Adjust the view to fit the current screen size.
1410 # Adjust the view to fit the current screen size.
1411 movecursor(state, state['pos'], state['pos'])
1411 movecursor(state, state['pos'], state['pos'])
1412
1412
1413 # Pack the windows against the top, each pane spread across the
1413 # Pack the windows against the top, each pane spread across the
1414 # full width of the screen.
1414 # full width of the screen.
1415 y, x = (0, 0)
1415 y, x = (0, 0)
1416 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1416 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1417 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1417 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1418 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1418 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1419
1419
1420 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1420 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1421 if e == E_PAGEDOWN:
1421 if e == E_PAGEDOWN:
1422 changeview(state, +1, 'page')
1422 changeview(state, +1, 'page')
1423 elif e == E_PAGEUP:
1423 elif e == E_PAGEUP:
1424 changeview(state, -1, 'page')
1424 changeview(state, -1, 'page')
1425 elif e == E_LINEDOWN:
1425 elif e == E_LINEDOWN:
1426 changeview(state, +1, 'line')
1426 changeview(state, +1, 'line')
1427 elif e == E_LINEUP:
1427 elif e == E_LINEUP:
1428 changeview(state, -1, 'line')
1428 changeview(state, -1, 'line')
1429
1429
1430 # start rendering
1430 # start rendering
1431 commitwin.erase()
1431 commitwin.erase()
1432 helpwin.erase()
1432 helpwin.erase()
1433 mainwin.erase()
1433 mainwin.erase()
1434 if curmode == MODE_PATCH:
1434 if curmode == MODE_PATCH:
1435 renderpatch(mainwin, state)
1435 renderpatch(mainwin, state)
1436 elif curmode == MODE_HELP:
1436 elif curmode == MODE_HELP:
1437 renderstring(mainwin, state, __doc__.strip().splitlines())
1437 renderstring(mainwin, state, __doc__.strip().splitlines())
1438 else:
1438 else:
1439 renderrules(mainwin, state)
1439 renderrules(mainwin, state)
1440 rendercommit(commitwin, state)
1440 rendercommit(commitwin, state)
1441 renderhelp(helpwin, state)
1441 renderhelp(helpwin, state)
1442 curses.doupdate()
1442 curses.doupdate()
1443 # done rendering
1443 # done rendering
1444 ch = stdscr.getkey()
1444 ch = stdscr.getkey()
1445 except curses.error:
1445 except curses.error:
1446 pass
1446 pass
1447
1447
1448 def _chistedit(ui, repo, *freeargs, **opts):
1448 def _chistedit(ui, repo, *freeargs, **opts):
1449 """interactively edit changeset history via a curses interface
1449 """interactively edit changeset history via a curses interface
1450
1450
1451 Provides a ncurses interface to histedit. Press ? in chistedit mode
1451 Provides a ncurses interface to histedit. Press ? in chistedit mode
1452 to see an extensive help. Requires python-curses to be installed."""
1452 to see an extensive help. Requires python-curses to be installed."""
1453
1453
1454 if curses is None:
1454 if curses is None:
1455 raise error.Abort(_("Python curses library required"))
1455 raise error.Abort(_("Python curses library required"))
1456
1456
1457 # disable color
1457 # disable color
1458 ui._colormode = None
1458 ui._colormode = None
1459
1459
1460 try:
1460 try:
1461 keep = opts.get('keep')
1461 keep = opts.get('keep')
1462 revs = opts.get('rev', [])[:]
1462 revs = opts.get('rev', [])[:]
1463 cmdutil.checkunfinished(repo)
1463 cmdutil.checkunfinished(repo)
1464 cmdutil.bailifchanged(repo)
1464 cmdutil.bailifchanged(repo)
1465
1465
1466 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1466 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1467 raise error.Abort(_('history edit already in progress, try '
1467 raise error.Abort(_('history edit already in progress, try '
1468 '--continue or --abort'))
1468 '--continue or --abort'))
1469 revs.extend(freeargs)
1469 revs.extend(freeargs)
1470 if not revs:
1470 if not revs:
1471 defaultrev = destutil.desthistedit(ui, repo)
1471 defaultrev = destutil.desthistedit(ui, repo)
1472 if defaultrev is not None:
1472 if defaultrev is not None:
1473 revs.append(defaultrev)
1473 revs.append(defaultrev)
1474 if len(revs) != 1:
1474 if len(revs) != 1:
1475 raise error.Abort(
1475 raise error.Abort(
1476 _('histedit requires exactly one ancestor revision'))
1476 _('histedit requires exactly one ancestor revision'))
1477
1477
1478 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1478 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1479 if len(rr) != 1:
1479 if len(rr) != 1:
1480 raise error.Abort(_('The specified revisions must have '
1480 raise error.Abort(_('The specified revisions must have '
1481 'exactly one common root'))
1481 'exactly one common root'))
1482 root = rr[0].node()
1482 root = rr[0].node()
1483
1483
1484 topmost = repo.dirstate.p1()
1484 topmost = repo.dirstate.p1()
1485 revs = between(repo, root, topmost, keep)
1485 revs = between(repo, root, topmost, keep)
1486 if not revs:
1486 if not revs:
1487 raise error.Abort(_('%s is not an ancestor of working directory') %
1487 raise error.Abort(_('%s is not an ancestor of working directory') %
1488 node.short(root))
1488 node.short(root))
1489
1489
1490 ctxs = []
1490 ctxs = []
1491 for i, r in enumerate(revs):
1491 for i, r in enumerate(revs):
1492 ctxs.append(histeditrule(repo[r], i))
1492 ctxs.append(histeditrule(repo[r], i))
1493 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1493 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1494 curses.echo()
1494 curses.echo()
1495 curses.endwin()
1495 curses.endwin()
1496 if rc is False:
1496 if rc is False:
1497 ui.write(_("histedit aborted\n"))
1497 ui.write(_("histedit aborted\n"))
1498 return 0
1498 return 0
1499 if type(rc) is list:
1499 if type(rc) is list:
1500 ui.status(_("running histedit\n"))
1500 ui.status(_("performing changes\n"))
1501 rules = makecommands(rc)
1501 rules = makecommands(rc)
1502 filename = repo.vfs.join('chistedit')
1502 filename = repo.vfs.join('chistedit')
1503 with open(filename, 'w+') as fp:
1503 with open(filename, 'w+') as fp:
1504 for r in rules:
1504 for r in rules:
1505 fp.write(r)
1505 fp.write(r)
1506 opts['commands'] = filename
1506 opts['commands'] = filename
1507 return _texthistedit(ui, repo, *freeargs, **opts)
1507 return _texthistedit(ui, repo, *freeargs, **opts)
1508 except KeyboardInterrupt:
1508 except KeyboardInterrupt:
1509 pass
1509 pass
1510 return -1
1510 return -1
1511
1511
1512 @command('histedit',
1512 @command('histedit',
1513 [('', 'commands', '',
1513 [('', 'commands', '',
1514 _('read history edits from the specified file'), _('FILE')),
1514 _('read history edits from the specified file'), _('FILE')),
1515 ('c', 'continue', False, _('continue an edit already in progress')),
1515 ('c', 'continue', False, _('continue an edit already in progress')),
1516 ('', 'edit-plan', False, _('edit remaining actions list')),
1516 ('', 'edit-plan', False, _('edit remaining actions list')),
1517 ('k', 'keep', False,
1517 ('k', 'keep', False,
1518 _("don't strip old nodes after edit is complete")),
1518 _("don't strip old nodes after edit is complete")),
1519 ('', 'abort', False, _('abort an edit in progress')),
1519 ('', 'abort', False, _('abort an edit in progress')),
1520 ('o', 'outgoing', False, _('changesets not found in destination')),
1520 ('o', 'outgoing', False, _('changesets not found in destination')),
1521 ('f', 'force', False,
1521 ('f', 'force', False,
1522 _('force outgoing even for unrelated repositories')),
1522 _('force outgoing even for unrelated repositories')),
1523 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1523 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1524 cmdutil.formatteropts,
1524 cmdutil.formatteropts,
1525 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1525 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1526 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1526 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1527 def histedit(ui, repo, *freeargs, **opts):
1527 def histedit(ui, repo, *freeargs, **opts):
1528 """interactively edit changeset history
1528 """interactively edit changeset history
1529
1529
1530 This command lets you edit a linear series of changesets (up to
1530 This command lets you edit a linear series of changesets (up to
1531 and including the working directory, which should be clean).
1531 and including the working directory, which should be clean).
1532 You can:
1532 You can:
1533
1533
1534 - `pick` to [re]order a changeset
1534 - `pick` to [re]order a changeset
1535
1535
1536 - `drop` to omit changeset
1536 - `drop` to omit changeset
1537
1537
1538 - `mess` to reword the changeset commit message
1538 - `mess` to reword the changeset commit message
1539
1539
1540 - `fold` to combine it with the preceding changeset (using the later date)
1540 - `fold` to combine it with the preceding changeset (using the later date)
1541
1541
1542 - `roll` like fold, but discarding this commit's description and date
1542 - `roll` like fold, but discarding this commit's description and date
1543
1543
1544 - `edit` to edit this changeset (preserving date)
1544 - `edit` to edit this changeset (preserving date)
1545
1545
1546 - `base` to checkout changeset and apply further changesets from there
1546 - `base` to checkout changeset and apply further changesets from there
1547
1547
1548 There are a number of ways to select the root changeset:
1548 There are a number of ways to select the root changeset:
1549
1549
1550 - Specify ANCESTOR directly
1550 - Specify ANCESTOR directly
1551
1551
1552 - Use --outgoing -- it will be the first linear changeset not
1552 - Use --outgoing -- it will be the first linear changeset not
1553 included in destination. (See :hg:`help config.paths.default-push`)
1553 included in destination. (See :hg:`help config.paths.default-push`)
1554
1554
1555 - Otherwise, the value from the "histedit.defaultrev" config option
1555 - Otherwise, the value from the "histedit.defaultrev" config option
1556 is used as a revset to select the base revision when ANCESTOR is not
1556 is used as a revset to select the base revision when ANCESTOR is not
1557 specified. The first revision returned by the revset is used. By
1557 specified. The first revision returned by the revset is used. By
1558 default, this selects the editable history that is unique to the
1558 default, this selects the editable history that is unique to the
1559 ancestry of the working directory.
1559 ancestry of the working directory.
1560
1560
1561 .. container:: verbose
1561 .. container:: verbose
1562
1562
1563 If you use --outgoing, this command will abort if there are ambiguous
1563 If you use --outgoing, this command will abort if there are ambiguous
1564 outgoing revisions. For example, if there are multiple branches
1564 outgoing revisions. For example, if there are multiple branches
1565 containing outgoing revisions.
1565 containing outgoing revisions.
1566
1566
1567 Use "min(outgoing() and ::.)" or similar revset specification
1567 Use "min(outgoing() and ::.)" or similar revset specification
1568 instead of --outgoing to specify edit target revision exactly in
1568 instead of --outgoing to specify edit target revision exactly in
1569 such ambiguous situation. See :hg:`help revsets` for detail about
1569 such ambiguous situation. See :hg:`help revsets` for detail about
1570 selecting revisions.
1570 selecting revisions.
1571
1571
1572 .. container:: verbose
1572 .. container:: verbose
1573
1573
1574 Examples:
1574 Examples:
1575
1575
1576 - A number of changes have been made.
1576 - A number of changes have been made.
1577 Revision 3 is no longer needed.
1577 Revision 3 is no longer needed.
1578
1578
1579 Start history editing from revision 3::
1579 Start history editing from revision 3::
1580
1580
1581 hg histedit -r 3
1581 hg histedit -r 3
1582
1582
1583 An editor opens, containing the list of revisions,
1583 An editor opens, containing the list of revisions,
1584 with specific actions specified::
1584 with specific actions specified::
1585
1585
1586 pick 5339bf82f0ca 3 Zworgle the foobar
1586 pick 5339bf82f0ca 3 Zworgle the foobar
1587 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1587 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1588 pick 0a9639fcda9d 5 Morgify the cromulancy
1588 pick 0a9639fcda9d 5 Morgify the cromulancy
1589
1589
1590 Additional information about the possible actions
1590 Additional information about the possible actions
1591 to take appears below the list of revisions.
1591 to take appears below the list of revisions.
1592
1592
1593 To remove revision 3 from the history,
1593 To remove revision 3 from the history,
1594 its action (at the beginning of the relevant line)
1594 its action (at the beginning of the relevant line)
1595 is changed to 'drop'::
1595 is changed to 'drop'::
1596
1596
1597 drop 5339bf82f0ca 3 Zworgle the foobar
1597 drop 5339bf82f0ca 3 Zworgle the foobar
1598 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1598 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1599 pick 0a9639fcda9d 5 Morgify the cromulancy
1599 pick 0a9639fcda9d 5 Morgify the cromulancy
1600
1600
1601 - A number of changes have been made.
1601 - A number of changes have been made.
1602 Revision 2 and 4 need to be swapped.
1602 Revision 2 and 4 need to be swapped.
1603
1603
1604 Start history editing from revision 2::
1604 Start history editing from revision 2::
1605
1605
1606 hg histedit -r 2
1606 hg histedit -r 2
1607
1607
1608 An editor opens, containing the list of revisions,
1608 An editor opens, containing the list of revisions,
1609 with specific actions specified::
1609 with specific actions specified::
1610
1610
1611 pick 252a1af424ad 2 Blorb a morgwazzle
1611 pick 252a1af424ad 2 Blorb a morgwazzle
1612 pick 5339bf82f0ca 3 Zworgle the foobar
1612 pick 5339bf82f0ca 3 Zworgle the foobar
1613 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1613 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1614
1614
1615 To swap revision 2 and 4, its lines are swapped
1615 To swap revision 2 and 4, its lines are swapped
1616 in the editor::
1616 in the editor::
1617
1617
1618 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1618 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1619 pick 5339bf82f0ca 3 Zworgle the foobar
1619 pick 5339bf82f0ca 3 Zworgle the foobar
1620 pick 252a1af424ad 2 Blorb a morgwazzle
1620 pick 252a1af424ad 2 Blorb a morgwazzle
1621
1621
1622 Returns 0 on success, 1 if user intervention is required (not only
1622 Returns 0 on success, 1 if user intervention is required (not only
1623 for intentional "edit" command, but also for resolving unexpected
1623 for intentional "edit" command, but also for resolving unexpected
1624 conflicts).
1624 conflicts).
1625 """
1625 """
1626 # kludge: _chistedit only works for starting an edit, not aborting
1626 # kludge: _chistedit only works for starting an edit, not aborting
1627 # or continuing, so fall back to regular _texthistedit for those
1627 # or continuing, so fall back to regular _texthistedit for those
1628 # operations.
1628 # operations.
1629 if ui.interface('histedit') == 'curses' and _getgoal(
1629 if ui.interface('histedit') == 'curses' and _getgoal(
1630 pycompat.byteskwargs(opts)) == goalnew:
1630 pycompat.byteskwargs(opts)) == goalnew:
1631 return _chistedit(ui, repo, *freeargs, **opts)
1631 return _chistedit(ui, repo, *freeargs, **opts)
1632 return _texthistedit(ui, repo, *freeargs, **opts)
1632 return _texthistedit(ui, repo, *freeargs, **opts)
1633
1633
1634 def _texthistedit(ui, repo, *freeargs, **opts):
1634 def _texthistedit(ui, repo, *freeargs, **opts):
1635 state = histeditstate(repo)
1635 state = histeditstate(repo)
1636 with repo.wlock() as wlock, repo.lock() as lock:
1636 with repo.wlock() as wlock, repo.lock() as lock:
1637 state.wlock = wlock
1637 state.wlock = wlock
1638 state.lock = lock
1638 state.lock = lock
1639 _histedit(ui, repo, state, *freeargs, **opts)
1639 _histedit(ui, repo, state, *freeargs, **opts)
1640
1640
1641 goalcontinue = 'continue'
1641 goalcontinue = 'continue'
1642 goalabort = 'abort'
1642 goalabort = 'abort'
1643 goaleditplan = 'edit-plan'
1643 goaleditplan = 'edit-plan'
1644 goalnew = 'new'
1644 goalnew = 'new'
1645
1645
1646 def _getgoal(opts):
1646 def _getgoal(opts):
1647 if opts.get(b'continue'):
1647 if opts.get(b'continue'):
1648 return goalcontinue
1648 return goalcontinue
1649 if opts.get(b'abort'):
1649 if opts.get(b'abort'):
1650 return goalabort
1650 return goalabort
1651 if opts.get(b'edit_plan'):
1651 if opts.get(b'edit_plan'):
1652 return goaleditplan
1652 return goaleditplan
1653 return goalnew
1653 return goalnew
1654
1654
1655 def _readfile(ui, path):
1655 def _readfile(ui, path):
1656 if path == '-':
1656 if path == '-':
1657 with ui.timeblockedsection('histedit'):
1657 with ui.timeblockedsection('histedit'):
1658 return ui.fin.read()
1658 return ui.fin.read()
1659 else:
1659 else:
1660 with open(path, 'rb') as f:
1660 with open(path, 'rb') as f:
1661 return f.read()
1661 return f.read()
1662
1662
1663 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1663 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1664 # TODO only abort if we try to histedit mq patches, not just
1664 # TODO only abort if we try to histedit mq patches, not just
1665 # blanket if mq patches are applied somewhere
1665 # blanket if mq patches are applied somewhere
1666 mq = getattr(repo, 'mq', None)
1666 mq = getattr(repo, 'mq', None)
1667 if mq and mq.applied:
1667 if mq and mq.applied:
1668 raise error.Abort(_('source has mq patches applied'))
1668 raise error.Abort(_('source has mq patches applied'))
1669
1669
1670 # basic argument incompatibility processing
1670 # basic argument incompatibility processing
1671 outg = opts.get('outgoing')
1671 outg = opts.get('outgoing')
1672 editplan = opts.get('edit_plan')
1672 editplan = opts.get('edit_plan')
1673 abort = opts.get('abort')
1673 abort = opts.get('abort')
1674 force = opts.get('force')
1674 force = opts.get('force')
1675 if force and not outg:
1675 if force and not outg:
1676 raise error.Abort(_('--force only allowed with --outgoing'))
1676 raise error.Abort(_('--force only allowed with --outgoing'))
1677 if goal == 'continue':
1677 if goal == 'continue':
1678 if any((outg, abort, revs, freeargs, rules, editplan)):
1678 if any((outg, abort, revs, freeargs, rules, editplan)):
1679 raise error.Abort(_('no arguments allowed with --continue'))
1679 raise error.Abort(_('no arguments allowed with --continue'))
1680 elif goal == 'abort':
1680 elif goal == 'abort':
1681 if any((outg, revs, freeargs, rules, editplan)):
1681 if any((outg, revs, freeargs, rules, editplan)):
1682 raise error.Abort(_('no arguments allowed with --abort'))
1682 raise error.Abort(_('no arguments allowed with --abort'))
1683 elif goal == 'edit-plan':
1683 elif goal == 'edit-plan':
1684 if any((outg, revs, freeargs)):
1684 if any((outg, revs, freeargs)):
1685 raise error.Abort(_('only --commands argument allowed with '
1685 raise error.Abort(_('only --commands argument allowed with '
1686 '--edit-plan'))
1686 '--edit-plan'))
1687 else:
1687 else:
1688 if state.inprogress():
1688 if state.inprogress():
1689 raise error.Abort(_('history edit already in progress, try '
1689 raise error.Abort(_('history edit already in progress, try '
1690 '--continue or --abort'))
1690 '--continue or --abort'))
1691 if outg:
1691 if outg:
1692 if revs:
1692 if revs:
1693 raise error.Abort(_('no revisions allowed with --outgoing'))
1693 raise error.Abort(_('no revisions allowed with --outgoing'))
1694 if len(freeargs) > 1:
1694 if len(freeargs) > 1:
1695 raise error.Abort(
1695 raise error.Abort(
1696 _('only one repo argument allowed with --outgoing'))
1696 _('only one repo argument allowed with --outgoing'))
1697 else:
1697 else:
1698 revs.extend(freeargs)
1698 revs.extend(freeargs)
1699 if len(revs) == 0:
1699 if len(revs) == 0:
1700 defaultrev = destutil.desthistedit(ui, repo)
1700 defaultrev = destutil.desthistedit(ui, repo)
1701 if defaultrev is not None:
1701 if defaultrev is not None:
1702 revs.append(defaultrev)
1702 revs.append(defaultrev)
1703
1703
1704 if len(revs) != 1:
1704 if len(revs) != 1:
1705 raise error.Abort(
1705 raise error.Abort(
1706 _('histedit requires exactly one ancestor revision'))
1706 _('histedit requires exactly one ancestor revision'))
1707
1707
1708 def _histedit(ui, repo, state, *freeargs, **opts):
1708 def _histedit(ui, repo, state, *freeargs, **opts):
1709 opts = pycompat.byteskwargs(opts)
1709 opts = pycompat.byteskwargs(opts)
1710 fm = ui.formatter('histedit', opts)
1710 fm = ui.formatter('histedit', opts)
1711 fm.startitem()
1711 fm.startitem()
1712 goal = _getgoal(opts)
1712 goal = _getgoal(opts)
1713 revs = opts.get('rev', [])
1713 revs = opts.get('rev', [])
1714 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1714 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1715 rules = opts.get('commands', '')
1715 rules = opts.get('commands', '')
1716 state.keep = opts.get('keep', False)
1716 state.keep = opts.get('keep', False)
1717
1717
1718 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1718 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1719
1719
1720 hastags = False
1720 hastags = False
1721 if revs:
1721 if revs:
1722 revs = scmutil.revrange(repo, revs)
1722 revs = scmutil.revrange(repo, revs)
1723 ctxs = [repo[rev] for rev in revs]
1723 ctxs = [repo[rev] for rev in revs]
1724 for ctx in ctxs:
1724 for ctx in ctxs:
1725 tags = [tag for tag in ctx.tags() if tag != 'tip']
1725 tags = [tag for tag in ctx.tags() if tag != 'tip']
1726 if not hastags:
1726 if not hastags:
1727 hastags = len(tags)
1727 hastags = len(tags)
1728 if hastags:
1728 if hastags:
1729 if ui.promptchoice(_('warning: tags associated with the given'
1729 if ui.promptchoice(_('warning: tags associated with the given'
1730 ' changeset will be lost after histedit.\n'
1730 ' changeset will be lost after histedit.\n'
1731 'do you want to continue (yN)? $$ &Yes $$ &No'),
1731 'do you want to continue (yN)? $$ &Yes $$ &No'),
1732 default=1):
1732 default=1):
1733 raise error.Abort(_('histedit cancelled\n'))
1733 raise error.Abort(_('histedit cancelled\n'))
1734 # rebuild state
1734 # rebuild state
1735 if goal == goalcontinue:
1735 if goal == goalcontinue:
1736 state.read()
1736 state.read()
1737 state = bootstrapcontinue(ui, state, opts)
1737 state = bootstrapcontinue(ui, state, opts)
1738 elif goal == goaleditplan:
1738 elif goal == goaleditplan:
1739 _edithisteditplan(ui, repo, state, rules)
1739 _edithisteditplan(ui, repo, state, rules)
1740 return
1740 return
1741 elif goal == goalabort:
1741 elif goal == goalabort:
1742 _aborthistedit(ui, repo, state, nobackup=nobackup)
1742 _aborthistedit(ui, repo, state, nobackup=nobackup)
1743 return
1743 return
1744 else:
1744 else:
1745 # goal == goalnew
1745 # goal == goalnew
1746 _newhistedit(ui, repo, state, revs, freeargs, opts)
1746 _newhistedit(ui, repo, state, revs, freeargs, opts)
1747
1747
1748 _continuehistedit(ui, repo, state)
1748 _continuehistedit(ui, repo, state)
1749 _finishhistedit(ui, repo, state, fm)
1749 _finishhistedit(ui, repo, state, fm)
1750 fm.end()
1750 fm.end()
1751
1751
1752 def _continuehistedit(ui, repo, state):
1752 def _continuehistedit(ui, repo, state):
1753 """This function runs after either:
1753 """This function runs after either:
1754 - bootstrapcontinue (if the goal is 'continue')
1754 - bootstrapcontinue (if the goal is 'continue')
1755 - _newhistedit (if the goal is 'new')
1755 - _newhistedit (if the goal is 'new')
1756 """
1756 """
1757 # preprocess rules so that we can hide inner folds from the user
1757 # preprocess rules so that we can hide inner folds from the user
1758 # and only show one editor
1758 # and only show one editor
1759 actions = state.actions[:]
1759 actions = state.actions[:]
1760 for idx, (action, nextact) in enumerate(
1760 for idx, (action, nextact) in enumerate(
1761 zip(actions, actions[1:] + [None])):
1761 zip(actions, actions[1:] + [None])):
1762 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1762 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1763 state.actions[idx].__class__ = _multifold
1763 state.actions[idx].__class__ = _multifold
1764
1764
1765 # Force an initial state file write, so the user can run --abort/continue
1765 # Force an initial state file write, so the user can run --abort/continue
1766 # even if there's an exception before the first transaction serialize.
1766 # even if there's an exception before the first transaction serialize.
1767 state.write()
1767 state.write()
1768
1768
1769 tr = None
1769 tr = None
1770 # Don't use singletransaction by default since it rolls the entire
1770 # Don't use singletransaction by default since it rolls the entire
1771 # transaction back if an unexpected exception happens (like a
1771 # transaction back if an unexpected exception happens (like a
1772 # pretxncommit hook throws, or the user aborts the commit msg editor).
1772 # pretxncommit hook throws, or the user aborts the commit msg editor).
1773 if ui.configbool("histedit", "singletransaction"):
1773 if ui.configbool("histedit", "singletransaction"):
1774 # Don't use a 'with' for the transaction, since actions may close
1774 # Don't use a 'with' for the transaction, since actions may close
1775 # and reopen a transaction. For example, if the action executes an
1775 # and reopen a transaction. For example, if the action executes an
1776 # external process it may choose to commit the transaction first.
1776 # external process it may choose to commit the transaction first.
1777 tr = repo.transaction('histedit')
1777 tr = repo.transaction('histedit')
1778 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1778 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1779 total=len(state.actions))
1779 total=len(state.actions))
1780 with progress, util.acceptintervention(tr):
1780 with progress, util.acceptintervention(tr):
1781 while state.actions:
1781 while state.actions:
1782 state.write(tr=tr)
1782 state.write(tr=tr)
1783 actobj = state.actions[0]
1783 actobj = state.actions[0]
1784 progress.increment(item=actobj.torule())
1784 progress.increment(item=actobj.torule())
1785 ui.debug('histedit: processing %s %s\n' % (actobj.verb,
1785 ui.debug('histedit: processing %s %s\n' % (actobj.verb,
1786 actobj.torule()))
1786 actobj.torule()))
1787 parentctx, replacement_ = actobj.run()
1787 parentctx, replacement_ = actobj.run()
1788 state.parentctxnode = parentctx.node()
1788 state.parentctxnode = parentctx.node()
1789 state.replacements.extend(replacement_)
1789 state.replacements.extend(replacement_)
1790 state.actions.pop(0)
1790 state.actions.pop(0)
1791
1791
1792 state.write()
1792 state.write()
1793
1793
1794 def _finishhistedit(ui, repo, state, fm):
1794 def _finishhistedit(ui, repo, state, fm):
1795 """This action runs when histedit is finishing its session"""
1795 """This action runs when histedit is finishing its session"""
1796 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1796 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1797
1797
1798 mapping, tmpnodes, created, ntm = processreplacement(state)
1798 mapping, tmpnodes, created, ntm = processreplacement(state)
1799 if mapping:
1799 if mapping:
1800 for prec, succs in mapping.iteritems():
1800 for prec, succs in mapping.iteritems():
1801 if not succs:
1801 if not succs:
1802 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1802 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1803 else:
1803 else:
1804 ui.debug('histedit: %s is replaced by %s\n' % (
1804 ui.debug('histedit: %s is replaced by %s\n' % (
1805 node.short(prec), node.short(succs[0])))
1805 node.short(prec), node.short(succs[0])))
1806 if len(succs) > 1:
1806 if len(succs) > 1:
1807 m = 'histedit: %s'
1807 m = 'histedit: %s'
1808 for n in succs[1:]:
1808 for n in succs[1:]:
1809 ui.debug(m % node.short(n))
1809 ui.debug(m % node.short(n))
1810
1810
1811 if not state.keep:
1811 if not state.keep:
1812 if mapping:
1812 if mapping:
1813 movetopmostbookmarks(repo, state.topmost, ntm)
1813 movetopmostbookmarks(repo, state.topmost, ntm)
1814 # TODO update mq state
1814 # TODO update mq state
1815 else:
1815 else:
1816 mapping = {}
1816 mapping = {}
1817
1817
1818 for n in tmpnodes:
1818 for n in tmpnodes:
1819 if n in repo:
1819 if n in repo:
1820 mapping[n] = ()
1820 mapping[n] = ()
1821
1821
1822 # remove entries about unknown nodes
1822 # remove entries about unknown nodes
1823 nodemap = repo.unfiltered().changelog.nodemap
1823 nodemap = repo.unfiltered().changelog.nodemap
1824 mapping = {k: v for k, v in mapping.items()
1824 mapping = {k: v for k, v in mapping.items()
1825 if k in nodemap and all(n in nodemap for n in v)}
1825 if k in nodemap and all(n in nodemap for n in v)}
1826 scmutil.cleanupnodes(repo, mapping, 'histedit')
1826 scmutil.cleanupnodes(repo, mapping, 'histedit')
1827 hf = fm.hexfunc
1827 hf = fm.hexfunc
1828 fl = fm.formatlist
1828 fl = fm.formatlist
1829 fd = fm.formatdict
1829 fd = fm.formatdict
1830 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1830 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1831 for oldn, newn in mapping.iteritems()},
1831 for oldn, newn in mapping.iteritems()},
1832 key="oldnode", value="newnodes")
1832 key="oldnode", value="newnodes")
1833 fm.data(nodechanges=nodechanges)
1833 fm.data(nodechanges=nodechanges)
1834
1834
1835 state.clear()
1835 state.clear()
1836 if os.path.exists(repo.sjoin('undo')):
1836 if os.path.exists(repo.sjoin('undo')):
1837 os.unlink(repo.sjoin('undo'))
1837 os.unlink(repo.sjoin('undo'))
1838 if repo.vfs.exists('histedit-last-edit.txt'):
1838 if repo.vfs.exists('histedit-last-edit.txt'):
1839 repo.vfs.unlink('histedit-last-edit.txt')
1839 repo.vfs.unlink('histedit-last-edit.txt')
1840
1840
1841 def _aborthistedit(ui, repo, state, nobackup=False):
1841 def _aborthistedit(ui, repo, state, nobackup=False):
1842 try:
1842 try:
1843 state.read()
1843 state.read()
1844 __, leafs, tmpnodes, __ = processreplacement(state)
1844 __, leafs, tmpnodes, __ = processreplacement(state)
1845 ui.debug('restore wc to old parent %s\n'
1845 ui.debug('restore wc to old parent %s\n'
1846 % node.short(state.topmost))
1846 % node.short(state.topmost))
1847
1847
1848 # Recover our old commits if necessary
1848 # Recover our old commits if necessary
1849 if not state.topmost in repo and state.backupfile:
1849 if not state.topmost in repo and state.backupfile:
1850 backupfile = repo.vfs.join(state.backupfile)
1850 backupfile = repo.vfs.join(state.backupfile)
1851 f = hg.openpath(ui, backupfile)
1851 f = hg.openpath(ui, backupfile)
1852 gen = exchange.readbundle(ui, f, backupfile)
1852 gen = exchange.readbundle(ui, f, backupfile)
1853 with repo.transaction('histedit.abort') as tr:
1853 with repo.transaction('histedit.abort') as tr:
1854 bundle2.applybundle(repo, gen, tr, source='histedit',
1854 bundle2.applybundle(repo, gen, tr, source='histedit',
1855 url='bundle:' + backupfile)
1855 url='bundle:' + backupfile)
1856
1856
1857 os.remove(backupfile)
1857 os.remove(backupfile)
1858
1858
1859 # check whether we should update away
1859 # check whether we should update away
1860 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1860 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1861 state.parentctxnode, leafs | tmpnodes):
1861 state.parentctxnode, leafs | tmpnodes):
1862 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1862 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1863 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1863 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1864 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1864 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1865 except Exception:
1865 except Exception:
1866 if state.inprogress():
1866 if state.inprogress():
1867 ui.warn(_('warning: encountered an exception during histedit '
1867 ui.warn(_('warning: encountered an exception during histedit '
1868 '--abort; the repository may not have been completely '
1868 '--abort; the repository may not have been completely '
1869 'cleaned up\n'))
1869 'cleaned up\n'))
1870 raise
1870 raise
1871 finally:
1871 finally:
1872 state.clear()
1872 state.clear()
1873
1873
1874 def _edithisteditplan(ui, repo, state, rules):
1874 def _edithisteditplan(ui, repo, state, rules):
1875 state.read()
1875 state.read()
1876 if not rules:
1876 if not rules:
1877 comment = geteditcomment(ui,
1877 comment = geteditcomment(ui,
1878 node.short(state.parentctxnode),
1878 node.short(state.parentctxnode),
1879 node.short(state.topmost))
1879 node.short(state.topmost))
1880 rules = ruleeditor(repo, ui, state.actions, comment)
1880 rules = ruleeditor(repo, ui, state.actions, comment)
1881 else:
1881 else:
1882 rules = _readfile(ui, rules)
1882 rules = _readfile(ui, rules)
1883 actions = parserules(rules, state)
1883 actions = parserules(rules, state)
1884 ctxs = [repo[act.node]
1884 ctxs = [repo[act.node]
1885 for act in state.actions if act.node]
1885 for act in state.actions if act.node]
1886 warnverifyactions(ui, repo, actions, state, ctxs)
1886 warnverifyactions(ui, repo, actions, state, ctxs)
1887 state.actions = actions
1887 state.actions = actions
1888 state.write()
1888 state.write()
1889
1889
1890 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1890 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1891 outg = opts.get('outgoing')
1891 outg = opts.get('outgoing')
1892 rules = opts.get('commands', '')
1892 rules = opts.get('commands', '')
1893 force = opts.get('force')
1893 force = opts.get('force')
1894
1894
1895 cmdutil.checkunfinished(repo)
1895 cmdutil.checkunfinished(repo)
1896 cmdutil.bailifchanged(repo)
1896 cmdutil.bailifchanged(repo)
1897
1897
1898 topmost = repo.dirstate.p1()
1898 topmost = repo.dirstate.p1()
1899 if outg:
1899 if outg:
1900 if freeargs:
1900 if freeargs:
1901 remote = freeargs[0]
1901 remote = freeargs[0]
1902 else:
1902 else:
1903 remote = None
1903 remote = None
1904 root = findoutgoing(ui, repo, remote, force, opts)
1904 root = findoutgoing(ui, repo, remote, force, opts)
1905 else:
1905 else:
1906 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1906 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1907 if len(rr) != 1:
1907 if len(rr) != 1:
1908 raise error.Abort(_('The specified revisions must have '
1908 raise error.Abort(_('The specified revisions must have '
1909 'exactly one common root'))
1909 'exactly one common root'))
1910 root = rr[0].node()
1910 root = rr[0].node()
1911
1911
1912 revs = between(repo, root, topmost, state.keep)
1912 revs = between(repo, root, topmost, state.keep)
1913 if not revs:
1913 if not revs:
1914 raise error.Abort(_('%s is not an ancestor of working directory') %
1914 raise error.Abort(_('%s is not an ancestor of working directory') %
1915 node.short(root))
1915 node.short(root))
1916
1916
1917 ctxs = [repo[r] for r in revs]
1917 ctxs = [repo[r] for r in revs]
1918 if not rules:
1918 if not rules:
1919 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1919 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1920 actions = [pick(state, r) for r in revs]
1920 actions = [pick(state, r) for r in revs]
1921 rules = ruleeditor(repo, ui, actions, comment)
1921 rules = ruleeditor(repo, ui, actions, comment)
1922 else:
1922 else:
1923 rules = _readfile(ui, rules)
1923 rules = _readfile(ui, rules)
1924 actions = parserules(rules, state)
1924 actions = parserules(rules, state)
1925 warnverifyactions(ui, repo, actions, state, ctxs)
1925 warnverifyactions(ui, repo, actions, state, ctxs)
1926
1926
1927 parentctxnode = repo[root].p1().node()
1927 parentctxnode = repo[root].p1().node()
1928
1928
1929 state.parentctxnode = parentctxnode
1929 state.parentctxnode = parentctxnode
1930 state.actions = actions
1930 state.actions = actions
1931 state.topmost = topmost
1931 state.topmost = topmost
1932 state.replacements = []
1932 state.replacements = []
1933
1933
1934 ui.log("histedit", "%d actions to histedit\n", len(actions),
1934 ui.log("histedit", "%d actions to histedit\n", len(actions),
1935 histedit_num_actions=len(actions))
1935 histedit_num_actions=len(actions))
1936
1936
1937 # Create a backup so we can always abort completely.
1937 # Create a backup so we can always abort completely.
1938 backupfile = None
1938 backupfile = None
1939 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1939 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1940 backupfile = repair.backupbundle(repo, [parentctxnode],
1940 backupfile = repair.backupbundle(repo, [parentctxnode],
1941 [topmost], root, 'histedit')
1941 [topmost], root, 'histedit')
1942 state.backupfile = backupfile
1942 state.backupfile = backupfile
1943
1943
1944 def _getsummary(ctx):
1944 def _getsummary(ctx):
1945 # a common pattern is to extract the summary but default to the empty
1945 # a common pattern is to extract the summary but default to the empty
1946 # string
1946 # string
1947 summary = ctx.description() or ''
1947 summary = ctx.description() or ''
1948 if summary:
1948 if summary:
1949 summary = summary.splitlines()[0]
1949 summary = summary.splitlines()[0]
1950 return summary
1950 return summary
1951
1951
1952 def bootstrapcontinue(ui, state, opts):
1952 def bootstrapcontinue(ui, state, opts):
1953 repo = state.repo
1953 repo = state.repo
1954
1954
1955 ms = mergemod.mergestate.read(repo)
1955 ms = mergemod.mergestate.read(repo)
1956 mergeutil.checkunresolved(ms)
1956 mergeutil.checkunresolved(ms)
1957
1957
1958 if state.actions:
1958 if state.actions:
1959 actobj = state.actions.pop(0)
1959 actobj = state.actions.pop(0)
1960
1960
1961 if _isdirtywc(repo):
1961 if _isdirtywc(repo):
1962 actobj.continuedirty()
1962 actobj.continuedirty()
1963 if _isdirtywc(repo):
1963 if _isdirtywc(repo):
1964 abortdirty()
1964 abortdirty()
1965
1965
1966 parentctx, replacements = actobj.continueclean()
1966 parentctx, replacements = actobj.continueclean()
1967
1967
1968 state.parentctxnode = parentctx.node()
1968 state.parentctxnode = parentctx.node()
1969 state.replacements.extend(replacements)
1969 state.replacements.extend(replacements)
1970
1970
1971 return state
1971 return state
1972
1972
1973 def between(repo, old, new, keep):
1973 def between(repo, old, new, keep):
1974 """select and validate the set of revision to edit
1974 """select and validate the set of revision to edit
1975
1975
1976 When keep is false, the specified set can't have children."""
1976 When keep is false, the specified set can't have children."""
1977 revs = repo.revs('%n::%n', old, new)
1977 revs = repo.revs('%n::%n', old, new)
1978 if revs and not keep:
1978 if revs and not keep:
1979 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1979 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1980 repo.revs('(%ld::) - (%ld)', revs, revs)):
1980 repo.revs('(%ld::) - (%ld)', revs, revs)):
1981 raise error.Abort(_('can only histedit a changeset together '
1981 raise error.Abort(_('can only histedit a changeset together '
1982 'with all its descendants'))
1982 'with all its descendants'))
1983 if repo.revs('(%ld) and merge()', revs):
1983 if repo.revs('(%ld) and merge()', revs):
1984 raise error.Abort(_('cannot edit history that contains merges'))
1984 raise error.Abort(_('cannot edit history that contains merges'))
1985 root = repo[revs.first()] # list is already sorted by repo.revs()
1985 root = repo[revs.first()] # list is already sorted by repo.revs()
1986 if not root.mutable():
1986 if not root.mutable():
1987 raise error.Abort(_('cannot edit public changeset: %s') % root,
1987 raise error.Abort(_('cannot edit public changeset: %s') % root,
1988 hint=_("see 'hg help phases' for details"))
1988 hint=_("see 'hg help phases' for details"))
1989 return pycompat.maplist(repo.changelog.node, revs)
1989 return pycompat.maplist(repo.changelog.node, revs)
1990
1990
1991 def ruleeditor(repo, ui, actions, editcomment=""):
1991 def ruleeditor(repo, ui, actions, editcomment=""):
1992 """open an editor to edit rules
1992 """open an editor to edit rules
1993
1993
1994 rules are in the format [ [act, ctx], ...] like in state.rules
1994 rules are in the format [ [act, ctx], ...] like in state.rules
1995 """
1995 """
1996 if repo.ui.configbool("experimental", "histedit.autoverb"):
1996 if repo.ui.configbool("experimental", "histedit.autoverb"):
1997 newact = util.sortdict()
1997 newact = util.sortdict()
1998 for act in actions:
1998 for act in actions:
1999 ctx = repo[act.node]
1999 ctx = repo[act.node]
2000 summary = _getsummary(ctx)
2000 summary = _getsummary(ctx)
2001 fword = summary.split(' ', 1)[0].lower()
2001 fword = summary.split(' ', 1)[0].lower()
2002 added = False
2002 added = False
2003
2003
2004 # if it doesn't end with the special character '!' just skip this
2004 # if it doesn't end with the special character '!' just skip this
2005 if fword.endswith('!'):
2005 if fword.endswith('!'):
2006 fword = fword[:-1]
2006 fword = fword[:-1]
2007 if fword in primaryactions | secondaryactions | tertiaryactions:
2007 if fword in primaryactions | secondaryactions | tertiaryactions:
2008 act.verb = fword
2008 act.verb = fword
2009 # get the target summary
2009 # get the target summary
2010 tsum = summary[len(fword) + 1:].lstrip()
2010 tsum = summary[len(fword) + 1:].lstrip()
2011 # safe but slow: reverse iterate over the actions so we
2011 # safe but slow: reverse iterate over the actions so we
2012 # don't clash on two commits having the same summary
2012 # don't clash on two commits having the same summary
2013 for na, l in reversed(list(newact.iteritems())):
2013 for na, l in reversed(list(newact.iteritems())):
2014 actx = repo[na.node]
2014 actx = repo[na.node]
2015 asum = _getsummary(actx)
2015 asum = _getsummary(actx)
2016 if asum == tsum:
2016 if asum == tsum:
2017 added = True
2017 added = True
2018 l.append(act)
2018 l.append(act)
2019 break
2019 break
2020
2020
2021 if not added:
2021 if not added:
2022 newact[act] = []
2022 newact[act] = []
2023
2023
2024 # copy over and flatten the new list
2024 # copy over and flatten the new list
2025 actions = []
2025 actions = []
2026 for na, l in newact.iteritems():
2026 for na, l in newact.iteritems():
2027 actions.append(na)
2027 actions.append(na)
2028 actions += l
2028 actions += l
2029
2029
2030 rules = '\n'.join([act.torule() for act in actions])
2030 rules = '\n'.join([act.torule() for act in actions])
2031 rules += '\n\n'
2031 rules += '\n\n'
2032 rules += editcomment
2032 rules += editcomment
2033 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2033 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2034 repopath=repo.path, action='histedit')
2034 repopath=repo.path, action='histedit')
2035
2035
2036 # Save edit rules in .hg/histedit-last-edit.txt in case
2036 # Save edit rules in .hg/histedit-last-edit.txt in case
2037 # the user needs to ask for help after something
2037 # the user needs to ask for help after something
2038 # surprising happens.
2038 # surprising happens.
2039 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2039 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2040 f.write(rules)
2040 f.write(rules)
2041
2041
2042 return rules
2042 return rules
2043
2043
2044 def parserules(rules, state):
2044 def parserules(rules, state):
2045 """Read the histedit rules string and return list of action objects """
2045 """Read the histedit rules string and return list of action objects """
2046 rules = [l for l in (r.strip() for r in rules.splitlines())
2046 rules = [l for l in (r.strip() for r in rules.splitlines())
2047 if l and not l.startswith('#')]
2047 if l and not l.startswith('#')]
2048 actions = []
2048 actions = []
2049 for r in rules:
2049 for r in rules:
2050 if ' ' not in r:
2050 if ' ' not in r:
2051 raise error.ParseError(_('malformed line "%s"') % r)
2051 raise error.ParseError(_('malformed line "%s"') % r)
2052 verb, rest = r.split(' ', 1)
2052 verb, rest = r.split(' ', 1)
2053
2053
2054 if verb not in actiontable:
2054 if verb not in actiontable:
2055 raise error.ParseError(_('unknown action "%s"') % verb)
2055 raise error.ParseError(_('unknown action "%s"') % verb)
2056
2056
2057 action = actiontable[verb].fromrule(state, rest)
2057 action = actiontable[verb].fromrule(state, rest)
2058 actions.append(action)
2058 actions.append(action)
2059 return actions
2059 return actions
2060
2060
2061 def warnverifyactions(ui, repo, actions, state, ctxs):
2061 def warnverifyactions(ui, repo, actions, state, ctxs):
2062 try:
2062 try:
2063 verifyactions(actions, state, ctxs)
2063 verifyactions(actions, state, ctxs)
2064 except error.ParseError:
2064 except error.ParseError:
2065 if repo.vfs.exists('histedit-last-edit.txt'):
2065 if repo.vfs.exists('histedit-last-edit.txt'):
2066 ui.warn(_('warning: histedit rules saved '
2066 ui.warn(_('warning: histedit rules saved '
2067 'to: .hg/histedit-last-edit.txt\n'))
2067 'to: .hg/histedit-last-edit.txt\n'))
2068 raise
2068 raise
2069
2069
2070 def verifyactions(actions, state, ctxs):
2070 def verifyactions(actions, state, ctxs):
2071 """Verify that there exists exactly one action per given changeset and
2071 """Verify that there exists exactly one action per given changeset and
2072 other constraints.
2072 other constraints.
2073
2073
2074 Will abort if there are to many or too few rules, a malformed rule,
2074 Will abort if there are to many or too few rules, a malformed rule,
2075 or a rule on a changeset outside of the user-given range.
2075 or a rule on a changeset outside of the user-given range.
2076 """
2076 """
2077 expected = set(c.node() for c in ctxs)
2077 expected = set(c.node() for c in ctxs)
2078 seen = set()
2078 seen = set()
2079 prev = None
2079 prev = None
2080
2080
2081 if actions and actions[0].verb in ['roll', 'fold']:
2081 if actions and actions[0].verb in ['roll', 'fold']:
2082 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2082 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2083 actions[0].verb)
2083 actions[0].verb)
2084
2084
2085 for action in actions:
2085 for action in actions:
2086 action.verify(prev, expected, seen)
2086 action.verify(prev, expected, seen)
2087 prev = action
2087 prev = action
2088 if action.node is not None:
2088 if action.node is not None:
2089 seen.add(action.node)
2089 seen.add(action.node)
2090 missing = sorted(expected - seen) # sort to stabilize output
2090 missing = sorted(expected - seen) # sort to stabilize output
2091
2091
2092 if state.repo.ui.configbool('histedit', 'dropmissing'):
2092 if state.repo.ui.configbool('histedit', 'dropmissing'):
2093 if len(actions) == 0:
2093 if len(actions) == 0:
2094 raise error.ParseError(_('no rules provided'),
2094 raise error.ParseError(_('no rules provided'),
2095 hint=_('use strip extension to remove commits'))
2095 hint=_('use strip extension to remove commits'))
2096
2096
2097 drops = [drop(state, n) for n in missing]
2097 drops = [drop(state, n) for n in missing]
2098 # put the in the beginning so they execute immediately and
2098 # put the in the beginning so they execute immediately and
2099 # don't show in the edit-plan in the future
2099 # don't show in the edit-plan in the future
2100 actions[:0] = drops
2100 actions[:0] = drops
2101 elif missing:
2101 elif missing:
2102 raise error.ParseError(_('missing rules for changeset %s') %
2102 raise error.ParseError(_('missing rules for changeset %s') %
2103 node.short(missing[0]),
2103 node.short(missing[0]),
2104 hint=_('use "drop %s" to discard, see also: '
2104 hint=_('use "drop %s" to discard, see also: '
2105 "'hg help -e histedit.config'")
2105 "'hg help -e histedit.config'")
2106 % node.short(missing[0]))
2106 % node.short(missing[0]))
2107
2107
2108 def adjustreplacementsfrommarkers(repo, oldreplacements):
2108 def adjustreplacementsfrommarkers(repo, oldreplacements):
2109 """Adjust replacements from obsolescence markers
2109 """Adjust replacements from obsolescence markers
2110
2110
2111 Replacements structure is originally generated based on
2111 Replacements structure is originally generated based on
2112 histedit's state and does not account for changes that are
2112 histedit's state and does not account for changes that are
2113 not recorded there. This function fixes that by adding
2113 not recorded there. This function fixes that by adding
2114 data read from obsolescence markers"""
2114 data read from obsolescence markers"""
2115 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2115 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2116 return oldreplacements
2116 return oldreplacements
2117
2117
2118 unfi = repo.unfiltered()
2118 unfi = repo.unfiltered()
2119 nm = unfi.changelog.nodemap
2119 nm = unfi.changelog.nodemap
2120 obsstore = repo.obsstore
2120 obsstore = repo.obsstore
2121 newreplacements = list(oldreplacements)
2121 newreplacements = list(oldreplacements)
2122 oldsuccs = [r[1] for r in oldreplacements]
2122 oldsuccs = [r[1] for r in oldreplacements]
2123 # successors that have already been added to succstocheck once
2123 # successors that have already been added to succstocheck once
2124 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2124 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2125 succstocheck = list(seensuccs)
2125 succstocheck = list(seensuccs)
2126 while succstocheck:
2126 while succstocheck:
2127 n = succstocheck.pop()
2127 n = succstocheck.pop()
2128 missing = nm.get(n) is None
2128 missing = nm.get(n) is None
2129 markers = obsstore.successors.get(n, ())
2129 markers = obsstore.successors.get(n, ())
2130 if missing and not markers:
2130 if missing and not markers:
2131 # dead end, mark it as such
2131 # dead end, mark it as such
2132 newreplacements.append((n, ()))
2132 newreplacements.append((n, ()))
2133 for marker in markers:
2133 for marker in markers:
2134 nsuccs = marker[1]
2134 nsuccs = marker[1]
2135 newreplacements.append((n, nsuccs))
2135 newreplacements.append((n, nsuccs))
2136 for nsucc in nsuccs:
2136 for nsucc in nsuccs:
2137 if nsucc not in seensuccs:
2137 if nsucc not in seensuccs:
2138 seensuccs.add(nsucc)
2138 seensuccs.add(nsucc)
2139 succstocheck.append(nsucc)
2139 succstocheck.append(nsucc)
2140
2140
2141 return newreplacements
2141 return newreplacements
2142
2142
2143 def processreplacement(state):
2143 def processreplacement(state):
2144 """process the list of replacements to return
2144 """process the list of replacements to return
2145
2145
2146 1) the final mapping between original and created nodes
2146 1) the final mapping between original and created nodes
2147 2) the list of temporary node created by histedit
2147 2) the list of temporary node created by histedit
2148 3) the list of new commit created by histedit"""
2148 3) the list of new commit created by histedit"""
2149 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2149 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2150 allsuccs = set()
2150 allsuccs = set()
2151 replaced = set()
2151 replaced = set()
2152 fullmapping = {}
2152 fullmapping = {}
2153 # initialize basic set
2153 # initialize basic set
2154 # fullmapping records all operations recorded in replacement
2154 # fullmapping records all operations recorded in replacement
2155 for rep in replacements:
2155 for rep in replacements:
2156 allsuccs.update(rep[1])
2156 allsuccs.update(rep[1])
2157 replaced.add(rep[0])
2157 replaced.add(rep[0])
2158 fullmapping.setdefault(rep[0], set()).update(rep[1])
2158 fullmapping.setdefault(rep[0], set()).update(rep[1])
2159 new = allsuccs - replaced
2159 new = allsuccs - replaced
2160 tmpnodes = allsuccs & replaced
2160 tmpnodes = allsuccs & replaced
2161 # Reduce content fullmapping into direct relation between original nodes
2161 # Reduce content fullmapping into direct relation between original nodes
2162 # and final node created during history edition
2162 # and final node created during history edition
2163 # Dropped changeset are replaced by an empty list
2163 # Dropped changeset are replaced by an empty list
2164 toproceed = set(fullmapping)
2164 toproceed = set(fullmapping)
2165 final = {}
2165 final = {}
2166 while toproceed:
2166 while toproceed:
2167 for x in list(toproceed):
2167 for x in list(toproceed):
2168 succs = fullmapping[x]
2168 succs = fullmapping[x]
2169 for s in list(succs):
2169 for s in list(succs):
2170 if s in toproceed:
2170 if s in toproceed:
2171 # non final node with unknown closure
2171 # non final node with unknown closure
2172 # We can't process this now
2172 # We can't process this now
2173 break
2173 break
2174 elif s in final:
2174 elif s in final:
2175 # non final node, replace with closure
2175 # non final node, replace with closure
2176 succs.remove(s)
2176 succs.remove(s)
2177 succs.update(final[s])
2177 succs.update(final[s])
2178 else:
2178 else:
2179 final[x] = succs
2179 final[x] = succs
2180 toproceed.remove(x)
2180 toproceed.remove(x)
2181 # remove tmpnodes from final mapping
2181 # remove tmpnodes from final mapping
2182 for n in tmpnodes:
2182 for n in tmpnodes:
2183 del final[n]
2183 del final[n]
2184 # we expect all changes involved in final to exist in the repo
2184 # we expect all changes involved in final to exist in the repo
2185 # turn `final` into list (topologically sorted)
2185 # turn `final` into list (topologically sorted)
2186 nm = state.repo.changelog.nodemap
2186 nm = state.repo.changelog.nodemap
2187 for prec, succs in final.items():
2187 for prec, succs in final.items():
2188 final[prec] = sorted(succs, key=nm.get)
2188 final[prec] = sorted(succs, key=nm.get)
2189
2189
2190 # computed topmost element (necessary for bookmark)
2190 # computed topmost element (necessary for bookmark)
2191 if new:
2191 if new:
2192 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2192 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2193 elif not final:
2193 elif not final:
2194 # Nothing rewritten at all. we won't need `newtopmost`
2194 # Nothing rewritten at all. we won't need `newtopmost`
2195 # It is the same as `oldtopmost` and `processreplacement` know it
2195 # It is the same as `oldtopmost` and `processreplacement` know it
2196 newtopmost = None
2196 newtopmost = None
2197 else:
2197 else:
2198 # every body died. The newtopmost is the parent of the root.
2198 # every body died. The newtopmost is the parent of the root.
2199 r = state.repo.changelog.rev
2199 r = state.repo.changelog.rev
2200 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2200 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2201
2201
2202 return final, tmpnodes, new, newtopmost
2202 return final, tmpnodes, new, newtopmost
2203
2203
2204 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2204 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2205 """Move bookmark from oldtopmost to newly created topmost
2205 """Move bookmark from oldtopmost to newly created topmost
2206
2206
2207 This is arguably a feature and we may only want that for the active
2207 This is arguably a feature and we may only want that for the active
2208 bookmark. But the behavior is kept compatible with the old version for now.
2208 bookmark. But the behavior is kept compatible with the old version for now.
2209 """
2209 """
2210 if not oldtopmost or not newtopmost:
2210 if not oldtopmost or not newtopmost:
2211 return
2211 return
2212 oldbmarks = repo.nodebookmarks(oldtopmost)
2212 oldbmarks = repo.nodebookmarks(oldtopmost)
2213 if oldbmarks:
2213 if oldbmarks:
2214 with repo.lock(), repo.transaction('histedit') as tr:
2214 with repo.lock(), repo.transaction('histedit') as tr:
2215 marks = repo._bookmarks
2215 marks = repo._bookmarks
2216 changes = []
2216 changes = []
2217 for name in oldbmarks:
2217 for name in oldbmarks:
2218 changes.append((name, newtopmost))
2218 changes.append((name, newtopmost))
2219 marks.applychanges(repo, tr, changes)
2219 marks.applychanges(repo, tr, changes)
2220
2220
2221 def cleanupnode(ui, repo, nodes, nobackup=False):
2221 def cleanupnode(ui, repo, nodes, nobackup=False):
2222 """strip a group of nodes from the repository
2222 """strip a group of nodes from the repository
2223
2223
2224 The set of node to strip may contains unknown nodes."""
2224 The set of node to strip may contains unknown nodes."""
2225 with repo.lock():
2225 with repo.lock():
2226 # do not let filtering get in the way of the cleanse
2226 # do not let filtering get in the way of the cleanse
2227 # we should probably get rid of obsolescence marker created during the
2227 # we should probably get rid of obsolescence marker created during the
2228 # histedit, but we currently do not have such information.
2228 # histedit, but we currently do not have such information.
2229 repo = repo.unfiltered()
2229 repo = repo.unfiltered()
2230 # Find all nodes that need to be stripped
2230 # Find all nodes that need to be stripped
2231 # (we use %lr instead of %ln to silently ignore unknown items)
2231 # (we use %lr instead of %ln to silently ignore unknown items)
2232 nm = repo.changelog.nodemap
2232 nm = repo.changelog.nodemap
2233 nodes = sorted(n for n in nodes if n in nm)
2233 nodes = sorted(n for n in nodes if n in nm)
2234 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2234 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2235 if roots:
2235 if roots:
2236 backup = not nobackup
2236 backup = not nobackup
2237 repair.strip(ui, repo, roots, backup=backup)
2237 repair.strip(ui, repo, roots, backup=backup)
2238
2238
2239 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2239 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2240 if isinstance(nodelist, str):
2240 if isinstance(nodelist, str):
2241 nodelist = [nodelist]
2241 nodelist = [nodelist]
2242 state = histeditstate(repo)
2242 state = histeditstate(repo)
2243 if state.inprogress():
2243 if state.inprogress():
2244 state.read()
2244 state.read()
2245 histedit_nodes = {action.node for action
2245 histedit_nodes = {action.node for action
2246 in state.actions if action.node}
2246 in state.actions if action.node}
2247 common_nodes = histedit_nodes & set(nodelist)
2247 common_nodes = histedit_nodes & set(nodelist)
2248 if common_nodes:
2248 if common_nodes:
2249 raise error.Abort(_("histedit in progress, can't strip %s")
2249 raise error.Abort(_("histedit in progress, can't strip %s")
2250 % ', '.join(node.short(x) for x in common_nodes))
2250 % ', '.join(node.short(x) for x in common_nodes))
2251 return orig(ui, repo, nodelist, *args, **kwargs)
2251 return orig(ui, repo, nodelist, *args, **kwargs)
2252
2252
2253 extensions.wrapfunction(repair, 'strip', stripwrapper)
2253 extensions.wrapfunction(repair, 'strip', stripwrapper)
2254
2254
2255 def summaryhook(ui, repo):
2255 def summaryhook(ui, repo):
2256 state = histeditstate(repo)
2256 state = histeditstate(repo)
2257 if not state.inprogress():
2257 if not state.inprogress():
2258 return
2258 return
2259 state.read()
2259 state.read()
2260 if state.actions:
2260 if state.actions:
2261 # i18n: column positioning for "hg summary"
2261 # i18n: column positioning for "hg summary"
2262 ui.write(_('hist: %s (histedit --continue)\n') %
2262 ui.write(_('hist: %s (histedit --continue)\n') %
2263 (ui.label(_('%d remaining'), 'histedit.remaining') %
2263 (ui.label(_('%d remaining'), 'histedit.remaining') %
2264 len(state.actions)))
2264 len(state.actions)))
2265
2265
2266 def extsetup(ui):
2266 def extsetup(ui):
2267 cmdutil.summaryhooks.add('histedit', summaryhook)
2267 cmdutil.summaryhooks.add('histedit', summaryhook)
2268 cmdutil.unfinishedstates.append(
2268 cmdutil.unfinishedstates.append(
2269 ['histedit-state', False, True, _('histedit in progress'),
2269 ['histedit-state', False, True, _('histedit in progress'),
2270 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2270 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2271 cmdutil.afterresolvedstates.append(
2271 cmdutil.afterresolvedstates.append(
2272 ['histedit-state', _('hg histedit --continue')])
2272 ['histedit-state', _('hg histedit --continue')])
General Comments 0
You need to be logged in to leave comments. Login now