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