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