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