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