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