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