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