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