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