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