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