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