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