##// END OF EJS Templates
histedit: rename `tip` to `topmost`...
Pierre-Yves David -
r17665:b6553395 default
parent child Browse files
Show More
@@ -1,736 +1,736 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 merge as mergemod
159 from mercurial import merge as mergemod
160 from mercurial.i18n import _
160 from mercurial.i18n import _
161
161
162 cmdtable = {}
162 cmdtable = {}
163 command = cmdutil.command(cmdtable)
163 command = cmdutil.command(cmdtable)
164
164
165 testedwith = 'internal'
165 testedwith = 'internal'
166
166
167 # i18n: command names and abbreviations must remain untranslated
167 # i18n: command names and abbreviations must remain untranslated
168 editcomment = _("""# Edit history between %s and %s
168 editcomment = _("""# Edit history between %s and %s
169 #
169 #
170 # Commands:
170 # Commands:
171 # p, pick = use commit
171 # p, pick = use commit
172 # e, edit = use commit, but stop for amending
172 # e, edit = use commit, but stop for amending
173 # f, fold = use commit, but fold into previous commit (combines N and N-1)
173 # f, fold = use commit, but fold into previous commit (combines N and N-1)
174 # d, drop = remove commit from history
174 # d, drop = remove commit from history
175 # m, mess = edit message without changing commit content
175 # m, mess = edit message without changing commit content
176 #
176 #
177 """)
177 """)
178
178
179 def applychanges(ui, repo, ctx, opts):
179 def applychanges(ui, repo, ctx, opts):
180 """Merge changeset from ctx (only) in the current working directory"""
180 """Merge changeset from ctx (only) in the current working directory"""
181 wcpar = repo.dirstate.parents()[0]
181 wcpar = repo.dirstate.parents()[0]
182 if ctx.p1().node() == wcpar:
182 if ctx.p1().node() == wcpar:
183 # edition ar "in place" we do not need to make any merge,
183 # edition ar "in place" we do not need to make any merge,
184 # just applies changes on parent for edition
184 # just applies changes on parent for edition
185 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
185 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
186 stats = None
186 stats = None
187 else:
187 else:
188 try:
188 try:
189 # ui.forcemerge is an internal variable, do not document
189 # ui.forcemerge is an internal variable, do not document
190 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
190 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''))
191 stats = mergemod.update(repo, ctx.node(), True, True, False,
191 stats = mergemod.update(repo, ctx.node(), True, True, False,
192 ctx.p1().node())
192 ctx.p1().node())
193 finally:
193 finally:
194 repo.ui.setconfig('ui', 'forcemerge', '')
194 repo.ui.setconfig('ui', 'forcemerge', '')
195 repo.setparents(wcpar, node.nullid)
195 repo.setparents(wcpar, node.nullid)
196 repo.dirstate.write()
196 repo.dirstate.write()
197 # fix up dirstate for copies and renames
197 # fix up dirstate for copies and renames
198 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
198 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
199 return stats
199 return stats
200
200
201 def collapse(repo, first, last, commitopts):
201 def collapse(repo, first, last, commitopts):
202 """collapse the set of revisions from first to last as new one.
202 """collapse the set of revisions from first to last as new one.
203
203
204 Expected commit options are:
204 Expected commit options are:
205 - message
205 - message
206 - date
206 - date
207 - username
207 - username
208 Edition of commit message is trigered in all case.
208 Edition of commit message is trigered in all case.
209
209
210 This function works in memory."""
210 This function works in memory."""
211 ctxs = list(repo.set('%d::%d', first, last))
211 ctxs = list(repo.set('%d::%d', first, last))
212 if not ctxs:
212 if not ctxs:
213 return None
213 return None
214 base = first.parents()[0]
214 base = first.parents()[0]
215
215
216 # commit a new version of the old changeset, including the update
216 # commit a new version of the old changeset, including the update
217 # collect all files which might be affected
217 # collect all files which might be affected
218 files = set()
218 files = set()
219 for ctx in ctxs:
219 for ctx in ctxs:
220 files.update(ctx.files())
220 files.update(ctx.files())
221
221
222 # Recompute copies (avoid recording a -> b -> a)
222 # Recompute copies (avoid recording a -> b -> a)
223 copied = copies.pathcopies(first, last)
223 copied = copies.pathcopies(first, last)
224
224
225 # prune files which were reverted by the updates
225 # prune files which were reverted by the updates
226 def samefile(f):
226 def samefile(f):
227 if f in last.manifest():
227 if f in last.manifest():
228 a = last.filectx(f)
228 a = last.filectx(f)
229 if f in base.manifest():
229 if f in base.manifest():
230 b = base.filectx(f)
230 b = base.filectx(f)
231 return (a.data() == b.data()
231 return (a.data() == b.data()
232 and a.flags() == b.flags())
232 and a.flags() == b.flags())
233 else:
233 else:
234 return False
234 return False
235 else:
235 else:
236 return f not in base.manifest()
236 return f not in base.manifest()
237 files = [f for f in files if not samefile(f)]
237 files = [f for f in files if not samefile(f)]
238 # commit version of these files as defined by head
238 # commit version of these files as defined by head
239 headmf = last.manifest()
239 headmf = last.manifest()
240 def filectxfn(repo, ctx, path):
240 def filectxfn(repo, ctx, path):
241 if path in headmf:
241 if path in headmf:
242 fctx = last[path]
242 fctx = last[path]
243 flags = fctx.flags()
243 flags = fctx.flags()
244 mctx = context.memfilectx(fctx.path(), fctx.data(),
244 mctx = context.memfilectx(fctx.path(), fctx.data(),
245 islink='l' in flags,
245 islink='l' in flags,
246 isexec='x' in flags,
246 isexec='x' in flags,
247 copied=copied.get(path))
247 copied=copied.get(path))
248 return mctx
248 return mctx
249 raise IOError()
249 raise IOError()
250
250
251 if commitopts.get('message'):
251 if commitopts.get('message'):
252 message = commitopts['message']
252 message = commitopts['message']
253 else:
253 else:
254 message = first.description()
254 message = first.description()
255 user = commitopts.get('user')
255 user = commitopts.get('user')
256 date = commitopts.get('date')
256 date = commitopts.get('date')
257 extra = first.extra()
257 extra = first.extra()
258
258
259 parents = (first.p1().node(), first.p2().node())
259 parents = (first.p1().node(), first.p2().node())
260 new = context.memctx(repo,
260 new = context.memctx(repo,
261 parents=parents,
261 parents=parents,
262 text=message,
262 text=message,
263 files=files,
263 files=files,
264 filectxfn=filectxfn,
264 filectxfn=filectxfn,
265 user=user,
265 user=user,
266 date=date,
266 date=date,
267 extra=extra)
267 extra=extra)
268 new._text = cmdutil.commitforceeditor(repo, new, [])
268 new._text = cmdutil.commitforceeditor(repo, new, [])
269 return repo.commitctx(new)
269 return repo.commitctx(new)
270
270
271 def pick(ui, repo, ctx, ha, opts):
271 def pick(ui, repo, ctx, ha, opts):
272 oldctx = repo[ha]
272 oldctx = repo[ha]
273 if oldctx.parents()[0] == ctx:
273 if oldctx.parents()[0] == ctx:
274 ui.debug('node %s unchanged\n' % ha)
274 ui.debug('node %s unchanged\n' % ha)
275 return oldctx, [], [], []
275 return oldctx, [], [], []
276 hg.update(repo, ctx.node())
276 hg.update(repo, ctx.node())
277 stats = applychanges(ui, repo, oldctx, opts)
277 stats = applychanges(ui, repo, oldctx, opts)
278 if stats and stats[3] > 0:
278 if stats and stats[3] > 0:
279 raise util.Abort(_('Fix up the change and run '
279 raise util.Abort(_('Fix up the change and run '
280 'hg histedit --continue'))
280 'hg histedit --continue'))
281 # drop the second merge parent
281 # drop the second merge parent
282 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
282 n = repo.commit(text=oldctx.description(), user=oldctx.user(),
283 date=oldctx.date(), extra=oldctx.extra())
283 date=oldctx.date(), extra=oldctx.extra())
284 if n is None:
284 if n is None:
285 ui.warn(_('%s: empty changeset\n')
285 ui.warn(_('%s: empty changeset\n')
286 % node.hex(ha))
286 % node.hex(ha))
287 return ctx, [], [], []
287 return ctx, [], [], []
288 return repo[n], [n], [oldctx.node()], []
288 return repo[n], [n], [oldctx.node()], []
289
289
290
290
291 def edit(ui, repo, ctx, ha, opts):
291 def edit(ui, repo, ctx, ha, opts):
292 oldctx = repo[ha]
292 oldctx = repo[ha]
293 hg.update(repo, ctx.node())
293 hg.update(repo, ctx.node())
294 applychanges(ui, repo, oldctx, opts)
294 applychanges(ui, repo, oldctx, opts)
295 raise util.Abort(_('Make changes as needed, you may commit or record as '
295 raise util.Abort(_('Make changes as needed, you may commit or record as '
296 'needed now.\nWhen you are finished, run hg'
296 'needed now.\nWhen you are finished, run hg'
297 ' histedit --continue to resume.'))
297 ' histedit --continue to resume.'))
298
298
299 def fold(ui, repo, ctx, ha, opts):
299 def fold(ui, repo, ctx, ha, opts):
300 oldctx = repo[ha]
300 oldctx = repo[ha]
301 hg.update(repo, ctx.node())
301 hg.update(repo, ctx.node())
302 stats = applychanges(ui, repo, oldctx, opts)
302 stats = applychanges(ui, repo, oldctx, opts)
303 if stats and stats[3] > 0:
303 if stats and stats[3] > 0:
304 raise util.Abort(_('Fix up the change and run '
304 raise util.Abort(_('Fix up the change and run '
305 'hg histedit --continue'))
305 'hg histedit --continue'))
306 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
306 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
307 date=oldctx.date(), extra=oldctx.extra())
307 date=oldctx.date(), extra=oldctx.extra())
308 if n is None:
308 if n is None:
309 ui.warn(_('%s: empty changeset')
309 ui.warn(_('%s: empty changeset')
310 % node.hex(ha))
310 % node.hex(ha))
311 return ctx, [], [], []
311 return ctx, [], [], []
312 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
312 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
313
313
314 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
314 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
315 parent = ctx.parents()[0].node()
315 parent = ctx.parents()[0].node()
316 hg.update(repo, parent)
316 hg.update(repo, parent)
317 ### prepare new commit data
317 ### prepare new commit data
318 commitopts = opts.copy()
318 commitopts = opts.copy()
319 # username
319 # username
320 if ctx.user() == oldctx.user():
320 if ctx.user() == oldctx.user():
321 username = ctx.user()
321 username = ctx.user()
322 else:
322 else:
323 username = ui.username()
323 username = ui.username()
324 commitopts['user'] = username
324 commitopts['user'] = username
325 # commit message
325 # commit message
326 newmessage = '\n***\n'.join(
326 newmessage = '\n***\n'.join(
327 [ctx.description()] +
327 [ctx.description()] +
328 [repo[r].description() for r in internalchanges] +
328 [repo[r].description() for r in internalchanges] +
329 [oldctx.description()]) + '\n'
329 [oldctx.description()]) + '\n'
330 commitopts['message'] = newmessage
330 commitopts['message'] = newmessage
331 # date
331 # date
332 commitopts['date'] = max(ctx.date(), oldctx.date())
332 commitopts['date'] = max(ctx.date(), oldctx.date())
333 n = collapse(repo, ctx, repo[newnode], commitopts)
333 n = collapse(repo, ctx, repo[newnode], commitopts)
334 if n is None:
334 if n is None:
335 return ctx, [], [], []
335 return ctx, [], [], []
336 hg.update(repo, n)
336 hg.update(repo, n)
337 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
337 return repo[n], [n], [oldctx.node(), ctx.node()], [newnode]
338
338
339 def drop(ui, repo, ctx, ha, opts):
339 def drop(ui, repo, ctx, ha, opts):
340 return ctx, [], [repo[ha].node()], []
340 return ctx, [], [repo[ha].node()], []
341
341
342
342
343 def message(ui, repo, ctx, ha, opts):
343 def message(ui, repo, ctx, ha, opts):
344 oldctx = repo[ha]
344 oldctx = repo[ha]
345 hg.update(repo, ctx.node())
345 hg.update(repo, ctx.node())
346 stats = applychanges(ui, repo, oldctx, opts)
346 stats = applychanges(ui, repo, oldctx, opts)
347 if stats and stats[3] > 0:
347 if stats and stats[3] > 0:
348 raise util.Abort(_('Fix up the change and run '
348 raise util.Abort(_('Fix up the change and run '
349 'hg histedit --continue'))
349 'hg histedit --continue'))
350 message = oldctx.description() + '\n'
350 message = oldctx.description() + '\n'
351 message = ui.edit(message, ui.username())
351 message = ui.edit(message, ui.username())
352 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
352 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
353 extra=oldctx.extra())
353 extra=oldctx.extra())
354 newctx = repo[new]
354 newctx = repo[new]
355 if oldctx.node() != newctx.node():
355 if oldctx.node() != newctx.node():
356 return newctx, [new], [oldctx.node()], []
356 return newctx, [new], [oldctx.node()], []
357 # We didn't make an edit, so just indicate no replaced nodes
357 # We didn't make an edit, so just indicate no replaced nodes
358 return newctx, [new], [], []
358 return newctx, [new], [], []
359
359
360 actiontable = {'p': pick,
360 actiontable = {'p': pick,
361 'pick': pick,
361 'pick': pick,
362 'e': edit,
362 'e': edit,
363 'edit': edit,
363 'edit': edit,
364 'f': fold,
364 'f': fold,
365 'fold': fold,
365 'fold': fold,
366 'd': drop,
366 'd': drop,
367 'drop': drop,
367 'drop': drop,
368 'm': message,
368 'm': message,
369 'mess': message,
369 'mess': message,
370 }
370 }
371
371
372 @command('histedit',
372 @command('histedit',
373 [('', 'commands', '',
373 [('', 'commands', '',
374 _('Read history edits from the specified file.')),
374 _('Read history edits from the specified file.')),
375 ('c', 'continue', False, _('continue an edit already in progress')),
375 ('c', 'continue', False, _('continue an edit already in progress')),
376 ('k', 'keep', False,
376 ('k', 'keep', False,
377 _("don't strip old nodes after edit is complete")),
377 _("don't strip old nodes after edit is complete")),
378 ('', 'abort', False, _('abort an edit in progress')),
378 ('', 'abort', False, _('abort an edit in progress')),
379 ('o', 'outgoing', False, _('changesets not found in destination')),
379 ('o', 'outgoing', False, _('changesets not found in destination')),
380 ('f', 'force', False,
380 ('f', 'force', False,
381 _('force outgoing even for unrelated repositories')),
381 _('force outgoing even for unrelated repositories')),
382 ('r', 'rev', [], _('first revision to be edited'))],
382 ('r', 'rev', [], _('first revision to be edited'))],
383 _("[PARENT]"))
383 _("[PARENT]"))
384 def histedit(ui, repo, *parent, **opts):
384 def histedit(ui, repo, *parent, **opts):
385 """interactively edit changeset history
385 """interactively edit changeset history
386 """
386 """
387 # TODO only abort if we try and histedit mq patches, not just
387 # TODO only abort if we try and histedit mq patches, not just
388 # blanket if mq patches are applied somewhere
388 # blanket if mq patches are applied somewhere
389 mq = getattr(repo, 'mq', None)
389 mq = getattr(repo, 'mq', None)
390 if mq and mq.applied:
390 if mq and mq.applied:
391 raise util.Abort(_('source has mq patches applied'))
391 raise util.Abort(_('source has mq patches applied'))
392
392
393 parent = list(parent) + opts.get('rev', [])
393 parent = list(parent) + opts.get('rev', [])
394 if opts.get('outgoing'):
394 if opts.get('outgoing'):
395 if len(parent) > 1:
395 if len(parent) > 1:
396 raise util.Abort(
396 raise util.Abort(
397 _('only one repo argument allowed with --outgoing'))
397 _('only one repo argument allowed with --outgoing'))
398 elif parent:
398 elif parent:
399 parent = parent[0]
399 parent = parent[0]
400
400
401 dest = ui.expandpath(parent or 'default-push', parent or 'default')
401 dest = ui.expandpath(parent or 'default-push', parent or 'default')
402 dest, revs = hg.parseurl(dest, None)[:2]
402 dest, revs = hg.parseurl(dest, None)[:2]
403 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
403 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
404
404
405 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
405 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
406 other = hg.peer(repo, opts, dest)
406 other = hg.peer(repo, opts, dest)
407
407
408 if revs:
408 if revs:
409 revs = [repo.lookup(rev) for rev in revs]
409 revs = [repo.lookup(rev) for rev in revs]
410
410
411 parent = discovery.findcommonoutgoing(
411 parent = discovery.findcommonoutgoing(
412 repo, other, [], force=opts.get('force')).missing[0:1]
412 repo, other, [], force=opts.get('force')).missing[0:1]
413 else:
413 else:
414 if opts.get('force'):
414 if opts.get('force'):
415 raise util.Abort(_('--force only allowed with --outgoing'))
415 raise util.Abort(_('--force only allowed with --outgoing'))
416
416
417 if opts.get('continue', False):
417 if opts.get('continue', False):
418 if len(parent) != 0:
418 if len(parent) != 0:
419 raise util.Abort(_('no arguments allowed with --continue'))
419 raise util.Abort(_('no arguments allowed with --continue'))
420 (parentctxnode, created, replaced,
420 (parentctxnode, created, replaced, tmpnodes,
421 tmpnodes, existing, rules, keep, tip, replacemap) = readstate(repo)
421 existing, rules, keep, topmost, replacemap) = readstate(repo)
422 currentparent, wantnull = repo.dirstate.parents()
422 currentparent, wantnull = repo.dirstate.parents()
423 parentctx = repo[parentctxnode]
423 parentctx = repo[parentctxnode]
424 # existing is the list of revisions initially considered by
424 # existing is the list of revisions initially considered by
425 # histedit. Here we use it to list new changesets, descendants
425 # histedit. Here we use it to list new changesets, descendants
426 # of parentctx without an 'existing' changeset in-between. We
426 # of parentctx without an 'existing' changeset in-between. We
427 # also have to exclude 'existing' changesets which were
427 # also have to exclude 'existing' changesets which were
428 # previously dropped.
428 # previously dropped.
429 descendants = set(c.node() for c in
429 descendants = set(c.node() for c in
430 repo.set('(%n::) - %n', parentctxnode, parentctxnode))
430 repo.set('(%n::) - %n', parentctxnode, parentctxnode))
431 existing = set(existing)
431 existing = set(existing)
432 notdropped = set(n for n in existing if n in descendants and
432 notdropped = set(n for n in existing if n in descendants and
433 (n not in replacemap or replacemap[n] in descendants))
433 (n not in replacemap or replacemap[n] in descendants))
434 # Discover any nodes the user has added in the interim. We can
434 # Discover any nodes the user has added in the interim. We can
435 # miss changesets which were dropped and recreated the same.
435 # miss changesets which were dropped and recreated the same.
436 newchildren = list(c.node() for c in repo.set(
436 newchildren = list(c.node() for c in repo.set(
437 'sort(%ln - (%ln or %ln::))', descendants, existing, notdropped))
437 'sort(%ln - (%ln or %ln::))', descendants, existing, notdropped))
438 action, currentnode = rules.pop(0)
438 action, currentnode = rules.pop(0)
439 if action in ('f', 'fold'):
439 if action in ('f', 'fold'):
440 tmpnodes.extend(newchildren)
440 tmpnodes.extend(newchildren)
441 else:
441 else:
442 created.extend(newchildren)
442 created.extend(newchildren)
443
443
444 m, a, r, d = repo.status()[:4]
444 m, a, r, d = repo.status()[:4]
445 oldctx = repo[currentnode]
445 oldctx = repo[currentnode]
446 message = oldctx.description() + '\n'
446 message = oldctx.description() + '\n'
447 if action in ('e', 'edit', 'm', 'mess'):
447 if action in ('e', 'edit', 'm', 'mess'):
448 message = ui.edit(message, ui.username())
448 message = ui.edit(message, ui.username())
449 elif action in ('f', 'fold'):
449 elif action in ('f', 'fold'):
450 message = 'fold-temp-revision %s' % currentnode
450 message = 'fold-temp-revision %s' % currentnode
451 new = None
451 new = None
452 if m or a or r or d:
452 if m or a or r or d:
453 new = repo.commit(text=message, user=oldctx.user(),
453 new = repo.commit(text=message, user=oldctx.user(),
454 date=oldctx.date(), extra=oldctx.extra())
454 date=oldctx.date(), extra=oldctx.extra())
455
455
456 # If we're resuming a fold and we have new changes, mark the
456 # If we're resuming a fold and we have new changes, mark the
457 # replacements and finish the fold. If not, it's more like a
457 # replacements and finish the fold. If not, it's more like a
458 # drop of the changesets that disappeared, and we can skip
458 # drop of the changesets that disappeared, and we can skip
459 # this step.
459 # this step.
460 if action in ('f', 'fold') and (new or newchildren):
460 if action in ('f', 'fold') and (new or newchildren):
461 if new:
461 if new:
462 tmpnodes.append(new)
462 tmpnodes.append(new)
463 else:
463 else:
464 new = newchildren[-1]
464 new = newchildren[-1]
465 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
465 (parentctx, created_, replaced_, tmpnodes_) = finishfold(
466 ui, repo, parentctx, oldctx, new, opts, newchildren)
466 ui, repo, parentctx, oldctx, new, opts, newchildren)
467 replaced.extend(replaced_)
467 replaced.extend(replaced_)
468 created.extend(created_)
468 created.extend(created_)
469 tmpnodes.extend(tmpnodes_)
469 tmpnodes.extend(tmpnodes_)
470 elif action not in ('d', 'drop'):
470 elif action not in ('d', 'drop'):
471 if new != oldctx.node():
471 if new != oldctx.node():
472 replaced.append(oldctx.node())
472 replaced.append(oldctx.node())
473 if new:
473 if new:
474 if new != oldctx.node():
474 if new != oldctx.node():
475 created.append(new)
475 created.append(new)
476 parentctx = repo[new]
476 parentctx = repo[new]
477
477
478 elif opts.get('abort', False):
478 elif opts.get('abort', False):
479 if len(parent) != 0:
479 if len(parent) != 0:
480 raise util.Abort(_('no arguments allowed with --abort'))
480 raise util.Abort(_('no arguments allowed with --abort'))
481 (parentctxnode, created, replaced, tmpnodes,
481 (parentctxnode, created, replaced, tmpnodes,
482 existing, rules, keep, tip, replacemap) = readstate(repo)
482 existing, rules, keep, topmost, replacemap) = readstate(repo)
483 ui.debug('restore wc to old tip %s\n' % node.hex(tip))
483 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
484 hg.clean(repo, tip)
484 hg.clean(repo, topmost)
485 cleanupnode(ui, repo, 'created', created)
485 cleanupnode(ui, repo, 'created', created)
486 cleanupnode(ui, repo, 'temp', tmpnodes)
486 cleanupnode(ui, repo, 'temp', tmpnodes)
487 os.unlink(os.path.join(repo.path, 'histedit-state'))
487 os.unlink(os.path.join(repo.path, 'histedit-state'))
488 return
488 return
489 else:
489 else:
490 cmdutil.bailifchanged(repo)
490 cmdutil.bailifchanged(repo)
491 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
491 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
492 raise util.Abort(_('history edit already in progress, try '
492 raise util.Abort(_('history edit already in progress, try '
493 '--continue or --abort'))
493 '--continue or --abort'))
494
494
495 tip, empty = repo.dirstate.parents()
495 topmost, empty = repo.dirstate.parents()
496
496
497
497
498 if len(parent) != 1:
498 if len(parent) != 1:
499 raise util.Abort(_('histedit requires exactly one parent revision'))
499 raise util.Abort(_('histedit requires exactly one parent revision'))
500 parent = scmutil.revsingle(repo, parent[0]).node()
500 parent = scmutil.revsingle(repo, parent[0]).node()
501
501
502 keep = opts.get('keep', False)
502 keep = opts.get('keep', False)
503 revs = between(repo, parent, tip, keep)
503 revs = between(repo, parent, topmost, keep)
504
504
505 ctxs = [repo[r] for r in revs]
505 ctxs = [repo[r] for r in revs]
506 existing = [r.node() for r in ctxs]
506 existing = [r.node() for r in ctxs]
507 rules = opts.get('commands', '')
507 rules = opts.get('commands', '')
508 if not rules:
508 if not rules:
509 rules = '\n'.join([makedesc(c) for c in ctxs])
509 rules = '\n'.join([makedesc(c) for c in ctxs])
510 rules += '\n\n'
510 rules += '\n\n'
511 rules += editcomment % (node.short(parent), node.short(tip))
511 rules += editcomment % (node.short(parent), node.short(topmost))
512 rules = ui.edit(rules, ui.username())
512 rules = ui.edit(rules, ui.username())
513 # Save edit rules in .hg/histedit-last-edit.txt in case
513 # Save edit rules in .hg/histedit-last-edit.txt in case
514 # the user needs to ask for help after something
514 # the user needs to ask for help after something
515 # surprising happens.
515 # surprising happens.
516 f = open(repo.join('histedit-last-edit.txt'), 'w')
516 f = open(repo.join('histedit-last-edit.txt'), 'w')
517 f.write(rules)
517 f.write(rules)
518 f.close()
518 f.close()
519 else:
519 else:
520 f = open(rules)
520 f = open(rules)
521 rules = f.read()
521 rules = f.read()
522 f.close()
522 f.close()
523 rules = [l for l in (r.strip() for r in rules.splitlines())
523 rules = [l for l in (r.strip() for r in rules.splitlines())
524 if l and not l[0] == '#']
524 if l and not l[0] == '#']
525 rules = verifyrules(rules, repo, ctxs)
525 rules = verifyrules(rules, repo, ctxs)
526
526
527 parentctx = repo[parent].parents()[0]
527 parentctx = repo[parent].parents()[0]
528 keep = opts.get('keep', False)
528 keep = opts.get('keep', False)
529 replaced = []
529 replaced = []
530 replacemap = {}
530 replacemap = {}
531 tmpnodes = []
531 tmpnodes = []
532 created = []
532 created = []
533
533
534
534
535 while rules:
535 while rules:
536 writestate(repo, parentctx.node(), created, replaced,
536 writestate(repo, parentctx.node(), created, replaced,
537 tmpnodes, existing, rules, keep, tip, replacemap)
537 tmpnodes, existing, rules, keep, topmost, replacemap)
538 action, ha = rules.pop(0)
538 action, ha = rules.pop(0)
539 ui.debug('histedit: processing %s %s\n' % (action, ha))
539 ui.debug('histedit: processing %s %s\n' % (action, ha))
540 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
540 (parentctx, created_, replaced_, tmpnodes_) = actiontable[action](
541 ui, repo, parentctx, ha, opts)
541 ui, repo, parentctx, ha, opts)
542
542
543 if replaced_:
543 if replaced_:
544 clen, rlen = len(created_), len(replaced_)
544 clen, rlen = len(created_), len(replaced_)
545 if clen == rlen == 1:
545 if clen == rlen == 1:
546 ui.debug('histedit: exact replacement of %s with %s\n' % (
546 ui.debug('histedit: exact replacement of %s with %s\n' % (
547 node.short(replaced_[0]), node.short(created_[0])))
547 node.short(replaced_[0]), node.short(created_[0])))
548
548
549 replacemap[replaced_[0]] = created_[0]
549 replacemap[replaced_[0]] = created_[0]
550 elif clen > rlen:
550 elif clen > rlen:
551 assert rlen == 1, ('unexpected replacement of '
551 assert rlen == 1, ('unexpected replacement of '
552 '%d changes with %d changes' % (rlen, clen))
552 '%d changes with %d changes' % (rlen, clen))
553 # made more changesets than we're replacing
553 # made more changesets than we're replacing
554 # TODO synthesize patch names for created patches
554 # TODO synthesize patch names for created patches
555 replacemap[replaced_[0]] = created_[-1]
555 replacemap[replaced_[0]] = created_[-1]
556 ui.debug('histedit: created many, assuming %s replaced by %s' %
556 ui.debug('histedit: created many, assuming %s replaced by %s' %
557 (node.short(replaced_[0]), node.short(created_[-1])))
557 (node.short(replaced_[0]), node.short(created_[-1])))
558 elif rlen > clen:
558 elif rlen > clen:
559 if not created_:
559 if not created_:
560 # This must be a drop. Try and put our metadata on
560 # This must be a drop. Try and put our metadata on
561 # the parent change.
561 # the parent change.
562 assert rlen == 1
562 assert rlen == 1
563 r = replaced_[0]
563 r = replaced_[0]
564 ui.debug('histedit: %s seems replaced with nothing, '
564 ui.debug('histedit: %s seems replaced with nothing, '
565 'finding a parent\n' % (node.short(r)))
565 'finding a parent\n' % (node.short(r)))
566 pctx = repo[r].parents()[0]
566 pctx = repo[r].parents()[0]
567 if pctx.node() in replacemap:
567 if pctx.node() in replacemap:
568 ui.debug('histedit: parent is already replaced\n')
568 ui.debug('histedit: parent is already replaced\n')
569 replacemap[r] = replacemap[pctx.node()]
569 replacemap[r] = replacemap[pctx.node()]
570 else:
570 else:
571 replacemap[r] = pctx.node()
571 replacemap[r] = pctx.node()
572 ui.debug('histedit: %s best replaced by %s\n' % (
572 ui.debug('histedit: %s best replaced by %s\n' % (
573 node.short(r), node.short(replacemap[r])))
573 node.short(r), node.short(replacemap[r])))
574 else:
574 else:
575 assert len(created_) == 1
575 assert len(created_) == 1
576 for r in replaced_:
576 for r in replaced_:
577 ui.debug('histedit: %s replaced by %s\n' % (
577 ui.debug('histedit: %s replaced by %s\n' % (
578 node.short(r), node.short(created_[0])))
578 node.short(r), node.short(created_[0])))
579 replacemap[r] = created_[0]
579 replacemap[r] = created_[0]
580 else:
580 else:
581 assert False, (
581 assert False, (
582 'Unhandled case in replacement mapping! '
582 'Unhandled case in replacement mapping! '
583 'replacing %d changes with %d changes' % (rlen, clen))
583 'replacing %d changes with %d changes' % (rlen, clen))
584 created.extend(created_)
584 created.extend(created_)
585 replaced.extend(replaced_)
585 replaced.extend(replaced_)
586 tmpnodes.extend(tmpnodes_)
586 tmpnodes.extend(tmpnodes_)
587
587
588 hg.update(repo, parentctx.node())
588 hg.update(repo, parentctx.node())
589
589
590 if not keep:
590 if not keep:
591 if replacemap:
591 if replacemap:
592 movebookmarks(ui, repo, replacemap, tmpnodes, created)
592 movebookmarks(ui, repo, replacemap, tmpnodes, created)
593 # TODO update mq state
593 # TODO update mq state
594 cleanupnode(ui, repo, 'replaced', replaced)
594 cleanupnode(ui, repo, 'replaced', replaced)
595
595
596 cleanupnode(ui, repo, 'temp', tmpnodes)
596 cleanupnode(ui, repo, 'temp', tmpnodes)
597 os.unlink(os.path.join(repo.path, 'histedit-state'))
597 os.unlink(os.path.join(repo.path, 'histedit-state'))
598 if os.path.exists(repo.sjoin('undo')):
598 if os.path.exists(repo.sjoin('undo')):
599 os.unlink(repo.sjoin('undo'))
599 os.unlink(repo.sjoin('undo'))
600
600
601
601
602 def between(repo, old, new, keep):
602 def between(repo, old, new, keep):
603 """select and validate the set of revision to edit
603 """select and validate the set of revision to edit
604
604
605 When keep is false, the specified set can't have children."""
605 When keep is false, the specified set can't have children."""
606 revs = [old]
606 revs = [old]
607 current = old
607 current = old
608 while current != new:
608 while current != new:
609 ctx = repo[current]
609 ctx = repo[current]
610 if not keep and len(ctx.children()) > 1:
610 if not keep and len(ctx.children()) > 1:
611 raise util.Abort(_('cannot edit history that would orphan nodes'))
611 raise util.Abort(_('cannot edit history that would orphan nodes'))
612 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
612 if len(ctx.parents()) != 1 and ctx.parents()[1] != node.nullid:
613 raise util.Abort(_("can't edit history with merges"))
613 raise util.Abort(_("can't edit history with merges"))
614 if not ctx.children():
614 if not ctx.children():
615 current = new
615 current = new
616 else:
616 else:
617 current = ctx.children()[0].node()
617 current = ctx.children()[0].node()
618 revs.append(current)
618 revs.append(current)
619 if len(repo[current].children()) and not keep:
619 if len(repo[current].children()) and not keep:
620 raise util.Abort(_('cannot edit history that would orphan nodes'))
620 raise util.Abort(_('cannot edit history that would orphan nodes'))
621 return revs
621 return revs
622
622
623
623
624 def writestate(repo, parentctxnode, created, replaced,
624 def writestate(repo, parentctxnode, created, replaced,
625 tmpnodes, existing, rules, keep, oldtip, replacemap):
625 tmpnodes, existing, rules, keep, topmost, replacemap):
626 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
626 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
627 pickle.dump((parentctxnode, created, replaced,
627 pickle.dump((parentctxnode, created, replaced,
628 tmpnodes, existing, rules, keep, oldtip, replacemap),
628 tmpnodes, existing, rules, keep, topmost, replacemap),
629 fp)
629 fp)
630 fp.close()
630 fp.close()
631
631
632 def readstate(repo):
632 def readstate(repo):
633 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
633 """Returns a tuple of (parentnode, created, replaced, tmp, existing, rules,
634 keep, oldtip, replacemap ).
634 keep, topmost, replacemap ).
635 """
635 """
636 fp = open(os.path.join(repo.path, 'histedit-state'))
636 fp = open(os.path.join(repo.path, 'histedit-state'))
637 return pickle.load(fp)
637 return pickle.load(fp)
638
638
639
639
640 def makedesc(c):
640 def makedesc(c):
641 """build a initial action line for a ctx `c`
641 """build a initial action line for a ctx `c`
642
642
643 line are in the form:
643 line are in the form:
644
644
645 pick <hash> <rev> <summary>
645 pick <hash> <rev> <summary>
646 """
646 """
647 summary = ''
647 summary = ''
648 if c.description():
648 if c.description():
649 summary = c.description().splitlines()[0]
649 summary = c.description().splitlines()[0]
650 line = 'pick %s %d %s' % (c, c.rev(), summary)
650 line = 'pick %s %d %s' % (c, c.rev(), summary)
651 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
651 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
652
652
653 def verifyrules(rules, repo, ctxs):
653 def verifyrules(rules, repo, ctxs):
654 """Verify that there exists exactly one edit rule per given changeset.
654 """Verify that there exists exactly one edit rule per given changeset.
655
655
656 Will abort if there are to many or too few rules, a malformed rule,
656 Will abort if there are to many or too few rules, a malformed rule,
657 or a rule on a changeset outside of the user-given range.
657 or a rule on a changeset outside of the user-given range.
658 """
658 """
659 parsed = []
659 parsed = []
660 if len(rules) != len(ctxs):
660 if len(rules) != len(ctxs):
661 raise util.Abort(_('must specify a rule for each changeset once'))
661 raise util.Abort(_('must specify a rule for each changeset once'))
662 for r in rules:
662 for r in rules:
663 if ' ' not in r:
663 if ' ' not in r:
664 raise util.Abort(_('malformed line "%s"') % r)
664 raise util.Abort(_('malformed line "%s"') % r)
665 action, rest = r.split(' ', 1)
665 action, rest = r.split(' ', 1)
666 if ' ' in rest.strip():
666 if ' ' in rest.strip():
667 ha, rest = rest.split(' ', 1)
667 ha, rest = rest.split(' ', 1)
668 else:
668 else:
669 ha = r.strip()
669 ha = r.strip()
670 try:
670 try:
671 if repo[ha] not in ctxs:
671 if repo[ha] not in ctxs:
672 raise util.Abort(
672 raise util.Abort(
673 _('may not use changesets other than the ones listed'))
673 _('may not use changesets other than the ones listed'))
674 except error.RepoError:
674 except error.RepoError:
675 raise util.Abort(_('unknown changeset %s listed') % ha)
675 raise util.Abort(_('unknown changeset %s listed') % ha)
676 if action not in actiontable:
676 if action not in actiontable:
677 raise util.Abort(_('unknown action "%s"') % action)
677 raise util.Abort(_('unknown action "%s"') % action)
678 parsed.append([action, ha])
678 parsed.append([action, ha])
679 return parsed
679 return parsed
680
680
681 def movebookmarks(ui, repo, replacemap, tmpnodes, created):
681 def movebookmarks(ui, repo, replacemap, tmpnodes, created):
682 """Move bookmark from old to newly created node"""
682 """Move bookmark from old to newly created node"""
683 ui.note(_('histedit: Should update metadata for the following '
683 ui.note(_('histedit: Should update metadata for the following '
684 'changes:\n'))
684 'changes:\n'))
685
685
686 def copybms(old, new):
686 def copybms(old, new):
687 if old in tmpnodes or old in created:
687 if old in tmpnodes or old in created:
688 # can't have any metadata we'd want to update
688 # can't have any metadata we'd want to update
689 return
689 return
690 while new in replacemap:
690 while new in replacemap:
691 new = replacemap[new]
691 new = replacemap[new]
692 ui.note(_('histedit: %s to %s\n') % (node.short(old),
692 ui.note(_('histedit: %s to %s\n') % (node.short(old),
693 node.short(new)))
693 node.short(new)))
694 octx = repo[old]
694 octx = repo[old]
695 marks = octx.bookmarks()
695 marks = octx.bookmarks()
696 if marks:
696 if marks:
697 ui.note(_('histedit: moving bookmarks %s\n') %
697 ui.note(_('histedit: moving bookmarks %s\n') %
698 ', '.join(marks))
698 ', '.join(marks))
699 for mark in marks:
699 for mark in marks:
700 repo._bookmarks[mark] = new
700 repo._bookmarks[mark] = new
701 bookmarks.write(repo)
701 bookmarks.write(repo)
702
702
703 # We assume that bookmarks on the tip should remain
703 # We assume that bookmarks on the tip should remain
704 # tipmost, but bookmarks on non-tip changesets should go
704 # tipmost, but bookmarks on non-tip changesets should go
705 # to their most reasonable successor. As a result, find
705 # to their most reasonable successor. As a result, find
706 # the old tip and new tip and copy those bookmarks first,
706 # the old tip and new tip and copy those bookmarks first,
707 # then do the rest of the bookmark copies.
707 # then do the rest of the bookmark copies.
708 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
708 oldtip = sorted(replacemap.keys(), key=repo.changelog.rev)[-1]
709 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
709 newtip = sorted(replacemap.values(), key=repo.changelog.rev)[-1]
710 copybms(oldtip, newtip)
710 copybms(oldtip, newtip)
711
711
712 for old, new in sorted(replacemap.iteritems()):
712 for old, new in sorted(replacemap.iteritems()):
713 copybms(old, new)
713 copybms(old, new)
714
714
715 def cleanupnode(ui, repo, name, nodes):
715 def cleanupnode(ui, repo, name, nodes):
716 """strip a group of nodes from the repository
716 """strip a group of nodes from the repository
717
717
718 The set of node to strip may contains unknown nodes."""
718 The set of node to strip may contains unknown nodes."""
719 ui.debug('should strip %s nodes %s\n' %
719 ui.debug('should strip %s nodes %s\n' %
720 (name, ', '.join([node.short(n) for n in nodes])))
720 (name, ', '.join([node.short(n) for n in nodes])))
721 lock = None
721 lock = None
722 try:
722 try:
723 lock = repo.lock()
723 lock = repo.lock()
724 # Find all node that need to be stripped
724 # Find all node that need to be stripped
725 # (we hg %lr instead of %ln to silently ignore unknown item
725 # (we hg %lr instead of %ln to silently ignore unknown item
726 nm = repo.changelog.nodemap
726 nm = repo.changelog.nodemap
727 nodes = [n for n in nodes if n in nm]
727 nodes = [n for n in nodes if n in nm]
728 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
728 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
729 for c in roots:
729 for c in roots:
730 # We should process node in reverse order to strip tip most first.
730 # We should process node in reverse order to strip tip most first.
731 # but this trigger a bug in changegroup hook.
731 # but this trigger a bug in changegroup hook.
732 # This would reduce bundle overhead
732 # This would reduce bundle overhead
733 repair.strip(ui, repo, c)
733 repair.strip(ui, repo, c)
734 finally:
734 finally:
735 lockmod.release(lock)
735 lockmod.release(lock)
736
736
General Comments 0
You need to be logged in to leave comments. Login now