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