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