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