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