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