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