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