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