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