##// END OF EJS Templates
histedit: process bookmarks in sorted order
Mads Kiilerich -
r18370:c605e12d default
parent child Browse files
Show More
@@ -1,771 +1,771 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commands:
33 # Commands:
34 # p, pick = use commit
34 # p, pick = use commit
35 # e, edit = use commit, but stop for amending
35 # e, edit = use commit, but stop for amending
36 # f, fold = use commit, but fold into previous commit (combines N and N-1)
36 # f, fold = use commit, but fold into previous commit (combines N and N-1)
37 # d, drop = remove commit from history
37 # d, drop = remove commit from history
38 # m, mess = edit message without changing commit content
38 # m, mess = edit message without changing commit content
39 #
39 #
40
40
41 In this file, lines beginning with ``#`` are ignored. You must specify a rule
41 In this file, lines beginning with ``#`` are ignored. You must specify a rule
42 for each revision in your history. For example, if you had meant to add gamma
42 for each revision in your history. For example, if you had meant to add gamma
43 before beta, and then wanted to add delta in the same revision as beta, you
43 before beta, and then wanted to add delta in the same revision as beta, you
44 would reorganize the file to look like this::
44 would reorganize the file to look like this::
45
45
46 pick 030b686bedc4 Add gamma
46 pick 030b686bedc4 Add gamma
47 pick c561b4e977df Add beta
47 pick c561b4e977df Add beta
48 fold 7c2fd3b9020c Add delta
48 fold 7c2fd3b9020c Add delta
49
49
50 # Edit history between c561b4e977df and 7c2fd3b9020c
50 # Edit history between c561b4e977df and 7c2fd3b9020c
51 #
51 #
52 # Commands:
52 # Commands:
53 # p, pick = use commit
53 # p, pick = use commit
54 # e, edit = use commit, but stop for amending
54 # e, edit = use commit, but stop for amending
55 # f, fold = use commit, but fold into previous commit (combines N and N-1)
55 # f, fold = use commit, but fold into previous commit (combines N and N-1)
56 # d, drop = remove commit from history
56 # d, drop = remove commit from history
57 # m, mess = edit message without changing commit content
57 # m, mess = edit message without changing commit content
58 #
58 #
59
59
60 At which point you close the editor and ``histedit`` starts working. When you
60 At which point you close the editor and ``histedit`` starts working. When you
61 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
61 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
62 those revisions together, offering you a chance to clean up the commit message::
62 those revisions together, offering you a chance to clean up the commit message::
63
63
64 Add beta
64 Add beta
65 ***
65 ***
66 Add delta
66 Add delta
67
67
68 Edit the commit message to your liking, then close the editor. For
68 Edit the commit message to your liking, then close the editor. For
69 this example, let's assume that the commit message was changed to
69 this example, let's assume that the commit message was changed to
70 ``Add beta and delta.`` After histedit has run and had a chance to
70 ``Add beta and delta.`` After histedit has run and had a chance to
71 remove any old or temporary revisions it needed, the history looks
71 remove any old or temporary revisions it needed, the history looks
72 like this::
72 like this::
73
73
74 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
74 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
75 | Add beta and delta.
75 | Add beta and delta.
76 |
76 |
77 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
77 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
78 | Add gamma
78 | Add gamma
79 |
79 |
80 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
80 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
81 Add alpha
81 Add alpha
82
82
83 Note that ``histedit`` does *not* remove any revisions (even its own temporary
83 Note that ``histedit`` does *not* remove any revisions (even its own temporary
84 ones) until after it has completed all the editing operations, so it will
84 ones) until after it has completed all the editing operations, so it will
85 probably perform several strip operations when it's done. For the above example,
85 probably perform several strip operations when it's done. For the above example,
86 it had to run strip twice. Strip can be slow depending on a variety of factors,
86 it had to run strip twice. Strip can be slow depending on a variety of factors,
87 so you might need to be a little patient. You can choose to keep the original
87 so you might need to be a little patient. You can choose to keep the original
88 revisions by passing the ``--keep`` flag.
88 revisions by passing the ``--keep`` flag.
89
89
90 The ``edit`` operation will drop you back to a command prompt,
90 The ``edit`` operation will drop you back to a command prompt,
91 allowing you to edit files freely, or even use ``hg record`` to commit
91 allowing you to edit files freely, or even use ``hg record`` to commit
92 some changes as a separate commit. When you're done, any remaining
92 some changes as a separate commit. When you're done, any remaining
93 uncommitted changes will be committed as well. When done, run ``hg
93 uncommitted changes will be committed as well. When done, run ``hg
94 histedit --continue`` to finish this step. You'll be prompted for a
94 histedit --continue`` to finish this step. You'll be prompted for a
95 new commit message, but the default commit message will be the
95 new commit message, but the default commit message will be the
96 original message for the ``edit`` ed revision.
96 original message for the ``edit`` ed revision.
97
97
98 The ``message`` operation will give you a chance to revise a commit
98 The ``message`` operation will give you a chance to revise a commit
99 message without changing the contents. It's a shortcut for doing
99 message without changing the contents. It's a shortcut for doing
100 ``edit`` immediately followed by `hg histedit --continue``.
100 ``edit`` immediately followed by `hg histedit --continue``.
101
101
102 If ``histedit`` encounters a conflict when moving a revision (while
102 If ``histedit`` encounters a conflict when moving a revision (while
103 handling ``pick`` or ``fold``), it'll stop in a similar manner to
103 handling ``pick`` or ``fold``), it'll stop in a similar manner to
104 ``edit`` with the difference that it won't prompt you for a commit
104 ``edit`` with the difference that it won't prompt you for a commit
105 message when done. If you decide at this point that you don't like how
105 message when done. If you decide at this point that you don't like how
106 much work it will be to rearrange history, or that you made a mistake,
106 much work it will be to rearrange history, or that you made a mistake,
107 you can use ``hg histedit --abort`` to abandon the new changes you
107 you can use ``hg histedit --abort`` to abandon the new changes you
108 have made and return to the state before you attempted to edit your
108 have made and return to the state before you attempted to edit your
109 history.
109 history.
110
110
111 If we clone the histedit-ed example repository above and add four more
111 If we clone the histedit-ed example repository above and add four more
112 changes, such that we have the following history::
112 changes, such that we have the following history::
113
113
114 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
114 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
115 | Add theta
115 | Add theta
116 |
116 |
117 o 5 140988835471 2009-04-27 18:04 -0500 stefan
117 o 5 140988835471 2009-04-27 18:04 -0500 stefan
118 | Add eta
118 | Add eta
119 |
119 |
120 o 4 122930637314 2009-04-27 18:04 -0500 stefan
120 o 4 122930637314 2009-04-27 18:04 -0500 stefan
121 | Add zeta
121 | Add zeta
122 |
122 |
123 o 3 836302820282 2009-04-27 18:04 -0500 stefan
123 o 3 836302820282 2009-04-27 18:04 -0500 stefan
124 | Add epsilon
124 | Add epsilon
125 |
125 |
126 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
126 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
127 | Add beta and delta.
127 | Add beta and delta.
128 |
128 |
129 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
129 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
130 | Add gamma
130 | Add gamma
131 |
131 |
132 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
132 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
133 Add alpha
133 Add alpha
134
134
135 If you run ``hg histedit --outgoing`` on the clone then it is the same
135 If you run ``hg histedit --outgoing`` on the clone then it is the same
136 as running ``hg histedit 836302820282``. If you need plan to push to a
136 as running ``hg histedit 836302820282``. If you need plan to push to a
137 repository that Mercurial does not detect to be related to the source
137 repository that Mercurial does not detect to be related to the source
138 repo, you can add a ``--force`` option.
138 repo, you can add a ``--force`` option.
139 """
139 """
140
140
141 try:
141 try:
142 import cPickle as pickle
142 import cPickle as pickle
143 except ImportError:
143 except ImportError:
144 import pickle
144 import pickle
145 import os
145 import os
146
146
147 from mercurial import cmdutil
147 from mercurial import cmdutil
148 from mercurial import discovery
148 from mercurial import discovery
149 from mercurial import error
149 from mercurial import error
150 from mercurial import copies
150 from mercurial import copies
151 from mercurial import context
151 from mercurial import context
152 from mercurial import hg
152 from mercurial import hg
153 from mercurial import lock as lockmod
153 from mercurial import lock as lockmod
154 from mercurial import node
154 from mercurial import node
155 from mercurial import repair
155 from mercurial import repair
156 from mercurial import scmutil
156 from mercurial import scmutil
157 from mercurial import util
157 from mercurial import util
158 from mercurial import obsolete
158 from mercurial import obsolete
159 from mercurial import merge as mergemod
159 from mercurial import merge as mergemod
160 from mercurial.i18n import _
160 from mercurial.i18n import _
161
161
162 cmdtable = {}
162 cmdtable = {}
163 command = cmdutil.command(cmdtable)
163 command = cmdutil.command(cmdtable)
164
164
165 testedwith = 'internal'
165 testedwith = 'internal'
166
166
167 # i18n: command names and abbreviations must remain untranslated
167 # i18n: command names and abbreviations must remain untranslated
168 editcomment = _("""# Edit history between %s and %s
168 editcomment = _("""# Edit history between %s and %s
169 #
169 #
170 # Commands:
170 # Commands:
171 # p, pick = use commit
171 # p, pick = use commit
172 # e, edit = use commit, but stop for amending
172 # e, edit = use commit, but stop for amending
173 # f, fold = use commit, but fold into previous commit (combines N and N-1)
173 # f, fold = use commit, but fold into previous commit (combines N and N-1)
174 # d, drop = remove commit from history
174 # d, drop = remove commit from history
175 # m, mess = edit message without changing commit content
175 # m, mess = edit message without changing commit content
176 #
176 #
177 """)
177 """)
178
178
179 def 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 Commit message is edited in all cases.
208 Commit message is edited in all cases.
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 new = repo[n]
288 new = repo[n]
289 return new, [(oldctx.node(), (n,))]
289 return new, [(oldctx.node(), (n,))]
290
290
291
291
292 def edit(ui, repo, ctx, ha, opts):
292 def edit(ui, repo, ctx, ha, opts):
293 oldctx = repo[ha]
293 oldctx = repo[ha]
294 hg.update(repo, ctx.node())
294 hg.update(repo, ctx.node())
295 applychanges(ui, repo, oldctx, opts)
295 applychanges(ui, repo, oldctx, opts)
296 raise util.Abort(_('Make changes as needed, you may commit or record as '
296 raise util.Abort(_('Make changes as needed, you may commit or record as '
297 'needed now.\nWhen you are finished, run hg'
297 'needed now.\nWhen you are finished, run hg'
298 ' histedit --continue to resume.'))
298 ' histedit --continue to resume.'))
299
299
300 def fold(ui, repo, ctx, ha, opts):
300 def fold(ui, repo, ctx, ha, opts):
301 oldctx = repo[ha]
301 oldctx = repo[ha]
302 hg.update(repo, ctx.node())
302 hg.update(repo, ctx.node())
303 stats = applychanges(ui, repo, oldctx, opts)
303 stats = applychanges(ui, repo, oldctx, opts)
304 if stats and stats[3] > 0:
304 if stats and stats[3] > 0:
305 raise util.Abort(_('Fix up the change and run '
305 raise util.Abort(_('Fix up the change and run '
306 'hg histedit --continue'))
306 'hg histedit --continue'))
307 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
307 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
308 date=oldctx.date(), extra=oldctx.extra())
308 date=oldctx.date(), extra=oldctx.extra())
309 if n is None:
309 if n is None:
310 ui.warn(_('%s: empty changeset')
310 ui.warn(_('%s: empty changeset')
311 % node.hex(ha))
311 % node.hex(ha))
312 return ctx, []
312 return ctx, []
313 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
313 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
314
314
315 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
315 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
316 parent = ctx.parents()[0].node()
316 parent = ctx.parents()[0].node()
317 hg.update(repo, parent)
317 hg.update(repo, parent)
318 ### prepare new commit data
318 ### prepare new commit data
319 commitopts = opts.copy()
319 commitopts = opts.copy()
320 # username
320 # username
321 if ctx.user() == oldctx.user():
321 if ctx.user() == oldctx.user():
322 username = ctx.user()
322 username = ctx.user()
323 else:
323 else:
324 username = ui.username()
324 username = ui.username()
325 commitopts['user'] = username
325 commitopts['user'] = username
326 # commit message
326 # commit message
327 newmessage = '\n***\n'.join(
327 newmessage = '\n***\n'.join(
328 [ctx.description()] +
328 [ctx.description()] +
329 [repo[r].description() for r in internalchanges] +
329 [repo[r].description() for r in internalchanges] +
330 [oldctx.description()]) + '\n'
330 [oldctx.description()]) + '\n'
331 commitopts['message'] = newmessage
331 commitopts['message'] = newmessage
332 # date
332 # date
333 commitopts['date'] = max(ctx.date(), oldctx.date())
333 commitopts['date'] = max(ctx.date(), oldctx.date())
334 n = collapse(repo, ctx, repo[newnode], commitopts)
334 n = collapse(repo, ctx, repo[newnode], commitopts)
335 if n is None:
335 if n is None:
336 return ctx, []
336 return ctx, []
337 hg.update(repo, n)
337 hg.update(repo, n)
338 replacements = [(oldctx.node(), (newnode,)),
338 replacements = [(oldctx.node(), (newnode,)),
339 (ctx.node(), (n,)),
339 (ctx.node(), (n,)),
340 (newnode, (n,)),
340 (newnode, (n,)),
341 ]
341 ]
342 for ich in internalchanges:
342 for ich in internalchanges:
343 replacements.append((ich, (n,)))
343 replacements.append((ich, (n,)))
344 return repo[n], replacements
344 return repo[n], replacements
345
345
346 def drop(ui, repo, ctx, ha, opts):
346 def drop(ui, repo, ctx, ha, opts):
347 return ctx, [(repo[ha].node(), ())]
347 return ctx, [(repo[ha].node(), ())]
348
348
349
349
350 def message(ui, repo, ctx, ha, opts):
350 def message(ui, repo, ctx, ha, opts):
351 oldctx = repo[ha]
351 oldctx = repo[ha]
352 hg.update(repo, ctx.node())
352 hg.update(repo, ctx.node())
353 stats = applychanges(ui, repo, oldctx, opts)
353 stats = applychanges(ui, repo, oldctx, opts)
354 if stats and stats[3] > 0:
354 if stats and stats[3] > 0:
355 raise util.Abort(_('Fix up the change and run '
355 raise util.Abort(_('Fix up the change and run '
356 'hg histedit --continue'))
356 'hg histedit --continue'))
357 message = oldctx.description() + '\n'
357 message = oldctx.description() + '\n'
358 message = ui.edit(message, ui.username())
358 message = ui.edit(message, ui.username())
359 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
359 new = repo.commit(text=message, user=oldctx.user(), date=oldctx.date(),
360 extra=oldctx.extra())
360 extra=oldctx.extra())
361 newctx = repo[new]
361 newctx = repo[new]
362 if oldctx.node() != newctx.node():
362 if oldctx.node() != newctx.node():
363 return newctx, [(oldctx.node(), (new,))]
363 return newctx, [(oldctx.node(), (new,))]
364 # We didn't make an edit, so just indicate no replaced nodes
364 # We didn't make an edit, so just indicate no replaced nodes
365 return newctx, []
365 return newctx, []
366
366
367 actiontable = {'p': pick,
367 actiontable = {'p': pick,
368 'pick': pick,
368 'pick': pick,
369 'e': edit,
369 'e': edit,
370 'edit': edit,
370 'edit': edit,
371 'f': fold,
371 'f': fold,
372 'fold': fold,
372 'fold': fold,
373 'd': drop,
373 'd': drop,
374 'drop': drop,
374 'drop': drop,
375 'm': message,
375 'm': message,
376 'mess': message,
376 'mess': message,
377 }
377 }
378
378
379 @command('histedit',
379 @command('histedit',
380 [('', 'commands', '',
380 [('', 'commands', '',
381 _('Read history edits from the specified file.')),
381 _('Read history edits from the specified file.')),
382 ('c', 'continue', False, _('continue an edit already in progress')),
382 ('c', 'continue', False, _('continue an edit already in progress')),
383 ('k', 'keep', False,
383 ('k', 'keep', False,
384 _("don't strip old nodes after edit is complete")),
384 _("don't strip old nodes after edit is complete")),
385 ('', 'abort', False, _('abort an edit in progress')),
385 ('', 'abort', False, _('abort an edit in progress')),
386 ('o', 'outgoing', False, _('changesets not found in destination')),
386 ('o', 'outgoing', False, _('changesets not found in destination')),
387 ('f', 'force', False,
387 ('f', 'force', False,
388 _('force outgoing even for unrelated repositories')),
388 _('force outgoing even for unrelated repositories')),
389 ('r', 'rev', [], _('first revision to be edited'))],
389 ('r', 'rev', [], _('first revision to be edited'))],
390 _("[PARENT]"))
390 _("[PARENT]"))
391 def histedit(ui, repo, *parent, **opts):
391 def histedit(ui, repo, *parent, **opts):
392 """interactively edit changeset history
392 """interactively edit changeset history
393 """
393 """
394 # TODO only abort if we try and histedit mq patches, not just
394 # TODO only abort if we try and histedit mq patches, not just
395 # blanket if mq patches are applied somewhere
395 # blanket if mq patches are applied somewhere
396 mq = getattr(repo, 'mq', None)
396 mq = getattr(repo, 'mq', None)
397 if mq and mq.applied:
397 if mq and mq.applied:
398 raise util.Abort(_('source has mq patches applied'))
398 raise util.Abort(_('source has mq patches applied'))
399
399
400 parent = list(parent) + opts.get('rev', [])
400 parent = list(parent) + opts.get('rev', [])
401 if opts.get('outgoing'):
401 if opts.get('outgoing'):
402 if len(parent) > 1:
402 if len(parent) > 1:
403 raise util.Abort(
403 raise util.Abort(
404 _('only one repo argument allowed with --outgoing'))
404 _('only one repo argument allowed with --outgoing'))
405 elif parent:
405 elif parent:
406 parent = parent[0]
406 parent = parent[0]
407
407
408 dest = ui.expandpath(parent or 'default-push', parent or 'default')
408 dest = ui.expandpath(parent or 'default-push', parent or 'default')
409 dest, revs = hg.parseurl(dest, None)[:2]
409 dest, revs = hg.parseurl(dest, None)[:2]
410 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
410 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
411
411
412 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
412 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
413 other = hg.peer(repo, opts, dest)
413 other = hg.peer(repo, opts, dest)
414
414
415 if revs:
415 if revs:
416 revs = [repo.lookup(rev) for rev in revs]
416 revs = [repo.lookup(rev) for rev in revs]
417
417
418 parent = discovery.findcommonoutgoing(
418 parent = discovery.findcommonoutgoing(
419 repo, other, [], force=opts.get('force')).missing[0:1]
419 repo, other, [], force=opts.get('force')).missing[0:1]
420 else:
420 else:
421 if opts.get('force'):
421 if opts.get('force'):
422 raise util.Abort(_('--force only allowed with --outgoing'))
422 raise util.Abort(_('--force only allowed with --outgoing'))
423
423
424 if opts.get('continue', False):
424 if opts.get('continue', False):
425 if len(parent) != 0:
425 if len(parent) != 0:
426 raise util.Abort(_('no arguments allowed with --continue'))
426 raise util.Abort(_('no arguments allowed with --continue'))
427 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
427 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
428 currentparent, wantnull = repo.dirstate.parents()
428 currentparent, wantnull = repo.dirstate.parents()
429 parentctx = repo[parentctxnode]
429 parentctx = repo[parentctxnode]
430 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
430 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
431 replacements.extend(repl)
431 replacements.extend(repl)
432 elif opts.get('abort', False):
432 elif opts.get('abort', False):
433 if len(parent) != 0:
433 if len(parent) != 0:
434 raise util.Abort(_('no arguments allowed with --abort'))
434 raise util.Abort(_('no arguments allowed with --abort'))
435 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
435 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
436 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
436 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
437 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
437 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
438 hg.clean(repo, topmost)
438 hg.clean(repo, topmost)
439 cleanupnode(ui, repo, 'created', tmpnodes)
439 cleanupnode(ui, repo, 'created', tmpnodes)
440 cleanupnode(ui, repo, 'temp', leafs)
440 cleanupnode(ui, repo, 'temp', leafs)
441 os.unlink(os.path.join(repo.path, 'histedit-state'))
441 os.unlink(os.path.join(repo.path, 'histedit-state'))
442 return
442 return
443 else:
443 else:
444 cmdutil.bailifchanged(repo)
444 cmdutil.bailifchanged(repo)
445 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
445 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
446 raise util.Abort(_('history edit already in progress, try '
446 raise util.Abort(_('history edit already in progress, try '
447 '--continue or --abort'))
447 '--continue or --abort'))
448
448
449 topmost, empty = repo.dirstate.parents()
449 topmost, empty = repo.dirstate.parents()
450
450
451 if len(parent) != 1:
451 if len(parent) != 1:
452 raise util.Abort(_('histedit requires exactly one parent revision'))
452 raise util.Abort(_('histedit requires exactly one parent revision'))
453 parent = scmutil.revsingle(repo, parent[0]).node()
453 parent = scmutil.revsingle(repo, parent[0]).node()
454
454
455 keep = opts.get('keep', False)
455 keep = opts.get('keep', False)
456 revs = between(repo, parent, topmost, keep)
456 revs = between(repo, parent, topmost, keep)
457 if not revs:
457 if not revs:
458 ui.warn(_('nothing to edit\n'))
458 ui.warn(_('nothing to edit\n'))
459 return 1
459 return 1
460
460
461 ctxs = [repo[r] for r in revs]
461 ctxs = [repo[r] for r in revs]
462 rules = opts.get('commands', '')
462 rules = opts.get('commands', '')
463 if not rules:
463 if not rules:
464 rules = '\n'.join([makedesc(c) for c in ctxs])
464 rules = '\n'.join([makedesc(c) for c in ctxs])
465 rules += '\n\n'
465 rules += '\n\n'
466 rules += editcomment % (node.short(parent), node.short(topmost))
466 rules += editcomment % (node.short(parent), node.short(topmost))
467 rules = ui.edit(rules, ui.username())
467 rules = ui.edit(rules, ui.username())
468 # Save edit rules in .hg/histedit-last-edit.txt in case
468 # Save edit rules in .hg/histedit-last-edit.txt in case
469 # the user needs to ask for help after something
469 # the user needs to ask for help after something
470 # surprising happens.
470 # surprising happens.
471 f = open(repo.join('histedit-last-edit.txt'), 'w')
471 f = open(repo.join('histedit-last-edit.txt'), 'w')
472 f.write(rules)
472 f.write(rules)
473 f.close()
473 f.close()
474 else:
474 else:
475 f = open(rules)
475 f = open(rules)
476 rules = f.read()
476 rules = f.read()
477 f.close()
477 f.close()
478 rules = [l for l in (r.strip() for r in rules.splitlines())
478 rules = [l for l in (r.strip() for r in rules.splitlines())
479 if l and not l[0] == '#']
479 if l and not l[0] == '#']
480 rules = verifyrules(rules, repo, ctxs)
480 rules = verifyrules(rules, repo, ctxs)
481
481
482 parentctx = repo[parent].parents()[0]
482 parentctx = repo[parent].parents()[0]
483 keep = opts.get('keep', False)
483 keep = opts.get('keep', False)
484 replacements = []
484 replacements = []
485
485
486
486
487 while rules:
487 while rules:
488 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
488 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
489 action, ha = rules.pop(0)
489 action, ha = rules.pop(0)
490 ui.debug('histedit: processing %s %s\n' % (action, ha))
490 ui.debug('histedit: processing %s %s\n' % (action, ha))
491 actfunc = actiontable[action]
491 actfunc = actiontable[action]
492 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
492 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
493 replacements.extend(replacement_)
493 replacements.extend(replacement_)
494
494
495 hg.update(repo, parentctx.node())
495 hg.update(repo, parentctx.node())
496
496
497 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
497 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
498 if mapping:
498 if mapping:
499 for prec, succs in mapping.iteritems():
499 for prec, succs in mapping.iteritems():
500 if not succs:
500 if not succs:
501 ui.debug('histedit: %s is dropped\n' % node.short(prec))
501 ui.debug('histedit: %s is dropped\n' % node.short(prec))
502 else:
502 else:
503 ui.debug('histedit: %s is replaced by %s\n' % (
503 ui.debug('histedit: %s is replaced by %s\n' % (
504 node.short(prec), node.short(succs[0])))
504 node.short(prec), node.short(succs[0])))
505 if len(succs) > 1:
505 if len(succs) > 1:
506 m = 'histedit: %s'
506 m = 'histedit: %s'
507 for n in succs[1:]:
507 for n in succs[1:]:
508 ui.debug(m % node.short(n))
508 ui.debug(m % node.short(n))
509
509
510 if not keep:
510 if not keep:
511 if mapping:
511 if mapping:
512 movebookmarks(ui, repo, mapping, topmost, ntm)
512 movebookmarks(ui, repo, mapping, topmost, ntm)
513 # TODO update mq state
513 # TODO update mq state
514 if obsolete._enabled:
514 if obsolete._enabled:
515 markers = []
515 markers = []
516 # sort by revision number because it sound "right"
516 # sort by revision number because it sound "right"
517 for prec in sorted(mapping, key=repo.changelog.rev):
517 for prec in sorted(mapping, key=repo.changelog.rev):
518 succs = mapping[prec]
518 succs = mapping[prec]
519 markers.append((repo[prec],
519 markers.append((repo[prec],
520 tuple(repo[s] for s in succs)))
520 tuple(repo[s] for s in succs)))
521 if markers:
521 if markers:
522 obsolete.createmarkers(repo, markers)
522 obsolete.createmarkers(repo, markers)
523 else:
523 else:
524 cleanupnode(ui, repo, 'replaced', mapping)
524 cleanupnode(ui, repo, 'replaced', mapping)
525
525
526 cleanupnode(ui, repo, 'temp', tmpnodes)
526 cleanupnode(ui, repo, 'temp', tmpnodes)
527 os.unlink(os.path.join(repo.path, 'histedit-state'))
527 os.unlink(os.path.join(repo.path, 'histedit-state'))
528 if os.path.exists(repo.sjoin('undo')):
528 if os.path.exists(repo.sjoin('undo')):
529 os.unlink(repo.sjoin('undo'))
529 os.unlink(repo.sjoin('undo'))
530
530
531
531
532 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
532 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
533 action, currentnode = rules.pop(0)
533 action, currentnode = rules.pop(0)
534 ctx = repo[currentnode]
534 ctx = repo[currentnode]
535 # is there any new commit between the expected parent and "."
535 # is there any new commit between the expected parent and "."
536 #
536 #
537 # note: does not take non linear new change in account (but previous
537 # note: does not take non linear new change in account (but previous
538 # implementation didn't used them anyway (issue3655)
538 # implementation didn't used them anyway (issue3655)
539 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
539 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
540 if not newchildren:
540 if not newchildren:
541 # `parentctxnode` should match but no result. This means that
541 # `parentctxnode` should match but no result. This means that
542 # currentnode is not a descendant from parentctxnode.
542 # currentnode is not a descendant from parentctxnode.
543 msg = _('working directory parent is not a descendant of %s')
543 msg = _('working directory parent is not a descendant of %s')
544 hint = _('update to %s or descendant and run "hg histedit '
544 hint = _('update to %s or descendant and run "hg histedit '
545 '--continue" again') % parentctx
545 '--continue" again') % parentctx
546 raise util.Abort(msg % parentctx, hint=hint)
546 raise util.Abort(msg % parentctx, hint=hint)
547 newchildren.pop(0) # remove parentctxnode
547 newchildren.pop(0) # remove parentctxnode
548 # Commit dirty working directory if necessary
548 # Commit dirty working directory if necessary
549 new = None
549 new = None
550 m, a, r, d = repo.status()[:4]
550 m, a, r, d = repo.status()[:4]
551 if m or a or r or d:
551 if m or a or r or d:
552 # prepare the message for the commit to comes
552 # prepare the message for the commit to comes
553 if action in ('f', 'fold'):
553 if action in ('f', 'fold'):
554 message = 'fold-temp-revision %s' % currentnode
554 message = 'fold-temp-revision %s' % currentnode
555 else:
555 else:
556 message = ctx.description() + '\n'
556 message = ctx.description() + '\n'
557 if action in ('e', 'edit', 'm', 'mess'):
557 if action in ('e', 'edit', 'm', 'mess'):
558 editor = cmdutil.commitforceeditor
558 editor = cmdutil.commitforceeditor
559 else:
559 else:
560 editor = False
560 editor = False
561 new = repo.commit(text=message, user=ctx.user(),
561 new = repo.commit(text=message, user=ctx.user(),
562 date=ctx.date(), extra=ctx.extra(),
562 date=ctx.date(), extra=ctx.extra(),
563 editor=editor)
563 editor=editor)
564 if new is not None:
564 if new is not None:
565 newchildren.append(new)
565 newchildren.append(new)
566
566
567 replacements = []
567 replacements = []
568 # track replacements
568 # track replacements
569 if ctx.node() not in newchildren:
569 if ctx.node() not in newchildren:
570 # note: new children may be empty when the changeset is dropped.
570 # note: new children may be empty when the changeset is dropped.
571 # this happen e.g during conflicting pick where we revert content
571 # this happen e.g during conflicting pick where we revert content
572 # to parent.
572 # to parent.
573 replacements.append((ctx.node(), tuple(newchildren)))
573 replacements.append((ctx.node(), tuple(newchildren)))
574
574
575 if action in ('f', 'fold'):
575 if action in ('f', 'fold'):
576 # finalize fold operation if applicable
576 # finalize fold operation if applicable
577 if new is None:
577 if new is None:
578 new = newchildren[-1]
578 new = newchildren[-1]
579 else:
579 else:
580 newchildren.pop() # remove new from internal changes
580 newchildren.pop() # remove new from internal changes
581 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
581 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
582 newchildren)
582 newchildren)
583 replacements.extend(repl)
583 replacements.extend(repl)
584 elif newchildren:
584 elif newchildren:
585 # otherwize update "parentctx" before proceding to further operation
585 # otherwize update "parentctx" before proceding to further operation
586 parentctx = repo[newchildren[-1]]
586 parentctx = repo[newchildren[-1]]
587 return parentctx, replacements
587 return parentctx, replacements
588
588
589
589
590 def between(repo, old, new, keep):
590 def between(repo, old, new, keep):
591 """select and validate the set of revision to edit
591 """select and validate the set of revision to edit
592
592
593 When keep is false, the specified set can't have children."""
593 When keep is false, the specified set can't have children."""
594 ctxs = list(repo.set('%n::%n', old, new))
594 ctxs = list(repo.set('%n::%n', old, new))
595 if ctxs and not keep:
595 if ctxs and not keep:
596 if (not obsolete._enabled and
596 if (not obsolete._enabled and
597 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
597 repo.revs('(%ld::) - (%ld)', 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 sorted(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 marks = repo._bookmarks
743 marks = repo._bookmarks
744 for mark, new in moves:
744 for mark, new in moves:
745 old = marks[mark]
745 old = marks[mark]
746 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
746 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
747 % (mark, node.short(old), node.short(new)))
747 % (mark, node.short(old), node.short(new)))
748 marks[mark] = new
748 marks[mark] = new
749 marks.write()
749 marks.write()
750
750
751 def cleanupnode(ui, repo, name, nodes):
751 def cleanupnode(ui, repo, name, nodes):
752 """strip a group of nodes from the repository
752 """strip a group of nodes from the repository
753
753
754 The set of node to strip may contains unknown nodes."""
754 The set of node to strip may contains unknown nodes."""
755 ui.debug('should strip %s nodes %s\n' %
755 ui.debug('should strip %s nodes %s\n' %
756 (name, ', '.join([node.short(n) for n in nodes])))
756 (name, ', '.join([node.short(n) for n in nodes])))
757 lock = None
757 lock = None
758 try:
758 try:
759 lock = repo.lock()
759 lock = repo.lock()
760 # Find all node that need to be stripped
760 # Find all node that need to be stripped
761 # (we hg %lr instead of %ln to silently ignore unknown item
761 # (we hg %lr instead of %ln to silently ignore unknown item
762 nm = repo.changelog.nodemap
762 nm = repo.changelog.nodemap
763 nodes = [n for n in nodes if n in nm]
763 nodes = [n for n in nodes if n in nm]
764 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
764 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
765 for c in roots:
765 for c in roots:
766 # We should process node in reverse order to strip tip most first.
766 # We should process node in reverse order to strip tip most first.
767 # but this trigger a bug in changegroup hook.
767 # but this trigger a bug in changegroup hook.
768 # This would reduce bundle overhead
768 # This would reduce bundle overhead
769 repair.strip(ui, repo, c)
769 repair.strip(ui, repo, c)
770 finally:
770 finally:
771 lockmod.release(lock)
771 lockmod.release(lock)
@@ -1,179 +1,179 b''
1 $ . "$TESTDIR/histedit-helpers.sh"
1 $ . "$TESTDIR/histedit-helpers.sh"
2
2
3 $ cat >> $HGRCPATH <<EOF
3 $ cat >> $HGRCPATH <<EOF
4 > [extensions]
4 > [extensions]
5 > graphlog=
5 > graphlog=
6 > histedit=
6 > histedit=
7 > EOF
7 > EOF
8
8
9 $ hg init r
9 $ hg init r
10 $ cd r
10 $ cd r
11
11
12 $ for x in a b c d e f ; do
12 $ for x in a b c d e f ; do
13 > echo $x > $x
13 > echo $x > $x
14 > hg add $x
14 > hg add $x
15 > hg ci -m $x
15 > hg ci -m $x
16 > done
16 > done
17
17
18 $ hg book -r 1 will-move-backwards
18 $ hg book -r 1 will-move-backwards
19 $ hg book -r 2 two
19 $ hg book -r 2 two
20 $ hg book -r 2 also-two
20 $ hg book -r 2 also-two
21 $ hg book -r 3 three
21 $ hg book -r 3 three
22 $ hg book -r 4 four
22 $ hg book -r 4 four
23 $ hg book -r tip five
23 $ hg book -r tip five
24 $ hg log --graph
24 $ hg log --graph
25 @ changeset: 5:652413bf663e
25 @ changeset: 5:652413bf663e
26 | bookmark: five
26 | bookmark: five
27 | tag: tip
27 | tag: tip
28 | user: test
28 | user: test
29 | date: Thu Jan 01 00:00:00 1970 +0000
29 | date: Thu Jan 01 00:00:00 1970 +0000
30 | summary: f
30 | summary: f
31 |
31 |
32 o changeset: 4:e860deea161a
32 o changeset: 4:e860deea161a
33 | bookmark: four
33 | bookmark: four
34 | user: test
34 | user: test
35 | date: Thu Jan 01 00:00:00 1970 +0000
35 | date: Thu Jan 01 00:00:00 1970 +0000
36 | summary: e
36 | summary: e
37 |
37 |
38 o changeset: 3:055a42cdd887
38 o changeset: 3:055a42cdd887
39 | bookmark: three
39 | bookmark: three
40 | user: test
40 | user: test
41 | date: Thu Jan 01 00:00:00 1970 +0000
41 | date: Thu Jan 01 00:00:00 1970 +0000
42 | summary: d
42 | summary: d
43 |
43 |
44 o changeset: 2:177f92b77385
44 o changeset: 2:177f92b77385
45 | bookmark: also-two
45 | bookmark: also-two
46 | bookmark: two
46 | bookmark: two
47 | user: test
47 | user: test
48 | date: Thu Jan 01 00:00:00 1970 +0000
48 | date: Thu Jan 01 00:00:00 1970 +0000
49 | summary: c
49 | summary: c
50 |
50 |
51 o changeset: 1:d2ae7f538514
51 o changeset: 1:d2ae7f538514
52 | bookmark: will-move-backwards
52 | bookmark: will-move-backwards
53 | user: test
53 | user: test
54 | date: Thu Jan 01 00:00:00 1970 +0000
54 | date: Thu Jan 01 00:00:00 1970 +0000
55 | summary: b
55 | summary: b
56 |
56 |
57 o changeset: 0:cb9a9f314b8b
57 o changeset: 0:cb9a9f314b8b
58 user: test
58 user: test
59 date: Thu Jan 01 00:00:00 1970 +0000
59 date: Thu Jan 01 00:00:00 1970 +0000
60 summary: a
60 summary: a
61
61
62 $ HGEDITOR=cat hg histedit 1
62 $ HGEDITOR=cat hg histedit 1
63 pick d2ae7f538514 1 b
63 pick d2ae7f538514 1 b
64 pick 177f92b77385 2 c
64 pick 177f92b77385 2 c
65 pick 055a42cdd887 3 d
65 pick 055a42cdd887 3 d
66 pick e860deea161a 4 e
66 pick e860deea161a 4 e
67 pick 652413bf663e 5 f
67 pick 652413bf663e 5 f
68
68
69 # Edit history between d2ae7f538514 and 652413bf663e
69 # Edit history between d2ae7f538514 and 652413bf663e
70 #
70 #
71 # Commands:
71 # Commands:
72 # p, pick = use commit
72 # p, pick = use commit
73 # e, edit = use commit, but stop for amending
73 # e, edit = use commit, but stop for amending
74 # f, fold = use commit, but fold into previous commit (combines N and N-1)
74 # f, fold = use commit, but fold into previous commit (combines N and N-1)
75 # d, drop = remove commit from history
75 # d, drop = remove commit from history
76 # m, mess = edit message without changing commit content
76 # m, mess = edit message without changing commit content
77 #
77 #
78 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
79 $ cat >> commands.txt <<EOF
79 $ cat >> commands.txt <<EOF
80 > pick 177f92b77385 2 c
80 > pick 177f92b77385 2 c
81 > drop d2ae7f538514 1 b
81 > drop d2ae7f538514 1 b
82 > pick 055a42cdd887 3 d
82 > pick 055a42cdd887 3 d
83 > fold e860deea161a 4 e
83 > fold e860deea161a 4 e
84 > pick 652413bf663e 5 f
84 > pick 652413bf663e 5 f
85 > EOF
85 > EOF
86 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
86 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
87 histedit: moving bookmarks two from 177f92b77385 to d36c0562f908
87 histedit: moving bookmarks also-two from 177f92b77385 to d36c0562f908
88 histedit: moving bookmarks three from 055a42cdd887 to ae467701c500
88 histedit: moving bookmarks five from 652413bf663e to 0efacef7cb48
89 histedit: moving bookmarks four from e860deea161a to ae467701c500
89 histedit: moving bookmarks four from e860deea161a to ae467701c500
90 histedit: moving bookmarks also-two from 177f92b77385 to d36c0562f908
90 histedit: moving bookmarks three from 055a42cdd887 to ae467701c500
91 histedit: moving bookmarks two from 177f92b77385 to d36c0562f908
91 histedit: moving bookmarks will-move-backwards from d2ae7f538514 to cb9a9f314b8b
92 histedit: moving bookmarks will-move-backwards from d2ae7f538514 to cb9a9f314b8b
92 histedit: moving bookmarks five from 652413bf663e to 0efacef7cb48
93 saved backup bundle to $TESTTMP/r/.hg/strip-backup/d2ae7f538514-backup.hg (glob)
93 saved backup bundle to $TESTTMP/r/.hg/strip-backup/d2ae7f538514-backup.hg (glob)
94 saved backup bundle to $TESTTMP/r/.hg/strip-backup/34a9919932c1-backup.hg (glob)
94 saved backup bundle to $TESTTMP/r/.hg/strip-backup/34a9919932c1-backup.hg (glob)
95 $ hg log --graph
95 $ hg log --graph
96 @ changeset: 3:0efacef7cb48
96 @ changeset: 3:0efacef7cb48
97 | bookmark: five
97 | bookmark: five
98 | tag: tip
98 | tag: tip
99 | user: test
99 | user: test
100 | date: Thu Jan 01 00:00:00 1970 +0000
100 | date: Thu Jan 01 00:00:00 1970 +0000
101 | summary: f
101 | summary: f
102 |
102 |
103 o changeset: 2:ae467701c500
103 o changeset: 2:ae467701c500
104 | bookmark: four
104 | bookmark: four
105 | bookmark: three
105 | bookmark: three
106 | user: test
106 | user: test
107 | date: Thu Jan 01 00:00:00 1970 +0000
107 | date: Thu Jan 01 00:00:00 1970 +0000
108 | summary: d
108 | summary: d
109 |
109 |
110 o changeset: 1:d36c0562f908
110 o changeset: 1:d36c0562f908
111 | bookmark: also-two
111 | bookmark: also-two
112 | bookmark: two
112 | bookmark: two
113 | user: test
113 | user: test
114 | date: Thu Jan 01 00:00:00 1970 +0000
114 | date: Thu Jan 01 00:00:00 1970 +0000
115 | summary: c
115 | summary: c
116 |
116 |
117 o changeset: 0:cb9a9f314b8b
117 o changeset: 0:cb9a9f314b8b
118 bookmark: will-move-backwards
118 bookmark: will-move-backwards
119 user: test
119 user: test
120 date: Thu Jan 01 00:00:00 1970 +0000
120 date: Thu Jan 01 00:00:00 1970 +0000
121 summary: a
121 summary: a
122
122
123 $ HGEDITOR=cat hg histedit 1
123 $ HGEDITOR=cat hg histedit 1
124 pick d36c0562f908 1 c
124 pick d36c0562f908 1 c
125 pick ae467701c500 2 d
125 pick ae467701c500 2 d
126 pick 0efacef7cb48 3 f
126 pick 0efacef7cb48 3 f
127
127
128 # Edit history between d36c0562f908 and 0efacef7cb48
128 # Edit history between d36c0562f908 and 0efacef7cb48
129 #
129 #
130 # Commands:
130 # Commands:
131 # p, pick = use commit
131 # p, pick = use commit
132 # e, edit = use commit, but stop for amending
132 # e, edit = use commit, but stop for amending
133 # f, fold = use commit, but fold into previous commit (combines N and N-1)
133 # f, fold = use commit, but fold into previous commit (combines N and N-1)
134 # d, drop = remove commit from history
134 # d, drop = remove commit from history
135 # m, mess = edit message without changing commit content
135 # m, mess = edit message without changing commit content
136 #
136 #
137 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
137 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
138 $ cat > commands.txt << EOF
138 $ cat > commands.txt << EOF
139 > pick d36c0562f908 1 c
139 > pick d36c0562f908 1 c
140 > pick 0efacef7cb48 3 f
140 > pick 0efacef7cb48 3 f
141 > pick ae467701c500 2 d
141 > pick ae467701c500 2 d
142 > EOF
142 > EOF
143 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
143 $ hg histedit 1 --commands commands.txt --verbose | grep histedit
144 histedit: moving bookmarks three from ae467701c500 to 1be9c35b4cb2
144 histedit: moving bookmarks five from 0efacef7cb48 to 1be9c35b4cb2
145 histedit: moving bookmarks four from ae467701c500 to 1be9c35b4cb2
145 histedit: moving bookmarks four from ae467701c500 to 1be9c35b4cb2
146 histedit: moving bookmarks five from 0efacef7cb48 to 1be9c35b4cb2
146 histedit: moving bookmarks three from ae467701c500 to 1be9c35b4cb2
147 saved backup bundle to $TESTTMP/r/.hg/strip-backup/ae467701c500-backup.hg (glob)
147 saved backup bundle to $TESTTMP/r/.hg/strip-backup/ae467701c500-backup.hg (glob)
148
148
149 We expect 'five' to stay at tip, since the tipmost bookmark is most
149 We expect 'five' to stay at tip, since the tipmost bookmark is most
150 likely the useful signal.
150 likely the useful signal.
151
151
152 $ hg log --graph
152 $ hg log --graph
153 @ changeset: 3:1be9c35b4cb2
153 @ changeset: 3:1be9c35b4cb2
154 | bookmark: five
154 | bookmark: five
155 | bookmark: four
155 | bookmark: four
156 | bookmark: three
156 | bookmark: three
157 | tag: tip
157 | tag: tip
158 | user: test
158 | user: test
159 | date: Thu Jan 01 00:00:00 1970 +0000
159 | date: Thu Jan 01 00:00:00 1970 +0000
160 | summary: d
160 | summary: d
161 |
161 |
162 o changeset: 2:7c044e3e33a9
162 o changeset: 2:7c044e3e33a9
163 | user: test
163 | user: test
164 | date: Thu Jan 01 00:00:00 1970 +0000
164 | date: Thu Jan 01 00:00:00 1970 +0000
165 | summary: f
165 | summary: f
166 |
166 |
167 o changeset: 1:d36c0562f908
167 o changeset: 1:d36c0562f908
168 | bookmark: also-two
168 | bookmark: also-two
169 | bookmark: two
169 | bookmark: two
170 | user: test
170 | user: test
171 | date: Thu Jan 01 00:00:00 1970 +0000
171 | date: Thu Jan 01 00:00:00 1970 +0000
172 | summary: c
172 | summary: c
173 |
173 |
174 o changeset: 0:cb9a9f314b8b
174 o changeset: 0:cb9a9f314b8b
175 bookmark: will-move-backwards
175 bookmark: will-move-backwards
176 user: test
176 user: test
177 date: Thu Jan 01 00:00:00 1970 +0000
177 date: Thu Jan 01 00:00:00 1970 +0000
178 summary: a
178 summary: a
179
179
General Comments 0
You need to be logged in to leave comments. Login now