##// END OF EJS Templates
chistedit: move movecursor() onto state class...
Martin von Zweigbergk -
r49021:fb30ad66 default
parent child Browse files
Show More
@@ -1,2664 +1,2663 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):
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)."""
1200 state.pos = newpos
1201
1202 mode, _ = state.mode
1203 if mode == MODE_RULES:
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
1206 # result in the cursor's rule being visible.
1207 modestate = state.modes[MODE_RULES]
1208 if newpos < modestate[b'line_offset']:
1209 modestate[b'line_offset'] = newpos
1210 elif newpos > modestate[b'line_offset'] + state.page_height - 1:
1211 modestate[b'line_offset'] = newpos - state.page_height + 1
1212
1213 # Reset the patch view region to the top of the new patch.
1214 state.modes[MODE_PATCH][b'line_offset'] = 0
1215
1216
1217 def changemode(state, mode):
1197 def changemode(state, mode):
1218 curmode, _ = state.mode
1198 curmode, _ = state.mode
1219 state.mode = (mode, curmode)
1199 state.mode = (mode, curmode)
1220 if mode == MODE_PATCH:
1200 if mode == MODE_PATCH:
1221 state.modes[MODE_PATCH][b'patchcontents'] = state.patch_contents()
1201 state.modes[MODE_PATCH][b'patchcontents'] = state.patch_contents()
1222
1202
1223
1203
1224 def makeselection(state, pos):
1204 def makeselection(state, pos):
1225 state.selected = pos
1205 state.selected = pos
1226
1206
1227
1207
1228 def swap(state, oldpos, newpos):
1208 def swap(state, oldpos, newpos):
1229 """Swap two positions and calculate necessary conflicts in
1209 """Swap two positions and calculate necessary conflicts in
1230 O(|newpos-oldpos|) time"""
1210 O(|newpos-oldpos|) time"""
1231
1211
1232 rules = state.rules
1212 rules = state.rules
1233 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1213 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1234
1214
1235 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1215 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1236
1216
1237 # TODO: swap should not know about histeditrule's internals
1217 # TODO: swap should not know about histeditrule's internals
1238 rules[newpos].pos = newpos
1218 rules[newpos].pos = newpos
1239 rules[oldpos].pos = oldpos
1219 rules[oldpos].pos = oldpos
1240
1220
1241 start = min(oldpos, newpos)
1221 start = min(oldpos, newpos)
1242 end = max(oldpos, newpos)
1222 end = max(oldpos, newpos)
1243 for r in pycompat.xrange(start, end + 1):
1223 for r in pycompat.xrange(start, end + 1):
1244 rules[newpos].checkconflicts(rules[r])
1224 rules[newpos].checkconflicts(rules[r])
1245 rules[oldpos].checkconflicts(rules[r])
1225 rules[oldpos].checkconflicts(rules[r])
1246
1226
1247 if state.selected:
1227 if state.selected:
1248 makeselection(state, newpos)
1228 makeselection(state, newpos)
1249
1229
1250
1230
1251 def changeaction(state, pos, action):
1231 def changeaction(state, pos, action):
1252 """Change the action state on the given position to the new action"""
1232 """Change the action state on the given position to the new action"""
1253 rules = state.rules
1233 rules = state.rules
1254 assert 0 <= pos < len(rules)
1234 assert 0 <= pos < len(rules)
1255 rules[pos].action = action
1235 rules[pos].action = action
1256
1236
1257
1237
1258 def cycleaction(state, pos, next=False):
1238 def cycleaction(state, pos, next=False):
1259 """Changes the action state the next or the previous action from
1239 """Changes the action state the next or the previous action from
1260 the action list"""
1240 the action list"""
1261 rules = state.rules
1241 rules = state.rules
1262 assert 0 <= pos < len(rules)
1242 assert 0 <= pos < len(rules)
1263 current = rules[pos].action
1243 current = rules[pos].action
1264
1244
1265 assert current in KEY_LIST
1245 assert current in KEY_LIST
1266
1246
1267 index = KEY_LIST.index(current)
1247 index = KEY_LIST.index(current)
1268 if next:
1248 if next:
1269 index += 1
1249 index += 1
1270 else:
1250 else:
1271 index -= 1
1251 index -= 1
1272 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1252 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1273
1253
1274
1254
1275 def changeview(state, delta, unit):
1255 def changeview(state, delta, unit):
1276 """Change the region of whatever is being viewed (a patch or the list of
1256 """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'."""
1257 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'."""
1278 mode, _ = state.mode
1258 mode, _ = state.mode
1279 if mode != MODE_PATCH:
1259 if mode != MODE_PATCH:
1280 return
1260 return
1281 mode_state = state.modes[mode]
1261 mode_state = state.modes[mode]
1282 num_lines = len(mode_state[b'patchcontents'])
1262 num_lines = len(mode_state[b'patchcontents'])
1283 page_height = state.page_height
1263 page_height = state.page_height
1284 unit = page_height if unit == b'page' else 1
1264 unit = page_height if unit == b'page' else 1
1285 num_pages = 1 + (num_lines - 1) // page_height
1265 num_pages = 1 + (num_lines - 1) // page_height
1286 max_offset = (num_pages - 1) * page_height
1266 max_offset = (num_pages - 1) * page_height
1287 newline = mode_state[b'line_offset'] + delta * unit
1267 newline = mode_state[b'line_offset'] + delta * unit
1288 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1268 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1289
1269
1290
1270
1291 def makecommands(rules):
1271 def makecommands(rules):
1292 """Returns a list of commands consumable by histedit --commands based on
1272 """Returns a list of commands consumable by histedit --commands based on
1293 our list of rules"""
1273 our list of rules"""
1294 commands = []
1274 commands = []
1295 for rules in rules:
1275 for rules in rules:
1296 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1276 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1297 return commands
1277 return commands
1298
1278
1299
1279
1300 def addln(win, y, x, line, color=None):
1280 def addln(win, y, x, line, color=None):
1301 """Add a line to the given window left padding but 100% filled with
1281 """Add a line to the given window left padding but 100% filled with
1302 whitespace characters, so that the color appears on the whole line"""
1282 whitespace characters, so that the color appears on the whole line"""
1303 maxy, maxx = win.getmaxyx()
1283 maxy, maxx = win.getmaxyx()
1304 length = maxx - 1 - x
1284 length = maxx - 1 - x
1305 line = bytes(line).ljust(length)[:length]
1285 line = bytes(line).ljust(length)[:length]
1306 if y < 0:
1286 if y < 0:
1307 y = maxy + y
1287 y = maxy + y
1308 if x < 0:
1288 if x < 0:
1309 x = maxx + x
1289 x = maxx + x
1310 if color:
1290 if color:
1311 win.addstr(y, x, line, color)
1291 win.addstr(y, x, line, color)
1312 else:
1292 else:
1313 win.addstr(y, x, line)
1293 win.addstr(y, x, line)
1314
1294
1315
1295
1316 def _trunc_head(line, n):
1296 def _trunc_head(line, n):
1317 if len(line) <= n:
1297 if len(line) <= n:
1318 return line
1298 return line
1319 return b'> ' + line[-(n - 2) :]
1299 return b'> ' + line[-(n - 2) :]
1320
1300
1321
1301
1322 def _trunc_tail(line, n):
1302 def _trunc_tail(line, n):
1323 if len(line) <= n:
1303 if len(line) <= n:
1324 return line
1304 return line
1325 return line[: n - 2] + b' >'
1305 return line[: n - 2] + b' >'
1326
1306
1327
1307
1328 class _chistedit_state(object):
1308 class _chistedit_state(object):
1329 def __init__(
1309 def __init__(
1330 self,
1310 self,
1331 repo,
1311 repo,
1332 rules,
1312 rules,
1333 stdscr,
1313 stdscr,
1334 ):
1314 ):
1335 self.repo = repo
1315 self.repo = repo
1336 self.rules = rules
1316 self.rules = rules
1337 self.stdscr = stdscr
1317 self.stdscr = stdscr
1338 self.pos = 0
1318 self.pos = 0
1339 self.selected = None
1319 self.selected = None
1340 self.mode = (MODE_INIT, MODE_INIT)
1320 self.mode = (MODE_INIT, MODE_INIT)
1341 self.page_height = None
1321 self.page_height = None
1342 self.modes = {
1322 self.modes = {
1343 MODE_RULES: {
1323 MODE_RULES: {
1344 b'line_offset': 0,
1324 b'line_offset': 0,
1345 },
1325 },
1346 MODE_PATCH: {
1326 MODE_PATCH: {
1347 b'line_offset': 0,
1327 b'line_offset': 0,
1348 },
1328 },
1349 }
1329 }
1350
1330
1351 def render_commit(self, win):
1331 def render_commit(self, win):
1352 """Renders the commit window that shows the log of the current selected
1332 """Renders the commit window that shows the log of the current selected
1353 commit"""
1333 commit"""
1354 pos = self.pos
1334 pos = self.pos
1355 rules = self.rules
1335 rules = self.rules
1356 rule = rules[pos]
1336 rule = rules[pos]
1357
1337
1358 ctx = rule.ctx
1338 ctx = rule.ctx
1359 win.box()
1339 win.box()
1360
1340
1361 maxy, maxx = win.getmaxyx()
1341 maxy, maxx = win.getmaxyx()
1362 length = maxx - 3
1342 length = maxx - 3
1363
1343
1364 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1344 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1365 win.addstr(1, 1, line[:length])
1345 win.addstr(1, 1, line[:length])
1366
1346
1367 line = b"user: %s" % ctx.user()
1347 line = b"user: %s" % ctx.user()
1368 win.addstr(2, 1, line[:length])
1348 win.addstr(2, 1, line[:length])
1369
1349
1370 bms = self.repo.nodebookmarks(ctx.node())
1350 bms = self.repo.nodebookmarks(ctx.node())
1371 line = b"bookmark: %s" % b' '.join(bms)
1351 line = b"bookmark: %s" % b' '.join(bms)
1372 win.addstr(3, 1, line[:length])
1352 win.addstr(3, 1, line[:length])
1373
1353
1374 line = b"summary: %s" % (ctx.description().splitlines()[0])
1354 line = b"summary: %s" % (ctx.description().splitlines()[0])
1375 win.addstr(4, 1, line[:length])
1355 win.addstr(4, 1, line[:length])
1376
1356
1377 line = b"files: "
1357 line = b"files: "
1378 win.addstr(5, 1, line)
1358 win.addstr(5, 1, line)
1379 fnx = 1 + len(line)
1359 fnx = 1 + len(line)
1380 fnmaxx = length - fnx + 1
1360 fnmaxx = length - fnx + 1
1381 y = 5
1361 y = 5
1382 fnmaxn = maxy - (1 + y) - 1
1362 fnmaxn = maxy - (1 + y) - 1
1383 files = ctx.files()
1363 files = ctx.files()
1384 for i, line1 in enumerate(files):
1364 for i, line1 in enumerate(files):
1385 if len(files) > fnmaxn and i == fnmaxn - 1:
1365 if len(files) > fnmaxn and i == fnmaxn - 1:
1386 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1366 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1387 y = y + 1
1367 y = y + 1
1388 break
1368 break
1389 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1369 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1390 y = y + 1
1370 y = y + 1
1391
1371
1392 conflicts = rule.conflicts
1372 conflicts = rule.conflicts
1393 if len(conflicts) > 0:
1373 if len(conflicts) > 0:
1394 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1374 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1395 conflictstr = b"changed files overlap with %s" % conflictstr
1375 conflictstr = b"changed files overlap with %s" % conflictstr
1396 else:
1376 else:
1397 conflictstr = b'no overlap'
1377 conflictstr = b'no overlap'
1398
1378
1399 win.addstr(y, 1, conflictstr[:length])
1379 win.addstr(y, 1, conflictstr[:length])
1400 win.noutrefresh()
1380 win.noutrefresh()
1401
1381
1402 def helplines(self):
1382 def helplines(self):
1403 if self.mode[0] == MODE_PATCH:
1383 if self.mode[0] == MODE_PATCH:
1404 help = b"""\
1384 help = b"""\
1405 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1385 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1406 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1386 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1407 """
1387 """
1408 else:
1388 else:
1409 help = b"""\
1389 help = b"""\
1410 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1390 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1411 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1391 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1412 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1392 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1413 """
1393 """
1414 return help.splitlines()
1394 return help.splitlines()
1415
1395
1416 def render_help(self, win):
1396 def render_help(self, win):
1417 maxy, maxx = win.getmaxyx()
1397 maxy, maxx = win.getmaxyx()
1418 for y, line in enumerate(self.helplines()):
1398 for y, line in enumerate(self.helplines()):
1419 if y >= maxy:
1399 if y >= maxy:
1420 break
1400 break
1421 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1401 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1422 win.noutrefresh()
1402 win.noutrefresh()
1423
1403
1424 def layout(self):
1404 def layout(self):
1425 maxy, maxx = self.stdscr.getmaxyx()
1405 maxy, maxx = self.stdscr.getmaxyx()
1426 helplen = len(self.helplines())
1406 helplen = len(self.helplines())
1427 mainlen = maxy - helplen - 12
1407 mainlen = maxy - helplen - 12
1428 if mainlen < 1:
1408 if mainlen < 1:
1429 raise error.Abort(
1409 raise error.Abort(
1430 _(b"terminal dimensions %d by %d too small for curses histedit")
1410 _(b"terminal dimensions %d by %d too small for curses histedit")
1431 % (maxy, maxx),
1411 % (maxy, maxx),
1432 hint=_(
1412 hint=_(
1433 b"enlarge your terminal or use --config ui.interface=text"
1413 b"enlarge your terminal or use --config ui.interface=text"
1434 ),
1414 ),
1435 )
1415 )
1436 return {
1416 return {
1437 b'commit': (12, maxx),
1417 b'commit': (12, maxx),
1438 b'help': (helplen, maxx),
1418 b'help': (helplen, maxx),
1439 b'main': (mainlen, maxx),
1419 b'main': (mainlen, maxx),
1440 }
1420 }
1441
1421
1442 def render_rules(self, rulesscr):
1422 def render_rules(self, rulesscr):
1443 rules = self.rules
1423 rules = self.rules
1444 pos = self.pos
1424 pos = self.pos
1445 selected = self.selected
1425 selected = self.selected
1446 start = self.modes[MODE_RULES][b'line_offset']
1426 start = self.modes[MODE_RULES][b'line_offset']
1447
1427
1448 conflicts = [r.ctx for r in rules if r.conflicts]
1428 conflicts = [r.ctx for r in rules if r.conflicts]
1449 if len(conflicts) > 0:
1429 if len(conflicts) > 0:
1450 line = b"potential conflict in %s" % b','.join(
1430 line = b"potential conflict in %s" % b','.join(
1451 map(pycompat.bytestr, conflicts)
1431 map(pycompat.bytestr, conflicts)
1452 )
1432 )
1453 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1433 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1454
1434
1455 for y, rule in enumerate(rules[start:]):
1435 for y, rule in enumerate(rules[start:]):
1456 if y >= self.page_height:
1436 if y >= self.page_height:
1457 break
1437 break
1458 if len(rule.conflicts) > 0:
1438 if len(rule.conflicts) > 0:
1459 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1439 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1460 else:
1440 else:
1461 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1441 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1462
1442
1463 if y + start == selected:
1443 if y + start == selected:
1464 rollcolor = COLOR_ROLL_SELECTED
1444 rollcolor = COLOR_ROLL_SELECTED
1465 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1445 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1466 elif y + start == pos:
1446 elif y + start == pos:
1467 rollcolor = COLOR_ROLL_CURRENT
1447 rollcolor = COLOR_ROLL_CURRENT
1468 addln(
1448 addln(
1469 rulesscr,
1449 rulesscr,
1470 y,
1450 y,
1471 2,
1451 2,
1472 rule,
1452 rule,
1473 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1453 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1474 )
1454 )
1475 else:
1455 else:
1476 rollcolor = COLOR_ROLL
1456 rollcolor = COLOR_ROLL
1477 addln(rulesscr, y, 2, rule)
1457 addln(rulesscr, y, 2, rule)
1478
1458
1479 if rule.action == b'roll':
1459 if rule.action == b'roll':
1480 rulesscr.addstr(
1460 rulesscr.addstr(
1481 y,
1461 y,
1482 2 + len(rule.prefix),
1462 2 + len(rule.prefix),
1483 rule.desc,
1463 rule.desc,
1484 curses.color_pair(rollcolor),
1464 curses.color_pair(rollcolor),
1485 )
1465 )
1486
1466
1487 rulesscr.noutrefresh()
1467 rulesscr.noutrefresh()
1488
1468
1489 def render_string(self, win, output, diffcolors=False):
1469 def render_string(self, win, output, diffcolors=False):
1490 maxy, maxx = win.getmaxyx()
1470 maxy, maxx = win.getmaxyx()
1491 length = min(maxy - 1, len(output))
1471 length = min(maxy - 1, len(output))
1492 for y in range(0, length):
1472 for y in range(0, length):
1493 line = output[y]
1473 line = output[y]
1494 if diffcolors:
1474 if diffcolors:
1495 if line and line[0] == b'+':
1475 if line and line[0] == b'+':
1496 win.addstr(
1476 win.addstr(
1497 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1477 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1498 )
1478 )
1499 elif line and line[0] == b'-':
1479 elif line and line[0] == b'-':
1500 win.addstr(
1480 win.addstr(
1501 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1481 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1502 )
1482 )
1503 elif line.startswith(b'@@ '):
1483 elif line.startswith(b'@@ '):
1504 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1484 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1505 else:
1485 else:
1506 win.addstr(y, 0, line)
1486 win.addstr(y, 0, line)
1507 else:
1487 else:
1508 win.addstr(y, 0, line)
1488 win.addstr(y, 0, line)
1509 win.noutrefresh()
1489 win.noutrefresh()
1510
1490
1511 def render_patch(self, win):
1491 def render_patch(self, win):
1512 start = self.modes[MODE_PATCH][b'line_offset']
1492 start = self.modes[MODE_PATCH][b'line_offset']
1513 content = self.modes[MODE_PATCH][b'patchcontents']
1493 content = self.modes[MODE_PATCH][b'patchcontents']
1514 self.render_string(win, content[start:], diffcolors=True)
1494 self.render_string(win, content[start:], diffcolors=True)
1515
1495
1516 def event(self, ch):
1496 def event(self, ch):
1517 """Change state based on the current character input
1497 """Change state based on the current character input
1518
1498
1519 This takes the current state and based on the current character input from
1499 This takes the current state and based on the current character input from
1520 the user we change the state.
1500 the user we change the state.
1521 """
1501 """
1522 selected = self.selected
1502 selected = self.selected
1523 oldpos = self.pos
1503 oldpos = self.pos
1524 rules = self.rules
1504 rules = self.rules
1525
1505
1526 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1506 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1527 return E_RESIZE
1507 return E_RESIZE
1528
1508
1529 lookup_ch = ch
1509 lookup_ch = ch
1530 if ch is not None and b'0' <= ch <= b'9':
1510 if ch is not None and b'0' <= ch <= b'9':
1531 lookup_ch = b'0'
1511 lookup_ch = b'0'
1532
1512
1533 curmode, prevmode = self.mode
1513 curmode, prevmode = self.mode
1534 action = KEYTABLE[curmode].get(
1514 action = KEYTABLE[curmode].get(
1535 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1515 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1536 )
1516 )
1537 if action is None:
1517 if action is None:
1538 return
1518 return
1539 if action in (b'down', b'move-down'):
1519 if action in (b'down', b'move-down'):
1540 newpos = min(oldpos + 1, len(rules) - 1)
1520 newpos = min(oldpos + 1, len(rules) - 1)
1541 movecursor(self, oldpos, newpos)
1521 self.move_cursor(oldpos, newpos)
1542 if selected is not None or action == b'move-down':
1522 if selected is not None or action == b'move-down':
1543 swap(self, oldpos, newpos)
1523 swap(self, oldpos, newpos)
1544 elif action in (b'up', b'move-up'):
1524 elif action in (b'up', b'move-up'):
1545 newpos = max(0, oldpos - 1)
1525 newpos = max(0, oldpos - 1)
1546 movecursor(self, oldpos, newpos)
1526 self.move_cursor(oldpos, newpos)
1547 if selected is not None or action == b'move-up':
1527 if selected is not None or action == b'move-up':
1548 swap(self, oldpos, newpos)
1528 swap(self, oldpos, newpos)
1549 elif action == b'next-action':
1529 elif action == b'next-action':
1550 cycleaction(self, oldpos, next=True)
1530 cycleaction(self, oldpos, next=True)
1551 elif action == b'prev-action':
1531 elif action == b'prev-action':
1552 cycleaction(self, oldpos, next=False)
1532 cycleaction(self, oldpos, next=False)
1553 elif action == b'select':
1533 elif action == b'select':
1554 selected = oldpos if selected is None else None
1534 selected = oldpos if selected is None else None
1555 makeselection(self, selected)
1535 makeselection(self, selected)
1556 elif action == b'goto' and int(ch) < len(rules) and len(rules) <= 10:
1536 elif action == b'goto' and int(ch) < len(rules) and len(rules) <= 10:
1557 newrule = next((r for r in rules if r.origpos == int(ch)))
1537 newrule = next((r for r in rules if r.origpos == int(ch)))
1558 movecursor(self, oldpos, newrule.pos)
1538 self.move_cursor(oldpos, newrule.pos)
1559 if selected is not None:
1539 if selected is not None:
1560 swap(self, oldpos, newrule.pos)
1540 swap(self, oldpos, newrule.pos)
1561 elif action.startswith(b'action-'):
1541 elif action.startswith(b'action-'):
1562 changeaction(self, oldpos, action[7:])
1542 changeaction(self, oldpos, action[7:])
1563 elif action == b'showpatch':
1543 elif action == b'showpatch':
1564 changemode(self, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1544 changemode(self, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1565 elif action == b'help':
1545 elif action == b'help':
1566 changemode(self, MODE_HELP if curmode != MODE_HELP else prevmode)
1546 changemode(self, MODE_HELP if curmode != MODE_HELP else prevmode)
1567 elif action == b'quit':
1547 elif action == b'quit':
1568 return E_QUIT
1548 return E_QUIT
1569 elif action == b'histedit':
1549 elif action == b'histedit':
1570 return E_HISTEDIT
1550 return E_HISTEDIT
1571 elif action == b'page-down':
1551 elif action == b'page-down':
1572 return E_PAGEDOWN
1552 return E_PAGEDOWN
1573 elif action == b'page-up':
1553 elif action == b'page-up':
1574 return E_PAGEUP
1554 return E_PAGEUP
1575 elif action == b'line-down':
1555 elif action == b'line-down':
1576 return E_LINEDOWN
1556 return E_LINEDOWN
1577 elif action == b'line-up':
1557 elif action == b'line-up':
1578 return E_LINEUP
1558 return E_LINEUP
1579
1559
1580 def patch_contents(self):
1560 def patch_contents(self):
1581 repo = self.repo
1561 repo = self.repo
1582 rule = self.rules[self.pos]
1562 rule = self.rules[self.pos]
1583 displayer = logcmdutil.changesetdisplayer(
1563 displayer = logcmdutil.changesetdisplayer(
1584 repo.ui,
1564 repo.ui,
1585 repo,
1565 repo,
1586 {b"patch": True, b"template": b"status"},
1566 {b"patch": True, b"template": b"status"},
1587 buffered=True,
1567 buffered=True,
1588 )
1568 )
1589 overrides = {(b'ui', b'verbose'): True}
1569 overrides = {(b'ui', b'verbose'): True}
1590 with repo.ui.configoverride(overrides, source=b'histedit'):
1570 with repo.ui.configoverride(overrides, source=b'histedit'):
1591 displayer.show(rule.ctx)
1571 displayer.show(rule.ctx)
1592 displayer.close()
1572 displayer.close()
1593 return displayer.hunk[rule.ctx.rev()].splitlines()
1573 return displayer.hunk[rule.ctx.rev()].splitlines()
1594
1574
1575 def move_cursor(self, oldpos, newpos):
1576 """Change the rule/changeset that the cursor is pointing to, regardless of
1577 current mode (you can switch between patches from the view patch window)."""
1578 self.pos = newpos
1579
1580 mode, _ = self.mode
1581 if mode == MODE_RULES:
1582 # Scroll through the list by updating the view for MODE_RULES, so that
1583 # even if we are not currently viewing the rules, switching back will
1584 # result in the cursor's rule being visible.
1585 modestate = self.modes[MODE_RULES]
1586 if newpos < modestate[b'line_offset']:
1587 modestate[b'line_offset'] = newpos
1588 elif newpos > modestate[b'line_offset'] + self.page_height - 1:
1589 modestate[b'line_offset'] = newpos - self.page_height + 1
1590
1591 # Reset the patch view region to the top of the new patch.
1592 self.modes[MODE_PATCH][b'line_offset'] = 0
1593
1595
1594
1596 def _chisteditmain(repo, rules, stdscr):
1595 def _chisteditmain(repo, rules, stdscr):
1597 try:
1596 try:
1598 curses.use_default_colors()
1597 curses.use_default_colors()
1599 except curses.error:
1598 except curses.error:
1600 pass
1599 pass
1601
1600
1602 # initialize color pattern
1601 # initialize color pattern
1603 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1602 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1604 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1603 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1605 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1604 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1606 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1605 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1607 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1606 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1608 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1607 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1609 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1608 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1610 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1609 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1611 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1610 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1612 curses.init_pair(
1611 curses.init_pair(
1613 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1612 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1614 )
1613 )
1615 curses.init_pair(COLOR_ROLL_SELECTED, curses.COLOR_RED, curses.COLOR_WHITE)
1614 curses.init_pair(COLOR_ROLL_SELECTED, curses.COLOR_RED, curses.COLOR_WHITE)
1616
1615
1617 # don't display the cursor
1616 # don't display the cursor
1618 try:
1617 try:
1619 curses.curs_set(0)
1618 curses.curs_set(0)
1620 except curses.error:
1619 except curses.error:
1621 pass
1620 pass
1622
1621
1623 def drawvertwin(size, y, x):
1622 def drawvertwin(size, y, x):
1624 win = curses.newwin(size[0], size[1], y, x)
1623 win = curses.newwin(size[0], size[1], y, x)
1625 y += size[0]
1624 y += size[0]
1626 return win, y, x
1625 return win, y, x
1627
1626
1628 state = _chistedit_state(repo, rules, stdscr)
1627 state = _chistedit_state(repo, rules, stdscr)
1629
1628
1630 # eventloop
1629 # eventloop
1631 ch = None
1630 ch = None
1632 stdscr.clear()
1631 stdscr.clear()
1633 stdscr.refresh()
1632 stdscr.refresh()
1634 while True:
1633 while True:
1635 oldmode, unused = state.mode
1634 oldmode, unused = state.mode
1636 if oldmode == MODE_INIT:
1635 if oldmode == MODE_INIT:
1637 changemode(state, MODE_RULES)
1636 changemode(state, MODE_RULES)
1638 e = state.event(ch)
1637 e = state.event(ch)
1639
1638
1640 if e == E_QUIT:
1639 if e == E_QUIT:
1641 return False
1640 return False
1642 if e == E_HISTEDIT:
1641 if e == E_HISTEDIT:
1643 return state.rules
1642 return state.rules
1644 else:
1643 else:
1645 if e == E_RESIZE:
1644 if e == E_RESIZE:
1646 size = screen_size()
1645 size = screen_size()
1647 if size != stdscr.getmaxyx():
1646 if size != stdscr.getmaxyx():
1648 curses.resizeterm(*size)
1647 curses.resizeterm(*size)
1649
1648
1650 sizes = state.layout()
1649 sizes = state.layout()
1651 curmode, unused = state.mode
1650 curmode, unused = state.mode
1652 if curmode != oldmode:
1651 if curmode != oldmode:
1653 state.page_height = sizes[b'main'][0]
1652 state.page_height = sizes[b'main'][0]
1654 # Adjust the view to fit the current screen size.
1653 # Adjust the view to fit the current screen size.
1655 movecursor(state, state.pos, state.pos)
1654 state.move_cursor(state.pos, state.pos)
1656
1655
1657 # Pack the windows against the top, each pane spread across the
1656 # Pack the windows against the top, each pane spread across the
1658 # full width of the screen.
1657 # full width of the screen.
1659 y, x = (0, 0)
1658 y, x = (0, 0)
1660 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1659 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1661 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1660 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1662 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1661 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1663
1662
1664 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1663 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1665 if e == E_PAGEDOWN:
1664 if e == E_PAGEDOWN:
1666 changeview(state, +1, b'page')
1665 changeview(state, +1, b'page')
1667 elif e == E_PAGEUP:
1666 elif e == E_PAGEUP:
1668 changeview(state, -1, b'page')
1667 changeview(state, -1, b'page')
1669 elif e == E_LINEDOWN:
1668 elif e == E_LINEDOWN:
1670 changeview(state, +1, b'line')
1669 changeview(state, +1, b'line')
1671 elif e == E_LINEUP:
1670 elif e == E_LINEUP:
1672 changeview(state, -1, b'line')
1671 changeview(state, -1, b'line')
1673
1672
1674 # start rendering
1673 # start rendering
1675 commitwin.erase()
1674 commitwin.erase()
1676 helpwin.erase()
1675 helpwin.erase()
1677 mainwin.erase()
1676 mainwin.erase()
1678 if curmode == MODE_PATCH:
1677 if curmode == MODE_PATCH:
1679 state.render_patch(mainwin)
1678 state.render_patch(mainwin)
1680 elif curmode == MODE_HELP:
1679 elif curmode == MODE_HELP:
1681 state.render_string(mainwin, __doc__.strip().splitlines())
1680 state.render_string(mainwin, __doc__.strip().splitlines())
1682 else:
1681 else:
1683 state.render_rules(mainwin)
1682 state.render_rules(mainwin)
1684 state.render_commit(commitwin)
1683 state.render_commit(commitwin)
1685 state.render_help(helpwin)
1684 state.render_help(helpwin)
1686 curses.doupdate()
1685 curses.doupdate()
1687 # done rendering
1686 # done rendering
1688 ch = encoding.strtolocal(stdscr.getkey())
1687 ch = encoding.strtolocal(stdscr.getkey())
1689
1688
1690
1689
1691 def _chistedit(ui, repo, freeargs, opts):
1690 def _chistedit(ui, repo, freeargs, opts):
1692 """interactively edit changeset history via a curses interface
1691 """interactively edit changeset history via a curses interface
1693
1692
1694 Provides a ncurses interface to histedit. Press ? in chistedit mode
1693 Provides a ncurses interface to histedit. Press ? in chistedit mode
1695 to see an extensive help. Requires python-curses to be installed."""
1694 to see an extensive help. Requires python-curses to be installed."""
1696
1695
1697 if curses is None:
1696 if curses is None:
1698 raise error.Abort(_(b"Python curses library required"))
1697 raise error.Abort(_(b"Python curses library required"))
1699
1698
1700 # disable color
1699 # disable color
1701 ui._colormode = None
1700 ui._colormode = None
1702
1701
1703 try:
1702 try:
1704 keep = opts.get(b'keep')
1703 keep = opts.get(b'keep')
1705 revs = opts.get(b'rev', [])[:]
1704 revs = opts.get(b'rev', [])[:]
1706 cmdutil.checkunfinished(repo)
1705 cmdutil.checkunfinished(repo)
1707 cmdutil.bailifchanged(repo)
1706 cmdutil.bailifchanged(repo)
1708
1707
1709 revs.extend(freeargs)
1708 revs.extend(freeargs)
1710 if not revs:
1709 if not revs:
1711 defaultrev = destutil.desthistedit(ui, repo)
1710 defaultrev = destutil.desthistedit(ui, repo)
1712 if defaultrev is not None:
1711 if defaultrev is not None:
1713 revs.append(defaultrev)
1712 revs.append(defaultrev)
1714 if len(revs) != 1:
1713 if len(revs) != 1:
1715 raise error.InputError(
1714 raise error.InputError(
1716 _(b'histedit requires exactly one ancestor revision')
1715 _(b'histedit requires exactly one ancestor revision')
1717 )
1716 )
1718
1717
1719 rr = list(repo.set(b'roots(%ld)', logcmdutil.revrange(repo, revs)))
1718 rr = list(repo.set(b'roots(%ld)', logcmdutil.revrange(repo, revs)))
1720 if len(rr) != 1:
1719 if len(rr) != 1:
1721 raise error.InputError(
1720 raise error.InputError(
1722 _(
1721 _(
1723 b'The specified revisions must have '
1722 b'The specified revisions must have '
1724 b'exactly one common root'
1723 b'exactly one common root'
1725 )
1724 )
1726 )
1725 )
1727 root = rr[0].node()
1726 root = rr[0].node()
1728
1727
1729 topmost = repo.dirstate.p1()
1728 topmost = repo.dirstate.p1()
1730 revs = between(repo, root, topmost, keep)
1729 revs = between(repo, root, topmost, keep)
1731 if not revs:
1730 if not revs:
1732 raise error.InputError(
1731 raise error.InputError(
1733 _(b'%s is not an ancestor of working directory') % short(root)
1732 _(b'%s is not an ancestor of working directory') % short(root)
1734 )
1733 )
1735
1734
1736 ctxs = []
1735 ctxs = []
1737 for i, r in enumerate(revs):
1736 for i, r in enumerate(revs):
1738 ctxs.append(histeditrule(ui, repo[r], i))
1737 ctxs.append(histeditrule(ui, repo[r], i))
1739 with util.with_lc_ctype():
1738 with util.with_lc_ctype():
1740 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1739 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1741 curses.echo()
1740 curses.echo()
1742 curses.endwin()
1741 curses.endwin()
1743 if rc is False:
1742 if rc is False:
1744 ui.write(_(b"histedit aborted\n"))
1743 ui.write(_(b"histedit aborted\n"))
1745 return 0
1744 return 0
1746 if type(rc) is list:
1745 if type(rc) is list:
1747 ui.status(_(b"performing changes\n"))
1746 ui.status(_(b"performing changes\n"))
1748 rules = makecommands(rc)
1747 rules = makecommands(rc)
1749 with repo.vfs(b'chistedit', b'w+') as fp:
1748 with repo.vfs(b'chistedit', b'w+') as fp:
1750 for r in rules:
1749 for r in rules:
1751 fp.write(r)
1750 fp.write(r)
1752 opts[b'commands'] = fp.name
1751 opts[b'commands'] = fp.name
1753 return _texthistedit(ui, repo, freeargs, opts)
1752 return _texthistedit(ui, repo, freeargs, opts)
1754 except KeyboardInterrupt:
1753 except KeyboardInterrupt:
1755 pass
1754 pass
1756 return -1
1755 return -1
1757
1756
1758
1757
1759 @command(
1758 @command(
1760 b'histedit',
1759 b'histedit',
1761 [
1760 [
1762 (
1761 (
1763 b'',
1762 b'',
1764 b'commands',
1763 b'commands',
1765 b'',
1764 b'',
1766 _(b'read history edits from the specified file'),
1765 _(b'read history edits from the specified file'),
1767 _(b'FILE'),
1766 _(b'FILE'),
1768 ),
1767 ),
1769 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1768 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1770 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1769 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1771 (
1770 (
1772 b'k',
1771 b'k',
1773 b'keep',
1772 b'keep',
1774 False,
1773 False,
1775 _(b"don't strip old nodes after edit is complete"),
1774 _(b"don't strip old nodes after edit is complete"),
1776 ),
1775 ),
1777 (b'', b'abort', False, _(b'abort an edit in progress')),
1776 (b'', b'abort', False, _(b'abort an edit in progress')),
1778 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1777 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1779 (
1778 (
1780 b'f',
1779 b'f',
1781 b'force',
1780 b'force',
1782 False,
1781 False,
1783 _(b'force outgoing even for unrelated repositories'),
1782 _(b'force outgoing even for unrelated repositories'),
1784 ),
1783 ),
1785 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1784 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1786 ]
1785 ]
1787 + cmdutil.formatteropts,
1786 + cmdutil.formatteropts,
1788 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1787 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1789 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1788 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1790 )
1789 )
1791 def histedit(ui, repo, *freeargs, **opts):
1790 def histedit(ui, repo, *freeargs, **opts):
1792 """interactively edit changeset history
1791 """interactively edit changeset history
1793
1792
1794 This command lets you edit a linear series of changesets (up to
1793 This command lets you edit a linear series of changesets (up to
1795 and including the working directory, which should be clean).
1794 and including the working directory, which should be clean).
1796 You can:
1795 You can:
1797
1796
1798 - `pick` to [re]order a changeset
1797 - `pick` to [re]order a changeset
1799
1798
1800 - `drop` to omit changeset
1799 - `drop` to omit changeset
1801
1800
1802 - `mess` to reword the changeset commit message
1801 - `mess` to reword the changeset commit message
1803
1802
1804 - `fold` to combine it with the preceding changeset (using the later date)
1803 - `fold` to combine it with the preceding changeset (using the later date)
1805
1804
1806 - `roll` like fold, but discarding this commit's description and date
1805 - `roll` like fold, but discarding this commit's description and date
1807
1806
1808 - `edit` to edit this changeset (preserving date)
1807 - `edit` to edit this changeset (preserving date)
1809
1808
1810 - `base` to checkout changeset and apply further changesets from there
1809 - `base` to checkout changeset and apply further changesets from there
1811
1810
1812 There are a number of ways to select the root changeset:
1811 There are a number of ways to select the root changeset:
1813
1812
1814 - Specify ANCESTOR directly
1813 - Specify ANCESTOR directly
1815
1814
1816 - Use --outgoing -- it will be the first linear changeset not
1815 - Use --outgoing -- it will be the first linear changeset not
1817 included in destination. (See :hg:`help config.paths.default-push`)
1816 included in destination. (See :hg:`help config.paths.default-push`)
1818
1817
1819 - Otherwise, the value from the "histedit.defaultrev" config option
1818 - Otherwise, the value from the "histedit.defaultrev" config option
1820 is used as a revset to select the base revision when ANCESTOR is not
1819 is used as a revset to select the base revision when ANCESTOR is not
1821 specified. The first revision returned by the revset is used. By
1820 specified. The first revision returned by the revset is used. By
1822 default, this selects the editable history that is unique to the
1821 default, this selects the editable history that is unique to the
1823 ancestry of the working directory.
1822 ancestry of the working directory.
1824
1823
1825 .. container:: verbose
1824 .. container:: verbose
1826
1825
1827 If you use --outgoing, this command will abort if there are ambiguous
1826 If you use --outgoing, this command will abort if there are ambiguous
1828 outgoing revisions. For example, if there are multiple branches
1827 outgoing revisions. For example, if there are multiple branches
1829 containing outgoing revisions.
1828 containing outgoing revisions.
1830
1829
1831 Use "min(outgoing() and ::.)" or similar revset specification
1830 Use "min(outgoing() and ::.)" or similar revset specification
1832 instead of --outgoing to specify edit target revision exactly in
1831 instead of --outgoing to specify edit target revision exactly in
1833 such ambiguous situation. See :hg:`help revsets` for detail about
1832 such ambiguous situation. See :hg:`help revsets` for detail about
1834 selecting revisions.
1833 selecting revisions.
1835
1834
1836 .. container:: verbose
1835 .. container:: verbose
1837
1836
1838 Examples:
1837 Examples:
1839
1838
1840 - A number of changes have been made.
1839 - A number of changes have been made.
1841 Revision 3 is no longer needed.
1840 Revision 3 is no longer needed.
1842
1841
1843 Start history editing from revision 3::
1842 Start history editing from revision 3::
1844
1843
1845 hg histedit -r 3
1844 hg histedit -r 3
1846
1845
1847 An editor opens, containing the list of revisions,
1846 An editor opens, containing the list of revisions,
1848 with specific actions specified::
1847 with specific actions specified::
1849
1848
1850 pick 5339bf82f0ca 3 Zworgle the foobar
1849 pick 5339bf82f0ca 3 Zworgle the foobar
1851 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1850 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1852 pick 0a9639fcda9d 5 Morgify the cromulancy
1851 pick 0a9639fcda9d 5 Morgify the cromulancy
1853
1852
1854 Additional information about the possible actions
1853 Additional information about the possible actions
1855 to take appears below the list of revisions.
1854 to take appears below the list of revisions.
1856
1855
1857 To remove revision 3 from the history,
1856 To remove revision 3 from the history,
1858 its action (at the beginning of the relevant line)
1857 its action (at the beginning of the relevant line)
1859 is changed to 'drop'::
1858 is changed to 'drop'::
1860
1859
1861 drop 5339bf82f0ca 3 Zworgle the foobar
1860 drop 5339bf82f0ca 3 Zworgle the foobar
1862 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1861 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1863 pick 0a9639fcda9d 5 Morgify the cromulancy
1862 pick 0a9639fcda9d 5 Morgify the cromulancy
1864
1863
1865 - A number of changes have been made.
1864 - A number of changes have been made.
1866 Revision 2 and 4 need to be swapped.
1865 Revision 2 and 4 need to be swapped.
1867
1866
1868 Start history editing from revision 2::
1867 Start history editing from revision 2::
1869
1868
1870 hg histedit -r 2
1869 hg histedit -r 2
1871
1870
1872 An editor opens, containing the list of revisions,
1871 An editor opens, containing the list of revisions,
1873 with specific actions specified::
1872 with specific actions specified::
1874
1873
1875 pick 252a1af424ad 2 Blorb a morgwazzle
1874 pick 252a1af424ad 2 Blorb a morgwazzle
1876 pick 5339bf82f0ca 3 Zworgle the foobar
1875 pick 5339bf82f0ca 3 Zworgle the foobar
1877 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1876 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1878
1877
1879 To swap revision 2 and 4, its lines are swapped
1878 To swap revision 2 and 4, its lines are swapped
1880 in the editor::
1879 in the editor::
1881
1880
1882 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1881 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1883 pick 5339bf82f0ca 3 Zworgle the foobar
1882 pick 5339bf82f0ca 3 Zworgle the foobar
1884 pick 252a1af424ad 2 Blorb a morgwazzle
1883 pick 252a1af424ad 2 Blorb a morgwazzle
1885
1884
1886 Returns 0 on success, 1 if user intervention is required (not only
1885 Returns 0 on success, 1 if user intervention is required (not only
1887 for intentional "edit" command, but also for resolving unexpected
1886 for intentional "edit" command, but also for resolving unexpected
1888 conflicts).
1887 conflicts).
1889 """
1888 """
1890 opts = pycompat.byteskwargs(opts)
1889 opts = pycompat.byteskwargs(opts)
1891
1890
1892 # kludge: _chistedit only works for starting an edit, not aborting
1891 # kludge: _chistedit only works for starting an edit, not aborting
1893 # or continuing, so fall back to regular _texthistedit for those
1892 # or continuing, so fall back to regular _texthistedit for those
1894 # operations.
1893 # operations.
1895 if ui.interface(b'histedit') == b'curses' and _getgoal(opts) == goalnew:
1894 if ui.interface(b'histedit') == b'curses' and _getgoal(opts) == goalnew:
1896 return _chistedit(ui, repo, freeargs, opts)
1895 return _chistedit(ui, repo, freeargs, opts)
1897 return _texthistedit(ui, repo, freeargs, opts)
1896 return _texthistedit(ui, repo, freeargs, opts)
1898
1897
1899
1898
1900 def _texthistedit(ui, repo, freeargs, opts):
1899 def _texthistedit(ui, repo, freeargs, opts):
1901 state = histeditstate(repo)
1900 state = histeditstate(repo)
1902 with repo.wlock() as wlock, repo.lock() as lock:
1901 with repo.wlock() as wlock, repo.lock() as lock:
1903 state.wlock = wlock
1902 state.wlock = wlock
1904 state.lock = lock
1903 state.lock = lock
1905 _histedit(ui, repo, state, freeargs, opts)
1904 _histedit(ui, repo, state, freeargs, opts)
1906
1905
1907
1906
1908 goalcontinue = b'continue'
1907 goalcontinue = b'continue'
1909 goalabort = b'abort'
1908 goalabort = b'abort'
1910 goaleditplan = b'edit-plan'
1909 goaleditplan = b'edit-plan'
1911 goalnew = b'new'
1910 goalnew = b'new'
1912
1911
1913
1912
1914 def _getgoal(opts):
1913 def _getgoal(opts):
1915 if opts.get(b'continue'):
1914 if opts.get(b'continue'):
1916 return goalcontinue
1915 return goalcontinue
1917 if opts.get(b'abort'):
1916 if opts.get(b'abort'):
1918 return goalabort
1917 return goalabort
1919 if opts.get(b'edit_plan'):
1918 if opts.get(b'edit_plan'):
1920 return goaleditplan
1919 return goaleditplan
1921 return goalnew
1920 return goalnew
1922
1921
1923
1922
1924 def _readfile(ui, path):
1923 def _readfile(ui, path):
1925 if path == b'-':
1924 if path == b'-':
1926 with ui.timeblockedsection(b'histedit'):
1925 with ui.timeblockedsection(b'histedit'):
1927 return ui.fin.read()
1926 return ui.fin.read()
1928 else:
1927 else:
1929 with open(path, b'rb') as f:
1928 with open(path, b'rb') as f:
1930 return f.read()
1929 return f.read()
1931
1930
1932
1931
1933 def _validateargs(ui, repo, freeargs, opts, goal, rules, revs):
1932 def _validateargs(ui, repo, freeargs, opts, goal, rules, revs):
1934 # TODO only abort if we try to histedit mq patches, not just
1933 # TODO only abort if we try to histedit mq patches, not just
1935 # blanket if mq patches are applied somewhere
1934 # blanket if mq patches are applied somewhere
1936 mq = getattr(repo, 'mq', None)
1935 mq = getattr(repo, 'mq', None)
1937 if mq and mq.applied:
1936 if mq and mq.applied:
1938 raise error.StateError(_(b'source has mq patches applied'))
1937 raise error.StateError(_(b'source has mq patches applied'))
1939
1938
1940 # basic argument incompatibility processing
1939 # basic argument incompatibility processing
1941 outg = opts.get(b'outgoing')
1940 outg = opts.get(b'outgoing')
1942 editplan = opts.get(b'edit_plan')
1941 editplan = opts.get(b'edit_plan')
1943 abort = opts.get(b'abort')
1942 abort = opts.get(b'abort')
1944 force = opts.get(b'force')
1943 force = opts.get(b'force')
1945 if force and not outg:
1944 if force and not outg:
1946 raise error.InputError(_(b'--force only allowed with --outgoing'))
1945 raise error.InputError(_(b'--force only allowed with --outgoing'))
1947 if goal == b'continue':
1946 if goal == b'continue':
1948 if any((outg, abort, revs, freeargs, rules, editplan)):
1947 if any((outg, abort, revs, freeargs, rules, editplan)):
1949 raise error.InputError(_(b'no arguments allowed with --continue'))
1948 raise error.InputError(_(b'no arguments allowed with --continue'))
1950 elif goal == b'abort':
1949 elif goal == b'abort':
1951 if any((outg, revs, freeargs, rules, editplan)):
1950 if any((outg, revs, freeargs, rules, editplan)):
1952 raise error.InputError(_(b'no arguments allowed with --abort'))
1951 raise error.InputError(_(b'no arguments allowed with --abort'))
1953 elif goal == b'edit-plan':
1952 elif goal == b'edit-plan':
1954 if any((outg, revs, freeargs)):
1953 if any((outg, revs, freeargs)):
1955 raise error.InputError(
1954 raise error.InputError(
1956 _(b'only --commands argument allowed with --edit-plan')
1955 _(b'only --commands argument allowed with --edit-plan')
1957 )
1956 )
1958 else:
1957 else:
1959 if outg:
1958 if outg:
1960 if revs:
1959 if revs:
1961 raise error.InputError(
1960 raise error.InputError(
1962 _(b'no revisions allowed with --outgoing')
1961 _(b'no revisions allowed with --outgoing')
1963 )
1962 )
1964 if len(freeargs) > 1:
1963 if len(freeargs) > 1:
1965 raise error.InputError(
1964 raise error.InputError(
1966 _(b'only one repo argument allowed with --outgoing')
1965 _(b'only one repo argument allowed with --outgoing')
1967 )
1966 )
1968 else:
1967 else:
1969 revs.extend(freeargs)
1968 revs.extend(freeargs)
1970 if len(revs) == 0:
1969 if len(revs) == 0:
1971 defaultrev = destutil.desthistedit(ui, repo)
1970 defaultrev = destutil.desthistedit(ui, repo)
1972 if defaultrev is not None:
1971 if defaultrev is not None:
1973 revs.append(defaultrev)
1972 revs.append(defaultrev)
1974
1973
1975 if len(revs) != 1:
1974 if len(revs) != 1:
1976 raise error.InputError(
1975 raise error.InputError(
1977 _(b'histedit requires exactly one ancestor revision')
1976 _(b'histedit requires exactly one ancestor revision')
1978 )
1977 )
1979
1978
1980
1979
1981 def _histedit(ui, repo, state, freeargs, opts):
1980 def _histedit(ui, repo, state, freeargs, opts):
1982 fm = ui.formatter(b'histedit', opts)
1981 fm = ui.formatter(b'histedit', opts)
1983 fm.startitem()
1982 fm.startitem()
1984 goal = _getgoal(opts)
1983 goal = _getgoal(opts)
1985 revs = opts.get(b'rev', [])
1984 revs = opts.get(b'rev', [])
1986 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
1985 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
1987 rules = opts.get(b'commands', b'')
1986 rules = opts.get(b'commands', b'')
1988 state.keep = opts.get(b'keep', False)
1987 state.keep = opts.get(b'keep', False)
1989
1988
1990 _validateargs(ui, repo, freeargs, opts, goal, rules, revs)
1989 _validateargs(ui, repo, freeargs, opts, goal, rules, revs)
1991
1990
1992 hastags = False
1991 hastags = False
1993 if revs:
1992 if revs:
1994 revs = logcmdutil.revrange(repo, revs)
1993 revs = logcmdutil.revrange(repo, revs)
1995 ctxs = [repo[rev] for rev in revs]
1994 ctxs = [repo[rev] for rev in revs]
1996 for ctx in ctxs:
1995 for ctx in ctxs:
1997 tags = [tag for tag in ctx.tags() if tag != b'tip']
1996 tags = [tag for tag in ctx.tags() if tag != b'tip']
1998 if not hastags:
1997 if not hastags:
1999 hastags = len(tags)
1998 hastags = len(tags)
2000 if hastags:
1999 if hastags:
2001 if ui.promptchoice(
2000 if ui.promptchoice(
2002 _(
2001 _(
2003 b'warning: tags associated with the given'
2002 b'warning: tags associated with the given'
2004 b' changeset will be lost after histedit.\n'
2003 b' changeset will be lost after histedit.\n'
2005 b'do you want to continue (yN)? $$ &Yes $$ &No'
2004 b'do you want to continue (yN)? $$ &Yes $$ &No'
2006 ),
2005 ),
2007 default=1,
2006 default=1,
2008 ):
2007 ):
2009 raise error.CanceledError(_(b'histedit cancelled\n'))
2008 raise error.CanceledError(_(b'histedit cancelled\n'))
2010 # rebuild state
2009 # rebuild state
2011 if goal == goalcontinue:
2010 if goal == goalcontinue:
2012 state.read()
2011 state.read()
2013 state = bootstrapcontinue(ui, state, opts)
2012 state = bootstrapcontinue(ui, state, opts)
2014 elif goal == goaleditplan:
2013 elif goal == goaleditplan:
2015 _edithisteditplan(ui, repo, state, rules)
2014 _edithisteditplan(ui, repo, state, rules)
2016 return
2015 return
2017 elif goal == goalabort:
2016 elif goal == goalabort:
2018 _aborthistedit(ui, repo, state, nobackup=nobackup)
2017 _aborthistedit(ui, repo, state, nobackup=nobackup)
2019 return
2018 return
2020 else:
2019 else:
2021 # goal == goalnew
2020 # goal == goalnew
2022 _newhistedit(ui, repo, state, revs, freeargs, opts)
2021 _newhistedit(ui, repo, state, revs, freeargs, opts)
2023
2022
2024 _continuehistedit(ui, repo, state)
2023 _continuehistedit(ui, repo, state)
2025 _finishhistedit(ui, repo, state, fm)
2024 _finishhistedit(ui, repo, state, fm)
2026 fm.end()
2025 fm.end()
2027
2026
2028
2027
2029 def _continuehistedit(ui, repo, state):
2028 def _continuehistedit(ui, repo, state):
2030 """This function runs after either:
2029 """This function runs after either:
2031 - bootstrapcontinue (if the goal is 'continue')
2030 - bootstrapcontinue (if the goal is 'continue')
2032 - _newhistedit (if the goal is 'new')
2031 - _newhistedit (if the goal is 'new')
2033 """
2032 """
2034 # preprocess rules so that we can hide inner folds from the user
2033 # preprocess rules so that we can hide inner folds from the user
2035 # and only show one editor
2034 # and only show one editor
2036 actions = state.actions[:]
2035 actions = state.actions[:]
2037 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2036 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2038 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2037 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2039 state.actions[idx].__class__ = _multifold
2038 state.actions[idx].__class__ = _multifold
2040
2039
2041 # Force an initial state file write, so the user can run --abort/continue
2040 # Force an initial state file write, so the user can run --abort/continue
2042 # even if there's an exception before the first transaction serialize.
2041 # even if there's an exception before the first transaction serialize.
2043 state.write()
2042 state.write()
2044
2043
2045 tr = None
2044 tr = None
2046 # Don't use singletransaction by default since it rolls the entire
2045 # Don't use singletransaction by default since it rolls the entire
2047 # transaction back if an unexpected exception happens (like a
2046 # transaction back if an unexpected exception happens (like a
2048 # pretxncommit hook throws, or the user aborts the commit msg editor).
2047 # pretxncommit hook throws, or the user aborts the commit msg editor).
2049 if ui.configbool(b"histedit", b"singletransaction"):
2048 if ui.configbool(b"histedit", b"singletransaction"):
2050 # Don't use a 'with' for the transaction, since actions may close
2049 # Don't use a 'with' for the transaction, since actions may close
2051 # and reopen a transaction. For example, if the action executes an
2050 # and reopen a transaction. For example, if the action executes an
2052 # external process it may choose to commit the transaction first.
2051 # external process it may choose to commit the transaction first.
2053 tr = repo.transaction(b'histedit')
2052 tr = repo.transaction(b'histedit')
2054 progress = ui.makeprogress(
2053 progress = ui.makeprogress(
2055 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2054 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2056 )
2055 )
2057 with progress, util.acceptintervention(tr):
2056 with progress, util.acceptintervention(tr):
2058 while state.actions:
2057 while state.actions:
2059 state.write(tr=tr)
2058 state.write(tr=tr)
2060 actobj = state.actions[0]
2059 actobj = state.actions[0]
2061 progress.increment(item=actobj.torule())
2060 progress.increment(item=actobj.torule())
2062 ui.debug(
2061 ui.debug(
2063 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2062 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2064 )
2063 )
2065 parentctx, replacement_ = actobj.run()
2064 parentctx, replacement_ = actobj.run()
2066 state.parentctxnode = parentctx.node()
2065 state.parentctxnode = parentctx.node()
2067 state.replacements.extend(replacement_)
2066 state.replacements.extend(replacement_)
2068 state.actions.pop(0)
2067 state.actions.pop(0)
2069
2068
2070 state.write()
2069 state.write()
2071
2070
2072
2071
2073 def _finishhistedit(ui, repo, state, fm):
2072 def _finishhistedit(ui, repo, state, fm):
2074 """This action runs when histedit is finishing its session"""
2073 """This action runs when histedit is finishing its session"""
2075 mergemod.update(repo[state.parentctxnode])
2074 mergemod.update(repo[state.parentctxnode])
2076
2075
2077 mapping, tmpnodes, created, ntm = processreplacement(state)
2076 mapping, tmpnodes, created, ntm = processreplacement(state)
2078 if mapping:
2077 if mapping:
2079 for prec, succs in pycompat.iteritems(mapping):
2078 for prec, succs in pycompat.iteritems(mapping):
2080 if not succs:
2079 if not succs:
2081 ui.debug(b'histedit: %s is dropped\n' % short(prec))
2080 ui.debug(b'histedit: %s is dropped\n' % short(prec))
2082 else:
2081 else:
2083 ui.debug(
2082 ui.debug(
2084 b'histedit: %s is replaced by %s\n'
2083 b'histedit: %s is replaced by %s\n'
2085 % (short(prec), short(succs[0]))
2084 % (short(prec), short(succs[0]))
2086 )
2085 )
2087 if len(succs) > 1:
2086 if len(succs) > 1:
2088 m = b'histedit: %s'
2087 m = b'histedit: %s'
2089 for n in succs[1:]:
2088 for n in succs[1:]:
2090 ui.debug(m % short(n))
2089 ui.debug(m % short(n))
2091
2090
2092 if not state.keep:
2091 if not state.keep:
2093 if mapping:
2092 if mapping:
2094 movetopmostbookmarks(repo, state.topmost, ntm)
2093 movetopmostbookmarks(repo, state.topmost, ntm)
2095 # TODO update mq state
2094 # TODO update mq state
2096 else:
2095 else:
2097 mapping = {}
2096 mapping = {}
2098
2097
2099 for n in tmpnodes:
2098 for n in tmpnodes:
2100 if n in repo:
2099 if n in repo:
2101 mapping[n] = ()
2100 mapping[n] = ()
2102
2101
2103 # remove entries about unknown nodes
2102 # remove entries about unknown nodes
2104 has_node = repo.unfiltered().changelog.index.has_node
2103 has_node = repo.unfiltered().changelog.index.has_node
2105 mapping = {
2104 mapping = {
2106 k: v
2105 k: v
2107 for k, v in mapping.items()
2106 for k, v in mapping.items()
2108 if has_node(k) and all(has_node(n) for n in v)
2107 if has_node(k) and all(has_node(n) for n in v)
2109 }
2108 }
2110 scmutil.cleanupnodes(repo, mapping, b'histedit')
2109 scmutil.cleanupnodes(repo, mapping, b'histedit')
2111 hf = fm.hexfunc
2110 hf = fm.hexfunc
2112 fl = fm.formatlist
2111 fl = fm.formatlist
2113 fd = fm.formatdict
2112 fd = fm.formatdict
2114 nodechanges = fd(
2113 nodechanges = fd(
2115 {
2114 {
2116 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2115 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2117 for oldn, newn in pycompat.iteritems(mapping)
2116 for oldn, newn in pycompat.iteritems(mapping)
2118 },
2117 },
2119 key=b"oldnode",
2118 key=b"oldnode",
2120 value=b"newnodes",
2119 value=b"newnodes",
2121 )
2120 )
2122 fm.data(nodechanges=nodechanges)
2121 fm.data(nodechanges=nodechanges)
2123
2122
2124 state.clear()
2123 state.clear()
2125 if os.path.exists(repo.sjoin(b'undo')):
2124 if os.path.exists(repo.sjoin(b'undo')):
2126 os.unlink(repo.sjoin(b'undo'))
2125 os.unlink(repo.sjoin(b'undo'))
2127 if repo.vfs.exists(b'histedit-last-edit.txt'):
2126 if repo.vfs.exists(b'histedit-last-edit.txt'):
2128 repo.vfs.unlink(b'histedit-last-edit.txt')
2127 repo.vfs.unlink(b'histedit-last-edit.txt')
2129
2128
2130
2129
2131 def _aborthistedit(ui, repo, state, nobackup=False):
2130 def _aborthistedit(ui, repo, state, nobackup=False):
2132 try:
2131 try:
2133 state.read()
2132 state.read()
2134 __, leafs, tmpnodes, __ = processreplacement(state)
2133 __, leafs, tmpnodes, __ = processreplacement(state)
2135 ui.debug(b'restore wc to old parent %s\n' % short(state.topmost))
2134 ui.debug(b'restore wc to old parent %s\n' % short(state.topmost))
2136
2135
2137 # Recover our old commits if necessary
2136 # Recover our old commits if necessary
2138 if not state.topmost in repo and state.backupfile:
2137 if not state.topmost in repo and state.backupfile:
2139 backupfile = repo.vfs.join(state.backupfile)
2138 backupfile = repo.vfs.join(state.backupfile)
2140 f = hg.openpath(ui, backupfile)
2139 f = hg.openpath(ui, backupfile)
2141 gen = exchange.readbundle(ui, f, backupfile)
2140 gen = exchange.readbundle(ui, f, backupfile)
2142 with repo.transaction(b'histedit.abort') as tr:
2141 with repo.transaction(b'histedit.abort') as tr:
2143 bundle2.applybundle(
2142 bundle2.applybundle(
2144 repo,
2143 repo,
2145 gen,
2144 gen,
2146 tr,
2145 tr,
2147 source=b'histedit',
2146 source=b'histedit',
2148 url=b'bundle:' + backupfile,
2147 url=b'bundle:' + backupfile,
2149 )
2148 )
2150
2149
2151 os.remove(backupfile)
2150 os.remove(backupfile)
2152
2151
2153 # check whether we should update away
2152 # check whether we should update away
2154 if repo.unfiltered().revs(
2153 if repo.unfiltered().revs(
2155 b'parents() and (%n or %ln::)',
2154 b'parents() and (%n or %ln::)',
2156 state.parentctxnode,
2155 state.parentctxnode,
2157 leafs | tmpnodes,
2156 leafs | tmpnodes,
2158 ):
2157 ):
2159 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2158 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2160 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2159 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2161 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2160 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2162 except Exception:
2161 except Exception:
2163 if state.inprogress():
2162 if state.inprogress():
2164 ui.warn(
2163 ui.warn(
2165 _(
2164 _(
2166 b'warning: encountered an exception during histedit '
2165 b'warning: encountered an exception during histedit '
2167 b'--abort; the repository may not have been completely '
2166 b'--abort; the repository may not have been completely '
2168 b'cleaned up\n'
2167 b'cleaned up\n'
2169 )
2168 )
2170 )
2169 )
2171 raise
2170 raise
2172 finally:
2171 finally:
2173 state.clear()
2172 state.clear()
2174
2173
2175
2174
2176 def hgaborthistedit(ui, repo):
2175 def hgaborthistedit(ui, repo):
2177 state = histeditstate(repo)
2176 state = histeditstate(repo)
2178 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2177 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2179 with repo.wlock() as wlock, repo.lock() as lock:
2178 with repo.wlock() as wlock, repo.lock() as lock:
2180 state.wlock = wlock
2179 state.wlock = wlock
2181 state.lock = lock
2180 state.lock = lock
2182 _aborthistedit(ui, repo, state, nobackup=nobackup)
2181 _aborthistedit(ui, repo, state, nobackup=nobackup)
2183
2182
2184
2183
2185 def _edithisteditplan(ui, repo, state, rules):
2184 def _edithisteditplan(ui, repo, state, rules):
2186 state.read()
2185 state.read()
2187 if not rules:
2186 if not rules:
2188 comment = geteditcomment(
2187 comment = geteditcomment(
2189 ui, short(state.parentctxnode), short(state.topmost)
2188 ui, short(state.parentctxnode), short(state.topmost)
2190 )
2189 )
2191 rules = ruleeditor(repo, ui, state.actions, comment)
2190 rules = ruleeditor(repo, ui, state.actions, comment)
2192 else:
2191 else:
2193 rules = _readfile(ui, rules)
2192 rules = _readfile(ui, rules)
2194 actions = parserules(rules, state)
2193 actions = parserules(rules, state)
2195 ctxs = [repo[act.node] for act in state.actions if act.node]
2194 ctxs = [repo[act.node] for act in state.actions if act.node]
2196 warnverifyactions(ui, repo, actions, state, ctxs)
2195 warnverifyactions(ui, repo, actions, state, ctxs)
2197 state.actions = actions
2196 state.actions = actions
2198 state.write()
2197 state.write()
2199
2198
2200
2199
2201 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2200 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2202 outg = opts.get(b'outgoing')
2201 outg = opts.get(b'outgoing')
2203 rules = opts.get(b'commands', b'')
2202 rules = opts.get(b'commands', b'')
2204 force = opts.get(b'force')
2203 force = opts.get(b'force')
2205
2204
2206 cmdutil.checkunfinished(repo)
2205 cmdutil.checkunfinished(repo)
2207 cmdutil.bailifchanged(repo)
2206 cmdutil.bailifchanged(repo)
2208
2207
2209 topmost = repo.dirstate.p1()
2208 topmost = repo.dirstate.p1()
2210 if outg:
2209 if outg:
2211 if freeargs:
2210 if freeargs:
2212 remote = freeargs[0]
2211 remote = freeargs[0]
2213 else:
2212 else:
2214 remote = None
2213 remote = None
2215 root = findoutgoing(ui, repo, remote, force, opts)
2214 root = findoutgoing(ui, repo, remote, force, opts)
2216 else:
2215 else:
2217 rr = list(repo.set(b'roots(%ld)', logcmdutil.revrange(repo, revs)))
2216 rr = list(repo.set(b'roots(%ld)', logcmdutil.revrange(repo, revs)))
2218 if len(rr) != 1:
2217 if len(rr) != 1:
2219 raise error.InputError(
2218 raise error.InputError(
2220 _(
2219 _(
2221 b'The specified revisions must have '
2220 b'The specified revisions must have '
2222 b'exactly one common root'
2221 b'exactly one common root'
2223 )
2222 )
2224 )
2223 )
2225 root = rr[0].node()
2224 root = rr[0].node()
2226
2225
2227 revs = between(repo, root, topmost, state.keep)
2226 revs = between(repo, root, topmost, state.keep)
2228 if not revs:
2227 if not revs:
2229 raise error.InputError(
2228 raise error.InputError(
2230 _(b'%s is not an ancestor of working directory') % short(root)
2229 _(b'%s is not an ancestor of working directory') % short(root)
2231 )
2230 )
2232
2231
2233 ctxs = [repo[r] for r in revs]
2232 ctxs = [repo[r] for r in revs]
2234
2233
2235 wctx = repo[None]
2234 wctx = repo[None]
2236 # Please don't ask me why `ancestors` is this value. I figured it
2235 # Please don't ask me why `ancestors` is this value. I figured it
2237 # out with print-debugging, not by actually understanding what the
2236 # out with print-debugging, not by actually understanding what the
2238 # merge code is doing. :(
2237 # merge code is doing. :(
2239 ancs = [repo[b'.']]
2238 ancs = [repo[b'.']]
2240 # Sniff-test to make sure we won't collide with untracked files in
2239 # Sniff-test to make sure we won't collide with untracked files in
2241 # the working directory. If we don't do this, we can get a
2240 # the working directory. If we don't do this, we can get a
2242 # collision after we've started histedit and backing out gets ugly
2241 # collision after we've started histedit and backing out gets ugly
2243 # for everyone, especially the user.
2242 # for everyone, especially the user.
2244 for c in [ctxs[0].p1()] + ctxs:
2243 for c in [ctxs[0].p1()] + ctxs:
2245 try:
2244 try:
2246 mergemod.calculateupdates(
2245 mergemod.calculateupdates(
2247 repo,
2246 repo,
2248 wctx,
2247 wctx,
2249 c,
2248 c,
2250 ancs,
2249 ancs,
2251 # These parameters were determined by print-debugging
2250 # These parameters were determined by print-debugging
2252 # what happens later on inside histedit.
2251 # what happens later on inside histedit.
2253 branchmerge=False,
2252 branchmerge=False,
2254 force=False,
2253 force=False,
2255 acceptremote=False,
2254 acceptremote=False,
2256 followcopies=False,
2255 followcopies=False,
2257 )
2256 )
2258 except error.Abort:
2257 except error.Abort:
2259 raise error.StateError(
2258 raise error.StateError(
2260 _(
2259 _(
2261 b"untracked files in working directory conflict with files in %s"
2260 b"untracked files in working directory conflict with files in %s"
2262 )
2261 )
2263 % c
2262 % c
2264 )
2263 )
2265
2264
2266 if not rules:
2265 if not rules:
2267 comment = geteditcomment(ui, short(root), short(topmost))
2266 comment = geteditcomment(ui, short(root), short(topmost))
2268 actions = [pick(state, r) for r in revs]
2267 actions = [pick(state, r) for r in revs]
2269 rules = ruleeditor(repo, ui, actions, comment)
2268 rules = ruleeditor(repo, ui, actions, comment)
2270 else:
2269 else:
2271 rules = _readfile(ui, rules)
2270 rules = _readfile(ui, rules)
2272 actions = parserules(rules, state)
2271 actions = parserules(rules, state)
2273 warnverifyactions(ui, repo, actions, state, ctxs)
2272 warnverifyactions(ui, repo, actions, state, ctxs)
2274
2273
2275 parentctxnode = repo[root].p1().node()
2274 parentctxnode = repo[root].p1().node()
2276
2275
2277 state.parentctxnode = parentctxnode
2276 state.parentctxnode = parentctxnode
2278 state.actions = actions
2277 state.actions = actions
2279 state.topmost = topmost
2278 state.topmost = topmost
2280 state.replacements = []
2279 state.replacements = []
2281
2280
2282 ui.log(
2281 ui.log(
2283 b"histedit",
2282 b"histedit",
2284 b"%d actions to histedit\n",
2283 b"%d actions to histedit\n",
2285 len(actions),
2284 len(actions),
2286 histedit_num_actions=len(actions),
2285 histedit_num_actions=len(actions),
2287 )
2286 )
2288
2287
2289 # Create a backup so we can always abort completely.
2288 # Create a backup so we can always abort completely.
2290 backupfile = None
2289 backupfile = None
2291 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2290 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2292 backupfile = repair.backupbundle(
2291 backupfile = repair.backupbundle(
2293 repo, [parentctxnode], [topmost], root, b'histedit'
2292 repo, [parentctxnode], [topmost], root, b'histedit'
2294 )
2293 )
2295 state.backupfile = backupfile
2294 state.backupfile = backupfile
2296
2295
2297
2296
2298 def _getsummary(ctx):
2297 def _getsummary(ctx):
2299 # a common pattern is to extract the summary but default to the empty
2298 # a common pattern is to extract the summary but default to the empty
2300 # string
2299 # string
2301 summary = ctx.description() or b''
2300 summary = ctx.description() or b''
2302 if summary:
2301 if summary:
2303 summary = summary.splitlines()[0]
2302 summary = summary.splitlines()[0]
2304 return summary
2303 return summary
2305
2304
2306
2305
2307 def bootstrapcontinue(ui, state, opts):
2306 def bootstrapcontinue(ui, state, opts):
2308 repo = state.repo
2307 repo = state.repo
2309
2308
2310 ms = mergestatemod.mergestate.read(repo)
2309 ms = mergestatemod.mergestate.read(repo)
2311 mergeutil.checkunresolved(ms)
2310 mergeutil.checkunresolved(ms)
2312
2311
2313 if state.actions:
2312 if state.actions:
2314 actobj = state.actions.pop(0)
2313 actobj = state.actions.pop(0)
2315
2314
2316 if _isdirtywc(repo):
2315 if _isdirtywc(repo):
2317 actobj.continuedirty()
2316 actobj.continuedirty()
2318 if _isdirtywc(repo):
2317 if _isdirtywc(repo):
2319 abortdirty()
2318 abortdirty()
2320
2319
2321 parentctx, replacements = actobj.continueclean()
2320 parentctx, replacements = actobj.continueclean()
2322
2321
2323 state.parentctxnode = parentctx.node()
2322 state.parentctxnode = parentctx.node()
2324 state.replacements.extend(replacements)
2323 state.replacements.extend(replacements)
2325
2324
2326 return state
2325 return state
2327
2326
2328
2327
2329 def between(repo, old, new, keep):
2328 def between(repo, old, new, keep):
2330 """select and validate the set of revision to edit
2329 """select and validate the set of revision to edit
2331
2330
2332 When keep is false, the specified set can't have children."""
2331 When keep is false, the specified set can't have children."""
2333 revs = repo.revs(b'%n::%n', old, new)
2332 revs = repo.revs(b'%n::%n', old, new)
2334 if revs and not keep:
2333 if revs and not keep:
2335 rewriteutil.precheck(repo, revs, b'edit')
2334 rewriteutil.precheck(repo, revs, b'edit')
2336 if repo.revs(b'(%ld) and merge()', revs):
2335 if repo.revs(b'(%ld) and merge()', revs):
2337 raise error.StateError(
2336 raise error.StateError(
2338 _(b'cannot edit history that contains merges')
2337 _(b'cannot edit history that contains merges')
2339 )
2338 )
2340 return pycompat.maplist(repo.changelog.node, revs)
2339 return pycompat.maplist(repo.changelog.node, revs)
2341
2340
2342
2341
2343 def ruleeditor(repo, ui, actions, editcomment=b""):
2342 def ruleeditor(repo, ui, actions, editcomment=b""):
2344 """open an editor to edit rules
2343 """open an editor to edit rules
2345
2344
2346 rules are in the format [ [act, ctx], ...] like in state.rules
2345 rules are in the format [ [act, ctx], ...] like in state.rules
2347 """
2346 """
2348 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2347 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2349 newact = util.sortdict()
2348 newact = util.sortdict()
2350 for act in actions:
2349 for act in actions:
2351 ctx = repo[act.node]
2350 ctx = repo[act.node]
2352 summary = _getsummary(ctx)
2351 summary = _getsummary(ctx)
2353 fword = summary.split(b' ', 1)[0].lower()
2352 fword = summary.split(b' ', 1)[0].lower()
2354 added = False
2353 added = False
2355
2354
2356 # if it doesn't end with the special character '!' just skip this
2355 # if it doesn't end with the special character '!' just skip this
2357 if fword.endswith(b'!'):
2356 if fword.endswith(b'!'):
2358 fword = fword[:-1]
2357 fword = fword[:-1]
2359 if fword in primaryactions | secondaryactions | tertiaryactions:
2358 if fword in primaryactions | secondaryactions | tertiaryactions:
2360 act.verb = fword
2359 act.verb = fword
2361 # get the target summary
2360 # get the target summary
2362 tsum = summary[len(fword) + 1 :].lstrip()
2361 tsum = summary[len(fword) + 1 :].lstrip()
2363 # safe but slow: reverse iterate over the actions so we
2362 # safe but slow: reverse iterate over the actions so we
2364 # don't clash on two commits having the same summary
2363 # don't clash on two commits having the same summary
2365 for na, l in reversed(list(pycompat.iteritems(newact))):
2364 for na, l in reversed(list(pycompat.iteritems(newact))):
2366 actx = repo[na.node]
2365 actx = repo[na.node]
2367 asum = _getsummary(actx)
2366 asum = _getsummary(actx)
2368 if asum == tsum:
2367 if asum == tsum:
2369 added = True
2368 added = True
2370 l.append(act)
2369 l.append(act)
2371 break
2370 break
2372
2371
2373 if not added:
2372 if not added:
2374 newact[act] = []
2373 newact[act] = []
2375
2374
2376 # copy over and flatten the new list
2375 # copy over and flatten the new list
2377 actions = []
2376 actions = []
2378 for na, l in pycompat.iteritems(newact):
2377 for na, l in pycompat.iteritems(newact):
2379 actions.append(na)
2378 actions.append(na)
2380 actions += l
2379 actions += l
2381
2380
2382 rules = b'\n'.join([act.torule() for act in actions])
2381 rules = b'\n'.join([act.torule() for act in actions])
2383 rules += b'\n\n'
2382 rules += b'\n\n'
2384 rules += editcomment
2383 rules += editcomment
2385 rules = ui.edit(
2384 rules = ui.edit(
2386 rules,
2385 rules,
2387 ui.username(),
2386 ui.username(),
2388 {b'prefix': b'histedit'},
2387 {b'prefix': b'histedit'},
2389 repopath=repo.path,
2388 repopath=repo.path,
2390 action=b'histedit',
2389 action=b'histedit',
2391 )
2390 )
2392
2391
2393 # Save edit rules in .hg/histedit-last-edit.txt in case
2392 # Save edit rules in .hg/histedit-last-edit.txt in case
2394 # the user needs to ask for help after something
2393 # the user needs to ask for help after something
2395 # surprising happens.
2394 # surprising happens.
2396 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2395 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2397 f.write(rules)
2396 f.write(rules)
2398
2397
2399 return rules
2398 return rules
2400
2399
2401
2400
2402 def parserules(rules, state):
2401 def parserules(rules, state):
2403 """Read the histedit rules string and return list of action objects"""
2402 """Read the histedit rules string and return list of action objects"""
2404 rules = [
2403 rules = [
2405 l
2404 l
2406 for l in (r.strip() for r in rules.splitlines())
2405 for l in (r.strip() for r in rules.splitlines())
2407 if l and not l.startswith(b'#')
2406 if l and not l.startswith(b'#')
2408 ]
2407 ]
2409 actions = []
2408 actions = []
2410 for r in rules:
2409 for r in rules:
2411 if b' ' not in r:
2410 if b' ' not in r:
2412 raise error.ParseError(_(b'malformed line "%s"') % r)
2411 raise error.ParseError(_(b'malformed line "%s"') % r)
2413 verb, rest = r.split(b' ', 1)
2412 verb, rest = r.split(b' ', 1)
2414
2413
2415 if verb not in actiontable:
2414 if verb not in actiontable:
2416 raise error.ParseError(_(b'unknown action "%s"') % verb)
2415 raise error.ParseError(_(b'unknown action "%s"') % verb)
2417
2416
2418 action = actiontable[verb].fromrule(state, rest)
2417 action = actiontable[verb].fromrule(state, rest)
2419 actions.append(action)
2418 actions.append(action)
2420 return actions
2419 return actions
2421
2420
2422
2421
2423 def warnverifyactions(ui, repo, actions, state, ctxs):
2422 def warnverifyactions(ui, repo, actions, state, ctxs):
2424 try:
2423 try:
2425 verifyactions(actions, state, ctxs)
2424 verifyactions(actions, state, ctxs)
2426 except error.ParseError:
2425 except error.ParseError:
2427 if repo.vfs.exists(b'histedit-last-edit.txt'):
2426 if repo.vfs.exists(b'histedit-last-edit.txt'):
2428 ui.warn(
2427 ui.warn(
2429 _(
2428 _(
2430 b'warning: histedit rules saved '
2429 b'warning: histedit rules saved '
2431 b'to: .hg/histedit-last-edit.txt\n'
2430 b'to: .hg/histedit-last-edit.txt\n'
2432 )
2431 )
2433 )
2432 )
2434 raise
2433 raise
2435
2434
2436
2435
2437 def verifyactions(actions, state, ctxs):
2436 def verifyactions(actions, state, ctxs):
2438 """Verify that there exists exactly one action per given changeset and
2437 """Verify that there exists exactly one action per given changeset and
2439 other constraints.
2438 other constraints.
2440
2439
2441 Will abort if there are to many or too few rules, a malformed rule,
2440 Will abort if there are to many or too few rules, a malformed rule,
2442 or a rule on a changeset outside of the user-given range.
2441 or a rule on a changeset outside of the user-given range.
2443 """
2442 """
2444 expected = {c.node() for c in ctxs}
2443 expected = {c.node() for c in ctxs}
2445 seen = set()
2444 seen = set()
2446 prev = None
2445 prev = None
2447
2446
2448 if actions and actions[0].verb in [b'roll', b'fold']:
2447 if actions and actions[0].verb in [b'roll', b'fold']:
2449 raise error.ParseError(
2448 raise error.ParseError(
2450 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2449 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2451 )
2450 )
2452
2451
2453 for action in actions:
2452 for action in actions:
2454 action.verify(prev, expected, seen)
2453 action.verify(prev, expected, seen)
2455 prev = action
2454 prev = action
2456 if action.node is not None:
2455 if action.node is not None:
2457 seen.add(action.node)
2456 seen.add(action.node)
2458 missing = sorted(expected - seen) # sort to stabilize output
2457 missing = sorted(expected - seen) # sort to stabilize output
2459
2458
2460 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2459 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2461 if len(actions) == 0:
2460 if len(actions) == 0:
2462 raise error.ParseError(
2461 raise error.ParseError(
2463 _(b'no rules provided'),
2462 _(b'no rules provided'),
2464 hint=_(b'use strip extension to remove commits'),
2463 hint=_(b'use strip extension to remove commits'),
2465 )
2464 )
2466
2465
2467 drops = [drop(state, n) for n in missing]
2466 drops = [drop(state, n) for n in missing]
2468 # put the in the beginning so they execute immediately and
2467 # put the in the beginning so they execute immediately and
2469 # don't show in the edit-plan in the future
2468 # don't show in the edit-plan in the future
2470 actions[:0] = drops
2469 actions[:0] = drops
2471 elif missing:
2470 elif missing:
2472 raise error.ParseError(
2471 raise error.ParseError(
2473 _(b'missing rules for changeset %s') % short(missing[0]),
2472 _(b'missing rules for changeset %s') % short(missing[0]),
2474 hint=_(
2473 hint=_(
2475 b'use "drop %s" to discard, see also: '
2474 b'use "drop %s" to discard, see also: '
2476 b"'hg help -e histedit.config'"
2475 b"'hg help -e histedit.config'"
2477 )
2476 )
2478 % short(missing[0]),
2477 % short(missing[0]),
2479 )
2478 )
2480
2479
2481
2480
2482 def adjustreplacementsfrommarkers(repo, oldreplacements):
2481 def adjustreplacementsfrommarkers(repo, oldreplacements):
2483 """Adjust replacements from obsolescence markers
2482 """Adjust replacements from obsolescence markers
2484
2483
2485 Replacements structure is originally generated based on
2484 Replacements structure is originally generated based on
2486 histedit's state and does not account for changes that are
2485 histedit's state and does not account for changes that are
2487 not recorded there. This function fixes that by adding
2486 not recorded there. This function fixes that by adding
2488 data read from obsolescence markers"""
2487 data read from obsolescence markers"""
2489 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2488 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2490 return oldreplacements
2489 return oldreplacements
2491
2490
2492 unfi = repo.unfiltered()
2491 unfi = repo.unfiltered()
2493 get_rev = unfi.changelog.index.get_rev
2492 get_rev = unfi.changelog.index.get_rev
2494 obsstore = repo.obsstore
2493 obsstore = repo.obsstore
2495 newreplacements = list(oldreplacements)
2494 newreplacements = list(oldreplacements)
2496 oldsuccs = [r[1] for r in oldreplacements]
2495 oldsuccs = [r[1] for r in oldreplacements]
2497 # successors that have already been added to succstocheck once
2496 # successors that have already been added to succstocheck once
2498 seensuccs = set().union(
2497 seensuccs = set().union(
2499 *oldsuccs
2498 *oldsuccs
2500 ) # create a set from an iterable of tuples
2499 ) # create a set from an iterable of tuples
2501 succstocheck = list(seensuccs)
2500 succstocheck = list(seensuccs)
2502 while succstocheck:
2501 while succstocheck:
2503 n = succstocheck.pop()
2502 n = succstocheck.pop()
2504 missing = get_rev(n) is None
2503 missing = get_rev(n) is None
2505 markers = obsstore.successors.get(n, ())
2504 markers = obsstore.successors.get(n, ())
2506 if missing and not markers:
2505 if missing and not markers:
2507 # dead end, mark it as such
2506 # dead end, mark it as such
2508 newreplacements.append((n, ()))
2507 newreplacements.append((n, ()))
2509 for marker in markers:
2508 for marker in markers:
2510 nsuccs = marker[1]
2509 nsuccs = marker[1]
2511 newreplacements.append((n, nsuccs))
2510 newreplacements.append((n, nsuccs))
2512 for nsucc in nsuccs:
2511 for nsucc in nsuccs:
2513 if nsucc not in seensuccs:
2512 if nsucc not in seensuccs:
2514 seensuccs.add(nsucc)
2513 seensuccs.add(nsucc)
2515 succstocheck.append(nsucc)
2514 succstocheck.append(nsucc)
2516
2515
2517 return newreplacements
2516 return newreplacements
2518
2517
2519
2518
2520 def processreplacement(state):
2519 def processreplacement(state):
2521 """process the list of replacements to return
2520 """process the list of replacements to return
2522
2521
2523 1) the final mapping between original and created nodes
2522 1) the final mapping between original and created nodes
2524 2) the list of temporary node created by histedit
2523 2) the list of temporary node created by histedit
2525 3) the list of new commit created by histedit"""
2524 3) the list of new commit created by histedit"""
2526 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2525 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2527 allsuccs = set()
2526 allsuccs = set()
2528 replaced = set()
2527 replaced = set()
2529 fullmapping = {}
2528 fullmapping = {}
2530 # initialize basic set
2529 # initialize basic set
2531 # fullmapping records all operations recorded in replacement
2530 # fullmapping records all operations recorded in replacement
2532 for rep in replacements:
2531 for rep in replacements:
2533 allsuccs.update(rep[1])
2532 allsuccs.update(rep[1])
2534 replaced.add(rep[0])
2533 replaced.add(rep[0])
2535 fullmapping.setdefault(rep[0], set()).update(rep[1])
2534 fullmapping.setdefault(rep[0], set()).update(rep[1])
2536 new = allsuccs - replaced
2535 new = allsuccs - replaced
2537 tmpnodes = allsuccs & replaced
2536 tmpnodes = allsuccs & replaced
2538 # Reduce content fullmapping into direct relation between original nodes
2537 # Reduce content fullmapping into direct relation between original nodes
2539 # and final node created during history edition
2538 # and final node created during history edition
2540 # Dropped changeset are replaced by an empty list
2539 # Dropped changeset are replaced by an empty list
2541 toproceed = set(fullmapping)
2540 toproceed = set(fullmapping)
2542 final = {}
2541 final = {}
2543 while toproceed:
2542 while toproceed:
2544 for x in list(toproceed):
2543 for x in list(toproceed):
2545 succs = fullmapping[x]
2544 succs = fullmapping[x]
2546 for s in list(succs):
2545 for s in list(succs):
2547 if s in toproceed:
2546 if s in toproceed:
2548 # non final node with unknown closure
2547 # non final node with unknown closure
2549 # We can't process this now
2548 # We can't process this now
2550 break
2549 break
2551 elif s in final:
2550 elif s in final:
2552 # non final node, replace with closure
2551 # non final node, replace with closure
2553 succs.remove(s)
2552 succs.remove(s)
2554 succs.update(final[s])
2553 succs.update(final[s])
2555 else:
2554 else:
2556 final[x] = succs
2555 final[x] = succs
2557 toproceed.remove(x)
2556 toproceed.remove(x)
2558 # remove tmpnodes from final mapping
2557 # remove tmpnodes from final mapping
2559 for n in tmpnodes:
2558 for n in tmpnodes:
2560 del final[n]
2559 del final[n]
2561 # we expect all changes involved in final to exist in the repo
2560 # we expect all changes involved in final to exist in the repo
2562 # turn `final` into list (topologically sorted)
2561 # turn `final` into list (topologically sorted)
2563 get_rev = state.repo.changelog.index.get_rev
2562 get_rev = state.repo.changelog.index.get_rev
2564 for prec, succs in final.items():
2563 for prec, succs in final.items():
2565 final[prec] = sorted(succs, key=get_rev)
2564 final[prec] = sorted(succs, key=get_rev)
2566
2565
2567 # computed topmost element (necessary for bookmark)
2566 # computed topmost element (necessary for bookmark)
2568 if new:
2567 if new:
2569 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2568 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2570 elif not final:
2569 elif not final:
2571 # Nothing rewritten at all. we won't need `newtopmost`
2570 # Nothing rewritten at all. we won't need `newtopmost`
2572 # It is the same as `oldtopmost` and `processreplacement` know it
2571 # It is the same as `oldtopmost` and `processreplacement` know it
2573 newtopmost = None
2572 newtopmost = None
2574 else:
2573 else:
2575 # every body died. The newtopmost is the parent of the root.
2574 # every body died. The newtopmost is the parent of the root.
2576 r = state.repo.changelog.rev
2575 r = state.repo.changelog.rev
2577 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2576 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2578
2577
2579 return final, tmpnodes, new, newtopmost
2578 return final, tmpnodes, new, newtopmost
2580
2579
2581
2580
2582 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2581 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2583 """Move bookmark from oldtopmost to newly created topmost
2582 """Move bookmark from oldtopmost to newly created topmost
2584
2583
2585 This is arguably a feature and we may only want that for the active
2584 This is arguably a feature and we may only want that for the active
2586 bookmark. But the behavior is kept compatible with the old version for now.
2585 bookmark. But the behavior is kept compatible with the old version for now.
2587 """
2586 """
2588 if not oldtopmost or not newtopmost:
2587 if not oldtopmost or not newtopmost:
2589 return
2588 return
2590 oldbmarks = repo.nodebookmarks(oldtopmost)
2589 oldbmarks = repo.nodebookmarks(oldtopmost)
2591 if oldbmarks:
2590 if oldbmarks:
2592 with repo.lock(), repo.transaction(b'histedit') as tr:
2591 with repo.lock(), repo.transaction(b'histedit') as tr:
2593 marks = repo._bookmarks
2592 marks = repo._bookmarks
2594 changes = []
2593 changes = []
2595 for name in oldbmarks:
2594 for name in oldbmarks:
2596 changes.append((name, newtopmost))
2595 changes.append((name, newtopmost))
2597 marks.applychanges(repo, tr, changes)
2596 marks.applychanges(repo, tr, changes)
2598
2597
2599
2598
2600 def cleanupnode(ui, repo, nodes, nobackup=False):
2599 def cleanupnode(ui, repo, nodes, nobackup=False):
2601 """strip a group of nodes from the repository
2600 """strip a group of nodes from the repository
2602
2601
2603 The set of node to strip may contains unknown nodes."""
2602 The set of node to strip may contains unknown nodes."""
2604 with repo.lock():
2603 with repo.lock():
2605 # do not let filtering get in the way of the cleanse
2604 # do not let filtering get in the way of the cleanse
2606 # we should probably get rid of obsolescence marker created during the
2605 # we should probably get rid of obsolescence marker created during the
2607 # histedit, but we currently do not have such information.
2606 # histedit, but we currently do not have such information.
2608 repo = repo.unfiltered()
2607 repo = repo.unfiltered()
2609 # Find all nodes that need to be stripped
2608 # Find all nodes that need to be stripped
2610 # (we use %lr instead of %ln to silently ignore unknown items)
2609 # (we use %lr instead of %ln to silently ignore unknown items)
2611 has_node = repo.changelog.index.has_node
2610 has_node = repo.changelog.index.has_node
2612 nodes = sorted(n for n in nodes if has_node(n))
2611 nodes = sorted(n for n in nodes if has_node(n))
2613 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2612 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2614 if roots:
2613 if roots:
2615 backup = not nobackup
2614 backup = not nobackup
2616 repair.strip(ui, repo, roots, backup=backup)
2615 repair.strip(ui, repo, roots, backup=backup)
2617
2616
2618
2617
2619 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2618 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2620 if isinstance(nodelist, bytes):
2619 if isinstance(nodelist, bytes):
2621 nodelist = [nodelist]
2620 nodelist = [nodelist]
2622 state = histeditstate(repo)
2621 state = histeditstate(repo)
2623 if state.inprogress():
2622 if state.inprogress():
2624 state.read()
2623 state.read()
2625 histedit_nodes = {
2624 histedit_nodes = {
2626 action.node for action in state.actions if action.node
2625 action.node for action in state.actions if action.node
2627 }
2626 }
2628 common_nodes = histedit_nodes & set(nodelist)
2627 common_nodes = histedit_nodes & set(nodelist)
2629 if common_nodes:
2628 if common_nodes:
2630 raise error.Abort(
2629 raise error.Abort(
2631 _(b"histedit in progress, can't strip %s")
2630 _(b"histedit in progress, can't strip %s")
2632 % b', '.join(short(x) for x in common_nodes)
2631 % b', '.join(short(x) for x in common_nodes)
2633 )
2632 )
2634 return orig(ui, repo, nodelist, *args, **kwargs)
2633 return orig(ui, repo, nodelist, *args, **kwargs)
2635
2634
2636
2635
2637 extensions.wrapfunction(repair, b'strip', stripwrapper)
2636 extensions.wrapfunction(repair, b'strip', stripwrapper)
2638
2637
2639
2638
2640 def summaryhook(ui, repo):
2639 def summaryhook(ui, repo):
2641 state = histeditstate(repo)
2640 state = histeditstate(repo)
2642 if not state.inprogress():
2641 if not state.inprogress():
2643 return
2642 return
2644 state.read()
2643 state.read()
2645 if state.actions:
2644 if state.actions:
2646 # i18n: column positioning for "hg summary"
2645 # i18n: column positioning for "hg summary"
2647 ui.write(
2646 ui.write(
2648 _(b'hist: %s (histedit --continue)\n')
2647 _(b'hist: %s (histedit --continue)\n')
2649 % (
2648 % (
2650 ui.label(_(b'%d remaining'), b'histedit.remaining')
2649 ui.label(_(b'%d remaining'), b'histedit.remaining')
2651 % len(state.actions)
2650 % len(state.actions)
2652 )
2651 )
2653 )
2652 )
2654
2653
2655
2654
2656 def extsetup(ui):
2655 def extsetup(ui):
2657 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2656 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2658 statemod.addunfinished(
2657 statemod.addunfinished(
2659 b'histedit',
2658 b'histedit',
2660 fname=b'histedit-state',
2659 fname=b'histedit-state',
2661 allowcommit=True,
2660 allowcommit=True,
2662 continueflag=True,
2661 continueflag=True,
2663 abortfunc=hgaborthistedit,
2662 abortfunc=hgaborthistedit,
2664 )
2663 )
General Comments 0
You need to be logged in to leave comments. Login now