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