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