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