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