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