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