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