##// END OF EJS Templates
histedit: clarify modes...
timeless -
r27714:bbb61a83 default
parent child Browse files
Show More
@@ -1,1529 +1,1532 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commits are listed from least to most recent
33 # Commits are listed from least to most recent
34 #
34 #
35 # Commands:
35 # Commands:
36 # p, pick = use commit
36 # p, pick = use commit
37 # e, edit = use commit, but stop for amending
37 # e, edit = use commit, but stop for amending
38 # f, fold = use commit, but combine it with the one above
38 # f, fold = use commit, but combine it with the one above
39 # r, roll = like fold, but discard this commit's description
39 # r, roll = like fold, but discard this commit's description
40 # d, drop = remove commit from history
40 # d, drop = remove commit from history
41 # m, mess = edit commit message without changing commit content
41 # m, mess = edit commit message without changing commit content
42 #
42 #
43
43
44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 for each revision in your history. For example, if you had meant to add gamma
45 for each revision in your history. For example, if you had meant to add gamma
46 before beta, and then wanted to add delta in the same revision as beta, you
46 before beta, and then wanted to add delta in the same revision as beta, you
47 would reorganize the file to look like this::
47 would reorganize the file to look like this::
48
48
49 pick 030b686bedc4 Add gamma
49 pick 030b686bedc4 Add gamma
50 pick c561b4e977df Add beta
50 pick c561b4e977df Add beta
51 fold 7c2fd3b9020c Add delta
51 fold 7c2fd3b9020c Add delta
52
52
53 # Edit history between c561b4e977df and 7c2fd3b9020c
53 # Edit history between c561b4e977df and 7c2fd3b9020c
54 #
54 #
55 # Commits are listed from least to most recent
55 # Commits are listed from least to most recent
56 #
56 #
57 # Commands:
57 # Commands:
58 # p, pick = use commit
58 # p, pick = use commit
59 # e, edit = use commit, but stop for amending
59 # e, edit = use commit, but stop for amending
60 # f, fold = use commit, but combine it with the one above
60 # f, fold = use commit, but combine it with the one above
61 # r, roll = like fold, but discard this commit's description
61 # r, roll = like fold, but discard this commit's description
62 # d, drop = remove commit from history
62 # d, drop = remove commit from history
63 # m, mess = edit commit message without changing commit content
63 # m, mess = edit commit message without changing commit content
64 #
64 #
65
65
66 At which point you close the editor and ``histedit`` starts working. When you
66 At which point you close the editor and ``histedit`` starts working. When you
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 those revisions together, offering you a chance to clean up the commit message::
68 those revisions together, offering you a chance to clean up the commit message::
69
69
70 Add beta
70 Add beta
71 ***
71 ***
72 Add delta
72 Add delta
73
73
74 Edit the commit message to your liking, then close the editor. For
74 Edit the commit message to your liking, then close the editor. For
75 this example, let's assume that the commit message was changed to
75 this example, let's assume that the commit message was changed to
76 ``Add beta and delta.`` After histedit has run and had a chance to
76 ``Add beta and delta.`` After histedit has run and had a chance to
77 remove any old or temporary revisions it needed, the history looks
77 remove any old or temporary revisions it needed, the history looks
78 like this::
78 like this::
79
79
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 | Add beta and delta.
81 | Add beta and delta.
82 |
82 |
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 | Add gamma
84 | Add gamma
85 |
85 |
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 Add alpha
87 Add alpha
88
88
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 ones) until after it has completed all the editing operations, so it will
90 ones) until after it has completed all the editing operations, so it will
91 probably perform several strip operations when it's done. For the above example,
91 probably perform several strip operations when it's done. For the above example,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 so you might need to be a little patient. You can choose to keep the original
93 so you might need to be a little patient. You can choose to keep the original
94 revisions by passing the ``--keep`` flag.
94 revisions by passing the ``--keep`` flag.
95
95
96 The ``edit`` operation will drop you back to a command prompt,
96 The ``edit`` operation will drop you back to a command prompt,
97 allowing you to edit files freely, or even use ``hg record`` to commit
97 allowing you to edit files freely, or even use ``hg record`` to commit
98 some changes as a separate commit. When you're done, any remaining
98 some changes as a separate commit. When you're done, any remaining
99 uncommitted changes will be committed as well. When done, run ``hg
99 uncommitted changes will be committed as well. When done, run ``hg
100 histedit --continue`` to finish this step. You'll be prompted for a
100 histedit --continue`` to finish this step. You'll be prompted for a
101 new commit message, but the default commit message will be the
101 new commit message, but the default commit message will be the
102 original message for the ``edit`` ed revision.
102 original message for the ``edit`` ed revision.
103
103
104 The ``message`` operation will give you a chance to revise a commit
104 The ``message`` operation will give you a chance to revise a commit
105 message without changing the contents. It's a shortcut for doing
105 message without changing the contents. It's a shortcut for doing
106 ``edit`` immediately followed by `hg histedit --continue``.
106 ``edit`` immediately followed by `hg histedit --continue``.
107
107
108 If ``histedit`` encounters a conflict when moving a revision (while
108 If ``histedit`` encounters a conflict when moving a revision (while
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 ``edit`` with the difference that it won't prompt you for a commit
110 ``edit`` with the difference that it won't prompt you for a commit
111 message when done. If you decide at this point that you don't like how
111 message when done. If you decide at this point that you don't like how
112 much work it will be to rearrange history, or that you made a mistake,
112 much work it will be to rearrange history, or that you made a mistake,
113 you can use ``hg histedit --abort`` to abandon the new changes you
113 you can use ``hg histedit --abort`` to abandon the new changes you
114 have made and return to the state before you attempted to edit your
114 have made and return to the state before you attempted to edit your
115 history.
115 history.
116
116
117 If we clone the histedit-ed example repository above and add four more
117 If we clone the histedit-ed example repository above and add four more
118 changes, such that we have the following history::
118 changes, such that we have the following history::
119
119
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 | Add theta
121 | Add theta
122 |
122 |
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 | Add eta
124 | Add eta
125 |
125 |
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 | Add zeta
127 | Add zeta
128 |
128 |
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 | Add epsilon
130 | Add epsilon
131 |
131 |
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 | Add beta and delta.
133 | Add beta and delta.
134 |
134 |
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 | Add gamma
136 | Add gamma
137 |
137 |
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 Add alpha
139 Add alpha
140
140
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 as running ``hg histedit 836302820282``. If you need plan to push to a
142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 repository that Mercurial does not detect to be related to the source
143 repository that Mercurial does not detect to be related to the source
144 repo, you can add a ``--force`` option.
144 repo, you can add a ``--force`` option.
145
145
146 Config
146 Config
147 ------
147 ------
148
148
149 Histedit rule lines are truncated to 80 characters by default. You
149 Histedit rule lines are truncated to 80 characters by default. You
150 can customize this behavior by setting a different length in your
150 can customize this behavior by setting a different length in your
151 configuration file::
151 configuration file::
152
152
153 [histedit]
153 [histedit]
154 linelen = 120 # truncate rule lines at 120 characters
154 linelen = 120 # truncate rule lines at 120 characters
155
155
156 ``hg histedit`` attempts to automatically choose an appropriate base
156 ``hg histedit`` attempts to automatically choose an appropriate base
157 revision to use. To change which base revision is used, define a
157 revision to use. To change which base revision is used, define a
158 revset in your configuration file::
158 revset in your configuration file::
159
159
160 [histedit]
160 [histedit]
161 defaultrev = only(.) & draft()
161 defaultrev = only(.) & draft()
162
162
163 By default each edited revision needs to be present in histedit commands.
163 By default each edited revision needs to be present in histedit commands.
164 To remove revision you need to use ``drop`` operation. You can configure
164 To remove revision you need to use ``drop`` operation. You can configure
165 the drop to be implicit for missing commits by adding:
165 the drop to be implicit for missing commits by adding:
166
166
167 [histedit]
167 [histedit]
168 dropmissing = True
168 dropmissing = True
169
169
170 """
170 """
171
171
172 import pickle
172 import pickle
173 import errno
173 import errno
174 import os
174 import os
175 import sys
175 import sys
176
176
177 from mercurial import bundle2
177 from mercurial import bundle2
178 from mercurial import cmdutil
178 from mercurial import cmdutil
179 from mercurial import discovery
179 from mercurial import discovery
180 from mercurial import error
180 from mercurial import error
181 from mercurial import copies
181 from mercurial import copies
182 from mercurial import context
182 from mercurial import context
183 from mercurial import destutil
183 from mercurial import destutil
184 from mercurial import exchange
184 from mercurial import exchange
185 from mercurial import extensions
185 from mercurial import extensions
186 from mercurial import hg
186 from mercurial import hg
187 from mercurial import node
187 from mercurial import node
188 from mercurial import repair
188 from mercurial import repair
189 from mercurial import scmutil
189 from mercurial import scmutil
190 from mercurial import util
190 from mercurial import util
191 from mercurial import obsolete
191 from mercurial import obsolete
192 from mercurial import merge as mergemod
192 from mercurial import merge as mergemod
193 from mercurial.lock import release
193 from mercurial.lock import release
194 from mercurial.i18n import _
194 from mercurial.i18n import _
195
195
196 cmdtable = {}
196 cmdtable = {}
197 command = cmdutil.command(cmdtable)
197 command = cmdutil.command(cmdtable)
198
198
199 class _constraints(object):
199 class _constraints(object):
200 # aborts if there are multiple rules for one node
200 # aborts if there are multiple rules for one node
201 noduplicates = 'noduplicates'
201 noduplicates = 'noduplicates'
202 # abort if the node does belong to edited stack
202 # abort if the node does belong to edited stack
203 forceother = 'forceother'
203 forceother = 'forceother'
204 # abort if the node doesn't belong to edited stack
204 # abort if the node doesn't belong to edited stack
205 noother = 'noother'
205 noother = 'noother'
206
206
207 @classmethod
207 @classmethod
208 def known(cls):
208 def known(cls):
209 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
209 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
210
210
211 # Note for extension authors: ONLY specify testedwith = 'internal' for
211 # Note for extension authors: ONLY specify testedwith = 'internal' for
212 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
212 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
213 # be specifying the version(s) of Mercurial they are tested with, or
213 # be specifying the version(s) of Mercurial they are tested with, or
214 # leave the attribute unspecified.
214 # leave the attribute unspecified.
215 testedwith = 'internal'
215 testedwith = 'internal'
216
216
217 actiontable = {}
217 actiontable = {}
218 primaryactions = set()
218 primaryactions = set()
219 secondaryactions = set()
219 secondaryactions = set()
220 tertiaryactions = set()
220 tertiaryactions = set()
221 internalactions = set()
221 internalactions = set()
222
222
223 def geteditcomment(first, last):
223 def geteditcomment(first, last):
224 """ construct the editor comment
224 """ construct the editor comment
225 The comment includes::
225 The comment includes::
226 - an intro
226 - an intro
227 - sorted primary commands
227 - sorted primary commands
228 - sorted short commands
228 - sorted short commands
229 - sorted long commands
229 - sorted long commands
230
230
231 Commands are only included once.
231 Commands are only included once.
232 """
232 """
233 intro = _("""Edit history between %s and %s
233 intro = _("""Edit history between %s and %s
234
234
235 Commits are listed from least to most recent
235 Commits are listed from least to most recent
236
236
237 Commands:
237 Commands:
238 """)
238 """)
239 actions = []
239 actions = []
240 def addverb(v):
240 def addverb(v):
241 a = actiontable[v]
241 a = actiontable[v]
242 lines = a.message.split("\n")
242 lines = a.message.split("\n")
243 if len(a.verbs):
243 if len(a.verbs):
244 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
244 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
245 actions.append(" %s = %s" % (v, lines[0]))
245 actions.append(" %s = %s" % (v, lines[0]))
246 actions.extend([' %s' for l in lines[1:]])
246 actions.extend([' %s' for l in lines[1:]])
247
247
248 for v in (
248 for v in (
249 sorted(primaryactions) +
249 sorted(primaryactions) +
250 sorted(secondaryactions) +
250 sorted(secondaryactions) +
251 sorted(tertiaryactions)
251 sorted(tertiaryactions)
252 ):
252 ):
253 addverb(v)
253 addverb(v)
254 actions.append('')
254 actions.append('')
255
255
256 return ''.join(['# %s\n' % l if l else '#\n'
256 return ''.join(['# %s\n' % l if l else '#\n'
257 for l in ((intro % (first, last)).split('\n')) + actions])
257 for l in ((intro % (first, last)).split('\n')) + actions])
258
258
259 class histeditstate(object):
259 class histeditstate(object):
260 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
260 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
261 topmost=None, replacements=None, lock=None, wlock=None):
261 topmost=None, replacements=None, lock=None, wlock=None):
262 self.repo = repo
262 self.repo = repo
263 self.actions = actions
263 self.actions = actions
264 self.keep = keep
264 self.keep = keep
265 self.topmost = topmost
265 self.topmost = topmost
266 self.parentctxnode = parentctxnode
266 self.parentctxnode = parentctxnode
267 self.lock = lock
267 self.lock = lock
268 self.wlock = wlock
268 self.wlock = wlock
269 self.backupfile = None
269 self.backupfile = None
270 if replacements is None:
270 if replacements is None:
271 self.replacements = []
271 self.replacements = []
272 else:
272 else:
273 self.replacements = replacements
273 self.replacements = replacements
274
274
275 def read(self):
275 def read(self):
276 """Load histedit state from disk and set fields appropriately."""
276 """Load histedit state from disk and set fields appropriately."""
277 try:
277 try:
278 state = self.repo.vfs.read('histedit-state')
278 state = self.repo.vfs.read('histedit-state')
279 except IOError as err:
279 except IOError as err:
280 if err.errno != errno.ENOENT:
280 if err.errno != errno.ENOENT:
281 raise
281 raise
282 raise error.Abort(_('no histedit in progress'))
282 raise error.Abort(_('no histedit in progress'))
283
283
284 if state.startswith('v1\n'):
284 if state.startswith('v1\n'):
285 data = self._load()
285 data = self._load()
286 parentctxnode, rules, keep, topmost, replacements, backupfile = data
286 parentctxnode, rules, keep, topmost, replacements, backupfile = data
287 else:
287 else:
288 data = pickle.loads(state)
288 data = pickle.loads(state)
289 parentctxnode, rules, keep, topmost, replacements = data
289 parentctxnode, rules, keep, topmost, replacements = data
290 backupfile = None
290 backupfile = None
291
291
292 self.parentctxnode = parentctxnode
292 self.parentctxnode = parentctxnode
293 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
293 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
294 actions = parserules(rules, self)
294 actions = parserules(rules, self)
295 self.actions = actions
295 self.actions = actions
296 self.keep = keep
296 self.keep = keep
297 self.topmost = topmost
297 self.topmost = topmost
298 self.replacements = replacements
298 self.replacements = replacements
299 self.backupfile = backupfile
299 self.backupfile = backupfile
300
300
301 def write(self):
301 def write(self):
302 fp = self.repo.vfs('histedit-state', 'w')
302 fp = self.repo.vfs('histedit-state', 'w')
303 fp.write('v1\n')
303 fp.write('v1\n')
304 fp.write('%s\n' % node.hex(self.parentctxnode))
304 fp.write('%s\n' % node.hex(self.parentctxnode))
305 fp.write('%s\n' % node.hex(self.topmost))
305 fp.write('%s\n' % node.hex(self.topmost))
306 fp.write('%s\n' % self.keep)
306 fp.write('%s\n' % self.keep)
307 fp.write('%d\n' % len(self.actions))
307 fp.write('%d\n' % len(self.actions))
308 for action in self.actions:
308 for action in self.actions:
309 fp.write('%s\n' % action.tostate())
309 fp.write('%s\n' % action.tostate())
310 fp.write('%d\n' % len(self.replacements))
310 fp.write('%d\n' % len(self.replacements))
311 for replacement in self.replacements:
311 for replacement in self.replacements:
312 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
312 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
313 for r in replacement[1])))
313 for r in replacement[1])))
314 backupfile = self.backupfile
314 backupfile = self.backupfile
315 if not backupfile:
315 if not backupfile:
316 backupfile = ''
316 backupfile = ''
317 fp.write('%s\n' % backupfile)
317 fp.write('%s\n' % backupfile)
318 fp.close()
318 fp.close()
319
319
320 def _load(self):
320 def _load(self):
321 fp = self.repo.vfs('histedit-state', 'r')
321 fp = self.repo.vfs('histedit-state', 'r')
322 lines = [l[:-1] for l in fp.readlines()]
322 lines = [l[:-1] for l in fp.readlines()]
323
323
324 index = 0
324 index = 0
325 lines[index] # version number
325 lines[index] # version number
326 index += 1
326 index += 1
327
327
328 parentctxnode = node.bin(lines[index])
328 parentctxnode = node.bin(lines[index])
329 index += 1
329 index += 1
330
330
331 topmost = node.bin(lines[index])
331 topmost = node.bin(lines[index])
332 index += 1
332 index += 1
333
333
334 keep = lines[index] == 'True'
334 keep = lines[index] == 'True'
335 index += 1
335 index += 1
336
336
337 # Rules
337 # Rules
338 rules = []
338 rules = []
339 rulelen = int(lines[index])
339 rulelen = int(lines[index])
340 index += 1
340 index += 1
341 for i in xrange(rulelen):
341 for i in xrange(rulelen):
342 ruleaction = lines[index]
342 ruleaction = lines[index]
343 index += 1
343 index += 1
344 rule = lines[index]
344 rule = lines[index]
345 index += 1
345 index += 1
346 rules.append((ruleaction, rule))
346 rules.append((ruleaction, rule))
347
347
348 # Replacements
348 # Replacements
349 replacements = []
349 replacements = []
350 replacementlen = int(lines[index])
350 replacementlen = int(lines[index])
351 index += 1
351 index += 1
352 for i in xrange(replacementlen):
352 for i in xrange(replacementlen):
353 replacement = lines[index]
353 replacement = lines[index]
354 original = node.bin(replacement[:40])
354 original = node.bin(replacement[:40])
355 succ = [node.bin(replacement[i:i + 40]) for i in
355 succ = [node.bin(replacement[i:i + 40]) for i in
356 range(40, len(replacement), 40)]
356 range(40, len(replacement), 40)]
357 replacements.append((original, succ))
357 replacements.append((original, succ))
358 index += 1
358 index += 1
359
359
360 backupfile = lines[index]
360 backupfile = lines[index]
361 index += 1
361 index += 1
362
362
363 fp.close()
363 fp.close()
364
364
365 return parentctxnode, rules, keep, topmost, replacements, backupfile
365 return parentctxnode, rules, keep, topmost, replacements, backupfile
366
366
367 def clear(self):
367 def clear(self):
368 if self.inprogress():
368 if self.inprogress():
369 self.repo.vfs.unlink('histedit-state')
369 self.repo.vfs.unlink('histedit-state')
370
370
371 def inprogress(self):
371 def inprogress(self):
372 return self.repo.vfs.exists('histedit-state')
372 return self.repo.vfs.exists('histedit-state')
373
373
374
374
375 class histeditaction(object):
375 class histeditaction(object):
376 def __init__(self, state, node):
376 def __init__(self, state, node):
377 self.state = state
377 self.state = state
378 self.repo = state.repo
378 self.repo = state.repo
379 self.node = node
379 self.node = node
380
380
381 @classmethod
381 @classmethod
382 def fromrule(cls, state, rule):
382 def fromrule(cls, state, rule):
383 """Parses the given rule, returning an instance of the histeditaction.
383 """Parses the given rule, returning an instance of the histeditaction.
384 """
384 """
385 rulehash = rule.strip().split(' ', 1)[0]
385 rulehash = rule.strip().split(' ', 1)[0]
386 try:
386 try:
387 rev = node.bin(rulehash)
387 rev = node.bin(rulehash)
388 except TypeError:
388 except TypeError:
389 raise error.ParseError("invalid changeset %s" % rulehash)
389 raise error.ParseError("invalid changeset %s" % rulehash)
390 return cls(state, rev)
390 return cls(state, rev)
391
391
392 def verify(self, prev):
392 def verify(self, prev):
393 """ Verifies semantic correctness of the rule"""
393 """ Verifies semantic correctness of the rule"""
394 repo = self.repo
394 repo = self.repo
395 ha = node.hex(self.node)
395 ha = node.hex(self.node)
396 try:
396 try:
397 self.node = repo[ha].node()
397 self.node = repo[ha].node()
398 except error.RepoError:
398 except error.RepoError:
399 raise error.ParseError(_('unknown changeset %s listed')
399 raise error.ParseError(_('unknown changeset %s listed')
400 % ha[:12])
400 % ha[:12])
401
401
402 def torule(self):
402 def torule(self):
403 """build a histedit rule line for an action
403 """build a histedit rule line for an action
404
404
405 by default lines are in the form:
405 by default lines are in the form:
406 <hash> <rev> <summary>
406 <hash> <rev> <summary>
407 """
407 """
408 ctx = self.repo[self.node]
408 ctx = self.repo[self.node]
409 summary = ''
409 summary = ''
410 if ctx.description():
410 if ctx.description():
411 summary = ctx.description().splitlines()[0]
411 summary = ctx.description().splitlines()[0]
412 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
412 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
413 # trim to 75 columns by default so it's not stupidly wide in my editor
413 # trim to 75 columns by default so it's not stupidly wide in my editor
414 # (the 5 more are left for verb)
414 # (the 5 more are left for verb)
415 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
415 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
416 maxlen = max(maxlen, 22) # avoid truncating hash
416 maxlen = max(maxlen, 22) # avoid truncating hash
417 return util.ellipsis(line, maxlen)
417 return util.ellipsis(line, maxlen)
418
418
419 def tostate(self):
419 def tostate(self):
420 """Print an action in format used by histedit state files
420 """Print an action in format used by histedit state files
421 (the first line is a verb, the remainder is the second)
421 (the first line is a verb, the remainder is the second)
422 """
422 """
423 return "%s\n%s" % (self.verb, node.hex(self.node))
423 return "%s\n%s" % (self.verb, node.hex(self.node))
424
424
425 def constraints(self):
425 def constraints(self):
426 """Return a set of constrains that this action should be verified for
426 """Return a set of constrains that this action should be verified for
427 """
427 """
428 return set([_constraints.noduplicates, _constraints.noother])
428 return set([_constraints.noduplicates, _constraints.noother])
429
429
430 def nodetoverify(self):
430 def nodetoverify(self):
431 """Returns a node associated with the action that will be used for
431 """Returns a node associated with the action that will be used for
432 verification purposes.
432 verification purposes.
433
433
434 If the action doesn't correspond to node it should return None
434 If the action doesn't correspond to node it should return None
435 """
435 """
436 return self.node
436 return self.node
437
437
438 def run(self):
438 def run(self):
439 """Runs the action. The default behavior is simply apply the action's
439 """Runs the action. The default behavior is simply apply the action's
440 rulectx onto the current parentctx."""
440 rulectx onto the current parentctx."""
441 self.applychange()
441 self.applychange()
442 self.continuedirty()
442 self.continuedirty()
443 return self.continueclean()
443 return self.continueclean()
444
444
445 def applychange(self):
445 def applychange(self):
446 """Applies the changes from this action's rulectx onto the current
446 """Applies the changes from this action's rulectx onto the current
447 parentctx, but does not commit them."""
447 parentctx, but does not commit them."""
448 repo = self.repo
448 repo = self.repo
449 rulectx = repo[self.node]
449 rulectx = repo[self.node]
450 hg.update(repo, self.state.parentctxnode, quietempty=True)
450 hg.update(repo, self.state.parentctxnode, quietempty=True)
451 stats = applychanges(repo.ui, repo, rulectx, {})
451 stats = applychanges(repo.ui, repo, rulectx, {})
452 if stats and stats[3] > 0:
452 if stats and stats[3] > 0:
453 raise error.InterventionRequired(
453 raise error.InterventionRequired(
454 _('Fix up the change (%s %s)') %
454 _('Fix up the change (%s %s)') %
455 (self.verb, node.short(self.node)),
455 (self.verb, node.short(self.node)),
456 hint=_('hg histedit --continue to resume'))
456 hint=_('hg histedit --continue to resume'))
457
457
458 def continuedirty(self):
458 def continuedirty(self):
459 """Continues the action when changes have been applied to the working
459 """Continues the action when changes have been applied to the working
460 copy. The default behavior is to commit the dirty changes."""
460 copy. The default behavior is to commit the dirty changes."""
461 repo = self.repo
461 repo = self.repo
462 rulectx = repo[self.node]
462 rulectx = repo[self.node]
463
463
464 editor = self.commiteditor()
464 editor = self.commiteditor()
465 commit = commitfuncfor(repo, rulectx)
465 commit = commitfuncfor(repo, rulectx)
466
466
467 commit(text=rulectx.description(), user=rulectx.user(),
467 commit(text=rulectx.description(), user=rulectx.user(),
468 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
468 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
469
469
470 def commiteditor(self):
470 def commiteditor(self):
471 """The editor to be used to edit the commit message."""
471 """The editor to be used to edit the commit message."""
472 return False
472 return False
473
473
474 def continueclean(self):
474 def continueclean(self):
475 """Continues the action when the working copy is clean. The default
475 """Continues the action when the working copy is clean. The default
476 behavior is to accept the current commit as the new version of the
476 behavior is to accept the current commit as the new version of the
477 rulectx."""
477 rulectx."""
478 ctx = self.repo['.']
478 ctx = self.repo['.']
479 if ctx.node() == self.state.parentctxnode:
479 if ctx.node() == self.state.parentctxnode:
480 self.repo.ui.warn(_('%s: empty changeset\n') %
480 self.repo.ui.warn(_('%s: empty changeset\n') %
481 node.short(self.node))
481 node.short(self.node))
482 return ctx, [(self.node, tuple())]
482 return ctx, [(self.node, tuple())]
483 if ctx.node() == self.node:
483 if ctx.node() == self.node:
484 # Nothing changed
484 # Nothing changed
485 return ctx, []
485 return ctx, []
486 return ctx, [(self.node, (ctx.node(),))]
486 return ctx, [(self.node, (ctx.node(),))]
487
487
488 def commitfuncfor(repo, src):
488 def commitfuncfor(repo, src):
489 """Build a commit function for the replacement of <src>
489 """Build a commit function for the replacement of <src>
490
490
491 This function ensure we apply the same treatment to all changesets.
491 This function ensure we apply the same treatment to all changesets.
492
492
493 - Add a 'histedit_source' entry in extra.
493 - Add a 'histedit_source' entry in extra.
494
494
495 Note that fold has its own separated logic because its handling is a bit
495 Note that fold has its own separated logic because its handling is a bit
496 different and not easily factored out of the fold method.
496 different and not easily factored out of the fold method.
497 """
497 """
498 phasemin = src.phase()
498 phasemin = src.phase()
499 def commitfunc(**kwargs):
499 def commitfunc(**kwargs):
500 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
500 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
501 try:
501 try:
502 repo.ui.setconfig('phases', 'new-commit', phasemin,
502 repo.ui.setconfig('phases', 'new-commit', phasemin,
503 'histedit')
503 'histedit')
504 extra = kwargs.get('extra', {}).copy()
504 extra = kwargs.get('extra', {}).copy()
505 extra['histedit_source'] = src.hex()
505 extra['histedit_source'] = src.hex()
506 kwargs['extra'] = extra
506 kwargs['extra'] = extra
507 return repo.commit(**kwargs)
507 return repo.commit(**kwargs)
508 finally:
508 finally:
509 repo.ui.restoreconfig(phasebackup)
509 repo.ui.restoreconfig(phasebackup)
510 return commitfunc
510 return commitfunc
511
511
512 def applychanges(ui, repo, ctx, opts):
512 def applychanges(ui, repo, ctx, opts):
513 """Merge changeset from ctx (only) in the current working directory"""
513 """Merge changeset from ctx (only) in the current working directory"""
514 wcpar = repo.dirstate.parents()[0]
514 wcpar = repo.dirstate.parents()[0]
515 if ctx.p1().node() == wcpar:
515 if ctx.p1().node() == wcpar:
516 # edits are "in place" we do not need to make any merge,
516 # edits are "in place" we do not need to make any merge,
517 # just applies changes on parent for editing
517 # just applies changes on parent for editing
518 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
518 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
519 stats = None
519 stats = None
520 else:
520 else:
521 try:
521 try:
522 # ui.forcemerge is an internal variable, do not document
522 # ui.forcemerge is an internal variable, do not document
523 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
523 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
524 'histedit')
524 'histedit')
525 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
525 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
526 finally:
526 finally:
527 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
527 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
528 return stats
528 return stats
529
529
530 def collapse(repo, first, last, commitopts, skipprompt=False):
530 def collapse(repo, first, last, commitopts, skipprompt=False):
531 """collapse the set of revisions from first to last as new one.
531 """collapse the set of revisions from first to last as new one.
532
532
533 Expected commit options are:
533 Expected commit options are:
534 - message
534 - message
535 - date
535 - date
536 - username
536 - username
537 Commit message is edited in all cases.
537 Commit message is edited in all cases.
538
538
539 This function works in memory."""
539 This function works in memory."""
540 ctxs = list(repo.set('%d::%d', first, last))
540 ctxs = list(repo.set('%d::%d', first, last))
541 if not ctxs:
541 if not ctxs:
542 return None
542 return None
543 for c in ctxs:
543 for c in ctxs:
544 if not c.mutable():
544 if not c.mutable():
545 raise error.ParseError(
545 raise error.ParseError(
546 _("cannot fold into public change %s") % node.short(c.node()))
546 _("cannot fold into public change %s") % node.short(c.node()))
547 base = first.parents()[0]
547 base = first.parents()[0]
548
548
549 # commit a new version of the old changeset, including the update
549 # commit a new version of the old changeset, including the update
550 # collect all files which might be affected
550 # collect all files which might be affected
551 files = set()
551 files = set()
552 for ctx in ctxs:
552 for ctx in ctxs:
553 files.update(ctx.files())
553 files.update(ctx.files())
554
554
555 # Recompute copies (avoid recording a -> b -> a)
555 # Recompute copies (avoid recording a -> b -> a)
556 copied = copies.pathcopies(base, last)
556 copied = copies.pathcopies(base, last)
557
557
558 # prune files which were reverted by the updates
558 # prune files which were reverted by the updates
559 def samefile(f):
559 def samefile(f):
560 if f in last.manifest():
560 if f in last.manifest():
561 a = last.filectx(f)
561 a = last.filectx(f)
562 if f in base.manifest():
562 if f in base.manifest():
563 b = base.filectx(f)
563 b = base.filectx(f)
564 return (a.data() == b.data()
564 return (a.data() == b.data()
565 and a.flags() == b.flags())
565 and a.flags() == b.flags())
566 else:
566 else:
567 return False
567 return False
568 else:
568 else:
569 return f not in base.manifest()
569 return f not in base.manifest()
570 files = [f for f in files if not samefile(f)]
570 files = [f for f in files if not samefile(f)]
571 # commit version of these files as defined by head
571 # commit version of these files as defined by head
572 headmf = last.manifest()
572 headmf = last.manifest()
573 def filectxfn(repo, ctx, path):
573 def filectxfn(repo, ctx, path):
574 if path in headmf:
574 if path in headmf:
575 fctx = last[path]
575 fctx = last[path]
576 flags = fctx.flags()
576 flags = fctx.flags()
577 mctx = context.memfilectx(repo,
577 mctx = context.memfilectx(repo,
578 fctx.path(), fctx.data(),
578 fctx.path(), fctx.data(),
579 islink='l' in flags,
579 islink='l' in flags,
580 isexec='x' in flags,
580 isexec='x' in flags,
581 copied=copied.get(path))
581 copied=copied.get(path))
582 return mctx
582 return mctx
583 return None
583 return None
584
584
585 if commitopts.get('message'):
585 if commitopts.get('message'):
586 message = commitopts['message']
586 message = commitopts['message']
587 else:
587 else:
588 message = first.description()
588 message = first.description()
589 user = commitopts.get('user')
589 user = commitopts.get('user')
590 date = commitopts.get('date')
590 date = commitopts.get('date')
591 extra = commitopts.get('extra')
591 extra = commitopts.get('extra')
592
592
593 parents = (first.p1().node(), first.p2().node())
593 parents = (first.p1().node(), first.p2().node())
594 editor = None
594 editor = None
595 if not skipprompt:
595 if not skipprompt:
596 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
596 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
597 new = context.memctx(repo,
597 new = context.memctx(repo,
598 parents=parents,
598 parents=parents,
599 text=message,
599 text=message,
600 files=files,
600 files=files,
601 filectxfn=filectxfn,
601 filectxfn=filectxfn,
602 user=user,
602 user=user,
603 date=date,
603 date=date,
604 extra=extra,
604 extra=extra,
605 editor=editor)
605 editor=editor)
606 return repo.commitctx(new)
606 return repo.commitctx(new)
607
607
608 def _isdirtywc(repo):
608 def _isdirtywc(repo):
609 return repo[None].dirty(missing=True)
609 return repo[None].dirty(missing=True)
610
610
611 def abortdirty():
611 def abortdirty():
612 raise error.Abort(_('working copy has pending changes'),
612 raise error.Abort(_('working copy has pending changes'),
613 hint=_('amend, commit, or revert them and run histedit '
613 hint=_('amend, commit, or revert them and run histedit '
614 '--continue, or abort with histedit --abort'))
614 '--continue, or abort with histedit --abort'))
615
615
616 def action(verbs, message, priority=False, internal=False):
616 def action(verbs, message, priority=False, internal=False):
617 def wrap(cls):
617 def wrap(cls):
618 assert not priority or not internal
618 assert not priority or not internal
619 verb = verbs[0]
619 verb = verbs[0]
620 if priority:
620 if priority:
621 primaryactions.add(verb)
621 primaryactions.add(verb)
622 elif internal:
622 elif internal:
623 internalactions.add(verb)
623 internalactions.add(verb)
624 elif len(verbs) > 1:
624 elif len(verbs) > 1:
625 secondaryactions.add(verb)
625 secondaryactions.add(verb)
626 else:
626 else:
627 tertiaryactions.add(verb)
627 tertiaryactions.add(verb)
628
628
629 cls.verb = verb
629 cls.verb = verb
630 cls.verbs = verbs
630 cls.verbs = verbs
631 cls.message = message
631 cls.message = message
632 for verb in verbs:
632 for verb in verbs:
633 actiontable[verb] = cls
633 actiontable[verb] = cls
634 return cls
634 return cls
635 return wrap
635 return wrap
636
636
637 @action(['pick', 'p'],
637 @action(['pick', 'p'],
638 _('use commit'),
638 _('use commit'),
639 priority=True)
639 priority=True)
640 class pick(histeditaction):
640 class pick(histeditaction):
641 def run(self):
641 def run(self):
642 rulectx = self.repo[self.node]
642 rulectx = self.repo[self.node]
643 if rulectx.parents()[0].node() == self.state.parentctxnode:
643 if rulectx.parents()[0].node() == self.state.parentctxnode:
644 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
644 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
645 return rulectx, []
645 return rulectx, []
646
646
647 return super(pick, self).run()
647 return super(pick, self).run()
648
648
649 @action(['edit', 'e'],
649 @action(['edit', 'e'],
650 _('use commit, but stop for amending'),
650 _('use commit, but stop for amending'),
651 priority=True)
651 priority=True)
652 class edit(histeditaction):
652 class edit(histeditaction):
653 def run(self):
653 def run(self):
654 repo = self.repo
654 repo = self.repo
655 rulectx = repo[self.node]
655 rulectx = repo[self.node]
656 hg.update(repo, self.state.parentctxnode, quietempty=True)
656 hg.update(repo, self.state.parentctxnode, quietempty=True)
657 applychanges(repo.ui, repo, rulectx, {})
657 applychanges(repo.ui, repo, rulectx, {})
658 raise error.InterventionRequired(
658 raise error.InterventionRequired(
659 _('Editing (%s), you may commit or record as needed now.')
659 _('Editing (%s), you may commit or record as needed now.')
660 % node.short(self.node),
660 % node.short(self.node),
661 hint=_('hg histedit --continue to resume'))
661 hint=_('hg histedit --continue to resume'))
662
662
663 def commiteditor(self):
663 def commiteditor(self):
664 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
664 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
665
665
666 @action(['fold', 'f'],
666 @action(['fold', 'f'],
667 _('use commit, but combine it with the one above'))
667 _('use commit, but combine it with the one above'))
668 class fold(histeditaction):
668 class fold(histeditaction):
669 def verify(self, prev):
669 def verify(self, prev):
670 """ Verifies semantic correctness of the fold rule"""
670 """ Verifies semantic correctness of the fold rule"""
671 super(fold, self).verify(prev)
671 super(fold, self).verify(prev)
672 repo = self.repo
672 repo = self.repo
673 if not prev:
673 if not prev:
674 c = repo[self.node].parents()[0]
674 c = repo[self.node].parents()[0]
675 elif not prev.verb in ('pick', 'base'):
675 elif not prev.verb in ('pick', 'base'):
676 return
676 return
677 else:
677 else:
678 c = repo[prev.node]
678 c = repo[prev.node]
679 if not c.mutable():
679 if not c.mutable():
680 raise error.ParseError(
680 raise error.ParseError(
681 _("cannot fold into public change %s") % node.short(c.node()))
681 _("cannot fold into public change %s") % node.short(c.node()))
682
682
683
683
684 def continuedirty(self):
684 def continuedirty(self):
685 repo = self.repo
685 repo = self.repo
686 rulectx = repo[self.node]
686 rulectx = repo[self.node]
687
687
688 commit = commitfuncfor(repo, rulectx)
688 commit = commitfuncfor(repo, rulectx)
689 commit(text='fold-temp-revision %s' % node.short(self.node),
689 commit(text='fold-temp-revision %s' % node.short(self.node),
690 user=rulectx.user(), date=rulectx.date(),
690 user=rulectx.user(), date=rulectx.date(),
691 extra=rulectx.extra())
691 extra=rulectx.extra())
692
692
693 def continueclean(self):
693 def continueclean(self):
694 repo = self.repo
694 repo = self.repo
695 ctx = repo['.']
695 ctx = repo['.']
696 rulectx = repo[self.node]
696 rulectx = repo[self.node]
697 parentctxnode = self.state.parentctxnode
697 parentctxnode = self.state.parentctxnode
698 if ctx.node() == parentctxnode:
698 if ctx.node() == parentctxnode:
699 repo.ui.warn(_('%s: empty changeset\n') %
699 repo.ui.warn(_('%s: empty changeset\n') %
700 node.short(self.node))
700 node.short(self.node))
701 return ctx, [(self.node, (parentctxnode,))]
701 return ctx, [(self.node, (parentctxnode,))]
702
702
703 parentctx = repo[parentctxnode]
703 parentctx = repo[parentctxnode]
704 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
704 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
705 parentctx))
705 parentctx))
706 if not newcommits:
706 if not newcommits:
707 repo.ui.warn(_('%s: cannot fold - working copy is not a '
707 repo.ui.warn(_('%s: cannot fold - working copy is not a '
708 'descendant of previous commit %s\n') %
708 'descendant of previous commit %s\n') %
709 (node.short(self.node), node.short(parentctxnode)))
709 (node.short(self.node), node.short(parentctxnode)))
710 return ctx, [(self.node, (ctx.node(),))]
710 return ctx, [(self.node, (ctx.node(),))]
711
711
712 middlecommits = newcommits.copy()
712 middlecommits = newcommits.copy()
713 middlecommits.discard(ctx.node())
713 middlecommits.discard(ctx.node())
714
714
715 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
715 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
716 middlecommits)
716 middlecommits)
717
717
718 def skipprompt(self):
718 def skipprompt(self):
719 """Returns true if the rule should skip the message editor.
719 """Returns true if the rule should skip the message editor.
720
720
721 For example, 'fold' wants to show an editor, but 'rollup'
721 For example, 'fold' wants to show an editor, but 'rollup'
722 doesn't want to.
722 doesn't want to.
723 """
723 """
724 return False
724 return False
725
725
726 def mergedescs(self):
726 def mergedescs(self):
727 """Returns true if the rule should merge messages of multiple changes.
727 """Returns true if the rule should merge messages of multiple changes.
728
728
729 This exists mainly so that 'rollup' rules can be a subclass of
729 This exists mainly so that 'rollup' rules can be a subclass of
730 'fold'.
730 'fold'.
731 """
731 """
732 return True
732 return True
733
733
734 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
734 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
735 parent = ctx.parents()[0].node()
735 parent = ctx.parents()[0].node()
736 hg.update(repo, parent)
736 hg.update(repo, parent)
737 ### prepare new commit data
737 ### prepare new commit data
738 commitopts = {}
738 commitopts = {}
739 commitopts['user'] = ctx.user()
739 commitopts['user'] = ctx.user()
740 # commit message
740 # commit message
741 if not self.mergedescs():
741 if not self.mergedescs():
742 newmessage = ctx.description()
742 newmessage = ctx.description()
743 else:
743 else:
744 newmessage = '\n***\n'.join(
744 newmessage = '\n***\n'.join(
745 [ctx.description()] +
745 [ctx.description()] +
746 [repo[r].description() for r in internalchanges] +
746 [repo[r].description() for r in internalchanges] +
747 [oldctx.description()]) + '\n'
747 [oldctx.description()]) + '\n'
748 commitopts['message'] = newmessage
748 commitopts['message'] = newmessage
749 # date
749 # date
750 commitopts['date'] = max(ctx.date(), oldctx.date())
750 commitopts['date'] = max(ctx.date(), oldctx.date())
751 extra = ctx.extra().copy()
751 extra = ctx.extra().copy()
752 # histedit_source
752 # histedit_source
753 # note: ctx is likely a temporary commit but that the best we can do
753 # note: ctx is likely a temporary commit but that the best we can do
754 # here. This is sufficient to solve issue3681 anyway.
754 # here. This is sufficient to solve issue3681 anyway.
755 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
755 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
756 commitopts['extra'] = extra
756 commitopts['extra'] = extra
757 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
757 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
758 try:
758 try:
759 phasemin = max(ctx.phase(), oldctx.phase())
759 phasemin = max(ctx.phase(), oldctx.phase())
760 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
760 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
761 n = collapse(repo, ctx, repo[newnode], commitopts,
761 n = collapse(repo, ctx, repo[newnode], commitopts,
762 skipprompt=self.skipprompt())
762 skipprompt=self.skipprompt())
763 finally:
763 finally:
764 repo.ui.restoreconfig(phasebackup)
764 repo.ui.restoreconfig(phasebackup)
765 if n is None:
765 if n is None:
766 return ctx, []
766 return ctx, []
767 hg.update(repo, n)
767 hg.update(repo, n)
768 replacements = [(oldctx.node(), (newnode,)),
768 replacements = [(oldctx.node(), (newnode,)),
769 (ctx.node(), (n,)),
769 (ctx.node(), (n,)),
770 (newnode, (n,)),
770 (newnode, (n,)),
771 ]
771 ]
772 for ich in internalchanges:
772 for ich in internalchanges:
773 replacements.append((ich, (n,)))
773 replacements.append((ich, (n,)))
774 return repo[n], replacements
774 return repo[n], replacements
775
775
776 class base(histeditaction):
776 class base(histeditaction):
777 def constraints(self):
777 def constraints(self):
778 return set([_constraints.forceother])
778 return set([_constraints.forceother])
779
779
780 def run(self):
780 def run(self):
781 if self.repo['.'].node() != self.node:
781 if self.repo['.'].node() != self.node:
782 mergemod.update(self.repo, self.node, False, True)
782 mergemod.update(self.repo, self.node, False, True)
783 # branchmerge, force)
783 # branchmerge, force)
784 return self.continueclean()
784 return self.continueclean()
785
785
786 def continuedirty(self):
786 def continuedirty(self):
787 abortdirty()
787 abortdirty()
788
788
789 def continueclean(self):
789 def continueclean(self):
790 basectx = self.repo['.']
790 basectx = self.repo['.']
791 return basectx, []
791 return basectx, []
792
792
793 @action(['_multifold'],
793 @action(['_multifold'],
794 _(
794 _(
795 """fold subclass used for when multiple folds happen in a row
795 """fold subclass used for when multiple folds happen in a row
796
796
797 We only want to fire the editor for the folded message once when
797 We only want to fire the editor for the folded message once when
798 (say) four changes are folded down into a single change. This is
798 (say) four changes are folded down into a single change. This is
799 similar to rollup, but we should preserve both messages so that
799 similar to rollup, but we should preserve both messages so that
800 when the last fold operation runs we can show the user all the
800 when the last fold operation runs we can show the user all the
801 commit messages in their editor.
801 commit messages in their editor.
802 """),
802 """),
803 internal=True)
803 internal=True)
804 class _multifold(fold):
804 class _multifold(fold):
805 def skipprompt(self):
805 def skipprompt(self):
806 return True
806 return True
807
807
808 @action(["roll", "r"],
808 @action(["roll", "r"],
809 _("like fold, but discard this commit's description"))
809 _("like fold, but discard this commit's description"))
810 class rollup(fold):
810 class rollup(fold):
811 def mergedescs(self):
811 def mergedescs(self):
812 return False
812 return False
813
813
814 def skipprompt(self):
814 def skipprompt(self):
815 return True
815 return True
816
816
817 @action(["drop", "d"],
817 @action(["drop", "d"],
818 _('remove commit from history'))
818 _('remove commit from history'))
819 class drop(histeditaction):
819 class drop(histeditaction):
820 def run(self):
820 def run(self):
821 parentctx = self.repo[self.state.parentctxnode]
821 parentctx = self.repo[self.state.parentctxnode]
822 return parentctx, [(self.node, tuple())]
822 return parentctx, [(self.node, tuple())]
823
823
824 @action(["mess", "m"],
824 @action(["mess", "m"],
825 _('edit commit message without changing commit content'),
825 _('edit commit message without changing commit content'),
826 priority=True)
826 priority=True)
827 class message(histeditaction):
827 class message(histeditaction):
828 def commiteditor(self):
828 def commiteditor(self):
829 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
829 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
830
830
831 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
831 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
832 """utility function to find the first outgoing changeset
832 """utility function to find the first outgoing changeset
833
833
834 Used by initialization code"""
834 Used by initialization code"""
835 if opts is None:
835 if opts is None:
836 opts = {}
836 opts = {}
837 dest = ui.expandpath(remote or 'default-push', remote or 'default')
837 dest = ui.expandpath(remote or 'default-push', remote or 'default')
838 dest, revs = hg.parseurl(dest, None)[:2]
838 dest, revs = hg.parseurl(dest, None)[:2]
839 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
839 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
840
840
841 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
841 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
842 other = hg.peer(repo, opts, dest)
842 other = hg.peer(repo, opts, dest)
843
843
844 if revs:
844 if revs:
845 revs = [repo.lookup(rev) for rev in revs]
845 revs = [repo.lookup(rev) for rev in revs]
846
846
847 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
847 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
848 if not outgoing.missing:
848 if not outgoing.missing:
849 raise error.Abort(_('no outgoing ancestors'))
849 raise error.Abort(_('no outgoing ancestors'))
850 roots = list(repo.revs("roots(%ln)", outgoing.missing))
850 roots = list(repo.revs("roots(%ln)", outgoing.missing))
851 if 1 < len(roots):
851 if 1 < len(roots):
852 msg = _('there are ambiguous outgoing revisions')
852 msg = _('there are ambiguous outgoing revisions')
853 hint = _('see "hg help histedit" for more detail')
853 hint = _('see "hg help histedit" for more detail')
854 raise error.Abort(msg, hint=hint)
854 raise error.Abort(msg, hint=hint)
855 return repo.lookup(roots[0])
855 return repo.lookup(roots[0])
856
856
857
857
858 @command('histedit',
858 @command('histedit',
859 [('', 'commands', '',
859 [('', 'commands', '',
860 _('read history edits from the specified file'), _('FILE')),
860 _('read history edits from the specified file'), _('FILE')),
861 ('c', 'continue', False, _('continue an edit already in progress')),
861 ('c', 'continue', False, _('continue an edit already in progress')),
862 ('', 'edit-plan', False, _('edit remaining actions list')),
862 ('', 'edit-plan', False, _('edit remaining actions list')),
863 ('k', 'keep', False,
863 ('k', 'keep', False,
864 _("don't strip old nodes after edit is complete")),
864 _("don't strip old nodes after edit is complete")),
865 ('', 'abort', False, _('abort an edit in progress')),
865 ('', 'abort', False, _('abort an edit in progress')),
866 ('o', 'outgoing', False, _('changesets not found in destination')),
866 ('o', 'outgoing', False, _('changesets not found in destination')),
867 ('f', 'force', False,
867 ('f', 'force', False,
868 _('force outgoing even for unrelated repositories')),
868 _('force outgoing even for unrelated repositories')),
869 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
869 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
870 _("[ANCESTOR] | --outgoing [URL]"))
870 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
871 def histedit(ui, repo, *freeargs, **opts):
871 def histedit(ui, repo, *freeargs, **opts):
872 """interactively edit changeset history
872 """interactively edit changeset history
873
873
874 This command lets you edit a linear series of changesets (up to
874 This command lets you edit a linear series of changesets (up to
875 and including the working directory, which should be clean).
875 and including the working directory, which should be clean).
876 You can::
876 You can::
877
877
878 - `pick` to [re]order a changeset
878 - `pick` to [re]order a changeset
879
879
880 - `drop` to omit changeset
880 - `drop` to omit changeset
881
881
882 - `mess` to reword the changeset commit message
882 - `mess` to reword the changeset commit message
883
883
884 - `fold` to combine it with the preceding changeset
884 - `fold` to combine it with the preceding changeset
885
885
886 - `roll` like fold, but discarding this commit's description
886 - `roll` like fold, but discarding this commit's description
887
887
888 - `edit` to edit this changeset
888 - `edit` to edit this changeset
889
889
890 The value from the "histedit.defaultrev" config option is used as a
890 There are a number of ways to select the root changset::
891 revset to select the base revision when ANCESTOR is not specified.
891
892 The first revision returned by the revset is used. By default, this
892 - Specify ANCESTOR directly
893 selects the editable history that is unique to the ancestry of the
894 working directory.
895
893
896 With --outgoing, this edits changesets not found in the
894 - Use --outgoing -- it will be the first linear changeset not
897 destination repository. If URL of the destination is omitted, the
895 included in destination. (See :hg:"help default-push")
898 'default-push' (or 'default') path will be used.
896
897 - Otherwise, the value from the "histedit.defaultrev" config option
898 is used as a revset to select the base revision when ANCESTOR is not
899 specified. The first revision returned by the revset is used. By
900 default, this selects the editable history that is unique to the
901 ancestry of the working directory.
899
902
900 .. container:: verbose
903 .. container:: verbose
901
904
902 If you use --outgoing, this command will abort if there are ambiguous
905 If you use --outgoing, this command will abort if there are ambiguous
903 outgoing revisions. For example, if there are multiple branches
906 outgoing revisions. For example, if there are multiple branches
904 containing outgoing revisions.
907 containing outgoing revisions.
905
908
906 Use "min(outgoing() and ::.)" or similar revset specification
909 Use "min(outgoing() and ::.)" or similar revset specification
907 instead of --outgoing to specify edit target revision exactly in
910 instead of --outgoing to specify edit target revision exactly in
908 such ambiguous situation. See :hg:`help revsets` for detail about
911 such ambiguous situation. See :hg:`help revsets` for detail about
909 selecting revisions.
912 selecting revisions.
910
913
911 .. container:: verbose
914 .. container:: verbose
912
915
913 Examples:
916 Examples:
914
917
915 - A number of changes have been made.
918 - A number of changes have been made.
916 Revision 3 is no longer needed.
919 Revision 3 is no longer needed.
917
920
918 Start history editing from revision 3::
921 Start history editing from revision 3::
919
922
920 hg histedit -r 3
923 hg histedit -r 3
921
924
922 An editor opens, containing the list of revisions,
925 An editor opens, containing the list of revisions,
923 with specific actions specified::
926 with specific actions specified::
924
927
925 pick 5339bf82f0ca 3 Zworgle the foobar
928 pick 5339bf82f0ca 3 Zworgle the foobar
926 pick 8ef592ce7cc4 4 Bedazzle the zerlog
929 pick 8ef592ce7cc4 4 Bedazzle the zerlog
927 pick 0a9639fcda9d 5 Morgify the cromulancy
930 pick 0a9639fcda9d 5 Morgify the cromulancy
928
931
929 Additional information about the possible actions
932 Additional information about the possible actions
930 to take appears below the list of revisions.
933 to take appears below the list of revisions.
931
934
932 To remove revision 3 from the history,
935 To remove revision 3 from the history,
933 its action (at the beginning of the relevant line)
936 its action (at the beginning of the relevant line)
934 is changed to 'drop'::
937 is changed to 'drop'::
935
938
936 drop 5339bf82f0ca 3 Zworgle the foobar
939 drop 5339bf82f0ca 3 Zworgle the foobar
937 pick 8ef592ce7cc4 4 Bedazzle the zerlog
940 pick 8ef592ce7cc4 4 Bedazzle the zerlog
938 pick 0a9639fcda9d 5 Morgify the cromulancy
941 pick 0a9639fcda9d 5 Morgify the cromulancy
939
942
940 - A number of changes have been made.
943 - A number of changes have been made.
941 Revision 2 and 4 need to be swapped.
944 Revision 2 and 4 need to be swapped.
942
945
943 Start history editing from revision 2::
946 Start history editing from revision 2::
944
947
945 hg histedit -r 2
948 hg histedit -r 2
946
949
947 An editor opens, containing the list of revisions,
950 An editor opens, containing the list of revisions,
948 with specific actions specified::
951 with specific actions specified::
949
952
950 pick 252a1af424ad 2 Blorb a morgwazzle
953 pick 252a1af424ad 2 Blorb a morgwazzle
951 pick 5339bf82f0ca 3 Zworgle the foobar
954 pick 5339bf82f0ca 3 Zworgle the foobar
952 pick 8ef592ce7cc4 4 Bedazzle the zerlog
955 pick 8ef592ce7cc4 4 Bedazzle the zerlog
953
956
954 To swap revision 2 and 4, its lines are swapped
957 To swap revision 2 and 4, its lines are swapped
955 in the editor::
958 in the editor::
956
959
957 pick 8ef592ce7cc4 4 Bedazzle the zerlog
960 pick 8ef592ce7cc4 4 Bedazzle the zerlog
958 pick 5339bf82f0ca 3 Zworgle the foobar
961 pick 5339bf82f0ca 3 Zworgle the foobar
959 pick 252a1af424ad 2 Blorb a morgwazzle
962 pick 252a1af424ad 2 Blorb a morgwazzle
960
963
961 Returns 0 on success, 1 if user intervention is required (not only
964 Returns 0 on success, 1 if user intervention is required (not only
962 for intentional "edit" command, but also for resolving unexpected
965 for intentional "edit" command, but also for resolving unexpected
963 conflicts).
966 conflicts).
964 """
967 """
965 state = histeditstate(repo)
968 state = histeditstate(repo)
966 try:
969 try:
967 state.wlock = repo.wlock()
970 state.wlock = repo.wlock()
968 state.lock = repo.lock()
971 state.lock = repo.lock()
969 _histedit(ui, repo, state, *freeargs, **opts)
972 _histedit(ui, repo, state, *freeargs, **opts)
970 finally:
973 finally:
971 release(state.lock, state.wlock)
974 release(state.lock, state.wlock)
972
975
973 def _histedit(ui, repo, state, *freeargs, **opts):
976 def _histedit(ui, repo, state, *freeargs, **opts):
974 # TODO only abort if we try to histedit mq patches, not just
977 # TODO only abort if we try to histedit mq patches, not just
975 # blanket if mq patches are applied somewhere
978 # blanket if mq patches are applied somewhere
976 mq = getattr(repo, 'mq', None)
979 mq = getattr(repo, 'mq', None)
977 if mq and mq.applied:
980 if mq and mq.applied:
978 raise error.Abort(_('source has mq patches applied'))
981 raise error.Abort(_('source has mq patches applied'))
979
982
980 # basic argument incompatibility processing
983 # basic argument incompatibility processing
981 outg = opts.get('outgoing')
984 outg = opts.get('outgoing')
982 cont = opts.get('continue')
985 cont = opts.get('continue')
983 editplan = opts.get('edit_plan')
986 editplan = opts.get('edit_plan')
984 abort = opts.get('abort')
987 abort = opts.get('abort')
985 force = opts.get('force')
988 force = opts.get('force')
986 rules = opts.get('commands', '')
989 rules = opts.get('commands', '')
987 revs = opts.get('rev', [])
990 revs = opts.get('rev', [])
988 goal = 'new' # This invocation goal, in new, continue, abort
991 goal = 'new' # This invocation goal, in new, continue, abort
989 if force and not outg:
992 if force and not outg:
990 raise error.Abort(_('--force only allowed with --outgoing'))
993 raise error.Abort(_('--force only allowed with --outgoing'))
991 if cont:
994 if cont:
992 if any((outg, abort, revs, freeargs, rules, editplan)):
995 if any((outg, abort, revs, freeargs, rules, editplan)):
993 raise error.Abort(_('no arguments allowed with --continue'))
996 raise error.Abort(_('no arguments allowed with --continue'))
994 goal = 'continue'
997 goal = 'continue'
995 elif abort:
998 elif abort:
996 if any((outg, revs, freeargs, rules, editplan)):
999 if any((outg, revs, freeargs, rules, editplan)):
997 raise error.Abort(_('no arguments allowed with --abort'))
1000 raise error.Abort(_('no arguments allowed with --abort'))
998 goal = 'abort'
1001 goal = 'abort'
999 elif editplan:
1002 elif editplan:
1000 if any((outg, revs, freeargs)):
1003 if any((outg, revs, freeargs)):
1001 raise error.Abort(_('only --commands argument allowed with '
1004 raise error.Abort(_('only --commands argument allowed with '
1002 '--edit-plan'))
1005 '--edit-plan'))
1003 goal = 'edit-plan'
1006 goal = 'edit-plan'
1004 else:
1007 else:
1005 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1008 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1006 raise error.Abort(_('history edit already in progress, try '
1009 raise error.Abort(_('history edit already in progress, try '
1007 '--continue or --abort'))
1010 '--continue or --abort'))
1008 if outg:
1011 if outg:
1009 if revs:
1012 if revs:
1010 raise error.Abort(_('no revisions allowed with --outgoing'))
1013 raise error.Abort(_('no revisions allowed with --outgoing'))
1011 if len(freeargs) > 1:
1014 if len(freeargs) > 1:
1012 raise error.Abort(
1015 raise error.Abort(
1013 _('only one repo argument allowed with --outgoing'))
1016 _('only one repo argument allowed with --outgoing'))
1014 else:
1017 else:
1015 revs.extend(freeargs)
1018 revs.extend(freeargs)
1016 if len(revs) == 0:
1019 if len(revs) == 0:
1017 defaultrev = destutil.desthistedit(ui, repo)
1020 defaultrev = destutil.desthistedit(ui, repo)
1018 if defaultrev is not None:
1021 if defaultrev is not None:
1019 revs.append(defaultrev)
1022 revs.append(defaultrev)
1020
1023
1021 if len(revs) != 1:
1024 if len(revs) != 1:
1022 raise error.Abort(
1025 raise error.Abort(
1023 _('histedit requires exactly one ancestor revision'))
1026 _('histedit requires exactly one ancestor revision'))
1024
1027
1025
1028
1026 replacements = []
1029 replacements = []
1027 state.keep = opts.get('keep', False)
1030 state.keep = opts.get('keep', False)
1028 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
1031 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
1029
1032
1030 # rebuild state
1033 # rebuild state
1031 if goal == 'continue':
1034 if goal == 'continue':
1032 state.read()
1035 state.read()
1033 state = bootstrapcontinue(ui, state, opts)
1036 state = bootstrapcontinue(ui, state, opts)
1034 elif goal == 'edit-plan':
1037 elif goal == 'edit-plan':
1035 state.read()
1038 state.read()
1036 if not rules:
1039 if not rules:
1037 comment = geteditcomment(node.short(state.parentctxnode),
1040 comment = geteditcomment(node.short(state.parentctxnode),
1038 node.short(state.topmost))
1041 node.short(state.topmost))
1039 rules = ruleeditor(repo, ui, state.actions, comment)
1042 rules = ruleeditor(repo, ui, state.actions, comment)
1040 else:
1043 else:
1041 if rules == '-':
1044 if rules == '-':
1042 f = sys.stdin
1045 f = sys.stdin
1043 else:
1046 else:
1044 f = open(rules)
1047 f = open(rules)
1045 rules = f.read()
1048 rules = f.read()
1046 f.close()
1049 f.close()
1047 actions = parserules(rules, state)
1050 actions = parserules(rules, state)
1048 ctxs = [repo[act.nodetoverify()] \
1051 ctxs = [repo[act.nodetoverify()] \
1049 for act in state.actions if act.nodetoverify()]
1052 for act in state.actions if act.nodetoverify()]
1050 warnverifyactions(ui, repo, actions, state, ctxs)
1053 warnverifyactions(ui, repo, actions, state, ctxs)
1051 state.actions = actions
1054 state.actions = actions
1052 state.write()
1055 state.write()
1053 return
1056 return
1054 elif goal == 'abort':
1057 elif goal == 'abort':
1055 try:
1058 try:
1056 state.read()
1059 state.read()
1057 tmpnodes, leafs = newnodestoabort(state)
1060 tmpnodes, leafs = newnodestoabort(state)
1058 ui.debug('restore wc to old parent %s\n'
1061 ui.debug('restore wc to old parent %s\n'
1059 % node.short(state.topmost))
1062 % node.short(state.topmost))
1060
1063
1061 # Recover our old commits if necessary
1064 # Recover our old commits if necessary
1062 if not state.topmost in repo and state.backupfile:
1065 if not state.topmost in repo and state.backupfile:
1063 backupfile = repo.join(state.backupfile)
1066 backupfile = repo.join(state.backupfile)
1064 f = hg.openpath(ui, backupfile)
1067 f = hg.openpath(ui, backupfile)
1065 gen = exchange.readbundle(ui, f, backupfile)
1068 gen = exchange.readbundle(ui, f, backupfile)
1066 tr = repo.transaction('histedit.abort')
1069 tr = repo.transaction('histedit.abort')
1067 try:
1070 try:
1068 if not isinstance(gen, bundle2.unbundle20):
1071 if not isinstance(gen, bundle2.unbundle20):
1069 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1072 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1070 if isinstance(gen, bundle2.unbundle20):
1073 if isinstance(gen, bundle2.unbundle20):
1071 bundle2.applybundle(repo, gen, tr,
1074 bundle2.applybundle(repo, gen, tr,
1072 source='histedit',
1075 source='histedit',
1073 url='bundle:' + backupfile)
1076 url='bundle:' + backupfile)
1074 tr.close()
1077 tr.close()
1075 finally:
1078 finally:
1076 tr.release()
1079 tr.release()
1077
1080
1078 os.remove(backupfile)
1081 os.remove(backupfile)
1079
1082
1080 # check whether we should update away
1083 # check whether we should update away
1081 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1084 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1082 state.parentctxnode, leafs | tmpnodes):
1085 state.parentctxnode, leafs | tmpnodes):
1083 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1086 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1084 cleanupnode(ui, repo, 'created', tmpnodes)
1087 cleanupnode(ui, repo, 'created', tmpnodes)
1085 cleanupnode(ui, repo, 'temp', leafs)
1088 cleanupnode(ui, repo, 'temp', leafs)
1086 except Exception:
1089 except Exception:
1087 if state.inprogress():
1090 if state.inprogress():
1088 ui.warn(_('warning: encountered an exception during histedit '
1091 ui.warn(_('warning: encountered an exception during histedit '
1089 '--abort; the repository may not have been completely '
1092 '--abort; the repository may not have been completely '
1090 'cleaned up\n'))
1093 'cleaned up\n'))
1091 raise
1094 raise
1092 finally:
1095 finally:
1093 state.clear()
1096 state.clear()
1094 return
1097 return
1095 else:
1098 else:
1096 cmdutil.checkunfinished(repo)
1099 cmdutil.checkunfinished(repo)
1097 cmdutil.bailifchanged(repo)
1100 cmdutil.bailifchanged(repo)
1098
1101
1099 topmost, empty = repo.dirstate.parents()
1102 topmost, empty = repo.dirstate.parents()
1100 if outg:
1103 if outg:
1101 if freeargs:
1104 if freeargs:
1102 remote = freeargs[0]
1105 remote = freeargs[0]
1103 else:
1106 else:
1104 remote = None
1107 remote = None
1105 root = findoutgoing(ui, repo, remote, force, opts)
1108 root = findoutgoing(ui, repo, remote, force, opts)
1106 else:
1109 else:
1107 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1110 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1108 if len(rr) != 1:
1111 if len(rr) != 1:
1109 raise error.Abort(_('The specified revisions must have '
1112 raise error.Abort(_('The specified revisions must have '
1110 'exactly one common root'))
1113 'exactly one common root'))
1111 root = rr[0].node()
1114 root = rr[0].node()
1112
1115
1113 revs = between(repo, root, topmost, state.keep)
1116 revs = between(repo, root, topmost, state.keep)
1114 if not revs:
1117 if not revs:
1115 raise error.Abort(_('%s is not an ancestor of working directory') %
1118 raise error.Abort(_('%s is not an ancestor of working directory') %
1116 node.short(root))
1119 node.short(root))
1117
1120
1118 ctxs = [repo[r] for r in revs]
1121 ctxs = [repo[r] for r in revs]
1119 if not rules:
1122 if not rules:
1120 comment = geteditcomment(node.short(root), node.short(topmost))
1123 comment = geteditcomment(node.short(root), node.short(topmost))
1121 actions = [pick(state, r) for r in revs]
1124 actions = [pick(state, r) for r in revs]
1122 rules = ruleeditor(repo, ui, actions, comment)
1125 rules = ruleeditor(repo, ui, actions, comment)
1123 else:
1126 else:
1124 if rules == '-':
1127 if rules == '-':
1125 f = sys.stdin
1128 f = sys.stdin
1126 else:
1129 else:
1127 f = open(rules)
1130 f = open(rules)
1128 rules = f.read()
1131 rules = f.read()
1129 f.close()
1132 f.close()
1130 actions = parserules(rules, state)
1133 actions = parserules(rules, state)
1131 warnverifyactions(ui, repo, actions, state, ctxs)
1134 warnverifyactions(ui, repo, actions, state, ctxs)
1132
1135
1133 parentctxnode = repo[root].parents()[0].node()
1136 parentctxnode = repo[root].parents()[0].node()
1134
1137
1135 state.parentctxnode = parentctxnode
1138 state.parentctxnode = parentctxnode
1136 state.actions = actions
1139 state.actions = actions
1137 state.topmost = topmost
1140 state.topmost = topmost
1138 state.replacements = replacements
1141 state.replacements = replacements
1139
1142
1140 # Create a backup so we can always abort completely.
1143 # Create a backup so we can always abort completely.
1141 backupfile = None
1144 backupfile = None
1142 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1145 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1143 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1146 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1144 'histedit')
1147 'histedit')
1145 state.backupfile = backupfile
1148 state.backupfile = backupfile
1146
1149
1147 # preprocess rules so that we can hide inner folds from the user
1150 # preprocess rules so that we can hide inner folds from the user
1148 # and only show one editor
1151 # and only show one editor
1149 actions = state.actions[:]
1152 actions = state.actions[:]
1150 for idx, (action, nextact) in enumerate(
1153 for idx, (action, nextact) in enumerate(
1151 zip(actions, actions[1:] + [None])):
1154 zip(actions, actions[1:] + [None])):
1152 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1155 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1153 state.actions[idx].__class__ = _multifold
1156 state.actions[idx].__class__ = _multifold
1154
1157
1155 total = len(state.actions)
1158 total = len(state.actions)
1156 pos = 0
1159 pos = 0
1157 while state.actions:
1160 while state.actions:
1158 state.write()
1161 state.write()
1159 actobj = state.actions.pop(0)
1162 actobj = state.actions.pop(0)
1160 pos += 1
1163 pos += 1
1161 ui.progress(_("editing"), pos, actobj.torule(),
1164 ui.progress(_("editing"), pos, actobj.torule(),
1162 _('changes'), total)
1165 _('changes'), total)
1163 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1166 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1164 actobj.torule()))
1167 actobj.torule()))
1165 parentctx, replacement_ = actobj.run()
1168 parentctx, replacement_ = actobj.run()
1166 state.parentctxnode = parentctx.node()
1169 state.parentctxnode = parentctx.node()
1167 state.replacements.extend(replacement_)
1170 state.replacements.extend(replacement_)
1168 state.write()
1171 state.write()
1169 ui.progress(_("editing"), None)
1172 ui.progress(_("editing"), None)
1170
1173
1171 hg.update(repo, state.parentctxnode, quietempty=True)
1174 hg.update(repo, state.parentctxnode, quietempty=True)
1172
1175
1173 mapping, tmpnodes, created, ntm = processreplacement(state)
1176 mapping, tmpnodes, created, ntm = processreplacement(state)
1174 if mapping:
1177 if mapping:
1175 for prec, succs in mapping.iteritems():
1178 for prec, succs in mapping.iteritems():
1176 if not succs:
1179 if not succs:
1177 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1180 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1178 else:
1181 else:
1179 ui.debug('histedit: %s is replaced by %s\n' % (
1182 ui.debug('histedit: %s is replaced by %s\n' % (
1180 node.short(prec), node.short(succs[0])))
1183 node.short(prec), node.short(succs[0])))
1181 if len(succs) > 1:
1184 if len(succs) > 1:
1182 m = 'histedit: %s'
1185 m = 'histedit: %s'
1183 for n in succs[1:]:
1186 for n in succs[1:]:
1184 ui.debug(m % node.short(n))
1187 ui.debug(m % node.short(n))
1185
1188
1186 if supportsmarkers:
1189 if supportsmarkers:
1187 # Only create markers if the temp nodes weren't already removed.
1190 # Only create markers if the temp nodes weren't already removed.
1188 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1191 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1189 if t in repo))
1192 if t in repo))
1190 else:
1193 else:
1191 cleanupnode(ui, repo, 'temp', tmpnodes)
1194 cleanupnode(ui, repo, 'temp', tmpnodes)
1192
1195
1193 if not state.keep:
1196 if not state.keep:
1194 if mapping:
1197 if mapping:
1195 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1198 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1196 # TODO update mq state
1199 # TODO update mq state
1197 if supportsmarkers:
1200 if supportsmarkers:
1198 markers = []
1201 markers = []
1199 # sort by revision number because it sound "right"
1202 # sort by revision number because it sound "right"
1200 for prec in sorted(mapping, key=repo.changelog.rev):
1203 for prec in sorted(mapping, key=repo.changelog.rev):
1201 succs = mapping[prec]
1204 succs = mapping[prec]
1202 markers.append((repo[prec],
1205 markers.append((repo[prec],
1203 tuple(repo[s] for s in succs)))
1206 tuple(repo[s] for s in succs)))
1204 if markers:
1207 if markers:
1205 obsolete.createmarkers(repo, markers)
1208 obsolete.createmarkers(repo, markers)
1206 else:
1209 else:
1207 cleanupnode(ui, repo, 'replaced', mapping)
1210 cleanupnode(ui, repo, 'replaced', mapping)
1208
1211
1209 state.clear()
1212 state.clear()
1210 if os.path.exists(repo.sjoin('undo')):
1213 if os.path.exists(repo.sjoin('undo')):
1211 os.unlink(repo.sjoin('undo'))
1214 os.unlink(repo.sjoin('undo'))
1212 if repo.vfs.exists('histedit-last-edit.txt'):
1215 if repo.vfs.exists('histedit-last-edit.txt'):
1213 repo.vfs.unlink('histedit-last-edit.txt')
1216 repo.vfs.unlink('histedit-last-edit.txt')
1214
1217
1215 def bootstrapcontinue(ui, state, opts):
1218 def bootstrapcontinue(ui, state, opts):
1216 repo = state.repo
1219 repo = state.repo
1217 if state.actions:
1220 if state.actions:
1218 actobj = state.actions.pop(0)
1221 actobj = state.actions.pop(0)
1219
1222
1220 if _isdirtywc(repo):
1223 if _isdirtywc(repo):
1221 actobj.continuedirty()
1224 actobj.continuedirty()
1222 if _isdirtywc(repo):
1225 if _isdirtywc(repo):
1223 abortdirty()
1226 abortdirty()
1224
1227
1225 parentctx, replacements = actobj.continueclean()
1228 parentctx, replacements = actobj.continueclean()
1226
1229
1227 state.parentctxnode = parentctx.node()
1230 state.parentctxnode = parentctx.node()
1228 state.replacements.extend(replacements)
1231 state.replacements.extend(replacements)
1229
1232
1230 return state
1233 return state
1231
1234
1232 def between(repo, old, new, keep):
1235 def between(repo, old, new, keep):
1233 """select and validate the set of revision to edit
1236 """select and validate the set of revision to edit
1234
1237
1235 When keep is false, the specified set can't have children."""
1238 When keep is false, the specified set can't have children."""
1236 ctxs = list(repo.set('%n::%n', old, new))
1239 ctxs = list(repo.set('%n::%n', old, new))
1237 if ctxs and not keep:
1240 if ctxs and not keep:
1238 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1241 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1239 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1242 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1240 raise error.Abort(_('cannot edit history that would orphan nodes'))
1243 raise error.Abort(_('cannot edit history that would orphan nodes'))
1241 if repo.revs('(%ld) and merge()', ctxs):
1244 if repo.revs('(%ld) and merge()', ctxs):
1242 raise error.Abort(_('cannot edit history that contains merges'))
1245 raise error.Abort(_('cannot edit history that contains merges'))
1243 root = ctxs[0] # list is already sorted by repo.set
1246 root = ctxs[0] # list is already sorted by repo.set
1244 if not root.mutable():
1247 if not root.mutable():
1245 raise error.Abort(_('cannot edit public changeset: %s') % root,
1248 raise error.Abort(_('cannot edit public changeset: %s') % root,
1246 hint=_('see "hg help phases" for details'))
1249 hint=_('see "hg help phases" for details'))
1247 return [c.node() for c in ctxs]
1250 return [c.node() for c in ctxs]
1248
1251
1249 def ruleeditor(repo, ui, actions, editcomment=""):
1252 def ruleeditor(repo, ui, actions, editcomment=""):
1250 """open an editor to edit rules
1253 """open an editor to edit rules
1251
1254
1252 rules are in the format [ [act, ctx], ...] like in state.rules
1255 rules are in the format [ [act, ctx], ...] like in state.rules
1253 """
1256 """
1254 rules = '\n'.join([act.torule() for act in actions])
1257 rules = '\n'.join([act.torule() for act in actions])
1255 rules += '\n\n'
1258 rules += '\n\n'
1256 rules += editcomment
1259 rules += editcomment
1257 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1260 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1258
1261
1259 # Save edit rules in .hg/histedit-last-edit.txt in case
1262 # Save edit rules in .hg/histedit-last-edit.txt in case
1260 # the user needs to ask for help after something
1263 # the user needs to ask for help after something
1261 # surprising happens.
1264 # surprising happens.
1262 f = open(repo.join('histedit-last-edit.txt'), 'w')
1265 f = open(repo.join('histedit-last-edit.txt'), 'w')
1263 f.write(rules)
1266 f.write(rules)
1264 f.close()
1267 f.close()
1265
1268
1266 return rules
1269 return rules
1267
1270
1268 def parserules(rules, state):
1271 def parserules(rules, state):
1269 """Read the histedit rules string and return list of action objects """
1272 """Read the histedit rules string and return list of action objects """
1270 rules = [l for l in (r.strip() for r in rules.splitlines())
1273 rules = [l for l in (r.strip() for r in rules.splitlines())
1271 if l and not l.startswith('#')]
1274 if l and not l.startswith('#')]
1272 actions = []
1275 actions = []
1273 for r in rules:
1276 for r in rules:
1274 if ' ' not in r:
1277 if ' ' not in r:
1275 raise error.ParseError(_('malformed line "%s"') % r)
1278 raise error.ParseError(_('malformed line "%s"') % r)
1276 verb, rest = r.split(' ', 1)
1279 verb, rest = r.split(' ', 1)
1277
1280
1278 if verb not in actiontable:
1281 if verb not in actiontable:
1279 raise error.ParseError(_('unknown action "%s"') % verb)
1282 raise error.ParseError(_('unknown action "%s"') % verb)
1280
1283
1281 action = actiontable[verb].fromrule(state, rest)
1284 action = actiontable[verb].fromrule(state, rest)
1282 actions.append(action)
1285 actions.append(action)
1283 return actions
1286 return actions
1284
1287
1285 def warnverifyactions(ui, repo, actions, state, ctxs):
1288 def warnverifyactions(ui, repo, actions, state, ctxs):
1286 try:
1289 try:
1287 verifyactions(actions, state, ctxs)
1290 verifyactions(actions, state, ctxs)
1288 except error.ParseError:
1291 except error.ParseError:
1289 if repo.vfs.exists('histedit-last-edit.txt'):
1292 if repo.vfs.exists('histedit-last-edit.txt'):
1290 ui.warn(_('warning: histedit rules saved '
1293 ui.warn(_('warning: histedit rules saved '
1291 'to: .hg/histedit-last-edit.txt\n'))
1294 'to: .hg/histedit-last-edit.txt\n'))
1292 raise
1295 raise
1293
1296
1294 def verifyactions(actions, state, ctxs):
1297 def verifyactions(actions, state, ctxs):
1295 """Verify that there exists exactly one action per given changeset and
1298 """Verify that there exists exactly one action per given changeset and
1296 other constraints.
1299 other constraints.
1297
1300
1298 Will abort if there are to many or too few rules, a malformed rule,
1301 Will abort if there are to many or too few rules, a malformed rule,
1299 or a rule on a changeset outside of the user-given range.
1302 or a rule on a changeset outside of the user-given range.
1300 """
1303 """
1301 expected = set(c.hex() for c in ctxs)
1304 expected = set(c.hex() for c in ctxs)
1302 seen = set()
1305 seen = set()
1303 prev = None
1306 prev = None
1304 for action in actions:
1307 for action in actions:
1305 action.verify(prev)
1308 action.verify(prev)
1306 prev = action
1309 prev = action
1307 constraints = action.constraints()
1310 constraints = action.constraints()
1308 for constraint in constraints:
1311 for constraint in constraints:
1309 if constraint not in _constraints.known():
1312 if constraint not in _constraints.known():
1310 raise error.ParseError(_('unknown constraint "%s"') %
1313 raise error.ParseError(_('unknown constraint "%s"') %
1311 constraint)
1314 constraint)
1312
1315
1313 nodetoverify = action.nodetoverify()
1316 nodetoverify = action.nodetoverify()
1314 if nodetoverify is not None:
1317 if nodetoverify is not None:
1315 ha = node.hex(nodetoverify)
1318 ha = node.hex(nodetoverify)
1316 if _constraints.noother in constraints and ha not in expected:
1319 if _constraints.noother in constraints and ha not in expected:
1317 raise error.ParseError(
1320 raise error.ParseError(
1318 _('%s "%s" changeset was not a candidate')
1321 _('%s "%s" changeset was not a candidate')
1319 % (action.verb, node.short(ha)),
1322 % (action.verb, node.short(ha)),
1320 hint=_('only use listed changesets'))
1323 hint=_('only use listed changesets'))
1321 if _constraints.forceother in constraints and ha in expected:
1324 if _constraints.forceother in constraints and ha in expected:
1322 raise error.ParseError(
1325 raise error.ParseError(
1323 _('%s "%s" changeset was not an edited list candidate')
1326 _('%s "%s" changeset was not an edited list candidate')
1324 % (action.verb, node.short(ha)),
1327 % (action.verb, node.short(ha)),
1325 hint=_('only use listed changesets'))
1328 hint=_('only use listed changesets'))
1326 if _constraints.noduplicates in constraints and ha in seen:
1329 if _constraints.noduplicates in constraints and ha in seen:
1327 raise error.ParseError(_(
1330 raise error.ParseError(_(
1328 'duplicated command for changeset %s') %
1331 'duplicated command for changeset %s') %
1329 ha[:12])
1332 ha[:12])
1330 seen.add(ha)
1333 seen.add(ha)
1331 missing = sorted(expected - seen) # sort to stabilize output
1334 missing = sorted(expected - seen) # sort to stabilize output
1332
1335
1333 if state.repo.ui.configbool('histedit', 'dropmissing'):
1336 if state.repo.ui.configbool('histedit', 'dropmissing'):
1334 drops = [drop(state, node.bin(n)) for n in missing]
1337 drops = [drop(state, node.bin(n)) for n in missing]
1335 # put the in the beginning so they execute immediately and
1338 # put the in the beginning so they execute immediately and
1336 # don't show in the edit-plan in the future
1339 # don't show in the edit-plan in the future
1337 actions[:0] = drops
1340 actions[:0] = drops
1338 elif missing:
1341 elif missing:
1339 raise error.ParseError(_('missing rules for changeset %s') %
1342 raise error.ParseError(_('missing rules for changeset %s') %
1340 missing[0][:12],
1343 missing[0][:12],
1341 hint=_('use "drop %s" to discard, see also: '
1344 hint=_('use "drop %s" to discard, see also: '
1342 '"hg help -e histedit.config"') % missing[0][:12])
1345 '"hg help -e histedit.config"') % missing[0][:12])
1343
1346
1344 def newnodestoabort(state):
1347 def newnodestoabort(state):
1345 """process the list of replacements to return
1348 """process the list of replacements to return
1346
1349
1347 1) the list of final node
1350 1) the list of final node
1348 2) the list of temporary node
1351 2) the list of temporary node
1349
1352
1350 This is meant to be used on abort as less data are required in this case.
1353 This is meant to be used on abort as less data are required in this case.
1351 """
1354 """
1352 replacements = state.replacements
1355 replacements = state.replacements
1353 allsuccs = set()
1356 allsuccs = set()
1354 replaced = set()
1357 replaced = set()
1355 for rep in replacements:
1358 for rep in replacements:
1356 allsuccs.update(rep[1])
1359 allsuccs.update(rep[1])
1357 replaced.add(rep[0])
1360 replaced.add(rep[0])
1358 newnodes = allsuccs - replaced
1361 newnodes = allsuccs - replaced
1359 tmpnodes = allsuccs & replaced
1362 tmpnodes = allsuccs & replaced
1360 return newnodes, tmpnodes
1363 return newnodes, tmpnodes
1361
1364
1362
1365
1363 def processreplacement(state):
1366 def processreplacement(state):
1364 """process the list of replacements to return
1367 """process the list of replacements to return
1365
1368
1366 1) the final mapping between original and created nodes
1369 1) the final mapping between original and created nodes
1367 2) the list of temporary node created by histedit
1370 2) the list of temporary node created by histedit
1368 3) the list of new commit created by histedit"""
1371 3) the list of new commit created by histedit"""
1369 replacements = state.replacements
1372 replacements = state.replacements
1370 allsuccs = set()
1373 allsuccs = set()
1371 replaced = set()
1374 replaced = set()
1372 fullmapping = {}
1375 fullmapping = {}
1373 # initialize basic set
1376 # initialize basic set
1374 # fullmapping records all operations recorded in replacement
1377 # fullmapping records all operations recorded in replacement
1375 for rep in replacements:
1378 for rep in replacements:
1376 allsuccs.update(rep[1])
1379 allsuccs.update(rep[1])
1377 replaced.add(rep[0])
1380 replaced.add(rep[0])
1378 fullmapping.setdefault(rep[0], set()).update(rep[1])
1381 fullmapping.setdefault(rep[0], set()).update(rep[1])
1379 new = allsuccs - replaced
1382 new = allsuccs - replaced
1380 tmpnodes = allsuccs & replaced
1383 tmpnodes = allsuccs & replaced
1381 # Reduce content fullmapping into direct relation between original nodes
1384 # Reduce content fullmapping into direct relation between original nodes
1382 # and final node created during history edition
1385 # and final node created during history edition
1383 # Dropped changeset are replaced by an empty list
1386 # Dropped changeset are replaced by an empty list
1384 toproceed = set(fullmapping)
1387 toproceed = set(fullmapping)
1385 final = {}
1388 final = {}
1386 while toproceed:
1389 while toproceed:
1387 for x in list(toproceed):
1390 for x in list(toproceed):
1388 succs = fullmapping[x]
1391 succs = fullmapping[x]
1389 for s in list(succs):
1392 for s in list(succs):
1390 if s in toproceed:
1393 if s in toproceed:
1391 # non final node with unknown closure
1394 # non final node with unknown closure
1392 # We can't process this now
1395 # We can't process this now
1393 break
1396 break
1394 elif s in final:
1397 elif s in final:
1395 # non final node, replace with closure
1398 # non final node, replace with closure
1396 succs.remove(s)
1399 succs.remove(s)
1397 succs.update(final[s])
1400 succs.update(final[s])
1398 else:
1401 else:
1399 final[x] = succs
1402 final[x] = succs
1400 toproceed.remove(x)
1403 toproceed.remove(x)
1401 # remove tmpnodes from final mapping
1404 # remove tmpnodes from final mapping
1402 for n in tmpnodes:
1405 for n in tmpnodes:
1403 del final[n]
1406 del final[n]
1404 # we expect all changes involved in final to exist in the repo
1407 # we expect all changes involved in final to exist in the repo
1405 # turn `final` into list (topologically sorted)
1408 # turn `final` into list (topologically sorted)
1406 nm = state.repo.changelog.nodemap
1409 nm = state.repo.changelog.nodemap
1407 for prec, succs in final.items():
1410 for prec, succs in final.items():
1408 final[prec] = sorted(succs, key=nm.get)
1411 final[prec] = sorted(succs, key=nm.get)
1409
1412
1410 # computed topmost element (necessary for bookmark)
1413 # computed topmost element (necessary for bookmark)
1411 if new:
1414 if new:
1412 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1415 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1413 elif not final:
1416 elif not final:
1414 # Nothing rewritten at all. we won't need `newtopmost`
1417 # Nothing rewritten at all. we won't need `newtopmost`
1415 # It is the same as `oldtopmost` and `processreplacement` know it
1418 # It is the same as `oldtopmost` and `processreplacement` know it
1416 newtopmost = None
1419 newtopmost = None
1417 else:
1420 else:
1418 # every body died. The newtopmost is the parent of the root.
1421 # every body died. The newtopmost is the parent of the root.
1419 r = state.repo.changelog.rev
1422 r = state.repo.changelog.rev
1420 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1423 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1421
1424
1422 return final, tmpnodes, new, newtopmost
1425 return final, tmpnodes, new, newtopmost
1423
1426
1424 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1427 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1425 """Move bookmark from old to newly created node"""
1428 """Move bookmark from old to newly created node"""
1426 if not mapping:
1429 if not mapping:
1427 # if nothing got rewritten there is not purpose for this function
1430 # if nothing got rewritten there is not purpose for this function
1428 return
1431 return
1429 moves = []
1432 moves = []
1430 for bk, old in sorted(repo._bookmarks.iteritems()):
1433 for bk, old in sorted(repo._bookmarks.iteritems()):
1431 if old == oldtopmost:
1434 if old == oldtopmost:
1432 # special case ensure bookmark stay on tip.
1435 # special case ensure bookmark stay on tip.
1433 #
1436 #
1434 # This is arguably a feature and we may only want that for the
1437 # This is arguably a feature and we may only want that for the
1435 # active bookmark. But the behavior is kept compatible with the old
1438 # active bookmark. But the behavior is kept compatible with the old
1436 # version for now.
1439 # version for now.
1437 moves.append((bk, newtopmost))
1440 moves.append((bk, newtopmost))
1438 continue
1441 continue
1439 base = old
1442 base = old
1440 new = mapping.get(base, None)
1443 new = mapping.get(base, None)
1441 if new is None:
1444 if new is None:
1442 continue
1445 continue
1443 while not new:
1446 while not new:
1444 # base is killed, trying with parent
1447 # base is killed, trying with parent
1445 base = repo[base].p1().node()
1448 base = repo[base].p1().node()
1446 new = mapping.get(base, (base,))
1449 new = mapping.get(base, (base,))
1447 # nothing to move
1450 # nothing to move
1448 moves.append((bk, new[-1]))
1451 moves.append((bk, new[-1]))
1449 if moves:
1452 if moves:
1450 lock = tr = None
1453 lock = tr = None
1451 try:
1454 try:
1452 lock = repo.lock()
1455 lock = repo.lock()
1453 tr = repo.transaction('histedit')
1456 tr = repo.transaction('histedit')
1454 marks = repo._bookmarks
1457 marks = repo._bookmarks
1455 for mark, new in moves:
1458 for mark, new in moves:
1456 old = marks[mark]
1459 old = marks[mark]
1457 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1460 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1458 % (mark, node.short(old), node.short(new)))
1461 % (mark, node.short(old), node.short(new)))
1459 marks[mark] = new
1462 marks[mark] = new
1460 marks.recordchange(tr)
1463 marks.recordchange(tr)
1461 tr.close()
1464 tr.close()
1462 finally:
1465 finally:
1463 release(tr, lock)
1466 release(tr, lock)
1464
1467
1465 def cleanupnode(ui, repo, name, nodes):
1468 def cleanupnode(ui, repo, name, nodes):
1466 """strip a group of nodes from the repository
1469 """strip a group of nodes from the repository
1467
1470
1468 The set of node to strip may contains unknown nodes."""
1471 The set of node to strip may contains unknown nodes."""
1469 ui.debug('should strip %s nodes %s\n' %
1472 ui.debug('should strip %s nodes %s\n' %
1470 (name, ', '.join([node.short(n) for n in nodes])))
1473 (name, ', '.join([node.short(n) for n in nodes])))
1471 lock = None
1474 lock = None
1472 try:
1475 try:
1473 lock = repo.lock()
1476 lock = repo.lock()
1474 # do not let filtering get in the way of the cleanse
1477 # do not let filtering get in the way of the cleanse
1475 # we should probably get rid of obsolescence marker created during the
1478 # we should probably get rid of obsolescence marker created during the
1476 # histedit, but we currently do not have such information.
1479 # histedit, but we currently do not have such information.
1477 repo = repo.unfiltered()
1480 repo = repo.unfiltered()
1478 # Find all nodes that need to be stripped
1481 # Find all nodes that need to be stripped
1479 # (we use %lr instead of %ln to silently ignore unknown items)
1482 # (we use %lr instead of %ln to silently ignore unknown items)
1480 nm = repo.changelog.nodemap
1483 nm = repo.changelog.nodemap
1481 nodes = sorted(n for n in nodes if n in nm)
1484 nodes = sorted(n for n in nodes if n in nm)
1482 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1485 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1483 for c in roots:
1486 for c in roots:
1484 # We should process node in reverse order to strip tip most first.
1487 # We should process node in reverse order to strip tip most first.
1485 # but this trigger a bug in changegroup hook.
1488 # but this trigger a bug in changegroup hook.
1486 # This would reduce bundle overhead
1489 # This would reduce bundle overhead
1487 repair.strip(ui, repo, c)
1490 repair.strip(ui, repo, c)
1488 finally:
1491 finally:
1489 release(lock)
1492 release(lock)
1490
1493
1491 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1494 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1492 if isinstance(nodelist, str):
1495 if isinstance(nodelist, str):
1493 nodelist = [nodelist]
1496 nodelist = [nodelist]
1494 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1497 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1495 state = histeditstate(repo)
1498 state = histeditstate(repo)
1496 state.read()
1499 state.read()
1497 histedit_nodes = set([action.nodetoverify() for action
1500 histedit_nodes = set([action.nodetoverify() for action
1498 in state.actions if action.nodetoverify()])
1501 in state.actions if action.nodetoverify()])
1499 strip_nodes = set([repo[n].node() for n in nodelist])
1502 strip_nodes = set([repo[n].node() for n in nodelist])
1500 common_nodes = histedit_nodes & strip_nodes
1503 common_nodes = histedit_nodes & strip_nodes
1501 if common_nodes:
1504 if common_nodes:
1502 raise error.Abort(_("histedit in progress, can't strip %s")
1505 raise error.Abort(_("histedit in progress, can't strip %s")
1503 % ', '.join(node.short(x) for x in common_nodes))
1506 % ', '.join(node.short(x) for x in common_nodes))
1504 return orig(ui, repo, nodelist, *args, **kwargs)
1507 return orig(ui, repo, nodelist, *args, **kwargs)
1505
1508
1506 extensions.wrapfunction(repair, 'strip', stripwrapper)
1509 extensions.wrapfunction(repair, 'strip', stripwrapper)
1507
1510
1508 def summaryhook(ui, repo):
1511 def summaryhook(ui, repo):
1509 if not os.path.exists(repo.join('histedit-state')):
1512 if not os.path.exists(repo.join('histedit-state')):
1510 return
1513 return
1511 state = histeditstate(repo)
1514 state = histeditstate(repo)
1512 state.read()
1515 state.read()
1513 if state.actions:
1516 if state.actions:
1514 # i18n: column positioning for "hg summary"
1517 # i18n: column positioning for "hg summary"
1515 ui.write(_('hist: %s (histedit --continue)\n') %
1518 ui.write(_('hist: %s (histedit --continue)\n') %
1516 (ui.label(_('%d remaining'), 'histedit.remaining') %
1519 (ui.label(_('%d remaining'), 'histedit.remaining') %
1517 len(state.actions)))
1520 len(state.actions)))
1518
1521
1519 def extsetup(ui):
1522 def extsetup(ui):
1520 cmdutil.summaryhooks.add('histedit', summaryhook)
1523 cmdutil.summaryhooks.add('histedit', summaryhook)
1521 cmdutil.unfinishedstates.append(
1524 cmdutil.unfinishedstates.append(
1522 ['histedit-state', False, True, _('histedit in progress'),
1525 ['histedit-state', False, True, _('histedit in progress'),
1523 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1526 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1524 cmdutil.afterresolvedstates.append(
1527 cmdutil.afterresolvedstates.append(
1525 ['histedit-state', _('hg histedit --continue')])
1528 ['histedit-state', _('hg histedit --continue')])
1526 if ui.configbool("experimental", "histeditng"):
1529 if ui.configbool("experimental", "histeditng"):
1527 globals()['base'] = action(['base', 'b'],
1530 globals()['base'] = action(['base', 'b'],
1528 _('checkout changeset and apply further changesets from there')
1531 _('checkout changeset and apply further changesets from there')
1529 )(base)
1532 )(base)
General Comments 0
You need to be logged in to leave comments. Login now