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