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