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