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