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