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