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