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