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