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