##// END OF EJS Templates
chistedit: use context manager to set verbose ui...
Jordi Gutiérrez Hermoso -
r42336:9e40c589 default
parent child Browse files
Show More
@@ -1,2295 +1,2296 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 repo.ui.popbuffer()
525 repo.ui.popbuffer()
526 stats = applychanges(repo.ui, repo, rulectx, {})
526 stats = applychanges(repo.ui, repo, rulectx, {})
527 repo.dirstate.setbranch(rulectx.branch())
527 repo.dirstate.setbranch(rulectx.branch())
528 if stats.unresolvedcount:
528 if stats.unresolvedcount:
529 raise error.InterventionRequired(
529 raise error.InterventionRequired(
530 _('Fix up the change (%s %s)') %
530 _('Fix up the change (%s %s)') %
531 (self.verb, node.short(self.node)),
531 (self.verb, node.short(self.node)),
532 hint=_('hg histedit --continue to resume'))
532 hint=_('hg histedit --continue to resume'))
533
533
534 def continuedirty(self):
534 def continuedirty(self):
535 """Continues the action when changes have been applied to the working
535 """Continues the action when changes have been applied to the working
536 copy. The default behavior is to commit the dirty changes."""
536 copy. The default behavior is to commit the dirty changes."""
537 repo = self.repo
537 repo = self.repo
538 rulectx = repo[self.node]
538 rulectx = repo[self.node]
539
539
540 editor = self.commiteditor()
540 editor = self.commiteditor()
541 commit = commitfuncfor(repo, rulectx)
541 commit = commitfuncfor(repo, rulectx)
542 if repo.ui.configbool('rewrite', 'update-timestamp'):
542 if repo.ui.configbool('rewrite', 'update-timestamp'):
543 date = dateutil.makedate()
543 date = dateutil.makedate()
544 else:
544 else:
545 date = rulectx.date()
545 date = rulectx.date()
546 commit(text=rulectx.description(), user=rulectx.user(),
546 commit(text=rulectx.description(), user=rulectx.user(),
547 date=date, extra=rulectx.extra(), editor=editor)
547 date=date, extra=rulectx.extra(), editor=editor)
548
548
549 def commiteditor(self):
549 def commiteditor(self):
550 """The editor to be used to edit the commit message."""
550 """The editor to be used to edit the commit message."""
551 return False
551 return False
552
552
553 def continueclean(self):
553 def continueclean(self):
554 """Continues the action when the working copy is clean. The default
554 """Continues the action when the working copy is clean. The default
555 behavior is to accept the current commit as the new version of the
555 behavior is to accept the current commit as the new version of the
556 rulectx."""
556 rulectx."""
557 ctx = self.repo['.']
557 ctx = self.repo['.']
558 if ctx.node() == self.state.parentctxnode:
558 if ctx.node() == self.state.parentctxnode:
559 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
559 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
560 node.short(self.node))
560 node.short(self.node))
561 return ctx, [(self.node, tuple())]
561 return ctx, [(self.node, tuple())]
562 if ctx.node() == self.node:
562 if ctx.node() == self.node:
563 # Nothing changed
563 # Nothing changed
564 return ctx, []
564 return ctx, []
565 return ctx, [(self.node, (ctx.node(),))]
565 return ctx, [(self.node, (ctx.node(),))]
566
566
567 def commitfuncfor(repo, src):
567 def commitfuncfor(repo, src):
568 """Build a commit function for the replacement of <src>
568 """Build a commit function for the replacement of <src>
569
569
570 This function ensure we apply the same treatment to all changesets.
570 This function ensure we apply the same treatment to all changesets.
571
571
572 - Add a 'histedit_source' entry in extra.
572 - Add a 'histedit_source' entry in extra.
573
573
574 Note that fold has its own separated logic because its handling is a bit
574 Note that fold has its own separated logic because its handling is a bit
575 different and not easily factored out of the fold method.
575 different and not easily factored out of the fold method.
576 """
576 """
577 phasemin = src.phase()
577 phasemin = src.phase()
578 def commitfunc(**kwargs):
578 def commitfunc(**kwargs):
579 overrides = {('phases', 'new-commit'): phasemin}
579 overrides = {('phases', 'new-commit'): phasemin}
580 with repo.ui.configoverride(overrides, 'histedit'):
580 with repo.ui.configoverride(overrides, 'histedit'):
581 extra = kwargs.get(r'extra', {}).copy()
581 extra = kwargs.get(r'extra', {}).copy()
582 extra['histedit_source'] = src.hex()
582 extra['histedit_source'] = src.hex()
583 kwargs[r'extra'] = extra
583 kwargs[r'extra'] = extra
584 return repo.commit(**kwargs)
584 return repo.commit(**kwargs)
585 return commitfunc
585 return commitfunc
586
586
587 def applychanges(ui, repo, ctx, opts):
587 def applychanges(ui, repo, ctx, opts):
588 """Merge changeset from ctx (only) in the current working directory"""
588 """Merge changeset from ctx (only) in the current working directory"""
589 wcpar = repo.dirstate.p1()
589 wcpar = repo.dirstate.p1()
590 if ctx.p1().node() == wcpar:
590 if ctx.p1().node() == wcpar:
591 # edits are "in place" we do not need to make any merge,
591 # edits are "in place" we do not need to make any merge,
592 # just applies changes on parent for editing
592 # just applies changes on parent for editing
593 ui.pushbuffer()
593 ui.pushbuffer()
594 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
594 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
595 stats = mergemod.updateresult(0, 0, 0, 0)
595 stats = mergemod.updateresult(0, 0, 0, 0)
596 ui.popbuffer()
596 ui.popbuffer()
597 else:
597 else:
598 try:
598 try:
599 # ui.forcemerge is an internal variable, do not document
599 # ui.forcemerge is an internal variable, do not document
600 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
600 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
601 'histedit')
601 'histedit')
602 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
602 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
603 finally:
603 finally:
604 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
604 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
605 return stats
605 return stats
606
606
607 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
607 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
608 """collapse the set of revisions from first to last as new one.
608 """collapse the set of revisions from first to last as new one.
609
609
610 Expected commit options are:
610 Expected commit options are:
611 - message
611 - message
612 - date
612 - date
613 - username
613 - username
614 Commit message is edited in all cases.
614 Commit message is edited in all cases.
615
615
616 This function works in memory."""
616 This function works in memory."""
617 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
617 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
618 if not ctxs:
618 if not ctxs:
619 return None
619 return None
620 for c in ctxs:
620 for c in ctxs:
621 if not c.mutable():
621 if not c.mutable():
622 raise error.ParseError(
622 raise error.ParseError(
623 _("cannot fold into public change %s") % node.short(c.node()))
623 _("cannot fold into public change %s") % node.short(c.node()))
624 base = firstctx.p1()
624 base = firstctx.p1()
625
625
626 # commit a new version of the old changeset, including the update
626 # commit a new version of the old changeset, including the update
627 # collect all files which might be affected
627 # collect all files which might be affected
628 files = set()
628 files = set()
629 for ctx in ctxs:
629 for ctx in ctxs:
630 files.update(ctx.files())
630 files.update(ctx.files())
631
631
632 # Recompute copies (avoid recording a -> b -> a)
632 # Recompute copies (avoid recording a -> b -> a)
633 copied = copies.pathcopies(base, lastctx)
633 copied = copies.pathcopies(base, lastctx)
634
634
635 # prune files which were reverted by the updates
635 # prune files which were reverted by the updates
636 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
636 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
637 # commit version of these files as defined by head
637 # commit version of these files as defined by head
638 headmf = lastctx.manifest()
638 headmf = lastctx.manifest()
639 def filectxfn(repo, ctx, path):
639 def filectxfn(repo, ctx, path):
640 if path in headmf:
640 if path in headmf:
641 fctx = lastctx[path]
641 fctx = lastctx[path]
642 flags = fctx.flags()
642 flags = fctx.flags()
643 mctx = context.memfilectx(repo, ctx,
643 mctx = context.memfilectx(repo, ctx,
644 fctx.path(), fctx.data(),
644 fctx.path(), fctx.data(),
645 islink='l' in flags,
645 islink='l' in flags,
646 isexec='x' in flags,
646 isexec='x' in flags,
647 copysource=copied.get(path))
647 copysource=copied.get(path))
648 return mctx
648 return mctx
649 return None
649 return None
650
650
651 if commitopts.get('message'):
651 if commitopts.get('message'):
652 message = commitopts['message']
652 message = commitopts['message']
653 else:
653 else:
654 message = firstctx.description()
654 message = firstctx.description()
655 user = commitopts.get('user')
655 user = commitopts.get('user')
656 date = commitopts.get('date')
656 date = commitopts.get('date')
657 extra = commitopts.get('extra')
657 extra = commitopts.get('extra')
658
658
659 parents = (firstctx.p1().node(), firstctx.p2().node())
659 parents = (firstctx.p1().node(), firstctx.p2().node())
660 editor = None
660 editor = None
661 if not skipprompt:
661 if not skipprompt:
662 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
662 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
663 new = context.memctx(repo,
663 new = context.memctx(repo,
664 parents=parents,
664 parents=parents,
665 text=message,
665 text=message,
666 files=files,
666 files=files,
667 filectxfn=filectxfn,
667 filectxfn=filectxfn,
668 user=user,
668 user=user,
669 date=date,
669 date=date,
670 extra=extra,
670 extra=extra,
671 editor=editor)
671 editor=editor)
672 return repo.commitctx(new)
672 return repo.commitctx(new)
673
673
674 def _isdirtywc(repo):
674 def _isdirtywc(repo):
675 return repo[None].dirty(missing=True)
675 return repo[None].dirty(missing=True)
676
676
677 def abortdirty():
677 def abortdirty():
678 raise error.Abort(_('working copy has pending changes'),
678 raise error.Abort(_('working copy has pending changes'),
679 hint=_('amend, commit, or revert them and run histedit '
679 hint=_('amend, commit, or revert them and run histedit '
680 '--continue, or abort with histedit --abort'))
680 '--continue, or abort with histedit --abort'))
681
681
682 def action(verbs, message, priority=False, internal=False):
682 def action(verbs, message, priority=False, internal=False):
683 def wrap(cls):
683 def wrap(cls):
684 assert not priority or not internal
684 assert not priority or not internal
685 verb = verbs[0]
685 verb = verbs[0]
686 if priority:
686 if priority:
687 primaryactions.add(verb)
687 primaryactions.add(verb)
688 elif internal:
688 elif internal:
689 internalactions.add(verb)
689 internalactions.add(verb)
690 elif len(verbs) > 1:
690 elif len(verbs) > 1:
691 secondaryactions.add(verb)
691 secondaryactions.add(verb)
692 else:
692 else:
693 tertiaryactions.add(verb)
693 tertiaryactions.add(verb)
694
694
695 cls.verb = verb
695 cls.verb = verb
696 cls.verbs = verbs
696 cls.verbs = verbs
697 cls.message = message
697 cls.message = message
698 for verb in verbs:
698 for verb in verbs:
699 actiontable[verb] = cls
699 actiontable[verb] = cls
700 return cls
700 return cls
701 return wrap
701 return wrap
702
702
703 @action(['pick', 'p'],
703 @action(['pick', 'p'],
704 _('use commit'),
704 _('use commit'),
705 priority=True)
705 priority=True)
706 class pick(histeditaction):
706 class pick(histeditaction):
707 def run(self):
707 def run(self):
708 rulectx = self.repo[self.node]
708 rulectx = self.repo[self.node]
709 if rulectx.p1().node() == self.state.parentctxnode:
709 if rulectx.p1().node() == self.state.parentctxnode:
710 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
710 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
711 return rulectx, []
711 return rulectx, []
712
712
713 return super(pick, self).run()
713 return super(pick, self).run()
714
714
715 @action(['edit', 'e'],
715 @action(['edit', 'e'],
716 _('use commit, but stop for amending'),
716 _('use commit, but stop for amending'),
717 priority=True)
717 priority=True)
718 class edit(histeditaction):
718 class edit(histeditaction):
719 def run(self):
719 def run(self):
720 repo = self.repo
720 repo = self.repo
721 rulectx = repo[self.node]
721 rulectx = repo[self.node]
722 hg.update(repo, self.state.parentctxnode, quietempty=True)
722 hg.update(repo, self.state.parentctxnode, quietempty=True)
723 applychanges(repo.ui, repo, rulectx, {})
723 applychanges(repo.ui, repo, rulectx, {})
724 raise error.InterventionRequired(
724 raise error.InterventionRequired(
725 _('Editing (%s), you may commit or record as needed now.')
725 _('Editing (%s), you may commit or record as needed now.')
726 % node.short(self.node),
726 % node.short(self.node),
727 hint=_('hg histedit --continue to resume'))
727 hint=_('hg histedit --continue to resume'))
728
728
729 def commiteditor(self):
729 def commiteditor(self):
730 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
730 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
731
731
732 @action(['fold', 'f'],
732 @action(['fold', 'f'],
733 _('use commit, but combine it with the one above'))
733 _('use commit, but combine it with the one above'))
734 class fold(histeditaction):
734 class fold(histeditaction):
735 def verify(self, prev, expected, seen):
735 def verify(self, prev, expected, seen):
736 """ Verifies semantic correctness of the fold rule"""
736 """ Verifies semantic correctness of the fold rule"""
737 super(fold, self).verify(prev, expected, seen)
737 super(fold, self).verify(prev, expected, seen)
738 repo = self.repo
738 repo = self.repo
739 if not prev:
739 if not prev:
740 c = repo[self.node].p1()
740 c = repo[self.node].p1()
741 elif not prev.verb in ('pick', 'base'):
741 elif not prev.verb in ('pick', 'base'):
742 return
742 return
743 else:
743 else:
744 c = repo[prev.node]
744 c = repo[prev.node]
745 if not c.mutable():
745 if not c.mutable():
746 raise error.ParseError(
746 raise error.ParseError(
747 _("cannot fold into public change %s") % node.short(c.node()))
747 _("cannot fold into public change %s") % node.short(c.node()))
748
748
749
749
750 def continuedirty(self):
750 def continuedirty(self):
751 repo = self.repo
751 repo = self.repo
752 rulectx = repo[self.node]
752 rulectx = repo[self.node]
753
753
754 commit = commitfuncfor(repo, rulectx)
754 commit = commitfuncfor(repo, rulectx)
755 commit(text='fold-temp-revision %s' % node.short(self.node),
755 commit(text='fold-temp-revision %s' % node.short(self.node),
756 user=rulectx.user(), date=rulectx.date(),
756 user=rulectx.user(), date=rulectx.date(),
757 extra=rulectx.extra())
757 extra=rulectx.extra())
758
758
759 def continueclean(self):
759 def continueclean(self):
760 repo = self.repo
760 repo = self.repo
761 ctx = repo['.']
761 ctx = repo['.']
762 rulectx = repo[self.node]
762 rulectx = repo[self.node]
763 parentctxnode = self.state.parentctxnode
763 parentctxnode = self.state.parentctxnode
764 if ctx.node() == parentctxnode:
764 if ctx.node() == parentctxnode:
765 repo.ui.warn(_('%s: empty changeset\n') %
765 repo.ui.warn(_('%s: empty changeset\n') %
766 node.short(self.node))
766 node.short(self.node))
767 return ctx, [(self.node, (parentctxnode,))]
767 return ctx, [(self.node, (parentctxnode,))]
768
768
769 parentctx = repo[parentctxnode]
769 parentctx = repo[parentctxnode]
770 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
770 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
771 parentctx.rev(),
771 parentctx.rev(),
772 parentctx.rev()))
772 parentctx.rev()))
773 if not newcommits:
773 if not newcommits:
774 repo.ui.warn(_('%s: cannot fold - working copy is not a '
774 repo.ui.warn(_('%s: cannot fold - working copy is not a '
775 'descendant of previous commit %s\n') %
775 'descendant of previous commit %s\n') %
776 (node.short(self.node), node.short(parentctxnode)))
776 (node.short(self.node), node.short(parentctxnode)))
777 return ctx, [(self.node, (ctx.node(),))]
777 return ctx, [(self.node, (ctx.node(),))]
778
778
779 middlecommits = newcommits.copy()
779 middlecommits = newcommits.copy()
780 middlecommits.discard(ctx.node())
780 middlecommits.discard(ctx.node())
781
781
782 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
782 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
783 middlecommits)
783 middlecommits)
784
784
785 def skipprompt(self):
785 def skipprompt(self):
786 """Returns true if the rule should skip the message editor.
786 """Returns true if the rule should skip the message editor.
787
787
788 For example, 'fold' wants to show an editor, but 'rollup'
788 For example, 'fold' wants to show an editor, but 'rollup'
789 doesn't want to.
789 doesn't want to.
790 """
790 """
791 return False
791 return False
792
792
793 def mergedescs(self):
793 def mergedescs(self):
794 """Returns true if the rule should merge messages of multiple changes.
794 """Returns true if the rule should merge messages of multiple changes.
795
795
796 This exists mainly so that 'rollup' rules can be a subclass of
796 This exists mainly so that 'rollup' rules can be a subclass of
797 'fold'.
797 'fold'.
798 """
798 """
799 return True
799 return True
800
800
801 def firstdate(self):
801 def firstdate(self):
802 """Returns true if the rule should preserve the date of the first
802 """Returns true if the rule should preserve the date of the first
803 change.
803 change.
804
804
805 This exists mainly so that 'rollup' rules can be a subclass of
805 This exists mainly so that 'rollup' rules can be a subclass of
806 'fold'.
806 'fold'.
807 """
807 """
808 return False
808 return False
809
809
810 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
810 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
811 parent = ctx.p1().node()
811 parent = ctx.p1().node()
812 hg.updaterepo(repo, parent, overwrite=False)
812 hg.updaterepo(repo, parent, overwrite=False)
813 ### prepare new commit data
813 ### prepare new commit data
814 commitopts = {}
814 commitopts = {}
815 commitopts['user'] = ctx.user()
815 commitopts['user'] = ctx.user()
816 # commit message
816 # commit message
817 if not self.mergedescs():
817 if not self.mergedescs():
818 newmessage = ctx.description()
818 newmessage = ctx.description()
819 else:
819 else:
820 newmessage = '\n***\n'.join(
820 newmessage = '\n***\n'.join(
821 [ctx.description()] +
821 [ctx.description()] +
822 [repo[r].description() for r in internalchanges] +
822 [repo[r].description() for r in internalchanges] +
823 [oldctx.description()]) + '\n'
823 [oldctx.description()]) + '\n'
824 commitopts['message'] = newmessage
824 commitopts['message'] = newmessage
825 # date
825 # date
826 if self.firstdate():
826 if self.firstdate():
827 commitopts['date'] = ctx.date()
827 commitopts['date'] = ctx.date()
828 else:
828 else:
829 commitopts['date'] = max(ctx.date(), oldctx.date())
829 commitopts['date'] = max(ctx.date(), oldctx.date())
830 # if date is to be updated to current
830 # if date is to be updated to current
831 if ui.configbool('rewrite', 'update-timestamp'):
831 if ui.configbool('rewrite', 'update-timestamp'):
832 commitopts['date'] = dateutil.makedate()
832 commitopts['date'] = dateutil.makedate()
833
833
834 extra = ctx.extra().copy()
834 extra = ctx.extra().copy()
835 # histedit_source
835 # histedit_source
836 # note: ctx is likely a temporary commit but that the best we can do
836 # note: ctx is likely a temporary commit but that the best we can do
837 # here. This is sufficient to solve issue3681 anyway.
837 # here. This is sufficient to solve issue3681 anyway.
838 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
838 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
839 commitopts['extra'] = extra
839 commitopts['extra'] = extra
840 phasemin = max(ctx.phase(), oldctx.phase())
840 phasemin = max(ctx.phase(), oldctx.phase())
841 overrides = {('phases', 'new-commit'): phasemin}
841 overrides = {('phases', 'new-commit'): phasemin}
842 with repo.ui.configoverride(overrides, 'histedit'):
842 with repo.ui.configoverride(overrides, 'histedit'):
843 n = collapse(repo, ctx, repo[newnode], commitopts,
843 n = collapse(repo, ctx, repo[newnode], commitopts,
844 skipprompt=self.skipprompt())
844 skipprompt=self.skipprompt())
845 if n is None:
845 if n is None:
846 return ctx, []
846 return ctx, []
847 hg.updaterepo(repo, n, overwrite=False)
847 hg.updaterepo(repo, n, overwrite=False)
848 replacements = [(oldctx.node(), (newnode,)),
848 replacements = [(oldctx.node(), (newnode,)),
849 (ctx.node(), (n,)),
849 (ctx.node(), (n,)),
850 (newnode, (n,)),
850 (newnode, (n,)),
851 ]
851 ]
852 for ich in internalchanges:
852 for ich in internalchanges:
853 replacements.append((ich, (n,)))
853 replacements.append((ich, (n,)))
854 return repo[n], replacements
854 return repo[n], replacements
855
855
856 @action(['base', 'b'],
856 @action(['base', 'b'],
857 _('checkout changeset and apply further changesets from there'))
857 _('checkout changeset and apply further changesets from there'))
858 class base(histeditaction):
858 class base(histeditaction):
859
859
860 def run(self):
860 def run(self):
861 if self.repo['.'].node() != self.node:
861 if self.repo['.'].node() != self.node:
862 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
862 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
863 return self.continueclean()
863 return self.continueclean()
864
864
865 def continuedirty(self):
865 def continuedirty(self):
866 abortdirty()
866 abortdirty()
867
867
868 def continueclean(self):
868 def continueclean(self):
869 basectx = self.repo['.']
869 basectx = self.repo['.']
870 return basectx, []
870 return basectx, []
871
871
872 def _verifynodeconstraints(self, prev, expected, seen):
872 def _verifynodeconstraints(self, prev, expected, seen):
873 # base can only be use with a node not in the edited set
873 # base can only be use with a node not in the edited set
874 if self.node in expected:
874 if self.node in expected:
875 msg = _('%s "%s" changeset was an edited list candidate')
875 msg = _('%s "%s" changeset was an edited list candidate')
876 raise error.ParseError(
876 raise error.ParseError(
877 msg % (self.verb, node.short(self.node)),
877 msg % (self.verb, node.short(self.node)),
878 hint=_('base must only use unlisted changesets'))
878 hint=_('base must only use unlisted changesets'))
879
879
880 @action(['_multifold'],
880 @action(['_multifold'],
881 _(
881 _(
882 """fold subclass used for when multiple folds happen in a row
882 """fold subclass used for when multiple folds happen in a row
883
883
884 We only want to fire the editor for the folded message once when
884 We only want to fire the editor for the folded message once when
885 (say) four changes are folded down into a single change. This is
885 (say) four changes are folded down into a single change. This is
886 similar to rollup, but we should preserve both messages so that
886 similar to rollup, but we should preserve both messages so that
887 when the last fold operation runs we can show the user all the
887 when the last fold operation runs we can show the user all the
888 commit messages in their editor.
888 commit messages in their editor.
889 """),
889 """),
890 internal=True)
890 internal=True)
891 class _multifold(fold):
891 class _multifold(fold):
892 def skipprompt(self):
892 def skipprompt(self):
893 return True
893 return True
894
894
895 @action(["roll", "r"],
895 @action(["roll", "r"],
896 _("like fold, but discard this commit's description and date"))
896 _("like fold, but discard this commit's description and date"))
897 class rollup(fold):
897 class rollup(fold):
898 def mergedescs(self):
898 def mergedescs(self):
899 return False
899 return False
900
900
901 def skipprompt(self):
901 def skipprompt(self):
902 return True
902 return True
903
903
904 def firstdate(self):
904 def firstdate(self):
905 return True
905 return True
906
906
907 @action(["drop", "d"],
907 @action(["drop", "d"],
908 _('remove commit from history'))
908 _('remove commit from history'))
909 class drop(histeditaction):
909 class drop(histeditaction):
910 def run(self):
910 def run(self):
911 parentctx = self.repo[self.state.parentctxnode]
911 parentctx = self.repo[self.state.parentctxnode]
912 return parentctx, [(self.node, tuple())]
912 return parentctx, [(self.node, tuple())]
913
913
914 @action(["mess", "m"],
914 @action(["mess", "m"],
915 _('edit commit message without changing commit content'),
915 _('edit commit message without changing commit content'),
916 priority=True)
916 priority=True)
917 class message(histeditaction):
917 class message(histeditaction):
918 def commiteditor(self):
918 def commiteditor(self):
919 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
919 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
920
920
921 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
921 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
922 """utility function to find the first outgoing changeset
922 """utility function to find the first outgoing changeset
923
923
924 Used by initialization code"""
924 Used by initialization code"""
925 if opts is None:
925 if opts is None:
926 opts = {}
926 opts = {}
927 dest = ui.expandpath(remote or 'default-push', remote or 'default')
927 dest = ui.expandpath(remote or 'default-push', remote or 'default')
928 dest, branches = hg.parseurl(dest, None)[:2]
928 dest, branches = hg.parseurl(dest, None)[:2]
929 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
929 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
930
930
931 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
931 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
932 other = hg.peer(repo, opts, dest)
932 other = hg.peer(repo, opts, dest)
933
933
934 if revs:
934 if revs:
935 revs = [repo.lookup(rev) for rev in revs]
935 revs = [repo.lookup(rev) for rev in revs]
936
936
937 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
937 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
938 if not outgoing.missing:
938 if not outgoing.missing:
939 raise error.Abort(_('no outgoing ancestors'))
939 raise error.Abort(_('no outgoing ancestors'))
940 roots = list(repo.revs("roots(%ln)", outgoing.missing))
940 roots = list(repo.revs("roots(%ln)", outgoing.missing))
941 if len(roots) > 1:
941 if len(roots) > 1:
942 msg = _('there are ambiguous outgoing revisions')
942 msg = _('there are ambiguous outgoing revisions')
943 hint = _("see 'hg help histedit' for more detail")
943 hint = _("see 'hg help histedit' for more detail")
944 raise error.Abort(msg, hint=hint)
944 raise error.Abort(msg, hint=hint)
945 return repo[roots[0]].node()
945 return repo[roots[0]].node()
946
946
947 # Curses Support
947 # Curses Support
948 try:
948 try:
949 import curses
949 import curses
950
950
951 # Curses requires setting the locale or it will default to the C
951 # Curses requires setting the locale or it will default to the C
952 # locale. This sets the locale to the user's default system
952 # locale. This sets the locale to the user's default system
953 # locale.
953 # locale.
954 import locale
954 import locale
955 locale.setlocale(locale.LC_ALL, r'')
955 locale.setlocale(locale.LC_ALL, r'')
956 except ImportError:
956 except ImportError:
957 curses = None
957 curses = None
958
958
959 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
959 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
960 ACTION_LABELS = {
960 ACTION_LABELS = {
961 'fold': '^fold',
961 'fold': '^fold',
962 'roll': '^roll',
962 'roll': '^roll',
963 }
963 }
964
964
965 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
965 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
966 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
966 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
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 repo.ui.verbose = True
1234 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1233 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1235 "patch": True, "template": "status"
1234 "patch": True, "template": "status"
1236 }, buffered=True)
1235 }, buffered=True)
1237 displayer.show(rule.ctx)
1236 overrides = {('ui', 'verbose'): True}
1238 displayer.close()
1237 with repo.ui.configoverride(overrides, source='histedit'):
1238 displayer.show(rule.ctx)
1239 displayer.close()
1239 return displayer.hunk[rule.ctx.rev()].splitlines()
1240 return displayer.hunk[rule.ctx.rev()].splitlines()
1240
1241
1241 def _chisteditmain(repo, rules, stdscr):
1242 def _chisteditmain(repo, rules, stdscr):
1242 try:
1243 try:
1243 curses.use_default_colors()
1244 curses.use_default_colors()
1244 except curses.error:
1245 except curses.error:
1245 pass
1246 pass
1246
1247
1247 # initialize color pattern
1248 # initialize color pattern
1248 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1249 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1249 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1250 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1250 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1251 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1251 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1252 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1252 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1253 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1253 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1254 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1254 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1255 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1255 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1256 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1256
1257
1257 # don't display the cursor
1258 # don't display the cursor
1258 try:
1259 try:
1259 curses.curs_set(0)
1260 curses.curs_set(0)
1260 except curses.error:
1261 except curses.error:
1261 pass
1262 pass
1262
1263
1263 def rendercommit(win, state):
1264 def rendercommit(win, state):
1264 """Renders the commit window that shows the log of the current selected
1265 """Renders the commit window that shows the log of the current selected
1265 commit"""
1266 commit"""
1266 pos = state['pos']
1267 pos = state['pos']
1267 rules = state['rules']
1268 rules = state['rules']
1268 rule = rules[pos]
1269 rule = rules[pos]
1269
1270
1270 ctx = rule.ctx
1271 ctx = rule.ctx
1271 win.box()
1272 win.box()
1272
1273
1273 maxy, maxx = win.getmaxyx()
1274 maxy, maxx = win.getmaxyx()
1274 length = maxx - 3
1275 length = maxx - 3
1275
1276
1276 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1277 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1277 win.addstr(1, 1, line[:length])
1278 win.addstr(1, 1, line[:length])
1278
1279
1279 line = "user: {0}".format(ctx.user())
1280 line = "user: {0}".format(ctx.user())
1280 win.addstr(2, 1, line[:length])
1281 win.addstr(2, 1, line[:length])
1281
1282
1282 bms = repo.nodebookmarks(ctx.node())
1283 bms = repo.nodebookmarks(ctx.node())
1283 line = "bookmark: {0}".format(' '.join(bms))
1284 line = "bookmark: {0}".format(' '.join(bms))
1284 win.addstr(3, 1, line[:length])
1285 win.addstr(3, 1, line[:length])
1285
1286
1286 line = "files: {0}".format(','.join(ctx.files()))
1287 line = "files: {0}".format(','.join(ctx.files()))
1287 win.addstr(4, 1, line[:length])
1288 win.addstr(4, 1, line[:length])
1288
1289
1289 line = "summary: {0}".format(ctx.description().splitlines()[0])
1290 line = "summary: {0}".format(ctx.description().splitlines()[0])
1290 win.addstr(5, 1, line[:length])
1291 win.addstr(5, 1, line[:length])
1291
1292
1292 conflicts = rule.conflicts
1293 conflicts = rule.conflicts
1293 if len(conflicts) > 0:
1294 if len(conflicts) > 0:
1294 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1295 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1295 conflictstr = "changed files overlap with {0}".format(conflictstr)
1296 conflictstr = "changed files overlap with {0}".format(conflictstr)
1296 else:
1297 else:
1297 conflictstr = 'no overlap'
1298 conflictstr = 'no overlap'
1298
1299
1299 win.addstr(6, 1, conflictstr[:length])
1300 win.addstr(6, 1, conflictstr[:length])
1300 win.noutrefresh()
1301 win.noutrefresh()
1301
1302
1302 def helplines(mode):
1303 def helplines(mode):
1303 if mode == MODE_PATCH:
1304 if mode == MODE_PATCH:
1304 help = """\
1305 help = """\
1305 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1306 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1306 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1307 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1307 """
1308 """
1308 else:
1309 else:
1309 help = """\
1310 help = """\
1310 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1311 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1311 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1312 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1312 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1313 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1313 """
1314 """
1314 return help.splitlines()
1315 return help.splitlines()
1315
1316
1316 def renderhelp(win, state):
1317 def renderhelp(win, state):
1317 maxy, maxx = win.getmaxyx()
1318 maxy, maxx = win.getmaxyx()
1318 mode, _ = state['mode']
1319 mode, _ = state['mode']
1319 for y, line in enumerate(helplines(mode)):
1320 for y, line in enumerate(helplines(mode)):
1320 if y >= maxy:
1321 if y >= maxy:
1321 break
1322 break
1322 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1323 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1323 win.noutrefresh()
1324 win.noutrefresh()
1324
1325
1325 def renderrules(rulesscr, state):
1326 def renderrules(rulesscr, state):
1326 rules = state['rules']
1327 rules = state['rules']
1327 pos = state['pos']
1328 pos = state['pos']
1328 selected = state['selected']
1329 selected = state['selected']
1329 start = state['modes'][MODE_RULES]['line_offset']
1330 start = state['modes'][MODE_RULES]['line_offset']
1330
1331
1331 conflicts = [r.ctx for r in rules if r.conflicts]
1332 conflicts = [r.ctx for r in rules if r.conflicts]
1332 if len(conflicts) > 0:
1333 if len(conflicts) > 0:
1333 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1334 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1334 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1335 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1335
1336
1336 for y, rule in enumerate(rules[start:]):
1337 for y, rule in enumerate(rules[start:]):
1337 if y >= state['page_height']:
1338 if y >= state['page_height']:
1338 break
1339 break
1339 if len(rule.conflicts) > 0:
1340 if len(rule.conflicts) > 0:
1340 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1341 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1341 else:
1342 else:
1342 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1343 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1343 if y + start == selected:
1344 if y + start == selected:
1344 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1345 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1345 elif y + start == pos:
1346 elif y + start == pos:
1346 addln(rulesscr, y, 2, rule,
1347 addln(rulesscr, y, 2, rule,
1347 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD)
1348 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD)
1348 else:
1349 else:
1349 addln(rulesscr, y, 2, rule)
1350 addln(rulesscr, y, 2, rule)
1350 rulesscr.noutrefresh()
1351 rulesscr.noutrefresh()
1351
1352
1352 def renderstring(win, state, output, diffcolors=False):
1353 def renderstring(win, state, output, diffcolors=False):
1353 maxy, maxx = win.getmaxyx()
1354 maxy, maxx = win.getmaxyx()
1354 length = min(maxy - 1, len(output))
1355 length = min(maxy - 1, len(output))
1355 for y in range(0, length):
1356 for y in range(0, length):
1356 line = output[y]
1357 line = output[y]
1357 if diffcolors:
1358 if diffcolors:
1358 if line and line[0] == '+':
1359 if line and line[0] == '+':
1359 win.addstr(
1360 win.addstr(
1360 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE))
1361 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE))
1361 elif line and line[0] == '-':
1362 elif line and line[0] == '-':
1362 win.addstr(
1363 win.addstr(
1363 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE))
1364 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE))
1364 elif line.startswith('@@ '):
1365 elif line.startswith('@@ '):
1365 win.addstr(
1366 win.addstr(
1366 y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1367 y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1367 else:
1368 else:
1368 win.addstr(y, 0, line)
1369 win.addstr(y, 0, line)
1369 else:
1370 else:
1370 win.addstr(y, 0, line)
1371 win.addstr(y, 0, line)
1371 win.noutrefresh()
1372 win.noutrefresh()
1372
1373
1373 def renderpatch(win, state):
1374 def renderpatch(win, state):
1374 start = state['modes'][MODE_PATCH]['line_offset']
1375 start = state['modes'][MODE_PATCH]['line_offset']
1375 renderstring(win, state, patchcontents(state)[start:], diffcolors=True)
1376 renderstring(win, state, patchcontents(state)[start:], diffcolors=True)
1376
1377
1377 def layout(mode):
1378 def layout(mode):
1378 maxy, maxx = stdscr.getmaxyx()
1379 maxy, maxx = stdscr.getmaxyx()
1379 helplen = len(helplines(mode))
1380 helplen = len(helplines(mode))
1380 return {
1381 return {
1381 'commit': (8, maxx),
1382 'commit': (8, maxx),
1382 'help': (helplen, maxx),
1383 'help': (helplen, maxx),
1383 'main': (maxy - helplen - 8, maxx),
1384 'main': (maxy - helplen - 8, maxx),
1384 }
1385 }
1385
1386
1386 def drawvertwin(size, y, x):
1387 def drawvertwin(size, y, x):
1387 win = curses.newwin(size[0], size[1], y, x)
1388 win = curses.newwin(size[0], size[1], y, x)
1388 y += size[0]
1389 y += size[0]
1389 return win, y, x
1390 return win, y, x
1390
1391
1391 state = {
1392 state = {
1392 'pos': 0,
1393 'pos': 0,
1393 'rules': rules,
1394 'rules': rules,
1394 'selected': None,
1395 'selected': None,
1395 'mode': (MODE_INIT, MODE_INIT),
1396 'mode': (MODE_INIT, MODE_INIT),
1396 'page_height': None,
1397 'page_height': None,
1397 'modes': {
1398 'modes': {
1398 MODE_RULES: {
1399 MODE_RULES: {
1399 'line_offset': 0,
1400 'line_offset': 0,
1400 },
1401 },
1401 MODE_PATCH: {
1402 MODE_PATCH: {
1402 'line_offset': 0,
1403 'line_offset': 0,
1403 }
1404 }
1404 },
1405 },
1405 'repo': repo,
1406 'repo': repo,
1406 }
1407 }
1407
1408
1408 # eventloop
1409 # eventloop
1409 ch = None
1410 ch = None
1410 stdscr.clear()
1411 stdscr.clear()
1411 stdscr.refresh()
1412 stdscr.refresh()
1412 while True:
1413 while True:
1413 try:
1414 try:
1414 oldmode, _ = state['mode']
1415 oldmode, _ = state['mode']
1415 if oldmode == MODE_INIT:
1416 if oldmode == MODE_INIT:
1416 changemode(state, MODE_RULES)
1417 changemode(state, MODE_RULES)
1417 e = event(state, ch)
1418 e = event(state, ch)
1418
1419
1419 if e == E_QUIT:
1420 if e == E_QUIT:
1420 return False
1421 return False
1421 if e == E_HISTEDIT:
1422 if e == E_HISTEDIT:
1422 return state['rules']
1423 return state['rules']
1423 else:
1424 else:
1424 if e == E_RESIZE:
1425 if e == E_RESIZE:
1425 size = screen_size()
1426 size = screen_size()
1426 if size != stdscr.getmaxyx():
1427 if size != stdscr.getmaxyx():
1427 curses.resizeterm(*size)
1428 curses.resizeterm(*size)
1428
1429
1429 curmode, _ = state['mode']
1430 curmode, _ = state['mode']
1430 sizes = layout(curmode)
1431 sizes = layout(curmode)
1431 if curmode != oldmode:
1432 if curmode != oldmode:
1432 state['page_height'] = sizes['main'][0]
1433 state['page_height'] = sizes['main'][0]
1433 # Adjust the view to fit the current screen size.
1434 # Adjust the view to fit the current screen size.
1434 movecursor(state, state['pos'], state['pos'])
1435 movecursor(state, state['pos'], state['pos'])
1435
1436
1436 # Pack the windows against the top, each pane spread across the
1437 # Pack the windows against the top, each pane spread across the
1437 # full width of the screen.
1438 # full width of the screen.
1438 y, x = (0, 0)
1439 y, x = (0, 0)
1439 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1440 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1440 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1441 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1441 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1442 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1442
1443
1443 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1444 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1444 if e == E_PAGEDOWN:
1445 if e == E_PAGEDOWN:
1445 changeview(state, +1, 'page')
1446 changeview(state, +1, 'page')
1446 elif e == E_PAGEUP:
1447 elif e == E_PAGEUP:
1447 changeview(state, -1, 'page')
1448 changeview(state, -1, 'page')
1448 elif e == E_LINEDOWN:
1449 elif e == E_LINEDOWN:
1449 changeview(state, +1, 'line')
1450 changeview(state, +1, 'line')
1450 elif e == E_LINEUP:
1451 elif e == E_LINEUP:
1451 changeview(state, -1, 'line')
1452 changeview(state, -1, 'line')
1452
1453
1453 # start rendering
1454 # start rendering
1454 commitwin.erase()
1455 commitwin.erase()
1455 helpwin.erase()
1456 helpwin.erase()
1456 mainwin.erase()
1457 mainwin.erase()
1457 if curmode == MODE_PATCH:
1458 if curmode == MODE_PATCH:
1458 renderpatch(mainwin, state)
1459 renderpatch(mainwin, state)
1459 elif curmode == MODE_HELP:
1460 elif curmode == MODE_HELP:
1460 renderstring(mainwin, state, __doc__.strip().splitlines())
1461 renderstring(mainwin, state, __doc__.strip().splitlines())
1461 else:
1462 else:
1462 renderrules(mainwin, state)
1463 renderrules(mainwin, state)
1463 rendercommit(commitwin, state)
1464 rendercommit(commitwin, state)
1464 renderhelp(helpwin, state)
1465 renderhelp(helpwin, state)
1465 curses.doupdate()
1466 curses.doupdate()
1466 # done rendering
1467 # done rendering
1467 ch = stdscr.getkey()
1468 ch = stdscr.getkey()
1468 except curses.error:
1469 except curses.error:
1469 pass
1470 pass
1470
1471
1471 def _chistedit(ui, repo, *freeargs, **opts):
1472 def _chistedit(ui, repo, *freeargs, **opts):
1472 """interactively edit changeset history via a curses interface
1473 """interactively edit changeset history via a curses interface
1473
1474
1474 Provides a ncurses interface to histedit. Press ? in chistedit mode
1475 Provides a ncurses interface to histedit. Press ? in chistedit mode
1475 to see an extensive help. Requires python-curses to be installed."""
1476 to see an extensive help. Requires python-curses to be installed."""
1476
1477
1477 if curses is None:
1478 if curses is None:
1478 raise error.Abort(_("Python curses library required"))
1479 raise error.Abort(_("Python curses library required"))
1479
1480
1480 # disable color
1481 # disable color
1481 ui._colormode = None
1482 ui._colormode = None
1482
1483
1483 try:
1484 try:
1484 keep = opts.get('keep')
1485 keep = opts.get('keep')
1485 revs = opts.get('rev', [])[:]
1486 revs = opts.get('rev', [])[:]
1486 cmdutil.checkunfinished(repo)
1487 cmdutil.checkunfinished(repo)
1487 cmdutil.bailifchanged(repo)
1488 cmdutil.bailifchanged(repo)
1488
1489
1489 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1490 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1490 raise error.Abort(_('history edit already in progress, try '
1491 raise error.Abort(_('history edit already in progress, try '
1491 '--continue or --abort'))
1492 '--continue or --abort'))
1492 revs.extend(freeargs)
1493 revs.extend(freeargs)
1493 if not revs:
1494 if not revs:
1494 defaultrev = destutil.desthistedit(ui, repo)
1495 defaultrev = destutil.desthistedit(ui, repo)
1495 if defaultrev is not None:
1496 if defaultrev is not None:
1496 revs.append(defaultrev)
1497 revs.append(defaultrev)
1497 if len(revs) != 1:
1498 if len(revs) != 1:
1498 raise error.Abort(
1499 raise error.Abort(
1499 _('histedit requires exactly one ancestor revision'))
1500 _('histedit requires exactly one ancestor revision'))
1500
1501
1501 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1502 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1502 if len(rr) != 1:
1503 if len(rr) != 1:
1503 raise error.Abort(_('The specified revisions must have '
1504 raise error.Abort(_('The specified revisions must have '
1504 'exactly one common root'))
1505 'exactly one common root'))
1505 root = rr[0].node()
1506 root = rr[0].node()
1506
1507
1507 topmost = repo.dirstate.p1()
1508 topmost = repo.dirstate.p1()
1508 revs = between(repo, root, topmost, keep)
1509 revs = between(repo, root, topmost, keep)
1509 if not revs:
1510 if not revs:
1510 raise error.Abort(_('%s is not an ancestor of working directory') %
1511 raise error.Abort(_('%s is not an ancestor of working directory') %
1511 node.short(root))
1512 node.short(root))
1512
1513
1513 ctxs = []
1514 ctxs = []
1514 for i, r in enumerate(revs):
1515 for i, r in enumerate(revs):
1515 ctxs.append(histeditrule(repo[r], i))
1516 ctxs.append(histeditrule(repo[r], i))
1516 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1517 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1517 curses.echo()
1518 curses.echo()
1518 curses.endwin()
1519 curses.endwin()
1519 if rc is False:
1520 if rc is False:
1520 ui.write(_("histedit aborted\n"))
1521 ui.write(_("histedit aborted\n"))
1521 return 0
1522 return 0
1522 if type(rc) is list:
1523 if type(rc) is list:
1523 ui.status(_("performing changes\n"))
1524 ui.status(_("performing changes\n"))
1524 rules = makecommands(rc)
1525 rules = makecommands(rc)
1525 filename = repo.vfs.join('chistedit')
1526 filename = repo.vfs.join('chistedit')
1526 with open(filename, 'w+') as fp:
1527 with open(filename, 'w+') as fp:
1527 for r in rules:
1528 for r in rules:
1528 fp.write(r)
1529 fp.write(r)
1529 opts['commands'] = filename
1530 opts['commands'] = filename
1530 return _texthistedit(ui, repo, *freeargs, **opts)
1531 return _texthistedit(ui, repo, *freeargs, **opts)
1531 except KeyboardInterrupt:
1532 except KeyboardInterrupt:
1532 pass
1533 pass
1533 return -1
1534 return -1
1534
1535
1535 @command('histedit',
1536 @command('histedit',
1536 [('', 'commands', '',
1537 [('', 'commands', '',
1537 _('read history edits from the specified file'), _('FILE')),
1538 _('read history edits from the specified file'), _('FILE')),
1538 ('c', 'continue', False, _('continue an edit already in progress')),
1539 ('c', 'continue', False, _('continue an edit already in progress')),
1539 ('', 'edit-plan', False, _('edit remaining actions list')),
1540 ('', 'edit-plan', False, _('edit remaining actions list')),
1540 ('k', 'keep', False,
1541 ('k', 'keep', False,
1541 _("don't strip old nodes after edit is complete")),
1542 _("don't strip old nodes after edit is complete")),
1542 ('', 'abort', False, _('abort an edit in progress')),
1543 ('', 'abort', False, _('abort an edit in progress')),
1543 ('o', 'outgoing', False, _('changesets not found in destination')),
1544 ('o', 'outgoing', False, _('changesets not found in destination')),
1544 ('f', 'force', False,
1545 ('f', 'force', False,
1545 _('force outgoing even for unrelated repositories')),
1546 _('force outgoing even for unrelated repositories')),
1546 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1547 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1547 cmdutil.formatteropts,
1548 cmdutil.formatteropts,
1548 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1549 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1549 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1550 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1550 def histedit(ui, repo, *freeargs, **opts):
1551 def histedit(ui, repo, *freeargs, **opts):
1551 """interactively edit changeset history
1552 """interactively edit changeset history
1552
1553
1553 This command lets you edit a linear series of changesets (up to
1554 This command lets you edit a linear series of changesets (up to
1554 and including the working directory, which should be clean).
1555 and including the working directory, which should be clean).
1555 You can:
1556 You can:
1556
1557
1557 - `pick` to [re]order a changeset
1558 - `pick` to [re]order a changeset
1558
1559
1559 - `drop` to omit changeset
1560 - `drop` to omit changeset
1560
1561
1561 - `mess` to reword the changeset commit message
1562 - `mess` to reword the changeset commit message
1562
1563
1563 - `fold` to combine it with the preceding changeset (using the later date)
1564 - `fold` to combine it with the preceding changeset (using the later date)
1564
1565
1565 - `roll` like fold, but discarding this commit's description and date
1566 - `roll` like fold, but discarding this commit's description and date
1566
1567
1567 - `edit` to edit this changeset (preserving date)
1568 - `edit` to edit this changeset (preserving date)
1568
1569
1569 - `base` to checkout changeset and apply further changesets from there
1570 - `base` to checkout changeset and apply further changesets from there
1570
1571
1571 There are a number of ways to select the root changeset:
1572 There are a number of ways to select the root changeset:
1572
1573
1573 - Specify ANCESTOR directly
1574 - Specify ANCESTOR directly
1574
1575
1575 - Use --outgoing -- it will be the first linear changeset not
1576 - Use --outgoing -- it will be the first linear changeset not
1576 included in destination. (See :hg:`help config.paths.default-push`)
1577 included in destination. (See :hg:`help config.paths.default-push`)
1577
1578
1578 - Otherwise, the value from the "histedit.defaultrev" config option
1579 - Otherwise, the value from the "histedit.defaultrev" config option
1579 is used as a revset to select the base revision when ANCESTOR is not
1580 is used as a revset to select the base revision when ANCESTOR is not
1580 specified. The first revision returned by the revset is used. By
1581 specified. The first revision returned by the revset is used. By
1581 default, this selects the editable history that is unique to the
1582 default, this selects the editable history that is unique to the
1582 ancestry of the working directory.
1583 ancestry of the working directory.
1583
1584
1584 .. container:: verbose
1585 .. container:: verbose
1585
1586
1586 If you use --outgoing, this command will abort if there are ambiguous
1587 If you use --outgoing, this command will abort if there are ambiguous
1587 outgoing revisions. For example, if there are multiple branches
1588 outgoing revisions. For example, if there are multiple branches
1588 containing outgoing revisions.
1589 containing outgoing revisions.
1589
1590
1590 Use "min(outgoing() and ::.)" or similar revset specification
1591 Use "min(outgoing() and ::.)" or similar revset specification
1591 instead of --outgoing to specify edit target revision exactly in
1592 instead of --outgoing to specify edit target revision exactly in
1592 such ambiguous situation. See :hg:`help revsets` for detail about
1593 such ambiguous situation. See :hg:`help revsets` for detail about
1593 selecting revisions.
1594 selecting revisions.
1594
1595
1595 .. container:: verbose
1596 .. container:: verbose
1596
1597
1597 Examples:
1598 Examples:
1598
1599
1599 - A number of changes have been made.
1600 - A number of changes have been made.
1600 Revision 3 is no longer needed.
1601 Revision 3 is no longer needed.
1601
1602
1602 Start history editing from revision 3::
1603 Start history editing from revision 3::
1603
1604
1604 hg histedit -r 3
1605 hg histedit -r 3
1605
1606
1606 An editor opens, containing the list of revisions,
1607 An editor opens, containing the list of revisions,
1607 with specific actions specified::
1608 with specific actions specified::
1608
1609
1609 pick 5339bf82f0ca 3 Zworgle the foobar
1610 pick 5339bf82f0ca 3 Zworgle the foobar
1610 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1611 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1611 pick 0a9639fcda9d 5 Morgify the cromulancy
1612 pick 0a9639fcda9d 5 Morgify the cromulancy
1612
1613
1613 Additional information about the possible actions
1614 Additional information about the possible actions
1614 to take appears below the list of revisions.
1615 to take appears below the list of revisions.
1615
1616
1616 To remove revision 3 from the history,
1617 To remove revision 3 from the history,
1617 its action (at the beginning of the relevant line)
1618 its action (at the beginning of the relevant line)
1618 is changed to 'drop'::
1619 is changed to 'drop'::
1619
1620
1620 drop 5339bf82f0ca 3 Zworgle the foobar
1621 drop 5339bf82f0ca 3 Zworgle the foobar
1621 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1622 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1622 pick 0a9639fcda9d 5 Morgify the cromulancy
1623 pick 0a9639fcda9d 5 Morgify the cromulancy
1623
1624
1624 - A number of changes have been made.
1625 - A number of changes have been made.
1625 Revision 2 and 4 need to be swapped.
1626 Revision 2 and 4 need to be swapped.
1626
1627
1627 Start history editing from revision 2::
1628 Start history editing from revision 2::
1628
1629
1629 hg histedit -r 2
1630 hg histedit -r 2
1630
1631
1631 An editor opens, containing the list of revisions,
1632 An editor opens, containing the list of revisions,
1632 with specific actions specified::
1633 with specific actions specified::
1633
1634
1634 pick 252a1af424ad 2 Blorb a morgwazzle
1635 pick 252a1af424ad 2 Blorb a morgwazzle
1635 pick 5339bf82f0ca 3 Zworgle the foobar
1636 pick 5339bf82f0ca 3 Zworgle the foobar
1636 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1637 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1637
1638
1638 To swap revision 2 and 4, its lines are swapped
1639 To swap revision 2 and 4, its lines are swapped
1639 in the editor::
1640 in the editor::
1640
1641
1641 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1642 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1642 pick 5339bf82f0ca 3 Zworgle the foobar
1643 pick 5339bf82f0ca 3 Zworgle the foobar
1643 pick 252a1af424ad 2 Blorb a morgwazzle
1644 pick 252a1af424ad 2 Blorb a morgwazzle
1644
1645
1645 Returns 0 on success, 1 if user intervention is required (not only
1646 Returns 0 on success, 1 if user intervention is required (not only
1646 for intentional "edit" command, but also for resolving unexpected
1647 for intentional "edit" command, but also for resolving unexpected
1647 conflicts).
1648 conflicts).
1648 """
1649 """
1649 # kludge: _chistedit only works for starting an edit, not aborting
1650 # kludge: _chistedit only works for starting an edit, not aborting
1650 # or continuing, so fall back to regular _texthistedit for those
1651 # or continuing, so fall back to regular _texthistedit for those
1651 # operations.
1652 # operations.
1652 if ui.interface('histedit') == 'curses' and _getgoal(
1653 if ui.interface('histedit') == 'curses' and _getgoal(
1653 pycompat.byteskwargs(opts)) == goalnew:
1654 pycompat.byteskwargs(opts)) == goalnew:
1654 return _chistedit(ui, repo, *freeargs, **opts)
1655 return _chistedit(ui, repo, *freeargs, **opts)
1655 return _texthistedit(ui, repo, *freeargs, **opts)
1656 return _texthistedit(ui, repo, *freeargs, **opts)
1656
1657
1657 def _texthistedit(ui, repo, *freeargs, **opts):
1658 def _texthistedit(ui, repo, *freeargs, **opts):
1658 state = histeditstate(repo)
1659 state = histeditstate(repo)
1659 with repo.wlock() as wlock, repo.lock() as lock:
1660 with repo.wlock() as wlock, repo.lock() as lock:
1660 state.wlock = wlock
1661 state.wlock = wlock
1661 state.lock = lock
1662 state.lock = lock
1662 _histedit(ui, repo, state, *freeargs, **opts)
1663 _histedit(ui, repo, state, *freeargs, **opts)
1663
1664
1664 goalcontinue = 'continue'
1665 goalcontinue = 'continue'
1665 goalabort = 'abort'
1666 goalabort = 'abort'
1666 goaleditplan = 'edit-plan'
1667 goaleditplan = 'edit-plan'
1667 goalnew = 'new'
1668 goalnew = 'new'
1668
1669
1669 def _getgoal(opts):
1670 def _getgoal(opts):
1670 if opts.get(b'continue'):
1671 if opts.get(b'continue'):
1671 return goalcontinue
1672 return goalcontinue
1672 if opts.get(b'abort'):
1673 if opts.get(b'abort'):
1673 return goalabort
1674 return goalabort
1674 if opts.get(b'edit_plan'):
1675 if opts.get(b'edit_plan'):
1675 return goaleditplan
1676 return goaleditplan
1676 return goalnew
1677 return goalnew
1677
1678
1678 def _readfile(ui, path):
1679 def _readfile(ui, path):
1679 if path == '-':
1680 if path == '-':
1680 with ui.timeblockedsection('histedit'):
1681 with ui.timeblockedsection('histedit'):
1681 return ui.fin.read()
1682 return ui.fin.read()
1682 else:
1683 else:
1683 with open(path, 'rb') as f:
1684 with open(path, 'rb') as f:
1684 return f.read()
1685 return f.read()
1685
1686
1686 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1687 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1687 # TODO only abort if we try to histedit mq patches, not just
1688 # TODO only abort if we try to histedit mq patches, not just
1688 # blanket if mq patches are applied somewhere
1689 # blanket if mq patches are applied somewhere
1689 mq = getattr(repo, 'mq', None)
1690 mq = getattr(repo, 'mq', None)
1690 if mq and mq.applied:
1691 if mq and mq.applied:
1691 raise error.Abort(_('source has mq patches applied'))
1692 raise error.Abort(_('source has mq patches applied'))
1692
1693
1693 # basic argument incompatibility processing
1694 # basic argument incompatibility processing
1694 outg = opts.get('outgoing')
1695 outg = opts.get('outgoing')
1695 editplan = opts.get('edit_plan')
1696 editplan = opts.get('edit_plan')
1696 abort = opts.get('abort')
1697 abort = opts.get('abort')
1697 force = opts.get('force')
1698 force = opts.get('force')
1698 if force and not outg:
1699 if force and not outg:
1699 raise error.Abort(_('--force only allowed with --outgoing'))
1700 raise error.Abort(_('--force only allowed with --outgoing'))
1700 if goal == 'continue':
1701 if goal == 'continue':
1701 if any((outg, abort, revs, freeargs, rules, editplan)):
1702 if any((outg, abort, revs, freeargs, rules, editplan)):
1702 raise error.Abort(_('no arguments allowed with --continue'))
1703 raise error.Abort(_('no arguments allowed with --continue'))
1703 elif goal == 'abort':
1704 elif goal == 'abort':
1704 if any((outg, revs, freeargs, rules, editplan)):
1705 if any((outg, revs, freeargs, rules, editplan)):
1705 raise error.Abort(_('no arguments allowed with --abort'))
1706 raise error.Abort(_('no arguments allowed with --abort'))
1706 elif goal == 'edit-plan':
1707 elif goal == 'edit-plan':
1707 if any((outg, revs, freeargs)):
1708 if any((outg, revs, freeargs)):
1708 raise error.Abort(_('only --commands argument allowed with '
1709 raise error.Abort(_('only --commands argument allowed with '
1709 '--edit-plan'))
1710 '--edit-plan'))
1710 else:
1711 else:
1711 if state.inprogress():
1712 if state.inprogress():
1712 raise error.Abort(_('history edit already in progress, try '
1713 raise error.Abort(_('history edit already in progress, try '
1713 '--continue or --abort'))
1714 '--continue or --abort'))
1714 if outg:
1715 if outg:
1715 if revs:
1716 if revs:
1716 raise error.Abort(_('no revisions allowed with --outgoing'))
1717 raise error.Abort(_('no revisions allowed with --outgoing'))
1717 if len(freeargs) > 1:
1718 if len(freeargs) > 1:
1718 raise error.Abort(
1719 raise error.Abort(
1719 _('only one repo argument allowed with --outgoing'))
1720 _('only one repo argument allowed with --outgoing'))
1720 else:
1721 else:
1721 revs.extend(freeargs)
1722 revs.extend(freeargs)
1722 if len(revs) == 0:
1723 if len(revs) == 0:
1723 defaultrev = destutil.desthistedit(ui, repo)
1724 defaultrev = destutil.desthistedit(ui, repo)
1724 if defaultrev is not None:
1725 if defaultrev is not None:
1725 revs.append(defaultrev)
1726 revs.append(defaultrev)
1726
1727
1727 if len(revs) != 1:
1728 if len(revs) != 1:
1728 raise error.Abort(
1729 raise error.Abort(
1729 _('histedit requires exactly one ancestor revision'))
1730 _('histedit requires exactly one ancestor revision'))
1730
1731
1731 def _histedit(ui, repo, state, *freeargs, **opts):
1732 def _histedit(ui, repo, state, *freeargs, **opts):
1732 opts = pycompat.byteskwargs(opts)
1733 opts = pycompat.byteskwargs(opts)
1733 fm = ui.formatter('histedit', opts)
1734 fm = ui.formatter('histedit', opts)
1734 fm.startitem()
1735 fm.startitem()
1735 goal = _getgoal(opts)
1736 goal = _getgoal(opts)
1736 revs = opts.get('rev', [])
1737 revs = opts.get('rev', [])
1737 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1738 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1738 rules = opts.get('commands', '')
1739 rules = opts.get('commands', '')
1739 state.keep = opts.get('keep', False)
1740 state.keep = opts.get('keep', False)
1740
1741
1741 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1742 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1742
1743
1743 hastags = False
1744 hastags = False
1744 if revs:
1745 if revs:
1745 revs = scmutil.revrange(repo, revs)
1746 revs = scmutil.revrange(repo, revs)
1746 ctxs = [repo[rev] for rev in revs]
1747 ctxs = [repo[rev] for rev in revs]
1747 for ctx in ctxs:
1748 for ctx in ctxs:
1748 tags = [tag for tag in ctx.tags() if tag != 'tip']
1749 tags = [tag for tag in ctx.tags() if tag != 'tip']
1749 if not hastags:
1750 if not hastags:
1750 hastags = len(tags)
1751 hastags = len(tags)
1751 if hastags:
1752 if hastags:
1752 if ui.promptchoice(_('warning: tags associated with the given'
1753 if ui.promptchoice(_('warning: tags associated with the given'
1753 ' changeset will be lost after histedit.\n'
1754 ' changeset will be lost after histedit.\n'
1754 'do you want to continue (yN)? $$ &Yes $$ &No'),
1755 'do you want to continue (yN)? $$ &Yes $$ &No'),
1755 default=1):
1756 default=1):
1756 raise error.Abort(_('histedit cancelled\n'))
1757 raise error.Abort(_('histedit cancelled\n'))
1757 # rebuild state
1758 # rebuild state
1758 if goal == goalcontinue:
1759 if goal == goalcontinue:
1759 state.read()
1760 state.read()
1760 state = bootstrapcontinue(ui, state, opts)
1761 state = bootstrapcontinue(ui, state, opts)
1761 elif goal == goaleditplan:
1762 elif goal == goaleditplan:
1762 _edithisteditplan(ui, repo, state, rules)
1763 _edithisteditplan(ui, repo, state, rules)
1763 return
1764 return
1764 elif goal == goalabort:
1765 elif goal == goalabort:
1765 _aborthistedit(ui, repo, state, nobackup=nobackup)
1766 _aborthistedit(ui, repo, state, nobackup=nobackup)
1766 return
1767 return
1767 else:
1768 else:
1768 # goal == goalnew
1769 # goal == goalnew
1769 _newhistedit(ui, repo, state, revs, freeargs, opts)
1770 _newhistedit(ui, repo, state, revs, freeargs, opts)
1770
1771
1771 _continuehistedit(ui, repo, state)
1772 _continuehistedit(ui, repo, state)
1772 _finishhistedit(ui, repo, state, fm)
1773 _finishhistedit(ui, repo, state, fm)
1773 fm.end()
1774 fm.end()
1774
1775
1775 def _continuehistedit(ui, repo, state):
1776 def _continuehistedit(ui, repo, state):
1776 """This function runs after either:
1777 """This function runs after either:
1777 - bootstrapcontinue (if the goal is 'continue')
1778 - bootstrapcontinue (if the goal is 'continue')
1778 - _newhistedit (if the goal is 'new')
1779 - _newhistedit (if the goal is 'new')
1779 """
1780 """
1780 # preprocess rules so that we can hide inner folds from the user
1781 # preprocess rules so that we can hide inner folds from the user
1781 # and only show one editor
1782 # and only show one editor
1782 actions = state.actions[:]
1783 actions = state.actions[:]
1783 for idx, (action, nextact) in enumerate(
1784 for idx, (action, nextact) in enumerate(
1784 zip(actions, actions[1:] + [None])):
1785 zip(actions, actions[1:] + [None])):
1785 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1786 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1786 state.actions[idx].__class__ = _multifold
1787 state.actions[idx].__class__ = _multifold
1787
1788
1788 # Force an initial state file write, so the user can run --abort/continue
1789 # Force an initial state file write, so the user can run --abort/continue
1789 # even if there's an exception before the first transaction serialize.
1790 # even if there's an exception before the first transaction serialize.
1790 state.write()
1791 state.write()
1791
1792
1792 tr = None
1793 tr = None
1793 # Don't use singletransaction by default since it rolls the entire
1794 # Don't use singletransaction by default since it rolls the entire
1794 # transaction back if an unexpected exception happens (like a
1795 # transaction back if an unexpected exception happens (like a
1795 # pretxncommit hook throws, or the user aborts the commit msg editor).
1796 # pretxncommit hook throws, or the user aborts the commit msg editor).
1796 if ui.configbool("histedit", "singletransaction"):
1797 if ui.configbool("histedit", "singletransaction"):
1797 # Don't use a 'with' for the transaction, since actions may close
1798 # Don't use a 'with' for the transaction, since actions may close
1798 # and reopen a transaction. For example, if the action executes an
1799 # and reopen a transaction. For example, if the action executes an
1799 # external process it may choose to commit the transaction first.
1800 # external process it may choose to commit the transaction first.
1800 tr = repo.transaction('histedit')
1801 tr = repo.transaction('histedit')
1801 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1802 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1802 total=len(state.actions))
1803 total=len(state.actions))
1803 with progress, util.acceptintervention(tr):
1804 with progress, util.acceptintervention(tr):
1804 while state.actions:
1805 while state.actions:
1805 state.write(tr=tr)
1806 state.write(tr=tr)
1806 actobj = state.actions[0]
1807 actobj = state.actions[0]
1807 progress.increment(item=actobj.torule())
1808 progress.increment(item=actobj.torule())
1808 ui.debug('histedit: processing %s %s\n' % (actobj.verb,
1809 ui.debug('histedit: processing %s %s\n' % (actobj.verb,
1809 actobj.torule()))
1810 actobj.torule()))
1810 parentctx, replacement_ = actobj.run()
1811 parentctx, replacement_ = actobj.run()
1811 state.parentctxnode = parentctx.node()
1812 state.parentctxnode = parentctx.node()
1812 state.replacements.extend(replacement_)
1813 state.replacements.extend(replacement_)
1813 state.actions.pop(0)
1814 state.actions.pop(0)
1814
1815
1815 state.write()
1816 state.write()
1816
1817
1817 def _finishhistedit(ui, repo, state, fm):
1818 def _finishhistedit(ui, repo, state, fm):
1818 """This action runs when histedit is finishing its session"""
1819 """This action runs when histedit is finishing its session"""
1819 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1820 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1820
1821
1821 mapping, tmpnodes, created, ntm = processreplacement(state)
1822 mapping, tmpnodes, created, ntm = processreplacement(state)
1822 if mapping:
1823 if mapping:
1823 for prec, succs in mapping.iteritems():
1824 for prec, succs in mapping.iteritems():
1824 if not succs:
1825 if not succs:
1825 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1826 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1826 else:
1827 else:
1827 ui.debug('histedit: %s is replaced by %s\n' % (
1828 ui.debug('histedit: %s is replaced by %s\n' % (
1828 node.short(prec), node.short(succs[0])))
1829 node.short(prec), node.short(succs[0])))
1829 if len(succs) > 1:
1830 if len(succs) > 1:
1830 m = 'histedit: %s'
1831 m = 'histedit: %s'
1831 for n in succs[1:]:
1832 for n in succs[1:]:
1832 ui.debug(m % node.short(n))
1833 ui.debug(m % node.short(n))
1833
1834
1834 if not state.keep:
1835 if not state.keep:
1835 if mapping:
1836 if mapping:
1836 movetopmostbookmarks(repo, state.topmost, ntm)
1837 movetopmostbookmarks(repo, state.topmost, ntm)
1837 # TODO update mq state
1838 # TODO update mq state
1838 else:
1839 else:
1839 mapping = {}
1840 mapping = {}
1840
1841
1841 for n in tmpnodes:
1842 for n in tmpnodes:
1842 if n in repo:
1843 if n in repo:
1843 mapping[n] = ()
1844 mapping[n] = ()
1844
1845
1845 # remove entries about unknown nodes
1846 # remove entries about unknown nodes
1846 nodemap = repo.unfiltered().changelog.nodemap
1847 nodemap = repo.unfiltered().changelog.nodemap
1847 mapping = {k: v for k, v in mapping.items()
1848 mapping = {k: v for k, v in mapping.items()
1848 if k in nodemap and all(n in nodemap for n in v)}
1849 if k in nodemap and all(n in nodemap for n in v)}
1849 scmutil.cleanupnodes(repo, mapping, 'histedit')
1850 scmutil.cleanupnodes(repo, mapping, 'histedit')
1850 hf = fm.hexfunc
1851 hf = fm.hexfunc
1851 fl = fm.formatlist
1852 fl = fm.formatlist
1852 fd = fm.formatdict
1853 fd = fm.formatdict
1853 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1854 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1854 for oldn, newn in mapping.iteritems()},
1855 for oldn, newn in mapping.iteritems()},
1855 key="oldnode", value="newnodes")
1856 key="oldnode", value="newnodes")
1856 fm.data(nodechanges=nodechanges)
1857 fm.data(nodechanges=nodechanges)
1857
1858
1858 state.clear()
1859 state.clear()
1859 if os.path.exists(repo.sjoin('undo')):
1860 if os.path.exists(repo.sjoin('undo')):
1860 os.unlink(repo.sjoin('undo'))
1861 os.unlink(repo.sjoin('undo'))
1861 if repo.vfs.exists('histedit-last-edit.txt'):
1862 if repo.vfs.exists('histedit-last-edit.txt'):
1862 repo.vfs.unlink('histedit-last-edit.txt')
1863 repo.vfs.unlink('histedit-last-edit.txt')
1863
1864
1864 def _aborthistedit(ui, repo, state, nobackup=False):
1865 def _aborthistedit(ui, repo, state, nobackup=False):
1865 try:
1866 try:
1866 state.read()
1867 state.read()
1867 __, leafs, tmpnodes, __ = processreplacement(state)
1868 __, leafs, tmpnodes, __ = processreplacement(state)
1868 ui.debug('restore wc to old parent %s\n'
1869 ui.debug('restore wc to old parent %s\n'
1869 % node.short(state.topmost))
1870 % node.short(state.topmost))
1870
1871
1871 # Recover our old commits if necessary
1872 # Recover our old commits if necessary
1872 if not state.topmost in repo and state.backupfile:
1873 if not state.topmost in repo and state.backupfile:
1873 backupfile = repo.vfs.join(state.backupfile)
1874 backupfile = repo.vfs.join(state.backupfile)
1874 f = hg.openpath(ui, backupfile)
1875 f = hg.openpath(ui, backupfile)
1875 gen = exchange.readbundle(ui, f, backupfile)
1876 gen = exchange.readbundle(ui, f, backupfile)
1876 with repo.transaction('histedit.abort') as tr:
1877 with repo.transaction('histedit.abort') as tr:
1877 bundle2.applybundle(repo, gen, tr, source='histedit',
1878 bundle2.applybundle(repo, gen, tr, source='histedit',
1878 url='bundle:' + backupfile)
1879 url='bundle:' + backupfile)
1879
1880
1880 os.remove(backupfile)
1881 os.remove(backupfile)
1881
1882
1882 # check whether we should update away
1883 # check whether we should update away
1883 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1884 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1884 state.parentctxnode, leafs | tmpnodes):
1885 state.parentctxnode, leafs | tmpnodes):
1885 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1886 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1886 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1887 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1887 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1888 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1888 except Exception:
1889 except Exception:
1889 if state.inprogress():
1890 if state.inprogress():
1890 ui.warn(_('warning: encountered an exception during histedit '
1891 ui.warn(_('warning: encountered an exception during histedit '
1891 '--abort; the repository may not have been completely '
1892 '--abort; the repository may not have been completely '
1892 'cleaned up\n'))
1893 'cleaned up\n'))
1893 raise
1894 raise
1894 finally:
1895 finally:
1895 state.clear()
1896 state.clear()
1896
1897
1897 def _edithisteditplan(ui, repo, state, rules):
1898 def _edithisteditplan(ui, repo, state, rules):
1898 state.read()
1899 state.read()
1899 if not rules:
1900 if not rules:
1900 comment = geteditcomment(ui,
1901 comment = geteditcomment(ui,
1901 node.short(state.parentctxnode),
1902 node.short(state.parentctxnode),
1902 node.short(state.topmost))
1903 node.short(state.topmost))
1903 rules = ruleeditor(repo, ui, state.actions, comment)
1904 rules = ruleeditor(repo, ui, state.actions, comment)
1904 else:
1905 else:
1905 rules = _readfile(ui, rules)
1906 rules = _readfile(ui, rules)
1906 actions = parserules(rules, state)
1907 actions = parserules(rules, state)
1907 ctxs = [repo[act.node]
1908 ctxs = [repo[act.node]
1908 for act in state.actions if act.node]
1909 for act in state.actions if act.node]
1909 warnverifyactions(ui, repo, actions, state, ctxs)
1910 warnverifyactions(ui, repo, actions, state, ctxs)
1910 state.actions = actions
1911 state.actions = actions
1911 state.write()
1912 state.write()
1912
1913
1913 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1914 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1914 outg = opts.get('outgoing')
1915 outg = opts.get('outgoing')
1915 rules = opts.get('commands', '')
1916 rules = opts.get('commands', '')
1916 force = opts.get('force')
1917 force = opts.get('force')
1917
1918
1918 cmdutil.checkunfinished(repo)
1919 cmdutil.checkunfinished(repo)
1919 cmdutil.bailifchanged(repo)
1920 cmdutil.bailifchanged(repo)
1920
1921
1921 topmost = repo.dirstate.p1()
1922 topmost = repo.dirstate.p1()
1922 if outg:
1923 if outg:
1923 if freeargs:
1924 if freeargs:
1924 remote = freeargs[0]
1925 remote = freeargs[0]
1925 else:
1926 else:
1926 remote = None
1927 remote = None
1927 root = findoutgoing(ui, repo, remote, force, opts)
1928 root = findoutgoing(ui, repo, remote, force, opts)
1928 else:
1929 else:
1929 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1930 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1930 if len(rr) != 1:
1931 if len(rr) != 1:
1931 raise error.Abort(_('The specified revisions must have '
1932 raise error.Abort(_('The specified revisions must have '
1932 'exactly one common root'))
1933 'exactly one common root'))
1933 root = rr[0].node()
1934 root = rr[0].node()
1934
1935
1935 revs = between(repo, root, topmost, state.keep)
1936 revs = between(repo, root, topmost, state.keep)
1936 if not revs:
1937 if not revs:
1937 raise error.Abort(_('%s is not an ancestor of working directory') %
1938 raise error.Abort(_('%s is not an ancestor of working directory') %
1938 node.short(root))
1939 node.short(root))
1939
1940
1940 ctxs = [repo[r] for r in revs]
1941 ctxs = [repo[r] for r in revs]
1941 if not rules:
1942 if not rules:
1942 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1943 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1943 actions = [pick(state, r) for r in revs]
1944 actions = [pick(state, r) for r in revs]
1944 rules = ruleeditor(repo, ui, actions, comment)
1945 rules = ruleeditor(repo, ui, actions, comment)
1945 else:
1946 else:
1946 rules = _readfile(ui, rules)
1947 rules = _readfile(ui, rules)
1947 actions = parserules(rules, state)
1948 actions = parserules(rules, state)
1948 warnverifyactions(ui, repo, actions, state, ctxs)
1949 warnverifyactions(ui, repo, actions, state, ctxs)
1949
1950
1950 parentctxnode = repo[root].p1().node()
1951 parentctxnode = repo[root].p1().node()
1951
1952
1952 state.parentctxnode = parentctxnode
1953 state.parentctxnode = parentctxnode
1953 state.actions = actions
1954 state.actions = actions
1954 state.topmost = topmost
1955 state.topmost = topmost
1955 state.replacements = []
1956 state.replacements = []
1956
1957
1957 ui.log("histedit", "%d actions to histedit\n", len(actions),
1958 ui.log("histedit", "%d actions to histedit\n", len(actions),
1958 histedit_num_actions=len(actions))
1959 histedit_num_actions=len(actions))
1959
1960
1960 # Create a backup so we can always abort completely.
1961 # Create a backup so we can always abort completely.
1961 backupfile = None
1962 backupfile = None
1962 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1963 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1963 backupfile = repair.backupbundle(repo, [parentctxnode],
1964 backupfile = repair.backupbundle(repo, [parentctxnode],
1964 [topmost], root, 'histedit')
1965 [topmost], root, 'histedit')
1965 state.backupfile = backupfile
1966 state.backupfile = backupfile
1966
1967
1967 def _getsummary(ctx):
1968 def _getsummary(ctx):
1968 # a common pattern is to extract the summary but default to the empty
1969 # a common pattern is to extract the summary but default to the empty
1969 # string
1970 # string
1970 summary = ctx.description() or ''
1971 summary = ctx.description() or ''
1971 if summary:
1972 if summary:
1972 summary = summary.splitlines()[0]
1973 summary = summary.splitlines()[0]
1973 return summary
1974 return summary
1974
1975
1975 def bootstrapcontinue(ui, state, opts):
1976 def bootstrapcontinue(ui, state, opts):
1976 repo = state.repo
1977 repo = state.repo
1977
1978
1978 ms = mergemod.mergestate.read(repo)
1979 ms = mergemod.mergestate.read(repo)
1979 mergeutil.checkunresolved(ms)
1980 mergeutil.checkunresolved(ms)
1980
1981
1981 if state.actions:
1982 if state.actions:
1982 actobj = state.actions.pop(0)
1983 actobj = state.actions.pop(0)
1983
1984
1984 if _isdirtywc(repo):
1985 if _isdirtywc(repo):
1985 actobj.continuedirty()
1986 actobj.continuedirty()
1986 if _isdirtywc(repo):
1987 if _isdirtywc(repo):
1987 abortdirty()
1988 abortdirty()
1988
1989
1989 parentctx, replacements = actobj.continueclean()
1990 parentctx, replacements = actobj.continueclean()
1990
1991
1991 state.parentctxnode = parentctx.node()
1992 state.parentctxnode = parentctx.node()
1992 state.replacements.extend(replacements)
1993 state.replacements.extend(replacements)
1993
1994
1994 return state
1995 return state
1995
1996
1996 def between(repo, old, new, keep):
1997 def between(repo, old, new, keep):
1997 """select and validate the set of revision to edit
1998 """select and validate the set of revision to edit
1998
1999
1999 When keep is false, the specified set can't have children."""
2000 When keep is false, the specified set can't have children."""
2000 revs = repo.revs('%n::%n', old, new)
2001 revs = repo.revs('%n::%n', old, new)
2001 if revs and not keep:
2002 if revs and not keep:
2002 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
2003 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
2003 repo.revs('(%ld::) - (%ld)', revs, revs)):
2004 repo.revs('(%ld::) - (%ld)', revs, revs)):
2004 raise error.Abort(_('can only histedit a changeset together '
2005 raise error.Abort(_('can only histedit a changeset together '
2005 'with all its descendants'))
2006 'with all its descendants'))
2006 if repo.revs('(%ld) and merge()', revs):
2007 if repo.revs('(%ld) and merge()', revs):
2007 raise error.Abort(_('cannot edit history that contains merges'))
2008 raise error.Abort(_('cannot edit history that contains merges'))
2008 root = repo[revs.first()] # list is already sorted by repo.revs()
2009 root = repo[revs.first()] # list is already sorted by repo.revs()
2009 if not root.mutable():
2010 if not root.mutable():
2010 raise error.Abort(_('cannot edit public changeset: %s') % root,
2011 raise error.Abort(_('cannot edit public changeset: %s') % root,
2011 hint=_("see 'hg help phases' for details"))
2012 hint=_("see 'hg help phases' for details"))
2012 return pycompat.maplist(repo.changelog.node, revs)
2013 return pycompat.maplist(repo.changelog.node, revs)
2013
2014
2014 def ruleeditor(repo, ui, actions, editcomment=""):
2015 def ruleeditor(repo, ui, actions, editcomment=""):
2015 """open an editor to edit rules
2016 """open an editor to edit rules
2016
2017
2017 rules are in the format [ [act, ctx], ...] like in state.rules
2018 rules are in the format [ [act, ctx], ...] like in state.rules
2018 """
2019 """
2019 if repo.ui.configbool("experimental", "histedit.autoverb"):
2020 if repo.ui.configbool("experimental", "histedit.autoverb"):
2020 newact = util.sortdict()
2021 newact = util.sortdict()
2021 for act in actions:
2022 for act in actions:
2022 ctx = repo[act.node]
2023 ctx = repo[act.node]
2023 summary = _getsummary(ctx)
2024 summary = _getsummary(ctx)
2024 fword = summary.split(' ', 1)[0].lower()
2025 fword = summary.split(' ', 1)[0].lower()
2025 added = False
2026 added = False
2026
2027
2027 # if it doesn't end with the special character '!' just skip this
2028 # if it doesn't end with the special character '!' just skip this
2028 if fword.endswith('!'):
2029 if fword.endswith('!'):
2029 fword = fword[:-1]
2030 fword = fword[:-1]
2030 if fword in primaryactions | secondaryactions | tertiaryactions:
2031 if fword in primaryactions | secondaryactions | tertiaryactions:
2031 act.verb = fword
2032 act.verb = fword
2032 # get the target summary
2033 # get the target summary
2033 tsum = summary[len(fword) + 1:].lstrip()
2034 tsum = summary[len(fword) + 1:].lstrip()
2034 # safe but slow: reverse iterate over the actions so we
2035 # safe but slow: reverse iterate over the actions so we
2035 # don't clash on two commits having the same summary
2036 # don't clash on two commits having the same summary
2036 for na, l in reversed(list(newact.iteritems())):
2037 for na, l in reversed(list(newact.iteritems())):
2037 actx = repo[na.node]
2038 actx = repo[na.node]
2038 asum = _getsummary(actx)
2039 asum = _getsummary(actx)
2039 if asum == tsum:
2040 if asum == tsum:
2040 added = True
2041 added = True
2041 l.append(act)
2042 l.append(act)
2042 break
2043 break
2043
2044
2044 if not added:
2045 if not added:
2045 newact[act] = []
2046 newact[act] = []
2046
2047
2047 # copy over and flatten the new list
2048 # copy over and flatten the new list
2048 actions = []
2049 actions = []
2049 for na, l in newact.iteritems():
2050 for na, l in newact.iteritems():
2050 actions.append(na)
2051 actions.append(na)
2051 actions += l
2052 actions += l
2052
2053
2053 rules = '\n'.join([act.torule() for act in actions])
2054 rules = '\n'.join([act.torule() for act in actions])
2054 rules += '\n\n'
2055 rules += '\n\n'
2055 rules += editcomment
2056 rules += editcomment
2056 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2057 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2057 repopath=repo.path, action='histedit')
2058 repopath=repo.path, action='histedit')
2058
2059
2059 # Save edit rules in .hg/histedit-last-edit.txt in case
2060 # Save edit rules in .hg/histedit-last-edit.txt in case
2060 # the user needs to ask for help after something
2061 # the user needs to ask for help after something
2061 # surprising happens.
2062 # surprising happens.
2062 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2063 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2063 f.write(rules)
2064 f.write(rules)
2064
2065
2065 return rules
2066 return rules
2066
2067
2067 def parserules(rules, state):
2068 def parserules(rules, state):
2068 """Read the histedit rules string and return list of action objects """
2069 """Read the histedit rules string and return list of action objects """
2069 rules = [l for l in (r.strip() for r in rules.splitlines())
2070 rules = [l for l in (r.strip() for r in rules.splitlines())
2070 if l and not l.startswith('#')]
2071 if l and not l.startswith('#')]
2071 actions = []
2072 actions = []
2072 for r in rules:
2073 for r in rules:
2073 if ' ' not in r:
2074 if ' ' not in r:
2074 raise error.ParseError(_('malformed line "%s"') % r)
2075 raise error.ParseError(_('malformed line "%s"') % r)
2075 verb, rest = r.split(' ', 1)
2076 verb, rest = r.split(' ', 1)
2076
2077
2077 if verb not in actiontable:
2078 if verb not in actiontable:
2078 raise error.ParseError(_('unknown action "%s"') % verb)
2079 raise error.ParseError(_('unknown action "%s"') % verb)
2079
2080
2080 action = actiontable[verb].fromrule(state, rest)
2081 action = actiontable[verb].fromrule(state, rest)
2081 actions.append(action)
2082 actions.append(action)
2082 return actions
2083 return actions
2083
2084
2084 def warnverifyactions(ui, repo, actions, state, ctxs):
2085 def warnverifyactions(ui, repo, actions, state, ctxs):
2085 try:
2086 try:
2086 verifyactions(actions, state, ctxs)
2087 verifyactions(actions, state, ctxs)
2087 except error.ParseError:
2088 except error.ParseError:
2088 if repo.vfs.exists('histedit-last-edit.txt'):
2089 if repo.vfs.exists('histedit-last-edit.txt'):
2089 ui.warn(_('warning: histedit rules saved '
2090 ui.warn(_('warning: histedit rules saved '
2090 'to: .hg/histedit-last-edit.txt\n'))
2091 'to: .hg/histedit-last-edit.txt\n'))
2091 raise
2092 raise
2092
2093
2093 def verifyactions(actions, state, ctxs):
2094 def verifyactions(actions, state, ctxs):
2094 """Verify that there exists exactly one action per given changeset and
2095 """Verify that there exists exactly one action per given changeset and
2095 other constraints.
2096 other constraints.
2096
2097
2097 Will abort if there are to many or too few rules, a malformed rule,
2098 Will abort if there are to many or too few rules, a malformed rule,
2098 or a rule on a changeset outside of the user-given range.
2099 or a rule on a changeset outside of the user-given range.
2099 """
2100 """
2100 expected = set(c.node() for c in ctxs)
2101 expected = set(c.node() for c in ctxs)
2101 seen = set()
2102 seen = set()
2102 prev = None
2103 prev = None
2103
2104
2104 if actions and actions[0].verb in ['roll', 'fold']:
2105 if actions and actions[0].verb in ['roll', 'fold']:
2105 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2106 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2106 actions[0].verb)
2107 actions[0].verb)
2107
2108
2108 for action in actions:
2109 for action in actions:
2109 action.verify(prev, expected, seen)
2110 action.verify(prev, expected, seen)
2110 prev = action
2111 prev = action
2111 if action.node is not None:
2112 if action.node is not None:
2112 seen.add(action.node)
2113 seen.add(action.node)
2113 missing = sorted(expected - seen) # sort to stabilize output
2114 missing = sorted(expected - seen) # sort to stabilize output
2114
2115
2115 if state.repo.ui.configbool('histedit', 'dropmissing'):
2116 if state.repo.ui.configbool('histedit', 'dropmissing'):
2116 if len(actions) == 0:
2117 if len(actions) == 0:
2117 raise error.ParseError(_('no rules provided'),
2118 raise error.ParseError(_('no rules provided'),
2118 hint=_('use strip extension to remove commits'))
2119 hint=_('use strip extension to remove commits'))
2119
2120
2120 drops = [drop(state, n) for n in missing]
2121 drops = [drop(state, n) for n in missing]
2121 # put the in the beginning so they execute immediately and
2122 # put the in the beginning so they execute immediately and
2122 # don't show in the edit-plan in the future
2123 # don't show in the edit-plan in the future
2123 actions[:0] = drops
2124 actions[:0] = drops
2124 elif missing:
2125 elif missing:
2125 raise error.ParseError(_('missing rules for changeset %s') %
2126 raise error.ParseError(_('missing rules for changeset %s') %
2126 node.short(missing[0]),
2127 node.short(missing[0]),
2127 hint=_('use "drop %s" to discard, see also: '
2128 hint=_('use "drop %s" to discard, see also: '
2128 "'hg help -e histedit.config'")
2129 "'hg help -e histedit.config'")
2129 % node.short(missing[0]))
2130 % node.short(missing[0]))
2130
2131
2131 def adjustreplacementsfrommarkers(repo, oldreplacements):
2132 def adjustreplacementsfrommarkers(repo, oldreplacements):
2132 """Adjust replacements from obsolescence markers
2133 """Adjust replacements from obsolescence markers
2133
2134
2134 Replacements structure is originally generated based on
2135 Replacements structure is originally generated based on
2135 histedit's state and does not account for changes that are
2136 histedit's state and does not account for changes that are
2136 not recorded there. This function fixes that by adding
2137 not recorded there. This function fixes that by adding
2137 data read from obsolescence markers"""
2138 data read from obsolescence markers"""
2138 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2139 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2139 return oldreplacements
2140 return oldreplacements
2140
2141
2141 unfi = repo.unfiltered()
2142 unfi = repo.unfiltered()
2142 nm = unfi.changelog.nodemap
2143 nm = unfi.changelog.nodemap
2143 obsstore = repo.obsstore
2144 obsstore = repo.obsstore
2144 newreplacements = list(oldreplacements)
2145 newreplacements = list(oldreplacements)
2145 oldsuccs = [r[1] for r in oldreplacements]
2146 oldsuccs = [r[1] for r in oldreplacements]
2146 # successors that have already been added to succstocheck once
2147 # successors that have already been added to succstocheck once
2147 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2148 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2148 succstocheck = list(seensuccs)
2149 succstocheck = list(seensuccs)
2149 while succstocheck:
2150 while succstocheck:
2150 n = succstocheck.pop()
2151 n = succstocheck.pop()
2151 missing = nm.get(n) is None
2152 missing = nm.get(n) is None
2152 markers = obsstore.successors.get(n, ())
2153 markers = obsstore.successors.get(n, ())
2153 if missing and not markers:
2154 if missing and not markers:
2154 # dead end, mark it as such
2155 # dead end, mark it as such
2155 newreplacements.append((n, ()))
2156 newreplacements.append((n, ()))
2156 for marker in markers:
2157 for marker in markers:
2157 nsuccs = marker[1]
2158 nsuccs = marker[1]
2158 newreplacements.append((n, nsuccs))
2159 newreplacements.append((n, nsuccs))
2159 for nsucc in nsuccs:
2160 for nsucc in nsuccs:
2160 if nsucc not in seensuccs:
2161 if nsucc not in seensuccs:
2161 seensuccs.add(nsucc)
2162 seensuccs.add(nsucc)
2162 succstocheck.append(nsucc)
2163 succstocheck.append(nsucc)
2163
2164
2164 return newreplacements
2165 return newreplacements
2165
2166
2166 def processreplacement(state):
2167 def processreplacement(state):
2167 """process the list of replacements to return
2168 """process the list of replacements to return
2168
2169
2169 1) the final mapping between original and created nodes
2170 1) the final mapping between original and created nodes
2170 2) the list of temporary node created by histedit
2171 2) the list of temporary node created by histedit
2171 3) the list of new commit created by histedit"""
2172 3) the list of new commit created by histedit"""
2172 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2173 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2173 allsuccs = set()
2174 allsuccs = set()
2174 replaced = set()
2175 replaced = set()
2175 fullmapping = {}
2176 fullmapping = {}
2176 # initialize basic set
2177 # initialize basic set
2177 # fullmapping records all operations recorded in replacement
2178 # fullmapping records all operations recorded in replacement
2178 for rep in replacements:
2179 for rep in replacements:
2179 allsuccs.update(rep[1])
2180 allsuccs.update(rep[1])
2180 replaced.add(rep[0])
2181 replaced.add(rep[0])
2181 fullmapping.setdefault(rep[0], set()).update(rep[1])
2182 fullmapping.setdefault(rep[0], set()).update(rep[1])
2182 new = allsuccs - replaced
2183 new = allsuccs - replaced
2183 tmpnodes = allsuccs & replaced
2184 tmpnodes = allsuccs & replaced
2184 # Reduce content fullmapping into direct relation between original nodes
2185 # Reduce content fullmapping into direct relation between original nodes
2185 # and final node created during history edition
2186 # and final node created during history edition
2186 # Dropped changeset are replaced by an empty list
2187 # Dropped changeset are replaced by an empty list
2187 toproceed = set(fullmapping)
2188 toproceed = set(fullmapping)
2188 final = {}
2189 final = {}
2189 while toproceed:
2190 while toproceed:
2190 for x in list(toproceed):
2191 for x in list(toproceed):
2191 succs = fullmapping[x]
2192 succs = fullmapping[x]
2192 for s in list(succs):
2193 for s in list(succs):
2193 if s in toproceed:
2194 if s in toproceed:
2194 # non final node with unknown closure
2195 # non final node with unknown closure
2195 # We can't process this now
2196 # We can't process this now
2196 break
2197 break
2197 elif s in final:
2198 elif s in final:
2198 # non final node, replace with closure
2199 # non final node, replace with closure
2199 succs.remove(s)
2200 succs.remove(s)
2200 succs.update(final[s])
2201 succs.update(final[s])
2201 else:
2202 else:
2202 final[x] = succs
2203 final[x] = succs
2203 toproceed.remove(x)
2204 toproceed.remove(x)
2204 # remove tmpnodes from final mapping
2205 # remove tmpnodes from final mapping
2205 for n in tmpnodes:
2206 for n in tmpnodes:
2206 del final[n]
2207 del final[n]
2207 # we expect all changes involved in final to exist in the repo
2208 # we expect all changes involved in final to exist in the repo
2208 # turn `final` into list (topologically sorted)
2209 # turn `final` into list (topologically sorted)
2209 nm = state.repo.changelog.nodemap
2210 nm = state.repo.changelog.nodemap
2210 for prec, succs in final.items():
2211 for prec, succs in final.items():
2211 final[prec] = sorted(succs, key=nm.get)
2212 final[prec] = sorted(succs, key=nm.get)
2212
2213
2213 # computed topmost element (necessary for bookmark)
2214 # computed topmost element (necessary for bookmark)
2214 if new:
2215 if new:
2215 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2216 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2216 elif not final:
2217 elif not final:
2217 # Nothing rewritten at all. we won't need `newtopmost`
2218 # Nothing rewritten at all. we won't need `newtopmost`
2218 # It is the same as `oldtopmost` and `processreplacement` know it
2219 # It is the same as `oldtopmost` and `processreplacement` know it
2219 newtopmost = None
2220 newtopmost = None
2220 else:
2221 else:
2221 # every body died. The newtopmost is the parent of the root.
2222 # every body died. The newtopmost is the parent of the root.
2222 r = state.repo.changelog.rev
2223 r = state.repo.changelog.rev
2223 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2224 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2224
2225
2225 return final, tmpnodes, new, newtopmost
2226 return final, tmpnodes, new, newtopmost
2226
2227
2227 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2228 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2228 """Move bookmark from oldtopmost to newly created topmost
2229 """Move bookmark from oldtopmost to newly created topmost
2229
2230
2230 This is arguably a feature and we may only want that for the active
2231 This is arguably a feature and we may only want that for the active
2231 bookmark. But the behavior is kept compatible with the old version for now.
2232 bookmark. But the behavior is kept compatible with the old version for now.
2232 """
2233 """
2233 if not oldtopmost or not newtopmost:
2234 if not oldtopmost or not newtopmost:
2234 return
2235 return
2235 oldbmarks = repo.nodebookmarks(oldtopmost)
2236 oldbmarks = repo.nodebookmarks(oldtopmost)
2236 if oldbmarks:
2237 if oldbmarks:
2237 with repo.lock(), repo.transaction('histedit') as tr:
2238 with repo.lock(), repo.transaction('histedit') as tr:
2238 marks = repo._bookmarks
2239 marks = repo._bookmarks
2239 changes = []
2240 changes = []
2240 for name in oldbmarks:
2241 for name in oldbmarks:
2241 changes.append((name, newtopmost))
2242 changes.append((name, newtopmost))
2242 marks.applychanges(repo, tr, changes)
2243 marks.applychanges(repo, tr, changes)
2243
2244
2244 def cleanupnode(ui, repo, nodes, nobackup=False):
2245 def cleanupnode(ui, repo, nodes, nobackup=False):
2245 """strip a group of nodes from the repository
2246 """strip a group of nodes from the repository
2246
2247
2247 The set of node to strip may contains unknown nodes."""
2248 The set of node to strip may contains unknown nodes."""
2248 with repo.lock():
2249 with repo.lock():
2249 # do not let filtering get in the way of the cleanse
2250 # do not let filtering get in the way of the cleanse
2250 # we should probably get rid of obsolescence marker created during the
2251 # we should probably get rid of obsolescence marker created during the
2251 # histedit, but we currently do not have such information.
2252 # histedit, but we currently do not have such information.
2252 repo = repo.unfiltered()
2253 repo = repo.unfiltered()
2253 # Find all nodes that need to be stripped
2254 # Find all nodes that need to be stripped
2254 # (we use %lr instead of %ln to silently ignore unknown items)
2255 # (we use %lr instead of %ln to silently ignore unknown items)
2255 nm = repo.changelog.nodemap
2256 nm = repo.changelog.nodemap
2256 nodes = sorted(n for n in nodes if n in nm)
2257 nodes = sorted(n for n in nodes if n in nm)
2257 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2258 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2258 if roots:
2259 if roots:
2259 backup = not nobackup
2260 backup = not nobackup
2260 repair.strip(ui, repo, roots, backup=backup)
2261 repair.strip(ui, repo, roots, backup=backup)
2261
2262
2262 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2263 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2263 if isinstance(nodelist, str):
2264 if isinstance(nodelist, str):
2264 nodelist = [nodelist]
2265 nodelist = [nodelist]
2265 state = histeditstate(repo)
2266 state = histeditstate(repo)
2266 if state.inprogress():
2267 if state.inprogress():
2267 state.read()
2268 state.read()
2268 histedit_nodes = {action.node for action
2269 histedit_nodes = {action.node for action
2269 in state.actions if action.node}
2270 in state.actions if action.node}
2270 common_nodes = histedit_nodes & set(nodelist)
2271 common_nodes = histedit_nodes & set(nodelist)
2271 if common_nodes:
2272 if common_nodes:
2272 raise error.Abort(_("histedit in progress, can't strip %s")
2273 raise error.Abort(_("histedit in progress, can't strip %s")
2273 % ', '.join(node.short(x) for x in common_nodes))
2274 % ', '.join(node.short(x) for x in common_nodes))
2274 return orig(ui, repo, nodelist, *args, **kwargs)
2275 return orig(ui, repo, nodelist, *args, **kwargs)
2275
2276
2276 extensions.wrapfunction(repair, 'strip', stripwrapper)
2277 extensions.wrapfunction(repair, 'strip', stripwrapper)
2277
2278
2278 def summaryhook(ui, repo):
2279 def summaryhook(ui, repo):
2279 state = histeditstate(repo)
2280 state = histeditstate(repo)
2280 if not state.inprogress():
2281 if not state.inprogress():
2281 return
2282 return
2282 state.read()
2283 state.read()
2283 if state.actions:
2284 if state.actions:
2284 # i18n: column positioning for "hg summary"
2285 # i18n: column positioning for "hg summary"
2285 ui.write(_('hist: %s (histedit --continue)\n') %
2286 ui.write(_('hist: %s (histedit --continue)\n') %
2286 (ui.label(_('%d remaining'), 'histedit.remaining') %
2287 (ui.label(_('%d remaining'), 'histedit.remaining') %
2287 len(state.actions)))
2288 len(state.actions)))
2288
2289
2289 def extsetup(ui):
2290 def extsetup(ui):
2290 cmdutil.summaryhooks.add('histedit', summaryhook)
2291 cmdutil.summaryhooks.add('histedit', summaryhook)
2291 cmdutil.unfinishedstates.append(
2292 cmdutil.unfinishedstates.append(
2292 ['histedit-state', False, True, _('histedit in progress'),
2293 ['histedit-state', False, True, _('histedit in progress'),
2293 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2294 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2294 cmdutil.afterresolvedstates.append(
2295 cmdutil.afterresolvedstates.append(
2295 ['histedit-state', _('hg histedit --continue')])
2296 ['histedit-state', _('hg histedit --continue')])
General Comments 0
You need to be logged in to leave comments. Login now