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