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