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