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