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