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