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