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