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