##// END OF EJS Templates
histedit: add description about exit code
FUJIWARA Katsunori -
r19972:1e13a5a9 stable
parent child Browse files
Show More
@@ -1,900 +1,904 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 roots = list(repo.revs("roots(%ln)", outgoing.missing))
425 roots = list(repo.revs("roots(%ln)", outgoing.missing))
426 if 1 < len(roots):
426 if 1 < len(roots):
427 msg = _('there are ambiguous outgoing revisions')
427 msg = _('there are ambiguous outgoing revisions')
428 hint = _('see "hg help histedit" for more detail')
428 hint = _('see "hg help histedit" for more detail')
429 raise util.Abort(msg, hint=hint)
429 raise util.Abort(msg, hint=hint)
430 return repo.lookup(roots[0])
430 return repo.lookup(roots[0])
431
431
432 actiontable = {'p': pick,
432 actiontable = {'p': pick,
433 'pick': pick,
433 'pick': pick,
434 'e': edit,
434 'e': edit,
435 'edit': edit,
435 'edit': edit,
436 'f': fold,
436 'f': fold,
437 'fold': fold,
437 'fold': fold,
438 'd': drop,
438 'd': drop,
439 'drop': drop,
439 'drop': drop,
440 'm': message,
440 'm': message,
441 'mess': message,
441 'mess': message,
442 }
442 }
443
443
444 @command('histedit',
444 @command('histedit',
445 [('', 'commands', '',
445 [('', 'commands', '',
446 _('Read history edits from the specified file.')),
446 _('Read history edits from the specified file.')),
447 ('c', 'continue', False, _('continue an edit already in progress')),
447 ('c', 'continue', False, _('continue an edit already in progress')),
448 ('k', 'keep', False,
448 ('k', 'keep', False,
449 _("don't strip old nodes after edit is complete")),
449 _("don't strip old nodes after edit is complete")),
450 ('', 'abort', False, _('abort an edit in progress')),
450 ('', 'abort', False, _('abort an edit in progress')),
451 ('o', 'outgoing', False, _('changesets not found in destination')),
451 ('o', 'outgoing', False, _('changesets not found in destination')),
452 ('f', 'force', False,
452 ('f', 'force', False,
453 _('force outgoing even for unrelated repositories')),
453 _('force outgoing even for unrelated repositories')),
454 ('r', 'rev', [], _('first revision to be edited'))],
454 ('r', 'rev', [], _('first revision to be edited'))],
455 _("ANCESTOR | --outgoing [URL]"))
455 _("ANCESTOR | --outgoing [URL]"))
456 def histedit(ui, repo, *freeargs, **opts):
456 def histedit(ui, repo, *freeargs, **opts):
457 """interactively edit changeset history
457 """interactively edit changeset history
458
458
459 This command edits changesets between ANCESTOR and the parent of
459 This command edits changesets between ANCESTOR and the parent of
460 the working directory.
460 the working directory.
461
461
462 With --outgoing, this edits changesets not found in the
462 With --outgoing, this edits changesets not found in the
463 destination repository. If URL of the destination is omitted, the
463 destination repository. If URL of the destination is omitted, the
464 'default-push' (or 'default') path will be used.
464 'default-push' (or 'default') path will be used.
465
465
466 For safety, this command is aborted, also if there are ambiguous
466 For safety, this command is aborted, also if there are ambiguous
467 outgoing revisions which may confuse users: for example, there are
467 outgoing revisions which may confuse users: for example, there are
468 multiple branches containing outgoing revisions.
468 multiple branches containing outgoing revisions.
469
469
470 Use "min(outgoing() and ::.)" or similar revset specification
470 Use "min(outgoing() and ::.)" or similar revset specification
471 instead of --outgoing to specify edit target revision exactly in
471 instead of --outgoing to specify edit target revision exactly in
472 such ambiguous situation. See :hg:`help revsets` for detail about
472 such ambiguous situation. See :hg:`help revsets` for detail about
473 selecting revisions.
473 selecting revisions.
474
475 Returns 0 on success, 1 if user intervention is required (not only
476 for intentional "edit" command, but also for resolving unexpected
477 conflicts).
474 """
478 """
475 # TODO only abort if we try and histedit mq patches, not just
479 # TODO only abort if we try and histedit mq patches, not just
476 # blanket if mq patches are applied somewhere
480 # blanket if mq patches are applied somewhere
477 mq = getattr(repo, 'mq', None)
481 mq = getattr(repo, 'mq', None)
478 if mq and mq.applied:
482 if mq and mq.applied:
479 raise util.Abort(_('source has mq patches applied'))
483 raise util.Abort(_('source has mq patches applied'))
480
484
481 # basic argument incompatibility processing
485 # basic argument incompatibility processing
482 outg = opts.get('outgoing')
486 outg = opts.get('outgoing')
483 cont = opts.get('continue')
487 cont = opts.get('continue')
484 abort = opts.get('abort')
488 abort = opts.get('abort')
485 force = opts.get('force')
489 force = opts.get('force')
486 rules = opts.get('commands', '')
490 rules = opts.get('commands', '')
487 revs = opts.get('rev', [])
491 revs = opts.get('rev', [])
488 goal = 'new' # This invocation goal, in new, continue, abort
492 goal = 'new' # This invocation goal, in new, continue, abort
489 if force and not outg:
493 if force and not outg:
490 raise util.Abort(_('--force only allowed with --outgoing'))
494 raise util.Abort(_('--force only allowed with --outgoing'))
491 if cont:
495 if cont:
492 if util.any((outg, abort, revs, freeargs, rules)):
496 if util.any((outg, abort, revs, freeargs, rules)):
493 raise util.Abort(_('no arguments allowed with --continue'))
497 raise util.Abort(_('no arguments allowed with --continue'))
494 goal = 'continue'
498 goal = 'continue'
495 elif abort:
499 elif abort:
496 if util.any((outg, revs, freeargs, rules)):
500 if util.any((outg, revs, freeargs, rules)):
497 raise util.Abort(_('no arguments allowed with --abort'))
501 raise util.Abort(_('no arguments allowed with --abort'))
498 goal = 'abort'
502 goal = 'abort'
499 else:
503 else:
500 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
504 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
501 raise util.Abort(_('history edit already in progress, try '
505 raise util.Abort(_('history edit already in progress, try '
502 '--continue or --abort'))
506 '--continue or --abort'))
503 if outg:
507 if outg:
504 if revs:
508 if revs:
505 raise util.Abort(_('no revisions allowed with --outgoing'))
509 raise util.Abort(_('no revisions allowed with --outgoing'))
506 if len(freeargs) > 1:
510 if len(freeargs) > 1:
507 raise util.Abort(
511 raise util.Abort(
508 _('only one repo argument allowed with --outgoing'))
512 _('only one repo argument allowed with --outgoing'))
509 else:
513 else:
510 revs.extend(freeargs)
514 revs.extend(freeargs)
511 if len(revs) != 1:
515 if len(revs) != 1:
512 raise util.Abort(
516 raise util.Abort(
513 _('histedit requires exactly one ancestor revision'))
517 _('histedit requires exactly one ancestor revision'))
514
518
515
519
516 if goal == 'continue':
520 if goal == 'continue':
517 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
521 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
518 parentctx = repo[parentctxnode]
522 parentctx = repo[parentctxnode]
519 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
523 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
520 replacements.extend(repl)
524 replacements.extend(repl)
521 elif goal == 'abort':
525 elif goal == 'abort':
522 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
526 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
523 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
527 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
524 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
528 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
525 # check whether we should update away
529 # check whether we should update away
526 parentnodes = [c.node() for c in repo[None].parents()]
530 parentnodes = [c.node() for c in repo[None].parents()]
527 for n in leafs | set([parentctxnode]):
531 for n in leafs | set([parentctxnode]):
528 if n in parentnodes:
532 if n in parentnodes:
529 hg.clean(repo, topmost)
533 hg.clean(repo, topmost)
530 break
534 break
531 else:
535 else:
532 pass
536 pass
533 cleanupnode(ui, repo, 'created', tmpnodes)
537 cleanupnode(ui, repo, 'created', tmpnodes)
534 cleanupnode(ui, repo, 'temp', leafs)
538 cleanupnode(ui, repo, 'temp', leafs)
535 os.unlink(os.path.join(repo.path, 'histedit-state'))
539 os.unlink(os.path.join(repo.path, 'histedit-state'))
536 return
540 return
537 else:
541 else:
538 cmdutil.checkunfinished(repo)
542 cmdutil.checkunfinished(repo)
539 cmdutil.bailifchanged(repo)
543 cmdutil.bailifchanged(repo)
540
544
541 topmost, empty = repo.dirstate.parents()
545 topmost, empty = repo.dirstate.parents()
542 if outg:
546 if outg:
543 if freeargs:
547 if freeargs:
544 remote = freeargs[0]
548 remote = freeargs[0]
545 else:
549 else:
546 remote = None
550 remote = None
547 root = findoutgoing(ui, repo, remote, force, opts)
551 root = findoutgoing(ui, repo, remote, force, opts)
548 else:
552 else:
549 root = revs[0]
553 root = revs[0]
550 root = scmutil.revsingle(repo, root).node()
554 root = scmutil.revsingle(repo, root).node()
551
555
552 keep = opts.get('keep', False)
556 keep = opts.get('keep', False)
553 revs = between(repo, root, topmost, keep)
557 revs = between(repo, root, topmost, keep)
554 if not revs:
558 if not revs:
555 raise util.Abort(_('%s is not an ancestor of working directory') %
559 raise util.Abort(_('%s is not an ancestor of working directory') %
556 node.short(root))
560 node.short(root))
557
561
558 ctxs = [repo[r] for r in revs]
562 ctxs = [repo[r] for r in revs]
559 if not rules:
563 if not rules:
560 rules = '\n'.join([makedesc(c) for c in ctxs])
564 rules = '\n'.join([makedesc(c) for c in ctxs])
561 rules += '\n\n'
565 rules += '\n\n'
562 rules += editcomment % (node.short(root), node.short(topmost))
566 rules += editcomment % (node.short(root), node.short(topmost))
563 rules = ui.edit(rules, ui.username())
567 rules = ui.edit(rules, ui.username())
564 # Save edit rules in .hg/histedit-last-edit.txt in case
568 # Save edit rules in .hg/histedit-last-edit.txt in case
565 # the user needs to ask for help after something
569 # the user needs to ask for help after something
566 # surprising happens.
570 # surprising happens.
567 f = open(repo.join('histedit-last-edit.txt'), 'w')
571 f = open(repo.join('histedit-last-edit.txt'), 'w')
568 f.write(rules)
572 f.write(rules)
569 f.close()
573 f.close()
570 else:
574 else:
571 if rules == '-':
575 if rules == '-':
572 f = sys.stdin
576 f = sys.stdin
573 else:
577 else:
574 f = open(rules)
578 f = open(rules)
575 rules = f.read()
579 rules = f.read()
576 f.close()
580 f.close()
577 rules = [l for l in (r.strip() for r in rules.splitlines())
581 rules = [l for l in (r.strip() for r in rules.splitlines())
578 if l and not l[0] == '#']
582 if l and not l[0] == '#']
579 rules = verifyrules(rules, repo, ctxs)
583 rules = verifyrules(rules, repo, ctxs)
580
584
581 parentctx = repo[root].parents()[0]
585 parentctx = repo[root].parents()[0]
582 keep = opts.get('keep', False)
586 keep = opts.get('keep', False)
583 replacements = []
587 replacements = []
584
588
585
589
586 while rules:
590 while rules:
587 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
591 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
588 action, ha = rules.pop(0)
592 action, ha = rules.pop(0)
589 ui.debug('histedit: processing %s %s\n' % (action, ha))
593 ui.debug('histedit: processing %s %s\n' % (action, ha))
590 actfunc = actiontable[action]
594 actfunc = actiontable[action]
591 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
595 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
592 replacements.extend(replacement_)
596 replacements.extend(replacement_)
593
597
594 hg.update(repo, parentctx.node())
598 hg.update(repo, parentctx.node())
595
599
596 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
600 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
597 if mapping:
601 if mapping:
598 for prec, succs in mapping.iteritems():
602 for prec, succs in mapping.iteritems():
599 if not succs:
603 if not succs:
600 ui.debug('histedit: %s is dropped\n' % node.short(prec))
604 ui.debug('histedit: %s is dropped\n' % node.short(prec))
601 else:
605 else:
602 ui.debug('histedit: %s is replaced by %s\n' % (
606 ui.debug('histedit: %s is replaced by %s\n' % (
603 node.short(prec), node.short(succs[0])))
607 node.short(prec), node.short(succs[0])))
604 if len(succs) > 1:
608 if len(succs) > 1:
605 m = 'histedit: %s'
609 m = 'histedit: %s'
606 for n in succs[1:]:
610 for n in succs[1:]:
607 ui.debug(m % node.short(n))
611 ui.debug(m % node.short(n))
608
612
609 if not keep:
613 if not keep:
610 if mapping:
614 if mapping:
611 movebookmarks(ui, repo, mapping, topmost, ntm)
615 movebookmarks(ui, repo, mapping, topmost, ntm)
612 # TODO update mq state
616 # TODO update mq state
613 if obsolete._enabled:
617 if obsolete._enabled:
614 markers = []
618 markers = []
615 # sort by revision number because it sound "right"
619 # sort by revision number because it sound "right"
616 for prec in sorted(mapping, key=repo.changelog.rev):
620 for prec in sorted(mapping, key=repo.changelog.rev):
617 succs = mapping[prec]
621 succs = mapping[prec]
618 markers.append((repo[prec],
622 markers.append((repo[prec],
619 tuple(repo[s] for s in succs)))
623 tuple(repo[s] for s in succs)))
620 if markers:
624 if markers:
621 obsolete.createmarkers(repo, markers)
625 obsolete.createmarkers(repo, markers)
622 else:
626 else:
623 cleanupnode(ui, repo, 'replaced', mapping)
627 cleanupnode(ui, repo, 'replaced', mapping)
624
628
625 cleanupnode(ui, repo, 'temp', tmpnodes)
629 cleanupnode(ui, repo, 'temp', tmpnodes)
626 os.unlink(os.path.join(repo.path, 'histedit-state'))
630 os.unlink(os.path.join(repo.path, 'histedit-state'))
627 if os.path.exists(repo.sjoin('undo')):
631 if os.path.exists(repo.sjoin('undo')):
628 os.unlink(repo.sjoin('undo'))
632 os.unlink(repo.sjoin('undo'))
629
633
630
634
631 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
635 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
632 action, currentnode = rules.pop(0)
636 action, currentnode = rules.pop(0)
633 ctx = repo[currentnode]
637 ctx = repo[currentnode]
634 # is there any new commit between the expected parent and "."
638 # is there any new commit between the expected parent and "."
635 #
639 #
636 # note: does not take non linear new change in account (but previous
640 # note: does not take non linear new change in account (but previous
637 # implementation didn't used them anyway (issue3655)
641 # implementation didn't used them anyway (issue3655)
638 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
642 newchildren = [c.node() for c in repo.set('(%d::.)', parentctx)]
639 if parentctx.node() != node.nullid:
643 if parentctx.node() != node.nullid:
640 if not newchildren:
644 if not newchildren:
641 # `parentctxnode` should match but no result. This means that
645 # `parentctxnode` should match but no result. This means that
642 # currentnode is not a descendant from parentctxnode.
646 # currentnode is not a descendant from parentctxnode.
643 msg = _('%s is not an ancestor of working directory')
647 msg = _('%s is not an ancestor of working directory')
644 hint = _('use "histedit --abort" to clear broken state')
648 hint = _('use "histedit --abort" to clear broken state')
645 raise util.Abort(msg % parentctx, hint=hint)
649 raise util.Abort(msg % parentctx, hint=hint)
646 newchildren.pop(0) # remove parentctxnode
650 newchildren.pop(0) # remove parentctxnode
647 # Commit dirty working directory if necessary
651 # Commit dirty working directory if necessary
648 new = None
652 new = None
649 m, a, r, d = repo.status()[:4]
653 m, a, r, d = repo.status()[:4]
650 if m or a or r or d:
654 if m or a or r or d:
651 # prepare the message for the commit to comes
655 # prepare the message for the commit to comes
652 if action in ('f', 'fold'):
656 if action in ('f', 'fold'):
653 message = 'fold-temp-revision %s' % currentnode
657 message = 'fold-temp-revision %s' % currentnode
654 else:
658 else:
655 message = ctx.description() + '\n'
659 message = ctx.description() + '\n'
656 if action in ('e', 'edit', 'm', 'mess'):
660 if action in ('e', 'edit', 'm', 'mess'):
657 editor = cmdutil.commitforceeditor
661 editor = cmdutil.commitforceeditor
658 else:
662 else:
659 editor = False
663 editor = False
660 commit = commitfuncfor(repo, ctx)
664 commit = commitfuncfor(repo, ctx)
661 new = commit(text=message, user=ctx.user(),
665 new = commit(text=message, user=ctx.user(),
662 date=ctx.date(), extra=ctx.extra(),
666 date=ctx.date(), extra=ctx.extra(),
663 editor=editor)
667 editor=editor)
664 if new is not None:
668 if new is not None:
665 newchildren.append(new)
669 newchildren.append(new)
666
670
667 replacements = []
671 replacements = []
668 # track replacements
672 # track replacements
669 if ctx.node() not in newchildren:
673 if ctx.node() not in newchildren:
670 # note: new children may be empty when the changeset is dropped.
674 # note: new children may be empty when the changeset is dropped.
671 # this happen e.g during conflicting pick where we revert content
675 # this happen e.g during conflicting pick where we revert content
672 # to parent.
676 # to parent.
673 replacements.append((ctx.node(), tuple(newchildren)))
677 replacements.append((ctx.node(), tuple(newchildren)))
674
678
675 if action in ('f', 'fold'):
679 if action in ('f', 'fold'):
676 if newchildren:
680 if newchildren:
677 # finalize fold operation if applicable
681 # finalize fold operation if applicable
678 if new is None:
682 if new is None:
679 new = newchildren[-1]
683 new = newchildren[-1]
680 else:
684 else:
681 newchildren.pop() # remove new from internal changes
685 newchildren.pop() # remove new from internal changes
682 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
686 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new, opts,
683 newchildren)
687 newchildren)
684 replacements.extend(repl)
688 replacements.extend(repl)
685 else:
689 else:
686 # newchildren is empty if the fold did not result in any commit
690 # newchildren is empty if the fold did not result in any commit
687 # this happen when all folded change are discarded during the
691 # this happen when all folded change are discarded during the
688 # merge.
692 # merge.
689 replacements.append((ctx.node(), (parentctx.node(),)))
693 replacements.append((ctx.node(), (parentctx.node(),)))
690 elif newchildren:
694 elif newchildren:
691 # otherwise update "parentctx" before proceeding to further operation
695 # otherwise update "parentctx" before proceeding to further operation
692 parentctx = repo[newchildren[-1]]
696 parentctx = repo[newchildren[-1]]
693 return parentctx, replacements
697 return parentctx, replacements
694
698
695
699
696 def between(repo, old, new, keep):
700 def between(repo, old, new, keep):
697 """select and validate the set of revision to edit
701 """select and validate the set of revision to edit
698
702
699 When keep is false, the specified set can't have children."""
703 When keep is false, the specified set can't have children."""
700 ctxs = list(repo.set('%n::%n', old, new))
704 ctxs = list(repo.set('%n::%n', old, new))
701 if ctxs and not keep:
705 if ctxs and not keep:
702 if (not obsolete._enabled and
706 if (not obsolete._enabled and
703 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
707 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
704 raise util.Abort(_('cannot edit history that would orphan nodes'))
708 raise util.Abort(_('cannot edit history that would orphan nodes'))
705 if repo.revs('(%ld) and merge()', ctxs):
709 if repo.revs('(%ld) and merge()', ctxs):
706 raise util.Abort(_('cannot edit history that contains merges'))
710 raise util.Abort(_('cannot edit history that contains merges'))
707 root = ctxs[0] # list is already sorted by repo.set
711 root = ctxs[0] # list is already sorted by repo.set
708 if not root.phase():
712 if not root.phase():
709 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
713 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
710 return [c.node() for c in ctxs]
714 return [c.node() for c in ctxs]
711
715
712
716
713 def writestate(repo, parentnode, rules, keep, topmost, replacements):
717 def writestate(repo, parentnode, rules, keep, topmost, replacements):
714 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
718 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
715 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
719 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
716 fp.close()
720 fp.close()
717
721
718 def readstate(repo):
722 def readstate(repo):
719 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
723 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
720 """
724 """
721 fp = open(os.path.join(repo.path, 'histedit-state'))
725 fp = open(os.path.join(repo.path, 'histedit-state'))
722 return pickle.load(fp)
726 return pickle.load(fp)
723
727
724
728
725 def makedesc(c):
729 def makedesc(c):
726 """build a initial action line for a ctx `c`
730 """build a initial action line for a ctx `c`
727
731
728 line are in the form:
732 line are in the form:
729
733
730 pick <hash> <rev> <summary>
734 pick <hash> <rev> <summary>
731 """
735 """
732 summary = ''
736 summary = ''
733 if c.description():
737 if c.description():
734 summary = c.description().splitlines()[0]
738 summary = c.description().splitlines()[0]
735 line = 'pick %s %d %s' % (c, c.rev(), summary)
739 line = 'pick %s %d %s' % (c, c.rev(), summary)
736 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
740 return line[:80] # trim to 80 chars so it's not stupidly wide in my editor
737
741
738 def verifyrules(rules, repo, ctxs):
742 def verifyrules(rules, repo, ctxs):
739 """Verify that there exists exactly one edit rule per given changeset.
743 """Verify that there exists exactly one edit rule per given changeset.
740
744
741 Will abort if there are to many or too few rules, a malformed rule,
745 Will abort if there are to many or too few rules, a malformed rule,
742 or a rule on a changeset outside of the user-given range.
746 or a rule on a changeset outside of the user-given range.
743 """
747 """
744 parsed = []
748 parsed = []
745 expected = set(str(c) for c in ctxs)
749 expected = set(str(c) for c in ctxs)
746 seen = set()
750 seen = set()
747 for r in rules:
751 for r in rules:
748 if ' ' not in r:
752 if ' ' not in r:
749 raise util.Abort(_('malformed line "%s"') % r)
753 raise util.Abort(_('malformed line "%s"') % r)
750 action, rest = r.split(' ', 1)
754 action, rest = r.split(' ', 1)
751 ha = rest.strip().split(' ', 1)[0]
755 ha = rest.strip().split(' ', 1)[0]
752 try:
756 try:
753 ha = str(repo[ha]) # ensure its a short hash
757 ha = str(repo[ha]) # ensure its a short hash
754 except error.RepoError:
758 except error.RepoError:
755 raise util.Abort(_('unknown changeset %s listed') % ha)
759 raise util.Abort(_('unknown changeset %s listed') % ha)
756 if ha not in expected:
760 if ha not in expected:
757 raise util.Abort(
761 raise util.Abort(
758 _('may not use changesets other than the ones listed'))
762 _('may not use changesets other than the ones listed'))
759 if ha in seen:
763 if ha in seen:
760 raise util.Abort(_('duplicated command for changeset %s') % ha)
764 raise util.Abort(_('duplicated command for changeset %s') % ha)
761 seen.add(ha)
765 seen.add(ha)
762 if action not in actiontable:
766 if action not in actiontable:
763 raise util.Abort(_('unknown action "%s"') % action)
767 raise util.Abort(_('unknown action "%s"') % action)
764 parsed.append([action, ha])
768 parsed.append([action, ha])
765 missing = sorted(expected - seen) # sort to stabilize output
769 missing = sorted(expected - seen) # sort to stabilize output
766 if missing:
770 if missing:
767 raise util.Abort(_('missing rules for changeset %s') % missing[0],
771 raise util.Abort(_('missing rules for changeset %s') % missing[0],
768 hint=_('do you want to use the drop action?'))
772 hint=_('do you want to use the drop action?'))
769 return parsed
773 return parsed
770
774
771 def processreplacement(repo, replacements):
775 def processreplacement(repo, replacements):
772 """process the list of replacements to return
776 """process the list of replacements to return
773
777
774 1) the final mapping between original and created nodes
778 1) the final mapping between original and created nodes
775 2) the list of temporary node created by histedit
779 2) the list of temporary node created by histedit
776 3) the list of new commit created by histedit"""
780 3) the list of new commit created by histedit"""
777 allsuccs = set()
781 allsuccs = set()
778 replaced = set()
782 replaced = set()
779 fullmapping = {}
783 fullmapping = {}
780 # initialise basic set
784 # initialise basic set
781 # fullmapping record all operation recorded in replacement
785 # fullmapping record all operation recorded in replacement
782 for rep in replacements:
786 for rep in replacements:
783 allsuccs.update(rep[1])
787 allsuccs.update(rep[1])
784 replaced.add(rep[0])
788 replaced.add(rep[0])
785 fullmapping.setdefault(rep[0], set()).update(rep[1])
789 fullmapping.setdefault(rep[0], set()).update(rep[1])
786 new = allsuccs - replaced
790 new = allsuccs - replaced
787 tmpnodes = allsuccs & replaced
791 tmpnodes = allsuccs & replaced
788 # Reduce content fullmapping into direct relation between original nodes
792 # Reduce content fullmapping into direct relation between original nodes
789 # and final node created during history edition
793 # and final node created during history edition
790 # Dropped changeset are replaced by an empty list
794 # Dropped changeset are replaced by an empty list
791 toproceed = set(fullmapping)
795 toproceed = set(fullmapping)
792 final = {}
796 final = {}
793 while toproceed:
797 while toproceed:
794 for x in list(toproceed):
798 for x in list(toproceed):
795 succs = fullmapping[x]
799 succs = fullmapping[x]
796 for s in list(succs):
800 for s in list(succs):
797 if s in toproceed:
801 if s in toproceed:
798 # non final node with unknown closure
802 # non final node with unknown closure
799 # We can't process this now
803 # We can't process this now
800 break
804 break
801 elif s in final:
805 elif s in final:
802 # non final node, replace with closure
806 # non final node, replace with closure
803 succs.remove(s)
807 succs.remove(s)
804 succs.update(final[s])
808 succs.update(final[s])
805 else:
809 else:
806 final[x] = succs
810 final[x] = succs
807 toproceed.remove(x)
811 toproceed.remove(x)
808 # remove tmpnodes from final mapping
812 # remove tmpnodes from final mapping
809 for n in tmpnodes:
813 for n in tmpnodes:
810 del final[n]
814 del final[n]
811 # we expect all changes involved in final to exist in the repo
815 # we expect all changes involved in final to exist in the repo
812 # turn `final` into list (topologically sorted)
816 # turn `final` into list (topologically sorted)
813 nm = repo.changelog.nodemap
817 nm = repo.changelog.nodemap
814 for prec, succs in final.items():
818 for prec, succs in final.items():
815 final[prec] = sorted(succs, key=nm.get)
819 final[prec] = sorted(succs, key=nm.get)
816
820
817 # computed topmost element (necessary for bookmark)
821 # computed topmost element (necessary for bookmark)
818 if new:
822 if new:
819 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
823 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
820 elif not final:
824 elif not final:
821 # Nothing rewritten at all. we won't need `newtopmost`
825 # Nothing rewritten at all. we won't need `newtopmost`
822 # It is the same as `oldtopmost` and `processreplacement` know it
826 # It is the same as `oldtopmost` and `processreplacement` know it
823 newtopmost = None
827 newtopmost = None
824 else:
828 else:
825 # every body died. The newtopmost is the parent of the root.
829 # every body died. The newtopmost is the parent of the root.
826 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
830 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
827
831
828 return final, tmpnodes, new, newtopmost
832 return final, tmpnodes, new, newtopmost
829
833
830 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
834 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
831 """Move bookmark from old to newly created node"""
835 """Move bookmark from old to newly created node"""
832 if not mapping:
836 if not mapping:
833 # if nothing got rewritten there is not purpose for this function
837 # if nothing got rewritten there is not purpose for this function
834 return
838 return
835 moves = []
839 moves = []
836 for bk, old in sorted(repo._bookmarks.iteritems()):
840 for bk, old in sorted(repo._bookmarks.iteritems()):
837 if old == oldtopmost:
841 if old == oldtopmost:
838 # special case ensure bookmark stay on tip.
842 # special case ensure bookmark stay on tip.
839 #
843 #
840 # This is arguably a feature and we may only want that for the
844 # This is arguably a feature and we may only want that for the
841 # active bookmark. But the behavior is kept compatible with the old
845 # active bookmark. But the behavior is kept compatible with the old
842 # version for now.
846 # version for now.
843 moves.append((bk, newtopmost))
847 moves.append((bk, newtopmost))
844 continue
848 continue
845 base = old
849 base = old
846 new = mapping.get(base, None)
850 new = mapping.get(base, None)
847 if new is None:
851 if new is None:
848 continue
852 continue
849 while not new:
853 while not new:
850 # base is killed, trying with parent
854 # base is killed, trying with parent
851 base = repo[base].p1().node()
855 base = repo[base].p1().node()
852 new = mapping.get(base, (base,))
856 new = mapping.get(base, (base,))
853 # nothing to move
857 # nothing to move
854 moves.append((bk, new[-1]))
858 moves.append((bk, new[-1]))
855 if moves:
859 if moves:
856 marks = repo._bookmarks
860 marks = repo._bookmarks
857 for mark, new in moves:
861 for mark, new in moves:
858 old = marks[mark]
862 old = marks[mark]
859 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
863 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
860 % (mark, node.short(old), node.short(new)))
864 % (mark, node.short(old), node.short(new)))
861 marks[mark] = new
865 marks[mark] = new
862 marks.write()
866 marks.write()
863
867
864 def cleanupnode(ui, repo, name, nodes):
868 def cleanupnode(ui, repo, name, nodes):
865 """strip a group of nodes from the repository
869 """strip a group of nodes from the repository
866
870
867 The set of node to strip may contains unknown nodes."""
871 The set of node to strip may contains unknown nodes."""
868 ui.debug('should strip %s nodes %s\n' %
872 ui.debug('should strip %s nodes %s\n' %
869 (name, ', '.join([node.short(n) for n in nodes])))
873 (name, ', '.join([node.short(n) for n in nodes])))
870 lock = None
874 lock = None
871 try:
875 try:
872 lock = repo.lock()
876 lock = repo.lock()
873 # Find all node that need to be stripped
877 # Find all node that need to be stripped
874 # (we hg %lr instead of %ln to silently ignore unknown item
878 # (we hg %lr instead of %ln to silently ignore unknown item
875 nm = repo.changelog.nodemap
879 nm = repo.changelog.nodemap
876 nodes = [n for n in nodes if n in nm]
880 nodes = [n for n in nodes if n in nm]
877 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
881 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
878 for c in roots:
882 for c in roots:
879 # We should process node in reverse order to strip tip most first.
883 # We should process node in reverse order to strip tip most first.
880 # but this trigger a bug in changegroup hook.
884 # but this trigger a bug in changegroup hook.
881 # This would reduce bundle overhead
885 # This would reduce bundle overhead
882 repair.strip(ui, repo, c)
886 repair.strip(ui, repo, c)
883 finally:
887 finally:
884 lockmod.release(lock)
888 lockmod.release(lock)
885
889
886 def summaryhook(ui, repo):
890 def summaryhook(ui, repo):
887 if not os.path.exists(repo.join('histedit-state')):
891 if not os.path.exists(repo.join('histedit-state')):
888 return
892 return
889 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
893 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
890 if rules:
894 if rules:
891 # i18n: column positioning for "hg summary"
895 # i18n: column positioning for "hg summary"
892 ui.write(_('hist: %s (histedit --continue)\n') %
896 ui.write(_('hist: %s (histedit --continue)\n') %
893 (ui.label(_('%d remaining'), 'histedit.remaining') %
897 (ui.label(_('%d remaining'), 'histedit.remaining') %
894 len(rules)))
898 len(rules)))
895
899
896 def extsetup(ui):
900 def extsetup(ui):
897 cmdutil.summaryhooks.add('histedit', summaryhook)
901 cmdutil.summaryhooks.add('histedit', summaryhook)
898 cmdutil.unfinishedstates.append(
902 cmdutil.unfinishedstates.append(
899 ['histedit-state', False, True, _('histedit in progress'),
903 ['histedit-state', False, True, _('histedit in progress'),
900 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
904 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
General Comments 0
You need to be logged in to leave comments. Login now