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