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