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