##// END OF EJS Templates
histedit: check fold of public change during verify
timeless -
r27542:bf0900d3 default
parent child Browse files
Show More
@@ -1,1436 +1,1452 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
39 # r, roll = like fold, but discard this commit's description
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 #
42 #
43
43
44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 for each revision in your history. For example, if you had meant to add gamma
45 for each revision in your history. For example, if you had meant to add gamma
46 before beta, and then wanted to add delta in the same revision as beta, you
46 before beta, and then wanted to add delta in the same revision as beta, you
47 would reorganize the file to look like this::
47 would reorganize the file to look like this::
48
48
49 pick 030b686bedc4 Add gamma
49 pick 030b686bedc4 Add gamma
50 pick c561b4e977df Add beta
50 pick c561b4e977df Add beta
51 fold 7c2fd3b9020c Add delta
51 fold 7c2fd3b9020c Add delta
52
52
53 # Edit history between c561b4e977df and 7c2fd3b9020c
53 # Edit history between c561b4e977df and 7c2fd3b9020c
54 #
54 #
55 # Commits are listed from least to most recent
55 # Commits are listed from least to most recent
56 #
56 #
57 # Commands:
57 # Commands:
58 # p, pick = use commit
58 # p, pick = use commit
59 # e, edit = use commit, but stop for amending
59 # e, edit = use commit, but stop for amending
60 # f, fold = use commit, but combine it with the one above
60 # f, fold = use commit, but combine it with the one above
61 # r, roll = like fold, but discard this commit's description
61 # r, roll = like fold, but discard this commit's description
62 # d, drop = remove commit from history
62 # d, drop = remove commit from history
63 # m, mess = edit commit message without changing commit content
63 # m, mess = edit commit message without changing commit content
64 #
64 #
65
65
66 At which point you close the editor and ``histedit`` starts working. When you
66 At which point you close the editor and ``histedit`` starts working. When you
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 those revisions together, offering you a chance to clean up the commit message::
68 those revisions together, offering you a chance to clean up the commit message::
69
69
70 Add beta
70 Add beta
71 ***
71 ***
72 Add delta
72 Add delta
73
73
74 Edit the commit message to your liking, then close the editor. For
74 Edit the commit message to your liking, then close the editor. For
75 this example, let's assume that the commit message was changed to
75 this example, let's assume that the commit message was changed to
76 ``Add beta and delta.`` After histedit has run and had a chance to
76 ``Add beta and delta.`` After histedit has run and had a chance to
77 remove any old or temporary revisions it needed, the history looks
77 remove any old or temporary revisions it needed, the history looks
78 like this::
78 like this::
79
79
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 | Add beta and delta.
81 | Add beta and delta.
82 |
82 |
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 | Add gamma
84 | Add gamma
85 |
85 |
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 Add alpha
87 Add alpha
88
88
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 ones) until after it has completed all the editing operations, so it will
90 ones) until after it has completed all the editing operations, so it will
91 probably perform several strip operations when it's done. For the above example,
91 probably perform several strip operations when it's done. For the above example,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 so you might need to be a little patient. You can choose to keep the original
93 so you might need to be a little patient. You can choose to keep the original
94 revisions by passing the ``--keep`` flag.
94 revisions by passing the ``--keep`` flag.
95
95
96 The ``edit`` operation will drop you back to a command prompt,
96 The ``edit`` operation will drop you back to a command prompt,
97 allowing you to edit files freely, or even use ``hg record`` to commit
97 allowing you to edit files freely, or even use ``hg record`` to commit
98 some changes as a separate commit. When you're done, any remaining
98 some changes as a separate commit. When you're done, any remaining
99 uncommitted changes will be committed as well. When done, run ``hg
99 uncommitted changes will be committed as well. When done, run ``hg
100 histedit --continue`` to finish this step. You'll be prompted for a
100 histedit --continue`` to finish this step. You'll be prompted for a
101 new commit message, but the default commit message will be the
101 new commit message, but the default commit message will be the
102 original message for the ``edit`` ed revision.
102 original message for the ``edit`` ed revision.
103
103
104 The ``message`` operation will give you a chance to revise a commit
104 The ``message`` operation will give you a chance to revise a commit
105 message without changing the contents. It's a shortcut for doing
105 message without changing the contents. It's a shortcut for doing
106 ``edit`` immediately followed by `hg histedit --continue``.
106 ``edit`` immediately followed by `hg histedit --continue``.
107
107
108 If ``histedit`` encounters a conflict when moving a revision (while
108 If ``histedit`` encounters a conflict when moving a revision (while
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 ``edit`` with the difference that it won't prompt you for a commit
110 ``edit`` with the difference that it won't prompt you for a commit
111 message when done. If you decide at this point that you don't like how
111 message when done. If you decide at this point that you don't like how
112 much work it will be to rearrange history, or that you made a mistake,
112 much work it will be to rearrange history, or that you made a mistake,
113 you can use ``hg histedit --abort`` to abandon the new changes you
113 you can use ``hg histedit --abort`` to abandon the new changes you
114 have made and return to the state before you attempted to edit your
114 have made and return to the state before you attempted to edit your
115 history.
115 history.
116
116
117 If we clone the histedit-ed example repository above and add four more
117 If we clone the histedit-ed example repository above and add four more
118 changes, such that we have the following history::
118 changes, such that we have the following history::
119
119
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 | Add theta
121 | Add theta
122 |
122 |
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 | Add eta
124 | Add eta
125 |
125 |
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 | Add zeta
127 | Add zeta
128 |
128 |
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 | Add epsilon
130 | Add epsilon
131 |
131 |
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 | Add beta and delta.
133 | Add beta and delta.
134 |
134 |
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 | Add gamma
136 | Add gamma
137 |
137 |
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 Add alpha
139 Add alpha
140
140
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 as running ``hg histedit 836302820282``. If you need plan to push to a
142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 repository that Mercurial does not detect to be related to the source
143 repository that Mercurial does not detect to be related to the source
144 repo, you can add a ``--force`` option.
144 repo, you can add a ``--force`` option.
145
145
146 Config
146 Config
147 ------
147 ------
148
148
149 Histedit rule lines are truncated to 80 characters by default. You
149 Histedit rule lines are truncated to 80 characters by default. You
150 can customize this behavior by setting a different length in your
150 can customize this behavior by setting a different length in your
151 configuration file::
151 configuration file::
152
152
153 [histedit]
153 [histedit]
154 linelen = 120 # truncate rule lines at 120 characters
154 linelen = 120 # truncate rule lines at 120 characters
155
155
156 ``hg histedit`` attempts to automatically choose an appropriate base
156 ``hg histedit`` attempts to automatically choose an appropriate base
157 revision to use. To change which base revision is used, define a
157 revision to use. To change which base revision is used, define a
158 revset in your configuration file::
158 revset in your configuration file::
159
159
160 [histedit]
160 [histedit]
161 defaultrev = only(.) & draft()
161 defaultrev = only(.) & draft()
162
162
163 By default each edited revision needs to be present in histedit commands.
163 By default each edited revision needs to be present in histedit commands.
164 To remove revision you need to use ``drop`` operation. You can configure
164 To remove revision you need to use ``drop`` operation. You can configure
165 the drop to be implicit for missing commits by adding:
165 the drop to be implicit for missing commits by adding:
166
166
167 [histedit]
167 [histedit]
168 dropmissing = True
168 dropmissing = True
169
169
170 """
170 """
171
171
172 import pickle
172 import pickle
173 import errno
173 import errno
174 import os
174 import os
175 import sys
175 import sys
176
176
177 from mercurial import bundle2
177 from mercurial import bundle2
178 from mercurial import cmdutil
178 from mercurial import cmdutil
179 from mercurial import discovery
179 from mercurial import discovery
180 from mercurial import error
180 from mercurial import error
181 from mercurial import copies
181 from mercurial import copies
182 from mercurial import context
182 from mercurial import context
183 from mercurial import destutil
183 from mercurial import destutil
184 from mercurial import exchange
184 from mercurial import exchange
185 from mercurial import extensions
185 from mercurial import extensions
186 from mercurial import hg
186 from mercurial import hg
187 from mercurial import node
187 from mercurial import node
188 from mercurial import repair
188 from mercurial import repair
189 from mercurial import scmutil
189 from mercurial import scmutil
190 from mercurial import util
190 from mercurial import util
191 from mercurial import obsolete
191 from mercurial import obsolete
192 from mercurial import merge as mergemod
192 from mercurial import merge as mergemod
193 from mercurial.lock import release
193 from mercurial.lock import release
194 from mercurial.i18n import _
194 from mercurial.i18n import _
195
195
196 cmdtable = {}
196 cmdtable = {}
197 command = cmdutil.command(cmdtable)
197 command = cmdutil.command(cmdtable)
198
198
199 class _constraints(object):
199 class _constraints(object):
200 # aborts if there are multiple rules for one node
200 # aborts if there are multiple rules for one node
201 noduplicates = 'noduplicates'
201 noduplicates = 'noduplicates'
202 # abort if the node does belong to edited stack
202 # abort if the node does belong to edited stack
203 forceother = 'forceother'
203 forceother = 'forceother'
204 # abort if the node doesn't belong to edited stack
204 # abort if the node doesn't belong to edited stack
205 noother = 'noother'
205 noother = 'noother'
206
206
207 @classmethod
207 @classmethod
208 def known(cls):
208 def known(cls):
209 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
209 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
210
210
211 # Note for extension authors: ONLY specify testedwith = 'internal' for
211 # Note for extension authors: ONLY specify testedwith = 'internal' for
212 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
212 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
213 # be specifying the version(s) of Mercurial they are tested with, or
213 # be specifying the version(s) of Mercurial they are tested with, or
214 # leave the attribute unspecified.
214 # leave the attribute unspecified.
215 testedwith = 'internal'
215 testedwith = 'internal'
216
216
217 # i18n: command names and abbreviations must remain untranslated
217 # i18n: command names and abbreviations must remain untranslated
218 editcomment = _("""# Edit history between %s and %s
218 editcomment = _("""# Edit history between %s and %s
219 #
219 #
220 # Commits are listed from least to most recent
220 # Commits are listed from least to most recent
221 #
221 #
222 # Commands:
222 # Commands:
223 # p, pick = use commit
223 # p, pick = use commit
224 # e, edit = use commit, but stop for amending
224 # e, edit = use commit, but stop for amending
225 # f, fold = use commit, but combine it with the one above
225 # f, fold = use commit, but combine it with the one above
226 # r, roll = like fold, but discard this commit's description
226 # r, roll = like fold, but discard this commit's description
227 # d, drop = remove commit from history
227 # d, drop = remove commit from history
228 # m, mess = edit commit message without changing commit content
228 # m, mess = edit commit message without changing commit content
229 #
229 #
230 """)
230 """)
231
231
232 class histeditstate(object):
232 class histeditstate(object):
233 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
233 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
234 topmost=None, replacements=None, lock=None, wlock=None):
234 topmost=None, replacements=None, lock=None, wlock=None):
235 self.repo = repo
235 self.repo = repo
236 self.actions = actions
236 self.actions = actions
237 self.keep = keep
237 self.keep = keep
238 self.topmost = topmost
238 self.topmost = topmost
239 self.parentctxnode = parentctxnode
239 self.parentctxnode = parentctxnode
240 self.lock = lock
240 self.lock = lock
241 self.wlock = wlock
241 self.wlock = wlock
242 self.backupfile = None
242 self.backupfile = None
243 if replacements is None:
243 if replacements is None:
244 self.replacements = []
244 self.replacements = []
245 else:
245 else:
246 self.replacements = replacements
246 self.replacements = replacements
247
247
248 def read(self):
248 def read(self):
249 """Load histedit state from disk and set fields appropriately."""
249 """Load histedit state from disk and set fields appropriately."""
250 try:
250 try:
251 state = self.repo.vfs.read('histedit-state')
251 state = self.repo.vfs.read('histedit-state')
252 except IOError as err:
252 except IOError as err:
253 if err.errno != errno.ENOENT:
253 if err.errno != errno.ENOENT:
254 raise
254 raise
255 raise error.Abort(_('no histedit in progress'))
255 raise error.Abort(_('no histedit in progress'))
256
256
257 if state.startswith('v1\n'):
257 if state.startswith('v1\n'):
258 data = self._load()
258 data = self._load()
259 parentctxnode, rules, keep, topmost, replacements, backupfile = data
259 parentctxnode, rules, keep, topmost, replacements, backupfile = data
260 else:
260 else:
261 data = pickle.loads(state)
261 data = pickle.loads(state)
262 parentctxnode, rules, keep, topmost, replacements = data
262 parentctxnode, rules, keep, topmost, replacements = data
263 backupfile = None
263 backupfile = None
264
264
265 self.parentctxnode = parentctxnode
265 self.parentctxnode = parentctxnode
266 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
266 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
267 actions = parserules(rules, self)
267 actions = parserules(rules, self)
268 self.actions = actions
268 self.actions = actions
269 self.keep = keep
269 self.keep = keep
270 self.topmost = topmost
270 self.topmost = topmost
271 self.replacements = replacements
271 self.replacements = replacements
272 self.backupfile = backupfile
272 self.backupfile = backupfile
273
273
274 def write(self):
274 def write(self):
275 fp = self.repo.vfs('histedit-state', 'w')
275 fp = self.repo.vfs('histedit-state', 'w')
276 fp.write('v1\n')
276 fp.write('v1\n')
277 fp.write('%s\n' % node.hex(self.parentctxnode))
277 fp.write('%s\n' % node.hex(self.parentctxnode))
278 fp.write('%s\n' % node.hex(self.topmost))
278 fp.write('%s\n' % node.hex(self.topmost))
279 fp.write('%s\n' % self.keep)
279 fp.write('%s\n' % self.keep)
280 fp.write('%d\n' % len(self.actions))
280 fp.write('%d\n' % len(self.actions))
281 for action in self.actions:
281 for action in self.actions:
282 fp.write('%s\n' % action.tostate())
282 fp.write('%s\n' % action.tostate())
283 fp.write('%d\n' % len(self.replacements))
283 fp.write('%d\n' % len(self.replacements))
284 for replacement in self.replacements:
284 for replacement in self.replacements:
285 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
285 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
286 for r in replacement[1])))
286 for r in replacement[1])))
287 backupfile = self.backupfile
287 backupfile = self.backupfile
288 if not backupfile:
288 if not backupfile:
289 backupfile = ''
289 backupfile = ''
290 fp.write('%s\n' % backupfile)
290 fp.write('%s\n' % backupfile)
291 fp.close()
291 fp.close()
292
292
293 def _load(self):
293 def _load(self):
294 fp = self.repo.vfs('histedit-state', 'r')
294 fp = self.repo.vfs('histedit-state', 'r')
295 lines = [l[:-1] for l in fp.readlines()]
295 lines = [l[:-1] for l in fp.readlines()]
296
296
297 index = 0
297 index = 0
298 lines[index] # version number
298 lines[index] # version number
299 index += 1
299 index += 1
300
300
301 parentctxnode = node.bin(lines[index])
301 parentctxnode = node.bin(lines[index])
302 index += 1
302 index += 1
303
303
304 topmost = node.bin(lines[index])
304 topmost = node.bin(lines[index])
305 index += 1
305 index += 1
306
306
307 keep = lines[index] == 'True'
307 keep = lines[index] == 'True'
308 index += 1
308 index += 1
309
309
310 # Rules
310 # Rules
311 rules = []
311 rules = []
312 rulelen = int(lines[index])
312 rulelen = int(lines[index])
313 index += 1
313 index += 1
314 for i in xrange(rulelen):
314 for i in xrange(rulelen):
315 ruleaction = lines[index]
315 ruleaction = lines[index]
316 index += 1
316 index += 1
317 rule = lines[index]
317 rule = lines[index]
318 index += 1
318 index += 1
319 rules.append((ruleaction, rule))
319 rules.append((ruleaction, rule))
320
320
321 # Replacements
321 # Replacements
322 replacements = []
322 replacements = []
323 replacementlen = int(lines[index])
323 replacementlen = int(lines[index])
324 index += 1
324 index += 1
325 for i in xrange(replacementlen):
325 for i in xrange(replacementlen):
326 replacement = lines[index]
326 replacement = lines[index]
327 original = node.bin(replacement[:40])
327 original = node.bin(replacement[:40])
328 succ = [node.bin(replacement[i:i + 40]) for i in
328 succ = [node.bin(replacement[i:i + 40]) for i in
329 range(40, len(replacement), 40)]
329 range(40, len(replacement), 40)]
330 replacements.append((original, succ))
330 replacements.append((original, succ))
331 index += 1
331 index += 1
332
332
333 backupfile = lines[index]
333 backupfile = lines[index]
334 index += 1
334 index += 1
335
335
336 fp.close()
336 fp.close()
337
337
338 return parentctxnode, rules, keep, topmost, replacements, backupfile
338 return parentctxnode, rules, keep, topmost, replacements, backupfile
339
339
340 def clear(self):
340 def clear(self):
341 if self.inprogress():
341 if self.inprogress():
342 self.repo.vfs.unlink('histedit-state')
342 self.repo.vfs.unlink('histedit-state')
343
343
344 def inprogress(self):
344 def inprogress(self):
345 return self.repo.vfs.exists('histedit-state')
345 return self.repo.vfs.exists('histedit-state')
346
346
347
347
348 class histeditaction(object):
348 class histeditaction(object):
349 def __init__(self, state, node):
349 def __init__(self, state, node):
350 self.state = state
350 self.state = state
351 self.repo = state.repo
351 self.repo = state.repo
352 self.node = node
352 self.node = node
353
353
354 @classmethod
354 @classmethod
355 def fromrule(cls, state, rule):
355 def fromrule(cls, state, rule):
356 """Parses the given rule, returning an instance of the histeditaction.
356 """Parses the given rule, returning an instance of the histeditaction.
357 """
357 """
358 rulehash = rule.strip().split(' ', 1)[0]
358 rulehash = rule.strip().split(' ', 1)[0]
359 return cls(state, node.bin(rulehash))
359 return cls(state, node.bin(rulehash))
360
360
361 def verify(self, prev):
361 def verify(self, prev):
362 """ Verifies semantic correctness of the rule"""
362 """ Verifies semantic correctness of the rule"""
363 repo = self.repo
363 repo = self.repo
364 ha = node.hex(self.node)
364 ha = node.hex(self.node)
365 try:
365 try:
366 self.node = repo[ha].node()
366 self.node = repo[ha].node()
367 except error.RepoError:
367 except error.RepoError:
368 raise error.Abort(_('unknown changeset %s listed')
368 raise error.Abort(_('unknown changeset %s listed')
369 % ha[:12])
369 % ha[:12])
370
370
371 def torule(self):
371 def torule(self):
372 """build a histedit rule line for an action
372 """build a histedit rule line for an action
373
373
374 by default lines are in the form:
374 by default lines are in the form:
375 <hash> <rev> <summary>
375 <hash> <rev> <summary>
376 """
376 """
377 ctx = self.repo[self.node]
377 ctx = self.repo[self.node]
378 summary = ''
378 summary = ''
379 if ctx.description():
379 if ctx.description():
380 summary = ctx.description().splitlines()[0]
380 summary = ctx.description().splitlines()[0]
381 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
381 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
382 # trim to 75 columns by default so it's not stupidly wide in my editor
382 # trim to 75 columns by default so it's not stupidly wide in my editor
383 # (the 5 more are left for verb)
383 # (the 5 more are left for verb)
384 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
384 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
385 maxlen = max(maxlen, 22) # avoid truncating hash
385 maxlen = max(maxlen, 22) # avoid truncating hash
386 return util.ellipsis(line, maxlen)
386 return util.ellipsis(line, maxlen)
387
387
388 def tostate(self):
388 def tostate(self):
389 """Print an action in format used by histedit state files
389 """Print an action in format used by histedit state files
390 (the first line is a verb, the remainder is the second)
390 (the first line is a verb, the remainder is the second)
391 """
391 """
392 return "%s\n%s" % (self.verb, node.hex(self.node))
392 return "%s\n%s" % (self.verb, node.hex(self.node))
393
393
394 def constraints(self):
394 def constraints(self):
395 """Return a set of constrains that this action should be verified for
395 """Return a set of constrains that this action should be verified for
396 """
396 """
397 return set([_constraints.noduplicates, _constraints.noother])
397 return set([_constraints.noduplicates, _constraints.noother])
398
398
399 def nodetoverify(self):
399 def nodetoverify(self):
400 """Returns a node associated with the action that will be used for
400 """Returns a node associated with the action that will be used for
401 verification purposes.
401 verification purposes.
402
402
403 If the action doesn't correspond to node it should return None
403 If the action doesn't correspond to node it should return None
404 """
404 """
405 return self.node
405 return self.node
406
406
407 def run(self):
407 def run(self):
408 """Runs the action. The default behavior is simply apply the action's
408 """Runs the action. The default behavior is simply apply the action's
409 rulectx onto the current parentctx."""
409 rulectx onto the current parentctx."""
410 self.applychange()
410 self.applychange()
411 self.continuedirty()
411 self.continuedirty()
412 return self.continueclean()
412 return self.continueclean()
413
413
414 def applychange(self):
414 def applychange(self):
415 """Applies the changes from this action's rulectx onto the current
415 """Applies the changes from this action's rulectx onto the current
416 parentctx, but does not commit them."""
416 parentctx, but does not commit them."""
417 repo = self.repo
417 repo = self.repo
418 rulectx = repo[self.node]
418 rulectx = repo[self.node]
419 hg.update(repo, self.state.parentctxnode, quietempty=True)
419 hg.update(repo, self.state.parentctxnode, quietempty=True)
420 stats = applychanges(repo.ui, repo, rulectx, {})
420 stats = applychanges(repo.ui, repo, rulectx, {})
421 if stats and stats[3] > 0:
421 if stats and stats[3] > 0:
422 raise error.InterventionRequired(_('Fix up the change and run '
422 raise error.InterventionRequired(_('Fix up the change and run '
423 'hg histedit --continue'))
423 'hg histedit --continue'))
424
424
425 def continuedirty(self):
425 def continuedirty(self):
426 """Continues the action when changes have been applied to the working
426 """Continues the action when changes have been applied to the working
427 copy. The default behavior is to commit the dirty changes."""
427 copy. The default behavior is to commit the dirty changes."""
428 repo = self.repo
428 repo = self.repo
429 rulectx = repo[self.node]
429 rulectx = repo[self.node]
430
430
431 editor = self.commiteditor()
431 editor = self.commiteditor()
432 commit = commitfuncfor(repo, rulectx)
432 commit = commitfuncfor(repo, rulectx)
433
433
434 commit(text=rulectx.description(), user=rulectx.user(),
434 commit(text=rulectx.description(), user=rulectx.user(),
435 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
435 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
436
436
437 def commiteditor(self):
437 def commiteditor(self):
438 """The editor to be used to edit the commit message."""
438 """The editor to be used to edit the commit message."""
439 return False
439 return False
440
440
441 def continueclean(self):
441 def continueclean(self):
442 """Continues the action when the working copy is clean. The default
442 """Continues the action when the working copy is clean. The default
443 behavior is to accept the current commit as the new version of the
443 behavior is to accept the current commit as the new version of the
444 rulectx."""
444 rulectx."""
445 ctx = self.repo['.']
445 ctx = self.repo['.']
446 if ctx.node() == self.state.parentctxnode:
446 if ctx.node() == self.state.parentctxnode:
447 self.repo.ui.warn(_('%s: empty changeset\n') %
447 self.repo.ui.warn(_('%s: empty changeset\n') %
448 node.short(self.node))
448 node.short(self.node))
449 return ctx, [(self.node, tuple())]
449 return ctx, [(self.node, tuple())]
450 if ctx.node() == self.node:
450 if ctx.node() == self.node:
451 # Nothing changed
451 # Nothing changed
452 return ctx, []
452 return ctx, []
453 return ctx, [(self.node, (ctx.node(),))]
453 return ctx, [(self.node, (ctx.node(),))]
454
454
455 def commitfuncfor(repo, src):
455 def commitfuncfor(repo, src):
456 """Build a commit function for the replacement of <src>
456 """Build a commit function for the replacement of <src>
457
457
458 This function ensure we apply the same treatment to all changesets.
458 This function ensure we apply the same treatment to all changesets.
459
459
460 - Add a 'histedit_source' entry in extra.
460 - Add a 'histedit_source' entry in extra.
461
461
462 Note that fold has its own separated logic because its handling is a bit
462 Note that fold has its own separated logic because its handling is a bit
463 different and not easily factored out of the fold method.
463 different and not easily factored out of the fold method.
464 """
464 """
465 phasemin = src.phase()
465 phasemin = src.phase()
466 def commitfunc(**kwargs):
466 def commitfunc(**kwargs):
467 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
467 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
468 try:
468 try:
469 repo.ui.setconfig('phases', 'new-commit', phasemin,
469 repo.ui.setconfig('phases', 'new-commit', phasemin,
470 'histedit')
470 'histedit')
471 extra = kwargs.get('extra', {}).copy()
471 extra = kwargs.get('extra', {}).copy()
472 extra['histedit_source'] = src.hex()
472 extra['histedit_source'] = src.hex()
473 kwargs['extra'] = extra
473 kwargs['extra'] = extra
474 return repo.commit(**kwargs)
474 return repo.commit(**kwargs)
475 finally:
475 finally:
476 repo.ui.restoreconfig(phasebackup)
476 repo.ui.restoreconfig(phasebackup)
477 return commitfunc
477 return commitfunc
478
478
479 def applychanges(ui, repo, ctx, opts):
479 def applychanges(ui, repo, ctx, opts):
480 """Merge changeset from ctx (only) in the current working directory"""
480 """Merge changeset from ctx (only) in the current working directory"""
481 wcpar = repo.dirstate.parents()[0]
481 wcpar = repo.dirstate.parents()[0]
482 if ctx.p1().node() == wcpar:
482 if ctx.p1().node() == wcpar:
483 # edits are "in place" we do not need to make any merge,
483 # edits are "in place" we do not need to make any merge,
484 # just applies changes on parent for edition
484 # just applies changes on parent for edition
485 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
485 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
486 stats = None
486 stats = None
487 else:
487 else:
488 try:
488 try:
489 # ui.forcemerge is an internal variable, do not document
489 # ui.forcemerge is an internal variable, do not document
490 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
490 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
491 'histedit')
491 'histedit')
492 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
492 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
493 finally:
493 finally:
494 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
494 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
495 return stats
495 return stats
496
496
497 def collapse(repo, first, last, commitopts, skipprompt=False):
497 def collapse(repo, first, last, commitopts, skipprompt=False):
498 """collapse the set of revisions from first to last as new one.
498 """collapse the set of revisions from first to last as new one.
499
499
500 Expected commit options are:
500 Expected commit options are:
501 - message
501 - message
502 - date
502 - date
503 - username
503 - username
504 Commit message is edited in all cases.
504 Commit message is edited in all cases.
505
505
506 This function works in memory."""
506 This function works in memory."""
507 ctxs = list(repo.set('%d::%d', first, last))
507 ctxs = list(repo.set('%d::%d', first, last))
508 if not ctxs:
508 if not ctxs:
509 return None
509 return None
510 for c in ctxs:
510 for c in ctxs:
511 if not c.mutable():
511 if not c.mutable():
512 raise error.Abort(
512 raise error.Abort(
513 _("cannot fold into public change %s") % node.short(c.node()))
513 _("cannot fold into public change %s") % node.short(c.node()))
514 base = first.parents()[0]
514 base = first.parents()[0]
515
515
516 # commit a new version of the old changeset, including the update
516 # commit a new version of the old changeset, including the update
517 # collect all files which might be affected
517 # collect all files which might be affected
518 files = set()
518 files = set()
519 for ctx in ctxs:
519 for ctx in ctxs:
520 files.update(ctx.files())
520 files.update(ctx.files())
521
521
522 # Recompute copies (avoid recording a -> b -> a)
522 # Recompute copies (avoid recording a -> b -> a)
523 copied = copies.pathcopies(base, last)
523 copied = copies.pathcopies(base, last)
524
524
525 # prune files which were reverted by the updates
525 # prune files which were reverted by the updates
526 def samefile(f):
526 def samefile(f):
527 if f in last.manifest():
527 if f in last.manifest():
528 a = last.filectx(f)
528 a = last.filectx(f)
529 if f in base.manifest():
529 if f in base.manifest():
530 b = base.filectx(f)
530 b = base.filectx(f)
531 return (a.data() == b.data()
531 return (a.data() == b.data()
532 and a.flags() == b.flags())
532 and a.flags() == b.flags())
533 else:
533 else:
534 return False
534 return False
535 else:
535 else:
536 return f not in base.manifest()
536 return f not in base.manifest()
537 files = [f for f in files if not samefile(f)]
537 files = [f for f in files if not samefile(f)]
538 # commit version of these files as defined by head
538 # commit version of these files as defined by head
539 headmf = last.manifest()
539 headmf = last.manifest()
540 def filectxfn(repo, ctx, path):
540 def filectxfn(repo, ctx, path):
541 if path in headmf:
541 if path in headmf:
542 fctx = last[path]
542 fctx = last[path]
543 flags = fctx.flags()
543 flags = fctx.flags()
544 mctx = context.memfilectx(repo,
544 mctx = context.memfilectx(repo,
545 fctx.path(), fctx.data(),
545 fctx.path(), fctx.data(),
546 islink='l' in flags,
546 islink='l' in flags,
547 isexec='x' in flags,
547 isexec='x' in flags,
548 copied=copied.get(path))
548 copied=copied.get(path))
549 return mctx
549 return mctx
550 return None
550 return None
551
551
552 if commitopts.get('message'):
552 if commitopts.get('message'):
553 message = commitopts['message']
553 message = commitopts['message']
554 else:
554 else:
555 message = first.description()
555 message = first.description()
556 user = commitopts.get('user')
556 user = commitopts.get('user')
557 date = commitopts.get('date')
557 date = commitopts.get('date')
558 extra = commitopts.get('extra')
558 extra = commitopts.get('extra')
559
559
560 parents = (first.p1().node(), first.p2().node())
560 parents = (first.p1().node(), first.p2().node())
561 editor = None
561 editor = None
562 if not skipprompt:
562 if not skipprompt:
563 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
563 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
564 new = context.memctx(repo,
564 new = context.memctx(repo,
565 parents=parents,
565 parents=parents,
566 text=message,
566 text=message,
567 files=files,
567 files=files,
568 filectxfn=filectxfn,
568 filectxfn=filectxfn,
569 user=user,
569 user=user,
570 date=date,
570 date=date,
571 extra=extra,
571 extra=extra,
572 editor=editor)
572 editor=editor)
573 return repo.commitctx(new)
573 return repo.commitctx(new)
574
574
575 def _isdirtywc(repo):
575 def _isdirtywc(repo):
576 return repo[None].dirty(missing=True)
576 return repo[None].dirty(missing=True)
577
577
578 def abortdirty():
578 def abortdirty():
579 raise error.Abort(_('working copy has pending changes'),
579 raise error.Abort(_('working copy has pending changes'),
580 hint=_('amend, commit, or revert them and run histedit '
580 hint=_('amend, commit, or revert them and run histedit '
581 '--continue, or abort with histedit --abort'))
581 '--continue, or abort with histedit --abort'))
582
582
583
583
584 actiontable = {}
584 actiontable = {}
585 actionlist = []
585 actionlist = []
586
586
587 def addhisteditaction(verbs):
587 def addhisteditaction(verbs):
588 def wrap(cls):
588 def wrap(cls):
589 cls.verb = verbs[0]
589 cls.verb = verbs[0]
590 for verb in verbs:
590 for verb in verbs:
591 actiontable[verb] = cls
591 actiontable[verb] = cls
592 actionlist.append(cls)
592 actionlist.append(cls)
593 return cls
593 return cls
594 return wrap
594 return wrap
595
595
596
596
597 @addhisteditaction(['pick', 'p'])
597 @addhisteditaction(['pick', 'p'])
598 class pick(histeditaction):
598 class pick(histeditaction):
599 def run(self):
599 def run(self):
600 rulectx = self.repo[self.node]
600 rulectx = self.repo[self.node]
601 if rulectx.parents()[0].node() == self.state.parentctxnode:
601 if rulectx.parents()[0].node() == self.state.parentctxnode:
602 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
602 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
603 return rulectx, []
603 return rulectx, []
604
604
605 return super(pick, self).run()
605 return super(pick, self).run()
606
606
607 @addhisteditaction(['edit', 'e'])
607 @addhisteditaction(['edit', 'e'])
608 class edit(histeditaction):
608 class edit(histeditaction):
609 def run(self):
609 def run(self):
610 repo = self.repo
610 repo = self.repo
611 rulectx = repo[self.node]
611 rulectx = repo[self.node]
612 hg.update(repo, self.state.parentctxnode, quietempty=True)
612 hg.update(repo, self.state.parentctxnode, quietempty=True)
613 applychanges(repo.ui, repo, rulectx, {})
613 applychanges(repo.ui, repo, rulectx, {})
614 raise error.InterventionRequired(
614 raise error.InterventionRequired(
615 _('Make changes as needed, you may commit or record as needed '
615 _('Make changes as needed, you may commit or record as needed '
616 'now.\nWhen you are finished, run hg histedit --continue to '
616 'now.\nWhen you are finished, run hg histedit --continue to '
617 'resume.'))
617 'resume.'))
618
618
619 def commiteditor(self):
619 def commiteditor(self):
620 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
620 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
621
621
622 @addhisteditaction(['fold', 'f'])
622 @addhisteditaction(['fold', 'f'])
623 class fold(histeditaction):
623 class fold(histeditaction):
624 def verify(self, prev):
625 """ Verifies semantic correctness of the fold rule"""
626 super(fold, self).verify(prev)
627 repo = self.repo
628 state = self.state
629 if not prev:
630 c = repo[self.node].parents()[0]
631 elif not prev.verb in ('pick', 'base'):
632 return
633 else:
634 c = repo[prev.node]
635 if not c.mutable():
636 raise error.Abort(
637 _("cannot fold into public change %s") % node.short(c.node()))
638
639
624 def continuedirty(self):
640 def continuedirty(self):
625 repo = self.repo
641 repo = self.repo
626 rulectx = repo[self.node]
642 rulectx = repo[self.node]
627
643
628 commit = commitfuncfor(repo, rulectx)
644 commit = commitfuncfor(repo, rulectx)
629 commit(text='fold-temp-revision %s' % node.short(self.node),
645 commit(text='fold-temp-revision %s' % node.short(self.node),
630 user=rulectx.user(), date=rulectx.date(),
646 user=rulectx.user(), date=rulectx.date(),
631 extra=rulectx.extra())
647 extra=rulectx.extra())
632
648
633 def continueclean(self):
649 def continueclean(self):
634 repo = self.repo
650 repo = self.repo
635 ctx = repo['.']
651 ctx = repo['.']
636 rulectx = repo[self.node]
652 rulectx = repo[self.node]
637 parentctxnode = self.state.parentctxnode
653 parentctxnode = self.state.parentctxnode
638 if ctx.node() == parentctxnode:
654 if ctx.node() == parentctxnode:
639 repo.ui.warn(_('%s: empty changeset\n') %
655 repo.ui.warn(_('%s: empty changeset\n') %
640 node.short(self.node))
656 node.short(self.node))
641 return ctx, [(self.node, (parentctxnode,))]
657 return ctx, [(self.node, (parentctxnode,))]
642
658
643 parentctx = repo[parentctxnode]
659 parentctx = repo[parentctxnode]
644 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
660 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
645 parentctx))
661 parentctx))
646 if not newcommits:
662 if not newcommits:
647 repo.ui.warn(_('%s: cannot fold - working copy is not a '
663 repo.ui.warn(_('%s: cannot fold - working copy is not a '
648 'descendant of previous commit %s\n') %
664 'descendant of previous commit %s\n') %
649 (node.short(self.node), node.short(parentctxnode)))
665 (node.short(self.node), node.short(parentctxnode)))
650 return ctx, [(self.node, (ctx.node(),))]
666 return ctx, [(self.node, (ctx.node(),))]
651
667
652 middlecommits = newcommits.copy()
668 middlecommits = newcommits.copy()
653 middlecommits.discard(ctx.node())
669 middlecommits.discard(ctx.node())
654
670
655 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
671 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
656 middlecommits)
672 middlecommits)
657
673
658 def skipprompt(self):
674 def skipprompt(self):
659 """Returns true if the rule should skip the message editor.
675 """Returns true if the rule should skip the message editor.
660
676
661 For example, 'fold' wants to show an editor, but 'rollup'
677 For example, 'fold' wants to show an editor, but 'rollup'
662 doesn't want to.
678 doesn't want to.
663 """
679 """
664 return False
680 return False
665
681
666 def mergedescs(self):
682 def mergedescs(self):
667 """Returns true if the rule should merge messages of multiple changes.
683 """Returns true if the rule should merge messages of multiple changes.
668
684
669 This exists mainly so that 'rollup' rules can be a subclass of
685 This exists mainly so that 'rollup' rules can be a subclass of
670 'fold'.
686 'fold'.
671 """
687 """
672 return True
688 return True
673
689
674 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
690 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
675 parent = ctx.parents()[0].node()
691 parent = ctx.parents()[0].node()
676 hg.update(repo, parent)
692 hg.update(repo, parent)
677 ### prepare new commit data
693 ### prepare new commit data
678 commitopts = {}
694 commitopts = {}
679 commitopts['user'] = ctx.user()
695 commitopts['user'] = ctx.user()
680 # commit message
696 # commit message
681 if not self.mergedescs():
697 if not self.mergedescs():
682 newmessage = ctx.description()
698 newmessage = ctx.description()
683 else:
699 else:
684 newmessage = '\n***\n'.join(
700 newmessage = '\n***\n'.join(
685 [ctx.description()] +
701 [ctx.description()] +
686 [repo[r].description() for r in internalchanges] +
702 [repo[r].description() for r in internalchanges] +
687 [oldctx.description()]) + '\n'
703 [oldctx.description()]) + '\n'
688 commitopts['message'] = newmessage
704 commitopts['message'] = newmessage
689 # date
705 # date
690 commitopts['date'] = max(ctx.date(), oldctx.date())
706 commitopts['date'] = max(ctx.date(), oldctx.date())
691 extra = ctx.extra().copy()
707 extra = ctx.extra().copy()
692 # histedit_source
708 # histedit_source
693 # note: ctx is likely a temporary commit but that the best we can do
709 # note: ctx is likely a temporary commit but that the best we can do
694 # here. This is sufficient to solve issue3681 anyway.
710 # here. This is sufficient to solve issue3681 anyway.
695 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
711 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
696 commitopts['extra'] = extra
712 commitopts['extra'] = extra
697 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
713 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
698 try:
714 try:
699 phasemin = max(ctx.phase(), oldctx.phase())
715 phasemin = max(ctx.phase(), oldctx.phase())
700 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
716 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
701 n = collapse(repo, ctx, repo[newnode], commitopts,
717 n = collapse(repo, ctx, repo[newnode], commitopts,
702 skipprompt=self.skipprompt())
718 skipprompt=self.skipprompt())
703 finally:
719 finally:
704 repo.ui.restoreconfig(phasebackup)
720 repo.ui.restoreconfig(phasebackup)
705 if n is None:
721 if n is None:
706 return ctx, []
722 return ctx, []
707 hg.update(repo, n)
723 hg.update(repo, n)
708 replacements = [(oldctx.node(), (newnode,)),
724 replacements = [(oldctx.node(), (newnode,)),
709 (ctx.node(), (n,)),
725 (ctx.node(), (n,)),
710 (newnode, (n,)),
726 (newnode, (n,)),
711 ]
727 ]
712 for ich in internalchanges:
728 for ich in internalchanges:
713 replacements.append((ich, (n,)))
729 replacements.append((ich, (n,)))
714 return repo[n], replacements
730 return repo[n], replacements
715
731
716 class base(histeditaction):
732 class base(histeditaction):
717 def constraints(self):
733 def constraints(self):
718 return set([_constraints.forceother])
734 return set([_constraints.forceother])
719
735
720 def run(self):
736 def run(self):
721 if self.repo['.'].node() != self.node:
737 if self.repo['.'].node() != self.node:
722 mergemod.update(self.repo, self.node, False, True)
738 mergemod.update(self.repo, self.node, False, True)
723 # branchmerge, force)
739 # branchmerge, force)
724 return self.continueclean()
740 return self.continueclean()
725
741
726 def continuedirty(self):
742 def continuedirty(self):
727 abortdirty()
743 abortdirty()
728
744
729 def continueclean(self):
745 def continueclean(self):
730 basectx = self.repo['.']
746 basectx = self.repo['.']
731 return basectx, []
747 return basectx, []
732
748
733 @addhisteditaction(['_multifold'])
749 @addhisteditaction(['_multifold'])
734 class _multifold(fold):
750 class _multifold(fold):
735 """fold subclass used for when multiple folds happen in a row
751 """fold subclass used for when multiple folds happen in a row
736
752
737 We only want to fire the editor for the folded message once when
753 We only want to fire the editor for the folded message once when
738 (say) four changes are folded down into a single change. This is
754 (say) four changes are folded down into a single change. This is
739 similar to rollup, but we should preserve both messages so that
755 similar to rollup, but we should preserve both messages so that
740 when the last fold operation runs we can show the user all the
756 when the last fold operation runs we can show the user all the
741 commit messages in their editor.
757 commit messages in their editor.
742 """
758 """
743 def skipprompt(self):
759 def skipprompt(self):
744 return True
760 return True
745
761
746 @addhisteditaction(["roll", "r"])
762 @addhisteditaction(["roll", "r"])
747 class rollup(fold):
763 class rollup(fold):
748 def mergedescs(self):
764 def mergedescs(self):
749 return False
765 return False
750
766
751 def skipprompt(self):
767 def skipprompt(self):
752 return True
768 return True
753
769
754 @addhisteditaction(["drop", "d"])
770 @addhisteditaction(["drop", "d"])
755 class drop(histeditaction):
771 class drop(histeditaction):
756 def run(self):
772 def run(self):
757 parentctx = self.repo[self.state.parentctxnode]
773 parentctx = self.repo[self.state.parentctxnode]
758 return parentctx, [(self.node, tuple())]
774 return parentctx, [(self.node, tuple())]
759
775
760 @addhisteditaction(["mess", "m"])
776 @addhisteditaction(["mess", "m"])
761 class message(histeditaction):
777 class message(histeditaction):
762 def commiteditor(self):
778 def commiteditor(self):
763 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
779 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
764
780
765 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
781 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
766 """utility function to find the first outgoing changeset
782 """utility function to find the first outgoing changeset
767
783
768 Used by initialization code"""
784 Used by initialization code"""
769 if opts is None:
785 if opts is None:
770 opts = {}
786 opts = {}
771 dest = ui.expandpath(remote or 'default-push', remote or 'default')
787 dest = ui.expandpath(remote or 'default-push', remote or 'default')
772 dest, revs = hg.parseurl(dest, None)[:2]
788 dest, revs = hg.parseurl(dest, None)[:2]
773 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
789 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
774
790
775 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
791 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
776 other = hg.peer(repo, opts, dest)
792 other = hg.peer(repo, opts, dest)
777
793
778 if revs:
794 if revs:
779 revs = [repo.lookup(rev) for rev in revs]
795 revs = [repo.lookup(rev) for rev in revs]
780
796
781 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
797 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
782 if not outgoing.missing:
798 if not outgoing.missing:
783 raise error.Abort(_('no outgoing ancestors'))
799 raise error.Abort(_('no outgoing ancestors'))
784 roots = list(repo.revs("roots(%ln)", outgoing.missing))
800 roots = list(repo.revs("roots(%ln)", outgoing.missing))
785 if 1 < len(roots):
801 if 1 < len(roots):
786 msg = _('there are ambiguous outgoing revisions')
802 msg = _('there are ambiguous outgoing revisions')
787 hint = _('see "hg help histedit" for more detail')
803 hint = _('see "hg help histedit" for more detail')
788 raise error.Abort(msg, hint=hint)
804 raise error.Abort(msg, hint=hint)
789 return repo.lookup(roots[0])
805 return repo.lookup(roots[0])
790
806
791
807
792 @command('histedit',
808 @command('histedit',
793 [('', 'commands', '',
809 [('', 'commands', '',
794 _('read history edits from the specified file'), _('FILE')),
810 _('read history edits from the specified file'), _('FILE')),
795 ('c', 'continue', False, _('continue an edit already in progress')),
811 ('c', 'continue', False, _('continue an edit already in progress')),
796 ('', 'edit-plan', False, _('edit remaining actions list')),
812 ('', 'edit-plan', False, _('edit remaining actions list')),
797 ('k', 'keep', False,
813 ('k', 'keep', False,
798 _("don't strip old nodes after edit is complete")),
814 _("don't strip old nodes after edit is complete")),
799 ('', 'abort', False, _('abort an edit in progress')),
815 ('', 'abort', False, _('abort an edit in progress')),
800 ('o', 'outgoing', False, _('changesets not found in destination')),
816 ('o', 'outgoing', False, _('changesets not found in destination')),
801 ('f', 'force', False,
817 ('f', 'force', False,
802 _('force outgoing even for unrelated repositories')),
818 _('force outgoing even for unrelated repositories')),
803 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
819 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
804 _("[ANCESTOR] | --outgoing [URL]"))
820 _("[ANCESTOR] | --outgoing [URL]"))
805 def histedit(ui, repo, *freeargs, **opts):
821 def histedit(ui, repo, *freeargs, **opts):
806 """interactively edit changeset history
822 """interactively edit changeset history
807
823
808 This command edits changesets between an ANCESTOR and the parent of
824 This command edits changesets between an ANCESTOR and the parent of
809 the working directory.
825 the working directory.
810
826
811 The value from the "histedit.defaultrev" config option is used as a
827 The value from the "histedit.defaultrev" config option is used as a
812 revset to select the base revision when ANCESTOR is not specified.
828 revset to select the base revision when ANCESTOR is not specified.
813 The first revision returned by the revset is used. By default, this
829 The first revision returned by the revset is used. By default, this
814 selects the editable history that is unique to the ancestry of the
830 selects the editable history that is unique to the ancestry of the
815 working directory.
831 working directory.
816
832
817 With --outgoing, this edits changesets not found in the
833 With --outgoing, this edits changesets not found in the
818 destination repository. If URL of the destination is omitted, the
834 destination repository. If URL of the destination is omitted, the
819 'default-push' (or 'default') path will be used.
835 'default-push' (or 'default') path will be used.
820
836
821 For safety, this command is also aborted if there are ambiguous
837 For safety, this command is also aborted if there are ambiguous
822 outgoing revisions which may confuse users: for example, if there
838 outgoing revisions which may confuse users: for example, if there
823 are multiple branches containing outgoing revisions.
839 are multiple branches containing outgoing revisions.
824
840
825 Use "min(outgoing() and ::.)" or similar revset specification
841 Use "min(outgoing() and ::.)" or similar revset specification
826 instead of --outgoing to specify edit target revision exactly in
842 instead of --outgoing to specify edit target revision exactly in
827 such ambiguous situation. See :hg:`help revsets` for detail about
843 such ambiguous situation. See :hg:`help revsets` for detail about
828 selecting revisions.
844 selecting revisions.
829
845
830 .. container:: verbose
846 .. container:: verbose
831
847
832 Examples:
848 Examples:
833
849
834 - A number of changes have been made.
850 - A number of changes have been made.
835 Revision 3 is no longer needed.
851 Revision 3 is no longer needed.
836
852
837 Start history editing from revision 3::
853 Start history editing from revision 3::
838
854
839 hg histedit -r 3
855 hg histedit -r 3
840
856
841 An editor opens, containing the list of revisions,
857 An editor opens, containing the list of revisions,
842 with specific actions specified::
858 with specific actions specified::
843
859
844 pick 5339bf82f0ca 3 Zworgle the foobar
860 pick 5339bf82f0ca 3 Zworgle the foobar
845 pick 8ef592ce7cc4 4 Bedazzle the zerlog
861 pick 8ef592ce7cc4 4 Bedazzle the zerlog
846 pick 0a9639fcda9d 5 Morgify the cromulancy
862 pick 0a9639fcda9d 5 Morgify the cromulancy
847
863
848 Additional information about the possible actions
864 Additional information about the possible actions
849 to take appears below the list of revisions.
865 to take appears below the list of revisions.
850
866
851 To remove revision 3 from the history,
867 To remove revision 3 from the history,
852 its action (at the beginning of the relevant line)
868 its action (at the beginning of the relevant line)
853 is changed to 'drop'::
869 is changed to 'drop'::
854
870
855 drop 5339bf82f0ca 3 Zworgle the foobar
871 drop 5339bf82f0ca 3 Zworgle the foobar
856 pick 8ef592ce7cc4 4 Bedazzle the zerlog
872 pick 8ef592ce7cc4 4 Bedazzle the zerlog
857 pick 0a9639fcda9d 5 Morgify the cromulancy
873 pick 0a9639fcda9d 5 Morgify the cromulancy
858
874
859 - A number of changes have been made.
875 - A number of changes have been made.
860 Revision 2 and 4 need to be swapped.
876 Revision 2 and 4 need to be swapped.
861
877
862 Start history editing from revision 2::
878 Start history editing from revision 2::
863
879
864 hg histedit -r 2
880 hg histedit -r 2
865
881
866 An editor opens, containing the list of revisions,
882 An editor opens, containing the list of revisions,
867 with specific actions specified::
883 with specific actions specified::
868
884
869 pick 252a1af424ad 2 Blorb a morgwazzle
885 pick 252a1af424ad 2 Blorb a morgwazzle
870 pick 5339bf82f0ca 3 Zworgle the foobar
886 pick 5339bf82f0ca 3 Zworgle the foobar
871 pick 8ef592ce7cc4 4 Bedazzle the zerlog
887 pick 8ef592ce7cc4 4 Bedazzle the zerlog
872
888
873 To swap revision 2 and 4, its lines are swapped
889 To swap revision 2 and 4, its lines are swapped
874 in the editor::
890 in the editor::
875
891
876 pick 8ef592ce7cc4 4 Bedazzle the zerlog
892 pick 8ef592ce7cc4 4 Bedazzle the zerlog
877 pick 5339bf82f0ca 3 Zworgle the foobar
893 pick 5339bf82f0ca 3 Zworgle the foobar
878 pick 252a1af424ad 2 Blorb a morgwazzle
894 pick 252a1af424ad 2 Blorb a morgwazzle
879
895
880 Returns 0 on success, 1 if user intervention is required (not only
896 Returns 0 on success, 1 if user intervention is required (not only
881 for intentional "edit" command, but also for resolving unexpected
897 for intentional "edit" command, but also for resolving unexpected
882 conflicts).
898 conflicts).
883 """
899 """
884 state = histeditstate(repo)
900 state = histeditstate(repo)
885 try:
901 try:
886 state.wlock = repo.wlock()
902 state.wlock = repo.wlock()
887 state.lock = repo.lock()
903 state.lock = repo.lock()
888 _histedit(ui, repo, state, *freeargs, **opts)
904 _histedit(ui, repo, state, *freeargs, **opts)
889 except error.Abort:
905 except error.Abort:
890 if repo.vfs.exists('histedit-last-edit.txt'):
906 if repo.vfs.exists('histedit-last-edit.txt'):
891 ui.warn(_('warning: histedit rules saved '
907 ui.warn(_('warning: histedit rules saved '
892 'to: .hg/histedit-last-edit.txt\n'))
908 'to: .hg/histedit-last-edit.txt\n'))
893 raise
909 raise
894 finally:
910 finally:
895 release(state.lock, state.wlock)
911 release(state.lock, state.wlock)
896
912
897 def _histedit(ui, repo, state, *freeargs, **opts):
913 def _histedit(ui, repo, state, *freeargs, **opts):
898 # TODO only abort if we try to histedit mq patches, not just
914 # TODO only abort if we try to histedit mq patches, not just
899 # blanket if mq patches are applied somewhere
915 # blanket if mq patches are applied somewhere
900 mq = getattr(repo, 'mq', None)
916 mq = getattr(repo, 'mq', None)
901 if mq and mq.applied:
917 if mq and mq.applied:
902 raise error.Abort(_('source has mq patches applied'))
918 raise error.Abort(_('source has mq patches applied'))
903
919
904 # basic argument incompatibility processing
920 # basic argument incompatibility processing
905 outg = opts.get('outgoing')
921 outg = opts.get('outgoing')
906 cont = opts.get('continue')
922 cont = opts.get('continue')
907 editplan = opts.get('edit_plan')
923 editplan = opts.get('edit_plan')
908 abort = opts.get('abort')
924 abort = opts.get('abort')
909 force = opts.get('force')
925 force = opts.get('force')
910 rules = opts.get('commands', '')
926 rules = opts.get('commands', '')
911 revs = opts.get('rev', [])
927 revs = opts.get('rev', [])
912 goal = 'new' # This invocation goal, in new, continue, abort
928 goal = 'new' # This invocation goal, in new, continue, abort
913 if force and not outg:
929 if force and not outg:
914 raise error.Abort(_('--force only allowed with --outgoing'))
930 raise error.Abort(_('--force only allowed with --outgoing'))
915 if cont:
931 if cont:
916 if any((outg, abort, revs, freeargs, rules, editplan)):
932 if any((outg, abort, revs, freeargs, rules, editplan)):
917 raise error.Abort(_('no arguments allowed with --continue'))
933 raise error.Abort(_('no arguments allowed with --continue'))
918 goal = 'continue'
934 goal = 'continue'
919 elif abort:
935 elif abort:
920 if any((outg, revs, freeargs, rules, editplan)):
936 if any((outg, revs, freeargs, rules, editplan)):
921 raise error.Abort(_('no arguments allowed with --abort'))
937 raise error.Abort(_('no arguments allowed with --abort'))
922 goal = 'abort'
938 goal = 'abort'
923 elif editplan:
939 elif editplan:
924 if any((outg, revs, freeargs)):
940 if any((outg, revs, freeargs)):
925 raise error.Abort(_('only --commands argument allowed with '
941 raise error.Abort(_('only --commands argument allowed with '
926 '--edit-plan'))
942 '--edit-plan'))
927 goal = 'edit-plan'
943 goal = 'edit-plan'
928 else:
944 else:
929 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
945 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
930 raise error.Abort(_('history edit already in progress, try '
946 raise error.Abort(_('history edit already in progress, try '
931 '--continue or --abort'))
947 '--continue or --abort'))
932 if outg:
948 if outg:
933 if revs:
949 if revs:
934 raise error.Abort(_('no revisions allowed with --outgoing'))
950 raise error.Abort(_('no revisions allowed with --outgoing'))
935 if len(freeargs) > 1:
951 if len(freeargs) > 1:
936 raise error.Abort(
952 raise error.Abort(
937 _('only one repo argument allowed with --outgoing'))
953 _('only one repo argument allowed with --outgoing'))
938 else:
954 else:
939 revs.extend(freeargs)
955 revs.extend(freeargs)
940 if len(revs) == 0:
956 if len(revs) == 0:
941 defaultrev = destutil.desthistedit(ui, repo)
957 defaultrev = destutil.desthistedit(ui, repo)
942 if defaultrev is not None:
958 if defaultrev is not None:
943 revs.append(defaultrev)
959 revs.append(defaultrev)
944
960
945 if len(revs) != 1:
961 if len(revs) != 1:
946 raise error.Abort(
962 raise error.Abort(
947 _('histedit requires exactly one ancestor revision'))
963 _('histedit requires exactly one ancestor revision'))
948
964
949
965
950 replacements = []
966 replacements = []
951 state.keep = opts.get('keep', False)
967 state.keep = opts.get('keep', False)
952 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
968 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
953
969
954 # rebuild state
970 # rebuild state
955 if goal == 'continue':
971 if goal == 'continue':
956 state.read()
972 state.read()
957 state = bootstrapcontinue(ui, state, opts)
973 state = bootstrapcontinue(ui, state, opts)
958 elif goal == 'edit-plan':
974 elif goal == 'edit-plan':
959 state.read()
975 state.read()
960 if not rules:
976 if not rules:
961 comment = editcomment % (node.short(state.parentctxnode),
977 comment = editcomment % (node.short(state.parentctxnode),
962 node.short(state.topmost))
978 node.short(state.topmost))
963 rules = ruleeditor(repo, ui, state.actions, comment)
979 rules = ruleeditor(repo, ui, state.actions, comment)
964 else:
980 else:
965 if rules == '-':
981 if rules == '-':
966 f = sys.stdin
982 f = sys.stdin
967 else:
983 else:
968 f = open(rules)
984 f = open(rules)
969 rules = f.read()
985 rules = f.read()
970 f.close()
986 f.close()
971 actions = parserules(rules, state)
987 actions = parserules(rules, state)
972 ctxs = [repo[act.nodetoverify()] \
988 ctxs = [repo[act.nodetoverify()] \
973 for act in state.actions if act.nodetoverify()]
989 for act in state.actions if act.nodetoverify()]
974 verifyactions(actions, state, ctxs)
990 verifyactions(actions, state, ctxs)
975 state.actions = actions
991 state.actions = actions
976 state.write()
992 state.write()
977 return
993 return
978 elif goal == 'abort':
994 elif goal == 'abort':
979 try:
995 try:
980 state.read()
996 state.read()
981 tmpnodes, leafs = newnodestoabort(state)
997 tmpnodes, leafs = newnodestoabort(state)
982 ui.debug('restore wc to old parent %s\n'
998 ui.debug('restore wc to old parent %s\n'
983 % node.short(state.topmost))
999 % node.short(state.topmost))
984
1000
985 # Recover our old commits if necessary
1001 # Recover our old commits if necessary
986 if not state.topmost in repo and state.backupfile:
1002 if not state.topmost in repo and state.backupfile:
987 backupfile = repo.join(state.backupfile)
1003 backupfile = repo.join(state.backupfile)
988 f = hg.openpath(ui, backupfile)
1004 f = hg.openpath(ui, backupfile)
989 gen = exchange.readbundle(ui, f, backupfile)
1005 gen = exchange.readbundle(ui, f, backupfile)
990 tr = repo.transaction('histedit.abort')
1006 tr = repo.transaction('histedit.abort')
991 try:
1007 try:
992 if not isinstance(gen, bundle2.unbundle20):
1008 if not isinstance(gen, bundle2.unbundle20):
993 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1009 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
994 if isinstance(gen, bundle2.unbundle20):
1010 if isinstance(gen, bundle2.unbundle20):
995 bundle2.applybundle(repo, gen, tr,
1011 bundle2.applybundle(repo, gen, tr,
996 source='histedit',
1012 source='histedit',
997 url='bundle:' + backupfile)
1013 url='bundle:' + backupfile)
998 tr.close()
1014 tr.close()
999 finally:
1015 finally:
1000 tr.release()
1016 tr.release()
1001
1017
1002 os.remove(backupfile)
1018 os.remove(backupfile)
1003
1019
1004 # check whether we should update away
1020 # check whether we should update away
1005 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1021 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1006 state.parentctxnode, leafs | tmpnodes):
1022 state.parentctxnode, leafs | tmpnodes):
1007 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1023 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1008 cleanupnode(ui, repo, 'created', tmpnodes)
1024 cleanupnode(ui, repo, 'created', tmpnodes)
1009 cleanupnode(ui, repo, 'temp', leafs)
1025 cleanupnode(ui, repo, 'temp', leafs)
1010 except Exception:
1026 except Exception:
1011 if state.inprogress():
1027 if state.inprogress():
1012 ui.warn(_('warning: encountered an exception during histedit '
1028 ui.warn(_('warning: encountered an exception during histedit '
1013 '--abort; the repository may not have been completely '
1029 '--abort; the repository may not have been completely '
1014 'cleaned up\n'))
1030 'cleaned up\n'))
1015 raise
1031 raise
1016 finally:
1032 finally:
1017 state.clear()
1033 state.clear()
1018 return
1034 return
1019 else:
1035 else:
1020 cmdutil.checkunfinished(repo)
1036 cmdutil.checkunfinished(repo)
1021 cmdutil.bailifchanged(repo)
1037 cmdutil.bailifchanged(repo)
1022
1038
1023 if repo.vfs.exists('histedit-last-edit.txt'):
1039 if repo.vfs.exists('histedit-last-edit.txt'):
1024 repo.vfs.unlink('histedit-last-edit.txt')
1040 repo.vfs.unlink('histedit-last-edit.txt')
1025 topmost, empty = repo.dirstate.parents()
1041 topmost, empty = repo.dirstate.parents()
1026 if outg:
1042 if outg:
1027 if freeargs:
1043 if freeargs:
1028 remote = freeargs[0]
1044 remote = freeargs[0]
1029 else:
1045 else:
1030 remote = None
1046 remote = None
1031 root = findoutgoing(ui, repo, remote, force, opts)
1047 root = findoutgoing(ui, repo, remote, force, opts)
1032 else:
1048 else:
1033 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1049 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1034 if len(rr) != 1:
1050 if len(rr) != 1:
1035 raise error.Abort(_('The specified revisions must have '
1051 raise error.Abort(_('The specified revisions must have '
1036 'exactly one common root'))
1052 'exactly one common root'))
1037 root = rr[0].node()
1053 root = rr[0].node()
1038
1054
1039 revs = between(repo, root, topmost, state.keep)
1055 revs = between(repo, root, topmost, state.keep)
1040 if not revs:
1056 if not revs:
1041 raise error.Abort(_('%s is not an ancestor of working directory') %
1057 raise error.Abort(_('%s is not an ancestor of working directory') %
1042 node.short(root))
1058 node.short(root))
1043
1059
1044 ctxs = [repo[r] for r in revs]
1060 ctxs = [repo[r] for r in revs]
1045 if not rules:
1061 if not rules:
1046 comment = editcomment % (node.short(root), node.short(topmost))
1062 comment = editcomment % (node.short(root), node.short(topmost))
1047 actions = [pick(state, r) for r in revs]
1063 actions = [pick(state, r) for r in revs]
1048 rules = ruleeditor(repo, ui, actions, comment)
1064 rules = ruleeditor(repo, ui, actions, comment)
1049 else:
1065 else:
1050 if rules == '-':
1066 if rules == '-':
1051 f = sys.stdin
1067 f = sys.stdin
1052 else:
1068 else:
1053 f = open(rules)
1069 f = open(rules)
1054 rules = f.read()
1070 rules = f.read()
1055 f.close()
1071 f.close()
1056 actions = parserules(rules, state)
1072 actions = parserules(rules, state)
1057 verifyactions(actions, state, ctxs)
1073 verifyactions(actions, state, ctxs)
1058
1074
1059 parentctxnode = repo[root].parents()[0].node()
1075 parentctxnode = repo[root].parents()[0].node()
1060
1076
1061 state.parentctxnode = parentctxnode
1077 state.parentctxnode = parentctxnode
1062 state.actions = actions
1078 state.actions = actions
1063 state.topmost = topmost
1079 state.topmost = topmost
1064 state.replacements = replacements
1080 state.replacements = replacements
1065
1081
1066 # Create a backup so we can always abort completely.
1082 # Create a backup so we can always abort completely.
1067 backupfile = None
1083 backupfile = None
1068 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1084 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1069 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1085 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1070 'histedit')
1086 'histedit')
1071 state.backupfile = backupfile
1087 state.backupfile = backupfile
1072
1088
1073 # preprocess rules so that we can hide inner folds from the user
1089 # preprocess rules so that we can hide inner folds from the user
1074 # and only show one editor
1090 # and only show one editor
1075 actions = state.actions[:]
1091 actions = state.actions[:]
1076 for idx, (action, nextact) in enumerate(
1092 for idx, (action, nextact) in enumerate(
1077 zip(actions, actions[1:] + [None])):
1093 zip(actions, actions[1:] + [None])):
1078 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1094 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1079 state.actions[idx].__class__ = _multifold
1095 state.actions[idx].__class__ = _multifold
1080
1096
1081 total = len(state.actions)
1097 total = len(state.actions)
1082 pos = 0
1098 pos = 0
1083 while state.actions:
1099 while state.actions:
1084 state.write()
1100 state.write()
1085 actobj = state.actions.pop(0)
1101 actobj = state.actions.pop(0)
1086 pos += 1
1102 pos += 1
1087 ui.progress(_("editing"), pos, actobj.torule(),
1103 ui.progress(_("editing"), pos, actobj.torule(),
1088 _('changes'), total)
1104 _('changes'), total)
1089 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1105 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1090 actobj.torule()))
1106 actobj.torule()))
1091 parentctx, replacement_ = actobj.run()
1107 parentctx, replacement_ = actobj.run()
1092 state.parentctxnode = parentctx.node()
1108 state.parentctxnode = parentctx.node()
1093 state.replacements.extend(replacement_)
1109 state.replacements.extend(replacement_)
1094 state.write()
1110 state.write()
1095 ui.progress(_("editing"), None)
1111 ui.progress(_("editing"), None)
1096
1112
1097 hg.update(repo, state.parentctxnode, quietempty=True)
1113 hg.update(repo, state.parentctxnode, quietempty=True)
1098
1114
1099 mapping, tmpnodes, created, ntm = processreplacement(state)
1115 mapping, tmpnodes, created, ntm = processreplacement(state)
1100 if mapping:
1116 if mapping:
1101 for prec, succs in mapping.iteritems():
1117 for prec, succs in mapping.iteritems():
1102 if not succs:
1118 if not succs:
1103 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1119 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1104 else:
1120 else:
1105 ui.debug('histedit: %s is replaced by %s\n' % (
1121 ui.debug('histedit: %s is replaced by %s\n' % (
1106 node.short(prec), node.short(succs[0])))
1122 node.short(prec), node.short(succs[0])))
1107 if len(succs) > 1:
1123 if len(succs) > 1:
1108 m = 'histedit: %s'
1124 m = 'histedit: %s'
1109 for n in succs[1:]:
1125 for n in succs[1:]:
1110 ui.debug(m % node.short(n))
1126 ui.debug(m % node.short(n))
1111
1127
1112 if supportsmarkers:
1128 if supportsmarkers:
1113 # Only create markers if the temp nodes weren't already removed.
1129 # Only create markers if the temp nodes weren't already removed.
1114 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1130 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1115 if t in repo))
1131 if t in repo))
1116 else:
1132 else:
1117 cleanupnode(ui, repo, 'temp', tmpnodes)
1133 cleanupnode(ui, repo, 'temp', tmpnodes)
1118
1134
1119 if not state.keep:
1135 if not state.keep:
1120 if mapping:
1136 if mapping:
1121 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1137 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1122 # TODO update mq state
1138 # TODO update mq state
1123 if supportsmarkers:
1139 if supportsmarkers:
1124 markers = []
1140 markers = []
1125 # sort by revision number because it sound "right"
1141 # sort by revision number because it sound "right"
1126 for prec in sorted(mapping, key=repo.changelog.rev):
1142 for prec in sorted(mapping, key=repo.changelog.rev):
1127 succs = mapping[prec]
1143 succs = mapping[prec]
1128 markers.append((repo[prec],
1144 markers.append((repo[prec],
1129 tuple(repo[s] for s in succs)))
1145 tuple(repo[s] for s in succs)))
1130 if markers:
1146 if markers:
1131 obsolete.createmarkers(repo, markers)
1147 obsolete.createmarkers(repo, markers)
1132 else:
1148 else:
1133 cleanupnode(ui, repo, 'replaced', mapping)
1149 cleanupnode(ui, repo, 'replaced', mapping)
1134
1150
1135 state.clear()
1151 state.clear()
1136 if os.path.exists(repo.sjoin('undo')):
1152 if os.path.exists(repo.sjoin('undo')):
1137 os.unlink(repo.sjoin('undo'))
1153 os.unlink(repo.sjoin('undo'))
1138
1154
1139 def bootstrapcontinue(ui, state, opts):
1155 def bootstrapcontinue(ui, state, opts):
1140 repo = state.repo
1156 repo = state.repo
1141 if state.actions:
1157 if state.actions:
1142 actobj = state.actions.pop(0)
1158 actobj = state.actions.pop(0)
1143
1159
1144 if _isdirtywc(repo):
1160 if _isdirtywc(repo):
1145 actobj.continuedirty()
1161 actobj.continuedirty()
1146 if _isdirtywc(repo):
1162 if _isdirtywc(repo):
1147 abortdirty()
1163 abortdirty()
1148
1164
1149 parentctx, replacements = actobj.continueclean()
1165 parentctx, replacements = actobj.continueclean()
1150
1166
1151 state.parentctxnode = parentctx.node()
1167 state.parentctxnode = parentctx.node()
1152 state.replacements.extend(replacements)
1168 state.replacements.extend(replacements)
1153
1169
1154 return state
1170 return state
1155
1171
1156 def between(repo, old, new, keep):
1172 def between(repo, old, new, keep):
1157 """select and validate the set of revision to edit
1173 """select and validate the set of revision to edit
1158
1174
1159 When keep is false, the specified set can't have children."""
1175 When keep is false, the specified set can't have children."""
1160 ctxs = list(repo.set('%n::%n', old, new))
1176 ctxs = list(repo.set('%n::%n', old, new))
1161 if ctxs and not keep:
1177 if ctxs and not keep:
1162 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1178 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1163 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1179 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1164 raise error.Abort(_('cannot edit history that would orphan nodes'))
1180 raise error.Abort(_('cannot edit history that would orphan nodes'))
1165 if repo.revs('(%ld) and merge()', ctxs):
1181 if repo.revs('(%ld) and merge()', ctxs):
1166 raise error.Abort(_('cannot edit history that contains merges'))
1182 raise error.Abort(_('cannot edit history that contains merges'))
1167 root = ctxs[0] # list is already sorted by repo.set
1183 root = ctxs[0] # list is already sorted by repo.set
1168 if not root.mutable():
1184 if not root.mutable():
1169 raise error.Abort(_('cannot edit public changeset: %s') % root,
1185 raise error.Abort(_('cannot edit public changeset: %s') % root,
1170 hint=_('see "hg help phases" for details'))
1186 hint=_('see "hg help phases" for details'))
1171 return [c.node() for c in ctxs]
1187 return [c.node() for c in ctxs]
1172
1188
1173 def ruleeditor(repo, ui, actions, editcomment=""):
1189 def ruleeditor(repo, ui, actions, editcomment=""):
1174 """open an editor to edit rules
1190 """open an editor to edit rules
1175
1191
1176 rules are in the format [ [act, ctx], ...] like in state.rules
1192 rules are in the format [ [act, ctx], ...] like in state.rules
1177 """
1193 """
1178 rules = '\n'.join([act.torule() for act in actions])
1194 rules = '\n'.join([act.torule() for act in actions])
1179 rules += '\n\n'
1195 rules += '\n\n'
1180 rules += editcomment
1196 rules += editcomment
1181 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1197 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1182
1198
1183 # Save edit rules in .hg/histedit-last-edit.txt in case
1199 # Save edit rules in .hg/histedit-last-edit.txt in case
1184 # the user needs to ask for help after something
1200 # the user needs to ask for help after something
1185 # surprising happens.
1201 # surprising happens.
1186 f = open(repo.join('histedit-last-edit.txt'), 'w')
1202 f = open(repo.join('histedit-last-edit.txt'), 'w')
1187 f.write(rules)
1203 f.write(rules)
1188 f.close()
1204 f.close()
1189
1205
1190 return rules
1206 return rules
1191
1207
1192 def parserules(rules, state):
1208 def parserules(rules, state):
1193 """Read the histedit rules string and return list of action objects """
1209 """Read the histedit rules string and return list of action objects """
1194 rules = [l for l in (r.strip() for r in rules.splitlines())
1210 rules = [l for l in (r.strip() for r in rules.splitlines())
1195 if l and not l.startswith('#')]
1211 if l and not l.startswith('#')]
1196 actions = []
1212 actions = []
1197 for r in rules:
1213 for r in rules:
1198 if ' ' not in r:
1214 if ' ' not in r:
1199 raise error.Abort(_('malformed line "%s"') % r)
1215 raise error.Abort(_('malformed line "%s"') % r)
1200 verb, rest = r.split(' ', 1)
1216 verb, rest = r.split(' ', 1)
1201
1217
1202 if verb not in actiontable:
1218 if verb not in actiontable:
1203 raise error.Abort(_('unknown action "%s"') % verb)
1219 raise error.Abort(_('unknown action "%s"') % verb)
1204
1220
1205 action = actiontable[verb].fromrule(state, rest)
1221 action = actiontable[verb].fromrule(state, rest)
1206 actions.append(action)
1222 actions.append(action)
1207 return actions
1223 return actions
1208
1224
1209 def verifyactions(actions, state, ctxs):
1225 def verifyactions(actions, state, ctxs):
1210 """Verify that there exists exactly one action per given changeset and
1226 """Verify that there exists exactly one action per given changeset and
1211 other constraints.
1227 other constraints.
1212
1228
1213 Will abort if there are to many or too few rules, a malformed rule,
1229 Will abort if there are to many or too few rules, a malformed rule,
1214 or a rule on a changeset outside of the user-given range.
1230 or a rule on a changeset outside of the user-given range.
1215 """
1231 """
1216 expected = set(c.hex() for c in ctxs)
1232 expected = set(c.hex() for c in ctxs)
1217 seen = set()
1233 seen = set()
1218 prev = None
1234 prev = None
1219 for action in actions:
1235 for action in actions:
1220 action.verify(prev)
1236 action.verify(prev)
1221 prev = action
1237 prev = action
1222 constraints = action.constraints()
1238 constraints = action.constraints()
1223 for constraint in constraints:
1239 for constraint in constraints:
1224 if constraint not in _constraints.known():
1240 if constraint not in _constraints.known():
1225 raise error.Abort(_('unknown constraint "%s"') % constraint)
1241 raise error.Abort(_('unknown constraint "%s"') % constraint)
1226
1242
1227 nodetoverify = action.nodetoverify()
1243 nodetoverify = action.nodetoverify()
1228 if nodetoverify is not None:
1244 if nodetoverify is not None:
1229 ha = node.hex(nodetoverify)
1245 ha = node.hex(nodetoverify)
1230 if _constraints.noother in constraints and ha not in expected:
1246 if _constraints.noother in constraints and ha not in expected:
1231 raise error.Abort(
1247 raise error.Abort(
1232 _('may not use "%s" with changesets '
1248 _('may not use "%s" with changesets '
1233 'other than the ones listed') % action.verb)
1249 'other than the ones listed') % action.verb)
1234 if _constraints.forceother in constraints and ha in expected:
1250 if _constraints.forceother in constraints and ha in expected:
1235 raise error.Abort(
1251 raise error.Abort(
1236 _('may not use "%s" with changesets '
1252 _('may not use "%s" with changesets '
1237 'within the edited list') % action.verb)
1253 'within the edited list') % action.verb)
1238 if _constraints.noduplicates in constraints and ha in seen:
1254 if _constraints.noduplicates in constraints and ha in seen:
1239 raise error.Abort(_('duplicated command for changeset %s') %
1255 raise error.Abort(_('duplicated command for changeset %s') %
1240 ha[:12])
1256 ha[:12])
1241 seen.add(ha)
1257 seen.add(ha)
1242 missing = sorted(expected - seen) # sort to stabilize output
1258 missing = sorted(expected - seen) # sort to stabilize output
1243
1259
1244 if state.repo.ui.configbool('histedit', 'dropmissing'):
1260 if state.repo.ui.configbool('histedit', 'dropmissing'):
1245 drops = [drop(state, node.bin(n)) for n in missing]
1261 drops = [drop(state, node.bin(n)) for n in missing]
1246 # put the in the beginning so they execute immediately and
1262 # put the in the beginning so they execute immediately and
1247 # don't show in the edit-plan in the future
1263 # don't show in the edit-plan in the future
1248 actions[:0] = drops
1264 actions[:0] = drops
1249 elif missing:
1265 elif missing:
1250 raise error.Abort(_('missing rules for changeset %s') %
1266 raise error.Abort(_('missing rules for changeset %s') %
1251 missing[0][:12],
1267 missing[0][:12],
1252 hint=_('use "drop %s" to discard, see also: '
1268 hint=_('use "drop %s" to discard, see also: '
1253 '"hg help -e histedit.config"') % missing[0][:12])
1269 '"hg help -e histedit.config"') % missing[0][:12])
1254
1270
1255 def newnodestoabort(state):
1271 def newnodestoabort(state):
1256 """process the list of replacements to return
1272 """process the list of replacements to return
1257
1273
1258 1) the list of final node
1274 1) the list of final node
1259 2) the list of temporary node
1275 2) the list of temporary node
1260
1276
1261 This meant to be used on abort as less data are required in this case.
1277 This meant to be used on abort as less data are required in this case.
1262 """
1278 """
1263 replacements = state.replacements
1279 replacements = state.replacements
1264 allsuccs = set()
1280 allsuccs = set()
1265 replaced = set()
1281 replaced = set()
1266 for rep in replacements:
1282 for rep in replacements:
1267 allsuccs.update(rep[1])
1283 allsuccs.update(rep[1])
1268 replaced.add(rep[0])
1284 replaced.add(rep[0])
1269 newnodes = allsuccs - replaced
1285 newnodes = allsuccs - replaced
1270 tmpnodes = allsuccs & replaced
1286 tmpnodes = allsuccs & replaced
1271 return newnodes, tmpnodes
1287 return newnodes, tmpnodes
1272
1288
1273
1289
1274 def processreplacement(state):
1290 def processreplacement(state):
1275 """process the list of replacements to return
1291 """process the list of replacements to return
1276
1292
1277 1) the final mapping between original and created nodes
1293 1) the final mapping between original and created nodes
1278 2) the list of temporary node created by histedit
1294 2) the list of temporary node created by histedit
1279 3) the list of new commit created by histedit"""
1295 3) the list of new commit created by histedit"""
1280 replacements = state.replacements
1296 replacements = state.replacements
1281 allsuccs = set()
1297 allsuccs = set()
1282 replaced = set()
1298 replaced = set()
1283 fullmapping = {}
1299 fullmapping = {}
1284 # initialize basic set
1300 # initialize basic set
1285 # fullmapping records all operations recorded in replacement
1301 # fullmapping records all operations recorded in replacement
1286 for rep in replacements:
1302 for rep in replacements:
1287 allsuccs.update(rep[1])
1303 allsuccs.update(rep[1])
1288 replaced.add(rep[0])
1304 replaced.add(rep[0])
1289 fullmapping.setdefault(rep[0], set()).update(rep[1])
1305 fullmapping.setdefault(rep[0], set()).update(rep[1])
1290 new = allsuccs - replaced
1306 new = allsuccs - replaced
1291 tmpnodes = allsuccs & replaced
1307 tmpnodes = allsuccs & replaced
1292 # Reduce content fullmapping into direct relation between original nodes
1308 # Reduce content fullmapping into direct relation between original nodes
1293 # and final node created during history edition
1309 # and final node created during history edition
1294 # Dropped changeset are replaced by an empty list
1310 # Dropped changeset are replaced by an empty list
1295 toproceed = set(fullmapping)
1311 toproceed = set(fullmapping)
1296 final = {}
1312 final = {}
1297 while toproceed:
1313 while toproceed:
1298 for x in list(toproceed):
1314 for x in list(toproceed):
1299 succs = fullmapping[x]
1315 succs = fullmapping[x]
1300 for s in list(succs):
1316 for s in list(succs):
1301 if s in toproceed:
1317 if s in toproceed:
1302 # non final node with unknown closure
1318 # non final node with unknown closure
1303 # We can't process this now
1319 # We can't process this now
1304 break
1320 break
1305 elif s in final:
1321 elif s in final:
1306 # non final node, replace with closure
1322 # non final node, replace with closure
1307 succs.remove(s)
1323 succs.remove(s)
1308 succs.update(final[s])
1324 succs.update(final[s])
1309 else:
1325 else:
1310 final[x] = succs
1326 final[x] = succs
1311 toproceed.remove(x)
1327 toproceed.remove(x)
1312 # remove tmpnodes from final mapping
1328 # remove tmpnodes from final mapping
1313 for n in tmpnodes:
1329 for n in tmpnodes:
1314 del final[n]
1330 del final[n]
1315 # we expect all changes involved in final to exist in the repo
1331 # we expect all changes involved in final to exist in the repo
1316 # turn `final` into list (topologically sorted)
1332 # turn `final` into list (topologically sorted)
1317 nm = state.repo.changelog.nodemap
1333 nm = state.repo.changelog.nodemap
1318 for prec, succs in final.items():
1334 for prec, succs in final.items():
1319 final[prec] = sorted(succs, key=nm.get)
1335 final[prec] = sorted(succs, key=nm.get)
1320
1336
1321 # computed topmost element (necessary for bookmark)
1337 # computed topmost element (necessary for bookmark)
1322 if new:
1338 if new:
1323 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1339 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1324 elif not final:
1340 elif not final:
1325 # Nothing rewritten at all. we won't need `newtopmost`
1341 # Nothing rewritten at all. we won't need `newtopmost`
1326 # It is the same as `oldtopmost` and `processreplacement` know it
1342 # It is the same as `oldtopmost` and `processreplacement` know it
1327 newtopmost = None
1343 newtopmost = None
1328 else:
1344 else:
1329 # every body died. The newtopmost is the parent of the root.
1345 # every body died. The newtopmost is the parent of the root.
1330 r = state.repo.changelog.rev
1346 r = state.repo.changelog.rev
1331 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1347 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1332
1348
1333 return final, tmpnodes, new, newtopmost
1349 return final, tmpnodes, new, newtopmost
1334
1350
1335 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1351 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1336 """Move bookmark from old to newly created node"""
1352 """Move bookmark from old to newly created node"""
1337 if not mapping:
1353 if not mapping:
1338 # if nothing got rewritten there is not purpose for this function
1354 # if nothing got rewritten there is not purpose for this function
1339 return
1355 return
1340 moves = []
1356 moves = []
1341 for bk, old in sorted(repo._bookmarks.iteritems()):
1357 for bk, old in sorted(repo._bookmarks.iteritems()):
1342 if old == oldtopmost:
1358 if old == oldtopmost:
1343 # special case ensure bookmark stay on tip.
1359 # special case ensure bookmark stay on tip.
1344 #
1360 #
1345 # This is arguably a feature and we may only want that for the
1361 # This is arguably a feature and we may only want that for the
1346 # active bookmark. But the behavior is kept compatible with the old
1362 # active bookmark. But the behavior is kept compatible with the old
1347 # version for now.
1363 # version for now.
1348 moves.append((bk, newtopmost))
1364 moves.append((bk, newtopmost))
1349 continue
1365 continue
1350 base = old
1366 base = old
1351 new = mapping.get(base, None)
1367 new = mapping.get(base, None)
1352 if new is None:
1368 if new is None:
1353 continue
1369 continue
1354 while not new:
1370 while not new:
1355 # base is killed, trying with parent
1371 # base is killed, trying with parent
1356 base = repo[base].p1().node()
1372 base = repo[base].p1().node()
1357 new = mapping.get(base, (base,))
1373 new = mapping.get(base, (base,))
1358 # nothing to move
1374 # nothing to move
1359 moves.append((bk, new[-1]))
1375 moves.append((bk, new[-1]))
1360 if moves:
1376 if moves:
1361 lock = tr = None
1377 lock = tr = None
1362 try:
1378 try:
1363 lock = repo.lock()
1379 lock = repo.lock()
1364 tr = repo.transaction('histedit')
1380 tr = repo.transaction('histedit')
1365 marks = repo._bookmarks
1381 marks = repo._bookmarks
1366 for mark, new in moves:
1382 for mark, new in moves:
1367 old = marks[mark]
1383 old = marks[mark]
1368 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1384 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1369 % (mark, node.short(old), node.short(new)))
1385 % (mark, node.short(old), node.short(new)))
1370 marks[mark] = new
1386 marks[mark] = new
1371 marks.recordchange(tr)
1387 marks.recordchange(tr)
1372 tr.close()
1388 tr.close()
1373 finally:
1389 finally:
1374 release(tr, lock)
1390 release(tr, lock)
1375
1391
1376 def cleanupnode(ui, repo, name, nodes):
1392 def cleanupnode(ui, repo, name, nodes):
1377 """strip a group of nodes from the repository
1393 """strip a group of nodes from the repository
1378
1394
1379 The set of node to strip may contains unknown nodes."""
1395 The set of node to strip may contains unknown nodes."""
1380 ui.debug('should strip %s nodes %s\n' %
1396 ui.debug('should strip %s nodes %s\n' %
1381 (name, ', '.join([node.short(n) for n in nodes])))
1397 (name, ', '.join([node.short(n) for n in nodes])))
1382 lock = None
1398 lock = None
1383 try:
1399 try:
1384 lock = repo.lock()
1400 lock = repo.lock()
1385 # do not let filtering get in the way of the cleanse
1401 # do not let filtering get in the way of the cleanse
1386 # we should probably get rid of obsolescence marker created during the
1402 # we should probably get rid of obsolescence marker created during the
1387 # histedit, but we currently do not have such information.
1403 # histedit, but we currently do not have such information.
1388 repo = repo.unfiltered()
1404 repo = repo.unfiltered()
1389 # Find all nodes that need to be stripped
1405 # Find all nodes that need to be stripped
1390 # (we use %lr instead of %ln to silently ignore unknown items)
1406 # (we use %lr instead of %ln to silently ignore unknown items)
1391 nm = repo.changelog.nodemap
1407 nm = repo.changelog.nodemap
1392 nodes = sorted(n for n in nodes if n in nm)
1408 nodes = sorted(n for n in nodes if n in nm)
1393 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1409 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1394 for c in roots:
1410 for c in roots:
1395 # We should process node in reverse order to strip tip most first.
1411 # We should process node in reverse order to strip tip most first.
1396 # but this trigger a bug in changegroup hook.
1412 # but this trigger a bug in changegroup hook.
1397 # This would reduce bundle overhead
1413 # This would reduce bundle overhead
1398 repair.strip(ui, repo, c)
1414 repair.strip(ui, repo, c)
1399 finally:
1415 finally:
1400 release(lock)
1416 release(lock)
1401
1417
1402 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1418 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1403 if isinstance(nodelist, str):
1419 if isinstance(nodelist, str):
1404 nodelist = [nodelist]
1420 nodelist = [nodelist]
1405 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1421 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1406 state = histeditstate(repo)
1422 state = histeditstate(repo)
1407 state.read()
1423 state.read()
1408 histedit_nodes = set([action.nodetoverify() for action
1424 histedit_nodes = set([action.nodetoverify() for action
1409 in state.actions if action.nodetoverify()])
1425 in state.actions if action.nodetoverify()])
1410 strip_nodes = set([repo[n].node() for n in nodelist])
1426 strip_nodes = set([repo[n].node() for n in nodelist])
1411 common_nodes = histedit_nodes & strip_nodes
1427 common_nodes = histedit_nodes & strip_nodes
1412 if common_nodes:
1428 if common_nodes:
1413 raise error.Abort(_("histedit in progress, can't strip %s")
1429 raise error.Abort(_("histedit in progress, can't strip %s")
1414 % ', '.join(node.short(x) for x in common_nodes))
1430 % ', '.join(node.short(x) for x in common_nodes))
1415 return orig(ui, repo, nodelist, *args, **kwargs)
1431 return orig(ui, repo, nodelist, *args, **kwargs)
1416
1432
1417 extensions.wrapfunction(repair, 'strip', stripwrapper)
1433 extensions.wrapfunction(repair, 'strip', stripwrapper)
1418
1434
1419 def summaryhook(ui, repo):
1435 def summaryhook(ui, repo):
1420 if not os.path.exists(repo.join('histedit-state')):
1436 if not os.path.exists(repo.join('histedit-state')):
1421 return
1437 return
1422 state = histeditstate(repo)
1438 state = histeditstate(repo)
1423 state.read()
1439 state.read()
1424 if state.actions:
1440 if state.actions:
1425 # i18n: column positioning for "hg summary"
1441 # i18n: column positioning for "hg summary"
1426 ui.write(_('hist: %s (histedit --continue)\n') %
1442 ui.write(_('hist: %s (histedit --continue)\n') %
1427 (ui.label(_('%d remaining'), 'histedit.remaining') %
1443 (ui.label(_('%d remaining'), 'histedit.remaining') %
1428 len(state.actions)))
1444 len(state.actions)))
1429
1445
1430 def extsetup(ui):
1446 def extsetup(ui):
1431 cmdutil.summaryhooks.add('histedit', summaryhook)
1447 cmdutil.summaryhooks.add('histedit', summaryhook)
1432 cmdutil.unfinishedstates.append(
1448 cmdutil.unfinishedstates.append(
1433 ['histedit-state', False, True, _('histedit in progress'),
1449 ['histedit-state', False, True, _('histedit in progress'),
1434 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1450 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1435 if ui.configbool("experimental", "histeditng"):
1451 if ui.configbool("experimental", "histeditng"):
1436 globals()['base'] = addhisteditaction(['base', 'b'])(base)
1452 globals()['base'] = addhisteditaction(['base', 'b'])(base)
@@ -1,477 +1,471 b''
1 $ . "$TESTDIR/histedit-helpers.sh"
1 $ . "$TESTDIR/histedit-helpers.sh"
2
2
3 $ cat >> $HGRCPATH <<EOF
3 $ cat >> $HGRCPATH <<EOF
4 > [extensions]
4 > [extensions]
5 > histedit=
5 > histedit=
6 > strip=
6 > strip=
7 > EOF
7 > EOF
8
8
9 $ initrepo ()
9 $ initrepo ()
10 > {
10 > {
11 > hg init r
11 > hg init r
12 > cd r
12 > cd r
13 > for x in a b c d e f g; do
13 > for x in a b c d e f g; do
14 > echo $x > $x
14 > echo $x > $x
15 > hg add $x
15 > hg add $x
16 > hg ci -m $x
16 > hg ci -m $x
17 > done
17 > done
18 > }
18 > }
19
19
20 $ initrepo
20 $ initrepo
21
21
22 log before edit
22 log before edit
23 $ hg log --graph
23 $ hg log --graph
24 @ changeset: 6:3c6a8ed2ebe8
24 @ changeset: 6:3c6a8ed2ebe8
25 | tag: tip
25 | tag: tip
26 | user: test
26 | user: test
27 | date: Thu Jan 01 00:00:00 1970 +0000
27 | date: Thu Jan 01 00:00:00 1970 +0000
28 | summary: g
28 | summary: g
29 |
29 |
30 o changeset: 5:652413bf663e
30 o changeset: 5:652413bf663e
31 | user: test
31 | user: test
32 | date: Thu Jan 01 00:00:00 1970 +0000
32 | date: Thu Jan 01 00:00:00 1970 +0000
33 | summary: f
33 | summary: f
34 |
34 |
35 o changeset: 4:e860deea161a
35 o changeset: 4:e860deea161a
36 | user: test
36 | user: test
37 | date: Thu Jan 01 00:00:00 1970 +0000
37 | date: Thu Jan 01 00:00:00 1970 +0000
38 | summary: e
38 | summary: e
39 |
39 |
40 o changeset: 3:055a42cdd887
40 o changeset: 3:055a42cdd887
41 | user: test
41 | user: test
42 | date: Thu Jan 01 00:00:00 1970 +0000
42 | date: Thu Jan 01 00:00:00 1970 +0000
43 | summary: d
43 | summary: d
44 |
44 |
45 o changeset: 2:177f92b77385
45 o changeset: 2:177f92b77385
46 | user: test
46 | user: test
47 | date: Thu Jan 01 00:00:00 1970 +0000
47 | date: Thu Jan 01 00:00:00 1970 +0000
48 | summary: c
48 | summary: c
49 |
49 |
50 o changeset: 1:d2ae7f538514
50 o changeset: 1:d2ae7f538514
51 | user: test
51 | user: test
52 | date: Thu Jan 01 00:00:00 1970 +0000
52 | date: Thu Jan 01 00:00:00 1970 +0000
53 | summary: b
53 | summary: b
54 |
54 |
55 o changeset: 0:cb9a9f314b8b
55 o changeset: 0:cb9a9f314b8b
56 user: test
56 user: test
57 date: Thu Jan 01 00:00:00 1970 +0000
57 date: Thu Jan 01 00:00:00 1970 +0000
58 summary: a
58 summary: a
59
59
60
60
61 edit the history
61 edit the history
62 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
62 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
63 > pick 177f92b77385 c
63 > pick 177f92b77385 c
64 > pick 055a42cdd887 d
64 > pick 055a42cdd887 d
65 > edit e860deea161a e
65 > edit e860deea161a e
66 > pick 652413bf663e f
66 > pick 652413bf663e f
67 > pick 3c6a8ed2ebe8 g
67 > pick 3c6a8ed2ebe8 g
68 > EOF
68 > EOF
69 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
69 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
70 Make changes as needed, you may commit or record as needed now.
70 Make changes as needed, you may commit or record as needed now.
71 When you are finished, run hg histedit --continue to resume.
71 When you are finished, run hg histedit --continue to resume.
72
72
73 edit the plan via the editor
73 edit the plan via the editor
74 $ cat >> $TESTTMP/editplan.sh <<EOF
74 $ cat >> $TESTTMP/editplan.sh <<EOF
75 > cat > \$1 <<EOF2
75 > cat > \$1 <<EOF2
76 > drop e860deea161a e
76 > drop e860deea161a e
77 > drop 652413bf663e f
77 > drop 652413bf663e f
78 > drop 3c6a8ed2ebe8 g
78 > drop 3c6a8ed2ebe8 g
79 > EOF2
79 > EOF2
80 > EOF
80 > EOF
81 $ HGEDITOR="sh $TESTTMP/editplan.sh" hg histedit --edit-plan
81 $ HGEDITOR="sh $TESTTMP/editplan.sh" hg histedit --edit-plan
82 $ cat .hg/histedit-state
82 $ cat .hg/histedit-state
83 v1
83 v1
84 055a42cdd88768532f9cf79daa407fc8d138de9b
84 055a42cdd88768532f9cf79daa407fc8d138de9b
85 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
85 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
86 False
86 False
87 3
87 3
88 drop
88 drop
89 e860deea161a2f77de56603b340ebbb4536308ae
89 e860deea161a2f77de56603b340ebbb4536308ae
90 drop
90 drop
91 652413bf663ef2a641cab26574e46d5f5a64a55a
91 652413bf663ef2a641cab26574e46d5f5a64a55a
92 drop
92 drop
93 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
93 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
94 0
94 0
95 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
95 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
96
96
97 edit the plan via --commands
97 edit the plan via --commands
98 $ hg histedit --edit-plan --commands - 2>&1 << EOF
98 $ hg histedit --edit-plan --commands - 2>&1 << EOF
99 > edit e860deea161a e
99 > edit e860deea161a e
100 > pick 652413bf663e f
100 > pick 652413bf663e f
101 > drop 3c6a8ed2ebe8 g
101 > drop 3c6a8ed2ebe8 g
102 > EOF
102 > EOF
103 $ cat .hg/histedit-state
103 $ cat .hg/histedit-state
104 v1
104 v1
105 055a42cdd88768532f9cf79daa407fc8d138de9b
105 055a42cdd88768532f9cf79daa407fc8d138de9b
106 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
106 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
107 False
107 False
108 3
108 3
109 edit
109 edit
110 e860deea161a2f77de56603b340ebbb4536308ae
110 e860deea161a2f77de56603b340ebbb4536308ae
111 pick
111 pick
112 652413bf663ef2a641cab26574e46d5f5a64a55a
112 652413bf663ef2a641cab26574e46d5f5a64a55a
113 drop
113 drop
114 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
114 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
115 0
115 0
116 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
116 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
117
117
118 Go at a random point and try to continue
118 Go at a random point and try to continue
119
119
120 $ hg id -n
120 $ hg id -n
121 3+
121 3+
122 $ hg up 0
122 $ hg up 0
123 abort: histedit in progress
123 abort: histedit in progress
124 (use 'hg histedit --continue' or 'hg histedit --abort')
124 (use 'hg histedit --continue' or 'hg histedit --abort')
125 [255]
125 [255]
126
126
127 Try to delete necessary commit
127 Try to delete necessary commit
128 $ hg strip -r 652413b
128 $ hg strip -r 652413b
129 abort: histedit in progress, can't strip 652413bf663e
129 abort: histedit in progress, can't strip 652413bf663e
130 [255]
130 [255]
131
131
132 commit, then edit the revision
132 commit, then edit the revision
133 $ hg ci -m 'wat'
133 $ hg ci -m 'wat'
134 created new head
134 created new head
135 $ echo a > e
135 $ echo a > e
136
136
137 qnew should fail while we're in the middle of the edit step
137 qnew should fail while we're in the middle of the edit step
138
138
139 $ hg --config extensions.mq= qnew please-fail
139 $ hg --config extensions.mq= qnew please-fail
140 abort: histedit in progress
140 abort: histedit in progress
141 (use 'hg histedit --continue' or 'hg histedit --abort')
141 (use 'hg histedit --continue' or 'hg histedit --abort')
142 [255]
142 [255]
143 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
143 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
144
144
145 $ hg log --graph
145 $ hg log --graph
146 @ changeset: 6:b5f70786f9b0
146 @ changeset: 6:b5f70786f9b0
147 | tag: tip
147 | tag: tip
148 | user: test
148 | user: test
149 | date: Thu Jan 01 00:00:00 1970 +0000
149 | date: Thu Jan 01 00:00:00 1970 +0000
150 | summary: f
150 | summary: f
151 |
151 |
152 o changeset: 5:a5e1ba2f7afb
152 o changeset: 5:a5e1ba2f7afb
153 | user: test
153 | user: test
154 | date: Thu Jan 01 00:00:00 1970 +0000
154 | date: Thu Jan 01 00:00:00 1970 +0000
155 | summary: foobaz
155 | summary: foobaz
156 |
156 |
157 o changeset: 4:1a60820cd1f6
157 o changeset: 4:1a60820cd1f6
158 | user: test
158 | user: test
159 | date: Thu Jan 01 00:00:00 1970 +0000
159 | date: Thu Jan 01 00:00:00 1970 +0000
160 | summary: wat
160 | summary: wat
161 |
161 |
162 o changeset: 3:055a42cdd887
162 o changeset: 3:055a42cdd887
163 | user: test
163 | user: test
164 | date: Thu Jan 01 00:00:00 1970 +0000
164 | date: Thu Jan 01 00:00:00 1970 +0000
165 | summary: d
165 | summary: d
166 |
166 |
167 o changeset: 2:177f92b77385
167 o changeset: 2:177f92b77385
168 | user: test
168 | user: test
169 | date: Thu Jan 01 00:00:00 1970 +0000
169 | date: Thu Jan 01 00:00:00 1970 +0000
170 | summary: c
170 | summary: c
171 |
171 |
172 o changeset: 1:d2ae7f538514
172 o changeset: 1:d2ae7f538514
173 | user: test
173 | user: test
174 | date: Thu Jan 01 00:00:00 1970 +0000
174 | date: Thu Jan 01 00:00:00 1970 +0000
175 | summary: b
175 | summary: b
176 |
176 |
177 o changeset: 0:cb9a9f314b8b
177 o changeset: 0:cb9a9f314b8b
178 user: test
178 user: test
179 date: Thu Jan 01 00:00:00 1970 +0000
179 date: Thu Jan 01 00:00:00 1970 +0000
180 summary: a
180 summary: a
181
181
182
182
183 $ hg cat e
183 $ hg cat e
184 a
184 a
185
185
186 Stripping necessary commits should not break --abort
186 Stripping necessary commits should not break --abort
187
187
188 $ hg histedit 1a60820cd1f6 --commands - 2>&1 << EOF| fixbundle
188 $ hg histedit 1a60820cd1f6 --commands - 2>&1 << EOF| fixbundle
189 > edit 1a60820cd1f6 wat
189 > edit 1a60820cd1f6 wat
190 > pick a5e1ba2f7afb foobaz
190 > pick a5e1ba2f7afb foobaz
191 > pick b5f70786f9b0 g
191 > pick b5f70786f9b0 g
192 > EOF
192 > EOF
193 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
193 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
194 Make changes as needed, you may commit or record as needed now.
194 Make changes as needed, you may commit or record as needed now.
195 When you are finished, run hg histedit --continue to resume.
195 When you are finished, run hg histedit --continue to resume.
196
196
197 $ mv .hg/histedit-state .hg/histedit-state.bak
197 $ mv .hg/histedit-state .hg/histedit-state.bak
198 $ hg strip -q -r b5f70786f9b0
198 $ hg strip -q -r b5f70786f9b0
199 $ mv .hg/histedit-state.bak .hg/histedit-state
199 $ mv .hg/histedit-state.bak .hg/histedit-state
200 $ hg histedit --abort
200 $ hg histedit --abort
201 adding changesets
201 adding changesets
202 adding manifests
202 adding manifests
203 adding file changes
203 adding file changes
204 added 1 changesets with 1 changes to 3 files
204 added 1 changesets with 1 changes to 3 files
205 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
205 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
206 $ hg log -r .
206 $ hg log -r .
207 changeset: 6:b5f70786f9b0
207 changeset: 6:b5f70786f9b0
208 tag: tip
208 tag: tip
209 user: test
209 user: test
210 date: Thu Jan 01 00:00:00 1970 +0000
210 date: Thu Jan 01 00:00:00 1970 +0000
211 summary: f
211 summary: f
212
212
213
213
214 check histedit_source
214 check histedit_source
215
215
216 $ hg log --debug --rev 5
216 $ hg log --debug --rev 5
217 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
217 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
218 phase: draft
218 phase: draft
219 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
219 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
220 parent: -1:0000000000000000000000000000000000000000
220 parent: -1:0000000000000000000000000000000000000000
221 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
221 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
222 user: test
222 user: test
223 date: Thu Jan 01 00:00:00 1970 +0000
223 date: Thu Jan 01 00:00:00 1970 +0000
224 files: e
224 files: e
225 extra: branch=default
225 extra: branch=default
226 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
226 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
227 description:
227 description:
228 foobaz
228 foobaz
229
229
230
230
231
231
232 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
232 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
233 > edit b5f70786f9b0 f
233 > edit b5f70786f9b0 f
234 > EOF
234 > EOF
235 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
235 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
236 Make changes as needed, you may commit or record as needed now.
236 Make changes as needed, you may commit or record as needed now.
237 When you are finished, run hg histedit --continue to resume.
237 When you are finished, run hg histedit --continue to resume.
238 $ hg status
238 $ hg status
239 A f
239 A f
240
240
241 $ hg summary
241 $ hg summary
242 parent: 5:a5e1ba2f7afb
242 parent: 5:a5e1ba2f7afb
243 foobaz
243 foobaz
244 branch: default
244 branch: default
245 commit: 1 added (new branch head)
245 commit: 1 added (new branch head)
246 update: 1 new changesets (update)
246 update: 1 new changesets (update)
247 phases: 7 draft
247 phases: 7 draft
248 hist: 1 remaining (histedit --continue)
248 hist: 1 remaining (histedit --continue)
249
249
250 (test also that editor is invoked if histedit is continued for
250 (test also that editor is invoked if histedit is continued for
251 "edit" action)
251 "edit" action)
252
252
253 $ HGEDITOR='cat' hg histedit --continue
253 $ HGEDITOR='cat' hg histedit --continue
254 f
254 f
255
255
256
256
257 HG: Enter commit message. Lines beginning with 'HG:' are removed.
257 HG: Enter commit message. Lines beginning with 'HG:' are removed.
258 HG: Leave message empty to abort commit.
258 HG: Leave message empty to abort commit.
259 HG: --
259 HG: --
260 HG: user: test
260 HG: user: test
261 HG: branch 'default'
261 HG: branch 'default'
262 HG: added f
262 HG: added f
263 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-backup.hg (glob)
263 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-backup.hg (glob)
264
264
265 $ hg status
265 $ hg status
266
266
267 log after edit
267 log after edit
268 $ hg log --limit 1
268 $ hg log --limit 1
269 changeset: 6:a107ee126658
269 changeset: 6:a107ee126658
270 tag: tip
270 tag: tip
271 user: test
271 user: test
272 date: Thu Jan 01 00:00:00 1970 +0000
272 date: Thu Jan 01 00:00:00 1970 +0000
273 summary: f
273 summary: f
274
274
275
275
276 say we'll change the message, but don't.
276 say we'll change the message, but don't.
277 $ cat > ../edit.sh <<EOF
277 $ cat > ../edit.sh <<EOF
278 > cat "\$1" | sed s/pick/mess/ > tmp
278 > cat "\$1" | sed s/pick/mess/ > tmp
279 > mv tmp "\$1"
279 > mv tmp "\$1"
280 > EOF
280 > EOF
281 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
281 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
282 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
282 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
283 $ hg status
283 $ hg status
284 $ hg log --limit 1
284 $ hg log --limit 1
285 changeset: 6:1fd3b2fe7754
285 changeset: 6:1fd3b2fe7754
286 tag: tip
286 tag: tip
287 user: test
287 user: test
288 date: Thu Jan 01 00:00:00 1970 +0000
288 date: Thu Jan 01 00:00:00 1970 +0000
289 summary: f
289 summary: f
290
290
291
291
292 modify the message
292 modify the message
293
293
294 check saving last-message.txt, at first
294 check saving last-message.txt, at first
295
295
296 $ cat > $TESTTMP/commitfailure.py <<EOF
296 $ cat > $TESTTMP/commitfailure.py <<EOF
297 > from mercurial import error
297 > from mercurial import error
298 > def reposetup(ui, repo):
298 > def reposetup(ui, repo):
299 > class commitfailure(repo.__class__):
299 > class commitfailure(repo.__class__):
300 > def commit(self, *args, **kwargs):
300 > def commit(self, *args, **kwargs):
301 > raise error.Abort('emulating unexpected abort')
301 > raise error.Abort('emulating unexpected abort')
302 > repo.__class__ = commitfailure
302 > repo.__class__ = commitfailure
303 > EOF
303 > EOF
304 $ cat >> .hg/hgrc <<EOF
304 $ cat >> .hg/hgrc <<EOF
305 > [extensions]
305 > [extensions]
306 > # this failure occurs before editor invocation
306 > # this failure occurs before editor invocation
307 > commitfailure = $TESTTMP/commitfailure.py
307 > commitfailure = $TESTTMP/commitfailure.py
308 > EOF
308 > EOF
309
309
310 $ cat > $TESTTMP/editor.sh <<EOF
310 $ cat > $TESTTMP/editor.sh <<EOF
311 > echo "==== before editing"
311 > echo "==== before editing"
312 > cat \$1
312 > cat \$1
313 > echo "===="
313 > echo "===="
314 > echo "check saving last-message.txt" >> \$1
314 > echo "check saving last-message.txt" >> \$1
315 > EOF
315 > EOF
316
316
317 (test that editor is not invoked before transaction starting)
317 (test that editor is not invoked before transaction starting)
318
318
319 $ rm -f .hg/last-message.txt
319 $ rm -f .hg/last-message.txt
320 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
320 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
321 > mess 1fd3b2fe7754 f
321 > mess 1fd3b2fe7754 f
322 > EOF
322 > EOF
323 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
323 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
324 abort: emulating unexpected abort
324 abort: emulating unexpected abort
325 $ test -f .hg/last-message.txt
325 $ test -f .hg/last-message.txt
326 [1]
326 [1]
327
327
328 $ cat >> .hg/hgrc <<EOF
328 $ cat >> .hg/hgrc <<EOF
329 > [extensions]
329 > [extensions]
330 > commitfailure = !
330 > commitfailure = !
331 > EOF
331 > EOF
332 $ hg histedit --abort -q
332 $ hg histedit --abort -q
333
333
334 (test that editor is invoked and commit message is saved into
334 (test that editor is invoked and commit message is saved into
335 "last-message.txt")
335 "last-message.txt")
336
336
337 $ cat >> .hg/hgrc <<EOF
337 $ cat >> .hg/hgrc <<EOF
338 > [hooks]
338 > [hooks]
339 > # this failure occurs after editor invocation
339 > # this failure occurs after editor invocation
340 > pretxncommit.unexpectedabort = false
340 > pretxncommit.unexpectedabort = false
341 > EOF
341 > EOF
342
342
343 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
343 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
344 A f
344 A f
345
345
346 $ rm -f .hg/last-message.txt
346 $ rm -f .hg/last-message.txt
347 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
347 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
348 > mess 1fd3b2fe7754 f
348 > mess 1fd3b2fe7754 f
349 > EOF
349 > EOF
350 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
350 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
351 adding f
351 adding f
352 ==== before editing
352 ==== before editing
353 f
353 f
354
354
355
355
356 HG: Enter commit message. Lines beginning with 'HG:' are removed.
356 HG: Enter commit message. Lines beginning with 'HG:' are removed.
357 HG: Leave message empty to abort commit.
357 HG: Leave message empty to abort commit.
358 HG: --
358 HG: --
359 HG: user: test
359 HG: user: test
360 HG: branch 'default'
360 HG: branch 'default'
361 HG: added f
361 HG: added f
362 ====
362 ====
363 note: commit message saved in .hg/last-message.txt
363 note: commit message saved in .hg/last-message.txt
364 transaction abort!
364 transaction abort!
365 rollback completed
365 rollback completed
366 abort: pretxncommit.unexpectedabort hook exited with status 1
366 abort: pretxncommit.unexpectedabort hook exited with status 1
367 [255]
367 [255]
368 $ cat .hg/last-message.txt
368 $ cat .hg/last-message.txt
369 f
369 f
370
370
371
371
372 check saving last-message.txt
372 check saving last-message.txt
373
373
374 (test also that editor is invoked if histedit is continued for "message"
374 (test also that editor is invoked if histedit is continued for "message"
375 action)
375 action)
376
376
377 $ HGEDITOR=cat hg histedit --continue
377 $ HGEDITOR=cat hg histedit --continue
378 f
378 f
379
379
380
380
381 HG: Enter commit message. Lines beginning with 'HG:' are removed.
381 HG: Enter commit message. Lines beginning with 'HG:' are removed.
382 HG: Leave message empty to abort commit.
382 HG: Leave message empty to abort commit.
383 HG: --
383 HG: --
384 HG: user: test
384 HG: user: test
385 HG: branch 'default'
385 HG: branch 'default'
386 HG: added f
386 HG: added f
387 note: commit message saved in .hg/last-message.txt
387 note: commit message saved in .hg/last-message.txt
388 transaction abort!
388 transaction abort!
389 rollback completed
389 rollback completed
390 abort: pretxncommit.unexpectedabort hook exited with status 1
390 abort: pretxncommit.unexpectedabort hook exited with status 1
391 [255]
391 [255]
392
392
393 $ cat >> .hg/hgrc <<EOF
393 $ cat >> .hg/hgrc <<EOF
394 > [hooks]
394 > [hooks]
395 > pretxncommit.unexpectedabort =
395 > pretxncommit.unexpectedabort =
396 > EOF
396 > EOF
397 $ hg histedit --abort -q
397 $ hg histedit --abort -q
398
398
399 then, check "modify the message" itself
399 then, check "modify the message" itself
400
400
401 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
401 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
402 > mess 1fd3b2fe7754 f
402 > mess 1fd3b2fe7754 f
403 > EOF
403 > EOF
404 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
404 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
405 $ hg status
405 $ hg status
406 $ hg log --limit 1
406 $ hg log --limit 1
407 changeset: 6:62feedb1200e
407 changeset: 6:62feedb1200e
408 tag: tip
408 tag: tip
409 user: test
409 user: test
410 date: Thu Jan 01 00:00:00 1970 +0000
410 date: Thu Jan 01 00:00:00 1970 +0000
411 summary: f
411 summary: f
412
412
413
413
414 rollback should not work after a histedit
414 rollback should not work after a histedit
415 $ hg rollback
415 $ hg rollback
416 no rollback information available
416 no rollback information available
417 [1]
417 [1]
418
418
419 $ cd ..
419 $ cd ..
420 $ hg clone -qr0 r r0
420 $ hg clone -qr0 r r0
421 $ cd r0
421 $ cd r0
422 $ hg phase -fdr0
422 $ hg phase -fdr0
423 $ hg histedit --commands - 0 2>&1 << EOF
423 $ hg histedit --commands - 0 2>&1 << EOF
424 > edit cb9a9f314b8b a > $EDITED
424 > edit cb9a9f314b8b a > $EDITED
425 > EOF
425 > EOF
426 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
426 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
427 adding a
427 adding a
428 Make changes as needed, you may commit or record as needed now.
428 Make changes as needed, you may commit or record as needed now.
429 When you are finished, run hg histedit --continue to resume.
429 When you are finished, run hg histedit --continue to resume.
430 [1]
430 [1]
431 $ HGEDITOR=true hg histedit --continue
431 $ HGEDITOR=true hg histedit --continue
432 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-backup.hg (glob)
432 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-backup.hg (glob)
433
433
434 $ hg log -G
434 $ hg log -G
435 @ changeset: 0:0efcea34f18a
435 @ changeset: 0:0efcea34f18a
436 tag: tip
436 tag: tip
437 user: test
437 user: test
438 date: Thu Jan 01 00:00:00 1970 +0000
438 date: Thu Jan 01 00:00:00 1970 +0000
439 summary: a
439 summary: a
440
440
441 $ echo foo >> b
441 $ echo foo >> b
442 $ hg addr
442 $ hg addr
443 adding b
443 adding b
444 $ hg ci -m 'add b'
444 $ hg ci -m 'add b'
445 $ echo foo >> a
445 $ echo foo >> a
446 $ hg ci -m 'extend a'
446 $ hg ci -m 'extend a'
447 $ hg phase --public 1
447 $ hg phase --public 1
448 Attempting to fold a change into a public change should not work:
448 Attempting to fold a change into a public change should not work:
449 $ cat > ../edit.sh <<EOF
449 $ cat > ../edit.sh <<EOF
450 > cat "\$1" | sed s/pick/fold/ > tmp
450 > cat "\$1" | sed s/pick/fold/ > tmp
451 > mv tmp "\$1"
451 > mv tmp "\$1"
452 > EOF
452 > EOF
453 $ HGEDITOR="sh ../edit.sh" hg histedit 2
453 $ HGEDITOR="sh ../edit.sh" hg histedit 2
454 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
455 reverting a
456 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
457 warning: histedit rules saved to: .hg/histedit-last-edit.txt
454 warning: histedit rules saved to: .hg/histedit-last-edit.txt
458 abort: cannot fold into public change 18aa70c8ad22
455 abort: cannot fold into public change 18aa70c8ad22
459 [255]
456 [255]
460 $ cat .hg/histedit-last-edit.txt
457 $ cat .hg/histedit-last-edit.txt
461 fold 0012be4a27ea 2 extend a
458 fold 0012be4a27ea 2 extend a
462
459
463 # Edit history between 0012be4a27ea and 0012be4a27ea
460 # Edit history between 0012be4a27ea and 0012be4a27ea
464 #
461 #
465 # Commits are listed from least to most recent
462 # Commits are listed from least to most recent
466 #
463 #
467 # Commands:
464 # Commands:
468 # p, fold = use commit
465 # p, fold = use commit
469 # e, edit = use commit, but stop for amending
466 # e, edit = use commit, but stop for amending
470 # f, fold = use commit, but combine it with the one above
467 # f, fold = use commit, but combine it with the one above
471 # r, roll = like fold, but discard this commit's description
468 # r, roll = like fold, but discard this commit's description
472 # d, drop = remove commit from history
469 # d, drop = remove commit from history
473 # m, mess = edit commit message without changing commit content
470 # m, mess = edit commit message without changing commit content
474 #
471 #
475 TODO: this abort shouldn't be required, but it is for now to leave the repo in
476 a clean state.
477 $ hg histedit --abort
General Comments 0
You need to be logged in to leave comments. Login now