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