##// END OF EJS Templates
histedit: allow operation from non-head if obsolete is enabled...
Pierre-Yves David -
r18165:0f5a0a20 default
parent child Browse files
Show More
@@ -1,770 +1,771 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 633536316234 and 7c2fd3b9020c
31 # Edit history between 633536316234 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 633536316234 and 7c2fd3b9020c
50 # Edit history between 633536316234 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 example repository above and add three more changes, such that
111 If we clone the example repository above and add three more changes, such that
112 we have the following history::
112 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 except ImportError:
143 except ImportError:
144 import pickle
144 import pickle
145 import os
145 import os
146
146
147 from mercurial import cmdutil
147 from mercurial import cmdutil
148 from mercurial import discovery
148 from mercurial import discovery
149 from mercurial import error
149 from mercurial import error
150 from mercurial import copies
150 from mercurial import copies
151 from mercurial import context
151 from mercurial import context
152 from mercurial import hg
152 from mercurial import hg
153 from mercurial import lock as lockmod
153 from mercurial import lock as lockmod
154 from mercurial import node
154 from mercurial import node
155 from mercurial import repair
155 from mercurial import repair
156 from mercurial import scmutil
156 from mercurial import scmutil
157 from mercurial import util
157 from mercurial import util
158 from mercurial import obsolete
158 from mercurial import obsolete
159 from mercurial import merge as mergemod
159 from mercurial import merge as mergemod
160 from mercurial.i18n import _
160 from mercurial.i18n import _
161
161
162 cmdtable = {}
162 cmdtable = {}
163 command = cmdutil.command(cmdtable)
163 command = cmdutil.command(cmdtable)
164
164
165 testedwith = 'internal'
165 testedwith = 'internal'
166
166
167 # i18n: command names and abbreviations must remain untranslated
167 # i18n: command names and abbreviations must remain untranslated
168 editcomment = _("""# Edit history between %s and %s
168 editcomment = _("""# Edit history between %s and %s
169 #
169 #
170 # Commands:
170 # Commands:
171 # p, pick = use commit
171 # p, pick = use commit
172 # e, edit = use commit, but stop for amending
172 # e, edit = use commit, but stop for amending
173 # f, fold = use commit, but fold into previous commit (combines N and N-1)
173 # f, fold = use commit, but fold into previous commit (combines N and N-1)
174 # d, drop = remove commit from history
174 # d, drop = remove commit from history
175 # m, mess = edit message without changing commit content
175 # m, mess = edit message without changing commit content
176 #
176 #
177 """)
177 """)
178
178
179 def applychanges(ui, repo, ctx, opts):
179 def applychanges(ui, repo, ctx, opts):
180 """Merge changeset from ctx (only) in the current working directory"""
180 """Merge changeset from ctx (only) in the current working directory"""
181 wcpar = repo.dirstate.parents()[0]
181 wcpar = repo.dirstate.parents()[0]
182 if ctx.p1().node() == wcpar:
182 if ctx.p1().node() == wcpar:
183 # edition ar "in place" we do not need to make any merge,
183 # edition ar "in place" we do not need to make any merge,
184 # just applies changes on parent for edition
184 # just applies changes on parent for edition
185 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
185 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
186 stats = None
186 stats = None
187 else:
187 else:
188 try:
188 try:
189 # ui.forcemerge is an internal variable, do not document
189 # ui.forcemerge is an internal variable, do not document
190 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
190 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
191 stats = mergemod.update(repo, ctx.node(), True, True, False,
191 stats = mergemod.update(repo, ctx.node(), True, True, False,
192 ctx.p1().node())
192 ctx.p1().node())
193 finally:
193 finally:
194 repo.ui.setconfig('ui', 'forcemerge', '')
194 repo.ui.setconfig('ui', 'forcemerge', '')
195 repo.setparents(wcpar, node.nullid)
195 repo.setparents(wcpar, node.nullid)
196 repo.dirstate.write()
196 repo.dirstate.write()
197 # fix up dirstate for copies and renames
197 # fix up dirstate for copies and renames
198 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
198 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
199 return stats
199 return stats
200
200
201 def collapse(repo, first, last, commitopts):
201 def collapse(repo, first, last, commitopts):
202 """collapse the set of revisions from first to last as new one.
202 """collapse the set of revisions from first to last as new one.
203
203
204 Expected commit options are:
204 Expected commit options are:
205 - message
205 - message
206 - date
206 - date
207 - username
207 - username
208 Commit message is edited in all cases.
208 Commit message is edited in all cases.
209
209
210 This function works in memory."""
210 This function works in memory."""
211 ctxs = list(repo.set('%d::%d', first, last))
211 ctxs = list(repo.set('%d::%d', first, last))
212 if not ctxs:
212 if not ctxs:
213 return None
213 return None
214 base = first.parents()[0]
214 base = first.parents()[0]
215
215
216 # commit a new version of the old changeset, including the update
216 # commit a new version of the old changeset, including the update
217 # collect all files which might be affected
217 # collect all files which might be affected
218 files = set()
218 files = set()
219 for ctx in ctxs:
219 for ctx in ctxs:
220 files.update(ctx.files())
220 files.update(ctx.files())
221
221
222 # Recompute copies (avoid recording a -> b -> a)
222 # Recompute copies (avoid recording a -> b -> a)
223 copied = copies.pathcopies(first, last)
223 copied = copies.pathcopies(first, last)
224
224
225 # prune files which were reverted by the updates
225 # prune files which were reverted by the updates
226 def samefile(f):
226 def samefile(f):
227 if f in last.manifest():
227 if f in last.manifest():
228 a = last.filectx(f)
228 a = last.filectx(f)
229 if f in base.manifest():
229 if f in base.manifest():
230 b = base.filectx(f)
230 b = base.filectx(f)
231 return (a.data() == b.data()
231 return (a.data() == b.data()
232 and a.flags() == b.flags())
232 and a.flags() == b.flags())
233 else:
233 else:
234 return False
234 return False
235 else:
235 else:
236 return f not in base.manifest()
236 return f not in base.manifest()
237 files = [f for f in files if not samefile(f)]
237 files = [f for f in files if not samefile(f)]
238 # commit version of these files as defined by head
238 # commit version of these files as defined by head
239 headmf = last.manifest()
239 headmf = last.manifest()
240 def filectxfn(repo, ctx, path):
240 def filectxfn(repo, ctx, path):
241 if path in headmf:
241 if path in headmf:
242 fctx = last[path]
242 fctx = last[path]
243 flags = fctx.flags()
243 flags = fctx.flags()
244 mctx = context.memfilectx(fctx.path(), fctx.data(),
244 mctx = context.memfilectx(fctx.path(), fctx.data(),
245 islink='l' in flags,
245 islink='l' in flags,
246 isexec='x' in flags,
246 isexec='x' in flags,
247 copied=copied.get(path))
247 copied=copied.get(path))
248 return mctx
248 return mctx
249 raise IOError()
249 raise IOError()
250
250
251 if commitopts.get('message'):
251 if commitopts.get('message'):
252 message = commitopts['message']
252 message = commitopts['message']
253 else:
253 else:
254 message = first.description()
254 message = first.description()
255 user = commitopts.get('user')
255 user = commitopts.get('user')
256 date = commitopts.get('date')
256 date = commitopts.get('date')
257 extra = first.extra()
257 extra = first.extra()
258
258
259 parents = (first.p1().node(), first.p2().node())
259 parents = (first.p1().node(), first.p2().node())
260 new = context.memctx(repo,
260 new = context.memctx(repo,
261 parents=parents,
261 parents=parents,
262 text=message,
262 text=message,
263 files=files,
263 files=files,
264 filectxfn=filectxfn,
264 filectxfn=filectxfn,
265 user=user,
265 user=user,
266 date=date,
266 date=date,
267 extra=extra)
267 extra=extra)
268 new._text = cmdutil.commitforceeditor(repo, new, [])
268 new._text = cmdutil.commitforceeditor(repo, new, [])
269 return repo.commitctx(new)
269 return repo.commitctx(new)
270
270
271 def pick(ui, repo, ctx, ha, opts):
271 def pick(ui, repo, ctx, ha, opts):
272 oldctx = repo[ha]
272 oldctx = repo[ha]
273 if oldctx.parents()[0] == ctx:
273 if oldctx.parents()[0] == ctx:
274 ui.debug('node %s unchanged\n' % ha)
274 ui.debug('node %s unchanged\n' % ha)
275 return oldctx, []
275 return oldctx, []
276 hg.update(repo, ctx.node())
276 hg.update(repo, ctx.node())
277 stats = applychanges(ui, repo, oldctx, opts)
277 stats = applychanges(ui, repo, oldctx, opts)
278 if stats and stats[3] > 0:
278 if stats and stats[3] > 0:
279 raise util.Abort(_('Fix up the change and run '
279 raise util.Abort(_('Fix up the change and run '
280 'hg histedit --continue'))
280 'hg histedit --continue'))
281 # drop the second merge parent
281 # drop the second merge parent
282 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
282 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
283 date=oldctx.date(), extra=oldctx.extra())
283 date=oldctx.date(), extra=oldctx.extra())
284 if n is None:
284 if n is None:
285 ui.warn(_('%s: empty changeset\n')
285 ui.warn(_('%s: empty changeset\n')
286 % node.hex(ha))
286 % node.hex(ha))
287 return ctx, []
287 return ctx, []
288 new = repo[n]
288 new = repo[n]
289 return new, [(oldctx.node(), (n,))]
289 return new, [(oldctx.node(), (n,))]
290
290
291
291
292 def edit(ui, repo, ctx, ha, opts):
292 def edit(ui, repo, ctx, ha, opts):
293 oldctx = repo[ha]
293 oldctx = repo[ha]
294 hg.update(repo, ctx.node())
294 hg.update(repo, ctx.node())
295 applychanges(ui, repo, oldctx, opts)
295 applychanges(ui, repo, oldctx, opts)
296 raise util.Abort(_('Make changes as needed, you may commit or record as '
296 raise util.Abort(_('Make changes as needed, you may commit or record as '
297 'needed now.\nWhen you are finished, run hg'
297 'needed now.\nWhen you are finished, run hg'
298 ' histedit --continue to resume.'))
298 ' histedit --continue to resume.'))
299
299
300 def fold(ui, repo, ctx, ha, opts):
300 def fold(ui, repo, ctx, ha, opts):
301 oldctx = repo[ha]
301 oldctx = repo[ha]
302 hg.update(repo, ctx.node())
302 hg.update(repo, ctx.node())
303 stats = applychanges(ui, repo, oldctx, opts)
303 stats = applychanges(ui, repo, oldctx, opts)
304 if stats and stats[3] > 0:
304 if stats and stats[3] > 0:
305 raise util.Abort(_('Fix up the change and run '
305 raise util.Abort(_('Fix up the change and run '
306 'hg histedit --continue'))
306 'hg histedit --continue'))
307 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
307 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
308 date=oldctx.date(), extra=oldctx.extra())
308 date=oldctx.date(), extra=oldctx.extra())
309 if n is None:
309 if n is None:
310 ui.warn(_('%s: empty changeset')
310 ui.warn(_('%s: empty changeset')
311 % node.hex(ha))
311 % node.hex(ha))
312 return ctx, []
312 return ctx, []
313 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
313 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
314
314
315 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
315 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
316 parent = ctx.parents()[0].node()
316 parent = ctx.parents()[0].node()
317 hg.update(repo, parent)
317 hg.update(repo, parent)
318 ### prepare new commit data
318 ### prepare new commit data
319 commitopts = opts.copy()
319 commitopts = opts.copy()
320 # username
320 # username
321 if ctx.user() == oldctx.user():
321 if ctx.user() == oldctx.user():
322 username = ctx.user()
322 username = ctx.user()
323 else:
323 else:
324 username = ui.username()
324 username = ui.username()
325 commitopts['user'] = username
325 commitopts['user'] = username
326 # commit message
326 # commit message
327 newmessage = '\n***\n'.join(
327 newmessage = '\n***\n'.join(
328 [ctx.description()] +
328 [ctx.description()] +
329 [repo[r].description() for r in internalchanges] +
329 [repo[r].description() for r in internalchanges] +
330 [oldctx.description()]) + '\n'
330 [oldctx.description()]) + '\n'
331 commitopts['message'] = newmessage
331 commitopts['message'] = newmessage
332 # date
332 # date
333 commitopts['date'] = max(ctx.date(), oldctx.date())
333 commitopts['date'] = max(ctx.date(), oldctx.date())
334 n = collapse(repo, ctx, repo[newnode], commitopts)
334 n = collapse(repo, ctx, repo[newnode], commitopts)
335 if n is None:
335 if n is None:
336 return ctx, []
336 return ctx, []
337 hg.update(repo, n)
337 hg.update(repo, n)
338 replacements = [(oldctx.node(), (newnode,)),
338 replacements = [(oldctx.node(), (newnode,)),
339 (ctx.node(), (n,)),
339 (ctx.node(), (n,)),
340 (newnode, (n,)),
340 (newnode, (n,)),
341 ]
341 ]
342 for ich in internalchanges:
342 for ich in internalchanges:
343 replacements.append((ich, (n,)))
343 replacements.append((ich, (n,)))
344 return repo[n], replacements
344 return repo[n], replacements
345
345
346 def drop(ui, repo, ctx, ha, opts):
346 def drop(ui, repo, ctx, ha, opts):
347 return ctx, [(repo[ha].node(), ())]
347 return ctx, [(repo[ha].node(), ())]
348
348
349
349
350 def message(ui, repo, ctx, ha, opts):
350 def message(ui, repo, ctx, ha, opts):
351 oldctx = repo[ha]
351 oldctx = repo[ha]
352 hg.update(repo, ctx.node())
352 hg.update(repo, ctx.node())
353 stats = applychanges(ui, repo, oldctx, opts)
353 stats = applychanges(ui, repo, oldctx, opts)
354 if stats and stats[3] > 0:
354 if stats and stats[3] > 0:
355 raise util.Abort(_('Fix up the change and run '
355 raise util.Abort(_('Fix up the change and run '
356 'hg histedit --continue'))
356 'hg histedit --continue'))
357 message = oldctx.description() + '\n'
357 message = oldctx.description() + '\n'
358 message = ui.edit(message, ui.username())
358 message = ui.edit(message, ui.username())
359 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
359 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
360 extra=oldctx.extra())
360 extra=oldctx.extra())
361 newctx = repo[new]
361 newctx = repo[new]
362 if oldctx.node() != newctx.node():
362 if oldctx.node() != newctx.node():
363 return newctx, [(oldctx.node(), (new,))]
363 return newctx, [(oldctx.node(), (new,))]
364 # We didn't make an edit, so just indicate no replaced nodes
364 # We didn't make an edit, so just indicate no replaced nodes
365 return newctx, []
365 return newctx, []
366
366
367 actiontable = {'p': pick,
367 actiontable = {'p': pick,
368 'pick': pick,
368 'pick': pick,
369 'e': edit,
369 'e': edit,
370 'edit': edit,
370 'edit': edit,
371 'f': fold,
371 'f': fold,
372 'fold': fold,
372 'fold': fold,
373 'd': drop,
373 'd': drop,
374 'drop': drop,
374 'drop': drop,
375 'm': message,
375 'm': message,
376 'mess': message,
376 'mess': message,
377 }
377 }
378
378
379 @command('histedit',
379 @command('histedit',
380 [('', 'commands', '',
380 [('', 'commands', '',
381 _('Read history edits from the specified file.')),
381 _('Read history edits from the specified file.')),
382 ('c', 'continue', False, _('continue an edit already in progress')),
382 ('c', 'continue', False, _('continue an edit already in progress')),
383 ('k', 'keep', False,
383 ('k', 'keep', False,
384 _("don't strip old nodes after edit is complete")),
384 _("don't strip old nodes after edit is complete")),
385 ('', 'abort', False, _('abort an edit in progress')),
385 ('', 'abort', False, _('abort an edit in progress')),
386 ('o', 'outgoing', False, _('changesets not found in destination')),
386 ('o', 'outgoing', False, _('changesets not found in destination')),
387 ('f', 'force', False,
387 ('f', 'force', False,
388 _('force outgoing even for unrelated repositories')),
388 _('force outgoing even for unrelated repositories')),
389 ('r', 'rev', [], _('first revision to be edited'))],
389 ('r', 'rev', [], _('first revision to be edited'))],
390 _("[PARENT]"))
390 _("[PARENT]"))
391 def histedit(ui, repo, *parent, **opts):
391 def histedit(ui, repo, *parent, **opts):
392 """interactively edit changeset history
392 """interactively edit changeset history
393 """
393 """
394 # TODO only abort if we try and histedit mq patches, not just
394 # TODO only abort if we try and histedit mq patches, not just
395 # blanket if mq patches are applied somewhere
395 # blanket if mq patches are applied somewhere
396 mq = getattr(repo, 'mq', None)
396 mq = getattr(repo, 'mq', None)
397 if mq and mq.applied:
397 if mq and mq.applied:
398 raise util.Abort(_('source has mq patches applied'))
398 raise util.Abort(_('source has mq patches applied'))
399
399
400 parent = list(parent) + opts.get('rev', [])
400 parent = list(parent) + opts.get('rev', [])
401 if opts.get('outgoing'):
401 if opts.get('outgoing'):
402 if len(parent) > 1:
402 if len(parent) > 1:
403 raise util.Abort(
403 raise util.Abort(
404 _('only one repo argument allowed with --outgoing'))
404 _('only one repo argument allowed with --outgoing'))
405 elif parent:
405 elif parent:
406 parent = parent[0]
406 parent = parent[0]
407
407
408 dest = ui.expandpath(parent or 'default-push', parent or 'default')
408 dest = ui.expandpath(parent or 'default-push', parent or 'default')
409 dest, revs = hg.parseurl(dest, None)[:2]
409 dest, revs = hg.parseurl(dest, None)[:2]
410 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
410 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
411
411
412 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
412 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
413 other = hg.peer(repo, opts, dest)
413 other = hg.peer(repo, opts, dest)
414
414
415 if revs:
415 if revs:
416 revs = [repo.lookup(rev) for rev in revs]
416 revs = [repo.lookup(rev) for rev in revs]
417
417
418 parent = discovery.findcommonoutgoing(
418 parent = discovery.findcommonoutgoing(
419 repo, other, [], force=opts.get('force')).missing[0:1]
419 repo, other, [], force=opts.get('force')).missing[0:1]
420 else:
420 else:
421 if opts.get('force'):
421 if opts.get('force'):
422 raise util.Abort(_('--force only allowed with --outgoing'))
422 raise util.Abort(_('--force only allowed with --outgoing'))
423
423
424 if opts.get('continue', False):
424 if opts.get('continue', False):
425 if len(parent) != 0:
425 if len(parent) != 0:
426 raise util.Abort(_('no arguments allowed with --continue'))
426 raise util.Abort(_('no arguments allowed with --continue'))
427 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
427 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
428 currentparent, wantnull = repo.dirstate.parents()
428 currentparent, wantnull = repo.dirstate.parents()
429 parentctx = repo[parentctxnode]
429 parentctx = repo[parentctxnode]
430 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
430 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
431 replacements.extend(repl)
431 replacements.extend(repl)
432 elif opts.get('abort', False):
432 elif opts.get('abort', False):
433 if len(parent) != 0:
433 if len(parent) != 0:
434 raise util.Abort(_('no arguments allowed with --abort'))
434 raise util.Abort(_('no arguments allowed with --abort'))
435 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
435 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
436 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
436 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
437 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
437 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
438 hg.clean(repo, topmost)
438 hg.clean(repo, topmost)
439 cleanupnode(ui, repo, 'created', tmpnodes)
439 cleanupnode(ui, repo, 'created', tmpnodes)
440 cleanupnode(ui, repo, 'temp', leafs)
440 cleanupnode(ui, repo, 'temp', leafs)
441 os.unlink(os.path.join(repo.path, 'histedit-state'))
441 os.unlink(os.path.join(repo.path, 'histedit-state'))
442 return
442 return
443 else:
443 else:
444 cmdutil.bailifchanged(repo)
444 cmdutil.bailifchanged(repo)
445 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
445 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
446 raise util.Abort(_('history edit already in progress, try '
446 raise util.Abort(_('history edit already in progress, try '
447 '--continue or --abort'))
447 '--continue or --abort'))
448
448
449 topmost, empty = repo.dirstate.parents()
449 topmost, empty = repo.dirstate.parents()
450
450
451 if len(parent) != 1:
451 if len(parent) != 1:
452 raise util.Abort(_('histedit requires exactly one parent revision'))
452 raise util.Abort(_('histedit requires exactly one parent revision'))
453 parent = scmutil.revsingle(repo, parent[0]).node()
453 parent = scmutil.revsingle(repo, parent[0]).node()
454
454
455 keep = opts.get('keep', False)
455 keep = opts.get('keep', False)
456 revs = between(repo, parent, topmost, keep)
456 revs = between(repo, parent, topmost, keep)
457 if not revs:
457 if not revs:
458 ui.warn(_('nothing to edit\n'))
458 ui.warn(_('nothing to edit\n'))
459 return 1
459 return 1
460
460
461 ctxs = [repo[r] for r in revs]
461 ctxs = [repo[r] for r in revs]
462 rules = opts.get('commands', '')
462 rules = opts.get('commands', '')
463 if not rules:
463 if not rules:
464 rules = '\n'.join([makedesc(c) for c in ctxs])
464 rules = '\n'.join([makedesc(c) for c in ctxs])
465 rules += '\n\n'
465 rules += '\n\n'
466 rules += editcomment % (node.short(parent), node.short(topmost))
466 rules += editcomment % (node.short(parent), node.short(topmost))
467 rules = ui.edit(rules, ui.username())
467 rules = ui.edit(rules, ui.username())
468 # Save edit rules in .hg/histedit-last-edit.txt in case
468 # Save edit rules in .hg/histedit-last-edit.txt in case
469 # the user needs to ask for help after something
469 # the user needs to ask for help after something
470 # surprising happens.
470 # surprising happens.
471 f = open(repo.join('histedit-last-edit.txt'), 'w')
471 f = open(repo.join('histedit-last-edit.txt'), 'w')
472 f.write(rules)
472 f.write(rules)
473 f.close()
473 f.close()
474 else:
474 else:
475 f = open(rules)
475 f = open(rules)
476 rules = f.read()
476 rules = f.read()
477 f.close()
477 f.close()
478 rules = [l for l in (r.strip() for r in rules.splitlines())
478 rules = [l for l in (r.strip() for r in rules.splitlines())
479 if l and not l[0] == '#']
479 if l and not l[0] == '#']
480 rules = verifyrules(rules, repo, ctxs)
480 rules = verifyrules(rules, repo, ctxs)
481
481
482 parentctx = repo[parent].parents()[0]
482 parentctx = repo[parent].parents()[0]
483 keep = opts.get('keep', False)
483 keep = opts.get('keep', False)
484 replacements = []
484 replacements = []
485
485
486
486
487 while rules:
487 while rules:
488 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
488 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
489 action, ha = rules.pop(0)
489 action, ha = rules.pop(0)
490 ui.debug('histedit: processing %s %s\n' % (action, ha))
490 ui.debug('histedit: processing %s %s\n' % (action, ha))
491 actfunc = actiontable[action]
491 actfunc = actiontable[action]
492 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
492 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
493 replacements.extend(replacement_)
493 replacements.extend(replacement_)
494
494
495 hg.update(repo, parentctx.node())
495 hg.update(repo, parentctx.node())
496
496
497 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
497 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
498 if mapping:
498 if mapping:
499 for prec, succs in mapping.iteritems():
499 for prec, succs in mapping.iteritems():
500 if not succs:
500 if not succs:
501 ui.debug('histedit: %s is dropped\n' % node.short(prec))
501 ui.debug('histedit: %s is dropped\n' % node.short(prec))
502 else:
502 else:
503 ui.debug('histedit: %s is replaced by %s\n' % (
503 ui.debug('histedit: %s is replaced by %s\n' % (
504 node.short(prec), node.short(succs[0])))
504 node.short(prec), node.short(succs[0])))
505 if len(succs) > 1:
505 if len(succs) > 1:
506 m = 'histedit: %s'
506 m = 'histedit: %s'
507 for n in succs[1:]:
507 for n in succs[1:]:
508 ui.debug(m % node.short(n))
508 ui.debug(m % node.short(n))
509
509
510 if not keep:
510 if not keep:
511 if mapping:
511 if mapping:
512 movebookmarks(ui, repo, mapping, topmost, ntm)
512 movebookmarks(ui, repo, mapping, topmost, ntm)
513 # TODO update mq state
513 # TODO update mq state
514 if obsolete._enabled:
514 if obsolete._enabled:
515 markers = []
515 markers = []
516 # sort by revision number because it sound "right"
516 # sort by revision number because it sound "right"
517 for prec in sorted(mapping, key=repo.changelog.rev):
517 for prec in sorted(mapping, key=repo.changelog.rev):
518 succs = mapping[prec]
518 succs = mapping[prec]
519 markers.append((repo[prec],
519 markers.append((repo[prec],
520 tuple(repo[s] for s in succs)))
520 tuple(repo[s] for s in succs)))
521 if markers:
521 if markers:
522 obsolete.createmarkers(repo, markers)
522 obsolete.createmarkers(repo, markers)
523 else:
523 else:
524 cleanupnode(ui, repo, 'replaced', mapping)
524 cleanupnode(ui, repo, 'replaced', mapping)
525
525
526 cleanupnode(ui, repo, 'temp', tmpnodes)
526 cleanupnode(ui, repo, 'temp', tmpnodes)
527 os.unlink(os.path.join(repo.path, 'histedit-state'))
527 os.unlink(os.path.join(repo.path, 'histedit-state'))
528 if os.path.exists(repo.sjoin('undo')):
528 if os.path.exists(repo.sjoin('undo')):
529 os.unlink(repo.sjoin('undo'))
529 os.unlink(repo.sjoin('undo'))
530
530
531
531
532 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
532 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
533 action, currentnode = rules.pop(0)
533 action, currentnode = rules.pop(0)
534 ctx = repo[currentnode]
534 ctx = repo[currentnode]
535 # is there any new commit between the expected parent and "."
535 # is there any new commit between the expected parent and "."
536 #
536 #
537 # note: does not take non linear new change in account (but previous
537 # note: does not take non linear new change in account (but previous
538 # implementation didn't used them anyway (issue3655)
538 # implementation didn't used them anyway (issue3655)
539 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
539 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
540 if not newchildren:
540 if not newchildren:
541 # `parentctxnode` should match but no result. This means that
541 # `parentctxnode` should match but no result. This means that
542 # currentnode is not a descendant from parentctxnode.
542 # currentnode is not a descendant from parentctxnode.
543 msg = _('working directory parent is not a descendant of %s')
543 msg = _('working directory parent is not a descendant of %s')
544 hint = _('update to %s or descendant and run "hg histedit '
544 hint = _('update to %s or descendant and run "hg histedit '
545 '--continue" again') % parentctx
545 '--continue" again') % parentctx
546 raise util.Abort(msg % parentctx, hint=hint)
546 raise util.Abort(msg % parentctx, hint=hint)
547 newchildren.pop(0) # remove parentctxnode
547 newchildren.pop(0) # remove parentctxnode
548 # Commit dirty working directory if necessary
548 # Commit dirty working directory if necessary
549 new = None
549 new = None
550 m, a, r, d = repo.status()[:4]
550 m, a, r, d = repo.status()[:4]
551 if m or a or r or d:
551 if m or a or r or d:
552 # prepare the message for the commit to comes
552 # prepare the message for the commit to comes
553 if action in ('f', 'fold'):
553 if action in ('f', 'fold'):
554 message = 'fold-temp-revision %s' % currentnode
554 message = 'fold-temp-revision %s' % currentnode
555 else:
555 else:
556 message = ctx.description() + '\n'
556 message = ctx.description() + '\n'
557 if action in ('e', 'edit', 'm', 'mess'):
557 if action in ('e', 'edit', 'm', 'mess'):
558 editor = cmdutil.commitforceeditor
558 editor = cmdutil.commitforceeditor
559 else:
559 else:
560 editor = False
560 editor = False
561 new = repo.commit(text=message, user=ctx.user(),
561 new = repo.commit(text=message, user=ctx.user(),
562 date=ctx.date(), extra=ctx.extra(),
562 date=ctx.date(), extra=ctx.extra(),
563 editor=editor)
563 editor=editor)
564 if new is not None:
564 if new is not None:
565 newchildren.append(new)
565 newchildren.append(new)
566
566
567 replacements = []
567 replacements = []
568 # track replacements
568 # track replacements
569 if ctx.node() not in newchildren:
569 if ctx.node() not in newchildren:
570 # note: new children may be empty when the changeset is dropped.
570 # note: new children may be empty when the changeset is dropped.
571 # this happen e.g during conflicting pick where we revert content
571 # this happen e.g during conflicting pick where we revert content
572 # to parent.
572 # to parent.
573 replacements.append((ctx.node(), tuple(newchildren)))
573 replacements.append((ctx.node(), tuple(newchildren)))
574
574
575 if action in ('f', 'fold'):
575 if action in ('f', 'fold'):
576 # finalize fold operation if applicable
576 # finalize fold operation if applicable
577 if new is None:
577 if new is None:
578 new = newchildren[-1]
578 new = newchildren[-1]
579 else:
579 else:
580 newchildren.pop() # remove new from internal changes
580 newchildren.pop() # remove new from internal changes
581 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
581 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
582 newchildren)
582 newchildren)
583 replacements.extend(repl)
583 replacements.extend(repl)
584 elif newchildren:
584 elif newchildren:
585 # otherwize update "parentctx" before proceding to further operation
585 # otherwize update "parentctx" before proceding to further operation
586 parentctx = repo[newchildren[-1]]
586 parentctx = repo[newchildren[-1]]
587 return parentctx, replacements
587 return parentctx, replacements
588
588
589
589
590 def between(repo, old, new, keep):
590 def between(repo, old, new, keep):
591 """select and validate the set of revision to edit
591 """select and validate the set of revision to edit
592
592
593 When keep is false, the specified set can't have children."""
593 When keep is false, the specified set can't have children."""
594 ctxs = list(repo.set('%n::%n', old, new))
594 ctxs = list(repo.set('%n::%n', old, new))
595 if ctxs and not keep:
595 if ctxs and not keep:
596 if repo.revs('(%ld::) - (%ld + hidden())', ctxs, ctxs):
596 if (not obsolete._enabled and
597 repo.revs('(%ld::) - (%ld + hidden())', ctxs, ctxs)):
597 raise util.Abort(_('cannot edit history that would orphan nodes'))
598 raise util.Abort(_('cannot edit history that would orphan nodes'))
598 root = ctxs[0] # list is already sorted by repo.set
599 root = ctxs[0] # list is already sorted by repo.set
599 if not root.phase():
600 if not root.phase():
600 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
601 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
601 return [c.node() for c in ctxs]
602 return [c.node() for c in ctxs]
602
603
603
604
604 def writestate(repo, parentnode, rules, keep, topmost, replacements):
605 def writestate(repo, parentnode, rules, keep, topmost, replacements):
605 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
606 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
606 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
607 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
607 fp.close()
608 fp.close()
608
609
609 def readstate(repo):
610 def readstate(repo):
610 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
611 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
611 """
612 """
612 fp = open(os.path.join(repo.path, 'histedit-state'))
613 fp = open(os.path.join(repo.path, 'histedit-state'))
613 return pickle.load(fp)
614 return pickle.load(fp)
614
615
615
616
616 def makedesc(c):
617 def makedesc(c):
617 """build a initial action line for a ctx `c`
618 """build a initial action line for a ctx `c`
618
619
619 line are in the form:
620 line are in the form:
620
621
621 pick <hash> <rev> <summary>
622 pick <hash> <rev> <summary>
622 """
623 """
623 summary = ''
624 summary = ''
624 if c.description():
625 if c.description():
625 summary = c.description().splitlines()[0]
626 summary = c.description().splitlines()[0]
626 line = 'pick %s %d %s' % (c, c.rev(), summary)
627 line = 'pick %s %d %s' % (c, c.rev(), summary)
627 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
628 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
628
629
629 def verifyrules(rules, repo, ctxs):
630 def verifyrules(rules, repo, ctxs):
630 """Verify that there exists exactly one edit rule per given changeset.
631 """Verify that there exists exactly one edit rule per given changeset.
631
632
632 Will abort if there are to many or too few rules, a malformed rule,
633 Will abort if there are to many or too few rules, a malformed rule,
633 or a rule on a changeset outside of the user-given range.
634 or a rule on a changeset outside of the user-given range.
634 """
635 """
635 parsed = []
636 parsed = []
636 if len(rules) != len(ctxs):
637 if len(rules) != len(ctxs):
637 raise util.Abort(_('must specify a rule for each changeset once'))
638 raise util.Abort(_('must specify a rule for each changeset once'))
638 for r in rules:
639 for r in rules:
639 if ' ' not in r:
640 if ' ' not in r:
640 raise util.Abort(_('malformed line "%s"') % r)
641 raise util.Abort(_('malformed line "%s"') % r)
641 action, rest = r.split(' ', 1)
642 action, rest = r.split(' ', 1)
642 if ' ' in rest.strip():
643 if ' ' in rest.strip():
643 ha, rest = rest.split(' ', 1)
644 ha, rest = rest.split(' ', 1)
644 else:
645 else:
645 ha = r.strip()
646 ha = r.strip()
646 try:
647 try:
647 if repo[ha] not in ctxs:
648 if repo[ha] not in ctxs:
648 raise util.Abort(
649 raise util.Abort(
649 _('may not use changesets other than the ones listed'))
650 _('may not use changesets other than the ones listed'))
650 except error.RepoError:
651 except error.RepoError:
651 raise util.Abort(_('unknown changeset %s listed') % ha)
652 raise util.Abort(_('unknown changeset %s listed') % ha)
652 if action not in actiontable:
653 if action not in actiontable:
653 raise util.Abort(_('unknown action "%s"') % action)
654 raise util.Abort(_('unknown action "%s"') % action)
654 parsed.append([action, ha])
655 parsed.append([action, ha])
655 return parsed
656 return parsed
656
657
657 def processreplacement(repo, replacements):
658 def processreplacement(repo, replacements):
658 """process the list of replacements to return
659 """process the list of replacements to return
659
660
660 1) the final mapping between original and created nodes
661 1) the final mapping between original and created nodes
661 2) the list of temporary node created by histedit
662 2) the list of temporary node created by histedit
662 3) the list of new commit created by histedit"""
663 3) the list of new commit created by histedit"""
663 allsuccs = set()
664 allsuccs = set()
664 replaced = set()
665 replaced = set()
665 fullmapping = {}
666 fullmapping = {}
666 # initialise basic set
667 # initialise basic set
667 # fullmapping record all operation recorded in replacement
668 # fullmapping record all operation recorded in replacement
668 for rep in replacements:
669 for rep in replacements:
669 allsuccs.update(rep[1])
670 allsuccs.update(rep[1])
670 replaced.add(rep[0])
671 replaced.add(rep[0])
671 fullmapping.setdefault(rep[0], set()).update(rep[1])
672 fullmapping.setdefault(rep[0], set()).update(rep[1])
672 new = allsuccs - replaced
673 new = allsuccs - replaced
673 tmpnodes = allsuccs & replaced
674 tmpnodes = allsuccs & replaced
674 # Reduce content fullmapping into direct relation between original nodes
675 # Reduce content fullmapping into direct relation between original nodes
675 # and final node created during history edition
676 # and final node created during history edition
676 # Dropped changeset are replaced by an empty list
677 # Dropped changeset are replaced by an empty list
677 toproceed = set(fullmapping)
678 toproceed = set(fullmapping)
678 final = {}
679 final = {}
679 while toproceed:
680 while toproceed:
680 for x in list(toproceed):
681 for x in list(toproceed):
681 succs = fullmapping[x]
682 succs = fullmapping[x]
682 for s in list(succs):
683 for s in list(succs):
683 if s in toproceed:
684 if s in toproceed:
684 # non final node with unknown closure
685 # non final node with unknown closure
685 # We can't process this now
686 # We can't process this now
686 break
687 break
687 elif s in final:
688 elif s in final:
688 # non final node, replace with closure
689 # non final node, replace with closure
689 succs.remove(s)
690 succs.remove(s)
690 succs.update(final[s])
691 succs.update(final[s])
691 else:
692 else:
692 final[x] = succs
693 final[x] = succs
693 toproceed.remove(x)
694 toproceed.remove(x)
694 # remove tmpnodes from final mapping
695 # remove tmpnodes from final mapping
695 for n in tmpnodes:
696 for n in tmpnodes:
696 del final[n]
697 del final[n]
697 # we expect all changes involved in final to exist in the repo
698 # we expect all changes involved in final to exist in the repo
698 # turn `final` into list (topologically sorted)
699 # turn `final` into list (topologically sorted)
699 nm = repo.changelog.nodemap
700 nm = repo.changelog.nodemap
700 for prec, succs in final.items():
701 for prec, succs in final.items():
701 final[prec] = sorted(succs, key=nm.get)
702 final[prec] = sorted(succs, key=nm.get)
702
703
703 # computed topmost element (necessary for bookmark)
704 # computed topmost element (necessary for bookmark)
704 if new:
705 if new:
705 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
706 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
706 elif not final:
707 elif not final:
707 # Nothing rewritten at all. we won't need `newtopmost`
708 # Nothing rewritten at all. we won't need `newtopmost`
708 # It is the same as `oldtopmost` and `processreplacement` know it
709 # It is the same as `oldtopmost` and `processreplacement` know it
709 newtopmost = None
710 newtopmost = None
710 else:
711 else:
711 # every body died. The newtopmost is the parent of the root.
712 # every body died. The newtopmost is the parent of the root.
712 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
713 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
713
714
714 return final, tmpnodes, new, newtopmost
715 return final, tmpnodes, new, newtopmost
715
716
716 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
717 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
717 """Move bookmark from old to newly created node"""
718 """Move bookmark from old to newly created node"""
718 if not mapping:
719 if not mapping:
719 # if nothing got rewritten there is not purpose for this function
720 # if nothing got rewritten there is not purpose for this function
720 return
721 return
721 moves = []
722 moves = []
722 for bk, old in repo._bookmarks.iteritems():
723 for bk, old in repo._bookmarks.iteritems():
723 if old == oldtopmost:
724 if old == oldtopmost:
724 # special case ensure bookmark stay on tip.
725 # special case ensure bookmark stay on tip.
725 #
726 #
726 # This is arguably a feature and we may only want that for the
727 # This is arguably a feature and we may only want that for the
727 # active bookmark. But the behavior is kept compatible with the old
728 # active bookmark. But the behavior is kept compatible with the old
728 # version for now.
729 # version for now.
729 moves.append((bk, newtopmost))
730 moves.append((bk, newtopmost))
730 continue
731 continue
731 base = old
732 base = old
732 new = mapping.get(base, None)
733 new = mapping.get(base, None)
733 if new is None:
734 if new is None:
734 continue
735 continue
735 while not new:
736 while not new:
736 # base is killed, trying with parent
737 # base is killed, trying with parent
737 base = repo[base].p1().node()
738 base = repo[base].p1().node()
738 new = mapping.get(base, (base,))
739 new = mapping.get(base, (base,))
739 # nothing to move
740 # nothing to move
740 moves.append((bk, new[-1]))
741 moves.append((bk, new[-1]))
741 if moves:
742 if moves:
742 marks = repo._bookmarks
743 marks = repo._bookmarks
743 for mark, new in moves:
744 for mark, new in moves:
744 old = marks[mark]
745 old = marks[mark]
745 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
746 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
746 % (mark, node.short(old), node.short(new)))
747 % (mark, node.short(old), node.short(new)))
747 marks[mark] = new
748 marks[mark] = new
748 marks.write()
749 marks.write()
749
750
750 def cleanupnode(ui, repo, name, nodes):
751 def cleanupnode(ui, repo, name, nodes):
751 """strip a group of nodes from the repository
752 """strip a group of nodes from the repository
752
753
753 The set of node to strip may contains unknown nodes."""
754 The set of node to strip may contains unknown nodes."""
754 ui.debug('should strip %s nodes %s\n' %
755 ui.debug('should strip %s nodes %s\n' %
755 (name, ', '.join([node.short(n) for n in nodes])))
756 (name, ', '.join([node.short(n) for n in nodes])))
756 lock = None
757 lock = None
757 try:
758 try:
758 lock = repo.lock()
759 lock = repo.lock()
759 # Find all node that need to be stripped
760 # Find all node that need to be stripped
760 # (we hg %lr instead of %ln to silently ignore unknown item
761 # (we hg %lr instead of %ln to silently ignore unknown item
761 nm = repo.changelog.nodemap
762 nm = repo.changelog.nodemap
762 nodes = [n for n in nodes if n in nm]
763 nodes = [n for n in nodes if n in nm]
763 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
764 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
764 for c in roots:
765 for c in roots:
765 # We should process node in reverse order to strip tip most first.
766 # We should process node in reverse order to strip tip most first.
766 # but this trigger a bug in changegroup hook.
767 # but this trigger a bug in changegroup hook.
767 # This would reduce bundle overhead
768 # This would reduce bundle overhead
768 repair.strip(ui, repo, c)
769 repair.strip(ui, repo, c)
769 finally:
770 finally:
770 lockmod.release(lock)
771 lockmod.release(lock)
@@ -1,143 +1,155 b''
1 $ . "$TESTDIR/histedit-helpers.sh"
1 $ . "$TESTDIR/histedit-helpers.sh"
2
2
3 Enable obsolete
3 Enable obsolete
4
4
5 $ cat > ${TESTTMP}/obs.py << EOF
5 $ cat > ${TESTTMP}/obs.py << EOF
6 > import mercurial.obsolete
6 > import mercurial.obsolete
7 > mercurial.obsolete._enabled = True
7 > mercurial.obsolete._enabled = True
8 > EOF
8 > EOF
9
9
10 $ cat >> $HGRCPATH << EOF
10 $ cat >> $HGRCPATH << EOF
11 > [ui]
11 > [ui]
12 > logtemplate= {rev}:{node|short} {desc|firstline}
12 > logtemplate= {rev}:{node|short} {desc|firstline}
13 > [phases]
13 > [phases]
14 > publish=False
14 > publish=False
15 > [extensions]'
15 > [extensions]'
16 > histedit=
16 > histedit=
17 >
17 >
18 > obs=${TESTTMP}/obs.py
18 > obs=${TESTTMP}/obs.py
19 > EOF
19 > EOF
20
20
21 $ hg init base
21 $ hg init base
22 $ cd base
22 $ cd base
23
23
24 $ for x in a b c d e f ; do
24 $ for x in a b c d e f ; do
25 > echo $x > $x
25 > echo $x > $x
26 > hg add $x
26 > hg add $x
27 > hg ci -m $x
27 > hg ci -m $x
28 > done
28 > done
29
29
30 $ hg log --graph
30 $ hg log --graph
31 @ 5:652413bf663e f
31 @ 5:652413bf663e f
32 |
32 |
33 o 4:e860deea161a e
33 o 4:e860deea161a e
34 |
34 |
35 o 3:055a42cdd887 d
35 o 3:055a42cdd887 d
36 |
36 |
37 o 2:177f92b77385 c
37 o 2:177f92b77385 c
38 |
38 |
39 o 1:d2ae7f538514 b
39 o 1:d2ae7f538514 b
40 |
40 |
41 o 0:cb9a9f314b8b a
41 o 0:cb9a9f314b8b a
42
42
43
43
44 $ HGEDITOR=cat hg histedit 1
44 $ HGEDITOR=cat hg histedit 1
45 pick d2ae7f538514 1 b
45 pick d2ae7f538514 1 b
46 pick 177f92b77385 2 c
46 pick 177f92b77385 2 c
47 pick 055a42cdd887 3 d
47 pick 055a42cdd887 3 d
48 pick e860deea161a 4 e
48 pick e860deea161a 4 e
49 pick 652413bf663e 5 f
49 pick 652413bf663e 5 f
50
50
51 # Edit history between d2ae7f538514 and 652413bf663e
51 # Edit history between d2ae7f538514 and 652413bf663e
52 #
52 #
53 # Commands:
53 # Commands:
54 # p, pick = use commit
54 # p, pick = use commit
55 # e, edit = use commit, but stop for amending
55 # e, edit = use commit, but stop for amending
56 # f, fold = use commit, but fold into previous commit (combines N and N-1)
56 # f, fold = use commit, but fold into previous commit (combines N and N-1)
57 # d, drop = remove commit from history
57 # d, drop = remove commit from history
58 # m, mess = edit message without changing commit content
58 # m, mess = edit message without changing commit content
59 #
59 #
60 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
60 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
61 $ cat > commands.txt <<EOF
61 $ cat > commands.txt <<EOF
62 > pick 177f92b77385 2 c
62 > pick 177f92b77385 2 c
63 > drop d2ae7f538514 1 b
63 > drop d2ae7f538514 1 b
64 > pick 055a42cdd887 3 d
64 > pick 055a42cdd887 3 d
65 > fold e860deea161a 4 e
65 > fold e860deea161a 4 e
66 > pick 652413bf663e 5 f
66 > pick 652413bf663e 5 f
67 > EOF
67 > EOF
68 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
68 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
69 saved backup bundle to $TESTTMP/base/.hg/strip-backup/34a9919932c1-backup.hg (glob)
69 saved backup bundle to $TESTTMP/base/.hg/strip-backup/34a9919932c1-backup.hg (glob)
70 $ hg log --graph --hidden
70 $ hg log --graph --hidden
71 @ 8:0efacef7cb48 f
71 @ 8:0efacef7cb48 f
72 |
72 |
73 o 7:ae467701c500 d
73 o 7:ae467701c500 d
74 |
74 |
75 o 6:d36c0562f908 c
75 o 6:d36c0562f908 c
76 |
76 |
77 | x 5:652413bf663e f
77 | x 5:652413bf663e f
78 | |
78 | |
79 | x 4:e860deea161a e
79 | x 4:e860deea161a e
80 | |
80 | |
81 | x 3:055a42cdd887 d
81 | x 3:055a42cdd887 d
82 | |
82 | |
83 | x 2:177f92b77385 c
83 | x 2:177f92b77385 c
84 | |
84 | |
85 | x 1:d2ae7f538514 b
85 | x 1:d2ae7f538514 b
86 |/
86 |/
87 o 0:cb9a9f314b8b a
87 o 0:cb9a9f314b8b a
88
88
89 $ hg debugobsolete
89 $ hg debugobsolete
90 d2ae7f538514cd87c17547b0de4cea71fe1af9fb 0 {'date': '* *', 'user': 'test'} (glob)
90 d2ae7f538514cd87c17547b0de4cea71fe1af9fb 0 {'date': '* *', 'user': 'test'} (glob)
91 177f92b773850b59254aa5e923436f921b55483b d36c0562f908c692f5204d606d4ff3537d41f1bf 0 {'date': '* *', 'user': 'test'} (glob)
91 177f92b773850b59254aa5e923436f921b55483b d36c0562f908c692f5204d606d4ff3537d41f1bf 0 {'date': '* *', 'user': 'test'} (glob)
92 055a42cdd88768532f9cf79daa407fc8d138de9b ae467701c5006bf21ffcfdb555b3d6b63280b6b7 0 {'date': '* *', 'user': 'test'} (glob)
92 055a42cdd88768532f9cf79daa407fc8d138de9b ae467701c5006bf21ffcfdb555b3d6b63280b6b7 0 {'date': '* *', 'user': 'test'} (glob)
93 e860deea161a2f77de56603b340ebbb4536308ae ae467701c5006bf21ffcfdb555b3d6b63280b6b7 0 {'date': '* *', 'user': 'test'} (glob)
93 e860deea161a2f77de56603b340ebbb4536308ae ae467701c5006bf21ffcfdb555b3d6b63280b6b7 0 {'date': '* *', 'user': 'test'} (glob)
94 652413bf663ef2a641cab26574e46d5f5a64a55a 0efacef7cb481bf574f69075b82d044fdbe5c20f 0 {'date': '* *', 'user': 'test'} (glob)
94 652413bf663ef2a641cab26574e46d5f5a64a55a 0efacef7cb481bf574f69075b82d044fdbe5c20f 0 {'date': '* *', 'user': 'test'} (glob)
95
95
96
96
97 Ensure hidden revision does not prevent histedit
97 Ensure hidden revision does not prevent histedit
98 -------------------------------------------------
98 -------------------------------------------------
99
99
100 create an hidden revision
100 create an hidden revision
101
101
102 $ cat > commands.txt <<EOF
102 $ cat > commands.txt <<EOF
103 > pick d36c0562f908 6 c
103 > pick d36c0562f908 6 c
104 > drop ae467701c500 7 d
104 > drop ae467701c500 7 d
105 > pick 0efacef7cb48 8 f
105 > pick 0efacef7cb48 8 f
106 > EOF
106 > EOF
107 $ hg histedit 6 --commands commands.txt
107 $ hg histedit 6 --commands commands.txt
108 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
108 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
109 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
109 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
110 $ hg log --graph
110 $ hg log --graph
111 @ 9:7c044e3e33a9 f
111 @ 9:7c044e3e33a9 f
112 |
112 |
113 o 6:d36c0562f908 c
113 o 6:d36c0562f908 c
114 |
114 |
115 o 0:cb9a9f314b8b a
115 o 0:cb9a9f314b8b a
116
116
117 check hidden revision are ignored (6 have hidden children 7 and 8)
117 check hidden revision are ignored (6 have hidden children 7 and 8)
118
118
119 $ cat > commands.txt <<EOF
119 $ cat > commands.txt <<EOF
120 > pick d36c0562f908 6 c
120 > pick d36c0562f908 6 c
121 > pick 7c044e3e33a9 8 f
121 > pick 7c044e3e33a9 8 f
122 > EOF
122 > EOF
123 $ hg histedit 6 --commands commands.txt
123 $ hg histedit 6 --commands commands.txt
124 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
124 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
125
125
126
126
127 Check that histedit respect phases
127 Check that histedit respect phases
128 =========================================
128 =========================================
129
129
130 (not directly related to the test file but doesn't deserve it's own test case)
130 (not directly related to the test file but doesn't deserve it's own test case)
131
131
132 $ hg log -G
132 $ hg log -G
133 @ 9:7c044e3e33a9 f
133 @ 9:7c044e3e33a9 f
134 |
134 |
135 o 6:d36c0562f908 c
135 o 6:d36c0562f908 c
136 |
136 |
137 o 0:cb9a9f314b8b a
137 o 0:cb9a9f314b8b a
138
138
139 $ hg ph -pv '.^'
139 $ hg ph -pv '.^'
140 phase changed for 2 changesets
140 phase changed for 2 changesets
141 $ hg histedit -r '.~2'
141 $ hg histedit -r '.~2'
142 abort: cannot edit immutable changeset: cb9a9f314b8b
142 abort: cannot edit immutable changeset: cb9a9f314b8b
143 [255]
143 [255]
144
145
146 +Test ui.prevent-unstable option
147 +------------------------------------
148
149 $ hg up '.^'
150 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
151 $ hg phase --force --draft .
152 $ hg log -r 'children(.)'
153 9:7c044e3e33a9 f (no-eol)
154 $ hg histedit -r '.'
155 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
General Comments 0
You need to be logged in to leave comments. Login now