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