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