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