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