##// END OF EJS Templates
histedit: do not use "min" on ctx...
Pierre-Yves David -
r17767:a787e46d default
parent child Browse files
Show More
@@ -1,768 +1,768 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 if not revs:
458 if not revs:
459 ui.warn(_('nothing to edit\n'))
459 ui.warn(_('nothing to edit\n'))
460 return 1
460 return 1
461
461
462 ctxs = [repo[r] for r in revs]
462 ctxs = [repo[r] for r in revs]
463 rules = opts.get('commands', '')
463 rules = opts.get('commands', '')
464 if not rules:
464 if not rules:
465 rules = '\n'.join([makedesc(c) for c in ctxs])
465 rules = '\n'.join([makedesc(c) for c in ctxs])
466 rules += '\n\n'
466 rules += '\n\n'
467 rules += editcomment % (node.short(parent), node.short(topmost))
467 rules += editcomment % (node.short(parent), node.short(topmost))
468 rules = ui.edit(rules, ui.username())
468 rules = ui.edit(rules, ui.username())
469 # Save edit rules in .hg/histedit-last-edit.txt in case
469 # Save edit rules in .hg/histedit-last-edit.txt in case
470 # the user needs to ask for help after something
470 # the user needs to ask for help after something
471 # surprising happens.
471 # surprising happens.
472 f = open(repo.join('histedit-last-edit.txt'), 'w')
472 f = open(repo.join('histedit-last-edit.txt'), 'w')
473 f.write(rules)
473 f.write(rules)
474 f.close()
474 f.close()
475 else:
475 else:
476 f = open(rules)
476 f = open(rules)
477 rules = f.read()
477 rules = f.read()
478 f.close()
478 f.close()
479 rules = [l for l in (r.strip() for r in rules.splitlines())
479 rules = [l for l in (r.strip() for r in rules.splitlines())
480 if l and not l[0] == '#']
480 if l and not l[0] == '#']
481 rules = verifyrules(rules, repo, ctxs)
481 rules = verifyrules(rules, repo, ctxs)
482
482
483 parentctx = repo[parent].parents()[0]
483 parentctx = repo[parent].parents()[0]
484 keep = opts.get('keep', False)
484 keep = opts.get('keep', False)
485 replacements = []
485 replacements = []
486
486
487
487
488 while rules:
488 while rules:
489 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
489 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
490 action, ha = rules.pop(0)
490 action, ha = rules.pop(0)
491 ui.debug('histedit: processing %s %s\n' % (action, ha))
491 ui.debug('histedit: processing %s %s\n' % (action, ha))
492 actfunc = actiontable[action]
492 actfunc = actiontable[action]
493 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
493 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
494 replacements.extend(replacement_)
494 replacements.extend(replacement_)
495
495
496 hg.update(repo, parentctx.node())
496 hg.update(repo, parentctx.node())
497
497
498 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
498 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
499 if mapping:
499 if mapping:
500 for prec, succs in mapping.iteritems():
500 for prec, succs in mapping.iteritems():
501 if not succs:
501 if not succs:
502 ui.debug('histedit: %s is dropped\n' % node.short(prec))
502 ui.debug('histedit: %s is dropped\n' % node.short(prec))
503 else:
503 else:
504 ui.debug('histedit: %s is replaced by %s\n' % (
504 ui.debug('histedit: %s is replaced by %s\n' % (
505 node.short(prec), node.short(succs[0])))
505 node.short(prec), node.short(succs[0])))
506 if len(succs) > 1:
506 if len(succs) > 1:
507 m = 'histedit: %s'
507 m = 'histedit: %s'
508 for n in succs[1:]:
508 for n in succs[1:]:
509 ui.debug(m % node.short(n))
509 ui.debug(m % node.short(n))
510
510
511 if not keep:
511 if not keep:
512 if mapping:
512 if mapping:
513 movebookmarks(ui, repo, mapping, topmost, ntm)
513 movebookmarks(ui, repo, mapping, topmost, ntm)
514 # TODO update mq state
514 # TODO update mq state
515 if obsolete._enabled:
515 if obsolete._enabled:
516 markers = []
516 markers = []
517 for prec, succs in mapping.iteritems():
517 for prec, succs in mapping.iteritems():
518 markers.append((repo[prec],
518 markers.append((repo[prec],
519 tuple(repo[s] for s in succs)))
519 tuple(repo[s] for s in succs)))
520 if markers:
520 if markers:
521 obsolete.createmarkers(repo, markers)
521 obsolete.createmarkers(repo, markers)
522 else:
522 else:
523 cleanupnode(ui, repo, 'replaced', mapping)
523 cleanupnode(ui, repo, 'replaced', mapping)
524
524
525 cleanupnode(ui, repo, 'temp', tmpnodes)
525 cleanupnode(ui, repo, 'temp', tmpnodes)
526 os.unlink(os.path.join(repo.path, 'histedit-state'))
526 os.unlink(os.path.join(repo.path, 'histedit-state'))
527 if os.path.exists(repo.sjoin('undo')):
527 if os.path.exists(repo.sjoin('undo')):
528 os.unlink(repo.sjoin('undo'))
528 os.unlink(repo.sjoin('undo'))
529
529
530
530
531 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
531 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
532 action, currentnode = rules.pop(0)
532 action, currentnode = rules.pop(0)
533 ctx = repo[currentnode]
533 ctx = repo[currentnode]
534 # is there any new commit between the expected parent and "."
534 # is there any new commit between the expected parent and "."
535 #
535 #
536 # note: does not take non linear new change in account (but previous
536 # note: does not take non linear new change in account (but previous
537 # implementation didn't used them anyway (issue3655)
537 # implementation didn't used them anyway (issue3655)
538 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
538 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
539 if not newchildren:
539 if not newchildren:
540 # `parentctxnode` should match but no result. This means that
540 # `parentctxnode` should match but no result. This means that
541 # currentnode is not a descendant from parentctxnode.
541 # currentnode is not a descendant from parentctxnode.
542 msg = _('working directory parent is not a descendant of %s')
542 msg = _('working directory parent is not a descendant of %s')
543 hint = _('update to %s or descendant and run "hg histedit '
543 hint = _('update to %s or descendant and run "hg histedit '
544 '--continue" again') % parentctx
544 '--continue" again') % parentctx
545 raise util.Abort(msg % parentctx, hint=hint)
545 raise util.Abort(msg % parentctx, hint=hint)
546 newchildren.pop(0) # remove parentctxnode
546 newchildren.pop(0) # remove parentctxnode
547 # Commit dirty working directory if necessary
547 # Commit dirty working directory if necessary
548 new = None
548 new = None
549 m, a, r, d = repo.status()[:4]
549 m, a, r, d = repo.status()[:4]
550 if m or a or r or d:
550 if m or a or r or d:
551 # prepare the message for the commit to comes
551 # prepare the message for the commit to comes
552 if action in ('f', 'fold'):
552 if action in ('f', 'fold'):
553 message = 'fold-temp-revision %s' % currentnode
553 message = 'fold-temp-revision %s' % currentnode
554 else:
554 else:
555 message = ctx.description() + '\n'
555 message = ctx.description() + '\n'
556 if action in ('e', 'edit', 'm', 'mess'):
556 if action in ('e', 'edit', 'm', 'mess'):
557 editor = cmdutil.commitforceeditor
557 editor = cmdutil.commitforceeditor
558 else:
558 else:
559 editor = False
559 editor = False
560 new = repo.commit(text=message, user=ctx.user(),
560 new = repo.commit(text=message, user=ctx.user(),
561 date=ctx.date(), extra=ctx.extra(),
561 date=ctx.date(), extra=ctx.extra(),
562 editor=editor)
562 editor=editor)
563 if new is not None:
563 if new is not None:
564 newchildren.append(new)
564 newchildren.append(new)
565
565
566 replacements = []
566 replacements = []
567 # track replacements
567 # track replacements
568 if ctx.node() not in newchildren:
568 if ctx.node() not in newchildren:
569 # note: new children may be empty when the changeset is dropped.
569 # note: new children may be empty when the changeset is dropped.
570 # this happen e.g during conflicting pick where we revert content
570 # this happen e.g during conflicting pick where we revert content
571 # to parent.
571 # to parent.
572 replacements.append((ctx.node(), tuple(newchildren)))
572 replacements.append((ctx.node(), tuple(newchildren)))
573
573
574 if action in ('f', 'fold'):
574 if action in ('f', 'fold'):
575 # finalize fold operation if applicable
575 # finalize fold operation if applicable
576 if new is None:
576 if new is None:
577 new = newchildren[-1]
577 new = newchildren[-1]
578 else:
578 else:
579 newchildren.pop() # remove new from internal changes
579 newchildren.pop() # remove new from internal changes
580 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
580 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
581 newchildren)
581 newchildren)
582 replacements.extend(repl)
582 replacements.extend(repl)
583 elif newchildren:
583 elif newchildren:
584 # otherwize update "parentctx" before proceding to further operation
584 # otherwize update "parentctx" before proceding to further operation
585 parentctx = repo[newchildren[-1]]
585 parentctx = repo[newchildren[-1]]
586 return parentctx, replacements
586 return parentctx, replacements
587
587
588
588
589 def between(repo, old, new, keep):
589 def between(repo, old, new, keep):
590 """select and validate the set of revision to edit
590 """select and validate the set of revision to edit
591
591
592 When keep is false, the specified set can't have children."""
592 When keep is false, the specified set can't have children."""
593 ctxs = list(repo.set('%n::%n', old, new))
593 ctxs = list(repo.set('%n::%n', old, new))
594 if ctxs and not keep:
594 if ctxs and not keep:
595 if repo.revs('(%ld::) - (%ld + hidden())', ctxs, ctxs):
595 if repo.revs('(%ld::) - (%ld + hidden())', ctxs, ctxs):
596 raise util.Abort(_('cannot edit history that would orphan nodes'))
596 raise util.Abort(_('cannot edit history that would orphan nodes'))
597 root = min(ctxs)
597 root = ctxs[0] # list is already sorted by repo.set
598 if not root.phase():
598 if not root.phase():
599 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
599 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
600 return [c.node() for c in ctxs]
600 return [c.node() for c in ctxs]
601
601
602
602
603 def writestate(repo, parentnode, rules, keep, topmost, replacements):
603 def writestate(repo, parentnode, rules, keep, topmost, replacements):
604 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
604 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
605 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
605 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
606 fp.close()
606 fp.close()
607
607
608 def readstate(repo):
608 def readstate(repo):
609 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
609 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
610 """
610 """
611 fp = open(os.path.join(repo.path, 'histedit-state'))
611 fp = open(os.path.join(repo.path, 'histedit-state'))
612 return pickle.load(fp)
612 return pickle.load(fp)
613
613
614
614
615 def makedesc(c):
615 def makedesc(c):
616 """build a initial action line for a ctx `c`
616 """build a initial action line for a ctx `c`
617
617
618 line are in the form:
618 line are in the form:
619
619
620 pick <hash> <rev> <summary>
620 pick <hash> <rev> <summary>
621 """
621 """
622 summary = ''
622 summary = ''
623 if c.description():
623 if c.description():
624 summary = c.description().splitlines()[0]
624 summary = c.description().splitlines()[0]
625 line = 'pick %s %d %s' % (c, c.rev(), summary)
625 line = 'pick %s %d %s' % (c, c.rev(), summary)
626 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
626 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
627
627
628 def verifyrules(rules, repo, ctxs):
628 def verifyrules(rules, repo, ctxs):
629 """Verify that there exists exactly one edit rule per given changeset.
629 """Verify that there exists exactly one edit rule per given changeset.
630
630
631 Will abort if there are to many or too few rules, a malformed rule,
631 Will abort if there are to many or too few rules, a malformed rule,
632 or a rule on a changeset outside of the user-given range.
632 or a rule on a changeset outside of the user-given range.
633 """
633 """
634 parsed = []
634 parsed = []
635 if len(rules) != len(ctxs):
635 if len(rules) != len(ctxs):
636 raise util.Abort(_('must specify a rule for each changeset once'))
636 raise util.Abort(_('must specify a rule for each changeset once'))
637 for r in rules:
637 for r in rules:
638 if ' ' not in r:
638 if ' ' not in r:
639 raise util.Abort(_('malformed line "%s"') % r)
639 raise util.Abort(_('malformed line "%s"') % r)
640 action, rest = r.split(' ', 1)
640 action, rest = r.split(' ', 1)
641 if ' ' in rest.strip():
641 if ' ' in rest.strip():
642 ha, rest = rest.split(' ', 1)
642 ha, rest = rest.split(' ', 1)
643 else:
643 else:
644 ha = r.strip()
644 ha = r.strip()
645 try:
645 try:
646 if repo[ha] not in ctxs:
646 if repo[ha] not in ctxs:
647 raise util.Abort(
647 raise util.Abort(
648 _('may not use changesets other than the ones listed'))
648 _('may not use changesets other than the ones listed'))
649 except error.RepoError:
649 except error.RepoError:
650 raise util.Abort(_('unknown changeset %s listed') % ha)
650 raise util.Abort(_('unknown changeset %s listed') % ha)
651 if action not in actiontable:
651 if action not in actiontable:
652 raise util.Abort(_('unknown action "%s"') % action)
652 raise util.Abort(_('unknown action "%s"') % action)
653 parsed.append([action, ha])
653 parsed.append([action, ha])
654 return parsed
654 return parsed
655
655
656 def processreplacement(repo, replacements):
656 def processreplacement(repo, replacements):
657 """process the list of replacements to return
657 """process the list of replacements to return
658
658
659 1) the final mapping between original and created nodes
659 1) the final mapping between original and created nodes
660 2) the list of temporary node created by histedit
660 2) the list of temporary node created by histedit
661 3) the list of new commit created by histedit"""
661 3) the list of new commit created by histedit"""
662 allsuccs = set()
662 allsuccs = set()
663 replaced = set()
663 replaced = set()
664 fullmapping = {}
664 fullmapping = {}
665 # initialise basic set
665 # initialise basic set
666 # fullmapping record all operation recorded in replacement
666 # fullmapping record all operation recorded in replacement
667 for rep in replacements:
667 for rep in replacements:
668 allsuccs.update(rep[1])
668 allsuccs.update(rep[1])
669 replaced.add(rep[0])
669 replaced.add(rep[0])
670 fullmapping.setdefault(rep[0], set()).update(rep[1])
670 fullmapping.setdefault(rep[0], set()).update(rep[1])
671 new = allsuccs - replaced
671 new = allsuccs - replaced
672 tmpnodes = allsuccs & replaced
672 tmpnodes = allsuccs & replaced
673 # Reduce content fullmapping into direct relation between original nodes
673 # Reduce content fullmapping into direct relation between original nodes
674 # and final node created during history edition
674 # and final node created during history edition
675 # Dropped changeset are replaced by an empty list
675 # Dropped changeset are replaced by an empty list
676 toproceed = set(fullmapping)
676 toproceed = set(fullmapping)
677 final = {}
677 final = {}
678 while toproceed:
678 while toproceed:
679 for x in list(toproceed):
679 for x in list(toproceed):
680 succs = fullmapping[x]
680 succs = fullmapping[x]
681 for s in list(succs):
681 for s in list(succs):
682 if s in toproceed:
682 if s in toproceed:
683 # non final node with unknown closure
683 # non final node with unknown closure
684 # We can't process this now
684 # We can't process this now
685 break
685 break
686 elif s in final:
686 elif s in final:
687 # non final node, replace with closure
687 # non final node, replace with closure
688 succs.remove(s)
688 succs.remove(s)
689 succs.update(final[s])
689 succs.update(final[s])
690 else:
690 else:
691 final[x] = succs
691 final[x] = succs
692 toproceed.remove(x)
692 toproceed.remove(x)
693 # remove tmpnodes from final mapping
693 # remove tmpnodes from final mapping
694 for n in tmpnodes:
694 for n in tmpnodes:
695 del final[n]
695 del final[n]
696 # we expect all changes involved in final to exist in the repo
696 # we expect all changes involved in final to exist in the repo
697 # turn `final` into list (topologically sorted)
697 # turn `final` into list (topologically sorted)
698 nm = repo.changelog.nodemap
698 nm = repo.changelog.nodemap
699 for prec, succs in final.items():
699 for prec, succs in final.items():
700 final[prec] = sorted(succs, key=nm.get)
700 final[prec] = sorted(succs, key=nm.get)
701
701
702 # computed topmost element (necessary for bookmark)
702 # computed topmost element (necessary for bookmark)
703 if new:
703 if new:
704 newtopmost = max(new, key=repo.changelog.rev)
704 newtopmost = max(new, key=repo.changelog.rev)
705 elif not final:
705 elif not final:
706 # Nothing rewritten at all. we won't need `newtopmost`
706 # Nothing rewritten at all. we won't need `newtopmost`
707 # It is the same as `oldtopmost` and `processreplacement` know it
707 # It is the same as `oldtopmost` and `processreplacement` know it
708 newtopmost = None
708 newtopmost = None
709 else:
709 else:
710 # every body died. The newtopmost is the parent of the root.
710 # every body died. The newtopmost is the parent of the root.
711 newtopmost = repo[min(final, key=repo.changelog.rev)].p1().node()
711 newtopmost = repo[min(final, key=repo.changelog.rev)].p1().node()
712
712
713 return final, tmpnodes, new, newtopmost
713 return final, tmpnodes, new, newtopmost
714
714
715 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
715 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
716 """Move bookmark from old to newly created node"""
716 """Move bookmark from old to newly created node"""
717 if not mapping:
717 if not mapping:
718 # if nothing got rewritten there is not purpose for this function
718 # if nothing got rewritten there is not purpose for this function
719 return
719 return
720 moves = []
720 moves = []
721 for bk, old in repo._bookmarks.iteritems():
721 for bk, old in repo._bookmarks.iteritems():
722 if old == oldtopmost:
722 if old == oldtopmost:
723 # special case ensure bookmark stay on tip.
723 # special case ensure bookmark stay on tip.
724 #
724 #
725 # This is arguably a feature and we may only want that for the
725 # This is arguably a feature and we may only want that for the
726 # active bookmark. But the behavior is kept compatible with the old
726 # active bookmark. But the behavior is kept compatible with the old
727 # version for now.
727 # version for now.
728 moves.append((bk, newtopmost))
728 moves.append((bk, newtopmost))
729 continue
729 continue
730 base = old
730 base = old
731 new = mapping.get(base, None)
731 new = mapping.get(base, None)
732 if new is None:
732 if new is None:
733 continue
733 continue
734 while not new:
734 while not new:
735 # base is killed, trying with parent
735 # base is killed, trying with parent
736 base = repo[base].p1().node()
736 base = repo[base].p1().node()
737 new = mapping.get(base, (base,))
737 new = mapping.get(base, (base,))
738 # nothing to move
738 # nothing to move
739 moves.append((bk, new[-1]))
739 moves.append((bk, new[-1]))
740 if moves:
740 if moves:
741 for mark, new in moves:
741 for mark, new in moves:
742 old = repo._bookmarks[mark]
742 old = repo._bookmarks[mark]
743 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
743 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
744 % (mark, node.short(old), node.short(new)))
744 % (mark, node.short(old), node.short(new)))
745 repo._bookmarks[mark] = new
745 repo._bookmarks[mark] = new
746 bookmarks.write(repo)
746 bookmarks.write(repo)
747
747
748 def cleanupnode(ui, repo, name, nodes):
748 def cleanupnode(ui, repo, name, nodes):
749 """strip a group of nodes from the repository
749 """strip a group of nodes from the repository
750
750
751 The set of node to strip may contains unknown nodes."""
751 The set of node to strip may contains unknown nodes."""
752 ui.debug('should strip %s nodes %s\n' %
752 ui.debug('should strip %s nodes %s\n' %
753 (name, ', '.join([node.short(n) for n in nodes])))
753 (name, ', '.join([node.short(n) for n in nodes])))
754 lock = None
754 lock = None
755 try:
755 try:
756 lock = repo.lock()
756 lock = repo.lock()
757 # Find all node that need to be stripped
757 # Find all node that need to be stripped
758 # (we hg %lr instead of %ln to silently ignore unknown item
758 # (we hg %lr instead of %ln to silently ignore unknown item
759 nm = repo.changelog.nodemap
759 nm = repo.changelog.nodemap
760 nodes = [n for n in nodes if n in nm]
760 nodes = [n for n in nodes if n in nm]
761 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
761 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
762 for c in roots:
762 for c in roots:
763 # We should process node in reverse order to strip tip most first.
763 # We should process node in reverse order to strip tip most first.
764 # but this trigger a bug in changegroup hook.
764 # but this trigger a bug in changegroup hook.
765 # This would reduce bundle overhead
765 # This would reduce bundle overhead
766 repair.strip(ui, repo, c)
766 repair.strip(ui, repo, c)
767 finally:
767 finally:
768 lockmod.release(lock)
768 lockmod.release(lock)
General Comments 0
You need to be logged in to leave comments. Login now