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