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