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