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