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