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