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