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