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