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