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