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