##// END OF EJS Templates
histedit: abort if there are multiple roots in "--outgoing" revisions...
FUJIWARA Katsunori -
r19841:fab75342 stable
parent child Browse files
Show More
@@ -1,892 +1,897 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 # hexlify nodes from outgoing, because we're going to parse
422 # hexlify nodes from outgoing, because we're going to parse
423 # parent[0] using revsingle below, and if the binary hash
423 # parent[0] using revsingle below, and if the binary hash
424 # contains special revset characters like ":" the revset
424 # contains special revset characters like ":" the revset
425 # parser can choke.
425 # parser can choke.
426 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
426 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
427 if not outgoing.missing:
427 if not outgoing.missing:
428 raise util.Abort(_('no outgoing ancestors'))
428 raise util.Abort(_('no outgoing ancestors'))
429 return outgoing.missing[0]
429 roots = list(repo.revs("roots(%ln)", outgoing.missing))
430 if 1 < len(roots):
431 msg = _('there are ambiguous outgoing revisions')
432 hint = _('see "hg help histedit" for more detail')
433 raise util.Abort(msg, hint=hint)
434 return repo.lookup(roots[0])
430
435
431 actiontable = {'p': pick,
436 actiontable = {'p': pick,
432 'pick': pick,
437 'pick': pick,
433 'e': edit,
438 'e': edit,
434 'edit': edit,
439 'edit': edit,
435 'f': fold,
440 'f': fold,
436 'fold': fold,
441 'fold': fold,
437 'd': drop,
442 'd': drop,
438 'drop': drop,
443 'drop': drop,
439 'm': message,
444 'm': message,
440 'mess': message,
445 'mess': message,
441 }
446 }
442
447
443 @command('histedit',
448 @command('histedit',
444 [('', 'commands', '',
449 [('', 'commands', '',
445 _('Read history edits from the specified file.')),
450 _('Read history edits from the specified file.')),
446 ('c', 'continue', False, _('continue an edit already in progress')),
451 ('c', 'continue', False, _('continue an edit already in progress')),
447 ('k', 'keep', False,
452 ('k', 'keep', False,
448 _("don't strip old nodes after edit is complete")),
453 _("don't strip old nodes after edit is complete")),
449 ('', 'abort', False, _('abort an edit in progress')),
454 ('', 'abort', False, _('abort an edit in progress')),
450 ('o', 'outgoing', False, _('changesets not found in destination')),
455 ('o', 'outgoing', False, _('changesets not found in destination')),
451 ('f', 'force', False,
456 ('f', 'force', False,
452 _('force outgoing even for unrelated repositories')),
457 _('force outgoing even for unrelated repositories')),
453 ('r', 'rev', [], _('first revision to be edited'))],
458 ('r', 'rev', [], _('first revision to be edited'))],
454 _("ANCESTOR | --outgoing [URL]"))
459 _("ANCESTOR | --outgoing [URL]"))
455 def histedit(ui, repo, *freeargs, **opts):
460 def histedit(ui, repo, *freeargs, **opts):
456 """interactively edit changeset history
461 """interactively edit changeset history
457
462
458 This command edits changesets between ANCESTOR and the parent of
463 This command edits changesets between ANCESTOR and the parent of
459 the working directory.
464 the working directory.
460
465
461 With --outgoing, this edits changesets not found in the
466 With --outgoing, this edits changesets not found in the
462 destination repository. If URL of the destination is omitted, the
467 destination repository. If URL of the destination is omitted, the
463 'default-push' (or 'default') path will be used.
468 'default-push' (or 'default') path will be used.
464 """
469 """
465 # TODO only abort if we try and histedit mq patches, not just
470 # TODO only abort if we try and histedit mq patches, not just
466 # blanket if mq patches are applied somewhere
471 # blanket if mq patches are applied somewhere
467 mq = getattr(repo, 'mq', None)
472 mq = getattr(repo, 'mq', None)
468 if mq and mq.applied:
473 if mq and mq.applied:
469 raise util.Abort(_('source has mq patches applied'))
474 raise util.Abort(_('source has mq patches applied'))
470
475
471 # basic argument incompatibility processing
476 # basic argument incompatibility processing
472 outg = opts.get('outgoing')
477 outg = opts.get('outgoing')
473 cont = opts.get('continue')
478 cont = opts.get('continue')
474 abort = opts.get('abort')
479 abort = opts.get('abort')
475 force = opts.get('force')
480 force = opts.get('force')
476 rules = opts.get('commands', '')
481 rules = opts.get('commands', '')
477 revs = opts.get('rev', [])
482 revs = opts.get('rev', [])
478 goal = 'new' # This invocation goal, in new, continue, abort
483 goal = 'new' # This invocation goal, in new, continue, abort
479 if force and not outg:
484 if force and not outg:
480 raise util.Abort(_('--force only allowed with --outgoing'))
485 raise util.Abort(_('--force only allowed with --outgoing'))
481 if cont:
486 if cont:
482 if util.any((outg, abort, revs, freeargs, rules)):
487 if util.any((outg, abort, revs, freeargs, rules)):
483 raise util.Abort(_('no arguments allowed with --continue'))
488 raise util.Abort(_('no arguments allowed with --continue'))
484 goal = 'continue'
489 goal = 'continue'
485 elif abort:
490 elif abort:
486 if util.any((outg, revs, freeargs, rules)):
491 if util.any((outg, revs, freeargs, rules)):
487 raise util.Abort(_('no arguments allowed with --abort'))
492 raise util.Abort(_('no arguments allowed with --abort'))
488 goal = 'abort'
493 goal = 'abort'
489 else:
494 else:
490 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
495 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
491 raise util.Abort(_('history edit already in progress, try '
496 raise util.Abort(_('history edit already in progress, try '
492 '--continue or --abort'))
497 '--continue or --abort'))
493 if outg:
498 if outg:
494 if revs:
499 if revs:
495 raise util.Abort(_('no revisions allowed with --outgoing'))
500 raise util.Abort(_('no revisions allowed with --outgoing'))
496 if len(freeargs) > 1:
501 if len(freeargs) > 1:
497 raise util.Abort(
502 raise util.Abort(
498 _('only one repo argument allowed with --outgoing'))
503 _('only one repo argument allowed with --outgoing'))
499 else:
504 else:
500 revs.extend(freeargs)
505 revs.extend(freeargs)
501 if len(revs) != 1:
506 if len(revs) != 1:
502 raise util.Abort(
507 raise util.Abort(
503 _('histedit requires exactly one ancestor revision'))
508 _('histedit requires exactly one ancestor revision'))
504
509
505
510
506 if goal == 'continue':
511 if goal == 'continue':
507 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
512 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
508 currentparent, wantnull = repo.dirstate.parents()
513 currentparent, wantnull = repo.dirstate.parents()
509 parentctx = repo[parentctxnode]
514 parentctx = repo[parentctxnode]
510 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
515 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
511 replacements.extend(repl)
516 replacements.extend(repl)
512 elif goal == 'abort':
517 elif goal == 'abort':
513 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
518 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
514 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
519 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
515 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
520 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
516 # check whether we should update away
521 # check whether we should update away
517 parentnodes = [c.node() for c in repo[None].parents()]
522 parentnodes = [c.node() for c in repo[None].parents()]
518 for n in leafs | set([parentctxnode]):
523 for n in leafs | set([parentctxnode]):
519 if n in parentnodes:
524 if n in parentnodes:
520 hg.clean(repo, topmost)
525 hg.clean(repo, topmost)
521 break
526 break
522 else:
527 else:
523 pass
528 pass
524 cleanupnode(ui, repo, 'created', tmpnodes)
529 cleanupnode(ui, repo, 'created', tmpnodes)
525 cleanupnode(ui, repo, 'temp', leafs)
530 cleanupnode(ui, repo, 'temp', leafs)
526 os.unlink(os.path.join(repo.path, 'histedit-state'))
531 os.unlink(os.path.join(repo.path, 'histedit-state'))
527 return
532 return
528 else:
533 else:
529 cmdutil.checkunfinished(repo)
534 cmdutil.checkunfinished(repo)
530 cmdutil.bailifchanged(repo)
535 cmdutil.bailifchanged(repo)
531
536
532 topmost, empty = repo.dirstate.parents()
537 topmost, empty = repo.dirstate.parents()
533 if outg:
538 if outg:
534 if freeargs:
539 if freeargs:
535 remote = freeargs[0]
540 remote = freeargs[0]
536 else:
541 else:
537 remote = None
542 remote = None
538 root = findoutgoing(ui, repo, remote, force, opts)
543 root = findoutgoing(ui, repo, remote, force, opts)
539 else:
544 else:
540 root = revs[0]
545 root = revs[0]
541 root = scmutil.revsingle(repo, root).node()
546 root = scmutil.revsingle(repo, root).node()
542
547
543 keep = opts.get('keep', False)
548 keep = opts.get('keep', False)
544 revs = between(repo, root, topmost, keep)
549 revs = between(repo, root, topmost, keep)
545 if not revs:
550 if not revs:
546 raise util.Abort(_('%s is not an ancestor of working directory') %
551 raise util.Abort(_('%s is not an ancestor of working directory') %
547 node.short(root))
552 node.short(root))
548
553
549 ctxs = [repo[r] for r in revs]
554 ctxs = [repo[r] for r in revs]
550 if not rules:
555 if not rules:
551 rules = '\n'.join([makedesc(c) for c in ctxs])
556 rules = '\n'.join([makedesc(c) for c in ctxs])
552 rules += '\n\n'
557 rules += '\n\n'
553 rules += editcomment % (node.short(root), node.short(topmost))
558 rules += editcomment % (node.short(root), node.short(topmost))
554 rules = ui.edit(rules, ui.username())
559 rules = ui.edit(rules, ui.username())
555 # Save edit rules in .hg/histedit-last-edit.txt in case
560 # Save edit rules in .hg/histedit-last-edit.txt in case
556 # the user needs to ask for help after something
561 # the user needs to ask for help after something
557 # surprising happens.
562 # surprising happens.
558 f = open(repo.join('histedit-last-edit.txt'), 'w')
563 f = open(repo.join('histedit-last-edit.txt'), 'w')
559 f.write(rules)
564 f.write(rules)
560 f.close()
565 f.close()
561 else:
566 else:
562 if rules == '-':
567 if rules == '-':
563 f = sys.stdin
568 f = sys.stdin
564 else:
569 else:
565 f = open(rules)
570 f = open(rules)
566 rules = f.read()
571 rules = f.read()
567 f.close()
572 f.close()
568 rules = [l for l in (r.strip() for r in rules.splitlines())
573 rules = [l for l in (r.strip() for r in rules.splitlines())
569 if l and not l[0] == '#']
574 if l and not l[0] == '#']
570 rules = verifyrules(rules, repo, ctxs)
575 rules = verifyrules(rules, repo, ctxs)
571
576
572 parentctx = repo[root].parents()[0]
577 parentctx = repo[root].parents()[0]
573 keep = opts.get('keep', False)
578 keep = opts.get('keep', False)
574 replacements = []
579 replacements = []
575
580
576
581
577 while rules:
582 while rules:
578 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
583 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
579 action, ha = rules.pop(0)
584 action, ha = rules.pop(0)
580 ui.debug('histedit: processing %s %s\n' % (action, ha))
585 ui.debug('histedit: processing %s %s\n' % (action, ha))
581 actfunc = actiontable[action]
586 actfunc = actiontable[action]
582 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
587 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
583 replacements.extend(replacement_)
588 replacements.extend(replacement_)
584
589
585 hg.update(repo, parentctx.node())
590 hg.update(repo, parentctx.node())
586
591
587 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
592 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
588 if mapping:
593 if mapping:
589 for prec, succs in mapping.iteritems():
594 for prec, succs in mapping.iteritems():
590 if not succs:
595 if not succs:
591 ui.debug('histedit: %s is dropped\n' % node.short(prec))
596 ui.debug('histedit: %s is dropped\n' % node.short(prec))
592 else:
597 else:
593 ui.debug('histedit: %s is replaced by %s\n' % (
598 ui.debug('histedit: %s is replaced by %s\n' % (
594 node.short(prec), node.short(succs[0])))
599 node.short(prec), node.short(succs[0])))
595 if len(succs) > 1:
600 if len(succs) > 1:
596 m = 'histedit: %s'
601 m = 'histedit: %s'
597 for n in succs[1:]:
602 for n in succs[1:]:
598 ui.debug(m % node.short(n))
603 ui.debug(m % node.short(n))
599
604
600 if not keep:
605 if not keep:
601 if mapping:
606 if mapping:
602 movebookmarks(ui, repo, mapping, topmost, ntm)
607 movebookmarks(ui, repo, mapping, topmost, ntm)
603 # TODO update mq state
608 # TODO update mq state
604 if obsolete._enabled:
609 if obsolete._enabled:
605 markers = []
610 markers = []
606 # sort by revision number because it sound "right"
611 # sort by revision number because it sound "right"
607 for prec in sorted(mapping, key=repo.changelog.rev):
612 for prec in sorted(mapping, key=repo.changelog.rev):
608 succs = mapping[prec]
613 succs = mapping[prec]
609 markers.append((repo[prec],
614 markers.append((repo[prec],
610 tuple(repo[s] for s in succs)))
615 tuple(repo[s] for s in succs)))
611 if markers:
616 if markers:
612 obsolete.createmarkers(repo, markers)
617 obsolete.createmarkers(repo, markers)
613 else:
618 else:
614 cleanupnode(ui, repo, 'replaced', mapping)
619 cleanupnode(ui, repo, 'replaced', mapping)
615
620
616 cleanupnode(ui, repo, 'temp', tmpnodes)
621 cleanupnode(ui, repo, 'temp', tmpnodes)
617 os.unlink(os.path.join(repo.path, 'histedit-state'))
622 os.unlink(os.path.join(repo.path, 'histedit-state'))
618 if os.path.exists(repo.sjoin('undo')):
623 if os.path.exists(repo.sjoin('undo')):
619 os.unlink(repo.sjoin('undo'))
624 os.unlink(repo.sjoin('undo'))
620
625
621
626
622 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
627 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
623 action, currentnode = rules.pop(0)
628 action, currentnode = rules.pop(0)
624 ctx = repo[currentnode]
629 ctx = repo[currentnode]
625 # is there any new commit between the expected parent and "."
630 # is there any new commit between the expected parent and "."
626 #
631 #
627 # note: does not take non linear new change in account (but previous
632 # note: does not take non linear new change in account (but previous
628 # implementation didn't used them anyway (issue3655)
633 # implementation didn't used them anyway (issue3655)
629 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
634 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
630 if parentctx.node() != node.nullid:
635 if parentctx.node() != node.nullid:
631 if not newchildren:
636 if not newchildren:
632 # `parentctxnode` should match but no result. This means that
637 # `parentctxnode` should match but no result. This means that
633 # currentnode is not a descendant from parentctxnode.
638 # currentnode is not a descendant from parentctxnode.
634 msg = _('%s is not an ancestor of working directory')
639 msg = _('%s is not an ancestor of working directory')
635 hint = _('update to %s or descendant and run "hg histedit '
640 hint = _('update to %s or descendant and run "hg histedit '
636 '--continue" again') % parentctx
641 '--continue" again') % parentctx
637 raise util.Abort(msg % parentctx, hint=hint)
642 raise util.Abort(msg % parentctx, hint=hint)
638 newchildren.pop(0) # remove parentctxnode
643 newchildren.pop(0) # remove parentctxnode
639 # Commit dirty working directory if necessary
644 # Commit dirty working directory if necessary
640 new = None
645 new = None
641 m, a, r, d = repo.status()[:4]
646 m, a, r, d = repo.status()[:4]
642 if m or a or r or d:
647 if m or a or r or d:
643 # prepare the message for the commit to comes
648 # prepare the message for the commit to comes
644 if action in ('f', 'fold'):
649 if action in ('f', 'fold'):
645 message = 'fold-temp-revision %s' % currentnode
650 message = 'fold-temp-revision %s' % currentnode
646 else:
651 else:
647 message = ctx.description() + '\n'
652 message = ctx.description() + '\n'
648 if action in ('e', 'edit', 'm', 'mess'):
653 if action in ('e', 'edit', 'm', 'mess'):
649 editor = cmdutil.commitforceeditor
654 editor = cmdutil.commitforceeditor
650 else:
655 else:
651 editor = False
656 editor = False
652 commit = commitfuncfor(repo, ctx)
657 commit = commitfuncfor(repo, ctx)
653 new = commit(text=message, user=ctx.user(),
658 new = commit(text=message, user=ctx.user(),
654 date=ctx.date(), extra=ctx.extra(),
659 date=ctx.date(), extra=ctx.extra(),
655 editor=editor)
660 editor=editor)
656 if new is not None:
661 if new is not None:
657 newchildren.append(new)
662 newchildren.append(new)
658
663
659 replacements = []
664 replacements = []
660 # track replacements
665 # track replacements
661 if ctx.node() not in newchildren:
666 if ctx.node() not in newchildren:
662 # note: new children may be empty when the changeset is dropped.
667 # note: new children may be empty when the changeset is dropped.
663 # this happen e.g during conflicting pick where we revert content
668 # this happen e.g during conflicting pick where we revert content
664 # to parent.
669 # to parent.
665 replacements.append((ctx.node(), tuple(newchildren)))
670 replacements.append((ctx.node(), tuple(newchildren)))
666
671
667 if action in ('f', 'fold'):
672 if action in ('f', 'fold'):
668 if newchildren:
673 if newchildren:
669 # finalize fold operation if applicable
674 # finalize fold operation if applicable
670 if new is None:
675 if new is None:
671 new = newchildren[-1]
676 new = newchildren[-1]
672 else:
677 else:
673 newchildren.pop() # remove new from internal changes
678 newchildren.pop() # remove new from internal changes
674 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
679 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
675 newchildren)
680 newchildren)
676 replacements.extend(repl)
681 replacements.extend(repl)
677 else:
682 else:
678 # newchildren is empty if the fold did not result in any commit
683 # newchildren is empty if the fold did not result in any commit
679 # this happen when all folded change are discarded during the
684 # this happen when all folded change are discarded during the
680 # merge.
685 # merge.
681 replacements.append((ctx.node(), (parentctx.node(),)))
686 replacements.append((ctx.node(), (parentctx.node(),)))
682 elif newchildren:
687 elif newchildren:
683 # otherwise update "parentctx" before proceeding to further operation
688 # otherwise update "parentctx" before proceeding to further operation
684 parentctx = repo[newchildren[-1]]
689 parentctx = repo[newchildren[-1]]
685 return parentctx, replacements
690 return parentctx, replacements
686
691
687
692
688 def between(repo, old, new, keep):
693 def between(repo, old, new, keep):
689 """select and validate the set of revision to edit
694 """select and validate the set of revision to edit
690
695
691 When keep is false, the specified set can't have children."""
696 When keep is false, the specified set can't have children."""
692 ctxs = list(repo.set('%n::%n', old, new))
697 ctxs = list(repo.set('%n::%n', old, new))
693 if ctxs and not keep:
698 if ctxs and not keep:
694 if (not obsolete._enabled and
699 if (not obsolete._enabled and
695 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
700 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
696 raise util.Abort(_('cannot edit history that would orphan nodes'))
701 raise util.Abort(_('cannot edit history that would orphan nodes'))
697 if repo.revs('(%ld) and merge()', ctxs):
702 if repo.revs('(%ld) and merge()', ctxs):
698 raise util.Abort(_('cannot edit history that contains merges'))
703 raise util.Abort(_('cannot edit history that contains merges'))
699 root = ctxs[0] # list is already sorted by repo.set
704 root = ctxs[0] # list is already sorted by repo.set
700 if not root.phase():
705 if not root.phase():
701 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
706 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
702 return [c.node() for c in ctxs]
707 return [c.node() for c in ctxs]
703
708
704
709
705 def writestate(repo, parentnode, rules, keep, topmost, replacements):
710 def writestate(repo, parentnode, rules, keep, topmost, replacements):
706 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
711 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
707 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
712 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
708 fp.close()
713 fp.close()
709
714
710 def readstate(repo):
715 def readstate(repo):
711 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
716 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
712 """
717 """
713 fp = open(os.path.join(repo.path, 'histedit-state'))
718 fp = open(os.path.join(repo.path, 'histedit-state'))
714 return pickle.load(fp)
719 return pickle.load(fp)
715
720
716
721
717 def makedesc(c):
722 def makedesc(c):
718 """build a initial action line for a ctx `c`
723 """build a initial action line for a ctx `c`
719
724
720 line are in the form:
725 line are in the form:
721
726
722 pick <hash> <rev> <summary>
727 pick <hash> <rev> <summary>
723 """
728 """
724 summary = ''
729 summary = ''
725 if c.description():
730 if c.description():
726 summary = c.description().splitlines()[0]
731 summary = c.description().splitlines()[0]
727 line = 'pick %s %d %s' % (c, c.rev(), summary)
732 line = 'pick %s %d %s' % (c, c.rev(), summary)
728 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
733 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
729
734
730 def verifyrules(rules, repo, ctxs):
735 def verifyrules(rules, repo, ctxs):
731 """Verify that there exists exactly one edit rule per given changeset.
736 """Verify that there exists exactly one edit rule per given changeset.
732
737
733 Will abort if there are to many or too few rules, a malformed rule,
738 Will abort if there are to many or too few rules, a malformed rule,
734 or a rule on a changeset outside of the user-given range.
739 or a rule on a changeset outside of the user-given range.
735 """
740 """
736 parsed = []
741 parsed = []
737 expected = set(str(c) for c in ctxs)
742 expected = set(str(c) for c in ctxs)
738 seen = set()
743 seen = set()
739 for r in rules:
744 for r in rules:
740 if ' ' not in r:
745 if ' ' not in r:
741 raise util.Abort(_('malformed line "%s"') % r)
746 raise util.Abort(_('malformed line "%s"') % r)
742 action, rest = r.split(' ', 1)
747 action, rest = r.split(' ', 1)
743 ha = rest.strip().split(' ', 1)[0]
748 ha = rest.strip().split(' ', 1)[0]
744 try:
749 try:
745 ha = str(repo[ha]) # ensure its a short hash
750 ha = str(repo[ha]) # ensure its a short hash
746 except error.RepoError:
751 except error.RepoError:
747 raise util.Abort(_('unknown changeset %s listed') % ha)
752 raise util.Abort(_('unknown changeset %s listed') % ha)
748 if ha not in expected:
753 if ha not in expected:
749 raise util.Abort(
754 raise util.Abort(
750 _('may not use changesets other than the ones listed'))
755 _('may not use changesets other than the ones listed'))
751 if ha in seen:
756 if ha in seen:
752 raise util.Abort(_('duplicated command for changeset %s') % ha)
757 raise util.Abort(_('duplicated command for changeset %s') % ha)
753 seen.add(ha)
758 seen.add(ha)
754 if action not in actiontable:
759 if action not in actiontable:
755 raise util.Abort(_('unknown action "%s"') % action)
760 raise util.Abort(_('unknown action "%s"') % action)
756 parsed.append([action, ha])
761 parsed.append([action, ha])
757 missing = sorted(expected - seen) # sort to stabilize output
762 missing = sorted(expected - seen) # sort to stabilize output
758 if missing:
763 if missing:
759 raise util.Abort(_('missing rules for changeset %s') % missing[0],
764 raise util.Abort(_('missing rules for changeset %s') % missing[0],
760 hint=_('do you want to use the drop action?'))
765 hint=_('do you want to use the drop action?'))
761 return parsed
766 return parsed
762
767
763 def processreplacement(repo, replacements):
768 def processreplacement(repo, replacements):
764 """process the list of replacements to return
769 """process the list of replacements to return
765
770
766 1) the final mapping between original and created nodes
771 1) the final mapping between original and created nodes
767 2) the list of temporary node created by histedit
772 2) the list of temporary node created by histedit
768 3) the list of new commit created by histedit"""
773 3) the list of new commit created by histedit"""
769 allsuccs = set()
774 allsuccs = set()
770 replaced = set()
775 replaced = set()
771 fullmapping = {}
776 fullmapping = {}
772 # initialise basic set
777 # initialise basic set
773 # fullmapping record all operation recorded in replacement
778 # fullmapping record all operation recorded in replacement
774 for rep in replacements:
779 for rep in replacements:
775 allsuccs.update(rep[1])
780 allsuccs.update(rep[1])
776 replaced.add(rep[0])
781 replaced.add(rep[0])
777 fullmapping.setdefault(rep[0], set()).update(rep[1])
782 fullmapping.setdefault(rep[0], set()).update(rep[1])
778 new = allsuccs - replaced
783 new = allsuccs - replaced
779 tmpnodes = allsuccs & replaced
784 tmpnodes = allsuccs & replaced
780 # Reduce content fullmapping into direct relation between original nodes
785 # Reduce content fullmapping into direct relation between original nodes
781 # and final node created during history edition
786 # and final node created during history edition
782 # Dropped changeset are replaced by an empty list
787 # Dropped changeset are replaced by an empty list
783 toproceed = set(fullmapping)
788 toproceed = set(fullmapping)
784 final = {}
789 final = {}
785 while toproceed:
790 while toproceed:
786 for x in list(toproceed):
791 for x in list(toproceed):
787 succs = fullmapping[x]
792 succs = fullmapping[x]
788 for s in list(succs):
793 for s in list(succs):
789 if s in toproceed:
794 if s in toproceed:
790 # non final node with unknown closure
795 # non final node with unknown closure
791 # We can't process this now
796 # We can't process this now
792 break
797 break
793 elif s in final:
798 elif s in final:
794 # non final node, replace with closure
799 # non final node, replace with closure
795 succs.remove(s)
800 succs.remove(s)
796 succs.update(final[s])
801 succs.update(final[s])
797 else:
802 else:
798 final[x] = succs
803 final[x] = succs
799 toproceed.remove(x)
804 toproceed.remove(x)
800 # remove tmpnodes from final mapping
805 # remove tmpnodes from final mapping
801 for n in tmpnodes:
806 for n in tmpnodes:
802 del final[n]
807 del final[n]
803 # we expect all changes involved in final to exist in the repo
808 # we expect all changes involved in final to exist in the repo
804 # turn `final` into list (topologically sorted)
809 # turn `final` into list (topologically sorted)
805 nm = repo.changelog.nodemap
810 nm = repo.changelog.nodemap
806 for prec, succs in final.items():
811 for prec, succs in final.items():
807 final[prec] = sorted(succs, key=nm.get)
812 final[prec] = sorted(succs, key=nm.get)
808
813
809 # computed topmost element (necessary for bookmark)
814 # computed topmost element (necessary for bookmark)
810 if new:
815 if new:
811 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
816 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
812 elif not final:
817 elif not final:
813 # Nothing rewritten at all. we won't need `newtopmost`
818 # Nothing rewritten at all. we won't need `newtopmost`
814 # It is the same as `oldtopmost` and `processreplacement` know it
819 # It is the same as `oldtopmost` and `processreplacement` know it
815 newtopmost = None
820 newtopmost = None
816 else:
821 else:
817 # every body died. The newtopmost is the parent of the root.
822 # every body died. The newtopmost is the parent of the root.
818 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
823 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
819
824
820 return final, tmpnodes, new, newtopmost
825 return final, tmpnodes, new, newtopmost
821
826
822 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
827 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
823 """Move bookmark from old to newly created node"""
828 """Move bookmark from old to newly created node"""
824 if not mapping:
829 if not mapping:
825 # if nothing got rewritten there is not purpose for this function
830 # if nothing got rewritten there is not purpose for this function
826 return
831 return
827 moves = []
832 moves = []
828 for bk, old in sorted(repo._bookmarks.iteritems()):
833 for bk, old in sorted(repo._bookmarks.iteritems()):
829 if old == oldtopmost:
834 if old == oldtopmost:
830 # special case ensure bookmark stay on tip.
835 # special case ensure bookmark stay on tip.
831 #
836 #
832 # This is arguably a feature and we may only want that for the
837 # This is arguably a feature and we may only want that for the
833 # active bookmark. But the behavior is kept compatible with the old
838 # active bookmark. But the behavior is kept compatible with the old
834 # version for now.
839 # version for now.
835 moves.append((bk, newtopmost))
840 moves.append((bk, newtopmost))
836 continue
841 continue
837 base = old
842 base = old
838 new = mapping.get(base, None)
843 new = mapping.get(base, None)
839 if new is None:
844 if new is None:
840 continue
845 continue
841 while not new:
846 while not new:
842 # base is killed, trying with parent
847 # base is killed, trying with parent
843 base = repo[base].p1().node()
848 base = repo[base].p1().node()
844 new = mapping.get(base, (base,))
849 new = mapping.get(base, (base,))
845 # nothing to move
850 # nothing to move
846 moves.append((bk, new[-1]))
851 moves.append((bk, new[-1]))
847 if moves:
852 if moves:
848 marks = repo._bookmarks
853 marks = repo._bookmarks
849 for mark, new in moves:
854 for mark, new in moves:
850 old = marks[mark]
855 old = marks[mark]
851 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
856 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
852 % (mark, node.short(old), node.short(new)))
857 % (mark, node.short(old), node.short(new)))
853 marks[mark] = new
858 marks[mark] = new
854 marks.write()
859 marks.write()
855
860
856 def cleanupnode(ui, repo, name, nodes):
861 def cleanupnode(ui, repo, name, nodes):
857 """strip a group of nodes from the repository
862 """strip a group of nodes from the repository
858
863
859 The set of node to strip may contains unknown nodes."""
864 The set of node to strip may contains unknown nodes."""
860 ui.debug('should strip %s nodes %s\n' %
865 ui.debug('should strip %s nodes %s\n' %
861 (name, ', '.join([node.short(n) for n in nodes])))
866 (name, ', '.join([node.short(n) for n in nodes])))
862 lock = None
867 lock = None
863 try:
868 try:
864 lock = repo.lock()
869 lock = repo.lock()
865 # Find all node that need to be stripped
870 # Find all node that need to be stripped
866 # (we hg %lr instead of %ln to silently ignore unknown item
871 # (we hg %lr instead of %ln to silently ignore unknown item
867 nm = repo.changelog.nodemap
872 nm = repo.changelog.nodemap
868 nodes = [n for n in nodes if n in nm]
873 nodes = [n for n in nodes if n in nm]
869 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
874 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
870 for c in roots:
875 for c in roots:
871 # We should process node in reverse order to strip tip most first.
876 # We should process node in reverse order to strip tip most first.
872 # but this trigger a bug in changegroup hook.
877 # but this trigger a bug in changegroup hook.
873 # This would reduce bundle overhead
878 # This would reduce bundle overhead
874 repair.strip(ui, repo, c)
879 repair.strip(ui, repo, c)
875 finally:
880 finally:
876 lockmod.release(lock)
881 lockmod.release(lock)
877
882
878 def summaryhook(ui, repo):
883 def summaryhook(ui, repo):
879 if not os.path.exists(repo.join('histedit-state')):
884 if not os.path.exists(repo.join('histedit-state')):
880 return
885 return
881 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
886 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
882 if rules:
887 if rules:
883 # i18n: column positioning for "hg summary"
888 # i18n: column positioning for "hg summary"
884 ui.write(_('hist: %s (histedit --continue)\n') %
889 ui.write(_('hist: %s (histedit --continue)\n') %
885 (ui.label(_('%d remaining'), 'histedit.remaining') %
890 (ui.label(_('%d remaining'), 'histedit.remaining') %
886 len(rules)))
891 len(rules)))
887
892
888 def extsetup(ui):
893 def extsetup(ui):
889 cmdutil.summaryhooks.add('histedit', summaryhook)
894 cmdutil.summaryhooks.add('histedit', summaryhook)
890 cmdutil.unfinishedstates.append(
895 cmdutil.unfinishedstates.append(
891 ['histedit-state', False, True, _('histedit in progress'),
896 ['histedit-state', False, True, _('histedit in progress'),
892 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
897 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,105 +1,139 b''
1 $ cat >> $HGRCPATH <<EOF
1 $ cat >> $HGRCPATH <<EOF
2 > [extensions]
2 > [extensions]
3 > graphlog=
3 > graphlog=
4 > histedit=
4 > histedit=
5 > EOF
5 > EOF
6
6
7 $ initrepos ()
7 $ initrepos ()
8 > {
8 > {
9 > hg init r
9 > hg init r
10 > cd r
10 > cd r
11 > for x in a b c ; do
11 > for x in a b c ; do
12 > echo $x > $x
12 > echo $x > $x
13 > hg add $x
13 > hg add $x
14 > hg ci -m $x
14 > hg ci -m $x
15 > done
15 > done
16 > cd ..
16 > cd ..
17 > hg clone r r2 | grep -v updating
17 > hg clone r r2 | grep -v updating
18 > cd r2
18 > cd r2
19 > for x in d e f ; do
19 > for x in d e f ; do
20 > echo $x > $x
20 > echo $x > $x
21 > hg add $x
21 > hg add $x
22 > hg ci -m $x
22 > hg ci -m $x
23 > done
23 > done
24 > cd ..
24 > cd ..
25 > hg init r3
25 > hg init r3
26 > cd r3
26 > cd r3
27 > for x in g h i ; do
27 > for x in g h i ; do
28 > echo $x > $x
28 > echo $x > $x
29 > hg add $x
29 > hg add $x
30 > hg ci -m $x
30 > hg ci -m $x
31 > done
31 > done
32 > cd ..
32 > cd ..
33 > }
33 > }
34
34
35 $ initrepos
35 $ initrepos
36 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
36 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
37
37
38 show the edit commands offered by outgoing
38 show the edit commands offered by outgoing
39 $ cd r2
39 $ cd r2
40 $ HGEDITOR=cat hg histedit --outgoing ../r | grep -v comparing | grep -v searching
40 $ HGEDITOR=cat hg histedit --outgoing ../r | grep -v comparing | grep -v searching
41 pick 055a42cdd887 3 d
41 pick 055a42cdd887 3 d
42 pick e860deea161a 4 e
42 pick e860deea161a 4 e
43 pick 652413bf663e 5 f
43 pick 652413bf663e 5 f
44
44
45 # Edit history between 055a42cdd887 and 652413bf663e
45 # Edit history between 055a42cdd887 and 652413bf663e
46 #
46 #
47 # Commands:
47 # Commands:
48 # p, pick = use commit
48 # p, pick = use commit
49 # e, edit = use commit, but stop for amending
49 # e, edit = use commit, but stop for amending
50 # f, fold = use commit, but fold into previous commit (combines N and N-1)
50 # f, fold = use commit, but fold into previous commit (combines N and N-1)
51 # d, drop = remove commit from history
51 # d, drop = remove commit from history
52 # m, mess = edit message without changing commit content
52 # m, mess = edit message without changing commit content
53 #
53 #
54 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
54 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
55 $ cd ..
55 $ cd ..
56
56
57 show the error from unrelated repos
57 show the error from unrelated repos
58 $ cd r3
58 $ cd r3
59 $ HGEDITOR=cat hg histedit --outgoing ../r | grep -v comparing | grep -v searching
59 $ HGEDITOR=cat hg histedit --outgoing ../r | grep -v comparing | grep -v searching
60 abort: repository is unrelated
60 abort: repository is unrelated
61 [1]
61 [1]
62 $ cd ..
62 $ cd ..
63
63
64 show the error from unrelated repos
64 show the error from unrelated repos
65 $ cd r3
65 $ cd r3
66 $ HGEDITOR=cat hg histedit --force --outgoing ../r
66 $ HGEDITOR=cat hg histedit --force --outgoing ../r
67 comparing with ../r
67 comparing with ../r
68 searching for changes
68 searching for changes
69 warning: repository is unrelated
69 warning: repository is unrelated
70 pick 2a4042b45417 0 g
70 pick 2a4042b45417 0 g
71 pick 68c46b4927ce 1 h
71 pick 68c46b4927ce 1 h
72 pick 51281e65ba79 2 i
72 pick 51281e65ba79 2 i
73
73
74 # Edit history between 2a4042b45417 and 51281e65ba79
74 # Edit history between 2a4042b45417 and 51281e65ba79
75 #
75 #
76 # Commands:
76 # Commands:
77 # p, pick = use commit
77 # p, pick = use commit
78 # e, edit = use commit, but stop for amending
78 # e, edit = use commit, but stop for amending
79 # f, fold = use commit, but fold into previous commit (combines N and N-1)
79 # f, fold = use commit, but fold into previous commit (combines N and N-1)
80 # d, drop = remove commit from history
80 # d, drop = remove commit from history
81 # m, mess = edit message without changing commit content
81 # m, mess = edit message without changing commit content
82 #
82 #
83 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
83 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
84 $ cd ..
84 $ cd ..
85
85
86 test sensitivity to branch in URL:
86 test sensitivity to branch in URL:
87
87
88 $ cd r2
88 $ cd r2
89 $ hg -q update 2
89 $ hg -q update 2
90 $ hg -q branch foo
90 $ hg -q branch foo
91 $ hg commit -m 'create foo branch'
91 $ hg commit -m 'create foo branch'
92 $ HGEDITOR=cat hg histedit --outgoing '../r#foo' | grep -v comparing | grep -v searching
92 $ HGEDITOR=cat hg histedit --outgoing '../r#foo' | grep -v comparing | grep -v searching
93 pick f26599ee3441 6 create foo branch
93 pick f26599ee3441 6 create foo branch
94
94
95 # Edit history between f26599ee3441 and f26599ee3441
95 # Edit history between f26599ee3441 and f26599ee3441
96 #
96 #
97 # Commands:
97 # Commands:
98 # p, pick = use commit
98 # p, pick = use commit
99 # e, edit = use commit, but stop for amending
99 # e, edit = use commit, but stop for amending
100 # f, fold = use commit, but fold into previous commit (combines N and N-1)
100 # f, fold = use commit, but fold into previous commit (combines N and N-1)
101 # d, drop = remove commit from history
101 # d, drop = remove commit from history
102 # m, mess = edit message without changing commit content
102 # m, mess = edit message without changing commit content
103 #
103 #
104 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
104 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
105
106 test to check number of roots in outgoing revisions
107
108 $ hg -q outgoing -G --template '{node|short}({branch})' '../r'
109 @ f26599ee3441(foo)
110
111 o 652413bf663e(default)
112 |
113 o e860deea161a(default)
114 |
115 o 055a42cdd887(default)
116
117 $ HGEDITOR=cat hg -q histedit --outgoing '../r'
118 abort: there are ambiguous outgoing revisions
119 (see "hg help histedit" for more detail)
120 [255]
121
122 $ hg -q update -C 2
123 $ echo aa >> a
124 $ hg -q commit -m 'another head on default'
125 $ hg -q outgoing -G --template '{node|short}({branch})' '../r#default'
126 @ 3879dc049647(default)
127
128 o 652413bf663e(default)
129 |
130 o e860deea161a(default)
131 |
132 o 055a42cdd887(default)
133
134 $ HGEDITOR=cat hg -q histedit --outgoing '../r#default'
135 abort: there are ambiguous outgoing revisions
136 (see "hg help histedit" for more detail)
137 [255]
138
105 $ cd ..
139 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now