##// END OF EJS Templates
histedit: don't allow to strip nodes which are necessary to continue histedit...
Mateusz Kwapich -
r24111:11d72683 default
parent child Browse files
Show More
@@ -1,989 +1,1008
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 extensions
161 from mercurial import hg
162 from mercurial import hg
162 from mercurial import node
163 from mercurial import node
163 from mercurial import repair
164 from mercurial import repair
164 from mercurial import scmutil
165 from mercurial import scmutil
165 from mercurial import util
166 from mercurial import util
166 from mercurial import obsolete
167 from mercurial import obsolete
167 from mercurial import merge as mergemod
168 from mercurial import merge as mergemod
168 from mercurial.lock import release
169 from mercurial.lock import release
169 from mercurial.i18n import _
170 from mercurial.i18n import _
170
171
171 cmdtable = {}
172 cmdtable = {}
172 command = cmdutil.command(cmdtable)
173 command = cmdutil.command(cmdtable)
173
174
174 testedwith = 'internal'
175 testedwith = 'internal'
175
176
176 # i18n: command names and abbreviations must remain untranslated
177 # i18n: command names and abbreviations must remain untranslated
177 editcomment = _("""# Edit history between %s and %s
178 editcomment = _("""# Edit history between %s and %s
178 #
179 #
179 # Commits are listed from least to most recent
180 # Commits are listed from least to most recent
180 #
181 #
181 # Commands:
182 # Commands:
182 # p, pick = use commit
183 # p, pick = use commit
183 # e, edit = use commit, but stop for amending
184 # e, edit = use commit, but stop for amending
184 # f, fold = use commit, but combine it with the one above
185 # f, fold = use commit, but combine it with the one above
185 # r, roll = like fold, but discard this commit's description
186 # r, roll = like fold, but discard this commit's description
186 # d, drop = remove commit from history
187 # d, drop = remove commit from history
187 # m, mess = edit message without changing commit content
188 # m, mess = edit message without changing commit content
188 #
189 #
189 """)
190 """)
190
191
191 class histeditstate(object):
192 class histeditstate(object):
192 def __init__(self, repo, parentctx=None, rules=None, keep=None,
193 def __init__(self, repo, parentctx=None, rules=None, keep=None,
193 topmost=None, replacements=None, lock=None, wlock=None):
194 topmost=None, replacements=None, lock=None, wlock=None):
194 self.repo = repo
195 self.repo = repo
195 self.rules = rules
196 self.rules = rules
196 self.keep = keep
197 self.keep = keep
197 self.topmost = topmost
198 self.topmost = topmost
198 self.parentctx = parentctx
199 self.parentctx = parentctx
199 self.lock = lock
200 self.lock = lock
200 self.wlock = wlock
201 self.wlock = wlock
201 if replacements is None:
202 if replacements is None:
202 self.replacements = []
203 self.replacements = []
203 else:
204 else:
204 self.replacements = replacements
205 self.replacements = replacements
205
206
206 def read(self):
207 def read(self):
207 """Load histedit state from disk and set fields appropriately."""
208 """Load histedit state from disk and set fields appropriately."""
208 try:
209 try:
209 fp = self.repo.vfs('histedit-state', 'r')
210 fp = self.repo.vfs('histedit-state', 'r')
210 except IOError, err:
211 except IOError, err:
211 if err.errno != errno.ENOENT:
212 if err.errno != errno.ENOENT:
212 raise
213 raise
213 raise util.Abort(_('no histedit in progress'))
214 raise util.Abort(_('no histedit in progress'))
214
215
215 parentctxnode, rules, keep, topmost, replacements = pickle.load(fp)
216 parentctxnode, rules, keep, topmost, replacements = pickle.load(fp)
216
217
217 self.parentctx = self.repo[parentctxnode]
218 self.parentctx = self.repo[parentctxnode]
218 self.rules = rules
219 self.rules = rules
219 self.keep = keep
220 self.keep = keep
220 self.topmost = topmost
221 self.topmost = topmost
221 self.replacements = replacements
222 self.replacements = replacements
222
223
223 def write(self):
224 def write(self):
224 fp = self.repo.vfs('histedit-state', 'w')
225 fp = self.repo.vfs('histedit-state', 'w')
225 pickle.dump((self.parentctx.node(), self.rules, self.keep,
226 pickle.dump((self.parentctx.node(), self.rules, self.keep,
226 self.topmost, self.replacements), fp)
227 self.topmost, self.replacements), fp)
227 fp.close()
228 fp.close()
228
229
229 def clear(self):
230 def clear(self):
230 self.repo.vfs.unlink('histedit-state')
231 self.repo.vfs.unlink('histedit-state')
231
232
232 def commitfuncfor(repo, src):
233 def commitfuncfor(repo, src):
233 """Build a commit function for the replacement of <src>
234 """Build a commit function for the replacement of <src>
234
235
235 This function ensure we apply the same treatment to all changesets.
236 This function ensure we apply the same treatment to all changesets.
236
237
237 - Add a 'histedit_source' entry in extra.
238 - Add a 'histedit_source' entry in extra.
238
239
239 Note that fold have its own separated logic because its handling is a bit
240 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.
241 different and not easily factored out of the fold method.
241 """
242 """
242 phasemin = src.phase()
243 phasemin = src.phase()
243 def commitfunc(**kwargs):
244 def commitfunc(**kwargs):
244 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
245 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
245 try:
246 try:
246 repo.ui.setconfig('phases', 'new-commit', phasemin,
247 repo.ui.setconfig('phases', 'new-commit', phasemin,
247 'histedit')
248 'histedit')
248 extra = kwargs.get('extra', {}).copy()
249 extra = kwargs.get('extra', {}).copy()
249 extra['histedit_source'] = src.hex()
250 extra['histedit_source'] = src.hex()
250 kwargs['extra'] = extra
251 kwargs['extra'] = extra
251 return repo.commit(**kwargs)
252 return repo.commit(**kwargs)
252 finally:
253 finally:
253 repo.ui.restoreconfig(phasebackup)
254 repo.ui.restoreconfig(phasebackup)
254 return commitfunc
255 return commitfunc
255
256
256 def applychanges(ui, repo, ctx, opts):
257 def applychanges(ui, repo, ctx, opts):
257 """Merge changeset from ctx (only) in the current working directory"""
258 """Merge changeset from ctx (only) in the current working directory"""
258 wcpar = repo.dirstate.parents()[0]
259 wcpar = repo.dirstate.parents()[0]
259 if ctx.p1().node() == wcpar:
260 if ctx.p1().node() == wcpar:
260 # edition ar "in place" we do not need to make any merge,
261 # edition ar "in place" we do not need to make any merge,
261 # just applies changes on parent for edition
262 # just applies changes on parent for edition
262 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
263 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
263 stats = None
264 stats = None
264 else:
265 else:
265 try:
266 try:
266 # ui.forcemerge is an internal variable, do not document
267 # ui.forcemerge is an internal variable, do not document
267 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
268 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
268 'histedit')
269 'histedit')
269 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
270 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
270 finally:
271 finally:
271 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
272 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
272 return stats
273 return stats
273
274
274 def collapse(repo, first, last, commitopts):
275 def collapse(repo, first, last, commitopts):
275 """collapse the set of revisions from first to last as new one.
276 """collapse the set of revisions from first to last as new one.
276
277
277 Expected commit options are:
278 Expected commit options are:
278 - message
279 - message
279 - date
280 - date
280 - username
281 - username
281 Commit message is edited in all cases.
282 Commit message is edited in all cases.
282
283
283 This function works in memory."""
284 This function works in memory."""
284 ctxs = list(repo.set('%d::%d', first, last))
285 ctxs = list(repo.set('%d::%d', first, last))
285 if not ctxs:
286 if not ctxs:
286 return None
287 return None
287 base = first.parents()[0]
288 base = first.parents()[0]
288
289
289 # commit a new version of the old changeset, including the update
290 # commit a new version of the old changeset, including the update
290 # collect all files which might be affected
291 # collect all files which might be affected
291 files = set()
292 files = set()
292 for ctx in ctxs:
293 for ctx in ctxs:
293 files.update(ctx.files())
294 files.update(ctx.files())
294
295
295 # Recompute copies (avoid recording a -> b -> a)
296 # Recompute copies (avoid recording a -> b -> a)
296 copied = copies.pathcopies(base, last)
297 copied = copies.pathcopies(base, last)
297
298
298 # prune files which were reverted by the updates
299 # prune files which were reverted by the updates
299 def samefile(f):
300 def samefile(f):
300 if f in last.manifest():
301 if f in last.manifest():
301 a = last.filectx(f)
302 a = last.filectx(f)
302 if f in base.manifest():
303 if f in base.manifest():
303 b = base.filectx(f)
304 b = base.filectx(f)
304 return (a.data() == b.data()
305 return (a.data() == b.data()
305 and a.flags() == b.flags())
306 and a.flags() == b.flags())
306 else:
307 else:
307 return False
308 return False
308 else:
309 else:
309 return f not in base.manifest()
310 return f not in base.manifest()
310 files = [f for f in files if not samefile(f)]
311 files = [f for f in files if not samefile(f)]
311 # commit version of these files as defined by head
312 # commit version of these files as defined by head
312 headmf = last.manifest()
313 headmf = last.manifest()
313 def filectxfn(repo, ctx, path):
314 def filectxfn(repo, ctx, path):
314 if path in headmf:
315 if path in headmf:
315 fctx = last[path]
316 fctx = last[path]
316 flags = fctx.flags()
317 flags = fctx.flags()
317 mctx = context.memfilectx(repo,
318 mctx = context.memfilectx(repo,
318 fctx.path(), fctx.data(),
319 fctx.path(), fctx.data(),
319 islink='l' in flags,
320 islink='l' in flags,
320 isexec='x' in flags,
321 isexec='x' in flags,
321 copied=copied.get(path))
322 copied=copied.get(path))
322 return mctx
323 return mctx
323 return None
324 return None
324
325
325 if commitopts.get('message'):
326 if commitopts.get('message'):
326 message = commitopts['message']
327 message = commitopts['message']
327 else:
328 else:
328 message = first.description()
329 message = first.description()
329 user = commitopts.get('user')
330 user = commitopts.get('user')
330 date = commitopts.get('date')
331 date = commitopts.get('date')
331 extra = commitopts.get('extra')
332 extra = commitopts.get('extra')
332
333
333 parents = (first.p1().node(), first.p2().node())
334 parents = (first.p1().node(), first.p2().node())
334 editor = None
335 editor = None
335 if not commitopts.get('rollup'):
336 if not commitopts.get('rollup'):
336 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
337 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
337 new = context.memctx(repo,
338 new = context.memctx(repo,
338 parents=parents,
339 parents=parents,
339 text=message,
340 text=message,
340 files=files,
341 files=files,
341 filectxfn=filectxfn,
342 filectxfn=filectxfn,
342 user=user,
343 user=user,
343 date=date,
344 date=date,
344 extra=extra,
345 extra=extra,
345 editor=editor)
346 editor=editor)
346 return repo.commitctx(new)
347 return repo.commitctx(new)
347
348
348 def pick(ui, state, ha, opts):
349 def pick(ui, state, ha, opts):
349 repo, ctx = state.repo, state.parentctx
350 repo, ctx = state.repo, state.parentctx
350 oldctx = repo[ha]
351 oldctx = repo[ha]
351 if oldctx.parents()[0] == ctx:
352 if oldctx.parents()[0] == ctx:
352 ui.debug('node %s unchanged\n' % ha[:12])
353 ui.debug('node %s unchanged\n' % ha[:12])
353 return oldctx, []
354 return oldctx, []
354 hg.update(repo, ctx.node())
355 hg.update(repo, ctx.node())
355 stats = applychanges(ui, repo, oldctx, opts)
356 stats = applychanges(ui, repo, oldctx, opts)
356 if stats and stats[3] > 0:
357 if stats and stats[3] > 0:
357 raise error.InterventionRequired(_('Fix up the change and run '
358 raise error.InterventionRequired(_('Fix up the change and run '
358 'hg histedit --continue'))
359 'hg histedit --continue'))
359 # drop the second merge parent
360 # drop the second merge parent
360 commit = commitfuncfor(repo, oldctx)
361 commit = commitfuncfor(repo, oldctx)
361 n = commit(text=oldctx.description(), user=oldctx.user(),
362 n = commit(text=oldctx.description(), user=oldctx.user(),
362 date=oldctx.date(), extra=oldctx.extra())
363 date=oldctx.date(), extra=oldctx.extra())
363 if n is None:
364 if n is None:
364 ui.warn(_('%s: empty changeset\n') % ha[:12])
365 ui.warn(_('%s: empty changeset\n') % ha[:12])
365 return ctx, []
366 return ctx, []
366 new = repo[n]
367 new = repo[n]
367 return new, [(oldctx.node(), (n,))]
368 return new, [(oldctx.node(), (n,))]
368
369
369
370
370 def edit(ui, state, ha, opts):
371 def edit(ui, state, ha, opts):
371 repo, ctx = state.repo, state.parentctx
372 repo, ctx = state.repo, state.parentctx
372 oldctx = repo[ha]
373 oldctx = repo[ha]
373 hg.update(repo, ctx.node())
374 hg.update(repo, ctx.node())
374 applychanges(ui, repo, oldctx, opts)
375 applychanges(ui, repo, oldctx, opts)
375 raise error.InterventionRequired(
376 raise error.InterventionRequired(
376 _('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'
377 'When you are finished, run hg histedit --continue to resume.'))
378 'When you are finished, run hg histedit --continue to resume.'))
378
379
379 def rollup(ui, state, ha, opts):
380 def rollup(ui, state, ha, opts):
380 rollupopts = opts.copy()
381 rollupopts = opts.copy()
381 rollupopts['rollup'] = True
382 rollupopts['rollup'] = True
382 return fold(ui, state, ha, rollupopts)
383 return fold(ui, state, ha, rollupopts)
383
384
384 def fold(ui, state, ha, opts):
385 def fold(ui, state, ha, opts):
385 repo, ctx = state.repo, state.parentctx
386 repo, ctx = state.repo, state.parentctx
386 oldctx = repo[ha]
387 oldctx = repo[ha]
387 hg.update(repo, ctx.node())
388 hg.update(repo, ctx.node())
388 stats = applychanges(ui, repo, oldctx, opts)
389 stats = applychanges(ui, repo, oldctx, opts)
389 if stats and stats[3] > 0:
390 if stats and stats[3] > 0:
390 raise error.InterventionRequired(
391 raise error.InterventionRequired(
391 _('Fix up the change and run hg histedit --continue'))
392 _('Fix up the change and run hg histedit --continue'))
392 n = repo.commit(text='fold-temp-revision %s' % ha[:12], user=oldctx.user(),
393 n = repo.commit(text='fold-temp-revision %s' % ha[:12], user=oldctx.user(),
393 date=oldctx.date(), extra=oldctx.extra())
394 date=oldctx.date(), extra=oldctx.extra())
394 if n is None:
395 if n is None:
395 ui.warn(_('%s: empty changeset') % ha[:12])
396 ui.warn(_('%s: empty changeset') % ha[:12])
396 return ctx, []
397 return ctx, []
397 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
398 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
398
399
399 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
400 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
400 parent = ctx.parents()[0].node()
401 parent = ctx.parents()[0].node()
401 hg.update(repo, parent)
402 hg.update(repo, parent)
402 ### prepare new commit data
403 ### prepare new commit data
403 commitopts = opts.copy()
404 commitopts = opts.copy()
404 commitopts['user'] = ctx.user()
405 commitopts['user'] = ctx.user()
405 # commit message
406 # commit message
406 if opts.get('rollup'):
407 if opts.get('rollup'):
407 newmessage = ctx.description()
408 newmessage = ctx.description()
408 else:
409 else:
409 newmessage = '\n***\n'.join(
410 newmessage = '\n***\n'.join(
410 [ctx.description()] +
411 [ctx.description()] +
411 [repo[r].description() for r in internalchanges] +
412 [repo[r].description() for r in internalchanges] +
412 [oldctx.description()]) + '\n'
413 [oldctx.description()]) + '\n'
413 commitopts['message'] = newmessage
414 commitopts['message'] = newmessage
414 # date
415 # date
415 commitopts['date'] = max(ctx.date(), oldctx.date())
416 commitopts['date'] = max(ctx.date(), oldctx.date())
416 extra = ctx.extra().copy()
417 extra = ctx.extra().copy()
417 # histedit_source
418 # histedit_source
418 # note: ctx is likely a temporary commit but that the best we can do here
419 # note: ctx is likely a temporary commit but that the best we can do here
419 # This is sufficient to solve issue3681 anyway
420 # This is sufficient to solve issue3681 anyway
420 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
421 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
421 commitopts['extra'] = extra
422 commitopts['extra'] = extra
422 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
423 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
423 try:
424 try:
424 phasemin = max(ctx.phase(), oldctx.phase())
425 phasemin = max(ctx.phase(), oldctx.phase())
425 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
426 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
426 n = collapse(repo, ctx, repo[newnode], commitopts)
427 n = collapse(repo, ctx, repo[newnode], commitopts)
427 finally:
428 finally:
428 repo.ui.restoreconfig(phasebackup)
429 repo.ui.restoreconfig(phasebackup)
429 if n is None:
430 if n is None:
430 return ctx, []
431 return ctx, []
431 hg.update(repo, n)
432 hg.update(repo, n)
432 replacements = [(oldctx.node(), (newnode,)),
433 replacements = [(oldctx.node(), (newnode,)),
433 (ctx.node(), (n,)),
434 (ctx.node(), (n,)),
434 (newnode, (n,)),
435 (newnode, (n,)),
435 ]
436 ]
436 for ich in internalchanges:
437 for ich in internalchanges:
437 replacements.append((ich, (n,)))
438 replacements.append((ich, (n,)))
438 return repo[n], replacements
439 return repo[n], replacements
439
440
440 def drop(ui, state, ha, opts):
441 def drop(ui, state, ha, opts):
441 repo, ctx = state.repo, state.parentctx
442 repo, ctx = state.repo, state.parentctx
442 return ctx, [(repo[ha].node(), ())]
443 return ctx, [(repo[ha].node(), ())]
443
444
444
445
445 def message(ui, state, ha, opts):
446 def message(ui, state, ha, opts):
446 repo, ctx = state.repo, state.parentctx
447 repo, ctx = state.repo, state.parentctx
447 oldctx = repo[ha]
448 oldctx = repo[ha]
448 hg.update(repo, ctx.node())
449 hg.update(repo, ctx.node())
449 stats = applychanges(ui, repo, oldctx, opts)
450 stats = applychanges(ui, repo, oldctx, opts)
450 if stats and stats[3] > 0:
451 if stats and stats[3] > 0:
451 raise error.InterventionRequired(
452 raise error.InterventionRequired(
452 _('Fix up the change and run hg histedit --continue'))
453 _('Fix up the change and run hg histedit --continue'))
453 message = oldctx.description()
454 message = oldctx.description()
454 commit = commitfuncfor(repo, oldctx)
455 commit = commitfuncfor(repo, oldctx)
455 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
456 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
456 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
457 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
457 extra=oldctx.extra(), editor=editor)
458 extra=oldctx.extra(), editor=editor)
458 newctx = repo[new]
459 newctx = repo[new]
459 if oldctx.node() != newctx.node():
460 if oldctx.node() != newctx.node():
460 return newctx, [(oldctx.node(), (new,))]
461 return newctx, [(oldctx.node(), (new,))]
461 # We didn't make an edit, so just indicate no replaced nodes
462 # We didn't make an edit, so just indicate no replaced nodes
462 return newctx, []
463 return newctx, []
463
464
464 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
465 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
465 """utility function to find the first outgoing changeset
466 """utility function to find the first outgoing changeset
466
467
467 Used by initialisation code"""
468 Used by initialisation code"""
468 dest = ui.expandpath(remote or 'default-push', remote or 'default')
469 dest = ui.expandpath(remote or 'default-push', remote or 'default')
469 dest, revs = hg.parseurl(dest, None)[:2]
470 dest, revs = hg.parseurl(dest, None)[:2]
470 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
471 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
471
472
472 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
473 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
473 other = hg.peer(repo, opts, dest)
474 other = hg.peer(repo, opts, dest)
474
475
475 if revs:
476 if revs:
476 revs = [repo.lookup(rev) for rev in revs]
477 revs = [repo.lookup(rev) for rev in revs]
477
478
478 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
479 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
479 if not outgoing.missing:
480 if not outgoing.missing:
480 raise util.Abort(_('no outgoing ancestors'))
481 raise util.Abort(_('no outgoing ancestors'))
481 roots = list(repo.revs("roots(%ln)", outgoing.missing))
482 roots = list(repo.revs("roots(%ln)", outgoing.missing))
482 if 1 < len(roots):
483 if 1 < len(roots):
483 msg = _('there are ambiguous outgoing revisions')
484 msg = _('there are ambiguous outgoing revisions')
484 hint = _('see "hg help histedit" for more detail')
485 hint = _('see "hg help histedit" for more detail')
485 raise util.Abort(msg, hint=hint)
486 raise util.Abort(msg, hint=hint)
486 return repo.lookup(roots[0])
487 return repo.lookup(roots[0])
487
488
488 actiontable = {'p': pick,
489 actiontable = {'p': pick,
489 'pick': pick,
490 'pick': pick,
490 'e': edit,
491 'e': edit,
491 'edit': edit,
492 'edit': edit,
492 'f': fold,
493 'f': fold,
493 'fold': fold,
494 'fold': fold,
494 'r': rollup,
495 'r': rollup,
495 'roll': rollup,
496 'roll': rollup,
496 'd': drop,
497 'd': drop,
497 'drop': drop,
498 'drop': drop,
498 'm': message,
499 'm': message,
499 'mess': message,
500 'mess': message,
500 }
501 }
501
502
502 @command('histedit',
503 @command('histedit',
503 [('', 'commands', '',
504 [('', 'commands', '',
504 _('Read history edits from the specified file.')),
505 _('Read history edits from the specified file.')),
505 ('c', 'continue', False, _('continue an edit already in progress')),
506 ('c', 'continue', False, _('continue an edit already in progress')),
506 ('k', 'keep', False,
507 ('k', 'keep', False,
507 _("don't strip old nodes after edit is complete")),
508 _("don't strip old nodes after edit is complete")),
508 ('', 'abort', False, _('abort an edit in progress')),
509 ('', 'abort', False, _('abort an edit in progress')),
509 ('o', 'outgoing', False, _('changesets not found in destination')),
510 ('o', 'outgoing', False, _('changesets not found in destination')),
510 ('f', 'force', False,
511 ('f', 'force', False,
511 _('force outgoing even for unrelated repositories')),
512 _('force outgoing even for unrelated repositories')),
512 ('r', 'rev', [], _('first revision to be edited'))],
513 ('r', 'rev', [], _('first revision to be edited'))],
513 _("ANCESTOR | --outgoing [URL]"))
514 _("ANCESTOR | --outgoing [URL]"))
514 def histedit(ui, repo, *freeargs, **opts):
515 def histedit(ui, repo, *freeargs, **opts):
515 """interactively edit changeset history
516 """interactively edit changeset history
516
517
517 This command edits changesets between ANCESTOR and the parent of
518 This command edits changesets between ANCESTOR and the parent of
518 the working directory.
519 the working directory.
519
520
520 With --outgoing, this edits changesets not found in the
521 With --outgoing, this edits changesets not found in the
521 destination repository. If URL of the destination is omitted, the
522 destination repository. If URL of the destination is omitted, the
522 'default-push' (or 'default') path will be used.
523 'default-push' (or 'default') path will be used.
523
524
524 For safety, this command is aborted, also if there are ambiguous
525 For safety, this command is aborted, also if there are ambiguous
525 outgoing revisions which may confuse users: for example, there are
526 outgoing revisions which may confuse users: for example, there are
526 multiple branches containing outgoing revisions.
527 multiple branches containing outgoing revisions.
527
528
528 Use "min(outgoing() and ::.)" or similar revset specification
529 Use "min(outgoing() and ::.)" or similar revset specification
529 instead of --outgoing to specify edit target revision exactly in
530 instead of --outgoing to specify edit target revision exactly in
530 such ambiguous situation. See :hg:`help revsets` for detail about
531 such ambiguous situation. See :hg:`help revsets` for detail about
531 selecting revisions.
532 selecting revisions.
532
533
533 Returns 0 on success, 1 if user intervention is required (not only
534 Returns 0 on success, 1 if user intervention is required (not only
534 for intentional "edit" command, but also for resolving unexpected
535 for intentional "edit" command, but also for resolving unexpected
535 conflicts).
536 conflicts).
536 """
537 """
537 state = histeditstate(repo)
538 state = histeditstate(repo)
538 try:
539 try:
539 state.wlock = repo.wlock()
540 state.wlock = repo.wlock()
540 state.lock = repo.lock()
541 state.lock = repo.lock()
541 _histedit(ui, repo, state, *freeargs, **opts)
542 _histedit(ui, repo, state, *freeargs, **opts)
542 finally:
543 finally:
543 release(state.lock, state.wlock)
544 release(state.lock, state.wlock)
544
545
545 def _histedit(ui, repo, state, *freeargs, **opts):
546 def _histedit(ui, repo, state, *freeargs, **opts):
546 # TODO only abort if we try and histedit mq patches, not just
547 # TODO only abort if we try and histedit mq patches, not just
547 # blanket if mq patches are applied somewhere
548 # blanket if mq patches are applied somewhere
548 mq = getattr(repo, 'mq', None)
549 mq = getattr(repo, 'mq', None)
549 if mq and mq.applied:
550 if mq and mq.applied:
550 raise util.Abort(_('source has mq patches applied'))
551 raise util.Abort(_('source has mq patches applied'))
551
552
552 # basic argument incompatibility processing
553 # basic argument incompatibility processing
553 outg = opts.get('outgoing')
554 outg = opts.get('outgoing')
554 cont = opts.get('continue')
555 cont = opts.get('continue')
555 abort = opts.get('abort')
556 abort = opts.get('abort')
556 force = opts.get('force')
557 force = opts.get('force')
557 rules = opts.get('commands', '')
558 rules = opts.get('commands', '')
558 revs = opts.get('rev', [])
559 revs = opts.get('rev', [])
559 goal = 'new' # This invocation goal, in new, continue, abort
560 goal = 'new' # This invocation goal, in new, continue, abort
560 if force and not outg:
561 if force and not outg:
561 raise util.Abort(_('--force only allowed with --outgoing'))
562 raise util.Abort(_('--force only allowed with --outgoing'))
562 if cont:
563 if cont:
563 if util.any((outg, abort, revs, freeargs, rules)):
564 if util.any((outg, abort, revs, freeargs, rules)):
564 raise util.Abort(_('no arguments allowed with --continue'))
565 raise util.Abort(_('no arguments allowed with --continue'))
565 goal = 'continue'
566 goal = 'continue'
566 elif abort:
567 elif abort:
567 if util.any((outg, revs, freeargs, rules)):
568 if util.any((outg, revs, freeargs, rules)):
568 raise util.Abort(_('no arguments allowed with --abort'))
569 raise util.Abort(_('no arguments allowed with --abort'))
569 goal = 'abort'
570 goal = 'abort'
570 else:
571 else:
571 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
572 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
572 raise util.Abort(_('history edit already in progress, try '
573 raise util.Abort(_('history edit already in progress, try '
573 '--continue or --abort'))
574 '--continue or --abort'))
574 if outg:
575 if outg:
575 if revs:
576 if revs:
576 raise util.Abort(_('no revisions allowed with --outgoing'))
577 raise util.Abort(_('no revisions allowed with --outgoing'))
577 if len(freeargs) > 1:
578 if len(freeargs) > 1:
578 raise util.Abort(
579 raise util.Abort(
579 _('only one repo argument allowed with --outgoing'))
580 _('only one repo argument allowed with --outgoing'))
580 else:
581 else:
581 revs.extend(freeargs)
582 revs.extend(freeargs)
582 if len(revs) == 0:
583 if len(revs) == 0:
583 histeditdefault = ui.config('histedit', 'defaultrev')
584 histeditdefault = ui.config('histedit', 'defaultrev')
584 if histeditdefault:
585 if histeditdefault:
585 revs.append(histeditdefault)
586 revs.append(histeditdefault)
586 if len(revs) != 1:
587 if len(revs) != 1:
587 raise util.Abort(
588 raise util.Abort(
588 _('histedit requires exactly one ancestor revision'))
589 _('histedit requires exactly one ancestor revision'))
589
590
590
591
591 replacements = []
592 replacements = []
592 keep = opts.get('keep', False)
593 keep = opts.get('keep', False)
593
594
594 # rebuild state
595 # rebuild state
595 if goal == 'continue':
596 if goal == 'continue':
596 state = histeditstate(repo)
597 state = histeditstate(repo)
597 state.read()
598 state.read()
598 state = bootstrapcontinue(ui, state, opts)
599 state = bootstrapcontinue(ui, state, opts)
599 elif goal == 'abort':
600 elif goal == 'abort':
600 state = histeditstate(repo)
601 state = histeditstate(repo)
601 state.read()
602 state.read()
602 mapping, tmpnodes, leafs, _ntm = processreplacement(state)
603 mapping, tmpnodes, leafs, _ntm = processreplacement(state)
603 ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
604 ui.debug('restore wc to old parent %s\n' % node.short(state.topmost))
604 # check whether we should update away
605 # check whether we should update away
605 parentnodes = [c.node() for c in repo[None].parents()]
606 parentnodes = [c.node() for c in repo[None].parents()]
606 for n in leafs | set([state.parentctx.node()]):
607 for n in leafs | set([state.parentctx.node()]):
607 if n in parentnodes:
608 if n in parentnodes:
608 hg.clean(repo, state.topmost)
609 hg.clean(repo, state.topmost)
609 break
610 break
610 else:
611 else:
611 pass
612 pass
612 cleanupnode(ui, repo, 'created', tmpnodes)
613 cleanupnode(ui, repo, 'created', tmpnodes)
613 cleanupnode(ui, repo, 'temp', leafs)
614 cleanupnode(ui, repo, 'temp', leafs)
614 state.clear()
615 state.clear()
615 return
616 return
616 else:
617 else:
617 cmdutil.checkunfinished(repo)
618 cmdutil.checkunfinished(repo)
618 cmdutil.bailifchanged(repo)
619 cmdutil.bailifchanged(repo)
619
620
620 topmost, empty = repo.dirstate.parents()
621 topmost, empty = repo.dirstate.parents()
621 if outg:
622 if outg:
622 if freeargs:
623 if freeargs:
623 remote = freeargs[0]
624 remote = freeargs[0]
624 else:
625 else:
625 remote = None
626 remote = None
626 root = findoutgoing(ui, repo, remote, force, opts)
627 root = findoutgoing(ui, repo, remote, force, opts)
627 else:
628 else:
628 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
629 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
629 if len(rr) != 1:
630 if len(rr) != 1:
630 raise util.Abort(_('The specified revisions must have '
631 raise util.Abort(_('The specified revisions must have '
631 'exactly one common root'))
632 'exactly one common root'))
632 root = rr[0].node()
633 root = rr[0].node()
633
634
634 revs = between(repo, root, topmost, keep)
635 revs = between(repo, root, topmost, keep)
635 if not revs:
636 if not revs:
636 raise util.Abort(_('%s is not an ancestor of working directory') %
637 raise util.Abort(_('%s is not an ancestor of working directory') %
637 node.short(root))
638 node.short(root))
638
639
639 ctxs = [repo[r] for r in revs]
640 ctxs = [repo[r] for r in revs]
640 if not rules:
641 if not rules:
641 rules = '\n'.join([makedesc(c) for c in ctxs])
642 rules = '\n'.join([makedesc(c) for c in ctxs])
642 rules += '\n\n'
643 rules += '\n\n'
643 rules += editcomment % (node.short(root), node.short(topmost))
644 rules += editcomment % (node.short(root), node.short(topmost))
644 rules = ui.edit(rules, ui.username())
645 rules = ui.edit(rules, ui.username())
645 # Save edit rules in .hg/histedit-last-edit.txt in case
646 # Save edit rules in .hg/histedit-last-edit.txt in case
646 # the user needs to ask for help after something
647 # the user needs to ask for help after something
647 # surprising happens.
648 # surprising happens.
648 f = open(repo.join('histedit-last-edit.txt'), 'w')
649 f = open(repo.join('histedit-last-edit.txt'), 'w')
649 f.write(rules)
650 f.write(rules)
650 f.close()
651 f.close()
651 else:
652 else:
652 if rules == '-':
653 if rules == '-':
653 f = sys.stdin
654 f = sys.stdin
654 else:
655 else:
655 f = open(rules)
656 f = open(rules)
656 rules = f.read()
657 rules = f.read()
657 f.close()
658 f.close()
658 rules = [l for l in (r.strip() for r in rules.splitlines())
659 rules = [l for l in (r.strip() for r in rules.splitlines())
659 if l and not l.startswith('#')]
660 if l and not l.startswith('#')]
660 rules = verifyrules(rules, repo, ctxs)
661 rules = verifyrules(rules, repo, ctxs)
661
662
662 parentctx = repo[root].parents()[0]
663 parentctx = repo[root].parents()[0]
663
664
664 state.parentctx = parentctx
665 state.parentctx = parentctx
665 state.rules = rules
666 state.rules = rules
666 state.keep = keep
667 state.keep = keep
667 state.topmost = topmost
668 state.topmost = topmost
668 state.replacements = replacements
669 state.replacements = replacements
669
670
670 while state.rules:
671 while state.rules:
671 state.write()
672 state.write()
672 action, ha = state.rules.pop(0)
673 action, ha = state.rules.pop(0)
673 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
674 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
674 actfunc = actiontable[action]
675 actfunc = actiontable[action]
675 state.parentctx, replacement_ = actfunc(ui, state, ha, opts)
676 state.parentctx, replacement_ = actfunc(ui, state, ha, opts)
676 state.replacements.extend(replacement_)
677 state.replacements.extend(replacement_)
678 state.write()
677
679
678 hg.update(repo, state.parentctx.node())
680 hg.update(repo, state.parentctx.node())
679
681
680 mapping, tmpnodes, created, ntm = processreplacement(state)
682 mapping, tmpnodes, created, ntm = processreplacement(state)
681 if mapping:
683 if mapping:
682 for prec, succs in mapping.iteritems():
684 for prec, succs in mapping.iteritems():
683 if not succs:
685 if not succs:
684 ui.debug('histedit: %s is dropped\n' % node.short(prec))
686 ui.debug('histedit: %s is dropped\n' % node.short(prec))
685 else:
687 else:
686 ui.debug('histedit: %s is replaced by %s\n' % (
688 ui.debug('histedit: %s is replaced by %s\n' % (
687 node.short(prec), node.short(succs[0])))
689 node.short(prec), node.short(succs[0])))
688 if len(succs) > 1:
690 if len(succs) > 1:
689 m = 'histedit: %s'
691 m = 'histedit: %s'
690 for n in succs[1:]:
692 for n in succs[1:]:
691 ui.debug(m % node.short(n))
693 ui.debug(m % node.short(n))
692
694
693 if not keep:
695 if not keep:
694 if mapping:
696 if mapping:
695 movebookmarks(ui, repo, mapping, state.topmost, ntm)
697 movebookmarks(ui, repo, mapping, state.topmost, ntm)
696 # TODO update mq state
698 # TODO update mq state
697 if obsolete.isenabled(repo, obsolete.createmarkersopt):
699 if obsolete.isenabled(repo, obsolete.createmarkersopt):
698 markers = []
700 markers = []
699 # sort by revision number because it sound "right"
701 # sort by revision number because it sound "right"
700 for prec in sorted(mapping, key=repo.changelog.rev):
702 for prec in sorted(mapping, key=repo.changelog.rev):
701 succs = mapping[prec]
703 succs = mapping[prec]
702 markers.append((repo[prec],
704 markers.append((repo[prec],
703 tuple(repo[s] for s in succs)))
705 tuple(repo[s] for s in succs)))
704 if markers:
706 if markers:
705 obsolete.createmarkers(repo, markers)
707 obsolete.createmarkers(repo, markers)
706 else:
708 else:
707 cleanupnode(ui, repo, 'replaced', mapping)
709 cleanupnode(ui, repo, 'replaced', mapping)
708
710
709 cleanupnode(ui, repo, 'temp', tmpnodes)
711 cleanupnode(ui, repo, 'temp', tmpnodes)
710 state.clear()
712 state.clear()
711 if os.path.exists(repo.sjoin('undo')):
713 if os.path.exists(repo.sjoin('undo')):
712 os.unlink(repo.sjoin('undo'))
714 os.unlink(repo.sjoin('undo'))
713
715
714 def gatherchildren(repo, ctx):
716 def gatherchildren(repo, ctx):
715 # is there any new commit between the expected parent and "."
717 # is there any new commit between the expected parent and "."
716 #
718 #
717 # note: does not take non linear new change in account (but previous
719 # note: does not take non linear new change in account (but previous
718 # implementation didn't used them anyway (issue3655)
720 # implementation didn't used them anyway (issue3655)
719 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
721 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
720 if ctx.node() != node.nullid:
722 if ctx.node() != node.nullid:
721 if not newchildren:
723 if not newchildren:
722 # `ctx` should match but no result. This means that
724 # `ctx` should match but no result. This means that
723 # currentnode is not a descendant from ctx.
725 # currentnode is not a descendant from ctx.
724 msg = _('%s is not an ancestor of working directory')
726 msg = _('%s is not an ancestor of working directory')
725 hint = _('use "histedit --abort" to clear broken state')
727 hint = _('use "histedit --abort" to clear broken state')
726 raise util.Abort(msg % ctx, hint=hint)
728 raise util.Abort(msg % ctx, hint=hint)
727 newchildren.pop(0) # remove ctx
729 newchildren.pop(0) # remove ctx
728 return newchildren
730 return newchildren
729
731
730 def bootstrapcontinue(ui, state, opts):
732 def bootstrapcontinue(ui, state, opts):
731 repo, parentctx = state.repo, state.parentctx
733 repo, parentctx = state.repo, state.parentctx
732 action, currentnode = state.rules.pop(0)
734 action, currentnode = state.rules.pop(0)
733 ctx = repo[currentnode]
735 ctx = repo[currentnode]
734
736
735 newchildren = gatherchildren(repo, parentctx)
737 newchildren = gatherchildren(repo, parentctx)
736
738
737 # Commit dirty working directory if necessary
739 # Commit dirty working directory if necessary
738 new = None
740 new = None
739 s = repo.status()
741 s = repo.status()
740 if s.modified or s.added or s.removed or s.deleted:
742 if s.modified or s.added or s.removed or s.deleted:
741 # prepare the message for the commit to comes
743 # prepare the message for the commit to comes
742 if action in ('f', 'fold', 'r', 'roll'):
744 if action in ('f', 'fold', 'r', 'roll'):
743 message = 'fold-temp-revision %s' % currentnode[:12]
745 message = 'fold-temp-revision %s' % currentnode[:12]
744 else:
746 else:
745 message = ctx.description()
747 message = ctx.description()
746 editopt = action in ('e', 'edit', 'm', 'mess')
748 editopt = action in ('e', 'edit', 'm', 'mess')
747 canonaction = {'e': 'edit', 'm': 'mess', 'p': 'pick'}
749 canonaction = {'e': 'edit', 'm': 'mess', 'p': 'pick'}
748 editform = 'histedit.%s' % canonaction.get(action, action)
750 editform = 'histedit.%s' % canonaction.get(action, action)
749 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
751 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
750 commit = commitfuncfor(repo, ctx)
752 commit = commitfuncfor(repo, ctx)
751 new = commit(text=message, user=ctx.user(), date=ctx.date(),
753 new = commit(text=message, user=ctx.user(), date=ctx.date(),
752 extra=ctx.extra(), editor=editor)
754 extra=ctx.extra(), editor=editor)
753 if new is not None:
755 if new is not None:
754 newchildren.append(new)
756 newchildren.append(new)
755
757
756 replacements = []
758 replacements = []
757 # track replacements
759 # track replacements
758 if ctx.node() not in newchildren:
760 if ctx.node() not in newchildren:
759 # note: new children may be empty when the changeset is dropped.
761 # note: new children may be empty when the changeset is dropped.
760 # this happen e.g during conflicting pick where we revert content
762 # this happen e.g during conflicting pick where we revert content
761 # to parent.
763 # to parent.
762 replacements.append((ctx.node(), tuple(newchildren)))
764 replacements.append((ctx.node(), tuple(newchildren)))
763
765
764 if action in ('f', 'fold', 'r', 'roll'):
766 if action in ('f', 'fold', 'r', 'roll'):
765 if newchildren:
767 if newchildren:
766 # finalize fold operation if applicable
768 # finalize fold operation if applicable
767 if new is None:
769 if new is None:
768 new = newchildren[-1]
770 new = newchildren[-1]
769 else:
771 else:
770 newchildren.pop() # remove new from internal changes
772 newchildren.pop() # remove new from internal changes
771 foldopts = opts
773 foldopts = opts
772 if action in ('r', 'roll'):
774 if action in ('r', 'roll'):
773 foldopts = foldopts.copy()
775 foldopts = foldopts.copy()
774 foldopts['rollup'] = True
776 foldopts['rollup'] = True
775 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new,
777 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new,
776 foldopts, newchildren)
778 foldopts, newchildren)
777 replacements.extend(repl)
779 replacements.extend(repl)
778 else:
780 else:
779 # newchildren is empty if the fold did not result in any commit
781 # newchildren is empty if the fold did not result in any commit
780 # this happen when all folded change are discarded during the
782 # this happen when all folded change are discarded during the
781 # merge.
783 # merge.
782 replacements.append((ctx.node(), (parentctx.node(),)))
784 replacements.append((ctx.node(), (parentctx.node(),)))
783 elif newchildren:
785 elif newchildren:
784 # otherwise update "parentctx" before proceeding to further operation
786 # otherwise update "parentctx" before proceeding to further operation
785 parentctx = repo[newchildren[-1]]
787 parentctx = repo[newchildren[-1]]
786
788
787 state.parentctx = parentctx
789 state.parentctx = parentctx
788 state.replacements.extend(replacements)
790 state.replacements.extend(replacements)
789
791
790 return state
792 return state
791
793
792 def between(repo, old, new, keep):
794 def between(repo, old, new, keep):
793 """select and validate the set of revision to edit
795 """select and validate the set of revision to edit
794
796
795 When keep is false, the specified set can't have children."""
797 When keep is false, the specified set can't have children."""
796 ctxs = list(repo.set('%n::%n', old, new))
798 ctxs = list(repo.set('%n::%n', old, new))
797 if ctxs and not keep:
799 if ctxs and not keep:
798 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
800 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
799 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
801 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
800 raise util.Abort(_('cannot edit history that would orphan nodes'))
802 raise util.Abort(_('cannot edit history that would orphan nodes'))
801 if repo.revs('(%ld) and merge()', ctxs):
803 if repo.revs('(%ld) and merge()', ctxs):
802 raise util.Abort(_('cannot edit history that contains merges'))
804 raise util.Abort(_('cannot edit history that contains merges'))
803 root = ctxs[0] # list is already sorted by repo.set
805 root = ctxs[0] # list is already sorted by repo.set
804 if not root.mutable():
806 if not root.mutable():
805 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
807 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
806 return [c.node() for c in ctxs]
808 return [c.node() for c in ctxs]
807
809
808 def makedesc(c):
810 def makedesc(c):
809 """build a initial action line for a ctx `c`
811 """build a initial action line for a ctx `c`
810
812
811 line are in the form:
813 line are in the form:
812
814
813 pick <hash> <rev> <summary>
815 pick <hash> <rev> <summary>
814 """
816 """
815 summary = ''
817 summary = ''
816 if c.description():
818 if c.description():
817 summary = c.description().splitlines()[0]
819 summary = c.description().splitlines()[0]
818 line = 'pick %s %d %s' % (c, c.rev(), summary)
820 line = 'pick %s %d %s' % (c, c.rev(), summary)
819 # trim to 80 columns so it's not stupidly wide in my editor
821 # trim to 80 columns so it's not stupidly wide in my editor
820 return util.ellipsis(line, 80)
822 return util.ellipsis(line, 80)
821
823
822 def verifyrules(rules, repo, ctxs):
824 def verifyrules(rules, repo, ctxs):
823 """Verify that there exists exactly one edit rule per given changeset.
825 """Verify that there exists exactly one edit rule per given changeset.
824
826
825 Will abort if there are to many or too few rules, a malformed rule,
827 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.
828 or a rule on a changeset outside of the user-given range.
827 """
829 """
828 parsed = []
830 parsed = []
829 expected = set(c.hex() for c in ctxs)
831 expected = set(c.hex() for c in ctxs)
830 seen = set()
832 seen = set()
831 for r in rules:
833 for r in rules:
832 if ' ' not in r:
834 if ' ' not in r:
833 raise util.Abort(_('malformed line "%s"') % r)
835 raise util.Abort(_('malformed line "%s"') % r)
834 action, rest = r.split(' ', 1)
836 action, rest = r.split(' ', 1)
835 ha = rest.strip().split(' ', 1)[0]
837 ha = rest.strip().split(' ', 1)[0]
836 try:
838 try:
837 ha = repo[ha].hex()
839 ha = repo[ha].hex()
838 except error.RepoError:
840 except error.RepoError:
839 raise util.Abort(_('unknown changeset %s listed') % ha[:12])
841 raise util.Abort(_('unknown changeset %s listed') % ha[:12])
840 if ha not in expected:
842 if ha not in expected:
841 raise util.Abort(
843 raise util.Abort(
842 _('may not use changesets other than the ones listed'))
844 _('may not use changesets other than the ones listed'))
843 if ha in seen:
845 if ha in seen:
844 raise util.Abort(_('duplicated command for changeset %s') %
846 raise util.Abort(_('duplicated command for changeset %s') %
845 ha[:12])
847 ha[:12])
846 seen.add(ha)
848 seen.add(ha)
847 if action not in actiontable:
849 if action not in actiontable:
848 raise util.Abort(_('unknown action "%s"') % action)
850 raise util.Abort(_('unknown action "%s"') % action)
849 parsed.append([action, ha])
851 parsed.append([action, ha])
850 missing = sorted(expected - seen) # sort to stabilize output
852 missing = sorted(expected - seen) # sort to stabilize output
851 if missing:
853 if missing:
852 raise util.Abort(_('missing rules for changeset %s') %
854 raise util.Abort(_('missing rules for changeset %s') %
853 missing[0][:12],
855 missing[0][:12],
854 hint=_('do you want to use the drop action?'))
856 hint=_('do you want to use the drop action?'))
855 return parsed
857 return parsed
856
858
857 def processreplacement(state):
859 def processreplacement(state):
858 """process the list of replacements to return
860 """process the list of replacements to return
859
861
860 1) the final mapping between original and created nodes
862 1) the final mapping between original and created nodes
861 2) the list of temporary node created by histedit
863 2) the list of temporary node created by histedit
862 3) the list of new commit created by histedit"""
864 3) the list of new commit created by histedit"""
863 replacements = state.replacements
865 replacements = state.replacements
864 allsuccs = set()
866 allsuccs = set()
865 replaced = set()
867 replaced = set()
866 fullmapping = {}
868 fullmapping = {}
867 # initialise basic set
869 # initialise basic set
868 # fullmapping record all operation recorded in replacement
870 # fullmapping record all operation recorded in replacement
869 for rep in replacements:
871 for rep in replacements:
870 allsuccs.update(rep[1])
872 allsuccs.update(rep[1])
871 replaced.add(rep[0])
873 replaced.add(rep[0])
872 fullmapping.setdefault(rep[0], set()).update(rep[1])
874 fullmapping.setdefault(rep[0], set()).update(rep[1])
873 new = allsuccs - replaced
875 new = allsuccs - replaced
874 tmpnodes = allsuccs & replaced
876 tmpnodes = allsuccs & replaced
875 # Reduce content fullmapping into direct relation between original nodes
877 # Reduce content fullmapping into direct relation between original nodes
876 # and final node created during history edition
878 # and final node created during history edition
877 # Dropped changeset are replaced by an empty list
879 # Dropped changeset are replaced by an empty list
878 toproceed = set(fullmapping)
880 toproceed = set(fullmapping)
879 final = {}
881 final = {}
880 while toproceed:
882 while toproceed:
881 for x in list(toproceed):
883 for x in list(toproceed):
882 succs = fullmapping[x]
884 succs = fullmapping[x]
883 for s in list(succs):
885 for s in list(succs):
884 if s in toproceed:
886 if s in toproceed:
885 # non final node with unknown closure
887 # non final node with unknown closure
886 # We can't process this now
888 # We can't process this now
887 break
889 break
888 elif s in final:
890 elif s in final:
889 # non final node, replace with closure
891 # non final node, replace with closure
890 succs.remove(s)
892 succs.remove(s)
891 succs.update(final[s])
893 succs.update(final[s])
892 else:
894 else:
893 final[x] = succs
895 final[x] = succs
894 toproceed.remove(x)
896 toproceed.remove(x)
895 # remove tmpnodes from final mapping
897 # remove tmpnodes from final mapping
896 for n in tmpnodes:
898 for n in tmpnodes:
897 del final[n]
899 del final[n]
898 # we expect all changes involved in final to exist in the repo
900 # we expect all changes involved in final to exist in the repo
899 # turn `final` into list (topologically sorted)
901 # turn `final` into list (topologically sorted)
900 nm = state.repo.changelog.nodemap
902 nm = state.repo.changelog.nodemap
901 for prec, succs in final.items():
903 for prec, succs in final.items():
902 final[prec] = sorted(succs, key=nm.get)
904 final[prec] = sorted(succs, key=nm.get)
903
905
904 # computed topmost element (necessary for bookmark)
906 # computed topmost element (necessary for bookmark)
905 if new:
907 if new:
906 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
908 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
907 elif not final:
909 elif not final:
908 # Nothing rewritten at all. we won't need `newtopmost`
910 # Nothing rewritten at all. we won't need `newtopmost`
909 # It is the same as `oldtopmost` and `processreplacement` know it
911 # It is the same as `oldtopmost` and `processreplacement` know it
910 newtopmost = None
912 newtopmost = None
911 else:
913 else:
912 # every body died. The newtopmost is the parent of the root.
914 # every body died. The newtopmost is the parent of the root.
913 r = state.repo.changelog.rev
915 r = state.repo.changelog.rev
914 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
916 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
915
917
916 return final, tmpnodes, new, newtopmost
918 return final, tmpnodes, new, newtopmost
917
919
918 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
920 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
919 """Move bookmark from old to newly created node"""
921 """Move bookmark from old to newly created node"""
920 if not mapping:
922 if not mapping:
921 # if nothing got rewritten there is not purpose for this function
923 # if nothing got rewritten there is not purpose for this function
922 return
924 return
923 moves = []
925 moves = []
924 for bk, old in sorted(repo._bookmarks.iteritems()):
926 for bk, old in sorted(repo._bookmarks.iteritems()):
925 if old == oldtopmost:
927 if old == oldtopmost:
926 # special case ensure bookmark stay on tip.
928 # special case ensure bookmark stay on tip.
927 #
929 #
928 # This is arguably a feature and we may only want that for the
930 # This is arguably a feature and we may only want that for the
929 # active bookmark. But the behavior is kept compatible with the old
931 # active bookmark. But the behavior is kept compatible with the old
930 # version for now.
932 # version for now.
931 moves.append((bk, newtopmost))
933 moves.append((bk, newtopmost))
932 continue
934 continue
933 base = old
935 base = old
934 new = mapping.get(base, None)
936 new = mapping.get(base, None)
935 if new is None:
937 if new is None:
936 continue
938 continue
937 while not new:
939 while not new:
938 # base is killed, trying with parent
940 # base is killed, trying with parent
939 base = repo[base].p1().node()
941 base = repo[base].p1().node()
940 new = mapping.get(base, (base,))
942 new = mapping.get(base, (base,))
941 # nothing to move
943 # nothing to move
942 moves.append((bk, new[-1]))
944 moves.append((bk, new[-1]))
943 if moves:
945 if moves:
944 marks = repo._bookmarks
946 marks = repo._bookmarks
945 for mark, new in moves:
947 for mark, new in moves:
946 old = marks[mark]
948 old = marks[mark]
947 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
949 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
948 % (mark, node.short(old), node.short(new)))
950 % (mark, node.short(old), node.short(new)))
949 marks[mark] = new
951 marks[mark] = new
950 marks.write()
952 marks.write()
951
953
952 def cleanupnode(ui, repo, name, nodes):
954 def cleanupnode(ui, repo, name, nodes):
953 """strip a group of nodes from the repository
955 """strip a group of nodes from the repository
954
956
955 The set of node to strip may contains unknown nodes."""
957 The set of node to strip may contains unknown nodes."""
956 ui.debug('should strip %s nodes %s\n' %
958 ui.debug('should strip %s nodes %s\n' %
957 (name, ', '.join([node.short(n) for n in nodes])))
959 (name, ', '.join([node.short(n) for n in nodes])))
958 lock = None
960 lock = None
959 try:
961 try:
960 lock = repo.lock()
962 lock = repo.lock()
961 # Find all node that need to be stripped
963 # Find all node that need to be stripped
962 # (we hg %lr instead of %ln to silently ignore unknown item
964 # (we hg %lr instead of %ln to silently ignore unknown item
963 nm = repo.changelog.nodemap
965 nm = repo.changelog.nodemap
964 nodes = sorted(n for n in nodes if n in nm)
966 nodes = sorted(n for n in nodes if n in nm)
965 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
967 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
966 for c in roots:
968 for c in roots:
967 # We should process node in reverse order to strip tip most first.
969 # We should process node in reverse order to strip tip most first.
968 # but this trigger a bug in changegroup hook.
970 # but this trigger a bug in changegroup hook.
969 # This would reduce bundle overhead
971 # This would reduce bundle overhead
970 repair.strip(ui, repo, c)
972 repair.strip(ui, repo, c)
971 finally:
973 finally:
972 release(lock)
974 release(lock)
973
975
976 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
977 if isinstance(nodelist, str):
978 nodelist = [nodelist]
979 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
980 state = histeditstate(repo)
981 state.read()
982 histedit_nodes = set([ctx for (action, ctx) in state.rules])
983 strip_nodes = set([repo[n].hex() for n in nodelist])
984 common_nodes = histedit_nodes & strip_nodes
985 if common_nodes:
986 raise util.Abort(_('unable to strip %s. Nodes are '
987 'used by history edit in progress.')
988 % ', '.join(common_nodes))
989 return orig(ui, repo, nodelist, *args, **kwargs)
990
991 extensions.wrapfunction(repair, 'strip', stripwrapper)
992
974 def summaryhook(ui, repo):
993 def summaryhook(ui, repo):
975 if not os.path.exists(repo.join('histedit-state')):
994 if not os.path.exists(repo.join('histedit-state')):
976 return
995 return
977 state = histeditstate(repo)
996 state = histeditstate(repo)
978 state.read()
997 state.read()
979 if state.rules:
998 if state.rules:
980 # i18n: column positioning for "hg summary"
999 # i18n: column positioning for "hg summary"
981 ui.write(_('hist: %s (histedit --continue)\n') %
1000 ui.write(_('hist: %s (histedit --continue)\n') %
982 (ui.label(_('%d remaining'), 'histedit.remaining') %
1001 (ui.label(_('%d remaining'), 'histedit.remaining') %
983 len(state.rules)))
1002 len(state.rules)))
984
1003
985 def extsetup(ui):
1004 def extsetup(ui):
986 cmdutil.summaryhooks.add('histedit', summaryhook)
1005 cmdutil.summaryhooks.add('histedit', summaryhook)
987 cmdutil.unfinishedstates.append(
1006 cmdutil.unfinishedstates.append(
988 ['histedit-state', False, True, _('histedit in progress'),
1007 ['histedit-state', False, True, _('histedit in progress'),
989 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1008 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,345 +1,351
1 $ . "$TESTDIR/histedit-helpers.sh"
1 $ . "$TESTDIR/histedit-helpers.sh"
2
2
3 $ cat >> $HGRCPATH <<EOF
3 $ cat >> $HGRCPATH <<EOF
4 > [extensions]
4 > [extensions]
5 > histedit=
5 > histedit=
6 > strip=
6 > EOF
7 > EOF
7
8
8 $ initrepo ()
9 $ initrepo ()
9 > {
10 > {
10 > hg init r
11 > hg init r
11 > cd r
12 > cd r
12 > for x in a b c d e f ; do
13 > for x in a b c d e f ; do
13 > echo $x > $x
14 > echo $x > $x
14 > hg add $x
15 > hg add $x
15 > hg ci -m $x
16 > hg ci -m $x
16 > done
17 > done
17 > }
18 > }
18
19
19 $ initrepo
20 $ initrepo
20
21
21 log before edit
22 log before edit
22 $ hg log --graph
23 $ hg log --graph
23 @ changeset: 5:652413bf663e
24 @ changeset: 5:652413bf663e
24 | tag: tip
25 | tag: tip
25 | user: test
26 | user: test
26 | date: Thu Jan 01 00:00:00 1970 +0000
27 | date: Thu Jan 01 00:00:00 1970 +0000
27 | summary: f
28 | summary: f
28 |
29 |
29 o changeset: 4:e860deea161a
30 o changeset: 4:e860deea161a
30 | user: test
31 | user: test
31 | date: Thu Jan 01 00:00:00 1970 +0000
32 | date: Thu Jan 01 00:00:00 1970 +0000
32 | summary: e
33 | summary: e
33 |
34 |
34 o changeset: 3:055a42cdd887
35 o changeset: 3:055a42cdd887
35 | user: test
36 | user: test
36 | date: Thu Jan 01 00:00:00 1970 +0000
37 | date: Thu Jan 01 00:00:00 1970 +0000
37 | summary: d
38 | summary: d
38 |
39 |
39 o changeset: 2:177f92b77385
40 o changeset: 2:177f92b77385
40 | user: test
41 | user: test
41 | date: Thu Jan 01 00:00:00 1970 +0000
42 | date: Thu Jan 01 00:00:00 1970 +0000
42 | summary: c
43 | summary: c
43 |
44 |
44 o changeset: 1:d2ae7f538514
45 o changeset: 1:d2ae7f538514
45 | user: test
46 | user: test
46 | date: Thu Jan 01 00:00:00 1970 +0000
47 | date: Thu Jan 01 00:00:00 1970 +0000
47 | summary: b
48 | summary: b
48 |
49 |
49 o changeset: 0:cb9a9f314b8b
50 o changeset: 0:cb9a9f314b8b
50 user: test
51 user: test
51 date: Thu Jan 01 00:00:00 1970 +0000
52 date: Thu Jan 01 00:00:00 1970 +0000
52 summary: a
53 summary: a
53
54
54
55
55 edit the history
56 edit the history
56 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
57 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
57 > pick 177f92b77385 c
58 > pick 177f92b77385 c
58 > pick 055a42cdd887 d
59 > pick 055a42cdd887 d
59 > edit e860deea161a e
60 > edit e860deea161a e
60 > pick 652413bf663e f
61 > pick 652413bf663e f
61 > EOF
62 > EOF
62 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
63 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
63 Make changes as needed, you may commit or record as needed now.
64 Make changes as needed, you may commit or record as needed now.
64 When you are finished, run hg histedit --continue to resume.
65 When you are finished, run hg histedit --continue to resume.
65
66
66 Go at a random point and try to continue
67 Go at a random point and try to continue
67
68
68 $ hg id -n
69 $ hg id -n
69 3+
70 3+
70 $ hg up 0
71 $ hg up 0
71 abort: histedit in progress
72 abort: histedit in progress
72 (use 'hg histedit --continue' or 'hg histedit --abort')
73 (use 'hg histedit --continue' or 'hg histedit --abort')
73 [255]
74 [255]
74
75
76 Try to delete necessary commit
77 $ hg strip -r 652413bf663e
78 abort: unable to strip 652413bf663ef2a641cab26574e46d5f5a64a55a. Nodes are used by history edit in progress.
79 [255]
80
75 commit, then edit the revision
81 commit, then edit the revision
76 $ hg ci -m 'wat'
82 $ hg ci -m 'wat'
77 created new head
83 created new head
78 $ echo a > e
84 $ echo a > e
79 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
85 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
80 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
86 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
81 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
87 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
82
88
83 $ hg log --graph
89 $ hg log --graph
84 @ changeset: 6:b5f70786f9b0
90 @ changeset: 6:b5f70786f9b0
85 | tag: tip
91 | tag: tip
86 | user: test
92 | user: test
87 | date: Thu Jan 01 00:00:00 1970 +0000
93 | date: Thu Jan 01 00:00:00 1970 +0000
88 | summary: f
94 | summary: f
89 |
95 |
90 o changeset: 5:a5e1ba2f7afb
96 o changeset: 5:a5e1ba2f7afb
91 | user: test
97 | user: test
92 | date: Thu Jan 01 00:00:00 1970 +0000
98 | date: Thu Jan 01 00:00:00 1970 +0000
93 | summary: foobaz
99 | summary: foobaz
94 |
100 |
95 o changeset: 4:1a60820cd1f6
101 o changeset: 4:1a60820cd1f6
96 | user: test
102 | user: test
97 | date: Thu Jan 01 00:00:00 1970 +0000
103 | date: Thu Jan 01 00:00:00 1970 +0000
98 | summary: wat
104 | summary: wat
99 |
105 |
100 o changeset: 3:055a42cdd887
106 o changeset: 3:055a42cdd887
101 | user: test
107 | user: test
102 | date: Thu Jan 01 00:00:00 1970 +0000
108 | date: Thu Jan 01 00:00:00 1970 +0000
103 | summary: d
109 | summary: d
104 |
110 |
105 o changeset: 2:177f92b77385
111 o changeset: 2:177f92b77385
106 | user: test
112 | user: test
107 | date: Thu Jan 01 00:00:00 1970 +0000
113 | date: Thu Jan 01 00:00:00 1970 +0000
108 | summary: c
114 | summary: c
109 |
115 |
110 o changeset: 1:d2ae7f538514
116 o changeset: 1:d2ae7f538514
111 | user: test
117 | user: test
112 | date: Thu Jan 01 00:00:00 1970 +0000
118 | date: Thu Jan 01 00:00:00 1970 +0000
113 | summary: b
119 | summary: b
114 |
120 |
115 o changeset: 0:cb9a9f314b8b
121 o changeset: 0:cb9a9f314b8b
116 user: test
122 user: test
117 date: Thu Jan 01 00:00:00 1970 +0000
123 date: Thu Jan 01 00:00:00 1970 +0000
118 summary: a
124 summary: a
119
125
120
126
121 $ hg cat e
127 $ hg cat e
122 a
128 a
123
129
124 check histedit_source
130 check histedit_source
125
131
126 $ hg log --debug --rev 5
132 $ hg log --debug --rev 5
127 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
133 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
128 phase: draft
134 phase: draft
129 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
135 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
130 parent: -1:0000000000000000000000000000000000000000
136 parent: -1:0000000000000000000000000000000000000000
131 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
137 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
132 user: test
138 user: test
133 date: Thu Jan 01 00:00:00 1970 +0000
139 date: Thu Jan 01 00:00:00 1970 +0000
134 files: e
140 files: e
135 extra: branch=default
141 extra: branch=default
136 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
142 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
137 description:
143 description:
138 foobaz
144 foobaz
139
145
140
146
141
147
142 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
148 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
143 > edit b5f70786f9b0 f
149 > edit b5f70786f9b0 f
144 > EOF
150 > EOF
145 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
151 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
146 Make changes as needed, you may commit or record as needed now.
152 Make changes as needed, you may commit or record as needed now.
147 When you are finished, run hg histedit --continue to resume.
153 When you are finished, run hg histedit --continue to resume.
148 $ hg status
154 $ hg status
149 A f
155 A f
150
156
151 $ hg summary
157 $ hg summary
152 parent: 5:a5e1ba2f7afb
158 parent: 5:a5e1ba2f7afb
153 foobaz
159 foobaz
154 branch: default
160 branch: default
155 commit: 1 added (new branch head)
161 commit: 1 added (new branch head)
156 update: 1 new changesets (update)
162 update: 1 new changesets (update)
157 hist: 1 remaining (histedit --continue)
163 hist: 1 remaining (histedit --continue)
158
164
159 (test also that editor is invoked if histedit is continued for
165 (test also that editor is invoked if histedit is continued for
160 "edit" action)
166 "edit" action)
161
167
162 $ HGEDITOR='cat' hg histedit --continue
168 $ HGEDITOR='cat' hg histedit --continue
163 f
169 f
164
170
165
171
166 HG: Enter commit message. Lines beginning with 'HG:' are removed.
172 HG: Enter commit message. Lines beginning with 'HG:' are removed.
167 HG: Leave message empty to abort commit.
173 HG: Leave message empty to abort commit.
168 HG: --
174 HG: --
169 HG: user: test
175 HG: user: test
170 HG: branch 'default'
176 HG: branch 'default'
171 HG: added f
177 HG: added f
172 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
178 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
173 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-backup.hg (glob)
179 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-backup.hg (glob)
174
180
175 $ hg status
181 $ hg status
176
182
177 log after edit
183 log after edit
178 $ hg log --limit 1
184 $ hg log --limit 1
179 changeset: 6:a107ee126658
185 changeset: 6:a107ee126658
180 tag: tip
186 tag: tip
181 user: test
187 user: test
182 date: Thu Jan 01 00:00:00 1970 +0000
188 date: Thu Jan 01 00:00:00 1970 +0000
183 summary: f
189 summary: f
184
190
185
191
186 say we'll change the message, but don't.
192 say we'll change the message, but don't.
187 $ cat > ../edit.sh <<EOF
193 $ cat > ../edit.sh <<EOF
188 > cat "\$1" | sed s/pick/mess/ > tmp
194 > cat "\$1" | sed s/pick/mess/ > tmp
189 > mv tmp "\$1"
195 > mv tmp "\$1"
190 > EOF
196 > EOF
191 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
197 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
192 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
198 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
193 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
199 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
194 $ hg status
200 $ hg status
195 $ hg log --limit 1
201 $ hg log --limit 1
196 changeset: 6:1fd3b2fe7754
202 changeset: 6:1fd3b2fe7754
197 tag: tip
203 tag: tip
198 user: test
204 user: test
199 date: Thu Jan 01 00:00:00 1970 +0000
205 date: Thu Jan 01 00:00:00 1970 +0000
200 summary: f
206 summary: f
201
207
202
208
203 modify the message
209 modify the message
204
210
205 check saving last-message.txt, at first
211 check saving last-message.txt, at first
206
212
207 $ cat > $TESTTMP/commitfailure.py <<EOF
213 $ cat > $TESTTMP/commitfailure.py <<EOF
208 > from mercurial import util
214 > from mercurial import util
209 > def reposetup(ui, repo):
215 > def reposetup(ui, repo):
210 > class commitfailure(repo.__class__):
216 > class commitfailure(repo.__class__):
211 > def commit(self, *args, **kwargs):
217 > def commit(self, *args, **kwargs):
212 > raise util.Abort('emulating unexpected abort')
218 > raise util.Abort('emulating unexpected abort')
213 > repo.__class__ = commitfailure
219 > repo.__class__ = commitfailure
214 > EOF
220 > EOF
215 $ cat >> .hg/hgrc <<EOF
221 $ cat >> .hg/hgrc <<EOF
216 > [extensions]
222 > [extensions]
217 > # this failure occurs before editor invocation
223 > # this failure occurs before editor invocation
218 > commitfailure = $TESTTMP/commitfailure.py
224 > commitfailure = $TESTTMP/commitfailure.py
219 > EOF
225 > EOF
220
226
221 $ cat > $TESTTMP/editor.sh <<EOF
227 $ cat > $TESTTMP/editor.sh <<EOF
222 > echo "==== before editing"
228 > echo "==== before editing"
223 > cat \$1
229 > cat \$1
224 > echo "===="
230 > echo "===="
225 > echo "check saving last-message.txt" >> \$1
231 > echo "check saving last-message.txt" >> \$1
226 > EOF
232 > EOF
227
233
228 (test that editor is not invoked before transaction starting)
234 (test that editor is not invoked before transaction starting)
229
235
230 $ rm -f .hg/last-message.txt
236 $ rm -f .hg/last-message.txt
231 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
237 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
232 > mess 1fd3b2fe7754 f
238 > mess 1fd3b2fe7754 f
233 > EOF
239 > EOF
234 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
240 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
235 abort: emulating unexpected abort
241 abort: emulating unexpected abort
236 $ test -f .hg/last-message.txt
242 $ test -f .hg/last-message.txt
237 [1]
243 [1]
238
244
239 $ cat >> .hg/hgrc <<EOF
245 $ cat >> .hg/hgrc <<EOF
240 > [extensions]
246 > [extensions]
241 > commitfailure = !
247 > commitfailure = !
242 > EOF
248 > EOF
243 $ hg histedit --abort -q
249 $ hg histedit --abort -q
244
250
245 (test that editor is invoked and commit message is saved into
251 (test that editor is invoked and commit message is saved into
246 "last-message.txt")
252 "last-message.txt")
247
253
248 $ cat >> .hg/hgrc <<EOF
254 $ cat >> .hg/hgrc <<EOF
249 > [hooks]
255 > [hooks]
250 > # this failure occurs after editor invocation
256 > # this failure occurs after editor invocation
251 > pretxncommit.unexpectedabort = false
257 > pretxncommit.unexpectedabort = false
252 > EOF
258 > EOF
253
259
254 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
260 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
255 A f
261 A f
256
262
257 $ rm -f .hg/last-message.txt
263 $ rm -f .hg/last-message.txt
258 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
264 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
259 > mess 1fd3b2fe7754 f
265 > mess 1fd3b2fe7754 f
260 > EOF
266 > EOF
261 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
267 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
262 adding f
268 adding f
263 ==== before editing
269 ==== before editing
264 f
270 f
265
271
266
272
267 HG: Enter commit message. Lines beginning with 'HG:' are removed.
273 HG: Enter commit message. Lines beginning with 'HG:' are removed.
268 HG: Leave message empty to abort commit.
274 HG: Leave message empty to abort commit.
269 HG: --
275 HG: --
270 HG: user: test
276 HG: user: test
271 HG: branch 'default'
277 HG: branch 'default'
272 HG: added f
278 HG: added f
273 ====
279 ====
274 transaction abort!
280 transaction abort!
275 rollback completed
281 rollback completed
276 note: commit message saved in .hg/last-message.txt
282 note: commit message saved in .hg/last-message.txt
277 abort: pretxncommit.unexpectedabort hook exited with status 1
283 abort: pretxncommit.unexpectedabort hook exited with status 1
278 [255]
284 [255]
279 $ cat .hg/last-message.txt
285 $ cat .hg/last-message.txt
280 f
286 f
281
287
282
288
283 check saving last-message.txt
289 check saving last-message.txt
284
290
285 (test also that editor is invoked if histedit is continued for "message"
291 (test also that editor is invoked if histedit is continued for "message"
286 action)
292 action)
287
293
288 $ HGEDITOR=cat hg histedit --continue
294 $ HGEDITOR=cat hg histedit --continue
289 f
295 f
290
296
291
297
292 HG: Enter commit message. Lines beginning with 'HG:' are removed.
298 HG: Enter commit message. Lines beginning with 'HG:' are removed.
293 HG: Leave message empty to abort commit.
299 HG: Leave message empty to abort commit.
294 HG: --
300 HG: --
295 HG: user: test
301 HG: user: test
296 HG: branch 'default'
302 HG: branch 'default'
297 HG: added f
303 HG: added f
298 transaction abort!
304 transaction abort!
299 rollback completed
305 rollback completed
300 note: commit message saved in .hg/last-message.txt
306 note: commit message saved in .hg/last-message.txt
301 abort: pretxncommit.unexpectedabort hook exited with status 1
307 abort: pretxncommit.unexpectedabort hook exited with status 1
302 [255]
308 [255]
303
309
304 $ cat >> .hg/hgrc <<EOF
310 $ cat >> .hg/hgrc <<EOF
305 > [hooks]
311 > [hooks]
306 > pretxncommit.unexpectedabort =
312 > pretxncommit.unexpectedabort =
307 > EOF
313 > EOF
308 $ hg histedit --abort -q
314 $ hg histedit --abort -q
309
315
310 then, check "modify the message" itself
316 then, check "modify the message" itself
311
317
312 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
318 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
313 > mess 1fd3b2fe7754 f
319 > mess 1fd3b2fe7754 f
314 > EOF
320 > EOF
315 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
321 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
316 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
322 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
317 $ hg status
323 $ hg status
318 $ hg log --limit 1
324 $ hg log --limit 1
319 changeset: 6:62feedb1200e
325 changeset: 6:62feedb1200e
320 tag: tip
326 tag: tip
321 user: test
327 user: test
322 date: Thu Jan 01 00:00:00 1970 +0000
328 date: Thu Jan 01 00:00:00 1970 +0000
323 summary: f
329 summary: f
324
330
325
331
326 rollback should not work after a histedit
332 rollback should not work after a histedit
327 $ hg rollback
333 $ hg rollback
328 no rollback information available
334 no rollback information available
329 [1]
335 [1]
330
336
331 $ cd ..
337 $ cd ..
332 $ hg clone -qr0 r r0
338 $ hg clone -qr0 r r0
333 $ cd r0
339 $ cd r0
334 $ hg phase -fdr0
340 $ hg phase -fdr0
335 $ hg histedit --commands - 0 2>&1 << EOF
341 $ hg histedit --commands - 0 2>&1 << EOF
336 > edit cb9a9f314b8b a > $EDITED
342 > edit cb9a9f314b8b a > $EDITED
337 > EOF
343 > EOF
338 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
344 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
339 adding a
345 adding a
340 Make changes as needed, you may commit or record as needed now.
346 Make changes as needed, you may commit or record as needed now.
341 When you are finished, run hg histedit --continue to resume.
347 When you are finished, run hg histedit --continue to resume.
342 [1]
348 [1]
343 $ HGEDITOR=true hg histedit --continue
349 $ HGEDITOR=true hg histedit --continue
344 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
350 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
345 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-backup.hg (glob)
351 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-backup.hg (glob)
General Comments 0
You need to be logged in to leave comments. Login now