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