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