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