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