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