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