##// END OF EJS Templates
histedit: switch state to store node instead of ctx...
Mateusz Kwapich -
r24112:5d5ec4fb default
parent child Browse files
Show More
@@ -1,1008 +1,1015 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, parentctx=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.parentctx = parentctx
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.parentctx = self.repo[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.parentctx.node(), 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, ctx = state.repo, state.parentctx
350 repo, ctxnode = state.repo, state.parentctxnode
351 ctx = repo[ctxnode]
351 oldctx = repo[ha]
352 oldctx = repo[ha]
352 if oldctx.parents()[0] == ctx:
353 if oldctx.parents()[0] == ctx:
353 ui.debug('node %s unchanged\n' % ha[:12])
354 ui.debug('node %s unchanged\n' % ha[:12])
354 return oldctx, []
355 return oldctx, []
355 hg.update(repo, ctx.node())
356 hg.update(repo, ctx.node())
356 stats = applychanges(ui, repo, oldctx, opts)
357 stats = applychanges(ui, repo, oldctx, opts)
357 if stats and stats[3] > 0:
358 if stats and stats[3] > 0:
358 raise error.InterventionRequired(_('Fix up the change and run '
359 raise error.InterventionRequired(_('Fix up the change and run '
359 'hg histedit --continue'))
360 'hg histedit --continue'))
360 # drop the second merge parent
361 # drop the second merge parent
361 commit = commitfuncfor(repo, oldctx)
362 commit = commitfuncfor(repo, oldctx)
362 n = commit(text=oldctx.description(), user=oldctx.user(),
363 n = commit(text=oldctx.description(), user=oldctx.user(),
363 date=oldctx.date(), extra=oldctx.extra())
364 date=oldctx.date(), extra=oldctx.extra())
364 if n is None:
365 if n is None:
365 ui.warn(_('%s: empty changeset\n') % ha[:12])
366 ui.warn(_('%s: empty changeset\n') % ha[:12])
366 return ctx, []
367 return ctx, []
367 new = repo[n]
368 new = repo[n]
368 return new, [(oldctx.node(), (n,))]
369 return new, [(oldctx.node(), (n,))]
369
370
370
371
371 def edit(ui, state, ha, opts):
372 def edit(ui, state, ha, opts):
372 repo, ctx = state.repo, state.parentctx
373 repo, ctxnode = state.repo, state.parentctxnode
374 ctx = repo[ctxnode]
373 oldctx = repo[ha]
375 oldctx = repo[ha]
374 hg.update(repo, ctx.node())
376 hg.update(repo, ctx.node())
375 applychanges(ui, repo, oldctx, opts)
377 applychanges(ui, repo, oldctx, opts)
376 raise error.InterventionRequired(
378 raise error.InterventionRequired(
377 _('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'
378 'When you are finished, run hg histedit --continue to resume.'))
380 'When you are finished, run hg histedit --continue to resume.'))
379
381
380 def rollup(ui, state, ha, opts):
382 def rollup(ui, state, ha, opts):
381 rollupopts = opts.copy()
383 rollupopts = opts.copy()
382 rollupopts['rollup'] = True
384 rollupopts['rollup'] = True
383 return fold(ui, state, ha, rollupopts)
385 return fold(ui, state, ha, rollupopts)
384
386
385 def fold(ui, state, ha, opts):
387 def fold(ui, state, ha, opts):
386 repo, ctx = state.repo, state.parentctx
388 repo, ctxnode = state.repo, state.parentctxnode
389 ctx = repo[ctxnode]
387 oldctx = repo[ha]
390 oldctx = repo[ha]
388 hg.update(repo, ctx.node())
391 hg.update(repo, ctx.node())
389 stats = applychanges(ui, repo, oldctx, opts)
392 stats = applychanges(ui, repo, oldctx, opts)
390 if stats and stats[3] > 0:
393 if stats and stats[3] > 0:
391 raise error.InterventionRequired(
394 raise error.InterventionRequired(
392 _('Fix up the change and run hg histedit --continue'))
395 _('Fix up the change and run hg histedit --continue'))
393 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(),
394 date=oldctx.date(), extra=oldctx.extra())
397 date=oldctx.date(), extra=oldctx.extra())
395 if n is None:
398 if n is None:
396 ui.warn(_('%s: empty changeset') % ha[:12])
399 ui.warn(_('%s: empty changeset') % ha[:12])
397 return ctx, []
400 return ctx, []
398 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
401 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
399
402
400 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
403 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
401 parent = ctx.parents()[0].node()
404 parent = ctx.parents()[0].node()
402 hg.update(repo, parent)
405 hg.update(repo, parent)
403 ### prepare new commit data
406 ### prepare new commit data
404 commitopts = opts.copy()
407 commitopts = opts.copy()
405 commitopts['user'] = ctx.user()
408 commitopts['user'] = ctx.user()
406 # commit message
409 # commit message
407 if opts.get('rollup'):
410 if opts.get('rollup'):
408 newmessage = ctx.description()
411 newmessage = ctx.description()
409 else:
412 else:
410 newmessage = '\n***\n'.join(
413 newmessage = '\n***\n'.join(
411 [ctx.description()] +
414 [ctx.description()] +
412 [repo[r].description() for r in internalchanges] +
415 [repo[r].description() for r in internalchanges] +
413 [oldctx.description()]) + '\n'
416 [oldctx.description()]) + '\n'
414 commitopts['message'] = newmessage
417 commitopts['message'] = newmessage
415 # date
418 # date
416 commitopts['date'] = max(ctx.date(), oldctx.date())
419 commitopts['date'] = max(ctx.date(), oldctx.date())
417 extra = ctx.extra().copy()
420 extra = ctx.extra().copy()
418 # histedit_source
421 # histedit_source
419 # 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
420 # This is sufficient to solve issue3681 anyway
423 # This is sufficient to solve issue3681 anyway
421 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
424 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
422 commitopts['extra'] = extra
425 commitopts['extra'] = extra
423 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
426 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
424 try:
427 try:
425 phasemin = max(ctx.phase(), oldctx.phase())
428 phasemin = max(ctx.phase(), oldctx.phase())
426 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
429 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
427 n = collapse(repo, ctx, repo[newnode], commitopts)
430 n = collapse(repo, ctx, repo[newnode], commitopts)
428 finally:
431 finally:
429 repo.ui.restoreconfig(phasebackup)
432 repo.ui.restoreconfig(phasebackup)
430 if n is None:
433 if n is None:
431 return ctx, []
434 return ctx, []
432 hg.update(repo, n)
435 hg.update(repo, n)
433 replacements = [(oldctx.node(), (newnode,)),
436 replacements = [(oldctx.node(), (newnode,)),
434 (ctx.node(), (n,)),
437 (ctx.node(), (n,)),
435 (newnode, (n,)),
438 (newnode, (n,)),
436 ]
439 ]
437 for ich in internalchanges:
440 for ich in internalchanges:
438 replacements.append((ich, (n,)))
441 replacements.append((ich, (n,)))
439 return repo[n], replacements
442 return repo[n], replacements
440
443
441 def drop(ui, state, ha, opts):
444 def drop(ui, state, ha, opts):
442 repo, ctx = state.repo, state.parentctx
445 repo, ctxnode = state.repo, state.parentctxnode
446 ctx = repo[ctxnode]
443 return ctx, [(repo[ha].node(), ())]
447 return ctx, [(repo[ha].node(), ())]
444
448
445
449
446 def message(ui, state, ha, opts):
450 def message(ui, state, ha, opts):
447 repo, ctx = state.repo, state.parentctx
451 repo, ctxnode = state.repo, state.parentctxnode
452 ctx = repo[ctxnode]
448 oldctx = repo[ha]
453 oldctx = repo[ha]
449 hg.update(repo, ctx.node())
454 hg.update(repo, ctx.node())
450 stats = applychanges(ui, repo, oldctx, opts)
455 stats = applychanges(ui, repo, oldctx, opts)
451 if stats and stats[3] > 0:
456 if stats and stats[3] > 0:
452 raise error.InterventionRequired(
457 raise error.InterventionRequired(
453 _('Fix up the change and run hg histedit --continue'))
458 _('Fix up the change and run hg histedit --continue'))
454 message = oldctx.description()
459 message = oldctx.description()
455 commit = commitfuncfor(repo, oldctx)
460 commit = commitfuncfor(repo, oldctx)
456 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
461 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
457 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
462 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
458 extra=oldctx.extra(), editor=editor)
463 extra=oldctx.extra(), editor=editor)
459 newctx = repo[new]
464 newctx = repo[new]
460 if oldctx.node() != newctx.node():
465 if oldctx.node() != newctx.node():
461 return newctx, [(oldctx.node(), (new,))]
466 return newctx, [(oldctx.node(), (new,))]
462 # 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
463 return newctx, []
468 return newctx, []
464
469
465 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
470 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
466 """utility function to find the first outgoing changeset
471 """utility function to find the first outgoing changeset
467
472
468 Used by initialisation code"""
473 Used by initialisation code"""
469 dest = ui.expandpath(remote or 'default-push', remote or 'default')
474 dest = ui.expandpath(remote or 'default-push', remote or 'default')
470 dest, revs = hg.parseurl(dest, None)[:2]
475 dest, revs = hg.parseurl(dest, None)[:2]
471 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
476 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
472
477
473 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
478 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
474 other = hg.peer(repo, opts, dest)
479 other = hg.peer(repo, opts, dest)
475
480
476 if revs:
481 if revs:
477 revs = [repo.lookup(rev) for rev in revs]
482 revs = [repo.lookup(rev) for rev in revs]
478
483
479 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
484 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
480 if not outgoing.missing:
485 if not outgoing.missing:
481 raise util.Abort(_('no outgoing ancestors'))
486 raise util.Abort(_('no outgoing ancestors'))
482 roots = list(repo.revs("roots(%ln)", outgoing.missing))
487 roots = list(repo.revs("roots(%ln)", outgoing.missing))
483 if 1 < len(roots):
488 if 1 < len(roots):
484 msg = _('there are ambiguous outgoing revisions')
489 msg = _('there are ambiguous outgoing revisions')
485 hint = _('see "hg help histedit" for more detail')
490 hint = _('see "hg help histedit" for more detail')
486 raise util.Abort(msg, hint=hint)
491 raise util.Abort(msg, hint=hint)
487 return repo.lookup(roots[0])
492 return repo.lookup(roots[0])
488
493
489 actiontable = {'p': pick,
494 actiontable = {'p': pick,
490 'pick': pick,
495 'pick': pick,
491 'e': edit,
496 'e': edit,
492 'edit': edit,
497 'edit': edit,
493 'f': fold,
498 'f': fold,
494 'fold': fold,
499 'fold': fold,
495 'r': rollup,
500 'r': rollup,
496 'roll': rollup,
501 'roll': rollup,
497 'd': drop,
502 'd': drop,
498 'drop': drop,
503 'drop': drop,
499 'm': message,
504 'm': message,
500 'mess': message,
505 'mess': message,
501 }
506 }
502
507
503 @command('histedit',
508 @command('histedit',
504 [('', 'commands', '',
509 [('', 'commands', '',
505 _('Read history edits from the specified file.')),
510 _('Read history edits from the specified file.')),
506 ('c', 'continue', False, _('continue an edit already in progress')),
511 ('c', 'continue', False, _('continue an edit already in progress')),
507 ('k', 'keep', False,
512 ('k', 'keep', False,
508 _("don't strip old nodes after edit is complete")),
513 _("don't strip old nodes after edit is complete")),
509 ('', 'abort', False, _('abort an edit in progress')),
514 ('', 'abort', False, _('abort an edit in progress')),
510 ('o', 'outgoing', False, _('changesets not found in destination')),
515 ('o', 'outgoing', False, _('changesets not found in destination')),
511 ('f', 'force', False,
516 ('f', 'force', False,
512 _('force outgoing even for unrelated repositories')),
517 _('force outgoing even for unrelated repositories')),
513 ('r', 'rev', [], _('first revision to be edited'))],
518 ('r', 'rev', [], _('first revision to be edited'))],
514 _("ANCESTOR | --outgoing [URL]"))
519 _("ANCESTOR | --outgoing [URL]"))
515 def histedit(ui, repo, *freeargs, **opts):
520 def histedit(ui, repo, *freeargs, **opts):
516 """interactively edit changeset history
521 """interactively edit changeset history
517
522
518 This command edits changesets between ANCESTOR and the parent of
523 This command edits changesets between ANCESTOR and the parent of
519 the working directory.
524 the working directory.
520
525
521 With --outgoing, this edits changesets not found in the
526 With --outgoing, this edits changesets not found in the
522 destination repository. If URL of the destination is omitted, the
527 destination repository. If URL of the destination is omitted, the
523 'default-push' (or 'default') path will be used.
528 'default-push' (or 'default') path will be used.
524
529
525 For safety, this command is aborted, also if there are ambiguous
530 For safety, this command is aborted, also if there are ambiguous
526 outgoing revisions which may confuse users: for example, there are
531 outgoing revisions which may confuse users: for example, there are
527 multiple branches containing outgoing revisions.
532 multiple branches containing outgoing revisions.
528
533
529 Use "min(outgoing() and ::.)" or similar revset specification
534 Use "min(outgoing() and ::.)" or similar revset specification
530 instead of --outgoing to specify edit target revision exactly in
535 instead of --outgoing to specify edit target revision exactly in
531 such ambiguous situation. See :hg:`help revsets` for detail about
536 such ambiguous situation. See :hg:`help revsets` for detail about
532 selecting revisions.
537 selecting revisions.
533
538
534 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
535 for intentional "edit" command, but also for resolving unexpected
540 for intentional "edit" command, but also for resolving unexpected
536 conflicts).
541 conflicts).
537 """
542 """
538 state = histeditstate(repo)
543 state = histeditstate(repo)
539 try:
544 try:
540 state.wlock = repo.wlock()
545 state.wlock = repo.wlock()
541 state.lock = repo.lock()
546 state.lock = repo.lock()
542 _histedit(ui, repo, state, *freeargs, **opts)
547 _histedit(ui, repo, state, *freeargs, **opts)
543 finally:
548 finally:
544 release(state.lock, state.wlock)
549 release(state.lock, state.wlock)
545
550
546 def _histedit(ui, repo, state, *freeargs, **opts):
551 def _histedit(ui, repo, state, *freeargs, **opts):
547 # 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
548 # blanket if mq patches are applied somewhere
553 # blanket if mq patches are applied somewhere
549 mq = getattr(repo, 'mq', None)
554 mq = getattr(repo, 'mq', None)
550 if mq and mq.applied:
555 if mq and mq.applied:
551 raise util.Abort(_('source has mq patches applied'))
556 raise util.Abort(_('source has mq patches applied'))
552
557
553 # basic argument incompatibility processing
558 # basic argument incompatibility processing
554 outg = opts.get('outgoing')
559 outg = opts.get('outgoing')
555 cont = opts.get('continue')
560 cont = opts.get('continue')
556 abort = opts.get('abort')
561 abort = opts.get('abort')
557 force = opts.get('force')
562 force = opts.get('force')
558 rules = opts.get('commands', '')
563 rules = opts.get('commands', '')
559 revs = opts.get('rev', [])
564 revs = opts.get('rev', [])
560 goal = 'new' # This invocation goal, in new, continue, abort
565 goal = 'new' # This invocation goal, in new, continue, abort
561 if force and not outg:
566 if force and not outg:
562 raise util.Abort(_('--force only allowed with --outgoing'))
567 raise util.Abort(_('--force only allowed with --outgoing'))
563 if cont:
568 if cont:
564 if util.any((outg, abort, revs, freeargs, rules)):
569 if util.any((outg, abort, revs, freeargs, rules)):
565 raise util.Abort(_('no arguments allowed with --continue'))
570 raise util.Abort(_('no arguments allowed with --continue'))
566 goal = 'continue'
571 goal = 'continue'
567 elif abort:
572 elif abort:
568 if util.any((outg, revs, freeargs, rules)):
573 if util.any((outg, revs, freeargs, rules)):
569 raise util.Abort(_('no arguments allowed with --abort'))
574 raise util.Abort(_('no arguments allowed with --abort'))
570 goal = 'abort'
575 goal = 'abort'
571 else:
576 else:
572 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
577 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
573 raise util.Abort(_('history edit already in progress, try '
578 raise util.Abort(_('history edit already in progress, try '
574 '--continue or --abort'))
579 '--continue or --abort'))
575 if outg:
580 if outg:
576 if revs:
581 if revs:
577 raise util.Abort(_('no revisions allowed with --outgoing'))
582 raise util.Abort(_('no revisions allowed with --outgoing'))
578 if len(freeargs) > 1:
583 if len(freeargs) > 1:
579 raise util.Abort(
584 raise util.Abort(
580 _('only one repo argument allowed with --outgoing'))
585 _('only one repo argument allowed with --outgoing'))
581 else:
586 else:
582 revs.extend(freeargs)
587 revs.extend(freeargs)
583 if len(revs) == 0:
588 if len(revs) == 0:
584 histeditdefault = ui.config('histedit', 'defaultrev')
589 histeditdefault = ui.config('histedit', 'defaultrev')
585 if histeditdefault:
590 if histeditdefault:
586 revs.append(histeditdefault)
591 revs.append(histeditdefault)
587 if len(revs) != 1:
592 if len(revs) != 1:
588 raise util.Abort(
593 raise util.Abort(
589 _('histedit requires exactly one ancestor revision'))
594 _('histedit requires exactly one ancestor revision'))
590
595
591
596
592 replacements = []
597 replacements = []
593 keep = opts.get('keep', False)
598 keep = opts.get('keep', False)
594
599
595 # rebuild state
600 # rebuild state
596 if goal == 'continue':
601 if goal == 'continue':
597 state = histeditstate(repo)
602 state = histeditstate(repo)
598 state.read()
603 state.read()
599 state = bootstrapcontinue(ui, state, opts)
604 state = bootstrapcontinue(ui, state, opts)
600 elif goal == 'abort':
605 elif goal == 'abort':
601 state = histeditstate(repo)
606 state = histeditstate(repo)
602 state.read()
607 state.read()
603 mapping, tmpnodes, leafs, _ntm = processreplacement(state)
608 mapping, tmpnodes, leafs, _ntm = processreplacement(state)
604 ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
609 ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
605 # check whether we should update away
610 # check whether we should update away
606 parentnodes = [c.node() for c in repo[None].parents()]
611 parentnodes = [c.node() for c in repo[None].parents()]
607 for n in leafs | set([state.parentctx.node()]):
612 for n in leafs | set([state.parentctxnode]):
608 if n in parentnodes:
613 if n in parentnodes:
609 hg.clean(repo, state.topmost)
614 hg.clean(repo, state.topmost)
610 break
615 break
611 else:
616 else:
612 pass
617 pass
613 cleanupnode(ui, repo, 'created', tmpnodes)
618 cleanupnode(ui, repo, 'created', tmpnodes)
614 cleanupnode(ui, repo, 'temp', leafs)
619 cleanupnode(ui, repo, 'temp', leafs)
615 state.clear()
620 state.clear()
616 return
621 return
617 else:
622 else:
618 cmdutil.checkunfinished(repo)
623 cmdutil.checkunfinished(repo)
619 cmdutil.bailifchanged(repo)
624 cmdutil.bailifchanged(repo)
620
625
621 topmost, empty = repo.dirstate.parents()
626 topmost, empty = repo.dirstate.parents()
622 if outg:
627 if outg:
623 if freeargs:
628 if freeargs:
624 remote = freeargs[0]
629 remote = freeargs[0]
625 else:
630 else:
626 remote = None
631 remote = None
627 root = findoutgoing(ui, repo, remote, force, opts)
632 root = findoutgoing(ui, repo, remote, force, opts)
628 else:
633 else:
629 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
634 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
630 if len(rr) != 1:
635 if len(rr) != 1:
631 raise util.Abort(_('The specified revisions must have '
636 raise util.Abort(_('The specified revisions must have '
632 'exactly one common root'))
637 'exactly one common root'))
633 root = rr[0].node()
638 root = rr[0].node()
634
639
635 revs = between(repo, root, topmost, keep)
640 revs = between(repo, root, topmost, keep)
636 if not revs:
641 if not revs:
637 raise util.Abort(_('%s is not an ancestor of working directory') %
642 raise util.Abort(_('%s is not an ancestor of working directory') %
638 node.short(root))
643 node.short(root))
639
644
640 ctxs = [repo[r] for r in revs]
645 ctxs = [repo[r] for r in revs]
641 if not rules:
646 if not rules:
642 rules = '\n'.join([makedesc(c) for c in ctxs])
647 rules = '\n'.join([makedesc(c) for c in ctxs])
643 rules += '\n\n'
648 rules += '\n\n'
644 rules += editcomment % (node.short(root), node.short(topmost))
649 rules += editcomment % (node.short(root), node.short(topmost))
645 rules = ui.edit(rules, ui.username())
650 rules = ui.edit(rules, ui.username())
646 # Save edit rules in .hg/histedit-last-edit.txt in case
651 # Save edit rules in .hg/histedit-last-edit.txt in case
647 # the user needs to ask for help after something
652 # the user needs to ask for help after something
648 # surprising happens.
653 # surprising happens.
649 f = open(repo.join('histedit-last-edit.txt'), 'w')
654 f = open(repo.join('histedit-last-edit.txt'), 'w')
650 f.write(rules)
655 f.write(rules)
651 f.close()
656 f.close()
652 else:
657 else:
653 if rules == '-':
658 if rules == '-':
654 f = sys.stdin
659 f = sys.stdin
655 else:
660 else:
656 f = open(rules)
661 f = open(rules)
657 rules = f.read()
662 rules = f.read()
658 f.close()
663 f.close()
659 rules = [l for l in (r.strip() for r in rules.splitlines())
664 rules = [l for l in (r.strip() for r in rules.splitlines())
660 if l and not l.startswith('#')]
665 if l and not l.startswith('#')]
661 rules = verifyrules(rules, repo, ctxs)
666 rules = verifyrules(rules, repo, ctxs)
662
667
663 parentctx = repo[root].parents()[0]
668 parentctxnode = repo[root].parents()[0].node()
664
669
665 state.parentctx = parentctx
670 state.parentctxnode = parentctxnode
666 state.rules = rules
671 state.rules = rules
667 state.keep = keep
672 state.keep = keep
668 state.topmost = topmost
673 state.topmost = topmost
669 state.replacements = replacements
674 state.replacements = replacements
670
675
671 while state.rules:
676 while state.rules:
672 state.write()
677 state.write()
673 action, ha = state.rules.pop(0)
678 action, ha = state.rules.pop(0)
674 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
679 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
675 actfunc = actiontable[action]
680 actfunc = actiontable[action]
676 state.parentctx, replacement_ = actfunc(ui, state, ha, opts)
681 parentctx, replacement_ = actfunc(ui, state, ha, opts)
682 state.parentctxnode = parentctx.node()
677 state.replacements.extend(replacement_)
683 state.replacements.extend(replacement_)
678 state.write()
684 state.write()
679
685
680 hg.update(repo, state.parentctx.node())
686 hg.update(repo, state.parentctxnode)
681
687
682 mapping, tmpnodes, created, ntm = processreplacement(state)
688 mapping, tmpnodes, created, ntm = processreplacement(state)
683 if mapping:
689 if mapping:
684 for prec, succs in mapping.iteritems():
690 for prec, succs in mapping.iteritems():
685 if not succs:
691 if not succs:
686 ui.debug('histedit: %s is dropped\n' % node.short(prec))
692 ui.debug('histedit: %s is dropped\n' % node.short(prec))
687 else:
693 else:
688 ui.debug('histedit: %s is replaced by %s\n' % (
694 ui.debug('histedit: %s is replaced by %s\n' % (
689 node.short(prec), node.short(succs[0])))
695 node.short(prec), node.short(succs[0])))
690 if len(succs) > 1:
696 if len(succs) > 1:
691 m = 'histedit: %s'
697 m = 'histedit: %s'
692 for n in succs[1:]:
698 for n in succs[1:]:
693 ui.debug(m % node.short(n))
699 ui.debug(m % node.short(n))
694
700
695 if not keep:
701 if not keep:
696 if mapping:
702 if mapping:
697 movebookmarks(ui, repo, mapping, state.topmost, ntm)
703 movebookmarks(ui, repo, mapping, state.topmost, ntm)
698 # TODO update mq state
704 # TODO update mq state
699 if obsolete.isenabled(repo, obsolete.createmarkersopt):
705 if obsolete.isenabled(repo, obsolete.createmarkersopt):
700 markers = []
706 markers = []
701 # sort by revision number because it sound "right"
707 # sort by revision number because it sound "right"
702 for prec in sorted(mapping, key=repo.changelog.rev):
708 for prec in sorted(mapping, key=repo.changelog.rev):
703 succs = mapping[prec]
709 succs = mapping[prec]
704 markers.append((repo[prec],
710 markers.append((repo[prec],
705 tuple(repo[s] for s in succs)))
711 tuple(repo[s] for s in succs)))
706 if markers:
712 if markers:
707 obsolete.createmarkers(repo, markers)
713 obsolete.createmarkers(repo, markers)
708 else:
714 else:
709 cleanupnode(ui, repo, 'replaced', mapping)
715 cleanupnode(ui, repo, 'replaced', mapping)
710
716
711 cleanupnode(ui, repo, 'temp', tmpnodes)
717 cleanupnode(ui, repo, 'temp', tmpnodes)
712 state.clear()
718 state.clear()
713 if os.path.exists(repo.sjoin('undo')):
719 if os.path.exists(repo.sjoin('undo')):
714 os.unlink(repo.sjoin('undo'))
720 os.unlink(repo.sjoin('undo'))
715
721
716 def gatherchildren(repo, ctx):
722 def gatherchildren(repo, ctx):
717 # is there any new commit between the expected parent and "."
723 # is there any new commit between the expected parent and "."
718 #
724 #
719 # note: does not take non linear new change in account (but previous
725 # note: does not take non linear new change in account (but previous
720 # implementation didn't used them anyway (issue3655)
726 # implementation didn't used them anyway (issue3655)
721 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
727 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
722 if ctx.node() != node.nullid:
728 if ctx.node() != node.nullid:
723 if not newchildren:
729 if not newchildren:
724 # `ctx` should match but no result. This means that
730 # `ctx` should match but no result. This means that
725 # currentnode is not a descendant from ctx.
731 # currentnode is not a descendant from ctx.
726 msg = _('%s is not an ancestor of working directory')
732 msg = _('%s is not an ancestor of working directory')
727 hint = _('use "histedit --abort" to clear broken state')
733 hint = _('use "histedit --abort" to clear broken state')
728 raise util.Abort(msg % ctx, hint=hint)
734 raise util.Abort(msg % ctx, hint=hint)
729 newchildren.pop(0) # remove ctx
735 newchildren.pop(0) # remove ctx
730 return newchildren
736 return newchildren
731
737
732 def bootstrapcontinue(ui, state, opts):
738 def bootstrapcontinue(ui, state, opts):
733 repo, parentctx = state.repo, state.parentctx
739 repo, parentctxnode = state.repo, state.parentctxnode
740 parentctx = repo[parentctxnode]
734 action, currentnode = state.rules.pop(0)
741 action, currentnode = state.rules.pop(0)
735 ctx = repo[currentnode]
742 ctx = repo[currentnode]
736
743
737 newchildren = gatherchildren(repo, parentctx)
744 newchildren = gatherchildren(repo, parentctx)
738
745
739 # Commit dirty working directory if necessary
746 # Commit dirty working directory if necessary
740 new = None
747 new = None
741 s = repo.status()
748 s = repo.status()
742 if s.modified or s.added or s.removed or s.deleted:
749 if s.modified or s.added or s.removed or s.deleted:
743 # prepare the message for the commit to comes
750 # prepare the message for the commit to comes
744 if action in ('f', 'fold', 'r', 'roll'):
751 if action in ('f', 'fold', 'r', 'roll'):
745 message = 'fold-temp-revision %s' % currentnode[:12]
752 message = 'fold-temp-revision %s' % currentnode[:12]
746 else:
753 else:
747 message = ctx.description()
754 message = ctx.description()
748 editopt = action in ('e', 'edit', 'm', 'mess')
755 editopt = action in ('e', 'edit', 'm', 'mess')
749 canonaction = {'e': 'edit', 'm': 'mess', 'p': 'pick'}
756 canonaction = {'e': 'edit', 'm': 'mess', 'p': 'pick'}
750 editform = 'histedit.%s' % canonaction.get(action, action)
757 editform = 'histedit.%s' % canonaction.get(action, action)
751 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
758 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
752 commit = commitfuncfor(repo, ctx)
759 commit = commitfuncfor(repo, ctx)
753 new = commit(text=message, user=ctx.user(), date=ctx.date(),
760 new = commit(text=message, user=ctx.user(), date=ctx.date(),
754 extra=ctx.extra(), editor=editor)
761 extra=ctx.extra(), editor=editor)
755 if new is not None:
762 if new is not None:
756 newchildren.append(new)
763 newchildren.append(new)
757
764
758 replacements = []
765 replacements = []
759 # track replacements
766 # track replacements
760 if ctx.node() not in newchildren:
767 if ctx.node() not in newchildren:
761 # note: new children may be empty when the changeset is dropped.
768 # note: new children may be empty when the changeset is dropped.
762 # this happen e.g during conflicting pick where we revert content
769 # this happen e.g during conflicting pick where we revert content
763 # to parent.
770 # to parent.
764 replacements.append((ctx.node(), tuple(newchildren)))
771 replacements.append((ctx.node(), tuple(newchildren)))
765
772
766 if action in ('f', 'fold', 'r', 'roll'):
773 if action in ('f', 'fold', 'r', 'roll'):
767 if newchildren:
774 if newchildren:
768 # finalize fold operation if applicable
775 # finalize fold operation if applicable
769 if new is None:
776 if new is None:
770 new = newchildren[-1]
777 new = newchildren[-1]
771 else:
778 else:
772 newchildren.pop() # remove new from internal changes
779 newchildren.pop() # remove new from internal changes
773 foldopts = opts
780 foldopts = opts
774 if action in ('r', 'roll'):
781 if action in ('r', 'roll'):
775 foldopts = foldopts.copy()
782 foldopts = foldopts.copy()
776 foldopts['rollup'] = True
783 foldopts['rollup'] = True
777 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new,
784 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new,
778 foldopts, newchildren)
785 foldopts, newchildren)
779 replacements.extend(repl)
786 replacements.extend(repl)
780 else:
787 else:
781 # newchildren is empty if the fold did not result in any commit
788 # newchildren is empty if the fold did not result in any commit
782 # this happen when all folded change are discarded during the
789 # this happen when all folded change are discarded during the
783 # merge.
790 # merge.
784 replacements.append((ctx.node(), (parentctx.node(),)))
791 replacements.append((ctx.node(), (parentctx.node(),)))
785 elif newchildren:
792 elif newchildren:
786 # otherwise update "parentctx" before proceeding to further operation
793 # otherwise update "parentctx" before proceeding to further operation
787 parentctx = repo[newchildren[-1]]
794 parentctx = repo[newchildren[-1]]
788
795
789 state.parentctx = parentctx
796 state.parentctxnode = parentctx.node()
790 state.replacements.extend(replacements)
797 state.replacements.extend(replacements)
791
798
792 return state
799 return state
793
800
794 def between(repo, old, new, keep):
801 def between(repo, old, new, keep):
795 """select and validate the set of revision to edit
802 """select and validate the set of revision to edit
796
803
797 When keep is false, the specified set can't have children."""
804 When keep is false, the specified set can't have children."""
798 ctxs = list(repo.set('%n::%n', old, new))
805 ctxs = list(repo.set('%n::%n', old, new))
799 if ctxs and not keep:
806 if ctxs and not keep:
800 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
807 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
801 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
808 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
802 raise util.Abort(_('cannot edit history that would orphan nodes'))
809 raise util.Abort(_('cannot edit history that would orphan nodes'))
803 if repo.revs('(%ld) and merge()', ctxs):
810 if repo.revs('(%ld) and merge()', ctxs):
804 raise util.Abort(_('cannot edit history that contains merges'))
811 raise util.Abort(_('cannot edit history that contains merges'))
805 root = ctxs[0] # list is already sorted by repo.set
812 root = ctxs[0] # list is already sorted by repo.set
806 if not root.mutable():
813 if not root.mutable():
807 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
814 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
808 return [c.node() for c in ctxs]
815 return [c.node() for c in ctxs]
809
816
810 def makedesc(c):
817 def makedesc(c):
811 """build a initial action line for a ctx `c`
818 """build a initial action line for a ctx `c`
812
819
813 line are in the form:
820 line are in the form:
814
821
815 pick <hash> <rev> <summary>
822 pick <hash> <rev> <summary>
816 """
823 """
817 summary = ''
824 summary = ''
818 if c.description():
825 if c.description():
819 summary = c.description().splitlines()[0]
826 summary = c.description().splitlines()[0]
820 line = 'pick %s %d %s' % (c, c.rev(), summary)
827 line = 'pick %s %d %s' % (c, c.rev(), summary)
821 # trim to 80 columns so it's not stupidly wide in my editor
828 # trim to 80 columns so it's not stupidly wide in my editor
822 return util.ellipsis(line, 80)
829 return util.ellipsis(line, 80)
823
830
824 def verifyrules(rules, repo, ctxs):
831 def verifyrules(rules, repo, ctxs):
825 """Verify that there exists exactly one edit rule per given changeset.
832 """Verify that there exists exactly one edit rule per given changeset.
826
833
827 Will abort if there are to many or too few rules, a malformed rule,
834 Will abort if there are to many or too few rules, a malformed rule,
828 or a rule on a changeset outside of the user-given range.
835 or a rule on a changeset outside of the user-given range.
829 """
836 """
830 parsed = []
837 parsed = []
831 expected = set(c.hex() for c in ctxs)
838 expected = set(c.hex() for c in ctxs)
832 seen = set()
839 seen = set()
833 for r in rules:
840 for r in rules:
834 if ' ' not in r:
841 if ' ' not in r:
835 raise util.Abort(_('malformed line "%s"') % r)
842 raise util.Abort(_('malformed line "%s"') % r)
836 action, rest = r.split(' ', 1)
843 action, rest = r.split(' ', 1)
837 ha = rest.strip().split(' ', 1)[0]
844 ha = rest.strip().split(' ', 1)[0]
838 try:
845 try:
839 ha = repo[ha].hex()
846 ha = repo[ha].hex()
840 except error.RepoError:
847 except error.RepoError:
841 raise util.Abort(_('unknown changeset %s listed') % ha[:12])
848 raise util.Abort(_('unknown changeset %s listed') % ha[:12])
842 if ha not in expected:
849 if ha not in expected:
843 raise util.Abort(
850 raise util.Abort(
844 _('may not use changesets other than the ones listed'))
851 _('may not use changesets other than the ones listed'))
845 if ha in seen:
852 if ha in seen:
846 raise util.Abort(_('duplicated command for changeset %s') %
853 raise util.Abort(_('duplicated command for changeset %s') %
847 ha[:12])
854 ha[:12])
848 seen.add(ha)
855 seen.add(ha)
849 if action not in actiontable:
856 if action not in actiontable:
850 raise util.Abort(_('unknown action "%s"') % action)
857 raise util.Abort(_('unknown action "%s"') % action)
851 parsed.append([action, ha])
858 parsed.append([action, ha])
852 missing = sorted(expected - seen) # sort to stabilize output
859 missing = sorted(expected - seen) # sort to stabilize output
853 if missing:
860 if missing:
854 raise util.Abort(_('missing rules for changeset %s') %
861 raise util.Abort(_('missing rules for changeset %s') %
855 missing[0][:12],
862 missing[0][:12],
856 hint=_('do you want to use the drop action?'))
863 hint=_('do you want to use the drop action?'))
857 return parsed
864 return parsed
858
865
859 def processreplacement(state):
866 def processreplacement(state):
860 """process the list of replacements to return
867 """process the list of replacements to return
861
868
862 1) the final mapping between original and created nodes
869 1) the final mapping between original and created nodes
863 2) the list of temporary node created by histedit
870 2) the list of temporary node created by histedit
864 3) the list of new commit created by histedit"""
871 3) the list of new commit created by histedit"""
865 replacements = state.replacements
872 replacements = state.replacements
866 allsuccs = set()
873 allsuccs = set()
867 replaced = set()
874 replaced = set()
868 fullmapping = {}
875 fullmapping = {}
869 # initialise basic set
876 # initialise basic set
870 # fullmapping record all operation recorded in replacement
877 # fullmapping record all operation recorded in replacement
871 for rep in replacements:
878 for rep in replacements:
872 allsuccs.update(rep[1])
879 allsuccs.update(rep[1])
873 replaced.add(rep[0])
880 replaced.add(rep[0])
874 fullmapping.setdefault(rep[0], set()).update(rep[1])
881 fullmapping.setdefault(rep[0], set()).update(rep[1])
875 new = allsuccs - replaced
882 new = allsuccs - replaced
876 tmpnodes = allsuccs & replaced
883 tmpnodes = allsuccs & replaced
877 # Reduce content fullmapping into direct relation between original nodes
884 # Reduce content fullmapping into direct relation between original nodes
878 # and final node created during history edition
885 # and final node created during history edition
879 # Dropped changeset are replaced by an empty list
886 # Dropped changeset are replaced by an empty list
880 toproceed = set(fullmapping)
887 toproceed = set(fullmapping)
881 final = {}
888 final = {}
882 while toproceed:
889 while toproceed:
883 for x in list(toproceed):
890 for x in list(toproceed):
884 succs = fullmapping[x]
891 succs = fullmapping[x]
885 for s in list(succs):
892 for s in list(succs):
886 if s in toproceed:
893 if s in toproceed:
887 # non final node with unknown closure
894 # non final node with unknown closure
888 # We can't process this now
895 # We can't process this now
889 break
896 break
890 elif s in final:
897 elif s in final:
891 # non final node, replace with closure
898 # non final node, replace with closure
892 succs.remove(s)
899 succs.remove(s)
893 succs.update(final[s])
900 succs.update(final[s])
894 else:
901 else:
895 final[x] = succs
902 final[x] = succs
896 toproceed.remove(x)
903 toproceed.remove(x)
897 # remove tmpnodes from final mapping
904 # remove tmpnodes from final mapping
898 for n in tmpnodes:
905 for n in tmpnodes:
899 del final[n]
906 del final[n]
900 # we expect all changes involved in final to exist in the repo
907 # we expect all changes involved in final to exist in the repo
901 # turn `final` into list (topologically sorted)
908 # turn `final` into list (topologically sorted)
902 nm = state.repo.changelog.nodemap
909 nm = state.repo.changelog.nodemap
903 for prec, succs in final.items():
910 for prec, succs in final.items():
904 final[prec] = sorted(succs, key=nm.get)
911 final[prec] = sorted(succs, key=nm.get)
905
912
906 # computed topmost element (necessary for bookmark)
913 # computed topmost element (necessary for bookmark)
907 if new:
914 if new:
908 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
915 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
909 elif not final:
916 elif not final:
910 # Nothing rewritten at all. we won't need `newtopmost`
917 # Nothing rewritten at all. we won't need `newtopmost`
911 # It is the same as `oldtopmost` and `processreplacement` know it
918 # It is the same as `oldtopmost` and `processreplacement` know it
912 newtopmost = None
919 newtopmost = None
913 else:
920 else:
914 # every body died. The newtopmost is the parent of the root.
921 # every body died. The newtopmost is the parent of the root.
915 r = state.repo.changelog.rev
922 r = state.repo.changelog.rev
916 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
923 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
917
924
918 return final, tmpnodes, new, newtopmost
925 return final, tmpnodes, new, newtopmost
919
926
920 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
927 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
921 """Move bookmark from old to newly created node"""
928 """Move bookmark from old to newly created node"""
922 if not mapping:
929 if not mapping:
923 # if nothing got rewritten there is not purpose for this function
930 # if nothing got rewritten there is not purpose for this function
924 return
931 return
925 moves = []
932 moves = []
926 for bk, old in sorted(repo._bookmarks.iteritems()):
933 for bk, old in sorted(repo._bookmarks.iteritems()):
927 if old == oldtopmost:
934 if old == oldtopmost:
928 # special case ensure bookmark stay on tip.
935 # special case ensure bookmark stay on tip.
929 #
936 #
930 # This is arguably a feature and we may only want that for the
937 # This is arguably a feature and we may only want that for the
931 # active bookmark. But the behavior is kept compatible with the old
938 # active bookmark. But the behavior is kept compatible with the old
932 # version for now.
939 # version for now.
933 moves.append((bk, newtopmost))
940 moves.append((bk, newtopmost))
934 continue
941 continue
935 base = old
942 base = old
936 new = mapping.get(base, None)
943 new = mapping.get(base, None)
937 if new is None:
944 if new is None:
938 continue
945 continue
939 while not new:
946 while not new:
940 # base is killed, trying with parent
947 # base is killed, trying with parent
941 base = repo[base].p1().node()
948 base = repo[base].p1().node()
942 new = mapping.get(base, (base,))
949 new = mapping.get(base, (base,))
943 # nothing to move
950 # nothing to move
944 moves.append((bk, new[-1]))
951 moves.append((bk, new[-1]))
945 if moves:
952 if moves:
946 marks = repo._bookmarks
953 marks = repo._bookmarks
947 for mark, new in moves:
954 for mark, new in moves:
948 old = marks[mark]
955 old = marks[mark]
949 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
956 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
950 % (mark, node.short(old), node.short(new)))
957 % (mark, node.short(old), node.short(new)))
951 marks[mark] = new
958 marks[mark] = new
952 marks.write()
959 marks.write()
953
960
954 def cleanupnode(ui, repo, name, nodes):
961 def cleanupnode(ui, repo, name, nodes):
955 """strip a group of nodes from the repository
962 """strip a group of nodes from the repository
956
963
957 The set of node to strip may contains unknown nodes."""
964 The set of node to strip may contains unknown nodes."""
958 ui.debug('should strip %s nodes %s\n' %
965 ui.debug('should strip %s nodes %s\n' %
959 (name, ', '.join([node.short(n) for n in nodes])))
966 (name, ', '.join([node.short(n) for n in nodes])))
960 lock = None
967 lock = None
961 try:
968 try:
962 lock = repo.lock()
969 lock = repo.lock()
963 # Find all node that need to be stripped
970 # Find all node that need to be stripped
964 # (we hg %lr instead of %ln to silently ignore unknown item
971 # (we hg %lr instead of %ln to silently ignore unknown item
965 nm = repo.changelog.nodemap
972 nm = repo.changelog.nodemap
966 nodes = sorted(n for n in nodes if n in nm)
973 nodes = sorted(n for n in nodes if n in nm)
967 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
974 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
968 for c in roots:
975 for c in roots:
969 # We should process node in reverse order to strip tip most first.
976 # We should process node in reverse order to strip tip most first.
970 # but this trigger a bug in changegroup hook.
977 # but this trigger a bug in changegroup hook.
971 # This would reduce bundle overhead
978 # This would reduce bundle overhead
972 repair.strip(ui, repo, c)
979 repair.strip(ui, repo, c)
973 finally:
980 finally:
974 release(lock)
981 release(lock)
975
982
976 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
983 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
977 if isinstance(nodelist, str):
984 if isinstance(nodelist, str):
978 nodelist = [nodelist]
985 nodelist = [nodelist]
979 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
986 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
980 state = histeditstate(repo)
987 state = histeditstate(repo)
981 state.read()
988 state.read()
982 histedit_nodes = set([ctx for (action, ctx) in state.rules])
989 histedit_nodes = set([ctx for (action, ctx) in state.rules])
983 strip_nodes = set([repo[n].hex() for n in nodelist])
990 strip_nodes = set([repo[n].hex() for n in nodelist])
984 common_nodes = histedit_nodes & strip_nodes
991 common_nodes = histedit_nodes & strip_nodes
985 if common_nodes:
992 if common_nodes:
986 raise util.Abort(_('unable to strip %s. Nodes are '
993 raise util.Abort(_('unable to strip %s. Nodes are '
987 'used by history edit in progress.')
994 'used by history edit in progress.')
988 % ', '.join(common_nodes))
995 % ', '.join(common_nodes))
989 return orig(ui, repo, nodelist, *args, **kwargs)
996 return orig(ui, repo, nodelist, *args, **kwargs)
990
997
991 extensions.wrapfunction(repair, 'strip', stripwrapper)
998 extensions.wrapfunction(repair, 'strip', stripwrapper)
992
999
993 def summaryhook(ui, repo):
1000 def summaryhook(ui, repo):
994 if not os.path.exists(repo.join('histedit-state')):
1001 if not os.path.exists(repo.join('histedit-state')):
995 return
1002 return
996 state = histeditstate(repo)
1003 state = histeditstate(repo)
997 state.read()
1004 state.read()
998 if state.rules:
1005 if state.rules:
999 # i18n: column positioning for "hg summary"
1006 # i18n: column positioning for "hg summary"
1000 ui.write(_('hist: %s (histedit --continue)\n') %
1007 ui.write(_('hist: %s (histedit --continue)\n') %
1001 (ui.label(_('%d remaining'), 'histedit.remaining') %
1008 (ui.label(_('%d remaining'), 'histedit.remaining') %
1002 len(state.rules)))
1009 len(state.rules)))
1003
1010
1004 def extsetup(ui):
1011 def extsetup(ui):
1005 cmdutil.summaryhooks.add('histedit', summaryhook)
1012 cmdutil.summaryhooks.add('histedit', summaryhook)
1006 cmdutil.unfinishedstates.append(
1013 cmdutil.unfinishedstates.append(
1007 ['histedit-state', False, True, _('histedit in progress'),
1014 ['histedit-state', False, True, _('histedit in progress'),
1008 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1015 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now