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