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