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