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