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