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