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