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