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