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