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