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