##// END OF EJS Templates
histedit: make histedit's commands accept revsets (issue5746)...
Sangeet Kumar Mishra -
r37124:3d3cff1f default
parent child Browse files
Show More
@@ -1,1654 +1,1661 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commits are listed from least to most recent
33 # Commits are listed from least to most recent
34 #
34 #
35 # Commands:
35 # Commands:
36 # p, pick = use commit
36 # p, pick = use commit
37 # e, edit = use commit, but stop for amending
37 # e, edit = use commit, but stop for amending
38 # f, fold = use commit, but combine it with the one above
38 # f, fold = use commit, but combine it with the one above
39 # r, roll = like fold, but discard this commit's description and date
39 # r, roll = like fold, but discard this commit's description and date
40 # d, drop = remove commit from history
40 # d, drop = remove commit from history
41 # m, mess = edit commit message without changing commit content
41 # m, mess = edit commit message without changing commit content
42 # b, base = checkout changeset and apply further changesets from there
42 # b, base = checkout changeset and apply further changesets from there
43 #
43 #
44
44
45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 for each revision in your history. For example, if you had meant to add gamma
46 for each revision in your history. For example, if you had meant to add gamma
47 before beta, and then wanted to add delta in the same revision as beta, you
47 before beta, and then wanted to add delta in the same revision as beta, you
48 would reorganize the file to look like this::
48 would reorganize the file to look like this::
49
49
50 pick 030b686bedc4 Add gamma
50 pick 030b686bedc4 Add gamma
51 pick c561b4e977df Add beta
51 pick c561b4e977df Add beta
52 fold 7c2fd3b9020c Add delta
52 fold 7c2fd3b9020c Add delta
53
53
54 # Edit history between c561b4e977df and 7c2fd3b9020c
54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 #
55 #
56 # Commits are listed from least to most recent
56 # Commits are listed from least to most recent
57 #
57 #
58 # Commands:
58 # Commands:
59 # p, pick = use commit
59 # p, pick = use commit
60 # e, edit = use commit, but stop for amending
60 # e, edit = use commit, but stop for amending
61 # f, fold = use commit, but combine it with the one above
61 # f, fold = use commit, but combine it with the one above
62 # r, roll = like fold, but discard this commit's description and date
62 # r, roll = like fold, but discard this commit's description and date
63 # d, drop = remove commit from history
63 # d, drop = remove commit from history
64 # m, mess = edit commit message without changing commit content
64 # m, mess = edit commit message without changing commit content
65 # b, base = checkout changeset and apply further changesets from there
65 # b, base = checkout changeset and apply further changesets from there
66 #
66 #
67
67
68 At which point you close the editor and ``histedit`` starts working. When you
68 At which point you close the editor and ``histedit`` starts working. When you
69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 those revisions together, offering you a chance to clean up the commit message::
70 those revisions together, offering you a chance to clean up the commit message::
71
71
72 Add beta
72 Add beta
73 ***
73 ***
74 Add delta
74 Add delta
75
75
76 Edit the commit message to your liking, then close the editor. The date used
76 Edit the commit message to your liking, then close the editor. The date used
77 for the commit will be the later of the two commits' dates. For this example,
77 for the commit will be the later of the two commits' dates. For this example,
78 let's assume that the commit message was changed to ``Add beta and delta.``
78 let's assume that the commit message was changed to ``Add beta and delta.``
79 After histedit has run and had a chance to remove any old or temporary
79 After histedit has run and had a chance to remove any old or temporary
80 revisions it needed, the history looks like this::
80 revisions it needed, the history looks like this::
81
81
82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 | Add beta and delta.
83 | Add beta and delta.
84 |
84 |
85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 | Add gamma
86 | Add gamma
87 |
87 |
88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 Add alpha
89 Add alpha
90
90
91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 ones) until after it has completed all the editing operations, so it will
92 ones) until after it has completed all the editing operations, so it will
93 probably perform several strip operations when it's done. For the above example,
93 probably perform several strip operations when it's done. For the above example,
94 it had to run strip twice. Strip can be slow depending on a variety of factors,
94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 so you might need to be a little patient. You can choose to keep the original
95 so you might need to be a little patient. You can choose to keep the original
96 revisions by passing the ``--keep`` flag.
96 revisions by passing the ``--keep`` flag.
97
97
98 The ``edit`` operation will drop you back to a command prompt,
98 The ``edit`` operation will drop you back to a command prompt,
99 allowing you to edit files freely, or even use ``hg record`` to commit
99 allowing you to edit files freely, or even use ``hg record`` to commit
100 some changes as a separate commit. When you're done, any remaining
100 some changes as a separate commit. When you're done, any remaining
101 uncommitted changes will be committed as well. When done, run ``hg
101 uncommitted changes will be committed as well. When done, run ``hg
102 histedit --continue`` to finish this step. If there are uncommitted
102 histedit --continue`` to finish this step. If there are uncommitted
103 changes, you'll be prompted for a new commit message, but the default
103 changes, you'll be prompted for a new commit message, but the default
104 commit message will be the original message for the ``edit`` ed
104 commit message will be the original message for the ``edit`` ed
105 revision, and the date of the original commit will be preserved.
105 revision, and the date of the original commit will be preserved.
106
106
107 The ``message`` operation will give you a chance to revise a commit
107 The ``message`` operation will give you a chance to revise a commit
108 message without changing the contents. It's a shortcut for doing
108 message without changing the contents. It's a shortcut for doing
109 ``edit`` immediately followed by `hg histedit --continue``.
109 ``edit`` immediately followed by `hg histedit --continue``.
110
110
111 If ``histedit`` encounters a conflict when moving a revision (while
111 If ``histedit`` encounters a conflict when moving a revision (while
112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 ``edit`` with the difference that it won't prompt you for a commit
113 ``edit`` with the difference that it won't prompt you for a commit
114 message when done. If you decide at this point that you don't like how
114 message when done. If you decide at this point that you don't like how
115 much work it will be to rearrange history, or that you made a mistake,
115 much work it will be to rearrange history, or that you made a mistake,
116 you can use ``hg histedit --abort`` to abandon the new changes you
116 you can use ``hg histedit --abort`` to abandon the new changes you
117 have made and return to the state before you attempted to edit your
117 have made and return to the state before you attempted to edit your
118 history.
118 history.
119
119
120 If we clone the histedit-ed example repository above and add four more
120 If we clone the histedit-ed example repository above and add four more
121 changes, such that we have the following history::
121 changes, such that we have the following history::
122
122
123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 | Add theta
124 | Add theta
125 |
125 |
126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 | Add eta
127 | Add eta
128 |
128 |
129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 | Add zeta
130 | Add zeta
131 |
131 |
132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 | Add epsilon
133 | Add epsilon
134 |
134 |
135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 | Add beta and delta.
136 | Add beta and delta.
137 |
137 |
138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 | Add gamma
139 | Add gamma
140 |
140 |
141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 Add alpha
142 Add alpha
143
143
144 If you run ``hg histedit --outgoing`` on the clone then it is the same
144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 as running ``hg histedit 836302820282``. If you need plan to push to a
145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 repository that Mercurial does not detect to be related to the source
146 repository that Mercurial does not detect to be related to the source
147 repo, you can add a ``--force`` option.
147 repo, you can add a ``--force`` option.
148
148
149 Config
149 Config
150 ------
150 ------
151
151
152 Histedit rule lines are truncated to 80 characters by default. You
152 Histedit rule lines are truncated to 80 characters by default. You
153 can customize this behavior by setting a different length in your
153 can customize this behavior by setting a different length in your
154 configuration file::
154 configuration file::
155
155
156 [histedit]
156 [histedit]
157 linelen = 120 # truncate rule lines at 120 characters
157 linelen = 120 # truncate rule lines at 120 characters
158
158
159 ``hg histedit`` attempts to automatically choose an appropriate base
159 ``hg histedit`` attempts to automatically choose an appropriate base
160 revision to use. To change which base revision is used, define a
160 revision to use. To change which base revision is used, define a
161 revset in your configuration file::
161 revset in your configuration file::
162
162
163 [histedit]
163 [histedit]
164 defaultrev = only(.) & draft()
164 defaultrev = only(.) & draft()
165
165
166 By default each edited revision needs to be present in histedit commands.
166 By default each edited revision needs to be present in histedit commands.
167 To remove revision you need to use ``drop`` operation. You can configure
167 To remove revision you need to use ``drop`` operation. You can configure
168 the drop to be implicit for missing commits by adding::
168 the drop to be implicit for missing commits by adding::
169
169
170 [histedit]
170 [histedit]
171 dropmissing = True
171 dropmissing = True
172
172
173 By default, histedit will close the transaction after each action. For
173 By default, histedit will close the transaction after each action. For
174 performance purposes, you can configure histedit to use a single transaction
174 performance purposes, you can configure histedit to use a single transaction
175 across the entire histedit. WARNING: This setting introduces a significant risk
175 across the entire histedit. WARNING: This setting introduces a significant risk
176 of losing the work you've done in a histedit if the histedit aborts
176 of losing the work you've done in a histedit if the histedit aborts
177 unexpectedly::
177 unexpectedly::
178
178
179 [histedit]
179 [histedit]
180 singletransaction = True
180 singletransaction = True
181
181
182 """
182 """
183
183
184 from __future__ import absolute_import
184 from __future__ import absolute_import
185
185
186 import errno
186 import errno
187 import os
187 import os
188
188
189 from mercurial.i18n import _
189 from mercurial.i18n import _
190 from mercurial import (
190 from mercurial import (
191 bundle2,
191 bundle2,
192 cmdutil,
192 cmdutil,
193 context,
193 context,
194 copies,
194 copies,
195 destutil,
195 destutil,
196 discovery,
196 discovery,
197 error,
197 error,
198 exchange,
198 exchange,
199 extensions,
199 extensions,
200 hg,
200 hg,
201 lock,
201 lock,
202 merge as mergemod,
202 merge as mergemod,
203 mergeutil,
203 mergeutil,
204 node,
204 node,
205 obsolete,
205 obsolete,
206 pycompat,
206 pycompat,
207 registrar,
207 registrar,
208 repair,
208 repair,
209 scmutil,
209 scmutil,
210 util,
210 util,
211 )
211 )
212 from mercurial.utils import (
212 from mercurial.utils import (
213 stringutil,
213 stringutil,
214 )
214 )
215
215
216 pickle = util.pickle
216 pickle = util.pickle
217 release = lock.release
217 release = lock.release
218 cmdtable = {}
218 cmdtable = {}
219 command = registrar.command(cmdtable)
219 command = registrar.command(cmdtable)
220
220
221 configtable = {}
221 configtable = {}
222 configitem = registrar.configitem(configtable)
222 configitem = registrar.configitem(configtable)
223 configitem('experimental', 'histedit.autoverb',
223 configitem('experimental', 'histedit.autoverb',
224 default=False,
224 default=False,
225 )
225 )
226 configitem('histedit', 'defaultrev',
226 configitem('histedit', 'defaultrev',
227 default=None,
227 default=None,
228 )
228 )
229 configitem('histedit', 'dropmissing',
229 configitem('histedit', 'dropmissing',
230 default=False,
230 default=False,
231 )
231 )
232 configitem('histedit', 'linelen',
232 configitem('histedit', 'linelen',
233 default=80,
233 default=80,
234 )
234 )
235 configitem('histedit', 'singletransaction',
235 configitem('histedit', 'singletransaction',
236 default=False,
236 default=False,
237 )
237 )
238
238
239 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
239 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
240 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
240 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
241 # be specifying the version(s) of Mercurial they are tested with, or
241 # be specifying the version(s) of Mercurial they are tested with, or
242 # leave the attribute unspecified.
242 # leave the attribute unspecified.
243 testedwith = 'ships-with-hg-core'
243 testedwith = 'ships-with-hg-core'
244
244
245 actiontable = {}
245 actiontable = {}
246 primaryactions = set()
246 primaryactions = set()
247 secondaryactions = set()
247 secondaryactions = set()
248 tertiaryactions = set()
248 tertiaryactions = set()
249 internalactions = set()
249 internalactions = set()
250
250
251 def geteditcomment(ui, first, last):
251 def geteditcomment(ui, first, last):
252 """ construct the editor comment
252 """ construct the editor comment
253 The comment includes::
253 The comment includes::
254 - an intro
254 - an intro
255 - sorted primary commands
255 - sorted primary commands
256 - sorted short commands
256 - sorted short commands
257 - sorted long commands
257 - sorted long commands
258 - additional hints
258 - additional hints
259
259
260 Commands are only included once.
260 Commands are only included once.
261 """
261 """
262 intro = _("""Edit history between %s and %s
262 intro = _("""Edit history between %s and %s
263
263
264 Commits are listed from least to most recent
264 Commits are listed from least to most recent
265
265
266 You can reorder changesets by reordering the lines
266 You can reorder changesets by reordering the lines
267
267
268 Commands:
268 Commands:
269 """)
269 """)
270 actions = []
270 actions = []
271 def addverb(v):
271 def addverb(v):
272 a = actiontable[v]
272 a = actiontable[v]
273 lines = a.message.split("\n")
273 lines = a.message.split("\n")
274 if len(a.verbs):
274 if len(a.verbs):
275 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
275 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
276 actions.append(" %s = %s" % (v, lines[0]))
276 actions.append(" %s = %s" % (v, lines[0]))
277 actions.extend([' %s' for l in lines[1:]])
277 actions.extend([' %s' for l in lines[1:]])
278
278
279 for v in (
279 for v in (
280 sorted(primaryactions) +
280 sorted(primaryactions) +
281 sorted(secondaryactions) +
281 sorted(secondaryactions) +
282 sorted(tertiaryactions)
282 sorted(tertiaryactions)
283 ):
283 ):
284 addverb(v)
284 addverb(v)
285 actions.append('')
285 actions.append('')
286
286
287 hints = []
287 hints = []
288 if ui.configbool('histedit', 'dropmissing'):
288 if ui.configbool('histedit', 'dropmissing'):
289 hints.append("Deleting a changeset from the list "
289 hints.append("Deleting a changeset from the list "
290 "will DISCARD it from the edited history!")
290 "will DISCARD it from the edited history!")
291
291
292 lines = (intro % (first, last)).split('\n') + actions + hints
292 lines = (intro % (first, last)).split('\n') + actions + hints
293
293
294 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
294 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
295
295
296 class histeditstate(object):
296 class histeditstate(object):
297 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
297 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
298 topmost=None, replacements=None, lock=None, wlock=None):
298 topmost=None, replacements=None, lock=None, wlock=None):
299 self.repo = repo
299 self.repo = repo
300 self.actions = actions
300 self.actions = actions
301 self.keep = keep
301 self.keep = keep
302 self.topmost = topmost
302 self.topmost = topmost
303 self.parentctxnode = parentctxnode
303 self.parentctxnode = parentctxnode
304 self.lock = lock
304 self.lock = lock
305 self.wlock = wlock
305 self.wlock = wlock
306 self.backupfile = None
306 self.backupfile = None
307 if replacements is None:
307 if replacements is None:
308 self.replacements = []
308 self.replacements = []
309 else:
309 else:
310 self.replacements = replacements
310 self.replacements = replacements
311
311
312 def read(self):
312 def read(self):
313 """Load histedit state from disk and set fields appropriately."""
313 """Load histedit state from disk and set fields appropriately."""
314 try:
314 try:
315 state = self.repo.vfs.read('histedit-state')
315 state = self.repo.vfs.read('histedit-state')
316 except IOError as err:
316 except IOError as err:
317 if err.errno != errno.ENOENT:
317 if err.errno != errno.ENOENT:
318 raise
318 raise
319 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
319 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
320
320
321 if state.startswith('v1\n'):
321 if state.startswith('v1\n'):
322 data = self._load()
322 data = self._load()
323 parentctxnode, rules, keep, topmost, replacements, backupfile = data
323 parentctxnode, rules, keep, topmost, replacements, backupfile = data
324 else:
324 else:
325 data = pickle.loads(state)
325 data = pickle.loads(state)
326 parentctxnode, rules, keep, topmost, replacements = data
326 parentctxnode, rules, keep, topmost, replacements = data
327 backupfile = None
327 backupfile = None
328
328
329 self.parentctxnode = parentctxnode
329 self.parentctxnode = parentctxnode
330 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
330 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
331 actions = parserules(rules, self)
331 actions = parserules(rules, self)
332 self.actions = actions
332 self.actions = actions
333 self.keep = keep
333 self.keep = keep
334 self.topmost = topmost
334 self.topmost = topmost
335 self.replacements = replacements
335 self.replacements = replacements
336 self.backupfile = backupfile
336 self.backupfile = backupfile
337
337
338 def write(self, tr=None):
338 def write(self, tr=None):
339 if tr:
339 if tr:
340 tr.addfilegenerator('histedit-state', ('histedit-state',),
340 tr.addfilegenerator('histedit-state', ('histedit-state',),
341 self._write, location='plain')
341 self._write, location='plain')
342 else:
342 else:
343 with self.repo.vfs("histedit-state", "w") as f:
343 with self.repo.vfs("histedit-state", "w") as f:
344 self._write(f)
344 self._write(f)
345
345
346 def _write(self, fp):
346 def _write(self, fp):
347 fp.write('v1\n')
347 fp.write('v1\n')
348 fp.write('%s\n' % node.hex(self.parentctxnode))
348 fp.write('%s\n' % node.hex(self.parentctxnode))
349 fp.write('%s\n' % node.hex(self.topmost))
349 fp.write('%s\n' % node.hex(self.topmost))
350 fp.write('%s\n' % ('True' if self.keep else 'False'))
350 fp.write('%s\n' % ('True' if self.keep else 'False'))
351 fp.write('%d\n' % len(self.actions))
351 fp.write('%d\n' % len(self.actions))
352 for action in self.actions:
352 for action in self.actions:
353 fp.write('%s\n' % action.tostate())
353 fp.write('%s\n' % action.tostate())
354 fp.write('%d\n' % len(self.replacements))
354 fp.write('%d\n' % len(self.replacements))
355 for replacement in self.replacements:
355 for replacement in self.replacements:
356 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
356 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
357 for r in replacement[1])))
357 for r in replacement[1])))
358 backupfile = self.backupfile
358 backupfile = self.backupfile
359 if not backupfile:
359 if not backupfile:
360 backupfile = ''
360 backupfile = ''
361 fp.write('%s\n' % backupfile)
361 fp.write('%s\n' % backupfile)
362
362
363 def _load(self):
363 def _load(self):
364 fp = self.repo.vfs('histedit-state', 'r')
364 fp = self.repo.vfs('histedit-state', 'r')
365 lines = [l[:-1] for l in fp.readlines()]
365 lines = [l[:-1] for l in fp.readlines()]
366
366
367 index = 0
367 index = 0
368 lines[index] # version number
368 lines[index] # version number
369 index += 1
369 index += 1
370
370
371 parentctxnode = node.bin(lines[index])
371 parentctxnode = node.bin(lines[index])
372 index += 1
372 index += 1
373
373
374 topmost = node.bin(lines[index])
374 topmost = node.bin(lines[index])
375 index += 1
375 index += 1
376
376
377 keep = lines[index] == 'True'
377 keep = lines[index] == 'True'
378 index += 1
378 index += 1
379
379
380 # Rules
380 # Rules
381 rules = []
381 rules = []
382 rulelen = int(lines[index])
382 rulelen = int(lines[index])
383 index += 1
383 index += 1
384 for i in xrange(rulelen):
384 for i in xrange(rulelen):
385 ruleaction = lines[index]
385 ruleaction = lines[index]
386 index += 1
386 index += 1
387 rule = lines[index]
387 rule = lines[index]
388 index += 1
388 index += 1
389 rules.append((ruleaction, rule))
389 rules.append((ruleaction, rule))
390
390
391 # Replacements
391 # Replacements
392 replacements = []
392 replacements = []
393 replacementlen = int(lines[index])
393 replacementlen = int(lines[index])
394 index += 1
394 index += 1
395 for i in xrange(replacementlen):
395 for i in xrange(replacementlen):
396 replacement = lines[index]
396 replacement = lines[index]
397 original = node.bin(replacement[:40])
397 original = node.bin(replacement[:40])
398 succ = [node.bin(replacement[i:i + 40]) for i in
398 succ = [node.bin(replacement[i:i + 40]) for i in
399 range(40, len(replacement), 40)]
399 range(40, len(replacement), 40)]
400 replacements.append((original, succ))
400 replacements.append((original, succ))
401 index += 1
401 index += 1
402
402
403 backupfile = lines[index]
403 backupfile = lines[index]
404 index += 1
404 index += 1
405
405
406 fp.close()
406 fp.close()
407
407
408 return parentctxnode, rules, keep, topmost, replacements, backupfile
408 return parentctxnode, rules, keep, topmost, replacements, backupfile
409
409
410 def clear(self):
410 def clear(self):
411 if self.inprogress():
411 if self.inprogress():
412 self.repo.vfs.unlink('histedit-state')
412 self.repo.vfs.unlink('histedit-state')
413
413
414 def inprogress(self):
414 def inprogress(self):
415 return self.repo.vfs.exists('histedit-state')
415 return self.repo.vfs.exists('histedit-state')
416
416
417
417
418 class histeditaction(object):
418 class histeditaction(object):
419 def __init__(self, state, node):
419 def __init__(self, state, node):
420 self.state = state
420 self.state = state
421 self.repo = state.repo
421 self.repo = state.repo
422 self.node = node
422 self.node = node
423
423
424 @classmethod
424 @classmethod
425 def fromrule(cls, state, rule):
425 def fromrule(cls, state, rule):
426 """Parses the given rule, returning an instance of the histeditaction.
426 """Parses the given rule, returning an instance of the histeditaction.
427 """
427 """
428 rulehash = rule.strip().split(' ', 1)[0]
428 ruleid = rule.strip().split(' ', 1)[0]
429 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
430 # Check for validation of rule ids and get the rulehash
429 try:
431 try:
432 rev = node.bin(ruleid)
433 except TypeError:
434 try:
435 _ctx = scmutil.revsingle(state.repo, ruleid)
436 rulehash = _ctx.hex()
430 rev = node.bin(rulehash)
437 rev = node.bin(rulehash)
431 except TypeError:
438 except error.RepoLookupError:
432 raise error.ParseError("invalid changeset %s" % rulehash)
439 raise error.ParseError("invalid changeset %s" % ruleid)
433 return cls(state, rev)
440 return cls(state, rev)
434
441
435 def verify(self, prev, expected, seen):
442 def verify(self, prev, expected, seen):
436 """ Verifies semantic correctness of the rule"""
443 """ Verifies semantic correctness of the rule"""
437 repo = self.repo
444 repo = self.repo
438 ha = node.hex(self.node)
445 ha = node.hex(self.node)
439 try:
446 try:
440 self.node = repo[ha].node()
447 self.node = repo[ha].node()
441 except error.RepoError:
448 except error.RepoError:
442 raise error.ParseError(_('unknown changeset %s listed')
449 raise error.ParseError(_('unknown changeset %s listed')
443 % ha[:12])
450 % ha[:12])
444 if self.node is not None:
451 if self.node is not None:
445 self._verifynodeconstraints(prev, expected, seen)
452 self._verifynodeconstraints(prev, expected, seen)
446
453
447 def _verifynodeconstraints(self, prev, expected, seen):
454 def _verifynodeconstraints(self, prev, expected, seen):
448 # by default command need a node in the edited list
455 # by default command need a node in the edited list
449 if self.node not in expected:
456 if self.node not in expected:
450 raise error.ParseError(_('%s "%s" changeset was not a candidate')
457 raise error.ParseError(_('%s "%s" changeset was not a candidate')
451 % (self.verb, node.short(self.node)),
458 % (self.verb, node.short(self.node)),
452 hint=_('only use listed changesets'))
459 hint=_('only use listed changesets'))
453 # and only one command per node
460 # and only one command per node
454 if self.node in seen:
461 if self.node in seen:
455 raise error.ParseError(_('duplicated command for changeset %s') %
462 raise error.ParseError(_('duplicated command for changeset %s') %
456 node.short(self.node))
463 node.short(self.node))
457
464
458 def torule(self):
465 def torule(self):
459 """build a histedit rule line for an action
466 """build a histedit rule line for an action
460
467
461 by default lines are in the form:
468 by default lines are in the form:
462 <hash> <rev> <summary>
469 <hash> <rev> <summary>
463 """
470 """
464 ctx = self.repo[self.node]
471 ctx = self.repo[self.node]
465 summary = _getsummary(ctx)
472 summary = _getsummary(ctx)
466 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
473 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
467 # trim to 75 columns by default so it's not stupidly wide in my editor
474 # trim to 75 columns by default so it's not stupidly wide in my editor
468 # (the 5 more are left for verb)
475 # (the 5 more are left for verb)
469 maxlen = self.repo.ui.configint('histedit', 'linelen')
476 maxlen = self.repo.ui.configint('histedit', 'linelen')
470 maxlen = max(maxlen, 22) # avoid truncating hash
477 maxlen = max(maxlen, 22) # avoid truncating hash
471 return stringutil.ellipsis(line, maxlen)
478 return stringutil.ellipsis(line, maxlen)
472
479
473 def tostate(self):
480 def tostate(self):
474 """Print an action in format used by histedit state files
481 """Print an action in format used by histedit state files
475 (the first line is a verb, the remainder is the second)
482 (the first line is a verb, the remainder is the second)
476 """
483 """
477 return "%s\n%s" % (self.verb, node.hex(self.node))
484 return "%s\n%s" % (self.verb, node.hex(self.node))
478
485
479 def run(self):
486 def run(self):
480 """Runs the action. The default behavior is simply apply the action's
487 """Runs the action. The default behavior is simply apply the action's
481 rulectx onto the current parentctx."""
488 rulectx onto the current parentctx."""
482 self.applychange()
489 self.applychange()
483 self.continuedirty()
490 self.continuedirty()
484 return self.continueclean()
491 return self.continueclean()
485
492
486 def applychange(self):
493 def applychange(self):
487 """Applies the changes from this action's rulectx onto the current
494 """Applies the changes from this action's rulectx onto the current
488 parentctx, but does not commit them."""
495 parentctx, but does not commit them."""
489 repo = self.repo
496 repo = self.repo
490 rulectx = repo[self.node]
497 rulectx = repo[self.node]
491 repo.ui.pushbuffer(error=True, labeled=True)
498 repo.ui.pushbuffer(error=True, labeled=True)
492 hg.update(repo, self.state.parentctxnode, quietempty=True)
499 hg.update(repo, self.state.parentctxnode, quietempty=True)
493 stats = applychanges(repo.ui, repo, rulectx, {})
500 stats = applychanges(repo.ui, repo, rulectx, {})
494 repo.dirstate.setbranch(rulectx.branch())
501 repo.dirstate.setbranch(rulectx.branch())
495 if stats and stats[3] > 0:
502 if stats and stats[3] > 0:
496 buf = repo.ui.popbuffer()
503 buf = repo.ui.popbuffer()
497 repo.ui.write(buf)
504 repo.ui.write(buf)
498 raise error.InterventionRequired(
505 raise error.InterventionRequired(
499 _('Fix up the change (%s %s)') %
506 _('Fix up the change (%s %s)') %
500 (self.verb, node.short(self.node)),
507 (self.verb, node.short(self.node)),
501 hint=_('hg histedit --continue to resume'))
508 hint=_('hg histedit --continue to resume'))
502 else:
509 else:
503 repo.ui.popbuffer()
510 repo.ui.popbuffer()
504
511
505 def continuedirty(self):
512 def continuedirty(self):
506 """Continues the action when changes have been applied to the working
513 """Continues the action when changes have been applied to the working
507 copy. The default behavior is to commit the dirty changes."""
514 copy. The default behavior is to commit the dirty changes."""
508 repo = self.repo
515 repo = self.repo
509 rulectx = repo[self.node]
516 rulectx = repo[self.node]
510
517
511 editor = self.commiteditor()
518 editor = self.commiteditor()
512 commit = commitfuncfor(repo, rulectx)
519 commit = commitfuncfor(repo, rulectx)
513
520
514 commit(text=rulectx.description(), user=rulectx.user(),
521 commit(text=rulectx.description(), user=rulectx.user(),
515 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
522 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
516
523
517 def commiteditor(self):
524 def commiteditor(self):
518 """The editor to be used to edit the commit message."""
525 """The editor to be used to edit the commit message."""
519 return False
526 return False
520
527
521 def continueclean(self):
528 def continueclean(self):
522 """Continues the action when the working copy is clean. The default
529 """Continues the action when the working copy is clean. The default
523 behavior is to accept the current commit as the new version of the
530 behavior is to accept the current commit as the new version of the
524 rulectx."""
531 rulectx."""
525 ctx = self.repo['.']
532 ctx = self.repo['.']
526 if ctx.node() == self.state.parentctxnode:
533 if ctx.node() == self.state.parentctxnode:
527 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
534 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
528 node.short(self.node))
535 node.short(self.node))
529 return ctx, [(self.node, tuple())]
536 return ctx, [(self.node, tuple())]
530 if ctx.node() == self.node:
537 if ctx.node() == self.node:
531 # Nothing changed
538 # Nothing changed
532 return ctx, []
539 return ctx, []
533 return ctx, [(self.node, (ctx.node(),))]
540 return ctx, [(self.node, (ctx.node(),))]
534
541
535 def commitfuncfor(repo, src):
542 def commitfuncfor(repo, src):
536 """Build a commit function for the replacement of <src>
543 """Build a commit function for the replacement of <src>
537
544
538 This function ensure we apply the same treatment to all changesets.
545 This function ensure we apply the same treatment to all changesets.
539
546
540 - Add a 'histedit_source' entry in extra.
547 - Add a 'histedit_source' entry in extra.
541
548
542 Note that fold has its own separated logic because its handling is a bit
549 Note that fold has its own separated logic because its handling is a bit
543 different and not easily factored out of the fold method.
550 different and not easily factored out of the fold method.
544 """
551 """
545 phasemin = src.phase()
552 phasemin = src.phase()
546 def commitfunc(**kwargs):
553 def commitfunc(**kwargs):
547 overrides = {('phases', 'new-commit'): phasemin}
554 overrides = {('phases', 'new-commit'): phasemin}
548 with repo.ui.configoverride(overrides, 'histedit'):
555 with repo.ui.configoverride(overrides, 'histedit'):
549 extra = kwargs.get(r'extra', {}).copy()
556 extra = kwargs.get(r'extra', {}).copy()
550 extra['histedit_source'] = src.hex()
557 extra['histedit_source'] = src.hex()
551 kwargs[r'extra'] = extra
558 kwargs[r'extra'] = extra
552 return repo.commit(**kwargs)
559 return repo.commit(**kwargs)
553 return commitfunc
560 return commitfunc
554
561
555 def applychanges(ui, repo, ctx, opts):
562 def applychanges(ui, repo, ctx, opts):
556 """Merge changeset from ctx (only) in the current working directory"""
563 """Merge changeset from ctx (only) in the current working directory"""
557 wcpar = repo.dirstate.parents()[0]
564 wcpar = repo.dirstate.parents()[0]
558 if ctx.p1().node() == wcpar:
565 if ctx.p1().node() == wcpar:
559 # edits are "in place" we do not need to make any merge,
566 # edits are "in place" we do not need to make any merge,
560 # just applies changes on parent for editing
567 # just applies changes on parent for editing
561 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
568 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
562 stats = None
569 stats = None
563 else:
570 else:
564 try:
571 try:
565 # ui.forcemerge is an internal variable, do not document
572 # ui.forcemerge is an internal variable, do not document
566 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
573 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
567 'histedit')
574 'histedit')
568 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
575 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
569 finally:
576 finally:
570 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
577 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
571 return stats
578 return stats
572
579
573 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
580 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
574 """collapse the set of revisions from first to last as new one.
581 """collapse the set of revisions from first to last as new one.
575
582
576 Expected commit options are:
583 Expected commit options are:
577 - message
584 - message
578 - date
585 - date
579 - username
586 - username
580 Commit message is edited in all cases.
587 Commit message is edited in all cases.
581
588
582 This function works in memory."""
589 This function works in memory."""
583 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
590 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
584 if not ctxs:
591 if not ctxs:
585 return None
592 return None
586 for c in ctxs:
593 for c in ctxs:
587 if not c.mutable():
594 if not c.mutable():
588 raise error.ParseError(
595 raise error.ParseError(
589 _("cannot fold into public change %s") % node.short(c.node()))
596 _("cannot fold into public change %s") % node.short(c.node()))
590 base = firstctx.parents()[0]
597 base = firstctx.parents()[0]
591
598
592 # commit a new version of the old changeset, including the update
599 # commit a new version of the old changeset, including the update
593 # collect all files which might be affected
600 # collect all files which might be affected
594 files = set()
601 files = set()
595 for ctx in ctxs:
602 for ctx in ctxs:
596 files.update(ctx.files())
603 files.update(ctx.files())
597
604
598 # Recompute copies (avoid recording a -> b -> a)
605 # Recompute copies (avoid recording a -> b -> a)
599 copied = copies.pathcopies(base, lastctx)
606 copied = copies.pathcopies(base, lastctx)
600
607
601 # prune files which were reverted by the updates
608 # prune files which were reverted by the updates
602 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
609 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
603 # commit version of these files as defined by head
610 # commit version of these files as defined by head
604 headmf = lastctx.manifest()
611 headmf = lastctx.manifest()
605 def filectxfn(repo, ctx, path):
612 def filectxfn(repo, ctx, path):
606 if path in headmf:
613 if path in headmf:
607 fctx = lastctx[path]
614 fctx = lastctx[path]
608 flags = fctx.flags()
615 flags = fctx.flags()
609 mctx = context.memfilectx(repo, ctx,
616 mctx = context.memfilectx(repo, ctx,
610 fctx.path(), fctx.data(),
617 fctx.path(), fctx.data(),
611 islink='l' in flags,
618 islink='l' in flags,
612 isexec='x' in flags,
619 isexec='x' in flags,
613 copied=copied.get(path))
620 copied=copied.get(path))
614 return mctx
621 return mctx
615 return None
622 return None
616
623
617 if commitopts.get('message'):
624 if commitopts.get('message'):
618 message = commitopts['message']
625 message = commitopts['message']
619 else:
626 else:
620 message = firstctx.description()
627 message = firstctx.description()
621 user = commitopts.get('user')
628 user = commitopts.get('user')
622 date = commitopts.get('date')
629 date = commitopts.get('date')
623 extra = commitopts.get('extra')
630 extra = commitopts.get('extra')
624
631
625 parents = (firstctx.p1().node(), firstctx.p2().node())
632 parents = (firstctx.p1().node(), firstctx.p2().node())
626 editor = None
633 editor = None
627 if not skipprompt:
634 if not skipprompt:
628 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
635 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
629 new = context.memctx(repo,
636 new = context.memctx(repo,
630 parents=parents,
637 parents=parents,
631 text=message,
638 text=message,
632 files=files,
639 files=files,
633 filectxfn=filectxfn,
640 filectxfn=filectxfn,
634 user=user,
641 user=user,
635 date=date,
642 date=date,
636 extra=extra,
643 extra=extra,
637 editor=editor)
644 editor=editor)
638 return repo.commitctx(new)
645 return repo.commitctx(new)
639
646
640 def _isdirtywc(repo):
647 def _isdirtywc(repo):
641 return repo[None].dirty(missing=True)
648 return repo[None].dirty(missing=True)
642
649
643 def abortdirty():
650 def abortdirty():
644 raise error.Abort(_('working copy has pending changes'),
651 raise error.Abort(_('working copy has pending changes'),
645 hint=_('amend, commit, or revert them and run histedit '
652 hint=_('amend, commit, or revert them and run histedit '
646 '--continue, or abort with histedit --abort'))
653 '--continue, or abort with histedit --abort'))
647
654
648 def action(verbs, message, priority=False, internal=False):
655 def action(verbs, message, priority=False, internal=False):
649 def wrap(cls):
656 def wrap(cls):
650 assert not priority or not internal
657 assert not priority or not internal
651 verb = verbs[0]
658 verb = verbs[0]
652 if priority:
659 if priority:
653 primaryactions.add(verb)
660 primaryactions.add(verb)
654 elif internal:
661 elif internal:
655 internalactions.add(verb)
662 internalactions.add(verb)
656 elif len(verbs) > 1:
663 elif len(verbs) > 1:
657 secondaryactions.add(verb)
664 secondaryactions.add(verb)
658 else:
665 else:
659 tertiaryactions.add(verb)
666 tertiaryactions.add(verb)
660
667
661 cls.verb = verb
668 cls.verb = verb
662 cls.verbs = verbs
669 cls.verbs = verbs
663 cls.message = message
670 cls.message = message
664 for verb in verbs:
671 for verb in verbs:
665 actiontable[verb] = cls
672 actiontable[verb] = cls
666 return cls
673 return cls
667 return wrap
674 return wrap
668
675
669 @action(['pick', 'p'],
676 @action(['pick', 'p'],
670 _('use commit'),
677 _('use commit'),
671 priority=True)
678 priority=True)
672 class pick(histeditaction):
679 class pick(histeditaction):
673 def run(self):
680 def run(self):
674 rulectx = self.repo[self.node]
681 rulectx = self.repo[self.node]
675 if rulectx.parents()[0].node() == self.state.parentctxnode:
682 if rulectx.parents()[0].node() == self.state.parentctxnode:
676 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
683 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
677 return rulectx, []
684 return rulectx, []
678
685
679 return super(pick, self).run()
686 return super(pick, self).run()
680
687
681 @action(['edit', 'e'],
688 @action(['edit', 'e'],
682 _('use commit, but stop for amending'),
689 _('use commit, but stop for amending'),
683 priority=True)
690 priority=True)
684 class edit(histeditaction):
691 class edit(histeditaction):
685 def run(self):
692 def run(self):
686 repo = self.repo
693 repo = self.repo
687 rulectx = repo[self.node]
694 rulectx = repo[self.node]
688 hg.update(repo, self.state.parentctxnode, quietempty=True)
695 hg.update(repo, self.state.parentctxnode, quietempty=True)
689 applychanges(repo.ui, repo, rulectx, {})
696 applychanges(repo.ui, repo, rulectx, {})
690 raise error.InterventionRequired(
697 raise error.InterventionRequired(
691 _('Editing (%s), you may commit or record as needed now.')
698 _('Editing (%s), you may commit or record as needed now.')
692 % node.short(self.node),
699 % node.short(self.node),
693 hint=_('hg histedit --continue to resume'))
700 hint=_('hg histedit --continue to resume'))
694
701
695 def commiteditor(self):
702 def commiteditor(self):
696 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
703 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
697
704
698 @action(['fold', 'f'],
705 @action(['fold', 'f'],
699 _('use commit, but combine it with the one above'))
706 _('use commit, but combine it with the one above'))
700 class fold(histeditaction):
707 class fold(histeditaction):
701 def verify(self, prev, expected, seen):
708 def verify(self, prev, expected, seen):
702 """ Verifies semantic correctness of the fold rule"""
709 """ Verifies semantic correctness of the fold rule"""
703 super(fold, self).verify(prev, expected, seen)
710 super(fold, self).verify(prev, expected, seen)
704 repo = self.repo
711 repo = self.repo
705 if not prev:
712 if not prev:
706 c = repo[self.node].parents()[0]
713 c = repo[self.node].parents()[0]
707 elif not prev.verb in ('pick', 'base'):
714 elif not prev.verb in ('pick', 'base'):
708 return
715 return
709 else:
716 else:
710 c = repo[prev.node]
717 c = repo[prev.node]
711 if not c.mutable():
718 if not c.mutable():
712 raise error.ParseError(
719 raise error.ParseError(
713 _("cannot fold into public change %s") % node.short(c.node()))
720 _("cannot fold into public change %s") % node.short(c.node()))
714
721
715
722
716 def continuedirty(self):
723 def continuedirty(self):
717 repo = self.repo
724 repo = self.repo
718 rulectx = repo[self.node]
725 rulectx = repo[self.node]
719
726
720 commit = commitfuncfor(repo, rulectx)
727 commit = commitfuncfor(repo, rulectx)
721 commit(text='fold-temp-revision %s' % node.short(self.node),
728 commit(text='fold-temp-revision %s' % node.short(self.node),
722 user=rulectx.user(), date=rulectx.date(),
729 user=rulectx.user(), date=rulectx.date(),
723 extra=rulectx.extra())
730 extra=rulectx.extra())
724
731
725 def continueclean(self):
732 def continueclean(self):
726 repo = self.repo
733 repo = self.repo
727 ctx = repo['.']
734 ctx = repo['.']
728 rulectx = repo[self.node]
735 rulectx = repo[self.node]
729 parentctxnode = self.state.parentctxnode
736 parentctxnode = self.state.parentctxnode
730 if ctx.node() == parentctxnode:
737 if ctx.node() == parentctxnode:
731 repo.ui.warn(_('%s: empty changeset\n') %
738 repo.ui.warn(_('%s: empty changeset\n') %
732 node.short(self.node))
739 node.short(self.node))
733 return ctx, [(self.node, (parentctxnode,))]
740 return ctx, [(self.node, (parentctxnode,))]
734
741
735 parentctx = repo[parentctxnode]
742 parentctx = repo[parentctxnode]
736 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
743 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
737 parentctx.rev(),
744 parentctx.rev(),
738 parentctx.rev()))
745 parentctx.rev()))
739 if not newcommits:
746 if not newcommits:
740 repo.ui.warn(_('%s: cannot fold - working copy is not a '
747 repo.ui.warn(_('%s: cannot fold - working copy is not a '
741 'descendant of previous commit %s\n') %
748 'descendant of previous commit %s\n') %
742 (node.short(self.node), node.short(parentctxnode)))
749 (node.short(self.node), node.short(parentctxnode)))
743 return ctx, [(self.node, (ctx.node(),))]
750 return ctx, [(self.node, (ctx.node(),))]
744
751
745 middlecommits = newcommits.copy()
752 middlecommits = newcommits.copy()
746 middlecommits.discard(ctx.node())
753 middlecommits.discard(ctx.node())
747
754
748 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
755 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
749 middlecommits)
756 middlecommits)
750
757
751 def skipprompt(self):
758 def skipprompt(self):
752 """Returns true if the rule should skip the message editor.
759 """Returns true if the rule should skip the message editor.
753
760
754 For example, 'fold' wants to show an editor, but 'rollup'
761 For example, 'fold' wants to show an editor, but 'rollup'
755 doesn't want to.
762 doesn't want to.
756 """
763 """
757 return False
764 return False
758
765
759 def mergedescs(self):
766 def mergedescs(self):
760 """Returns true if the rule should merge messages of multiple changes.
767 """Returns true if the rule should merge messages of multiple changes.
761
768
762 This exists mainly so that 'rollup' rules can be a subclass of
769 This exists mainly so that 'rollup' rules can be a subclass of
763 'fold'.
770 'fold'.
764 """
771 """
765 return True
772 return True
766
773
767 def firstdate(self):
774 def firstdate(self):
768 """Returns true if the rule should preserve the date of the first
775 """Returns true if the rule should preserve the date of the first
769 change.
776 change.
770
777
771 This exists mainly so that 'rollup' rules can be a subclass of
778 This exists mainly so that 'rollup' rules can be a subclass of
772 'fold'.
779 'fold'.
773 """
780 """
774 return False
781 return False
775
782
776 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
783 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
777 parent = ctx.parents()[0].node()
784 parent = ctx.parents()[0].node()
778 repo.ui.pushbuffer()
785 repo.ui.pushbuffer()
779 hg.update(repo, parent)
786 hg.update(repo, parent)
780 repo.ui.popbuffer()
787 repo.ui.popbuffer()
781 ### prepare new commit data
788 ### prepare new commit data
782 commitopts = {}
789 commitopts = {}
783 commitopts['user'] = ctx.user()
790 commitopts['user'] = ctx.user()
784 # commit message
791 # commit message
785 if not self.mergedescs():
792 if not self.mergedescs():
786 newmessage = ctx.description()
793 newmessage = ctx.description()
787 else:
794 else:
788 newmessage = '\n***\n'.join(
795 newmessage = '\n***\n'.join(
789 [ctx.description()] +
796 [ctx.description()] +
790 [repo[r].description() for r in internalchanges] +
797 [repo[r].description() for r in internalchanges] +
791 [oldctx.description()]) + '\n'
798 [oldctx.description()]) + '\n'
792 commitopts['message'] = newmessage
799 commitopts['message'] = newmessage
793 # date
800 # date
794 if self.firstdate():
801 if self.firstdate():
795 commitopts['date'] = ctx.date()
802 commitopts['date'] = ctx.date()
796 else:
803 else:
797 commitopts['date'] = max(ctx.date(), oldctx.date())
804 commitopts['date'] = max(ctx.date(), oldctx.date())
798 extra = ctx.extra().copy()
805 extra = ctx.extra().copy()
799 # histedit_source
806 # histedit_source
800 # note: ctx is likely a temporary commit but that the best we can do
807 # note: ctx is likely a temporary commit but that the best we can do
801 # here. This is sufficient to solve issue3681 anyway.
808 # here. This is sufficient to solve issue3681 anyway.
802 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
809 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
803 commitopts['extra'] = extra
810 commitopts['extra'] = extra
804 phasemin = max(ctx.phase(), oldctx.phase())
811 phasemin = max(ctx.phase(), oldctx.phase())
805 overrides = {('phases', 'new-commit'): phasemin}
812 overrides = {('phases', 'new-commit'): phasemin}
806 with repo.ui.configoverride(overrides, 'histedit'):
813 with repo.ui.configoverride(overrides, 'histedit'):
807 n = collapse(repo, ctx, repo[newnode], commitopts,
814 n = collapse(repo, ctx, repo[newnode], commitopts,
808 skipprompt=self.skipprompt())
815 skipprompt=self.skipprompt())
809 if n is None:
816 if n is None:
810 return ctx, []
817 return ctx, []
811 repo.ui.pushbuffer()
818 repo.ui.pushbuffer()
812 hg.update(repo, n)
819 hg.update(repo, n)
813 repo.ui.popbuffer()
820 repo.ui.popbuffer()
814 replacements = [(oldctx.node(), (newnode,)),
821 replacements = [(oldctx.node(), (newnode,)),
815 (ctx.node(), (n,)),
822 (ctx.node(), (n,)),
816 (newnode, (n,)),
823 (newnode, (n,)),
817 ]
824 ]
818 for ich in internalchanges:
825 for ich in internalchanges:
819 replacements.append((ich, (n,)))
826 replacements.append((ich, (n,)))
820 return repo[n], replacements
827 return repo[n], replacements
821
828
822 @action(['base', 'b'],
829 @action(['base', 'b'],
823 _('checkout changeset and apply further changesets from there'))
830 _('checkout changeset and apply further changesets from there'))
824 class base(histeditaction):
831 class base(histeditaction):
825
832
826 def run(self):
833 def run(self):
827 if self.repo['.'].node() != self.node:
834 if self.repo['.'].node() != self.node:
828 mergemod.update(self.repo, self.node, False, True)
835 mergemod.update(self.repo, self.node, False, True)
829 # branchmerge, force)
836 # branchmerge, force)
830 return self.continueclean()
837 return self.continueclean()
831
838
832 def continuedirty(self):
839 def continuedirty(self):
833 abortdirty()
840 abortdirty()
834
841
835 def continueclean(self):
842 def continueclean(self):
836 basectx = self.repo['.']
843 basectx = self.repo['.']
837 return basectx, []
844 return basectx, []
838
845
839 def _verifynodeconstraints(self, prev, expected, seen):
846 def _verifynodeconstraints(self, prev, expected, seen):
840 # base can only be use with a node not in the edited set
847 # base can only be use with a node not in the edited set
841 if self.node in expected:
848 if self.node in expected:
842 msg = _('%s "%s" changeset was an edited list candidate')
849 msg = _('%s "%s" changeset was an edited list candidate')
843 raise error.ParseError(
850 raise error.ParseError(
844 msg % (self.verb, node.short(self.node)),
851 msg % (self.verb, node.short(self.node)),
845 hint=_('base must only use unlisted changesets'))
852 hint=_('base must only use unlisted changesets'))
846
853
847 @action(['_multifold'],
854 @action(['_multifold'],
848 _(
855 _(
849 """fold subclass used for when multiple folds happen in a row
856 """fold subclass used for when multiple folds happen in a row
850
857
851 We only want to fire the editor for the folded message once when
858 We only want to fire the editor for the folded message once when
852 (say) four changes are folded down into a single change. This is
859 (say) four changes are folded down into a single change. This is
853 similar to rollup, but we should preserve both messages so that
860 similar to rollup, but we should preserve both messages so that
854 when the last fold operation runs we can show the user all the
861 when the last fold operation runs we can show the user all the
855 commit messages in their editor.
862 commit messages in their editor.
856 """),
863 """),
857 internal=True)
864 internal=True)
858 class _multifold(fold):
865 class _multifold(fold):
859 def skipprompt(self):
866 def skipprompt(self):
860 return True
867 return True
861
868
862 @action(["roll", "r"],
869 @action(["roll", "r"],
863 _("like fold, but discard this commit's description and date"))
870 _("like fold, but discard this commit's description and date"))
864 class rollup(fold):
871 class rollup(fold):
865 def mergedescs(self):
872 def mergedescs(self):
866 return False
873 return False
867
874
868 def skipprompt(self):
875 def skipprompt(self):
869 return True
876 return True
870
877
871 def firstdate(self):
878 def firstdate(self):
872 return True
879 return True
873
880
874 @action(["drop", "d"],
881 @action(["drop", "d"],
875 _('remove commit from history'))
882 _('remove commit from history'))
876 class drop(histeditaction):
883 class drop(histeditaction):
877 def run(self):
884 def run(self):
878 parentctx = self.repo[self.state.parentctxnode]
885 parentctx = self.repo[self.state.parentctxnode]
879 return parentctx, [(self.node, tuple())]
886 return parentctx, [(self.node, tuple())]
880
887
881 @action(["mess", "m"],
888 @action(["mess", "m"],
882 _('edit commit message without changing commit content'),
889 _('edit commit message without changing commit content'),
883 priority=True)
890 priority=True)
884 class message(histeditaction):
891 class message(histeditaction):
885 def commiteditor(self):
892 def commiteditor(self):
886 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
893 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
887
894
888 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
895 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
889 """utility function to find the first outgoing changeset
896 """utility function to find the first outgoing changeset
890
897
891 Used by initialization code"""
898 Used by initialization code"""
892 if opts is None:
899 if opts is None:
893 opts = {}
900 opts = {}
894 dest = ui.expandpath(remote or 'default-push', remote or 'default')
901 dest = ui.expandpath(remote or 'default-push', remote or 'default')
895 dest, revs = hg.parseurl(dest, None)[:2]
902 dest, revs = hg.parseurl(dest, None)[:2]
896 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
903 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
897
904
898 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
905 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
899 other = hg.peer(repo, opts, dest)
906 other = hg.peer(repo, opts, dest)
900
907
901 if revs:
908 if revs:
902 revs = [repo.lookup(rev) for rev in revs]
909 revs = [repo.lookup(rev) for rev in revs]
903
910
904 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
911 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
905 if not outgoing.missing:
912 if not outgoing.missing:
906 raise error.Abort(_('no outgoing ancestors'))
913 raise error.Abort(_('no outgoing ancestors'))
907 roots = list(repo.revs("roots(%ln)", outgoing.missing))
914 roots = list(repo.revs("roots(%ln)", outgoing.missing))
908 if 1 < len(roots):
915 if 1 < len(roots):
909 msg = _('there are ambiguous outgoing revisions')
916 msg = _('there are ambiguous outgoing revisions')
910 hint = _("see 'hg help histedit' for more detail")
917 hint = _("see 'hg help histedit' for more detail")
911 raise error.Abort(msg, hint=hint)
918 raise error.Abort(msg, hint=hint)
912 return repo.lookup(roots[0])
919 return repo.lookup(roots[0])
913
920
914 @command('histedit',
921 @command('histedit',
915 [('', 'commands', '',
922 [('', 'commands', '',
916 _('read history edits from the specified file'), _('FILE')),
923 _('read history edits from the specified file'), _('FILE')),
917 ('c', 'continue', False, _('continue an edit already in progress')),
924 ('c', 'continue', False, _('continue an edit already in progress')),
918 ('', 'edit-plan', False, _('edit remaining actions list')),
925 ('', 'edit-plan', False, _('edit remaining actions list')),
919 ('k', 'keep', False,
926 ('k', 'keep', False,
920 _("don't strip old nodes after edit is complete")),
927 _("don't strip old nodes after edit is complete")),
921 ('', 'abort', False, _('abort an edit in progress')),
928 ('', 'abort', False, _('abort an edit in progress')),
922 ('o', 'outgoing', False, _('changesets not found in destination')),
929 ('o', 'outgoing', False, _('changesets not found in destination')),
923 ('f', 'force', False,
930 ('f', 'force', False,
924 _('force outgoing even for unrelated repositories')),
931 _('force outgoing even for unrelated repositories')),
925 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
932 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
926 cmdutil.formatteropts,
933 cmdutil.formatteropts,
927 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
934 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
928 def histedit(ui, repo, *freeargs, **opts):
935 def histedit(ui, repo, *freeargs, **opts):
929 """interactively edit changeset history
936 """interactively edit changeset history
930
937
931 This command lets you edit a linear series of changesets (up to
938 This command lets you edit a linear series of changesets (up to
932 and including the working directory, which should be clean).
939 and including the working directory, which should be clean).
933 You can:
940 You can:
934
941
935 - `pick` to [re]order a changeset
942 - `pick` to [re]order a changeset
936
943
937 - `drop` to omit changeset
944 - `drop` to omit changeset
938
945
939 - `mess` to reword the changeset commit message
946 - `mess` to reword the changeset commit message
940
947
941 - `fold` to combine it with the preceding changeset (using the later date)
948 - `fold` to combine it with the preceding changeset (using the later date)
942
949
943 - `roll` like fold, but discarding this commit's description and date
950 - `roll` like fold, but discarding this commit's description and date
944
951
945 - `edit` to edit this changeset (preserving date)
952 - `edit` to edit this changeset (preserving date)
946
953
947 - `base` to checkout changeset and apply further changesets from there
954 - `base` to checkout changeset and apply further changesets from there
948
955
949 There are a number of ways to select the root changeset:
956 There are a number of ways to select the root changeset:
950
957
951 - Specify ANCESTOR directly
958 - Specify ANCESTOR directly
952
959
953 - Use --outgoing -- it will be the first linear changeset not
960 - Use --outgoing -- it will be the first linear changeset not
954 included in destination. (See :hg:`help config.paths.default-push`)
961 included in destination. (See :hg:`help config.paths.default-push`)
955
962
956 - Otherwise, the value from the "histedit.defaultrev" config option
963 - Otherwise, the value from the "histedit.defaultrev" config option
957 is used as a revset to select the base revision when ANCESTOR is not
964 is used as a revset to select the base revision when ANCESTOR is not
958 specified. The first revision returned by the revset is used. By
965 specified. The first revision returned by the revset is used. By
959 default, this selects the editable history that is unique to the
966 default, this selects the editable history that is unique to the
960 ancestry of the working directory.
967 ancestry of the working directory.
961
968
962 .. container:: verbose
969 .. container:: verbose
963
970
964 If you use --outgoing, this command will abort if there are ambiguous
971 If you use --outgoing, this command will abort if there are ambiguous
965 outgoing revisions. For example, if there are multiple branches
972 outgoing revisions. For example, if there are multiple branches
966 containing outgoing revisions.
973 containing outgoing revisions.
967
974
968 Use "min(outgoing() and ::.)" or similar revset specification
975 Use "min(outgoing() and ::.)" or similar revset specification
969 instead of --outgoing to specify edit target revision exactly in
976 instead of --outgoing to specify edit target revision exactly in
970 such ambiguous situation. See :hg:`help revsets` for detail about
977 such ambiguous situation. See :hg:`help revsets` for detail about
971 selecting revisions.
978 selecting revisions.
972
979
973 .. container:: verbose
980 .. container:: verbose
974
981
975 Examples:
982 Examples:
976
983
977 - A number of changes have been made.
984 - A number of changes have been made.
978 Revision 3 is no longer needed.
985 Revision 3 is no longer needed.
979
986
980 Start history editing from revision 3::
987 Start history editing from revision 3::
981
988
982 hg histedit -r 3
989 hg histedit -r 3
983
990
984 An editor opens, containing the list of revisions,
991 An editor opens, containing the list of revisions,
985 with specific actions specified::
992 with specific actions specified::
986
993
987 pick 5339bf82f0ca 3 Zworgle the foobar
994 pick 5339bf82f0ca 3 Zworgle the foobar
988 pick 8ef592ce7cc4 4 Bedazzle the zerlog
995 pick 8ef592ce7cc4 4 Bedazzle the zerlog
989 pick 0a9639fcda9d 5 Morgify the cromulancy
996 pick 0a9639fcda9d 5 Morgify the cromulancy
990
997
991 Additional information about the possible actions
998 Additional information about the possible actions
992 to take appears below the list of revisions.
999 to take appears below the list of revisions.
993
1000
994 To remove revision 3 from the history,
1001 To remove revision 3 from the history,
995 its action (at the beginning of the relevant line)
1002 its action (at the beginning of the relevant line)
996 is changed to 'drop'::
1003 is changed to 'drop'::
997
1004
998 drop 5339bf82f0ca 3 Zworgle the foobar
1005 drop 5339bf82f0ca 3 Zworgle the foobar
999 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1006 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1000 pick 0a9639fcda9d 5 Morgify the cromulancy
1007 pick 0a9639fcda9d 5 Morgify the cromulancy
1001
1008
1002 - A number of changes have been made.
1009 - A number of changes have been made.
1003 Revision 2 and 4 need to be swapped.
1010 Revision 2 and 4 need to be swapped.
1004
1011
1005 Start history editing from revision 2::
1012 Start history editing from revision 2::
1006
1013
1007 hg histedit -r 2
1014 hg histedit -r 2
1008
1015
1009 An editor opens, containing the list of revisions,
1016 An editor opens, containing the list of revisions,
1010 with specific actions specified::
1017 with specific actions specified::
1011
1018
1012 pick 252a1af424ad 2 Blorb a morgwazzle
1019 pick 252a1af424ad 2 Blorb a morgwazzle
1013 pick 5339bf82f0ca 3 Zworgle the foobar
1020 pick 5339bf82f0ca 3 Zworgle the foobar
1014 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1021 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1015
1022
1016 To swap revision 2 and 4, its lines are swapped
1023 To swap revision 2 and 4, its lines are swapped
1017 in the editor::
1024 in the editor::
1018
1025
1019 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1026 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1020 pick 5339bf82f0ca 3 Zworgle the foobar
1027 pick 5339bf82f0ca 3 Zworgle the foobar
1021 pick 252a1af424ad 2 Blorb a morgwazzle
1028 pick 252a1af424ad 2 Blorb a morgwazzle
1022
1029
1023 Returns 0 on success, 1 if user intervention is required (not only
1030 Returns 0 on success, 1 if user intervention is required (not only
1024 for intentional "edit" command, but also for resolving unexpected
1031 for intentional "edit" command, but also for resolving unexpected
1025 conflicts).
1032 conflicts).
1026 """
1033 """
1027 state = histeditstate(repo)
1034 state = histeditstate(repo)
1028 try:
1035 try:
1029 state.wlock = repo.wlock()
1036 state.wlock = repo.wlock()
1030 state.lock = repo.lock()
1037 state.lock = repo.lock()
1031 _histedit(ui, repo, state, *freeargs, **opts)
1038 _histedit(ui, repo, state, *freeargs, **opts)
1032 finally:
1039 finally:
1033 release(state.lock, state.wlock)
1040 release(state.lock, state.wlock)
1034
1041
1035 goalcontinue = 'continue'
1042 goalcontinue = 'continue'
1036 goalabort = 'abort'
1043 goalabort = 'abort'
1037 goaleditplan = 'edit-plan'
1044 goaleditplan = 'edit-plan'
1038 goalnew = 'new'
1045 goalnew = 'new'
1039
1046
1040 def _getgoal(opts):
1047 def _getgoal(opts):
1041 if opts.get('continue'):
1048 if opts.get('continue'):
1042 return goalcontinue
1049 return goalcontinue
1043 if opts.get('abort'):
1050 if opts.get('abort'):
1044 return goalabort
1051 return goalabort
1045 if opts.get('edit_plan'):
1052 if opts.get('edit_plan'):
1046 return goaleditplan
1053 return goaleditplan
1047 return goalnew
1054 return goalnew
1048
1055
1049 def _readfile(ui, path):
1056 def _readfile(ui, path):
1050 if path == '-':
1057 if path == '-':
1051 with ui.timeblockedsection('histedit'):
1058 with ui.timeblockedsection('histedit'):
1052 return ui.fin.read()
1059 return ui.fin.read()
1053 else:
1060 else:
1054 with open(path, 'rb') as f:
1061 with open(path, 'rb') as f:
1055 return f.read()
1062 return f.read()
1056
1063
1057 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1064 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1058 # TODO only abort if we try to histedit mq patches, not just
1065 # TODO only abort if we try to histedit mq patches, not just
1059 # blanket if mq patches are applied somewhere
1066 # blanket if mq patches are applied somewhere
1060 mq = getattr(repo, 'mq', None)
1067 mq = getattr(repo, 'mq', None)
1061 if mq and mq.applied:
1068 if mq and mq.applied:
1062 raise error.Abort(_('source has mq patches applied'))
1069 raise error.Abort(_('source has mq patches applied'))
1063
1070
1064 # basic argument incompatibility processing
1071 # basic argument incompatibility processing
1065 outg = opts.get('outgoing')
1072 outg = opts.get('outgoing')
1066 editplan = opts.get('edit_plan')
1073 editplan = opts.get('edit_plan')
1067 abort = opts.get('abort')
1074 abort = opts.get('abort')
1068 force = opts.get('force')
1075 force = opts.get('force')
1069 if force and not outg:
1076 if force and not outg:
1070 raise error.Abort(_('--force only allowed with --outgoing'))
1077 raise error.Abort(_('--force only allowed with --outgoing'))
1071 if goal == 'continue':
1078 if goal == 'continue':
1072 if any((outg, abort, revs, freeargs, rules, editplan)):
1079 if any((outg, abort, revs, freeargs, rules, editplan)):
1073 raise error.Abort(_('no arguments allowed with --continue'))
1080 raise error.Abort(_('no arguments allowed with --continue'))
1074 elif goal == 'abort':
1081 elif goal == 'abort':
1075 if any((outg, revs, freeargs, rules, editplan)):
1082 if any((outg, revs, freeargs, rules, editplan)):
1076 raise error.Abort(_('no arguments allowed with --abort'))
1083 raise error.Abort(_('no arguments allowed with --abort'))
1077 elif goal == 'edit-plan':
1084 elif goal == 'edit-plan':
1078 if any((outg, revs, freeargs)):
1085 if any((outg, revs, freeargs)):
1079 raise error.Abort(_('only --commands argument allowed with '
1086 raise error.Abort(_('only --commands argument allowed with '
1080 '--edit-plan'))
1087 '--edit-plan'))
1081 else:
1088 else:
1082 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1089 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1083 raise error.Abort(_('history edit already in progress, try '
1090 raise error.Abort(_('history edit already in progress, try '
1084 '--continue or --abort'))
1091 '--continue or --abort'))
1085 if outg:
1092 if outg:
1086 if revs:
1093 if revs:
1087 raise error.Abort(_('no revisions allowed with --outgoing'))
1094 raise error.Abort(_('no revisions allowed with --outgoing'))
1088 if len(freeargs) > 1:
1095 if len(freeargs) > 1:
1089 raise error.Abort(
1096 raise error.Abort(
1090 _('only one repo argument allowed with --outgoing'))
1097 _('only one repo argument allowed with --outgoing'))
1091 else:
1098 else:
1092 revs.extend(freeargs)
1099 revs.extend(freeargs)
1093 if len(revs) == 0:
1100 if len(revs) == 0:
1094 defaultrev = destutil.desthistedit(ui, repo)
1101 defaultrev = destutil.desthistedit(ui, repo)
1095 if defaultrev is not None:
1102 if defaultrev is not None:
1096 revs.append(defaultrev)
1103 revs.append(defaultrev)
1097
1104
1098 if len(revs) != 1:
1105 if len(revs) != 1:
1099 raise error.Abort(
1106 raise error.Abort(
1100 _('histedit requires exactly one ancestor revision'))
1107 _('histedit requires exactly one ancestor revision'))
1101
1108
1102 def _histedit(ui, repo, state, *freeargs, **opts):
1109 def _histedit(ui, repo, state, *freeargs, **opts):
1103 opts = pycompat.byteskwargs(opts)
1110 opts = pycompat.byteskwargs(opts)
1104 fm = ui.formatter('histedit', opts)
1111 fm = ui.formatter('histedit', opts)
1105 fm.startitem()
1112 fm.startitem()
1106 goal = _getgoal(opts)
1113 goal = _getgoal(opts)
1107 revs = opts.get('rev', [])
1114 revs = opts.get('rev', [])
1108 rules = opts.get('commands', '')
1115 rules = opts.get('commands', '')
1109 state.keep = opts.get('keep', False)
1116 state.keep = opts.get('keep', False)
1110
1117
1111 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1118 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1112
1119
1113 # rebuild state
1120 # rebuild state
1114 if goal == goalcontinue:
1121 if goal == goalcontinue:
1115 state.read()
1122 state.read()
1116 state = bootstrapcontinue(ui, state, opts)
1123 state = bootstrapcontinue(ui, state, opts)
1117 elif goal == goaleditplan:
1124 elif goal == goaleditplan:
1118 _edithisteditplan(ui, repo, state, rules)
1125 _edithisteditplan(ui, repo, state, rules)
1119 return
1126 return
1120 elif goal == goalabort:
1127 elif goal == goalabort:
1121 _aborthistedit(ui, repo, state)
1128 _aborthistedit(ui, repo, state)
1122 return
1129 return
1123 else:
1130 else:
1124 # goal == goalnew
1131 # goal == goalnew
1125 _newhistedit(ui, repo, state, revs, freeargs, opts)
1132 _newhistedit(ui, repo, state, revs, freeargs, opts)
1126
1133
1127 _continuehistedit(ui, repo, state)
1134 _continuehistedit(ui, repo, state)
1128 _finishhistedit(ui, repo, state, fm)
1135 _finishhistedit(ui, repo, state, fm)
1129 fm.end()
1136 fm.end()
1130
1137
1131 def _continuehistedit(ui, repo, state):
1138 def _continuehistedit(ui, repo, state):
1132 """This function runs after either:
1139 """This function runs after either:
1133 - bootstrapcontinue (if the goal is 'continue')
1140 - bootstrapcontinue (if the goal is 'continue')
1134 - _newhistedit (if the goal is 'new')
1141 - _newhistedit (if the goal is 'new')
1135 """
1142 """
1136 # preprocess rules so that we can hide inner folds from the user
1143 # preprocess rules so that we can hide inner folds from the user
1137 # and only show one editor
1144 # and only show one editor
1138 actions = state.actions[:]
1145 actions = state.actions[:]
1139 for idx, (action, nextact) in enumerate(
1146 for idx, (action, nextact) in enumerate(
1140 zip(actions, actions[1:] + [None])):
1147 zip(actions, actions[1:] + [None])):
1141 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1148 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1142 state.actions[idx].__class__ = _multifold
1149 state.actions[idx].__class__ = _multifold
1143
1150
1144 # Force an initial state file write, so the user can run --abort/continue
1151 # Force an initial state file write, so the user can run --abort/continue
1145 # even if there's an exception before the first transaction serialize.
1152 # even if there's an exception before the first transaction serialize.
1146 state.write()
1153 state.write()
1147
1154
1148 total = len(state.actions)
1155 total = len(state.actions)
1149 pos = 0
1156 pos = 0
1150 tr = None
1157 tr = None
1151 # Don't use singletransaction by default since it rolls the entire
1158 # Don't use singletransaction by default since it rolls the entire
1152 # transaction back if an unexpected exception happens (like a
1159 # transaction back if an unexpected exception happens (like a
1153 # pretxncommit hook throws, or the user aborts the commit msg editor).
1160 # pretxncommit hook throws, or the user aborts the commit msg editor).
1154 if ui.configbool("histedit", "singletransaction"):
1161 if ui.configbool("histedit", "singletransaction"):
1155 # Don't use a 'with' for the transaction, since actions may close
1162 # Don't use a 'with' for the transaction, since actions may close
1156 # and reopen a transaction. For example, if the action executes an
1163 # and reopen a transaction. For example, if the action executes an
1157 # external process it may choose to commit the transaction first.
1164 # external process it may choose to commit the transaction first.
1158 tr = repo.transaction('histedit')
1165 tr = repo.transaction('histedit')
1159 with util.acceptintervention(tr):
1166 with util.acceptintervention(tr):
1160 while state.actions:
1167 while state.actions:
1161 state.write(tr=tr)
1168 state.write(tr=tr)
1162 actobj = state.actions[0]
1169 actobj = state.actions[0]
1163 pos += 1
1170 pos += 1
1164 ui.progress(_("editing"), pos, actobj.torule(),
1171 ui.progress(_("editing"), pos, actobj.torule(),
1165 _('changes'), total)
1172 _('changes'), total)
1166 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1173 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1167 actobj.torule()))
1174 actobj.torule()))
1168 parentctx, replacement_ = actobj.run()
1175 parentctx, replacement_ = actobj.run()
1169 state.parentctxnode = parentctx.node()
1176 state.parentctxnode = parentctx.node()
1170 state.replacements.extend(replacement_)
1177 state.replacements.extend(replacement_)
1171 state.actions.pop(0)
1178 state.actions.pop(0)
1172
1179
1173 state.write()
1180 state.write()
1174 ui.progress(_("editing"), None)
1181 ui.progress(_("editing"), None)
1175
1182
1176 def _finishhistedit(ui, repo, state, fm):
1183 def _finishhistedit(ui, repo, state, fm):
1177 """This action runs when histedit is finishing its session"""
1184 """This action runs when histedit is finishing its session"""
1178 repo.ui.pushbuffer()
1185 repo.ui.pushbuffer()
1179 hg.update(repo, state.parentctxnode, quietempty=True)
1186 hg.update(repo, state.parentctxnode, quietempty=True)
1180 repo.ui.popbuffer()
1187 repo.ui.popbuffer()
1181
1188
1182 mapping, tmpnodes, created, ntm = processreplacement(state)
1189 mapping, tmpnodes, created, ntm = processreplacement(state)
1183 if mapping:
1190 if mapping:
1184 for prec, succs in mapping.iteritems():
1191 for prec, succs in mapping.iteritems():
1185 if not succs:
1192 if not succs:
1186 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1193 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1187 else:
1194 else:
1188 ui.debug('histedit: %s is replaced by %s\n' % (
1195 ui.debug('histedit: %s is replaced by %s\n' % (
1189 node.short(prec), node.short(succs[0])))
1196 node.short(prec), node.short(succs[0])))
1190 if len(succs) > 1:
1197 if len(succs) > 1:
1191 m = 'histedit: %s'
1198 m = 'histedit: %s'
1192 for n in succs[1:]:
1199 for n in succs[1:]:
1193 ui.debug(m % node.short(n))
1200 ui.debug(m % node.short(n))
1194
1201
1195 if not state.keep:
1202 if not state.keep:
1196 if mapping:
1203 if mapping:
1197 movetopmostbookmarks(repo, state.topmost, ntm)
1204 movetopmostbookmarks(repo, state.topmost, ntm)
1198 # TODO update mq state
1205 # TODO update mq state
1199 else:
1206 else:
1200 mapping = {}
1207 mapping = {}
1201
1208
1202 for n in tmpnodes:
1209 for n in tmpnodes:
1203 mapping[n] = ()
1210 mapping[n] = ()
1204
1211
1205 # remove entries about unknown nodes
1212 # remove entries about unknown nodes
1206 nodemap = repo.unfiltered().changelog.nodemap
1213 nodemap = repo.unfiltered().changelog.nodemap
1207 mapping = {k: v for k, v in mapping.items()
1214 mapping = {k: v for k, v in mapping.items()
1208 if k in nodemap and all(n in nodemap for n in v)}
1215 if k in nodemap and all(n in nodemap for n in v)}
1209 scmutil.cleanupnodes(repo, mapping, 'histedit')
1216 scmutil.cleanupnodes(repo, mapping, 'histedit')
1210 hf = fm.hexfunc
1217 hf = fm.hexfunc
1211 fl = fm.formatlist
1218 fl = fm.formatlist
1212 fd = fm.formatdict
1219 fd = fm.formatdict
1213 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1220 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1214 for oldn, newn in mapping.iteritems()},
1221 for oldn, newn in mapping.iteritems()},
1215 key="oldnode", value="newnodes")
1222 key="oldnode", value="newnodes")
1216 fm.data(nodechanges=nodechanges)
1223 fm.data(nodechanges=nodechanges)
1217
1224
1218 state.clear()
1225 state.clear()
1219 if os.path.exists(repo.sjoin('undo')):
1226 if os.path.exists(repo.sjoin('undo')):
1220 os.unlink(repo.sjoin('undo'))
1227 os.unlink(repo.sjoin('undo'))
1221 if repo.vfs.exists('histedit-last-edit.txt'):
1228 if repo.vfs.exists('histedit-last-edit.txt'):
1222 repo.vfs.unlink('histedit-last-edit.txt')
1229 repo.vfs.unlink('histedit-last-edit.txt')
1223
1230
1224 def _aborthistedit(ui, repo, state):
1231 def _aborthistedit(ui, repo, state):
1225 try:
1232 try:
1226 state.read()
1233 state.read()
1227 __, leafs, tmpnodes, __ = processreplacement(state)
1234 __, leafs, tmpnodes, __ = processreplacement(state)
1228 ui.debug('restore wc to old parent %s\n'
1235 ui.debug('restore wc to old parent %s\n'
1229 % node.short(state.topmost))
1236 % node.short(state.topmost))
1230
1237
1231 # Recover our old commits if necessary
1238 # Recover our old commits if necessary
1232 if not state.topmost in repo and state.backupfile:
1239 if not state.topmost in repo and state.backupfile:
1233 backupfile = repo.vfs.join(state.backupfile)
1240 backupfile = repo.vfs.join(state.backupfile)
1234 f = hg.openpath(ui, backupfile)
1241 f = hg.openpath(ui, backupfile)
1235 gen = exchange.readbundle(ui, f, backupfile)
1242 gen = exchange.readbundle(ui, f, backupfile)
1236 with repo.transaction('histedit.abort') as tr:
1243 with repo.transaction('histedit.abort') as tr:
1237 bundle2.applybundle(repo, gen, tr, source='histedit',
1244 bundle2.applybundle(repo, gen, tr, source='histedit',
1238 url='bundle:' + backupfile)
1245 url='bundle:' + backupfile)
1239
1246
1240 os.remove(backupfile)
1247 os.remove(backupfile)
1241
1248
1242 # check whether we should update away
1249 # check whether we should update away
1243 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1250 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1244 state.parentctxnode, leafs | tmpnodes):
1251 state.parentctxnode, leafs | tmpnodes):
1245 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1252 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1246 cleanupnode(ui, repo, tmpnodes)
1253 cleanupnode(ui, repo, tmpnodes)
1247 cleanupnode(ui, repo, leafs)
1254 cleanupnode(ui, repo, leafs)
1248 except Exception:
1255 except Exception:
1249 if state.inprogress():
1256 if state.inprogress():
1250 ui.warn(_('warning: encountered an exception during histedit '
1257 ui.warn(_('warning: encountered an exception during histedit '
1251 '--abort; the repository may not have been completely '
1258 '--abort; the repository may not have been completely '
1252 'cleaned up\n'))
1259 'cleaned up\n'))
1253 raise
1260 raise
1254 finally:
1261 finally:
1255 state.clear()
1262 state.clear()
1256
1263
1257 def _edithisteditplan(ui, repo, state, rules):
1264 def _edithisteditplan(ui, repo, state, rules):
1258 state.read()
1265 state.read()
1259 if not rules:
1266 if not rules:
1260 comment = geteditcomment(ui,
1267 comment = geteditcomment(ui,
1261 node.short(state.parentctxnode),
1268 node.short(state.parentctxnode),
1262 node.short(state.topmost))
1269 node.short(state.topmost))
1263 rules = ruleeditor(repo, ui, state.actions, comment)
1270 rules = ruleeditor(repo, ui, state.actions, comment)
1264 else:
1271 else:
1265 rules = _readfile(ui, rules)
1272 rules = _readfile(ui, rules)
1266 actions = parserules(rules, state)
1273 actions = parserules(rules, state)
1267 ctxs = [repo[act.node] \
1274 ctxs = [repo[act.node] \
1268 for act in state.actions if act.node]
1275 for act in state.actions if act.node]
1269 warnverifyactions(ui, repo, actions, state, ctxs)
1276 warnverifyactions(ui, repo, actions, state, ctxs)
1270 state.actions = actions
1277 state.actions = actions
1271 state.write()
1278 state.write()
1272
1279
1273 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1280 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1274 outg = opts.get('outgoing')
1281 outg = opts.get('outgoing')
1275 rules = opts.get('commands', '')
1282 rules = opts.get('commands', '')
1276 force = opts.get('force')
1283 force = opts.get('force')
1277
1284
1278 cmdutil.checkunfinished(repo)
1285 cmdutil.checkunfinished(repo)
1279 cmdutil.bailifchanged(repo)
1286 cmdutil.bailifchanged(repo)
1280
1287
1281 topmost, empty = repo.dirstate.parents()
1288 topmost, empty = repo.dirstate.parents()
1282 if outg:
1289 if outg:
1283 if freeargs:
1290 if freeargs:
1284 remote = freeargs[0]
1291 remote = freeargs[0]
1285 else:
1292 else:
1286 remote = None
1293 remote = None
1287 root = findoutgoing(ui, repo, remote, force, opts)
1294 root = findoutgoing(ui, repo, remote, force, opts)
1288 else:
1295 else:
1289 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1296 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1290 if len(rr) != 1:
1297 if len(rr) != 1:
1291 raise error.Abort(_('The specified revisions must have '
1298 raise error.Abort(_('The specified revisions must have '
1292 'exactly one common root'))
1299 'exactly one common root'))
1293 root = rr[0].node()
1300 root = rr[0].node()
1294
1301
1295 revs = between(repo, root, topmost, state.keep)
1302 revs = between(repo, root, topmost, state.keep)
1296 if not revs:
1303 if not revs:
1297 raise error.Abort(_('%s is not an ancestor of working directory') %
1304 raise error.Abort(_('%s is not an ancestor of working directory') %
1298 node.short(root))
1305 node.short(root))
1299
1306
1300 ctxs = [repo[r] for r in revs]
1307 ctxs = [repo[r] for r in revs]
1301 if not rules:
1308 if not rules:
1302 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1309 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1303 actions = [pick(state, r) for r in revs]
1310 actions = [pick(state, r) for r in revs]
1304 rules = ruleeditor(repo, ui, actions, comment)
1311 rules = ruleeditor(repo, ui, actions, comment)
1305 else:
1312 else:
1306 rules = _readfile(ui, rules)
1313 rules = _readfile(ui, rules)
1307 actions = parserules(rules, state)
1314 actions = parserules(rules, state)
1308 warnverifyactions(ui, repo, actions, state, ctxs)
1315 warnverifyactions(ui, repo, actions, state, ctxs)
1309
1316
1310 parentctxnode = repo[root].parents()[0].node()
1317 parentctxnode = repo[root].parents()[0].node()
1311
1318
1312 state.parentctxnode = parentctxnode
1319 state.parentctxnode = parentctxnode
1313 state.actions = actions
1320 state.actions = actions
1314 state.topmost = topmost
1321 state.topmost = topmost
1315 state.replacements = []
1322 state.replacements = []
1316
1323
1317 ui.log("histedit", "%d actions to histedit", len(actions),
1324 ui.log("histedit", "%d actions to histedit", len(actions),
1318 histedit_num_actions=len(actions))
1325 histedit_num_actions=len(actions))
1319
1326
1320 # Create a backup so we can always abort completely.
1327 # Create a backup so we can always abort completely.
1321 backupfile = None
1328 backupfile = None
1322 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1329 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1323 backupfile = repair.backupbundle(repo, [parentctxnode],
1330 backupfile = repair.backupbundle(repo, [parentctxnode],
1324 [topmost], root, 'histedit')
1331 [topmost], root, 'histedit')
1325 state.backupfile = backupfile
1332 state.backupfile = backupfile
1326
1333
1327 def _getsummary(ctx):
1334 def _getsummary(ctx):
1328 # a common pattern is to extract the summary but default to the empty
1335 # a common pattern is to extract the summary but default to the empty
1329 # string
1336 # string
1330 summary = ctx.description() or ''
1337 summary = ctx.description() or ''
1331 if summary:
1338 if summary:
1332 summary = summary.splitlines()[0]
1339 summary = summary.splitlines()[0]
1333 return summary
1340 return summary
1334
1341
1335 def bootstrapcontinue(ui, state, opts):
1342 def bootstrapcontinue(ui, state, opts):
1336 repo = state.repo
1343 repo = state.repo
1337
1344
1338 ms = mergemod.mergestate.read(repo)
1345 ms = mergemod.mergestate.read(repo)
1339 mergeutil.checkunresolved(ms)
1346 mergeutil.checkunresolved(ms)
1340
1347
1341 if state.actions:
1348 if state.actions:
1342 actobj = state.actions.pop(0)
1349 actobj = state.actions.pop(0)
1343
1350
1344 if _isdirtywc(repo):
1351 if _isdirtywc(repo):
1345 actobj.continuedirty()
1352 actobj.continuedirty()
1346 if _isdirtywc(repo):
1353 if _isdirtywc(repo):
1347 abortdirty()
1354 abortdirty()
1348
1355
1349 parentctx, replacements = actobj.continueclean()
1356 parentctx, replacements = actobj.continueclean()
1350
1357
1351 state.parentctxnode = parentctx.node()
1358 state.parentctxnode = parentctx.node()
1352 state.replacements.extend(replacements)
1359 state.replacements.extend(replacements)
1353
1360
1354 return state
1361 return state
1355
1362
1356 def between(repo, old, new, keep):
1363 def between(repo, old, new, keep):
1357 """select and validate the set of revision to edit
1364 """select and validate the set of revision to edit
1358
1365
1359 When keep is false, the specified set can't have children."""
1366 When keep is false, the specified set can't have children."""
1360 revs = repo.revs('%n::%n', old, new)
1367 revs = repo.revs('%n::%n', old, new)
1361 if revs and not keep:
1368 if revs and not keep:
1362 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1369 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1363 repo.revs('(%ld::) - (%ld)', revs, revs)):
1370 repo.revs('(%ld::) - (%ld)', revs, revs)):
1364 raise error.Abort(_('can only histedit a changeset together '
1371 raise error.Abort(_('can only histedit a changeset together '
1365 'with all its descendants'))
1372 'with all its descendants'))
1366 if repo.revs('(%ld) and merge()', revs):
1373 if repo.revs('(%ld) and merge()', revs):
1367 raise error.Abort(_('cannot edit history that contains merges'))
1374 raise error.Abort(_('cannot edit history that contains merges'))
1368 root = repo[revs.first()] # list is already sorted by repo.revs()
1375 root = repo[revs.first()] # list is already sorted by repo.revs()
1369 if not root.mutable():
1376 if not root.mutable():
1370 raise error.Abort(_('cannot edit public changeset: %s') % root,
1377 raise error.Abort(_('cannot edit public changeset: %s') % root,
1371 hint=_("see 'hg help phases' for details"))
1378 hint=_("see 'hg help phases' for details"))
1372 return pycompat.maplist(repo.changelog.node, revs)
1379 return pycompat.maplist(repo.changelog.node, revs)
1373
1380
1374 def ruleeditor(repo, ui, actions, editcomment=""):
1381 def ruleeditor(repo, ui, actions, editcomment=""):
1375 """open an editor to edit rules
1382 """open an editor to edit rules
1376
1383
1377 rules are in the format [ [act, ctx], ...] like in state.rules
1384 rules are in the format [ [act, ctx], ...] like in state.rules
1378 """
1385 """
1379 if repo.ui.configbool("experimental", "histedit.autoverb"):
1386 if repo.ui.configbool("experimental", "histedit.autoverb"):
1380 newact = util.sortdict()
1387 newact = util.sortdict()
1381 for act in actions:
1388 for act in actions:
1382 ctx = repo[act.node]
1389 ctx = repo[act.node]
1383 summary = _getsummary(ctx)
1390 summary = _getsummary(ctx)
1384 fword = summary.split(' ', 1)[0].lower()
1391 fword = summary.split(' ', 1)[0].lower()
1385 added = False
1392 added = False
1386
1393
1387 # if it doesn't end with the special character '!' just skip this
1394 # if it doesn't end with the special character '!' just skip this
1388 if fword.endswith('!'):
1395 if fword.endswith('!'):
1389 fword = fword[:-1]
1396 fword = fword[:-1]
1390 if fword in primaryactions | secondaryactions | tertiaryactions:
1397 if fword in primaryactions | secondaryactions | tertiaryactions:
1391 act.verb = fword
1398 act.verb = fword
1392 # get the target summary
1399 # get the target summary
1393 tsum = summary[len(fword) + 1:].lstrip()
1400 tsum = summary[len(fword) + 1:].lstrip()
1394 # safe but slow: reverse iterate over the actions so we
1401 # safe but slow: reverse iterate over the actions so we
1395 # don't clash on two commits having the same summary
1402 # don't clash on two commits having the same summary
1396 for na, l in reversed(list(newact.iteritems())):
1403 for na, l in reversed(list(newact.iteritems())):
1397 actx = repo[na.node]
1404 actx = repo[na.node]
1398 asum = _getsummary(actx)
1405 asum = _getsummary(actx)
1399 if asum == tsum:
1406 if asum == tsum:
1400 added = True
1407 added = True
1401 l.append(act)
1408 l.append(act)
1402 break
1409 break
1403
1410
1404 if not added:
1411 if not added:
1405 newact[act] = []
1412 newact[act] = []
1406
1413
1407 # copy over and flatten the new list
1414 # copy over and flatten the new list
1408 actions = []
1415 actions = []
1409 for na, l in newact.iteritems():
1416 for na, l in newact.iteritems():
1410 actions.append(na)
1417 actions.append(na)
1411 actions += l
1418 actions += l
1412
1419
1413 rules = '\n'.join([act.torule() for act in actions])
1420 rules = '\n'.join([act.torule() for act in actions])
1414 rules += '\n\n'
1421 rules += '\n\n'
1415 rules += editcomment
1422 rules += editcomment
1416 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1423 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1417 repopath=repo.path, action='histedit')
1424 repopath=repo.path, action='histedit')
1418
1425
1419 # Save edit rules in .hg/histedit-last-edit.txt in case
1426 # Save edit rules in .hg/histedit-last-edit.txt in case
1420 # the user needs to ask for help after something
1427 # the user needs to ask for help after something
1421 # surprising happens.
1428 # surprising happens.
1422 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
1429 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
1423 f.write(rules)
1430 f.write(rules)
1424
1431
1425 return rules
1432 return rules
1426
1433
1427 def parserules(rules, state):
1434 def parserules(rules, state):
1428 """Read the histedit rules string and return list of action objects """
1435 """Read the histedit rules string and return list of action objects """
1429 rules = [l for l in (r.strip() for r in rules.splitlines())
1436 rules = [l for l in (r.strip() for r in rules.splitlines())
1430 if l and not l.startswith('#')]
1437 if l and not l.startswith('#')]
1431 actions = []
1438 actions = []
1432 for r in rules:
1439 for r in rules:
1433 if ' ' not in r:
1440 if ' ' not in r:
1434 raise error.ParseError(_('malformed line "%s"') % r)
1441 raise error.ParseError(_('malformed line "%s"') % r)
1435 verb, rest = r.split(' ', 1)
1442 verb, rest = r.split(' ', 1)
1436
1443
1437 if verb not in actiontable:
1444 if verb not in actiontable:
1438 raise error.ParseError(_('unknown action "%s"') % verb)
1445 raise error.ParseError(_('unknown action "%s"') % verb)
1439
1446
1440 action = actiontable[verb].fromrule(state, rest)
1447 action = actiontable[verb].fromrule(state, rest)
1441 actions.append(action)
1448 actions.append(action)
1442 return actions
1449 return actions
1443
1450
1444 def warnverifyactions(ui, repo, actions, state, ctxs):
1451 def warnverifyactions(ui, repo, actions, state, ctxs):
1445 try:
1452 try:
1446 verifyactions(actions, state, ctxs)
1453 verifyactions(actions, state, ctxs)
1447 except error.ParseError:
1454 except error.ParseError:
1448 if repo.vfs.exists('histedit-last-edit.txt'):
1455 if repo.vfs.exists('histedit-last-edit.txt'):
1449 ui.warn(_('warning: histedit rules saved '
1456 ui.warn(_('warning: histedit rules saved '
1450 'to: .hg/histedit-last-edit.txt\n'))
1457 'to: .hg/histedit-last-edit.txt\n'))
1451 raise
1458 raise
1452
1459
1453 def verifyactions(actions, state, ctxs):
1460 def verifyactions(actions, state, ctxs):
1454 """Verify that there exists exactly one action per given changeset and
1461 """Verify that there exists exactly one action per given changeset and
1455 other constraints.
1462 other constraints.
1456
1463
1457 Will abort if there are to many or too few rules, a malformed rule,
1464 Will abort if there are to many or too few rules, a malformed rule,
1458 or a rule on a changeset outside of the user-given range.
1465 or a rule on a changeset outside of the user-given range.
1459 """
1466 """
1460 expected = set(c.node() for c in ctxs)
1467 expected = set(c.node() for c in ctxs)
1461 seen = set()
1468 seen = set()
1462 prev = None
1469 prev = None
1463
1470
1464 if actions and actions[0].verb in ['roll', 'fold']:
1471 if actions and actions[0].verb in ['roll', 'fold']:
1465 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1472 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1466 actions[0].verb)
1473 actions[0].verb)
1467
1474
1468 for action in actions:
1475 for action in actions:
1469 action.verify(prev, expected, seen)
1476 action.verify(prev, expected, seen)
1470 prev = action
1477 prev = action
1471 if action.node is not None:
1478 if action.node is not None:
1472 seen.add(action.node)
1479 seen.add(action.node)
1473 missing = sorted(expected - seen) # sort to stabilize output
1480 missing = sorted(expected - seen) # sort to stabilize output
1474
1481
1475 if state.repo.ui.configbool('histedit', 'dropmissing'):
1482 if state.repo.ui.configbool('histedit', 'dropmissing'):
1476 if len(actions) == 0:
1483 if len(actions) == 0:
1477 raise error.ParseError(_('no rules provided'),
1484 raise error.ParseError(_('no rules provided'),
1478 hint=_('use strip extension to remove commits'))
1485 hint=_('use strip extension to remove commits'))
1479
1486
1480 drops = [drop(state, n) for n in missing]
1487 drops = [drop(state, n) for n in missing]
1481 # put the in the beginning so they execute immediately and
1488 # put the in the beginning so they execute immediately and
1482 # don't show in the edit-plan in the future
1489 # don't show in the edit-plan in the future
1483 actions[:0] = drops
1490 actions[:0] = drops
1484 elif missing:
1491 elif missing:
1485 raise error.ParseError(_('missing rules for changeset %s') %
1492 raise error.ParseError(_('missing rules for changeset %s') %
1486 node.short(missing[0]),
1493 node.short(missing[0]),
1487 hint=_('use "drop %s" to discard, see also: '
1494 hint=_('use "drop %s" to discard, see also: '
1488 "'hg help -e histedit.config'")
1495 "'hg help -e histedit.config'")
1489 % node.short(missing[0]))
1496 % node.short(missing[0]))
1490
1497
1491 def adjustreplacementsfrommarkers(repo, oldreplacements):
1498 def adjustreplacementsfrommarkers(repo, oldreplacements):
1492 """Adjust replacements from obsolescence markers
1499 """Adjust replacements from obsolescence markers
1493
1500
1494 Replacements structure is originally generated based on
1501 Replacements structure is originally generated based on
1495 histedit's state and does not account for changes that are
1502 histedit's state and does not account for changes that are
1496 not recorded there. This function fixes that by adding
1503 not recorded there. This function fixes that by adding
1497 data read from obsolescence markers"""
1504 data read from obsolescence markers"""
1498 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1505 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1499 return oldreplacements
1506 return oldreplacements
1500
1507
1501 unfi = repo.unfiltered()
1508 unfi = repo.unfiltered()
1502 nm = unfi.changelog.nodemap
1509 nm = unfi.changelog.nodemap
1503 obsstore = repo.obsstore
1510 obsstore = repo.obsstore
1504 newreplacements = list(oldreplacements)
1511 newreplacements = list(oldreplacements)
1505 oldsuccs = [r[1] for r in oldreplacements]
1512 oldsuccs = [r[1] for r in oldreplacements]
1506 # successors that have already been added to succstocheck once
1513 # successors that have already been added to succstocheck once
1507 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1514 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1508 succstocheck = list(seensuccs)
1515 succstocheck = list(seensuccs)
1509 while succstocheck:
1516 while succstocheck:
1510 n = succstocheck.pop()
1517 n = succstocheck.pop()
1511 missing = nm.get(n) is None
1518 missing = nm.get(n) is None
1512 markers = obsstore.successors.get(n, ())
1519 markers = obsstore.successors.get(n, ())
1513 if missing and not markers:
1520 if missing and not markers:
1514 # dead end, mark it as such
1521 # dead end, mark it as such
1515 newreplacements.append((n, ()))
1522 newreplacements.append((n, ()))
1516 for marker in markers:
1523 for marker in markers:
1517 nsuccs = marker[1]
1524 nsuccs = marker[1]
1518 newreplacements.append((n, nsuccs))
1525 newreplacements.append((n, nsuccs))
1519 for nsucc in nsuccs:
1526 for nsucc in nsuccs:
1520 if nsucc not in seensuccs:
1527 if nsucc not in seensuccs:
1521 seensuccs.add(nsucc)
1528 seensuccs.add(nsucc)
1522 succstocheck.append(nsucc)
1529 succstocheck.append(nsucc)
1523
1530
1524 return newreplacements
1531 return newreplacements
1525
1532
1526 def processreplacement(state):
1533 def processreplacement(state):
1527 """process the list of replacements to return
1534 """process the list of replacements to return
1528
1535
1529 1) the final mapping between original and created nodes
1536 1) the final mapping between original and created nodes
1530 2) the list of temporary node created by histedit
1537 2) the list of temporary node created by histedit
1531 3) the list of new commit created by histedit"""
1538 3) the list of new commit created by histedit"""
1532 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1539 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1533 allsuccs = set()
1540 allsuccs = set()
1534 replaced = set()
1541 replaced = set()
1535 fullmapping = {}
1542 fullmapping = {}
1536 # initialize basic set
1543 # initialize basic set
1537 # fullmapping records all operations recorded in replacement
1544 # fullmapping records all operations recorded in replacement
1538 for rep in replacements:
1545 for rep in replacements:
1539 allsuccs.update(rep[1])
1546 allsuccs.update(rep[1])
1540 replaced.add(rep[0])
1547 replaced.add(rep[0])
1541 fullmapping.setdefault(rep[0], set()).update(rep[1])
1548 fullmapping.setdefault(rep[0], set()).update(rep[1])
1542 new = allsuccs - replaced
1549 new = allsuccs - replaced
1543 tmpnodes = allsuccs & replaced
1550 tmpnodes = allsuccs & replaced
1544 # Reduce content fullmapping into direct relation between original nodes
1551 # Reduce content fullmapping into direct relation between original nodes
1545 # and final node created during history edition
1552 # and final node created during history edition
1546 # Dropped changeset are replaced by an empty list
1553 # Dropped changeset are replaced by an empty list
1547 toproceed = set(fullmapping)
1554 toproceed = set(fullmapping)
1548 final = {}
1555 final = {}
1549 while toproceed:
1556 while toproceed:
1550 for x in list(toproceed):
1557 for x in list(toproceed):
1551 succs = fullmapping[x]
1558 succs = fullmapping[x]
1552 for s in list(succs):
1559 for s in list(succs):
1553 if s in toproceed:
1560 if s in toproceed:
1554 # non final node with unknown closure
1561 # non final node with unknown closure
1555 # We can't process this now
1562 # We can't process this now
1556 break
1563 break
1557 elif s in final:
1564 elif s in final:
1558 # non final node, replace with closure
1565 # non final node, replace with closure
1559 succs.remove(s)
1566 succs.remove(s)
1560 succs.update(final[s])
1567 succs.update(final[s])
1561 else:
1568 else:
1562 final[x] = succs
1569 final[x] = succs
1563 toproceed.remove(x)
1570 toproceed.remove(x)
1564 # remove tmpnodes from final mapping
1571 # remove tmpnodes from final mapping
1565 for n in tmpnodes:
1572 for n in tmpnodes:
1566 del final[n]
1573 del final[n]
1567 # we expect all changes involved in final to exist in the repo
1574 # we expect all changes involved in final to exist in the repo
1568 # turn `final` into list (topologically sorted)
1575 # turn `final` into list (topologically sorted)
1569 nm = state.repo.changelog.nodemap
1576 nm = state.repo.changelog.nodemap
1570 for prec, succs in final.items():
1577 for prec, succs in final.items():
1571 final[prec] = sorted(succs, key=nm.get)
1578 final[prec] = sorted(succs, key=nm.get)
1572
1579
1573 # computed topmost element (necessary for bookmark)
1580 # computed topmost element (necessary for bookmark)
1574 if new:
1581 if new:
1575 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1582 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1576 elif not final:
1583 elif not final:
1577 # Nothing rewritten at all. we won't need `newtopmost`
1584 # Nothing rewritten at all. we won't need `newtopmost`
1578 # It is the same as `oldtopmost` and `processreplacement` know it
1585 # It is the same as `oldtopmost` and `processreplacement` know it
1579 newtopmost = None
1586 newtopmost = None
1580 else:
1587 else:
1581 # every body died. The newtopmost is the parent of the root.
1588 # every body died. The newtopmost is the parent of the root.
1582 r = state.repo.changelog.rev
1589 r = state.repo.changelog.rev
1583 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1590 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1584
1591
1585 return final, tmpnodes, new, newtopmost
1592 return final, tmpnodes, new, newtopmost
1586
1593
1587 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1594 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1588 """Move bookmark from oldtopmost to newly created topmost
1595 """Move bookmark from oldtopmost to newly created topmost
1589
1596
1590 This is arguably a feature and we may only want that for the active
1597 This is arguably a feature and we may only want that for the active
1591 bookmark. But the behavior is kept compatible with the old version for now.
1598 bookmark. But the behavior is kept compatible with the old version for now.
1592 """
1599 """
1593 if not oldtopmost or not newtopmost:
1600 if not oldtopmost or not newtopmost:
1594 return
1601 return
1595 oldbmarks = repo.nodebookmarks(oldtopmost)
1602 oldbmarks = repo.nodebookmarks(oldtopmost)
1596 if oldbmarks:
1603 if oldbmarks:
1597 with repo.lock(), repo.transaction('histedit') as tr:
1604 with repo.lock(), repo.transaction('histedit') as tr:
1598 marks = repo._bookmarks
1605 marks = repo._bookmarks
1599 changes = []
1606 changes = []
1600 for name in oldbmarks:
1607 for name in oldbmarks:
1601 changes.append((name, newtopmost))
1608 changes.append((name, newtopmost))
1602 marks.applychanges(repo, tr, changes)
1609 marks.applychanges(repo, tr, changes)
1603
1610
1604 def cleanupnode(ui, repo, nodes):
1611 def cleanupnode(ui, repo, nodes):
1605 """strip a group of nodes from the repository
1612 """strip a group of nodes from the repository
1606
1613
1607 The set of node to strip may contains unknown nodes."""
1614 The set of node to strip may contains unknown nodes."""
1608 with repo.lock():
1615 with repo.lock():
1609 # do not let filtering get in the way of the cleanse
1616 # do not let filtering get in the way of the cleanse
1610 # we should probably get rid of obsolescence marker created during the
1617 # we should probably get rid of obsolescence marker created during the
1611 # histedit, but we currently do not have such information.
1618 # histedit, but we currently do not have such information.
1612 repo = repo.unfiltered()
1619 repo = repo.unfiltered()
1613 # Find all nodes that need to be stripped
1620 # Find all nodes that need to be stripped
1614 # (we use %lr instead of %ln to silently ignore unknown items)
1621 # (we use %lr instead of %ln to silently ignore unknown items)
1615 nm = repo.changelog.nodemap
1622 nm = repo.changelog.nodemap
1616 nodes = sorted(n for n in nodes if n in nm)
1623 nodes = sorted(n for n in nodes if n in nm)
1617 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1624 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1618 if roots:
1625 if roots:
1619 repair.strip(ui, repo, roots)
1626 repair.strip(ui, repo, roots)
1620
1627
1621 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1628 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1622 if isinstance(nodelist, str):
1629 if isinstance(nodelist, str):
1623 nodelist = [nodelist]
1630 nodelist = [nodelist]
1624 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1631 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1625 state = histeditstate(repo)
1632 state = histeditstate(repo)
1626 state.read()
1633 state.read()
1627 histedit_nodes = {action.node for action
1634 histedit_nodes = {action.node for action
1628 in state.actions if action.node}
1635 in state.actions if action.node}
1629 common_nodes = histedit_nodes & set(nodelist)
1636 common_nodes = histedit_nodes & set(nodelist)
1630 if common_nodes:
1637 if common_nodes:
1631 raise error.Abort(_("histedit in progress, can't strip %s")
1638 raise error.Abort(_("histedit in progress, can't strip %s")
1632 % ', '.join(node.short(x) for x in common_nodes))
1639 % ', '.join(node.short(x) for x in common_nodes))
1633 return orig(ui, repo, nodelist, *args, **kwargs)
1640 return orig(ui, repo, nodelist, *args, **kwargs)
1634
1641
1635 extensions.wrapfunction(repair, 'strip', stripwrapper)
1642 extensions.wrapfunction(repair, 'strip', stripwrapper)
1636
1643
1637 def summaryhook(ui, repo):
1644 def summaryhook(ui, repo):
1638 if not os.path.exists(repo.vfs.join('histedit-state')):
1645 if not os.path.exists(repo.vfs.join('histedit-state')):
1639 return
1646 return
1640 state = histeditstate(repo)
1647 state = histeditstate(repo)
1641 state.read()
1648 state.read()
1642 if state.actions:
1649 if state.actions:
1643 # i18n: column positioning for "hg summary"
1650 # i18n: column positioning for "hg summary"
1644 ui.write(_('hist: %s (histedit --continue)\n') %
1651 ui.write(_('hist: %s (histedit --continue)\n') %
1645 (ui.label(_('%d remaining'), 'histedit.remaining') %
1652 (ui.label(_('%d remaining'), 'histedit.remaining') %
1646 len(state.actions)))
1653 len(state.actions)))
1647
1654
1648 def extsetup(ui):
1655 def extsetup(ui):
1649 cmdutil.summaryhooks.add('histedit', summaryhook)
1656 cmdutil.summaryhooks.add('histedit', summaryhook)
1650 cmdutil.unfinishedstates.append(
1657 cmdutil.unfinishedstates.append(
1651 ['histedit-state', False, True, _('histedit in progress'),
1658 ['histedit-state', False, True, _('histedit in progress'),
1652 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1659 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1653 cmdutil.afterresolvedstates.append(
1660 cmdutil.afterresolvedstates.append(
1654 ['histedit-state', _('hg histedit --continue')])
1661 ['histedit-state', _('hg histedit --continue')])
@@ -1,554 +1,590 b''
1 Test argument handling and various data parsing
1 Test argument handling and various data parsing
2 ==================================================
2 ==================================================
3
3
4
4
5 Enable extensions used by this test.
5 Enable extensions used by this test.
6 $ cat >>$HGRCPATH <<EOF
6 $ cat >>$HGRCPATH <<EOF
7 > [extensions]
7 > [extensions]
8 > histedit=
8 > histedit=
9 > EOF
9 > EOF
10
10
11 Repo setup.
11 Repo setup.
12 $ hg init foo
12 $ hg init foo
13 $ cd foo
13 $ cd foo
14 $ echo alpha >> alpha
14 $ echo alpha >> alpha
15 $ hg addr
15 $ hg addr
16 adding alpha
16 adding alpha
17 $ hg ci -m one
17 $ hg ci -m one
18 $ echo alpha >> alpha
18 $ echo alpha >> alpha
19 $ hg ci -m two
19 $ hg ci -m two
20 $ echo alpha >> alpha
20 $ echo alpha >> alpha
21 $ hg ci -m three
21 $ hg ci -m three
22 $ echo alpha >> alpha
22 $ echo alpha >> alpha
23 $ hg ci -m four
23 $ hg ci -m four
24 $ echo alpha >> alpha
24 $ echo alpha >> alpha
25 $ hg ci -m five
25 $ hg ci -m five
26
26
27 $ hg log --style compact --graph
27 $ hg log --style compact --graph
28 @ 4[tip] 08d98a8350f3 1970-01-01 00:00 +0000 test
28 @ 4[tip] 08d98a8350f3 1970-01-01 00:00 +0000 test
29 | five
29 | five
30 |
30 |
31 o 3 c8e68270e35a 1970-01-01 00:00 +0000 test
31 o 3 c8e68270e35a 1970-01-01 00:00 +0000 test
32 | four
32 | four
33 |
33 |
34 o 2 eb57da33312f 1970-01-01 00:00 +0000 test
34 o 2 eb57da33312f 1970-01-01 00:00 +0000 test
35 | three
35 | three
36 |
36 |
37 o 1 579e40513370 1970-01-01 00:00 +0000 test
37 o 1 579e40513370 1970-01-01 00:00 +0000 test
38 | two
38 | two
39 |
39 |
40 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
40 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
41 one
41 one
42
42
43
43
44 histedit --continue/--abort with no existing state
44 histedit --continue/--abort with no existing state
45 --------------------------------------------------
45 --------------------------------------------------
46
46
47 $ hg histedit --continue
47 $ hg histedit --continue
48 abort: no histedit in progress
48 abort: no histedit in progress
49 [255]
49 [255]
50 $ hg histedit --abort
50 $ hg histedit --abort
51 abort: no histedit in progress
51 abort: no histedit in progress
52 [255]
52 [255]
53
53
54 Run a dummy edit to make sure we get tip^^ correctly via revsingle.
54 Run a dummy edit to make sure we get tip^^ correctly via revsingle.
55 --------------------------------------------------------------------
55 --------------------------------------------------------------------
56
56
57 $ HGEDITOR=cat hg histedit "tip^^"
57 $ HGEDITOR=cat hg histedit "tip^^"
58 pick eb57da33312f 2 three
58 pick eb57da33312f 2 three
59 pick c8e68270e35a 3 four
59 pick c8e68270e35a 3 four
60 pick 08d98a8350f3 4 five
60 pick 08d98a8350f3 4 five
61
61
62 # Edit history between eb57da33312f and 08d98a8350f3
62 # Edit history between eb57da33312f and 08d98a8350f3
63 #
63 #
64 # Commits are listed from least to most recent
64 # Commits are listed from least to most recent
65 #
65 #
66 # You can reorder changesets by reordering the lines
66 # You can reorder changesets by reordering the lines
67 #
67 #
68 # Commands:
68 # Commands:
69 #
69 #
70 # e, edit = use commit, but stop for amending
70 # e, edit = use commit, but stop for amending
71 # m, mess = edit commit message without changing commit content
71 # m, mess = edit commit message without changing commit content
72 # p, pick = use commit
72 # p, pick = use commit
73 # b, base = checkout changeset and apply further changesets from there
73 # b, base = checkout changeset and apply further changesets from there
74 # d, drop = remove commit from history
74 # d, drop = remove commit from history
75 # f, fold = use commit, but combine it with the one above
75 # f, fold = use commit, but combine it with the one above
76 # r, roll = like fold, but discard this commit's description and date
76 # r, roll = like fold, but discard this commit's description and date
77 #
77 #
78
78
79 Run on a revision not ancestors of the current working directory.
79 Run on a revision not ancestors of the current working directory.
80 --------------------------------------------------------------------
80 --------------------------------------------------------------------
81
81
82 $ hg up 2
82 $ hg up 2
83 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
83 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
84 $ hg histedit -r 4
84 $ hg histedit -r 4
85 abort: 08d98a8350f3 is not an ancestor of working directory
85 abort: 08d98a8350f3 is not an ancestor of working directory
86 [255]
86 [255]
87 $ hg up --quiet
87 $ hg up --quiet
88
88
89
89
90 Test that we pick the minimum of a revrange
90 Test that we pick the minimum of a revrange
91 ---------------------------------------
91 ---------------------------------------
92
92
93 $ HGEDITOR=cat hg histedit '2::' --commands - << EOF
93 $ HGEDITOR=cat hg histedit '2::' --commands - << EOF
94 > pick eb57da33312f 2 three
94 > pick eb57da33312f 2 three
95 > pick c8e68270e35a 3 four
95 > pick c8e68270e35a 3 four
96 > pick 08d98a8350f3 4 five
96 > pick 08d98a8350f3 4 five
97 > EOF
97 > EOF
98 $ hg up --quiet
98 $ hg up --quiet
99
99
100 $ HGEDITOR=cat hg histedit 'tip:2' --commands - << EOF
100 $ HGEDITOR=cat hg histedit 'tip:2' --commands - << EOF
101 > pick eb57da33312f 2 three
101 > pick eb57da33312f 2 three
102 > pick c8e68270e35a 3 four
102 > pick c8e68270e35a 3 four
103 > pick 08d98a8350f3 4 five
103 > pick 08d98a8350f3 4 five
104 > EOF
104 > EOF
105 $ hg up --quiet
105 $ hg up --quiet
106
106
107 Test config specified default
107 Test config specified default
108 -----------------------------
108 -----------------------------
109
109
110 $ HGEDITOR=cat hg histedit --config "histedit.defaultrev=only(.) - ::eb57da33312f" --commands - << EOF
110 $ HGEDITOR=cat hg histedit --config "histedit.defaultrev=only(.) - ::eb57da33312f" --commands - << EOF
111 > pick c8e68270e35a 3 four
111 > pick c8e68270e35a 3 four
112 > pick 08d98a8350f3 4 five
112 > pick 08d98a8350f3 4 five
113 > EOF
113 > EOF
114
114
115 Run on a revision not descendants of the initial parent
115 Run on a revision not descendants of the initial parent
116 --------------------------------------------------------------------
116 --------------------------------------------------------------------
117
117
118 Test the message shown for inconsistent histedit state, which may be
118 Test the message shown for inconsistent histedit state, which may be
119 created (and forgotten) by Mercurial earlier than 2.7. This emulates
119 created (and forgotten) by Mercurial earlier than 2.7. This emulates
120 Mercurial earlier than 2.7 by renaming ".hg/histedit-state"
120 Mercurial earlier than 2.7 by renaming ".hg/histedit-state"
121 temporarily.
121 temporarily.
122
122
123 $ hg log -G -T '{rev} {shortest(node)} {desc}\n' -r 2::
123 $ hg log -G -T '{rev} {shortest(node)} {desc}\n' -r 2::
124 @ 4 08d9 five
124 @ 4 08d9 five
125 |
125 |
126 o 3 c8e6 four
126 o 3 c8e6 four
127 |
127 |
128 o 2 eb57 three
128 o 2 eb57 three
129 |
129 |
130 ~
130 ~
131 $ HGEDITOR=cat hg histedit -r 4 --commands - << EOF
131 $ HGEDITOR=cat hg histedit -r 4 --commands - << EOF
132 > edit 08d98a8350f3 4 five
132 > edit 08d98a8350f3 4 five
133 > EOF
133 > EOF
134 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
134 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
135 reverting alpha
135 reverting alpha
136 Editing (08d98a8350f3), you may commit or record as needed now.
136 Editing (08d98a8350f3), you may commit or record as needed now.
137 (hg histedit --continue to resume)
137 (hg histedit --continue to resume)
138 [1]
138 [1]
139
139
140 $ hg graft --continue
140 $ hg graft --continue
141 abort: no graft in progress
141 abort: no graft in progress
142 (continue: hg histedit --continue)
142 (continue: hg histedit --continue)
143 [255]
143 [255]
144
144
145 $ mv .hg/histedit-state .hg/histedit-state.back
145 $ mv .hg/histedit-state .hg/histedit-state.back
146 $ hg update --quiet --clean 2
146 $ hg update --quiet --clean 2
147 $ echo alpha >> alpha
147 $ echo alpha >> alpha
148 $ mv .hg/histedit-state.back .hg/histedit-state
148 $ mv .hg/histedit-state.back .hg/histedit-state
149
149
150 $ hg histedit --continue
150 $ hg histedit --continue
151 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/08d98a8350f3-02594089-histedit.hg
151 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/08d98a8350f3-02594089-histedit.hg
152 $ hg log -G -T '{rev} {shortest(node)} {desc}\n' -r 2::
152 $ hg log -G -T '{rev} {shortest(node)} {desc}\n' -r 2::
153 @ 4 f5ed five
153 @ 4 f5ed five
154 |
154 |
155 | o 3 c8e6 four
155 | o 3 c8e6 four
156 |/
156 |/
157 o 2 eb57 three
157 o 2 eb57 three
158 |
158 |
159 ~
159 ~
160
160
161 $ hg unbundle -q $TESTTMP/foo/.hg/strip-backup/08d98a8350f3-02594089-histedit.hg
161 $ hg unbundle -q $TESTTMP/foo/.hg/strip-backup/08d98a8350f3-02594089-histedit.hg
162 $ hg strip -q -r f5ed --config extensions.strip=
162 $ hg strip -q -r f5ed --config extensions.strip=
163 $ hg up -q 08d98a8350f3
163 $ hg up -q 08d98a8350f3
164
164
165 Test that missing revisions are detected
165 Test that missing revisions are detected
166 ---------------------------------------
166 ---------------------------------------
167
167
168 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
168 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
169 > pick eb57da33312f 2 three
169 > pick eb57da33312f 2 three
170 > pick 08d98a8350f3 4 five
170 > pick 08d98a8350f3 4 five
171 > EOF
171 > EOF
172 hg: parse error: missing rules for changeset c8e68270e35a
172 hg: parse error: missing rules for changeset c8e68270e35a
173 (use "drop c8e68270e35a" to discard, see also: 'hg help -e histedit.config')
173 (use "drop c8e68270e35a" to discard, see also: 'hg help -e histedit.config')
174 [255]
174 [255]
175
175
176 Test that extra revisions are detected
176 Test that extra revisions are detected
177 ---------------------------------------
177 ---------------------------------------
178
178
179 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
179 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
180 > pick 6058cbb6cfd7 0 one
180 > pick 6058cbb6cfd7 0 one
181 > pick c8e68270e35a 3 four
181 > pick c8e68270e35a 3 four
182 > pick 08d98a8350f3 4 five
182 > pick 08d98a8350f3 4 five
183 > EOF
183 > EOF
184 hg: parse error: pick "6058cbb6cfd7" changeset was not a candidate
184 hg: parse error: pick "6058cbb6cfd7" changeset was not a candidate
185 (only use listed changesets)
185 (only use listed changesets)
186 [255]
186 [255]
187
187
188 Test malformed line
188 Test malformed line
189 ---------------------------------------
189 ---------------------------------------
190
190
191 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
191 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
192 > pickeb57da33312f2three
192 > pickeb57da33312f2three
193 > pick c8e68270e35a 3 four
193 > pick c8e68270e35a 3 four
194 > pick 08d98a8350f3 4 five
194 > pick 08d98a8350f3 4 five
195 > EOF
195 > EOF
196 hg: parse error: malformed line "pickeb57da33312f2three"
196 hg: parse error: malformed line "pickeb57da33312f2three"
197 [255]
197 [255]
198
198
199 Test unknown changeset
199 Test unknown changeset
200 ---------------------------------------
200 ---------------------------------------
201
201
202 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
202 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
203 > pick 0123456789ab 2 three
203 > pick 0123456789ab 2 three
204 > pick c8e68270e35a 3 four
204 > pick c8e68270e35a 3 four
205 > pick 08d98a8350f3 4 five
205 > pick 08d98a8350f3 4 five
206 > EOF
206 > EOF
207 hg: parse error: unknown changeset 0123456789ab listed
207 hg: parse error: unknown changeset 0123456789ab listed
208 [255]
208 [255]
209
209
210 Test unknown command
210 Test unknown command
211 ---------------------------------------
211 ---------------------------------------
212
212
213 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
213 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
214 > coin eb57da33312f 2 three
214 > coin eb57da33312f 2 three
215 > pick c8e68270e35a 3 four
215 > pick c8e68270e35a 3 four
216 > pick 08d98a8350f3 4 five
216 > pick 08d98a8350f3 4 five
217 > EOF
217 > EOF
218 hg: parse error: unknown action "coin"
218 hg: parse error: unknown action "coin"
219 [255]
219 [255]
220
220
221 Test duplicated changeset
221 Test duplicated changeset
222 ---------------------------------------
222 ---------------------------------------
223
223
224 So one is missing and one appear twice.
224 So one is missing and one appear twice.
225
225
226 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
226 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
227 > pick eb57da33312f 2 three
227 > pick eb57da33312f 2 three
228 > pick eb57da33312f 2 three
228 > pick eb57da33312f 2 three
229 > pick 08d98a8350f3 4 five
229 > pick 08d98a8350f3 4 five
230 > EOF
230 > EOF
231 hg: parse error: duplicated command for changeset eb57da33312f
231 hg: parse error: duplicated command for changeset eb57da33312f
232 [255]
232 [255]
233
233
234 Test bogus rev
234 Test bogus rev
235 ---------------------------------------
235 ---------------------------------------
236
236
237 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
237 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
238 > pick eb57da33312f 2 three
238 > pick eb57da33312f 2 three
239 > pick 0
239 > pick 0u98
240 > pick 08d98a8350f3 4 five
240 > pick 08d98a8350f3 4 five
241 > EOF
241 > EOF
242 hg: parse error: invalid changeset 0
242 hg: parse error: invalid changeset 0u98
243 [255]
243 [255]
244
244
245 Test short version of command
245 Test short version of command
246 ---------------------------------------
246 ---------------------------------------
247
247
248 Note: we use varying amounts of white space between command name and changeset
248 Note: we use varying amounts of white space between command name and changeset
249 short hash. This tests issue3893.
249 short hash. This tests issue3893.
250
250
251 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
251 $ HGEDITOR=cat hg histedit "tip^^" --commands - << EOF
252 > pick eb57da33312f 2 three
252 > pick eb57da33312f 2 three
253 > p c8e68270e35a 3 four
253 > p c8e68270e35a 3 four
254 > f 08d98a8350f3 4 five
254 > f 08d98a8350f3 4 five
255 > EOF
255 > EOF
256 four
256 four
257 ***
257 ***
258 five
258 five
259
259
260
260
261
261
262 HG: Enter commit message. Lines beginning with 'HG:' are removed.
262 HG: Enter commit message. Lines beginning with 'HG:' are removed.
263 HG: Leave message empty to abort commit.
263 HG: Leave message empty to abort commit.
264 HG: --
264 HG: --
265 HG: user: test
265 HG: user: test
266 HG: branch 'default'
266 HG: branch 'default'
267 HG: changed alpha
267 HG: changed alpha
268 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/c8e68270e35a-63d8b8d8-histedit.hg
268 saved backup bundle to $TESTTMP/foo/.hg/strip-backup/c8e68270e35a-63d8b8d8-histedit.hg
269
269
270 $ hg update -q 2
270 $ hg update -q 2
271 $ echo x > x
271 $ echo x > x
272 $ hg add x
272 $ hg add x
273 $ hg commit -m'x' x
273 $ hg commit -m'x' x
274 created new head
274 created new head
275 $ hg histedit -r 'heads(all())'
275 $ hg histedit -r 'heads(all())'
276 abort: The specified revisions must have exactly one common root
276 abort: The specified revisions must have exactly one common root
277 [255]
277 [255]
278
278
279 Test that trimming description using multi-byte characters
279 Test that trimming description using multi-byte characters
280 --------------------------------------------------------------------
280 --------------------------------------------------------------------
281
281
282 $ $PYTHON <<EOF
282 $ $PYTHON <<EOF
283 > fp = open('logfile', 'wb')
283 > fp = open('logfile', 'wb')
284 > fp.write(b'12345678901234567890123456789012345678901234567890' +
284 > fp.write(b'12345678901234567890123456789012345678901234567890' +
285 > b'12345') # there are 5 more columns for 80 columns
285 > b'12345') # there are 5 more columns for 80 columns
286 >
286 >
287 > # 2 x 4 = 8 columns, but 3 x 4 = 12 bytes
287 > # 2 x 4 = 8 columns, but 3 x 4 = 12 bytes
288 > fp.write(u'\u3042\u3044\u3046\u3048'.encode('utf-8'))
288 > fp.write(u'\u3042\u3044\u3046\u3048'.encode('utf-8'))
289 >
289 >
290 > fp.close()
290 > fp.close()
291 > EOF
291 > EOF
292 $ echo xx >> x
292 $ echo xx >> x
293 $ hg --encoding utf-8 commit --logfile logfile
293 $ hg --encoding utf-8 commit --logfile logfile
294
294
295 $ HGEDITOR=cat hg --encoding utf-8 histedit tip
295 $ HGEDITOR=cat hg --encoding utf-8 histedit tip
296 pick 3d3ea1f3a10b 5 1234567890123456789012345678901234567890123456789012345\xe3\x81\x82... (esc)
296 pick 3d3ea1f3a10b 5 1234567890123456789012345678901234567890123456789012345\xe3\x81\x82... (esc)
297
297
298 # Edit history between 3d3ea1f3a10b and 3d3ea1f3a10b
298 # Edit history between 3d3ea1f3a10b and 3d3ea1f3a10b
299 #
299 #
300 # Commits are listed from least to most recent
300 # Commits are listed from least to most recent
301 #
301 #
302 # You can reorder changesets by reordering the lines
302 # You can reorder changesets by reordering the lines
303 #
303 #
304 # Commands:
304 # Commands:
305 #
305 #
306 # e, edit = use commit, but stop for amending
306 # e, edit = use commit, but stop for amending
307 # m, mess = edit commit message without changing commit content
307 # m, mess = edit commit message without changing commit content
308 # p, pick = use commit
308 # p, pick = use commit
309 # b, base = checkout changeset and apply further changesets from there
309 # b, base = checkout changeset and apply further changesets from there
310 # d, drop = remove commit from history
310 # d, drop = remove commit from history
311 # f, fold = use commit, but combine it with the one above
311 # f, fold = use commit, but combine it with the one above
312 # r, roll = like fold, but discard this commit's description and date
312 # r, roll = like fold, but discard this commit's description and date
313 #
313 #
314
314
315 Test --continue with --keep
315 Test --continue with --keep
316
316
317 $ hg strip -q -r . --config extensions.strip=
317 $ hg strip -q -r . --config extensions.strip=
318 $ hg histedit '.^' -q --keep --commands - << EOF
318 $ hg histedit '.^' -q --keep --commands - << EOF
319 > edit eb57da33312f 2 three
319 > edit eb57da33312f 2 three
320 > pick f3cfcca30c44 4 x
320 > pick f3cfcca30c44 4 x
321 > EOF
321 > EOF
322 Editing (eb57da33312f), you may commit or record as needed now.
322 Editing (eb57da33312f), you may commit or record as needed now.
323 (hg histedit --continue to resume)
323 (hg histedit --continue to resume)
324 [1]
324 [1]
325 $ echo edit >> alpha
325 $ echo edit >> alpha
326 $ hg histedit -q --continue
326 $ hg histedit -q --continue
327 $ hg log -G -T '{rev}:{node|short} {desc}'
327 $ hg log -G -T '{rev}:{node|short} {desc}'
328 @ 6:8fda0c726bf2 x
328 @ 6:8fda0c726bf2 x
329 |
329 |
330 o 5:63379946892c three
330 o 5:63379946892c three
331 |
331 |
332 | o 4:f3cfcca30c44 x
332 | o 4:f3cfcca30c44 x
333 | |
333 | |
334 | | o 3:2a30f3cfee78 four
334 | | o 3:2a30f3cfee78 four
335 | |/ ***
335 | |/ ***
336 | | five
336 | | five
337 | o 2:eb57da33312f three
337 | o 2:eb57da33312f three
338 |/
338 |/
339 o 1:579e40513370 two
339 o 1:579e40513370 two
340 |
340 |
341 o 0:6058cbb6cfd7 one
341 o 0:6058cbb6cfd7 one
342
342
343
343
344 Test that abort fails gracefully on exception
344 Test that abort fails gracefully on exception
345 ----------------------------------------------
345 ----------------------------------------------
346 $ hg histedit . -q --commands - << EOF
346 $ hg histedit . -q --commands - << EOF
347 > edit 8fda0c726bf2 6 x
347 > edit 8fda0c726bf2 6 x
348 > EOF
348 > EOF
349 Editing (8fda0c726bf2), you may commit or record as needed now.
349 Editing (8fda0c726bf2), you may commit or record as needed now.
350 (hg histedit --continue to resume)
350 (hg histedit --continue to resume)
351 [1]
351 [1]
352 Corrupt histedit state file
352 Corrupt histedit state file
353 $ sed 's/8fda0c726bf2/123456789012/' .hg/histedit-state > ../corrupt-histedit
353 $ sed 's/8fda0c726bf2/123456789012/' .hg/histedit-state > ../corrupt-histedit
354 $ mv ../corrupt-histedit .hg/histedit-state
354 $ mv ../corrupt-histedit .hg/histedit-state
355 $ hg histedit --abort
355 $ hg histedit --abort
356 warning: encountered an exception during histedit --abort; the repository may not have been completely cleaned up
356 warning: encountered an exception during histedit --abort; the repository may not have been completely cleaned up
357 abort: $TESTTMP/foo/.hg/strip-backup/*-histedit.hg: $ENOENT$ (glob) (windows !)
357 abort: $TESTTMP/foo/.hg/strip-backup/*-histedit.hg: $ENOENT$ (glob) (windows !)
358 abort: $ENOENT$: $TESTTMP/foo/.hg/strip-backup/*-histedit.hg (glob) (no-windows !)
358 abort: $ENOENT$: $TESTTMP/foo/.hg/strip-backup/*-histedit.hg (glob) (no-windows !)
359 [255]
359 [255]
360 Histedit state has been exited
360 Histedit state has been exited
361 $ hg summary -q
361 $ hg summary -q
362 parent: 5:63379946892c
362 parent: 5:63379946892c
363 commit: 1 added, 1 unknown (new branch head)
363 commit: 1 added, 1 unknown (new branch head)
364 update: 4 new changesets (update)
364 update: 4 new changesets (update)
365
365
366 $ cd ..
366 $ cd ..
367
367
368 Set up default base revision tests
368 Set up default base revision tests
369
369
370 $ hg init defaultbase
370 $ hg init defaultbase
371 $ cd defaultbase
371 $ cd defaultbase
372 $ touch foo
372 $ touch foo
373 $ hg -q commit -A -m root
373 $ hg -q commit -A -m root
374 $ echo 1 > foo
374 $ echo 1 > foo
375 $ hg commit -m 'public 1'
375 $ hg commit -m 'public 1'
376 $ hg phase --force --public -r .
376 $ hg phase --force --public -r .
377 $ echo 2 > foo
377 $ echo 2 > foo
378 $ hg commit -m 'draft after public'
378 $ hg commit -m 'draft after public'
379 $ hg -q up -r 1
379 $ hg -q up -r 1
380 $ echo 3 > foo
380 $ echo 3 > foo
381 $ hg commit -m 'head 1 public'
381 $ hg commit -m 'head 1 public'
382 created new head
382 created new head
383 $ hg phase --force --public -r .
383 $ hg phase --force --public -r .
384 $ echo 4 > foo
384 $ echo 4 > foo
385 $ hg commit -m 'head 1 draft 1'
385 $ hg commit -m 'head 1 draft 1'
386 $ echo 5 > foo
386 $ echo 5 > foo
387 $ hg commit -m 'head 1 draft 2'
387 $ hg commit -m 'head 1 draft 2'
388 $ hg -q up -r 2
388 $ hg -q up -r 2
389 $ echo 6 > foo
389 $ echo 6 > foo
390 $ hg commit -m 'head 2 commit 1'
390 $ hg commit -m 'head 2 commit 1'
391 $ echo 7 > foo
391 $ echo 7 > foo
392 $ hg commit -m 'head 2 commit 2'
392 $ hg commit -m 'head 2 commit 2'
393 $ hg -q up -r 2
393 $ hg -q up -r 2
394 $ echo 8 > foo
394 $ echo 8 > foo
395 $ hg commit -m 'head 3'
395 $ hg commit -m 'head 3'
396 created new head
396 created new head
397 $ hg -q up -r 2
397 $ hg -q up -r 2
398 $ echo 9 > foo
398 $ echo 9 > foo
399 $ hg commit -m 'head 4'
399 $ hg commit -m 'head 4'
400 created new head
400 created new head
401 $ hg merge --tool :local -r 8
401 $ hg merge --tool :local -r 8
402 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
402 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
403 (branch merge, don't forget to commit)
403 (branch merge, don't forget to commit)
404 $ hg commit -m 'merge head 3 into head 4'
404 $ hg commit -m 'merge head 3 into head 4'
405 $ echo 11 > foo
405 $ echo 11 > foo
406 $ hg commit -m 'commit 1 after merge'
406 $ hg commit -m 'commit 1 after merge'
407 $ echo 12 > foo
407 $ echo 12 > foo
408 $ hg commit -m 'commit 2 after merge'
408 $ hg commit -m 'commit 2 after merge'
409
409
410 $ hg log -G -T '{rev}:{node|short} {phase} {desc}\n'
410 $ hg log -G -T '{rev}:{node|short} {phase} {desc}\n'
411 @ 12:8cde254db839 draft commit 2 after merge
411 @ 12:8cde254db839 draft commit 2 after merge
412 |
412 |
413 o 11:6f2f0241f119 draft commit 1 after merge
413 o 11:6f2f0241f119 draft commit 1 after merge
414 |
414 |
415 o 10:90506cc76b00 draft merge head 3 into head 4
415 o 10:90506cc76b00 draft merge head 3 into head 4
416 |\
416 |\
417 | o 9:f8607a373a97 draft head 4
417 | o 9:f8607a373a97 draft head 4
418 | |
418 | |
419 o | 8:0da92be05148 draft head 3
419 o | 8:0da92be05148 draft head 3
420 |/
420 |/
421 | o 7:4c35cdf97d5e draft head 2 commit 2
421 | o 7:4c35cdf97d5e draft head 2 commit 2
422 | |
422 | |
423 | o 6:931820154288 draft head 2 commit 1
423 | o 6:931820154288 draft head 2 commit 1
424 |/
424 |/
425 | o 5:8cdc02b9bc63 draft head 1 draft 2
425 | o 5:8cdc02b9bc63 draft head 1 draft 2
426 | |
426 | |
427 | o 4:463b8c0d2973 draft head 1 draft 1
427 | o 4:463b8c0d2973 draft head 1 draft 1
428 | |
428 | |
429 | o 3:23a0c4eefcbf public head 1 public
429 | o 3:23a0c4eefcbf public head 1 public
430 | |
430 | |
431 o | 2:4117331c3abb draft draft after public
431 o | 2:4117331c3abb draft draft after public
432 |/
432 |/
433 o 1:4426d359ea59 public public 1
433 o 1:4426d359ea59 public public 1
434 |
434 |
435 o 0:54136a8ddf32 public root
435 o 0:54136a8ddf32 public root
436
436
437
437
438 Default base revision should stop at public changesets
438 Default base revision should stop at public changesets
439
439
440 $ hg -q up 8cdc02b9bc63
440 $ hg -q up 8cdc02b9bc63
441 $ hg histedit --commands - <<EOF
441 $ hg histedit --commands - <<EOF
442 > pick 463b8c0d2973
442 > pick 463b8c0d2973
443 > pick 8cdc02b9bc63
443 > pick 8cdc02b9bc63
444 > EOF
444 > EOF
445
445
446 Default base revision should stop at branchpoint
446 Default base revision should stop at branchpoint
447
447
448 $ hg -q up 4c35cdf97d5e
448 $ hg -q up 4c35cdf97d5e
449 $ hg histedit --commands - <<EOF
449 $ hg histedit --commands - <<EOF
450 > pick 931820154288
450 > pick 931820154288
451 > pick 4c35cdf97d5e
451 > pick 4c35cdf97d5e
452 > EOF
452 > EOF
453
453
454 Default base revision should stop at merge commit
454 Default base revision should stop at merge commit
455
455
456 $ hg -q up 8cde254db839
456 $ hg -q up 8cde254db839
457 $ hg histedit --commands - <<EOF
457 $ hg histedit --commands - <<EOF
458 > pick 6f2f0241f119
458 > pick 6f2f0241f119
459 > pick 8cde254db839
459 > pick 8cde254db839
460 > EOF
460 > EOF
461
461
462 commit --amend should abort if histedit is in progress
462 commit --amend should abort if histedit is in progress
463 (issue4800) and markers are not being created.
463 (issue4800) and markers are not being created.
464 Eventually, histedit could perhaps look at `source` extra,
464 Eventually, histedit could perhaps look at `source` extra,
465 in which case this test should be revisited.
465 in which case this test should be revisited.
466
466
467 $ hg -q up 8cde254db839
467 $ hg -q up 8cde254db839
468 $ hg histedit 6f2f0241f119 --commands - <<EOF
468 $ hg histedit 6f2f0241f119 --commands - <<EOF
469 > pick 8cde254db839
469 > pick 8cde254db839
470 > edit 6f2f0241f119
470 > edit 6f2f0241f119
471 > EOF
471 > EOF
472 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
472 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
473 merging foo
473 merging foo
474 warning: conflicts while merging foo! (edit, then use 'hg resolve --mark')
474 warning: conflicts while merging foo! (edit, then use 'hg resolve --mark')
475 Fix up the change (pick 8cde254db839)
475 Fix up the change (pick 8cde254db839)
476 (hg histedit --continue to resume)
476 (hg histedit --continue to resume)
477 [1]
477 [1]
478 $ hg resolve -m --all
478 $ hg resolve -m --all
479 (no more unresolved files)
479 (no more unresolved files)
480 continue: hg histedit --continue
480 continue: hg histedit --continue
481 $ hg histedit --cont
481 $ hg histedit --cont
482 merging foo
482 merging foo
483 warning: conflicts while merging foo! (edit, then use 'hg resolve --mark')
483 warning: conflicts while merging foo! (edit, then use 'hg resolve --mark')
484 Editing (6f2f0241f119), you may commit or record as needed now.
484 Editing (6f2f0241f119), you may commit or record as needed now.
485 (hg histedit --continue to resume)
485 (hg histedit --continue to resume)
486 [1]
486 [1]
487 $ hg resolve -m --all
487 $ hg resolve -m --all
488 (no more unresolved files)
488 (no more unresolved files)
489 continue: hg histedit --continue
489 continue: hg histedit --continue
490 $ hg commit --amend -m 'reject this fold'
490 $ hg commit --amend -m 'reject this fold'
491 abort: histedit in progress
491 abort: histedit in progress
492 (use 'hg histedit --continue' or 'hg histedit --abort')
492 (use 'hg histedit --continue' or 'hg histedit --abort')
493 [255]
493 [255]
494
494
495 With markers enabled, histedit does not get confused, and
495 With markers enabled, histedit does not get confused, and
496 amend should not be blocked by the ongoing histedit.
496 amend should not be blocked by the ongoing histedit.
497
497
498 $ cat >>$HGRCPATH <<EOF
498 $ cat >>$HGRCPATH <<EOF
499 > [experimental]
499 > [experimental]
500 > evolution.createmarkers=True
500 > evolution.createmarkers=True
501 > evolution.allowunstable=True
501 > evolution.allowunstable=True
502 > EOF
502 > EOF
503 $ hg commit --amend -m 'allow this fold'
503 $ hg commit --amend -m 'allow this fold'
504 $ hg histedit --continue
504 $ hg histedit --continue
505
505
506 $ cd ..
506 $ cd ..
507
507
508 Test autoverb feature
508 Test autoverb feature
509
509
510 $ hg init autoverb
510 $ hg init autoverb
511 $ cd autoverb
511 $ cd autoverb
512 $ echo alpha >> alpha
512 $ echo alpha >> alpha
513 $ hg ci -qAm one
513 $ hg ci -qAm one
514 $ echo alpha >> alpha
514 $ echo alpha >> alpha
515 $ hg ci -qm two
515 $ hg ci -qm two
516 $ echo beta >> beta
516 $ echo beta >> beta
517 $ hg ci -qAm "roll! one"
517 $ hg ci -qAm "roll! one"
518
518
519 $ hg log --style compact --graph
519 $ hg log --style compact --graph
520 @ 2[tip] 4f34d0f8b5fa 1970-01-01 00:00 +0000 test
520 @ 2[tip] 4f34d0f8b5fa 1970-01-01 00:00 +0000 test
521 | roll! one
521 | roll! one
522 |
522 |
523 o 1 579e40513370 1970-01-01 00:00 +0000 test
523 o 1 579e40513370 1970-01-01 00:00 +0000 test
524 | two
524 | two
525 |
525 |
526 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
526 o 0 6058cbb6cfd7 1970-01-01 00:00 +0000 test
527 one
527 one
528
528
529
529
530 Check that 'roll' is selected by default
530 Check that 'roll' is selected by default
531
531
532 $ HGEDITOR=cat hg histedit 0 --config experimental.histedit.autoverb=True
532 $ HGEDITOR=cat hg histedit 0 --config experimental.histedit.autoverb=True
533 pick 6058cbb6cfd7 0 one
533 pick 6058cbb6cfd7 0 one
534 roll 4f34d0f8b5fa 2 roll! one
534 roll 4f34d0f8b5fa 2 roll! one
535 pick 579e40513370 1 two
535 pick 579e40513370 1 two
536
536
537 # Edit history between 6058cbb6cfd7 and 4f34d0f8b5fa
537 # Edit history between 6058cbb6cfd7 and 4f34d0f8b5fa
538 #
538 #
539 # Commits are listed from least to most recent
539 # Commits are listed from least to most recent
540 #
540 #
541 # You can reorder changesets by reordering the lines
541 # You can reorder changesets by reordering the lines
542 #
542 #
543 # Commands:
543 # Commands:
544 #
544 #
545 # e, edit = use commit, but stop for amending
545 # e, edit = use commit, but stop for amending
546 # m, mess = edit commit message without changing commit content
546 # m, mess = edit commit message without changing commit content
547 # p, pick = use commit
547 # p, pick = use commit
548 # b, base = checkout changeset and apply further changesets from there
548 # b, base = checkout changeset and apply further changesets from there
549 # d, drop = remove commit from history
549 # d, drop = remove commit from history
550 # f, fold = use commit, but combine it with the one above
550 # f, fold = use commit, but combine it with the one above
551 # r, roll = like fold, but discard this commit's description and date
551 # r, roll = like fold, but discard this commit's description and date
552 #
552 #
553
553
554 $ cd ..
554 $ cd ..
555
556 Check that histedit's commands accept revsets
557 $ hg init bar
558 $ cd bar
559 $ echo w >> a
560 $ hg ci -qAm "adds a"
561 $ echo x >> b
562 $ hg ci -qAm "adds b"
563 $ echo y >> c
564 $ hg ci -qAm "adds c"
565 $ echo z >> d
566 $ hg ci -qAm "adds d"
567 $ hg log -G -T '{rev} {desc}\n'
568 @ 3 adds d
569 |
570 o 2 adds c
571 |
572 o 1 adds b
573 |
574 o 0 adds a
575
576 $ HGEDITOR=cat hg histedit "2" --commands - << EOF
577 > base -4 adds c
578 > pick 2 adds c
579 > pick tip adds d
580 > EOF
581 $ hg log -G -T '{rev} {desc}\n'
582 @ 5 adds d
583 |
584 o 4 adds c
585 |
586 | o 1 adds b
587 |/
588 o 0 adds a
589
590
General Comments 0
You need to be logged in to leave comments. Login now