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