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