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