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