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