##// END OF EJS Templates
histedit: edit with custom filename...
Mykola Nikishov -
r27154:3bc7919f default
parent child Browse files
Show More
@@ -1,1367 +1,1367 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 Histedit rule lines are truncated to 80 characters by default. You
146 Histedit rule lines are truncated to 80 characters by default. You
147 can customize this behavior by setting a different length in your
147 can customize this behavior by setting a different length in your
148 configuration file::
148 configuration file::
149
149
150 [histedit]
150 [histedit]
151 linelen = 120 # truncate rule lines at 120 characters
151 linelen = 120 # truncate rule lines at 120 characters
152 """
152 """
153
153
154 try:
154 try:
155 import cPickle as pickle
155 import cPickle as pickle
156 pickle.dump # import now
156 pickle.dump # import now
157 except ImportError:
157 except ImportError:
158 import pickle
158 import pickle
159 import errno
159 import errno
160 import os
160 import os
161 import sys
161 import sys
162
162
163 from mercurial import bundle2
163 from mercurial import bundle2
164 from mercurial import cmdutil
164 from mercurial import cmdutil
165 from mercurial import discovery
165 from mercurial import discovery
166 from mercurial import error
166 from mercurial import error
167 from mercurial import copies
167 from mercurial import copies
168 from mercurial import context
168 from mercurial import context
169 from mercurial import exchange
169 from mercurial import exchange
170 from mercurial import extensions
170 from mercurial import extensions
171 from mercurial import hg
171 from mercurial import hg
172 from mercurial import node
172 from mercurial import node
173 from mercurial import repair
173 from mercurial import repair
174 from mercurial import scmutil
174 from mercurial import scmutil
175 from mercurial import util
175 from mercurial import util
176 from mercurial import obsolete
176 from mercurial import obsolete
177 from mercurial import merge as mergemod
177 from mercurial import merge as mergemod
178 from mercurial.lock import release
178 from mercurial.lock import release
179 from mercurial.i18n import _
179 from mercurial.i18n import _
180
180
181 cmdtable = {}
181 cmdtable = {}
182 command = cmdutil.command(cmdtable)
182 command = cmdutil.command(cmdtable)
183
183
184 class _constraints(object):
184 class _constraints(object):
185 # aborts if there are multiple rules for one node
185 # aborts if there are multiple rules for one node
186 noduplicates = 'noduplicates'
186 noduplicates = 'noduplicates'
187 # abort if the node does belong to edited stack
187 # abort if the node does belong to edited stack
188 forceother = 'forceother'
188 forceother = 'forceother'
189 # abort if the node doesn't belong to edited stack
189 # abort if the node doesn't belong to edited stack
190 noother = 'noother'
190 noother = 'noother'
191
191
192 @classmethod
192 @classmethod
193 def known(cls):
193 def known(cls):
194 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
194 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
195
195
196 # Note for extension authors: ONLY specify testedwith = 'internal' for
196 # Note for extension authors: ONLY specify testedwith = 'internal' for
197 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
197 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
198 # be specifying the version(s) of Mercurial they are tested with, or
198 # be specifying the version(s) of Mercurial they are tested with, or
199 # leave the attribute unspecified.
199 # leave the attribute unspecified.
200 testedwith = 'internal'
200 testedwith = 'internal'
201
201
202 # i18n: command names and abbreviations must remain untranslated
202 # i18n: command names and abbreviations must remain untranslated
203 editcomment = _("""# Edit history between %s and %s
203 editcomment = _("""# Edit history between %s and %s
204 #
204 #
205 # Commits are listed from least to most recent
205 # Commits are listed from least to most recent
206 #
206 #
207 # Commands:
207 # Commands:
208 # p, pick = use commit
208 # p, pick = use commit
209 # e, edit = use commit, but stop for amending
209 # e, edit = use commit, but stop for amending
210 # f, fold = use commit, but combine it with the one above
210 # f, fold = use commit, but combine it with the one above
211 # r, roll = like fold, but discard this commit's description
211 # r, roll = like fold, but discard this commit's description
212 # d, drop = remove commit from history
212 # d, drop = remove commit from history
213 # m, mess = edit commit message without changing commit content
213 # m, mess = edit commit message without changing commit content
214 #
214 #
215 """)
215 """)
216
216
217 class histeditstate(object):
217 class histeditstate(object):
218 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
218 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
219 topmost=None, replacements=None, lock=None, wlock=None):
219 topmost=None, replacements=None, lock=None, wlock=None):
220 self.repo = repo
220 self.repo = repo
221 self.rules = rules
221 self.rules = rules
222 self.keep = keep
222 self.keep = keep
223 self.topmost = topmost
223 self.topmost = topmost
224 self.parentctxnode = parentctxnode
224 self.parentctxnode = parentctxnode
225 self.lock = lock
225 self.lock = lock
226 self.wlock = wlock
226 self.wlock = wlock
227 self.backupfile = None
227 self.backupfile = None
228 if replacements is None:
228 if replacements is None:
229 self.replacements = []
229 self.replacements = []
230 else:
230 else:
231 self.replacements = replacements
231 self.replacements = replacements
232
232
233 def read(self):
233 def read(self):
234 """Load histedit state from disk and set fields appropriately."""
234 """Load histedit state from disk and set fields appropriately."""
235 try:
235 try:
236 fp = self.repo.vfs('histedit-state', 'r')
236 fp = self.repo.vfs('histedit-state', 'r')
237 except IOError as err:
237 except IOError as err:
238 if err.errno != errno.ENOENT:
238 if err.errno != errno.ENOENT:
239 raise
239 raise
240 raise error.Abort(_('no histedit in progress'))
240 raise error.Abort(_('no histedit in progress'))
241
241
242 try:
242 try:
243 data = pickle.load(fp)
243 data = pickle.load(fp)
244 parentctxnode, rules, keep, topmost, replacements = data
244 parentctxnode, rules, keep, topmost, replacements = data
245 backupfile = None
245 backupfile = None
246 except pickle.UnpicklingError:
246 except pickle.UnpicklingError:
247 data = self._load()
247 data = self._load()
248 parentctxnode, rules, keep, topmost, replacements, backupfile = data
248 parentctxnode, rules, keep, topmost, replacements, backupfile = data
249
249
250 self.parentctxnode = parentctxnode
250 self.parentctxnode = parentctxnode
251 self.rules = rules
251 self.rules = rules
252 self.keep = keep
252 self.keep = keep
253 self.topmost = topmost
253 self.topmost = topmost
254 self.replacements = replacements
254 self.replacements = replacements
255 self.backupfile = backupfile
255 self.backupfile = backupfile
256
256
257 def write(self):
257 def write(self):
258 fp = self.repo.vfs('histedit-state', 'w')
258 fp = self.repo.vfs('histedit-state', 'w')
259 fp.write('v1\n')
259 fp.write('v1\n')
260 fp.write('%s\n' % node.hex(self.parentctxnode))
260 fp.write('%s\n' % node.hex(self.parentctxnode))
261 fp.write('%s\n' % node.hex(self.topmost))
261 fp.write('%s\n' % node.hex(self.topmost))
262 fp.write('%s\n' % self.keep)
262 fp.write('%s\n' % self.keep)
263 fp.write('%d\n' % len(self.rules))
263 fp.write('%d\n' % len(self.rules))
264 for rule in self.rules:
264 for rule in self.rules:
265 fp.write('%s\n' % rule[0]) # action
265 fp.write('%s\n' % rule[0]) # action
266 fp.write('%s\n' % rule[1]) # remainder
266 fp.write('%s\n' % rule[1]) # remainder
267 fp.write('%d\n' % len(self.replacements))
267 fp.write('%d\n' % len(self.replacements))
268 for replacement in self.replacements:
268 for replacement in self.replacements:
269 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
269 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
270 for r in replacement[1])))
270 for r in replacement[1])))
271 backupfile = self.backupfile
271 backupfile = self.backupfile
272 if not backupfile:
272 if not backupfile:
273 backupfile = ''
273 backupfile = ''
274 fp.write('%s\n' % backupfile)
274 fp.write('%s\n' % backupfile)
275 fp.close()
275 fp.close()
276
276
277 def _load(self):
277 def _load(self):
278 fp = self.repo.vfs('histedit-state', 'r')
278 fp = self.repo.vfs('histedit-state', 'r')
279 lines = [l[:-1] for l in fp.readlines()]
279 lines = [l[:-1] for l in fp.readlines()]
280
280
281 index = 0
281 index = 0
282 lines[index] # version number
282 lines[index] # version number
283 index += 1
283 index += 1
284
284
285 parentctxnode = node.bin(lines[index])
285 parentctxnode = node.bin(lines[index])
286 index += 1
286 index += 1
287
287
288 topmost = node.bin(lines[index])
288 topmost = node.bin(lines[index])
289 index += 1
289 index += 1
290
290
291 keep = lines[index] == 'True'
291 keep = lines[index] == 'True'
292 index += 1
292 index += 1
293
293
294 # Rules
294 # Rules
295 rules = []
295 rules = []
296 rulelen = int(lines[index])
296 rulelen = int(lines[index])
297 index += 1
297 index += 1
298 for i in xrange(rulelen):
298 for i in xrange(rulelen):
299 ruleaction = lines[index]
299 ruleaction = lines[index]
300 index += 1
300 index += 1
301 rule = lines[index]
301 rule = lines[index]
302 index += 1
302 index += 1
303 rules.append((ruleaction, rule))
303 rules.append((ruleaction, rule))
304
304
305 # Replacements
305 # Replacements
306 replacements = []
306 replacements = []
307 replacementlen = int(lines[index])
307 replacementlen = int(lines[index])
308 index += 1
308 index += 1
309 for i in xrange(replacementlen):
309 for i in xrange(replacementlen):
310 replacement = lines[index]
310 replacement = lines[index]
311 original = node.bin(replacement[:40])
311 original = node.bin(replacement[:40])
312 succ = [node.bin(replacement[i:i + 40]) for i in
312 succ = [node.bin(replacement[i:i + 40]) for i in
313 range(40, len(replacement), 40)]
313 range(40, len(replacement), 40)]
314 replacements.append((original, succ))
314 replacements.append((original, succ))
315 index += 1
315 index += 1
316
316
317 backupfile = lines[index]
317 backupfile = lines[index]
318 index += 1
318 index += 1
319
319
320 fp.close()
320 fp.close()
321
321
322 return parentctxnode, rules, keep, topmost, replacements, backupfile
322 return parentctxnode, rules, keep, topmost, replacements, backupfile
323
323
324 def clear(self):
324 def clear(self):
325 if self.inprogress():
325 if self.inprogress():
326 self.repo.vfs.unlink('histedit-state')
326 self.repo.vfs.unlink('histedit-state')
327
327
328 def inprogress(self):
328 def inprogress(self):
329 return self.repo.vfs.exists('histedit-state')
329 return self.repo.vfs.exists('histedit-state')
330
330
331 class histeditaction(object):
331 class histeditaction(object):
332 def __init__(self, state, node):
332 def __init__(self, state, node):
333 self.state = state
333 self.state = state
334 self.repo = state.repo
334 self.repo = state.repo
335 self.node = node
335 self.node = node
336
336
337 @classmethod
337 @classmethod
338 def fromrule(cls, state, rule):
338 def fromrule(cls, state, rule):
339 """Parses the given rule, returning an instance of the histeditaction.
339 """Parses the given rule, returning an instance of the histeditaction.
340 """
340 """
341 repo = state.repo
341 repo = state.repo
342 rulehash = rule.strip().split(' ', 1)[0]
342 rulehash = rule.strip().split(' ', 1)[0]
343 try:
343 try:
344 node = repo[rulehash].node()
344 node = repo[rulehash].node()
345 except error.RepoError:
345 except error.RepoError:
346 raise error.Abort(_('unknown changeset %s listed') % rulehash[:12])
346 raise error.Abort(_('unknown changeset %s listed') % rulehash[:12])
347 return cls(state, node)
347 return cls(state, node)
348
348
349 def constraints(self):
349 def constraints(self):
350 """Return a set of constrains that this action should be verified for
350 """Return a set of constrains that this action should be verified for
351 """
351 """
352 return set([_constraints.noduplicates, _constraints.noother])
352 return set([_constraints.noduplicates, _constraints.noother])
353
353
354 def nodetoverify(self):
354 def nodetoverify(self):
355 """Returns a node associated with the action that will be used for
355 """Returns a node associated with the action that will be used for
356 verification purposes.
356 verification purposes.
357
357
358 If the action doesn't correspond to node it should return None
358 If the action doesn't correspond to node it should return None
359 """
359 """
360 return self.node
360 return self.node
361
361
362 def run(self):
362 def run(self):
363 """Runs the action. The default behavior is simply apply the action's
363 """Runs the action. The default behavior is simply apply the action's
364 rulectx onto the current parentctx."""
364 rulectx onto the current parentctx."""
365 self.applychange()
365 self.applychange()
366 self.continuedirty()
366 self.continuedirty()
367 return self.continueclean()
367 return self.continueclean()
368
368
369 def applychange(self):
369 def applychange(self):
370 """Applies the changes from this action's rulectx onto the current
370 """Applies the changes from this action's rulectx onto the current
371 parentctx, but does not commit them."""
371 parentctx, but does not commit them."""
372 repo = self.repo
372 repo = self.repo
373 rulectx = repo[self.node]
373 rulectx = repo[self.node]
374 hg.update(repo, self.state.parentctxnode)
374 hg.update(repo, self.state.parentctxnode)
375 stats = applychanges(repo.ui, repo, rulectx, {})
375 stats = applychanges(repo.ui, repo, rulectx, {})
376 if stats and stats[3] > 0:
376 if stats and stats[3] > 0:
377 raise error.InterventionRequired(_('Fix up the change and run '
377 raise error.InterventionRequired(_('Fix up the change and run '
378 'hg histedit --continue'))
378 'hg histedit --continue'))
379
379
380 def continuedirty(self):
380 def continuedirty(self):
381 """Continues the action when changes have been applied to the working
381 """Continues the action when changes have been applied to the working
382 copy. The default behavior is to commit the dirty changes."""
382 copy. The default behavior is to commit the dirty changes."""
383 repo = self.repo
383 repo = self.repo
384 rulectx = repo[self.node]
384 rulectx = repo[self.node]
385
385
386 editor = self.commiteditor()
386 editor = self.commiteditor()
387 commit = commitfuncfor(repo, rulectx)
387 commit = commitfuncfor(repo, rulectx)
388
388
389 commit(text=rulectx.description(), user=rulectx.user(),
389 commit(text=rulectx.description(), user=rulectx.user(),
390 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
390 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
391
391
392 def commiteditor(self):
392 def commiteditor(self):
393 """The editor to be used to edit the commit message."""
393 """The editor to be used to edit the commit message."""
394 return False
394 return False
395
395
396 def continueclean(self):
396 def continueclean(self):
397 """Continues the action when the working copy is clean. The default
397 """Continues the action when the working copy is clean. The default
398 behavior is to accept the current commit as the new version of the
398 behavior is to accept the current commit as the new version of the
399 rulectx."""
399 rulectx."""
400 ctx = self.repo['.']
400 ctx = self.repo['.']
401 if ctx.node() == self.state.parentctxnode:
401 if ctx.node() == self.state.parentctxnode:
402 self.repo.ui.warn(_('%s: empty changeset\n') %
402 self.repo.ui.warn(_('%s: empty changeset\n') %
403 node.short(self.node))
403 node.short(self.node))
404 return ctx, [(self.node, tuple())]
404 return ctx, [(self.node, tuple())]
405 if ctx.node() == self.node:
405 if ctx.node() == self.node:
406 # Nothing changed
406 # Nothing changed
407 return ctx, []
407 return ctx, []
408 return ctx, [(self.node, (ctx.node(),))]
408 return ctx, [(self.node, (ctx.node(),))]
409
409
410 def commitfuncfor(repo, src):
410 def commitfuncfor(repo, src):
411 """Build a commit function for the replacement of <src>
411 """Build a commit function for the replacement of <src>
412
412
413 This function ensure we apply the same treatment to all changesets.
413 This function ensure we apply the same treatment to all changesets.
414
414
415 - Add a 'histedit_source' entry in extra.
415 - Add a 'histedit_source' entry in extra.
416
416
417 Note that fold has its own separated logic because its handling is a bit
417 Note that fold has its own separated logic because its handling is a bit
418 different and not easily factored out of the fold method.
418 different and not easily factored out of the fold method.
419 """
419 """
420 phasemin = src.phase()
420 phasemin = src.phase()
421 def commitfunc(**kwargs):
421 def commitfunc(**kwargs):
422 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
422 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
423 try:
423 try:
424 repo.ui.setconfig('phases', 'new-commit', phasemin,
424 repo.ui.setconfig('phases', 'new-commit', phasemin,
425 'histedit')
425 'histedit')
426 extra = kwargs.get('extra', {}).copy()
426 extra = kwargs.get('extra', {}).copy()
427 extra['histedit_source'] = src.hex()
427 extra['histedit_source'] = src.hex()
428 kwargs['extra'] = extra
428 kwargs['extra'] = extra
429 return repo.commit(**kwargs)
429 return repo.commit(**kwargs)
430 finally:
430 finally:
431 repo.ui.restoreconfig(phasebackup)
431 repo.ui.restoreconfig(phasebackup)
432 return commitfunc
432 return commitfunc
433
433
434 def applychanges(ui, repo, ctx, opts):
434 def applychanges(ui, repo, ctx, opts):
435 """Merge changeset from ctx (only) in the current working directory"""
435 """Merge changeset from ctx (only) in the current working directory"""
436 wcpar = repo.dirstate.parents()[0]
436 wcpar = repo.dirstate.parents()[0]
437 if ctx.p1().node() == wcpar:
437 if ctx.p1().node() == wcpar:
438 # edits are "in place" we do not need to make any merge,
438 # edits are "in place" we do not need to make any merge,
439 # just applies changes on parent for edition
439 # just applies changes on parent for edition
440 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
440 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
441 stats = None
441 stats = None
442 else:
442 else:
443 try:
443 try:
444 # ui.forcemerge is an internal variable, do not document
444 # ui.forcemerge is an internal variable, do not document
445 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
445 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
446 'histedit')
446 'histedit')
447 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
447 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
448 finally:
448 finally:
449 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
449 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
450 return stats
450 return stats
451
451
452 def collapse(repo, first, last, commitopts, skipprompt=False):
452 def collapse(repo, first, last, commitopts, skipprompt=False):
453 """collapse the set of revisions from first to last as new one.
453 """collapse the set of revisions from first to last as new one.
454
454
455 Expected commit options are:
455 Expected commit options are:
456 - message
456 - message
457 - date
457 - date
458 - username
458 - username
459 Commit message is edited in all cases.
459 Commit message is edited in all cases.
460
460
461 This function works in memory."""
461 This function works in memory."""
462 ctxs = list(repo.set('%d::%d', first, last))
462 ctxs = list(repo.set('%d::%d', first, last))
463 if not ctxs:
463 if not ctxs:
464 return None
464 return None
465 for c in ctxs:
465 for c in ctxs:
466 if not c.mutable():
466 if not c.mutable():
467 raise error.Abort(
467 raise error.Abort(
468 _("cannot fold into public change %s") % node.short(c.node()))
468 _("cannot fold into public change %s") % node.short(c.node()))
469 base = first.parents()[0]
469 base = first.parents()[0]
470
470
471 # commit a new version of the old changeset, including the update
471 # commit a new version of the old changeset, including the update
472 # collect all files which might be affected
472 # collect all files which might be affected
473 files = set()
473 files = set()
474 for ctx in ctxs:
474 for ctx in ctxs:
475 files.update(ctx.files())
475 files.update(ctx.files())
476
476
477 # Recompute copies (avoid recording a -> b -> a)
477 # Recompute copies (avoid recording a -> b -> a)
478 copied = copies.pathcopies(base, last)
478 copied = copies.pathcopies(base, last)
479
479
480 # prune files which were reverted by the updates
480 # prune files which were reverted by the updates
481 def samefile(f):
481 def samefile(f):
482 if f in last.manifest():
482 if f in last.manifest():
483 a = last.filectx(f)
483 a = last.filectx(f)
484 if f in base.manifest():
484 if f in base.manifest():
485 b = base.filectx(f)
485 b = base.filectx(f)
486 return (a.data() == b.data()
486 return (a.data() == b.data()
487 and a.flags() == b.flags())
487 and a.flags() == b.flags())
488 else:
488 else:
489 return False
489 return False
490 else:
490 else:
491 return f not in base.manifest()
491 return f not in base.manifest()
492 files = [f for f in files if not samefile(f)]
492 files = [f for f in files if not samefile(f)]
493 # commit version of these files as defined by head
493 # commit version of these files as defined by head
494 headmf = last.manifest()
494 headmf = last.manifest()
495 def filectxfn(repo, ctx, path):
495 def filectxfn(repo, ctx, path):
496 if path in headmf:
496 if path in headmf:
497 fctx = last[path]
497 fctx = last[path]
498 flags = fctx.flags()
498 flags = fctx.flags()
499 mctx = context.memfilectx(repo,
499 mctx = context.memfilectx(repo,
500 fctx.path(), fctx.data(),
500 fctx.path(), fctx.data(),
501 islink='l' in flags,
501 islink='l' in flags,
502 isexec='x' in flags,
502 isexec='x' in flags,
503 copied=copied.get(path))
503 copied=copied.get(path))
504 return mctx
504 return mctx
505 return None
505 return None
506
506
507 if commitopts.get('message'):
507 if commitopts.get('message'):
508 message = commitopts['message']
508 message = commitopts['message']
509 else:
509 else:
510 message = first.description()
510 message = first.description()
511 user = commitopts.get('user')
511 user = commitopts.get('user')
512 date = commitopts.get('date')
512 date = commitopts.get('date')
513 extra = commitopts.get('extra')
513 extra = commitopts.get('extra')
514
514
515 parents = (first.p1().node(), first.p2().node())
515 parents = (first.p1().node(), first.p2().node())
516 editor = None
516 editor = None
517 if not skipprompt:
517 if not skipprompt:
518 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
518 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
519 new = context.memctx(repo,
519 new = context.memctx(repo,
520 parents=parents,
520 parents=parents,
521 text=message,
521 text=message,
522 files=files,
522 files=files,
523 filectxfn=filectxfn,
523 filectxfn=filectxfn,
524 user=user,
524 user=user,
525 date=date,
525 date=date,
526 extra=extra,
526 extra=extra,
527 editor=editor)
527 editor=editor)
528 return repo.commitctx(new)
528 return repo.commitctx(new)
529
529
530 def _isdirtywc(repo):
530 def _isdirtywc(repo):
531 return repo[None].dirty(missing=True)
531 return repo[None].dirty(missing=True)
532
532
533 def abortdirty():
533 def abortdirty():
534 raise error.Abort(_('working copy has pending changes'),
534 raise error.Abort(_('working copy has pending changes'),
535 hint=_('amend, commit, or revert them and run histedit '
535 hint=_('amend, commit, or revert them and run histedit '
536 '--continue, or abort with histedit --abort'))
536 '--continue, or abort with histedit --abort'))
537
537
538
538
539 class pick(histeditaction):
539 class pick(histeditaction):
540 def run(self):
540 def run(self):
541 rulectx = self.repo[self.node]
541 rulectx = self.repo[self.node]
542 if rulectx.parents()[0].node() == self.state.parentctxnode:
542 if rulectx.parents()[0].node() == self.state.parentctxnode:
543 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
543 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
544 return rulectx, []
544 return rulectx, []
545
545
546 return super(pick, self).run()
546 return super(pick, self).run()
547
547
548 class edit(histeditaction):
548 class edit(histeditaction):
549 def run(self):
549 def run(self):
550 repo = self.repo
550 repo = self.repo
551 rulectx = repo[self.node]
551 rulectx = repo[self.node]
552 hg.update(repo, self.state.parentctxnode)
552 hg.update(repo, self.state.parentctxnode)
553 applychanges(repo.ui, repo, rulectx, {})
553 applychanges(repo.ui, repo, rulectx, {})
554 raise error.InterventionRequired(
554 raise error.InterventionRequired(
555 _('Make changes as needed, you may commit or record as needed '
555 _('Make changes as needed, you may commit or record as needed '
556 'now.\nWhen you are finished, run hg histedit --continue to '
556 'now.\nWhen you are finished, run hg histedit --continue to '
557 'resume.'))
557 'resume.'))
558
558
559 def commiteditor(self):
559 def commiteditor(self):
560 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
560 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
561
561
562 class fold(histeditaction):
562 class fold(histeditaction):
563 def continuedirty(self):
563 def continuedirty(self):
564 repo = self.repo
564 repo = self.repo
565 rulectx = repo[self.node]
565 rulectx = repo[self.node]
566
566
567 commit = commitfuncfor(repo, rulectx)
567 commit = commitfuncfor(repo, rulectx)
568 commit(text='fold-temp-revision %s' % node.short(self.node),
568 commit(text='fold-temp-revision %s' % node.short(self.node),
569 user=rulectx.user(), date=rulectx.date(),
569 user=rulectx.user(), date=rulectx.date(),
570 extra=rulectx.extra())
570 extra=rulectx.extra())
571
571
572 def continueclean(self):
572 def continueclean(self):
573 repo = self.repo
573 repo = self.repo
574 ctx = repo['.']
574 ctx = repo['.']
575 rulectx = repo[self.node]
575 rulectx = repo[self.node]
576 parentctxnode = self.state.parentctxnode
576 parentctxnode = self.state.parentctxnode
577 if ctx.node() == parentctxnode:
577 if ctx.node() == parentctxnode:
578 repo.ui.warn(_('%s: empty changeset\n') %
578 repo.ui.warn(_('%s: empty changeset\n') %
579 node.short(self.node))
579 node.short(self.node))
580 return ctx, [(self.node, (parentctxnode,))]
580 return ctx, [(self.node, (parentctxnode,))]
581
581
582 parentctx = repo[parentctxnode]
582 parentctx = repo[parentctxnode]
583 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
583 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
584 parentctx))
584 parentctx))
585 if not newcommits:
585 if not newcommits:
586 repo.ui.warn(_('%s: cannot fold - working copy is not a '
586 repo.ui.warn(_('%s: cannot fold - working copy is not a '
587 'descendant of previous commit %s\n') %
587 'descendant of previous commit %s\n') %
588 (node.short(self.node), node.short(parentctxnode)))
588 (node.short(self.node), node.short(parentctxnode)))
589 return ctx, [(self.node, (ctx.node(),))]
589 return ctx, [(self.node, (ctx.node(),))]
590
590
591 middlecommits = newcommits.copy()
591 middlecommits = newcommits.copy()
592 middlecommits.discard(ctx.node())
592 middlecommits.discard(ctx.node())
593
593
594 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
594 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
595 middlecommits)
595 middlecommits)
596
596
597 def skipprompt(self):
597 def skipprompt(self):
598 """Returns true if the rule should skip the message editor.
598 """Returns true if the rule should skip the message editor.
599
599
600 For example, 'fold' wants to show an editor, but 'rollup'
600 For example, 'fold' wants to show an editor, but 'rollup'
601 doesn't want to.
601 doesn't want to.
602 """
602 """
603 return False
603 return False
604
604
605 def mergedescs(self):
605 def mergedescs(self):
606 """Returns true if the rule should merge messages of multiple changes.
606 """Returns true if the rule should merge messages of multiple changes.
607
607
608 This exists mainly so that 'rollup' rules can be a subclass of
608 This exists mainly so that 'rollup' rules can be a subclass of
609 'fold'.
609 'fold'.
610 """
610 """
611 return True
611 return True
612
612
613 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
613 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
614 parent = ctx.parents()[0].node()
614 parent = ctx.parents()[0].node()
615 hg.update(repo, parent)
615 hg.update(repo, parent)
616 ### prepare new commit data
616 ### prepare new commit data
617 commitopts = {}
617 commitopts = {}
618 commitopts['user'] = ctx.user()
618 commitopts['user'] = ctx.user()
619 # commit message
619 # commit message
620 if not self.mergedescs():
620 if not self.mergedescs():
621 newmessage = ctx.description()
621 newmessage = ctx.description()
622 else:
622 else:
623 newmessage = '\n***\n'.join(
623 newmessage = '\n***\n'.join(
624 [ctx.description()] +
624 [ctx.description()] +
625 [repo[r].description() for r in internalchanges] +
625 [repo[r].description() for r in internalchanges] +
626 [oldctx.description()]) + '\n'
626 [oldctx.description()]) + '\n'
627 commitopts['message'] = newmessage
627 commitopts['message'] = newmessage
628 # date
628 # date
629 commitopts['date'] = max(ctx.date(), oldctx.date())
629 commitopts['date'] = max(ctx.date(), oldctx.date())
630 extra = ctx.extra().copy()
630 extra = ctx.extra().copy()
631 # histedit_source
631 # histedit_source
632 # note: ctx is likely a temporary commit but that the best we can do
632 # note: ctx is likely a temporary commit but that the best we can do
633 # here. This is sufficient to solve issue3681 anyway.
633 # here. This is sufficient to solve issue3681 anyway.
634 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
634 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
635 commitopts['extra'] = extra
635 commitopts['extra'] = extra
636 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
636 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
637 try:
637 try:
638 phasemin = max(ctx.phase(), oldctx.phase())
638 phasemin = max(ctx.phase(), oldctx.phase())
639 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
639 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
640 n = collapse(repo, ctx, repo[newnode], commitopts,
640 n = collapse(repo, ctx, repo[newnode], commitopts,
641 skipprompt=self.skipprompt())
641 skipprompt=self.skipprompt())
642 finally:
642 finally:
643 repo.ui.restoreconfig(phasebackup)
643 repo.ui.restoreconfig(phasebackup)
644 if n is None:
644 if n is None:
645 return ctx, []
645 return ctx, []
646 hg.update(repo, n)
646 hg.update(repo, n)
647 replacements = [(oldctx.node(), (newnode,)),
647 replacements = [(oldctx.node(), (newnode,)),
648 (ctx.node(), (n,)),
648 (ctx.node(), (n,)),
649 (newnode, (n,)),
649 (newnode, (n,)),
650 ]
650 ]
651 for ich in internalchanges:
651 for ich in internalchanges:
652 replacements.append((ich, (n,)))
652 replacements.append((ich, (n,)))
653 return repo[n], replacements
653 return repo[n], replacements
654
654
655 class base(histeditaction):
655 class base(histeditaction):
656 def constraints(self):
656 def constraints(self):
657 return set([_constraints.forceother])
657 return set([_constraints.forceother])
658
658
659 def run(self):
659 def run(self):
660 if self.repo['.'].node() != self.node:
660 if self.repo['.'].node() != self.node:
661 mergemod.update(self.repo, self.node, False, True, False)
661 mergemod.update(self.repo, self.node, False, True, False)
662 # branchmerge, force, partial)
662 # branchmerge, force, partial)
663 return self.continueclean()
663 return self.continueclean()
664
664
665 def continuedirty(self):
665 def continuedirty(self):
666 abortdirty()
666 abortdirty()
667
667
668 def continueclean(self):
668 def continueclean(self):
669 basectx = self.repo['.']
669 basectx = self.repo['.']
670 return basectx, []
670 return basectx, []
671
671
672 class _multifold(fold):
672 class _multifold(fold):
673 """fold subclass used for when multiple folds happen in a row
673 """fold subclass used for when multiple folds happen in a row
674
674
675 We only want to fire the editor for the folded message once when
675 We only want to fire the editor for the folded message once when
676 (say) four changes are folded down into a single change. This is
676 (say) four changes are folded down into a single change. This is
677 similar to rollup, but we should preserve both messages so that
677 similar to rollup, but we should preserve both messages so that
678 when the last fold operation runs we can show the user all the
678 when the last fold operation runs we can show the user all the
679 commit messages in their editor.
679 commit messages in their editor.
680 """
680 """
681 def skipprompt(self):
681 def skipprompt(self):
682 return True
682 return True
683
683
684 class rollup(fold):
684 class rollup(fold):
685 def mergedescs(self):
685 def mergedescs(self):
686 return False
686 return False
687
687
688 def skipprompt(self):
688 def skipprompt(self):
689 return True
689 return True
690
690
691 class drop(histeditaction):
691 class drop(histeditaction):
692 def run(self):
692 def run(self):
693 parentctx = self.repo[self.state.parentctxnode]
693 parentctx = self.repo[self.state.parentctxnode]
694 return parentctx, [(self.node, tuple())]
694 return parentctx, [(self.node, tuple())]
695
695
696 class message(histeditaction):
696 class message(histeditaction):
697 def commiteditor(self):
697 def commiteditor(self):
698 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
698 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
699
699
700 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
700 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
701 """utility function to find the first outgoing changeset
701 """utility function to find the first outgoing changeset
702
702
703 Used by initialization code"""
703 Used by initialization code"""
704 if opts is None:
704 if opts is None:
705 opts = {}
705 opts = {}
706 dest = ui.expandpath(remote or 'default-push', remote or 'default')
706 dest = ui.expandpath(remote or 'default-push', remote or 'default')
707 dest, revs = hg.parseurl(dest, None)[:2]
707 dest, revs = hg.parseurl(dest, None)[:2]
708 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
708 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
709
709
710 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
710 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
711 other = hg.peer(repo, opts, dest)
711 other = hg.peer(repo, opts, dest)
712
712
713 if revs:
713 if revs:
714 revs = [repo.lookup(rev) for rev in revs]
714 revs = [repo.lookup(rev) for rev in revs]
715
715
716 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
716 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
717 if not outgoing.missing:
717 if not outgoing.missing:
718 raise error.Abort(_('no outgoing ancestors'))
718 raise error.Abort(_('no outgoing ancestors'))
719 roots = list(repo.revs("roots(%ln)", outgoing.missing))
719 roots = list(repo.revs("roots(%ln)", outgoing.missing))
720 if 1 < len(roots):
720 if 1 < len(roots):
721 msg = _('there are ambiguous outgoing revisions')
721 msg = _('there are ambiguous outgoing revisions')
722 hint = _('see "hg help histedit" for more detail')
722 hint = _('see "hg help histedit" for more detail')
723 raise error.Abort(msg, hint=hint)
723 raise error.Abort(msg, hint=hint)
724 return repo.lookup(roots[0])
724 return repo.lookup(roots[0])
725
725
726 actiontable = {'p': pick,
726 actiontable = {'p': pick,
727 'pick': pick,
727 'pick': pick,
728 'e': edit,
728 'e': edit,
729 'edit': edit,
729 'edit': edit,
730 'f': fold,
730 'f': fold,
731 'fold': fold,
731 'fold': fold,
732 '_multifold': _multifold,
732 '_multifold': _multifold,
733 'r': rollup,
733 'r': rollup,
734 'roll': rollup,
734 'roll': rollup,
735 'd': drop,
735 'd': drop,
736 'drop': drop,
736 'drop': drop,
737 'm': message,
737 'm': message,
738 'mess': message,
738 'mess': message,
739 }
739 }
740
740
741 @command('histedit',
741 @command('histedit',
742 [('', 'commands', '',
742 [('', 'commands', '',
743 _('read history edits from the specified file'), _('FILE')),
743 _('read history edits from the specified file'), _('FILE')),
744 ('c', 'continue', False, _('continue an edit already in progress')),
744 ('c', 'continue', False, _('continue an edit already in progress')),
745 ('', 'edit-plan', False, _('edit remaining actions list')),
745 ('', 'edit-plan', False, _('edit remaining actions list')),
746 ('k', 'keep', False,
746 ('k', 'keep', False,
747 _("don't strip old nodes after edit is complete")),
747 _("don't strip old nodes after edit is complete")),
748 ('', 'abort', False, _('abort an edit in progress')),
748 ('', 'abort', False, _('abort an edit in progress')),
749 ('o', 'outgoing', False, _('changesets not found in destination')),
749 ('o', 'outgoing', False, _('changesets not found in destination')),
750 ('f', 'force', False,
750 ('f', 'force', False,
751 _('force outgoing even for unrelated repositories')),
751 _('force outgoing even for unrelated repositories')),
752 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
752 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
753 _("ANCESTOR | --outgoing [URL]"))
753 _("ANCESTOR | --outgoing [URL]"))
754 def histedit(ui, repo, *freeargs, **opts):
754 def histedit(ui, repo, *freeargs, **opts):
755 """interactively edit changeset history
755 """interactively edit changeset history
756
756
757 This command edits changesets between ANCESTOR and the parent of
757 This command edits changesets between ANCESTOR and the parent of
758 the working directory.
758 the working directory.
759
759
760 With --outgoing, this edits changesets not found in the
760 With --outgoing, this edits changesets not found in the
761 destination repository. If URL of the destination is omitted, the
761 destination repository. If URL of the destination is omitted, the
762 'default-push' (or 'default') path will be used.
762 'default-push' (or 'default') path will be used.
763
763
764 For safety, this command is also aborted if there are ambiguous
764 For safety, this command is also aborted if there are ambiguous
765 outgoing revisions which may confuse users: for example, if there
765 outgoing revisions which may confuse users: for example, if there
766 are multiple branches containing outgoing revisions.
766 are multiple branches containing outgoing revisions.
767
767
768 Use "min(outgoing() and ::.)" or similar revset specification
768 Use "min(outgoing() and ::.)" or similar revset specification
769 instead of --outgoing to specify edit target revision exactly in
769 instead of --outgoing to specify edit target revision exactly in
770 such ambiguous situation. See :hg:`help revsets` for detail about
770 such ambiguous situation. See :hg:`help revsets` for detail about
771 selecting revisions.
771 selecting revisions.
772
772
773 .. container:: verbose
773 .. container:: verbose
774
774
775 Examples:
775 Examples:
776
776
777 - A number of changes have been made.
777 - A number of changes have been made.
778 Revision 3 is no longer needed.
778 Revision 3 is no longer needed.
779
779
780 Start history editing from revision 3::
780 Start history editing from revision 3::
781
781
782 hg histedit -r 3
782 hg histedit -r 3
783
783
784 An editor opens, containing the list of revisions,
784 An editor opens, containing the list of revisions,
785 with specific actions specified::
785 with specific actions specified::
786
786
787 pick 5339bf82f0ca 3 Zworgle the foobar
787 pick 5339bf82f0ca 3 Zworgle the foobar
788 pick 8ef592ce7cc4 4 Bedazzle the zerlog
788 pick 8ef592ce7cc4 4 Bedazzle the zerlog
789 pick 0a9639fcda9d 5 Morgify the cromulancy
789 pick 0a9639fcda9d 5 Morgify the cromulancy
790
790
791 Additional information about the possible actions
791 Additional information about the possible actions
792 to take appears below the list of revisions.
792 to take appears below the list of revisions.
793
793
794 To remove revision 3 from the history,
794 To remove revision 3 from the history,
795 its action (at the beginning of the relevant line)
795 its action (at the beginning of the relevant line)
796 is changed to 'drop'::
796 is changed to 'drop'::
797
797
798 drop 5339bf82f0ca 3 Zworgle the foobar
798 drop 5339bf82f0ca 3 Zworgle the foobar
799 pick 8ef592ce7cc4 4 Bedazzle the zerlog
799 pick 8ef592ce7cc4 4 Bedazzle the zerlog
800 pick 0a9639fcda9d 5 Morgify the cromulancy
800 pick 0a9639fcda9d 5 Morgify the cromulancy
801
801
802 - A number of changes have been made.
802 - A number of changes have been made.
803 Revision 2 and 4 need to be swapped.
803 Revision 2 and 4 need to be swapped.
804
804
805 Start history editing from revision 2::
805 Start history editing from revision 2::
806
806
807 hg histedit -r 2
807 hg histedit -r 2
808
808
809 An editor opens, containing the list of revisions,
809 An editor opens, containing the list of revisions,
810 with specific actions specified::
810 with specific actions specified::
811
811
812 pick 252a1af424ad 2 Blorb a morgwazzle
812 pick 252a1af424ad 2 Blorb a morgwazzle
813 pick 5339bf82f0ca 3 Zworgle the foobar
813 pick 5339bf82f0ca 3 Zworgle the foobar
814 pick 8ef592ce7cc4 4 Bedazzle the zerlog
814 pick 8ef592ce7cc4 4 Bedazzle the zerlog
815
815
816 To swap revision 2 and 4, its lines are swapped
816 To swap revision 2 and 4, its lines are swapped
817 in the editor::
817 in the editor::
818
818
819 pick 8ef592ce7cc4 4 Bedazzle the zerlog
819 pick 8ef592ce7cc4 4 Bedazzle the zerlog
820 pick 5339bf82f0ca 3 Zworgle the foobar
820 pick 5339bf82f0ca 3 Zworgle the foobar
821 pick 252a1af424ad 2 Blorb a morgwazzle
821 pick 252a1af424ad 2 Blorb a morgwazzle
822
822
823 Returns 0 on success, 1 if user intervention is required (not only
823 Returns 0 on success, 1 if user intervention is required (not only
824 for intentional "edit" command, but also for resolving unexpected
824 for intentional "edit" command, but also for resolving unexpected
825 conflicts).
825 conflicts).
826 """
826 """
827 state = histeditstate(repo)
827 state = histeditstate(repo)
828 try:
828 try:
829 state.wlock = repo.wlock()
829 state.wlock = repo.wlock()
830 state.lock = repo.lock()
830 state.lock = repo.lock()
831 _histedit(ui, repo, state, *freeargs, **opts)
831 _histedit(ui, repo, state, *freeargs, **opts)
832 finally:
832 finally:
833 release(state.lock, state.wlock)
833 release(state.lock, state.wlock)
834
834
835 def _histedit(ui, repo, state, *freeargs, **opts):
835 def _histedit(ui, repo, state, *freeargs, **opts):
836 # TODO only abort if we try and histedit mq patches, not just
836 # TODO only abort if we try and histedit mq patches, not just
837 # blanket if mq patches are applied somewhere
837 # blanket if mq patches are applied somewhere
838 mq = getattr(repo, 'mq', None)
838 mq = getattr(repo, 'mq', None)
839 if mq and mq.applied:
839 if mq and mq.applied:
840 raise error.Abort(_('source has mq patches applied'))
840 raise error.Abort(_('source has mq patches applied'))
841
841
842 # basic argument incompatibility processing
842 # basic argument incompatibility processing
843 outg = opts.get('outgoing')
843 outg = opts.get('outgoing')
844 cont = opts.get('continue')
844 cont = opts.get('continue')
845 editplan = opts.get('edit_plan')
845 editplan = opts.get('edit_plan')
846 abort = opts.get('abort')
846 abort = opts.get('abort')
847 force = opts.get('force')
847 force = opts.get('force')
848 rules = opts.get('commands', '')
848 rules = opts.get('commands', '')
849 revs = opts.get('rev', [])
849 revs = opts.get('rev', [])
850 goal = 'new' # This invocation goal, in new, continue, abort
850 goal = 'new' # This invocation goal, in new, continue, abort
851 if force and not outg:
851 if force and not outg:
852 raise error.Abort(_('--force only allowed with --outgoing'))
852 raise error.Abort(_('--force only allowed with --outgoing'))
853 if cont:
853 if cont:
854 if any((outg, abort, revs, freeargs, rules, editplan)):
854 if any((outg, abort, revs, freeargs, rules, editplan)):
855 raise error.Abort(_('no arguments allowed with --continue'))
855 raise error.Abort(_('no arguments allowed with --continue'))
856 goal = 'continue'
856 goal = 'continue'
857 elif abort:
857 elif abort:
858 if any((outg, revs, freeargs, rules, editplan)):
858 if any((outg, revs, freeargs, rules, editplan)):
859 raise error.Abort(_('no arguments allowed with --abort'))
859 raise error.Abort(_('no arguments allowed with --abort'))
860 goal = 'abort'
860 goal = 'abort'
861 elif editplan:
861 elif editplan:
862 if any((outg, revs, freeargs)):
862 if any((outg, revs, freeargs)):
863 raise error.Abort(_('only --commands argument allowed with '
863 raise error.Abort(_('only --commands argument allowed with '
864 '--edit-plan'))
864 '--edit-plan'))
865 goal = 'edit-plan'
865 goal = 'edit-plan'
866 else:
866 else:
867 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
867 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
868 raise error.Abort(_('history edit already in progress, try '
868 raise error.Abort(_('history edit already in progress, try '
869 '--continue or --abort'))
869 '--continue or --abort'))
870 if outg:
870 if outg:
871 if revs:
871 if revs:
872 raise error.Abort(_('no revisions allowed with --outgoing'))
872 raise error.Abort(_('no revisions allowed with --outgoing'))
873 if len(freeargs) > 1:
873 if len(freeargs) > 1:
874 raise error.Abort(
874 raise error.Abort(
875 _('only one repo argument allowed with --outgoing'))
875 _('only one repo argument allowed with --outgoing'))
876 else:
876 else:
877 revs.extend(freeargs)
877 revs.extend(freeargs)
878 if len(revs) == 0:
878 if len(revs) == 0:
879 # experimental config: histedit.defaultrev
879 # experimental config: histedit.defaultrev
880 histeditdefault = ui.config('histedit', 'defaultrev')
880 histeditdefault = ui.config('histedit', 'defaultrev')
881 if histeditdefault:
881 if histeditdefault:
882 revs.append(histeditdefault)
882 revs.append(histeditdefault)
883 if len(revs) != 1:
883 if len(revs) != 1:
884 raise error.Abort(
884 raise error.Abort(
885 _('histedit requires exactly one ancestor revision'))
885 _('histedit requires exactly one ancestor revision'))
886
886
887
887
888 replacements = []
888 replacements = []
889 state.keep = opts.get('keep', False)
889 state.keep = opts.get('keep', False)
890 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
890 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
891
891
892 # rebuild state
892 # rebuild state
893 if goal == 'continue':
893 if goal == 'continue':
894 state.read()
894 state.read()
895 state = bootstrapcontinue(ui, state, opts)
895 state = bootstrapcontinue(ui, state, opts)
896 elif goal == 'edit-plan':
896 elif goal == 'edit-plan':
897 state.read()
897 state.read()
898 if not rules:
898 if not rules:
899 comment = editcomment % (node.short(state.parentctxnode),
899 comment = editcomment % (node.short(state.parentctxnode),
900 node.short(state.topmost))
900 node.short(state.topmost))
901 rules = ruleeditor(repo, ui, state.rules, comment)
901 rules = ruleeditor(repo, ui, state.rules, comment)
902 else:
902 else:
903 if rules == '-':
903 if rules == '-':
904 f = sys.stdin
904 f = sys.stdin
905 else:
905 else:
906 f = open(rules)
906 f = open(rules)
907 rules = f.read()
907 rules = f.read()
908 f.close()
908 f.close()
909 rules = [l for l in (r.strip() for r in rules.splitlines())
909 rules = [l for l in (r.strip() for r in rules.splitlines())
910 if l and not l.startswith('#')]
910 if l and not l.startswith('#')]
911 rules = verifyrules(rules, state, [repo[c] for [_a, c] in state.rules])
911 rules = verifyrules(rules, state, [repo[c] for [_a, c] in state.rules])
912 state.rules = rules
912 state.rules = rules
913 state.write()
913 state.write()
914 return
914 return
915 elif goal == 'abort':
915 elif goal == 'abort':
916 try:
916 try:
917 state.read()
917 state.read()
918 tmpnodes, leafs = newnodestoabort(state)
918 tmpnodes, leafs = newnodestoabort(state)
919 ui.debug('restore wc to old parent %s\n'
919 ui.debug('restore wc to old parent %s\n'
920 % node.short(state.topmost))
920 % node.short(state.topmost))
921
921
922 # Recover our old commits if necessary
922 # Recover our old commits if necessary
923 if not state.topmost in repo and state.backupfile:
923 if not state.topmost in repo and state.backupfile:
924 backupfile = repo.join(state.backupfile)
924 backupfile = repo.join(state.backupfile)
925 f = hg.openpath(ui, backupfile)
925 f = hg.openpath(ui, backupfile)
926 gen = exchange.readbundle(ui, f, backupfile)
926 gen = exchange.readbundle(ui, f, backupfile)
927 tr = repo.transaction('histedit.abort')
927 tr = repo.transaction('histedit.abort')
928 try:
928 try:
929 if not isinstance(gen, bundle2.unbundle20):
929 if not isinstance(gen, bundle2.unbundle20):
930 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
930 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
931 if isinstance(gen, bundle2.unbundle20):
931 if isinstance(gen, bundle2.unbundle20):
932 bundle2.applybundle(repo, gen, tr,
932 bundle2.applybundle(repo, gen, tr,
933 source='histedit',
933 source='histedit',
934 url='bundle:' + backupfile)
934 url='bundle:' + backupfile)
935 tr.close()
935 tr.close()
936 finally:
936 finally:
937 tr.release()
937 tr.release()
938
938
939 os.remove(backupfile)
939 os.remove(backupfile)
940
940
941 # check whether we should update away
941 # check whether we should update away
942 if repo.unfiltered().revs('parents() and (%n or %ln::)',
942 if repo.unfiltered().revs('parents() and (%n or %ln::)',
943 state.parentctxnode, leafs | tmpnodes):
943 state.parentctxnode, leafs | tmpnodes):
944 hg.clean(repo, state.topmost)
944 hg.clean(repo, state.topmost)
945 cleanupnode(ui, repo, 'created', tmpnodes)
945 cleanupnode(ui, repo, 'created', tmpnodes)
946 cleanupnode(ui, repo, 'temp', leafs)
946 cleanupnode(ui, repo, 'temp', leafs)
947 except Exception:
947 except Exception:
948 if state.inprogress():
948 if state.inprogress():
949 ui.warn(_('warning: encountered an exception during histedit '
949 ui.warn(_('warning: encountered an exception during histedit '
950 '--abort; the repository may not have been completely '
950 '--abort; the repository may not have been completely '
951 'cleaned up\n'))
951 'cleaned up\n'))
952 raise
952 raise
953 finally:
953 finally:
954 state.clear()
954 state.clear()
955 return
955 return
956 else:
956 else:
957 cmdutil.checkunfinished(repo)
957 cmdutil.checkunfinished(repo)
958 cmdutil.bailifchanged(repo)
958 cmdutil.bailifchanged(repo)
959
959
960 topmost, empty = repo.dirstate.parents()
960 topmost, empty = repo.dirstate.parents()
961 if outg:
961 if outg:
962 if freeargs:
962 if freeargs:
963 remote = freeargs[0]
963 remote = freeargs[0]
964 else:
964 else:
965 remote = None
965 remote = None
966 root = findoutgoing(ui, repo, remote, force, opts)
966 root = findoutgoing(ui, repo, remote, force, opts)
967 else:
967 else:
968 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
968 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
969 if len(rr) != 1:
969 if len(rr) != 1:
970 raise error.Abort(_('The specified revisions must have '
970 raise error.Abort(_('The specified revisions must have '
971 'exactly one common root'))
971 'exactly one common root'))
972 root = rr[0].node()
972 root = rr[0].node()
973
973
974 revs = between(repo, root, topmost, state.keep)
974 revs = between(repo, root, topmost, state.keep)
975 if not revs:
975 if not revs:
976 raise error.Abort(_('%s is not an ancestor of working directory') %
976 raise error.Abort(_('%s is not an ancestor of working directory') %
977 node.short(root))
977 node.short(root))
978
978
979 ctxs = [repo[r] for r in revs]
979 ctxs = [repo[r] for r in revs]
980 if not rules:
980 if not rules:
981 comment = editcomment % (node.short(root), node.short(topmost))
981 comment = editcomment % (node.short(root), node.short(topmost))
982 rules = ruleeditor(repo, ui, [['pick', c] for c in ctxs], comment)
982 rules = ruleeditor(repo, ui, [['pick', c] for c in ctxs], comment)
983 else:
983 else:
984 if rules == '-':
984 if rules == '-':
985 f = sys.stdin
985 f = sys.stdin
986 else:
986 else:
987 f = open(rules)
987 f = open(rules)
988 rules = f.read()
988 rules = f.read()
989 f.close()
989 f.close()
990 rules = [l for l in (r.strip() for r in rules.splitlines())
990 rules = [l for l in (r.strip() for r in rules.splitlines())
991 if l and not l.startswith('#')]
991 if l and not l.startswith('#')]
992 rules = verifyrules(rules, state, ctxs)
992 rules = verifyrules(rules, state, ctxs)
993
993
994 parentctxnode = repo[root].parents()[0].node()
994 parentctxnode = repo[root].parents()[0].node()
995
995
996 state.parentctxnode = parentctxnode
996 state.parentctxnode = parentctxnode
997 state.rules = rules
997 state.rules = rules
998 state.topmost = topmost
998 state.topmost = topmost
999 state.replacements = replacements
999 state.replacements = replacements
1000
1000
1001 # Create a backup so we can always abort completely.
1001 # Create a backup so we can always abort completely.
1002 backupfile = None
1002 backupfile = None
1003 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1003 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1004 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1004 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1005 'histedit')
1005 'histedit')
1006 state.backupfile = backupfile
1006 state.backupfile = backupfile
1007
1007
1008 # preprocess rules so that we can hide inner folds from the user
1008 # preprocess rules so that we can hide inner folds from the user
1009 # and only show one editor
1009 # and only show one editor
1010 rules = state.rules[:]
1010 rules = state.rules[:]
1011 for idx, ((action, ha), (nextact, unused)) in enumerate(
1011 for idx, ((action, ha), (nextact, unused)) in enumerate(
1012 zip(rules, rules[1:] + [(None, None)])):
1012 zip(rules, rules[1:] + [(None, None)])):
1013 if action == 'fold' and nextact == 'fold':
1013 if action == 'fold' and nextact == 'fold':
1014 state.rules[idx] = '_multifold', ha
1014 state.rules[idx] = '_multifold', ha
1015
1015
1016 while state.rules:
1016 while state.rules:
1017 state.write()
1017 state.write()
1018 action, ha = state.rules.pop(0)
1018 action, ha = state.rules.pop(0)
1019 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
1019 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
1020 actobj = actiontable[action].fromrule(state, ha)
1020 actobj = actiontable[action].fromrule(state, ha)
1021 parentctx, replacement_ = actobj.run()
1021 parentctx, replacement_ = actobj.run()
1022 state.parentctxnode = parentctx.node()
1022 state.parentctxnode = parentctx.node()
1023 state.replacements.extend(replacement_)
1023 state.replacements.extend(replacement_)
1024 state.write()
1024 state.write()
1025
1025
1026 hg.update(repo, state.parentctxnode)
1026 hg.update(repo, state.parentctxnode)
1027
1027
1028 mapping, tmpnodes, created, ntm = processreplacement(state)
1028 mapping, tmpnodes, created, ntm = processreplacement(state)
1029 if mapping:
1029 if mapping:
1030 for prec, succs in mapping.iteritems():
1030 for prec, succs in mapping.iteritems():
1031 if not succs:
1031 if not succs:
1032 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1032 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1033 else:
1033 else:
1034 ui.debug('histedit: %s is replaced by %s\n' % (
1034 ui.debug('histedit: %s is replaced by %s\n' % (
1035 node.short(prec), node.short(succs[0])))
1035 node.short(prec), node.short(succs[0])))
1036 if len(succs) > 1:
1036 if len(succs) > 1:
1037 m = 'histedit: %s'
1037 m = 'histedit: %s'
1038 for n in succs[1:]:
1038 for n in succs[1:]:
1039 ui.debug(m % node.short(n))
1039 ui.debug(m % node.short(n))
1040
1040
1041 if supportsmarkers:
1041 if supportsmarkers:
1042 # Only create markers if the temp nodes weren't already removed.
1042 # Only create markers if the temp nodes weren't already removed.
1043 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1043 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1044 if t in repo))
1044 if t in repo))
1045 else:
1045 else:
1046 cleanupnode(ui, repo, 'temp', tmpnodes)
1046 cleanupnode(ui, repo, 'temp', tmpnodes)
1047
1047
1048 if not state.keep:
1048 if not state.keep:
1049 if mapping:
1049 if mapping:
1050 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1050 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1051 # TODO update mq state
1051 # TODO update mq state
1052 if supportsmarkers:
1052 if supportsmarkers:
1053 markers = []
1053 markers = []
1054 # sort by revision number because it sound "right"
1054 # sort by revision number because it sound "right"
1055 for prec in sorted(mapping, key=repo.changelog.rev):
1055 for prec in sorted(mapping, key=repo.changelog.rev):
1056 succs = mapping[prec]
1056 succs = mapping[prec]
1057 markers.append((repo[prec],
1057 markers.append((repo[prec],
1058 tuple(repo[s] for s in succs)))
1058 tuple(repo[s] for s in succs)))
1059 if markers:
1059 if markers:
1060 obsolete.createmarkers(repo, markers)
1060 obsolete.createmarkers(repo, markers)
1061 else:
1061 else:
1062 cleanupnode(ui, repo, 'replaced', mapping)
1062 cleanupnode(ui, repo, 'replaced', mapping)
1063
1063
1064 state.clear()
1064 state.clear()
1065 if os.path.exists(repo.sjoin('undo')):
1065 if os.path.exists(repo.sjoin('undo')):
1066 os.unlink(repo.sjoin('undo'))
1066 os.unlink(repo.sjoin('undo'))
1067
1067
1068 def bootstrapcontinue(ui, state, opts):
1068 def bootstrapcontinue(ui, state, opts):
1069 repo = state.repo
1069 repo = state.repo
1070 if state.rules:
1070 if state.rules:
1071 action, currentnode = state.rules.pop(0)
1071 action, currentnode = state.rules.pop(0)
1072
1072
1073 actobj = actiontable[action].fromrule(state, currentnode)
1073 actobj = actiontable[action].fromrule(state, currentnode)
1074
1074
1075 if _isdirtywc(repo):
1075 if _isdirtywc(repo):
1076 actobj.continuedirty()
1076 actobj.continuedirty()
1077 if _isdirtywc(repo):
1077 if _isdirtywc(repo):
1078 abortdirty()
1078 abortdirty()
1079
1079
1080 parentctx, replacements = actobj.continueclean()
1080 parentctx, replacements = actobj.continueclean()
1081
1081
1082 state.parentctxnode = parentctx.node()
1082 state.parentctxnode = parentctx.node()
1083 state.replacements.extend(replacements)
1083 state.replacements.extend(replacements)
1084
1084
1085 return state
1085 return state
1086
1086
1087 def between(repo, old, new, keep):
1087 def between(repo, old, new, keep):
1088 """select and validate the set of revision to edit
1088 """select and validate the set of revision to edit
1089
1089
1090 When keep is false, the specified set can't have children."""
1090 When keep is false, the specified set can't have children."""
1091 ctxs = list(repo.set('%n::%n', old, new))
1091 ctxs = list(repo.set('%n::%n', old, new))
1092 if ctxs and not keep:
1092 if ctxs and not keep:
1093 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1093 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1094 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1094 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1095 raise error.Abort(_('cannot edit history that would orphan nodes'))
1095 raise error.Abort(_('cannot edit history that would orphan nodes'))
1096 if repo.revs('(%ld) and merge()', ctxs):
1096 if repo.revs('(%ld) and merge()', ctxs):
1097 raise error.Abort(_('cannot edit history that contains merges'))
1097 raise error.Abort(_('cannot edit history that contains merges'))
1098 root = ctxs[0] # list is already sorted by repo.set
1098 root = ctxs[0] # list is already sorted by repo.set
1099 if not root.mutable():
1099 if not root.mutable():
1100 raise error.Abort(_('cannot edit public changeset: %s') % root,
1100 raise error.Abort(_('cannot edit public changeset: %s') % root,
1101 hint=_('see "hg help phases" for details'))
1101 hint=_('see "hg help phases" for details'))
1102 return [c.node() for c in ctxs]
1102 return [c.node() for c in ctxs]
1103
1103
1104 def makedesc(repo, action, rev):
1104 def makedesc(repo, action, rev):
1105 """build a initial action line for a ctx
1105 """build a initial action line for a ctx
1106
1106
1107 line are in the form:
1107 line are in the form:
1108
1108
1109 <action> <hash> <rev> <summary>
1109 <action> <hash> <rev> <summary>
1110 """
1110 """
1111 ctx = repo[rev]
1111 ctx = repo[rev]
1112 summary = ''
1112 summary = ''
1113 if ctx.description():
1113 if ctx.description():
1114 summary = ctx.description().splitlines()[0]
1114 summary = ctx.description().splitlines()[0]
1115 line = '%s %s %d %s' % (action, ctx, ctx.rev(), summary)
1115 line = '%s %s %d %s' % (action, ctx, ctx.rev(), summary)
1116 # trim to 80 columns so it's not stupidly wide in my editor
1116 # trim to 80 columns so it's not stupidly wide in my editor
1117 maxlen = repo.ui.configint('histedit', 'linelen', default=80)
1117 maxlen = repo.ui.configint('histedit', 'linelen', default=80)
1118 maxlen = max(maxlen, 22) # avoid truncating hash
1118 maxlen = max(maxlen, 22) # avoid truncating hash
1119 return util.ellipsis(line, maxlen)
1119 return util.ellipsis(line, maxlen)
1120
1120
1121 def ruleeditor(repo, ui, rules, editcomment=""):
1121 def ruleeditor(repo, ui, rules, editcomment=""):
1122 """open an editor to edit rules
1122 """open an editor to edit rules
1123
1123
1124 rules are in the format [ [act, ctx], ...] like in state.rules
1124 rules are in the format [ [act, ctx], ...] like in state.rules
1125 """
1125 """
1126 rules = '\n'.join([makedesc(repo, act, rev) for [act, rev] in rules])
1126 rules = '\n'.join([makedesc(repo, act, rev) for [act, rev] in rules])
1127 rules += '\n\n'
1127 rules += '\n\n'
1128 rules += editcomment
1128 rules += editcomment
1129 rules = ui.edit(rules, ui.username())
1129 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1130
1130
1131 # Save edit rules in .hg/histedit-last-edit.txt in case
1131 # Save edit rules in .hg/histedit-last-edit.txt in case
1132 # the user needs to ask for help after something
1132 # the user needs to ask for help after something
1133 # surprising happens.
1133 # surprising happens.
1134 f = open(repo.join('histedit-last-edit.txt'), 'w')
1134 f = open(repo.join('histedit-last-edit.txt'), 'w')
1135 f.write(rules)
1135 f.write(rules)
1136 f.close()
1136 f.close()
1137
1137
1138 return rules
1138 return rules
1139
1139
1140 def verifyrules(rules, state, ctxs):
1140 def verifyrules(rules, state, ctxs):
1141 """Verify that there exists exactly one edit rule per given changeset.
1141 """Verify that there exists exactly one edit rule per given changeset.
1142
1142
1143 Will abort if there are to many or too few rules, a malformed rule,
1143 Will abort if there are to many or too few rules, a malformed rule,
1144 or a rule on a changeset outside of the user-given range.
1144 or a rule on a changeset outside of the user-given range.
1145 """
1145 """
1146 parsed = []
1146 parsed = []
1147 expected = set(c.hex() for c in ctxs)
1147 expected = set(c.hex() for c in ctxs)
1148 seen = set()
1148 seen = set()
1149 for r in rules:
1149 for r in rules:
1150 if ' ' not in r:
1150 if ' ' not in r:
1151 raise error.Abort(_('malformed line "%s"') % r)
1151 raise error.Abort(_('malformed line "%s"') % r)
1152 verb, rest = r.split(' ', 1)
1152 verb, rest = r.split(' ', 1)
1153
1153
1154 if verb not in actiontable or verb.startswith('_'):
1154 if verb not in actiontable or verb.startswith('_'):
1155 raise error.Abort(_('unknown action "%s"') % verb)
1155 raise error.Abort(_('unknown action "%s"') % verb)
1156 action = actiontable[verb].fromrule(state, rest)
1156 action = actiontable[verb].fromrule(state, rest)
1157 constraints = action.constraints()
1157 constraints = action.constraints()
1158 for constraint in constraints:
1158 for constraint in constraints:
1159 if constraint not in _constraints.known():
1159 if constraint not in _constraints.known():
1160 error.Abort(_('unknown constraint "%s"') % constraint)
1160 error.Abort(_('unknown constraint "%s"') % constraint)
1161
1161
1162 nodetoverify = action.nodetoverify()
1162 nodetoverify = action.nodetoverify()
1163 if nodetoverify is not None:
1163 if nodetoverify is not None:
1164 ha = node.hex(nodetoverify)
1164 ha = node.hex(nodetoverify)
1165 if _constraints.noother in constraints and ha not in expected:
1165 if _constraints.noother in constraints and ha not in expected:
1166 raise error.Abort(
1166 raise error.Abort(
1167 _('may not use "%s" with changesets '
1167 _('may not use "%s" with changesets '
1168 'other than the ones listed') % verb)
1168 'other than the ones listed') % verb)
1169 if _constraints.forceother in constraints and ha in expected:
1169 if _constraints.forceother in constraints and ha in expected:
1170 raise error.Abort(
1170 raise error.Abort(
1171 _('may not use "%s" with changesets '
1171 _('may not use "%s" with changesets '
1172 'within the edited list') % verb)
1172 'within the edited list') % verb)
1173 if _constraints.noduplicates in constraints and ha in seen:
1173 if _constraints.noduplicates in constraints and ha in seen:
1174 raise error.Abort(_('duplicated command for changeset %s') %
1174 raise error.Abort(_('duplicated command for changeset %s') %
1175 ha[:12])
1175 ha[:12])
1176 seen.add(ha)
1176 seen.add(ha)
1177 rest = ha
1177 rest = ha
1178 parsed.append([verb, rest])
1178 parsed.append([verb, rest])
1179 missing = sorted(expected - seen) # sort to stabilize output
1179 missing = sorted(expected - seen) # sort to stabilize output
1180 if missing:
1180 if missing:
1181 raise error.Abort(_('missing rules for changeset %s') %
1181 raise error.Abort(_('missing rules for changeset %s') %
1182 missing[0][:12],
1182 missing[0][:12],
1183 hint=_('do you want to use the drop action?'))
1183 hint=_('do you want to use the drop action?'))
1184 return parsed
1184 return parsed
1185
1185
1186 def newnodestoabort(state):
1186 def newnodestoabort(state):
1187 """process the list of replacements to return
1187 """process the list of replacements to return
1188
1188
1189 1) the list of final node
1189 1) the list of final node
1190 2) the list of temporary node
1190 2) the list of temporary node
1191
1191
1192 This meant to be used on abort as less data are required in this case.
1192 This meant to be used on abort as less data are required in this case.
1193 """
1193 """
1194 replacements = state.replacements
1194 replacements = state.replacements
1195 allsuccs = set()
1195 allsuccs = set()
1196 replaced = set()
1196 replaced = set()
1197 for rep in replacements:
1197 for rep in replacements:
1198 allsuccs.update(rep[1])
1198 allsuccs.update(rep[1])
1199 replaced.add(rep[0])
1199 replaced.add(rep[0])
1200 newnodes = allsuccs - replaced
1200 newnodes = allsuccs - replaced
1201 tmpnodes = allsuccs & replaced
1201 tmpnodes = allsuccs & replaced
1202 return newnodes, tmpnodes
1202 return newnodes, tmpnodes
1203
1203
1204
1204
1205 def processreplacement(state):
1205 def processreplacement(state):
1206 """process the list of replacements to return
1206 """process the list of replacements to return
1207
1207
1208 1) the final mapping between original and created nodes
1208 1) the final mapping between original and created nodes
1209 2) the list of temporary node created by histedit
1209 2) the list of temporary node created by histedit
1210 3) the list of new commit created by histedit"""
1210 3) the list of new commit created by histedit"""
1211 replacements = state.replacements
1211 replacements = state.replacements
1212 allsuccs = set()
1212 allsuccs = set()
1213 replaced = set()
1213 replaced = set()
1214 fullmapping = {}
1214 fullmapping = {}
1215 # initialize basic set
1215 # initialize basic set
1216 # fullmapping records all operations recorded in replacement
1216 # fullmapping records all operations recorded in replacement
1217 for rep in replacements:
1217 for rep in replacements:
1218 allsuccs.update(rep[1])
1218 allsuccs.update(rep[1])
1219 replaced.add(rep[0])
1219 replaced.add(rep[0])
1220 fullmapping.setdefault(rep[0], set()).update(rep[1])
1220 fullmapping.setdefault(rep[0], set()).update(rep[1])
1221 new = allsuccs - replaced
1221 new = allsuccs - replaced
1222 tmpnodes = allsuccs & replaced
1222 tmpnodes = allsuccs & replaced
1223 # Reduce content fullmapping into direct relation between original nodes
1223 # Reduce content fullmapping into direct relation between original nodes
1224 # and final node created during history edition
1224 # and final node created during history edition
1225 # Dropped changeset are replaced by an empty list
1225 # Dropped changeset are replaced by an empty list
1226 toproceed = set(fullmapping)
1226 toproceed = set(fullmapping)
1227 final = {}
1227 final = {}
1228 while toproceed:
1228 while toproceed:
1229 for x in list(toproceed):
1229 for x in list(toproceed):
1230 succs = fullmapping[x]
1230 succs = fullmapping[x]
1231 for s in list(succs):
1231 for s in list(succs):
1232 if s in toproceed:
1232 if s in toproceed:
1233 # non final node with unknown closure
1233 # non final node with unknown closure
1234 # We can't process this now
1234 # We can't process this now
1235 break
1235 break
1236 elif s in final:
1236 elif s in final:
1237 # non final node, replace with closure
1237 # non final node, replace with closure
1238 succs.remove(s)
1238 succs.remove(s)
1239 succs.update(final[s])
1239 succs.update(final[s])
1240 else:
1240 else:
1241 final[x] = succs
1241 final[x] = succs
1242 toproceed.remove(x)
1242 toproceed.remove(x)
1243 # remove tmpnodes from final mapping
1243 # remove tmpnodes from final mapping
1244 for n in tmpnodes:
1244 for n in tmpnodes:
1245 del final[n]
1245 del final[n]
1246 # we expect all changes involved in final to exist in the repo
1246 # we expect all changes involved in final to exist in the repo
1247 # turn `final` into list (topologically sorted)
1247 # turn `final` into list (topologically sorted)
1248 nm = state.repo.changelog.nodemap
1248 nm = state.repo.changelog.nodemap
1249 for prec, succs in final.items():
1249 for prec, succs in final.items():
1250 final[prec] = sorted(succs, key=nm.get)
1250 final[prec] = sorted(succs, key=nm.get)
1251
1251
1252 # computed topmost element (necessary for bookmark)
1252 # computed topmost element (necessary for bookmark)
1253 if new:
1253 if new:
1254 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1254 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1255 elif not final:
1255 elif not final:
1256 # Nothing rewritten at all. we won't need `newtopmost`
1256 # Nothing rewritten at all. we won't need `newtopmost`
1257 # It is the same as `oldtopmost` and `processreplacement` know it
1257 # It is the same as `oldtopmost` and `processreplacement` know it
1258 newtopmost = None
1258 newtopmost = None
1259 else:
1259 else:
1260 # every body died. The newtopmost is the parent of the root.
1260 # every body died. The newtopmost is the parent of the root.
1261 r = state.repo.changelog.rev
1261 r = state.repo.changelog.rev
1262 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1262 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1263
1263
1264 return final, tmpnodes, new, newtopmost
1264 return final, tmpnodes, new, newtopmost
1265
1265
1266 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1266 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1267 """Move bookmark from old to newly created node"""
1267 """Move bookmark from old to newly created node"""
1268 if not mapping:
1268 if not mapping:
1269 # if nothing got rewritten there is not purpose for this function
1269 # if nothing got rewritten there is not purpose for this function
1270 return
1270 return
1271 moves = []
1271 moves = []
1272 for bk, old in sorted(repo._bookmarks.iteritems()):
1272 for bk, old in sorted(repo._bookmarks.iteritems()):
1273 if old == oldtopmost:
1273 if old == oldtopmost:
1274 # special case ensure bookmark stay on tip.
1274 # special case ensure bookmark stay on tip.
1275 #
1275 #
1276 # This is arguably a feature and we may only want that for the
1276 # This is arguably a feature and we may only want that for the
1277 # active bookmark. But the behavior is kept compatible with the old
1277 # active bookmark. But the behavior is kept compatible with the old
1278 # version for now.
1278 # version for now.
1279 moves.append((bk, newtopmost))
1279 moves.append((bk, newtopmost))
1280 continue
1280 continue
1281 base = old
1281 base = old
1282 new = mapping.get(base, None)
1282 new = mapping.get(base, None)
1283 if new is None:
1283 if new is None:
1284 continue
1284 continue
1285 while not new:
1285 while not new:
1286 # base is killed, trying with parent
1286 # base is killed, trying with parent
1287 base = repo[base].p1().node()
1287 base = repo[base].p1().node()
1288 new = mapping.get(base, (base,))
1288 new = mapping.get(base, (base,))
1289 # nothing to move
1289 # nothing to move
1290 moves.append((bk, new[-1]))
1290 moves.append((bk, new[-1]))
1291 if moves:
1291 if moves:
1292 lock = tr = None
1292 lock = tr = None
1293 try:
1293 try:
1294 lock = repo.lock()
1294 lock = repo.lock()
1295 tr = repo.transaction('histedit')
1295 tr = repo.transaction('histedit')
1296 marks = repo._bookmarks
1296 marks = repo._bookmarks
1297 for mark, new in moves:
1297 for mark, new in moves:
1298 old = marks[mark]
1298 old = marks[mark]
1299 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1299 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1300 % (mark, node.short(old), node.short(new)))
1300 % (mark, node.short(old), node.short(new)))
1301 marks[mark] = new
1301 marks[mark] = new
1302 marks.recordchange(tr)
1302 marks.recordchange(tr)
1303 tr.close()
1303 tr.close()
1304 finally:
1304 finally:
1305 release(tr, lock)
1305 release(tr, lock)
1306
1306
1307 def cleanupnode(ui, repo, name, nodes):
1307 def cleanupnode(ui, repo, name, nodes):
1308 """strip a group of nodes from the repository
1308 """strip a group of nodes from the repository
1309
1309
1310 The set of node to strip may contains unknown nodes."""
1310 The set of node to strip may contains unknown nodes."""
1311 ui.debug('should strip %s nodes %s\n' %
1311 ui.debug('should strip %s nodes %s\n' %
1312 (name, ', '.join([node.short(n) for n in nodes])))
1312 (name, ', '.join([node.short(n) for n in nodes])))
1313 lock = None
1313 lock = None
1314 try:
1314 try:
1315 lock = repo.lock()
1315 lock = repo.lock()
1316 # do not let filtering get in the way of the cleanse
1316 # do not let filtering get in the way of the cleanse
1317 # we should probably get rid of obsolescence marker created during the
1317 # we should probably get rid of obsolescence marker created during the
1318 # histedit, but we currently do not have such information.
1318 # histedit, but we currently do not have such information.
1319 repo = repo.unfiltered()
1319 repo = repo.unfiltered()
1320 # Find all nodes that need to be stripped
1320 # Find all nodes that need to be stripped
1321 # (we use %lr instead of %ln to silently ignore unknown items)
1321 # (we use %lr instead of %ln to silently ignore unknown items)
1322 nm = repo.changelog.nodemap
1322 nm = repo.changelog.nodemap
1323 nodes = sorted(n for n in nodes if n in nm)
1323 nodes = sorted(n for n in nodes if n in nm)
1324 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1324 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1325 for c in roots:
1325 for c in roots:
1326 # We should process node in reverse order to strip tip most first.
1326 # We should process node in reverse order to strip tip most first.
1327 # but this trigger a bug in changegroup hook.
1327 # but this trigger a bug in changegroup hook.
1328 # This would reduce bundle overhead
1328 # This would reduce bundle overhead
1329 repair.strip(ui, repo, c)
1329 repair.strip(ui, repo, c)
1330 finally:
1330 finally:
1331 release(lock)
1331 release(lock)
1332
1332
1333 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1333 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1334 if isinstance(nodelist, str):
1334 if isinstance(nodelist, str):
1335 nodelist = [nodelist]
1335 nodelist = [nodelist]
1336 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1336 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1337 state = histeditstate(repo)
1337 state = histeditstate(repo)
1338 state.read()
1338 state.read()
1339 histedit_nodes = set([repo[rulehash].node() for (action, rulehash)
1339 histedit_nodes = set([repo[rulehash].node() for (action, rulehash)
1340 in state.rules if rulehash in repo])
1340 in state.rules if rulehash in repo])
1341 strip_nodes = set([repo[n].node() for n in nodelist])
1341 strip_nodes = set([repo[n].node() for n in nodelist])
1342 common_nodes = histedit_nodes & strip_nodes
1342 common_nodes = histedit_nodes & strip_nodes
1343 if common_nodes:
1343 if common_nodes:
1344 raise error.Abort(_("histedit in progress, can't strip %s")
1344 raise error.Abort(_("histedit in progress, can't strip %s")
1345 % ', '.join(node.short(x) for x in common_nodes))
1345 % ', '.join(node.short(x) for x in common_nodes))
1346 return orig(ui, repo, nodelist, *args, **kwargs)
1346 return orig(ui, repo, nodelist, *args, **kwargs)
1347
1347
1348 extensions.wrapfunction(repair, 'strip', stripwrapper)
1348 extensions.wrapfunction(repair, 'strip', stripwrapper)
1349
1349
1350 def summaryhook(ui, repo):
1350 def summaryhook(ui, repo):
1351 if not os.path.exists(repo.join('histedit-state')):
1351 if not os.path.exists(repo.join('histedit-state')):
1352 return
1352 return
1353 state = histeditstate(repo)
1353 state = histeditstate(repo)
1354 state.read()
1354 state.read()
1355 if state.rules:
1355 if state.rules:
1356 # i18n: column positioning for "hg summary"
1356 # i18n: column positioning for "hg summary"
1357 ui.write(_('hist: %s (histedit --continue)\n') %
1357 ui.write(_('hist: %s (histedit --continue)\n') %
1358 (ui.label(_('%d remaining'), 'histedit.remaining') %
1358 (ui.label(_('%d remaining'), 'histedit.remaining') %
1359 len(state.rules)))
1359 len(state.rules)))
1360
1360
1361 def extsetup(ui):
1361 def extsetup(ui):
1362 cmdutil.summaryhooks.add('histedit', summaryhook)
1362 cmdutil.summaryhooks.add('histedit', summaryhook)
1363 cmdutil.unfinishedstates.append(
1363 cmdutil.unfinishedstates.append(
1364 ['histedit-state', False, True, _('histedit in progress'),
1364 ['histedit-state', False, True, _('histedit in progress'),
1365 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1365 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1366 if ui.configbool("experimental", "histeditng"):
1366 if ui.configbool("experimental", "histeditng"):
1367 actiontable.update({'b': base, 'base': base})
1367 actiontable.update({'b': base, 'base': base})
General Comments 0
You need to be logged in to leave comments. Login now