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