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