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