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