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