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