##// END OF EJS Templates
histedit: mention histedit-last-edit.txt on abort...
timeless -
r27171:3028ea0a default
parent child Browse files
Show More
@@ -1,1367 +1,1374 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 commit message without changing commit content
41 # m, mess = edit commit 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 commit message without changing commit content
63 # m, mess = edit commit message without changing commit content
64 #
64 #
65
65
66 At which point you close the editor and ``histedit`` starts working. When you
66 At which point you close the editor and ``histedit`` starts working. When you
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 those revisions together, offering you a chance to clean up the commit message::
68 those revisions together, offering you a chance to clean up the commit message::
69
69
70 Add beta
70 Add beta
71 ***
71 ***
72 Add delta
72 Add delta
73
73
74 Edit the commit message to your liking, then close the editor. For
74 Edit the commit message to your liking, then close the editor. For
75 this example, let's assume that the commit message was changed to
75 this example, let's assume that the commit message was changed to
76 ``Add beta and delta.`` After histedit has run and had a chance to
76 ``Add beta and delta.`` After histedit has run and had a chance to
77 remove any old or temporary revisions it needed, the history looks
77 remove any old or temporary revisions it needed, the history looks
78 like this::
78 like this::
79
79
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 | Add beta and delta.
81 | Add beta and delta.
82 |
82 |
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 | Add gamma
84 | Add gamma
85 |
85 |
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 Add alpha
87 Add alpha
88
88
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 ones) until after it has completed all the editing operations, so it will
90 ones) until after it has completed all the editing operations, so it will
91 probably perform several strip operations when it's done. For the above example,
91 probably perform several strip operations when it's done. For the above example,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 so you might need to be a little patient. You can choose to keep the original
93 so you might need to be a little patient. You can choose to keep the original
94 revisions by passing the ``--keep`` flag.
94 revisions by passing the ``--keep`` flag.
95
95
96 The ``edit`` operation will drop you back to a command prompt,
96 The ``edit`` operation will drop you back to a command prompt,
97 allowing you to edit files freely, or even use ``hg record`` to commit
97 allowing you to edit files freely, or even use ``hg record`` to commit
98 some changes as a separate commit. When you're done, any remaining
98 some changes as a separate commit. When you're done, any remaining
99 uncommitted changes will be committed as well. When done, run ``hg
99 uncommitted changes will be committed as well. When done, run ``hg
100 histedit --continue`` to finish this step. You'll be prompted for a
100 histedit --continue`` to finish this step. You'll be prompted for a
101 new commit message, but the default commit message will be the
101 new commit message, but the default commit message will be the
102 original message for the ``edit`` ed revision.
102 original message for the ``edit`` ed revision.
103
103
104 The ``message`` operation will give you a chance to revise a commit
104 The ``message`` operation will give you a chance to revise a commit
105 message without changing the contents. It's a shortcut for doing
105 message without changing the contents. It's a shortcut for doing
106 ``edit`` immediately followed by `hg histedit --continue``.
106 ``edit`` immediately followed by `hg histedit --continue``.
107
107
108 If ``histedit`` encounters a conflict when moving a revision (while
108 If ``histedit`` encounters a conflict when moving a revision (while
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 ``edit`` with the difference that it won't prompt you for a commit
110 ``edit`` with the difference that it won't prompt you for a commit
111 message when done. If you decide at this point that you don't like how
111 message when done. If you decide at this point that you don't like how
112 much work it will be to rearrange history, or that you made a mistake,
112 much work it will be to rearrange history, or that you made a mistake,
113 you can use ``hg histedit --abort`` to abandon the new changes you
113 you can use ``hg histedit --abort`` to abandon the new changes you
114 have made and return to the state before you attempted to edit your
114 have made and return to the state before you attempted to edit your
115 history.
115 history.
116
116
117 If we clone the histedit-ed example repository above and add four more
117 If we clone the histedit-ed example repository above and add four more
118 changes, such that we have the following history::
118 changes, such that we have the following history::
119
119
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 | Add theta
121 | Add theta
122 |
122 |
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 | Add eta
124 | Add eta
125 |
125 |
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 | Add zeta
127 | Add zeta
128 |
128 |
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 | Add epsilon
130 | Add epsilon
131 |
131 |
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 | Add beta and delta.
133 | Add beta and delta.
134 |
134 |
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 | Add gamma
136 | Add gamma
137 |
137 |
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 Add alpha
139 Add alpha
140
140
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 as running ``hg histedit 836302820282``. If you need plan to push to a
142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 repository that Mercurial does not detect to be related to the source
143 repository that Mercurial does not detect to be related to the source
144 repo, you can add a ``--force`` option.
144 repo, you can add a ``--force`` option.
145
145
146 Histedit rule lines are truncated to 80 characters by default. You
146 Histedit rule lines are truncated to 80 characters by default. You
147 can customize this behavior by setting a different length in your
147 can customize this behavior by setting a different length in your
148 configuration file::
148 configuration file::
149
149
150 [histedit]
150 [histedit]
151 linelen = 120 # truncate rule lines at 120 characters
151 linelen = 120 # truncate rule lines at 120 characters
152 """
152 """
153
153
154 try:
154 try:
155 import cPickle as pickle
155 import cPickle as pickle
156 pickle.dump # import now
156 pickle.dump # import now
157 except ImportError:
157 except ImportError:
158 import pickle
158 import pickle
159 import errno
159 import errno
160 import os
160 import os
161 import sys
161 import sys
162
162
163 from mercurial import bundle2
163 from mercurial import bundle2
164 from mercurial import cmdutil
164 from mercurial import cmdutil
165 from mercurial import discovery
165 from mercurial import discovery
166 from mercurial import error
166 from mercurial import error
167 from mercurial import copies
167 from mercurial import copies
168 from mercurial import context
168 from mercurial import context
169 from mercurial import exchange
169 from mercurial import exchange
170 from mercurial import extensions
170 from mercurial import extensions
171 from mercurial import hg
171 from mercurial import hg
172 from mercurial import node
172 from mercurial import node
173 from mercurial import repair
173 from mercurial import repair
174 from mercurial import scmutil
174 from mercurial import scmutil
175 from mercurial import util
175 from mercurial import util
176 from mercurial import obsolete
176 from mercurial import obsolete
177 from mercurial import merge as mergemod
177 from mercurial import merge as mergemod
178 from mercurial.lock import release
178 from mercurial.lock import release
179 from mercurial.i18n import _
179 from mercurial.i18n import _
180
180
181 cmdtable = {}
181 cmdtable = {}
182 command = cmdutil.command(cmdtable)
182 command = cmdutil.command(cmdtable)
183
183
184 class _constraints(object):
184 class _constraints(object):
185 # aborts if there are multiple rules for one node
185 # aborts if there are multiple rules for one node
186 noduplicates = 'noduplicates'
186 noduplicates = 'noduplicates'
187 # abort if the node does belong to edited stack
187 # abort if the node does belong to edited stack
188 forceother = 'forceother'
188 forceother = 'forceother'
189 # abort if the node doesn't belong to edited stack
189 # abort if the node doesn't belong to edited stack
190 noother = 'noother'
190 noother = 'noother'
191
191
192 @classmethod
192 @classmethod
193 def known(cls):
193 def known(cls):
194 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
194 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
195
195
196 # Note for extension authors: ONLY specify testedwith = 'internal' for
196 # Note for extension authors: ONLY specify testedwith = 'internal' for
197 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
197 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
198 # be specifying the version(s) of Mercurial they are tested with, or
198 # be specifying the version(s) of Mercurial they are tested with, or
199 # leave the attribute unspecified.
199 # leave the attribute unspecified.
200 testedwith = 'internal'
200 testedwith = 'internal'
201
201
202 # i18n: command names and abbreviations must remain untranslated
202 # i18n: command names and abbreviations must remain untranslated
203 editcomment = _("""# Edit history between %s and %s
203 editcomment = _("""# Edit history between %s and %s
204 #
204 #
205 # Commits are listed from least to most recent
205 # Commits are listed from least to most recent
206 #
206 #
207 # Commands:
207 # Commands:
208 # p, pick = use commit
208 # p, pick = use commit
209 # e, edit = use commit, but stop for amending
209 # e, edit = use commit, but stop for amending
210 # f, fold = use commit, but combine it with the one above
210 # f, fold = use commit, but combine it with the one above
211 # r, roll = like fold, but discard this commit's description
211 # r, roll = like fold, but discard this commit's description
212 # d, drop = remove commit from history
212 # d, drop = remove commit from history
213 # m, mess = edit commit message without changing commit content
213 # m, mess = edit commit message without changing commit content
214 #
214 #
215 """)
215 """)
216
216
217 class histeditstate(object):
217 class histeditstate(object):
218 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
218 def __init__(self, repo, parentctxnode=None, rules=None, keep=None,
219 topmost=None, replacements=None, lock=None, wlock=None):
219 topmost=None, replacements=None, lock=None, wlock=None):
220 self.repo = repo
220 self.repo = repo
221 self.rules = rules
221 self.rules = rules
222 self.keep = keep
222 self.keep = keep
223 self.topmost = topmost
223 self.topmost = topmost
224 self.parentctxnode = parentctxnode
224 self.parentctxnode = parentctxnode
225 self.lock = lock
225 self.lock = lock
226 self.wlock = wlock
226 self.wlock = wlock
227 self.backupfile = None
227 self.backupfile = None
228 if replacements is None:
228 if replacements is None:
229 self.replacements = []
229 self.replacements = []
230 else:
230 else:
231 self.replacements = replacements
231 self.replacements = replacements
232
232
233 def read(self):
233 def read(self):
234 """Load histedit state from disk and set fields appropriately."""
234 """Load histedit state from disk and set fields appropriately."""
235 try:
235 try:
236 fp = self.repo.vfs('histedit-state', 'r')
236 fp = self.repo.vfs('histedit-state', 'r')
237 except IOError as err:
237 except IOError as err:
238 if err.errno != errno.ENOENT:
238 if err.errno != errno.ENOENT:
239 raise
239 raise
240 raise error.Abort(_('no histedit in progress'))
240 raise error.Abort(_('no histedit in progress'))
241
241
242 try:
242 try:
243 data = pickle.load(fp)
243 data = pickle.load(fp)
244 parentctxnode, rules, keep, topmost, replacements = data
244 parentctxnode, rules, keep, topmost, replacements = data
245 backupfile = None
245 backupfile = None
246 except pickle.UnpicklingError:
246 except pickle.UnpicklingError:
247 data = self._load()
247 data = self._load()
248 parentctxnode, rules, keep, topmost, replacements, backupfile = data
248 parentctxnode, rules, keep, topmost, replacements, backupfile = data
249
249
250 self.parentctxnode = parentctxnode
250 self.parentctxnode = parentctxnode
251 self.rules = rules
251 self.rules = rules
252 self.keep = keep
252 self.keep = keep
253 self.topmost = topmost
253 self.topmost = topmost
254 self.replacements = replacements
254 self.replacements = replacements
255 self.backupfile = backupfile
255 self.backupfile = backupfile
256
256
257 def write(self):
257 def write(self):
258 fp = self.repo.vfs('histedit-state', 'w')
258 fp = self.repo.vfs('histedit-state', 'w')
259 fp.write('v1\n')
259 fp.write('v1\n')
260 fp.write('%s\n' % node.hex(self.parentctxnode))
260 fp.write('%s\n' % node.hex(self.parentctxnode))
261 fp.write('%s\n' % node.hex(self.topmost))
261 fp.write('%s\n' % node.hex(self.topmost))
262 fp.write('%s\n' % self.keep)
262 fp.write('%s\n' % self.keep)
263 fp.write('%d\n' % len(self.rules))
263 fp.write('%d\n' % len(self.rules))
264 for rule in self.rules:
264 for rule in self.rules:
265 fp.write('%s\n' % rule[0]) # action
265 fp.write('%s\n' % rule[0]) # action
266 fp.write('%s\n' % rule[1]) # remainder
266 fp.write('%s\n' % rule[1]) # remainder
267 fp.write('%d\n' % len(self.replacements))
267 fp.write('%d\n' % len(self.replacements))
268 for replacement in self.replacements:
268 for replacement in self.replacements:
269 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
269 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
270 for r in replacement[1])))
270 for r in replacement[1])))
271 backupfile = self.backupfile
271 backupfile = self.backupfile
272 if not backupfile:
272 if not backupfile:
273 backupfile = ''
273 backupfile = ''
274 fp.write('%s\n' % backupfile)
274 fp.write('%s\n' % backupfile)
275 fp.close()
275 fp.close()
276
276
277 def _load(self):
277 def _load(self):
278 fp = self.repo.vfs('histedit-state', 'r')
278 fp = self.repo.vfs('histedit-state', 'r')
279 lines = [l[:-1] for l in fp.readlines()]
279 lines = [l[:-1] for l in fp.readlines()]
280
280
281 index = 0
281 index = 0
282 lines[index] # version number
282 lines[index] # version number
283 index += 1
283 index += 1
284
284
285 parentctxnode = node.bin(lines[index])
285 parentctxnode = node.bin(lines[index])
286 index += 1
286 index += 1
287
287
288 topmost = node.bin(lines[index])
288 topmost = node.bin(lines[index])
289 index += 1
289 index += 1
290
290
291 keep = lines[index] == 'True'
291 keep = lines[index] == 'True'
292 index += 1
292 index += 1
293
293
294 # Rules
294 # Rules
295 rules = []
295 rules = []
296 rulelen = int(lines[index])
296 rulelen = int(lines[index])
297 index += 1
297 index += 1
298 for i in xrange(rulelen):
298 for i in xrange(rulelen):
299 ruleaction = lines[index]
299 ruleaction = lines[index]
300 index += 1
300 index += 1
301 rule = lines[index]
301 rule = lines[index]
302 index += 1
302 index += 1
303 rules.append((ruleaction, rule))
303 rules.append((ruleaction, rule))
304
304
305 # Replacements
305 # Replacements
306 replacements = []
306 replacements = []
307 replacementlen = int(lines[index])
307 replacementlen = int(lines[index])
308 index += 1
308 index += 1
309 for i in xrange(replacementlen):
309 for i in xrange(replacementlen):
310 replacement = lines[index]
310 replacement = lines[index]
311 original = node.bin(replacement[:40])
311 original = node.bin(replacement[:40])
312 succ = [node.bin(replacement[i:i + 40]) for i in
312 succ = [node.bin(replacement[i:i + 40]) for i in
313 range(40, len(replacement), 40)]
313 range(40, len(replacement), 40)]
314 replacements.append((original, succ))
314 replacements.append((original, succ))
315 index += 1
315 index += 1
316
316
317 backupfile = lines[index]
317 backupfile = lines[index]
318 index += 1
318 index += 1
319
319
320 fp.close()
320 fp.close()
321
321
322 return parentctxnode, rules, keep, topmost, replacements, backupfile
322 return parentctxnode, rules, keep, topmost, replacements, backupfile
323
323
324 def clear(self):
324 def clear(self):
325 if self.inprogress():
325 if self.inprogress():
326 self.repo.vfs.unlink('histedit-state')
326 self.repo.vfs.unlink('histedit-state')
327
327
328 def inprogress(self):
328 def inprogress(self):
329 return self.repo.vfs.exists('histedit-state')
329 return self.repo.vfs.exists('histedit-state')
330
330
331 class histeditaction(object):
331 class histeditaction(object):
332 def __init__(self, state, node):
332 def __init__(self, state, node):
333 self.state = state
333 self.state = state
334 self.repo = state.repo
334 self.repo = state.repo
335 self.node = node
335 self.node = node
336
336
337 @classmethod
337 @classmethod
338 def fromrule(cls, state, rule):
338 def fromrule(cls, state, rule):
339 """Parses the given rule, returning an instance of the histeditaction.
339 """Parses the given rule, returning an instance of the histeditaction.
340 """
340 """
341 repo = state.repo
341 repo = state.repo
342 rulehash = rule.strip().split(' ', 1)[0]
342 rulehash = rule.strip().split(' ', 1)[0]
343 try:
343 try:
344 node = repo[rulehash].node()
344 node = repo[rulehash].node()
345 except error.RepoError:
345 except error.RepoError:
346 raise error.Abort(_('unknown changeset %s listed') % rulehash[:12])
346 raise error.Abort(_('unknown changeset %s listed') % rulehash[:12])
347 return cls(state, node)
347 return cls(state, node)
348
348
349 def constraints(self):
349 def constraints(self):
350 """Return a set of constrains that this action should be verified for
350 """Return a set of constrains that this action should be verified for
351 """
351 """
352 return set([_constraints.noduplicates, _constraints.noother])
352 return set([_constraints.noduplicates, _constraints.noother])
353
353
354 def nodetoverify(self):
354 def nodetoverify(self):
355 """Returns a node associated with the action that will be used for
355 """Returns a node associated with the action that will be used for
356 verification purposes.
356 verification purposes.
357
357
358 If the action doesn't correspond to node it should return None
358 If the action doesn't correspond to node it should return None
359 """
359 """
360 return self.node
360 return self.node
361
361
362 def run(self):
362 def run(self):
363 """Runs the action. The default behavior is simply apply the action's
363 """Runs the action. The default behavior is simply apply the action's
364 rulectx onto the current parentctx."""
364 rulectx onto the current parentctx."""
365 self.applychange()
365 self.applychange()
366 self.continuedirty()
366 self.continuedirty()
367 return self.continueclean()
367 return self.continueclean()
368
368
369 def applychange(self):
369 def applychange(self):
370 """Applies the changes from this action's rulectx onto the current
370 """Applies the changes from this action's rulectx onto the current
371 parentctx, but does not commit them."""
371 parentctx, but does not commit them."""
372 repo = self.repo
372 repo = self.repo
373 rulectx = repo[self.node]
373 rulectx = repo[self.node]
374 hg.update(repo, self.state.parentctxnode)
374 hg.update(repo, self.state.parentctxnode)
375 stats = applychanges(repo.ui, repo, rulectx, {})
375 stats = applychanges(repo.ui, repo, rulectx, {})
376 if stats and stats[3] > 0:
376 if stats and stats[3] > 0:
377 raise error.InterventionRequired(_('Fix up the change and run '
377 raise error.InterventionRequired(_('Fix up the change and run '
378 'hg histedit --continue'))
378 'hg histedit --continue'))
379
379
380 def continuedirty(self):
380 def continuedirty(self):
381 """Continues the action when changes have been applied to the working
381 """Continues the action when changes have been applied to the working
382 copy. The default behavior is to commit the dirty changes."""
382 copy. The default behavior is to commit the dirty changes."""
383 repo = self.repo
383 repo = self.repo
384 rulectx = repo[self.node]
384 rulectx = repo[self.node]
385
385
386 editor = self.commiteditor()
386 editor = self.commiteditor()
387 commit = commitfuncfor(repo, rulectx)
387 commit = commitfuncfor(repo, rulectx)
388
388
389 commit(text=rulectx.description(), user=rulectx.user(),
389 commit(text=rulectx.description(), user=rulectx.user(),
390 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
390 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
391
391
392 def commiteditor(self):
392 def commiteditor(self):
393 """The editor to be used to edit the commit message."""
393 """The editor to be used to edit the commit message."""
394 return False
394 return False
395
395
396 def continueclean(self):
396 def continueclean(self):
397 """Continues the action when the working copy is clean. The default
397 """Continues the action when the working copy is clean. The default
398 behavior is to accept the current commit as the new version of the
398 behavior is to accept the current commit as the new version of the
399 rulectx."""
399 rulectx."""
400 ctx = self.repo['.']
400 ctx = self.repo['.']
401 if ctx.node() == self.state.parentctxnode:
401 if ctx.node() == self.state.parentctxnode:
402 self.repo.ui.warn(_('%s: empty changeset\n') %
402 self.repo.ui.warn(_('%s: empty changeset\n') %
403 node.short(self.node))
403 node.short(self.node))
404 return ctx, [(self.node, tuple())]
404 return ctx, [(self.node, tuple())]
405 if ctx.node() == self.node:
405 if ctx.node() == self.node:
406 # Nothing changed
406 # Nothing changed
407 return ctx, []
407 return ctx, []
408 return ctx, [(self.node, (ctx.node(),))]
408 return ctx, [(self.node, (ctx.node(),))]
409
409
410 def commitfuncfor(repo, src):
410 def commitfuncfor(repo, src):
411 """Build a commit function for the replacement of <src>
411 """Build a commit function for the replacement of <src>
412
412
413 This function ensure we apply the same treatment to all changesets.
413 This function ensure we apply the same treatment to all changesets.
414
414
415 - Add a 'histedit_source' entry in extra.
415 - Add a 'histedit_source' entry in extra.
416
416
417 Note that fold has its own separated logic because its handling is a bit
417 Note that fold has its own separated logic because its handling is a bit
418 different and not easily factored out of the fold method.
418 different and not easily factored out of the fold method.
419 """
419 """
420 phasemin = src.phase()
420 phasemin = src.phase()
421 def commitfunc(**kwargs):
421 def commitfunc(**kwargs):
422 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
422 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
423 try:
423 try:
424 repo.ui.setconfig('phases', 'new-commit', phasemin,
424 repo.ui.setconfig('phases', 'new-commit', phasemin,
425 'histedit')
425 'histedit')
426 extra = kwargs.get('extra', {}).copy()
426 extra = kwargs.get('extra', {}).copy()
427 extra['histedit_source'] = src.hex()
427 extra['histedit_source'] = src.hex()
428 kwargs['extra'] = extra
428 kwargs['extra'] = extra
429 return repo.commit(**kwargs)
429 return repo.commit(**kwargs)
430 finally:
430 finally:
431 repo.ui.restoreconfig(phasebackup)
431 repo.ui.restoreconfig(phasebackup)
432 return commitfunc
432 return commitfunc
433
433
434 def applychanges(ui, repo, ctx, opts):
434 def applychanges(ui, repo, ctx, opts):
435 """Merge changeset from ctx (only) in the current working directory"""
435 """Merge changeset from ctx (only) in the current working directory"""
436 wcpar = repo.dirstate.parents()[0]
436 wcpar = repo.dirstate.parents()[0]
437 if ctx.p1().node() == wcpar:
437 if ctx.p1().node() == wcpar:
438 # edits are "in place" we do not need to make any merge,
438 # edits are "in place" we do not need to make any merge,
439 # just applies changes on parent for edition
439 # just applies changes on parent for edition
440 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
440 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
441 stats = None
441 stats = None
442 else:
442 else:
443 try:
443 try:
444 # ui.forcemerge is an internal variable, do not document
444 # ui.forcemerge is an internal variable, do not document
445 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
445 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
446 'histedit')
446 'histedit')
447 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
447 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
448 finally:
448 finally:
449 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
449 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
450 return stats
450 return stats
451
451
452 def collapse(repo, first, last, commitopts, skipprompt=False):
452 def collapse(repo, first, last, commitopts, skipprompt=False):
453 """collapse the set of revisions from first to last as new one.
453 """collapse the set of revisions from first to last as new one.
454
454
455 Expected commit options are:
455 Expected commit options are:
456 - message
456 - message
457 - date
457 - date
458 - username
458 - username
459 Commit message is edited in all cases.
459 Commit message is edited in all cases.
460
460
461 This function works in memory."""
461 This function works in memory."""
462 ctxs = list(repo.set('%d::%d', first, last))
462 ctxs = list(repo.set('%d::%d', first, last))
463 if not ctxs:
463 if not ctxs:
464 return None
464 return None
465 for c in ctxs:
465 for c in ctxs:
466 if not c.mutable():
466 if not c.mutable():
467 raise error.Abort(
467 raise error.Abort(
468 _("cannot fold into public change %s") % node.short(c.node()))
468 _("cannot fold into public change %s") % node.short(c.node()))
469 base = first.parents()[0]
469 base = first.parents()[0]
470
470
471 # commit a new version of the old changeset, including the update
471 # commit a new version of the old changeset, including the update
472 # collect all files which might be affected
472 # collect all files which might be affected
473 files = set()
473 files = set()
474 for ctx in ctxs:
474 for ctx in ctxs:
475 files.update(ctx.files())
475 files.update(ctx.files())
476
476
477 # Recompute copies (avoid recording a -> b -> a)
477 # Recompute copies (avoid recording a -> b -> a)
478 copied = copies.pathcopies(base, last)
478 copied = copies.pathcopies(base, last)
479
479
480 # prune files which were reverted by the updates
480 # prune files which were reverted by the updates
481 def samefile(f):
481 def samefile(f):
482 if f in last.manifest():
482 if f in last.manifest():
483 a = last.filectx(f)
483 a = last.filectx(f)
484 if f in base.manifest():
484 if f in base.manifest():
485 b = base.filectx(f)
485 b = base.filectx(f)
486 return (a.data() == b.data()
486 return (a.data() == b.data()
487 and a.flags() == b.flags())
487 and a.flags() == b.flags())
488 else:
488 else:
489 return False
489 return False
490 else:
490 else:
491 return f not in base.manifest()
491 return f not in base.manifest()
492 files = [f for f in files if not samefile(f)]
492 files = [f for f in files if not samefile(f)]
493 # commit version of these files as defined by head
493 # commit version of these files as defined by head
494 headmf = last.manifest()
494 headmf = last.manifest()
495 def filectxfn(repo, ctx, path):
495 def filectxfn(repo, ctx, path):
496 if path in headmf:
496 if path in headmf:
497 fctx = last[path]
497 fctx = last[path]
498 flags = fctx.flags()
498 flags = fctx.flags()
499 mctx = context.memfilectx(repo,
499 mctx = context.memfilectx(repo,
500 fctx.path(), fctx.data(),
500 fctx.path(), fctx.data(),
501 islink='l' in flags,
501 islink='l' in flags,
502 isexec='x' in flags,
502 isexec='x' in flags,
503 copied=copied.get(path))
503 copied=copied.get(path))
504 return mctx
504 return mctx
505 return None
505 return None
506
506
507 if commitopts.get('message'):
507 if commitopts.get('message'):
508 message = commitopts['message']
508 message = commitopts['message']
509 else:
509 else:
510 message = first.description()
510 message = first.description()
511 user = commitopts.get('user')
511 user = commitopts.get('user')
512 date = commitopts.get('date')
512 date = commitopts.get('date')
513 extra = commitopts.get('extra')
513 extra = commitopts.get('extra')
514
514
515 parents = (first.p1().node(), first.p2().node())
515 parents = (first.p1().node(), first.p2().node())
516 editor = None
516 editor = None
517 if not skipprompt:
517 if not skipprompt:
518 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
518 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
519 new = context.memctx(repo,
519 new = context.memctx(repo,
520 parents=parents,
520 parents=parents,
521 text=message,
521 text=message,
522 files=files,
522 files=files,
523 filectxfn=filectxfn,
523 filectxfn=filectxfn,
524 user=user,
524 user=user,
525 date=date,
525 date=date,
526 extra=extra,
526 extra=extra,
527 editor=editor)
527 editor=editor)
528 return repo.commitctx(new)
528 return repo.commitctx(new)
529
529
530 def _isdirtywc(repo):
530 def _isdirtywc(repo):
531 return repo[None].dirty(missing=True)
531 return repo[None].dirty(missing=True)
532
532
533 def abortdirty():
533 def abortdirty():
534 raise error.Abort(_('working copy has pending changes'),
534 raise error.Abort(_('working copy has pending changes'),
535 hint=_('amend, commit, or revert them and run histedit '
535 hint=_('amend, commit, or revert them and run histedit '
536 '--continue, or abort with histedit --abort'))
536 '--continue, or abort with histedit --abort'))
537
537
538
538
539 class pick(histeditaction):
539 class pick(histeditaction):
540 def run(self):
540 def run(self):
541 rulectx = self.repo[self.node]
541 rulectx = self.repo[self.node]
542 if rulectx.parents()[0].node() == self.state.parentctxnode:
542 if rulectx.parents()[0].node() == self.state.parentctxnode:
543 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
543 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
544 return rulectx, []
544 return rulectx, []
545
545
546 return super(pick, self).run()
546 return super(pick, self).run()
547
547
548 class edit(histeditaction):
548 class edit(histeditaction):
549 def run(self):
549 def run(self):
550 repo = self.repo
550 repo = self.repo
551 rulectx = repo[self.node]
551 rulectx = repo[self.node]
552 hg.update(repo, self.state.parentctxnode)
552 hg.update(repo, self.state.parentctxnode)
553 applychanges(repo.ui, repo, rulectx, {})
553 applychanges(repo.ui, repo, rulectx, {})
554 raise error.InterventionRequired(
554 raise error.InterventionRequired(
555 _('Make changes as needed, you may commit or record as needed '
555 _('Make changes as needed, you may commit or record as needed '
556 'now.\nWhen you are finished, run hg histedit --continue to '
556 'now.\nWhen you are finished, run hg histedit --continue to '
557 'resume.'))
557 'resume.'))
558
558
559 def commiteditor(self):
559 def commiteditor(self):
560 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
560 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
561
561
562 class fold(histeditaction):
562 class fold(histeditaction):
563 def continuedirty(self):
563 def continuedirty(self):
564 repo = self.repo
564 repo = self.repo
565 rulectx = repo[self.node]
565 rulectx = repo[self.node]
566
566
567 commit = commitfuncfor(repo, rulectx)
567 commit = commitfuncfor(repo, rulectx)
568 commit(text='fold-temp-revision %s' % node.short(self.node),
568 commit(text='fold-temp-revision %s' % node.short(self.node),
569 user=rulectx.user(), date=rulectx.date(),
569 user=rulectx.user(), date=rulectx.date(),
570 extra=rulectx.extra())
570 extra=rulectx.extra())
571
571
572 def continueclean(self):
572 def continueclean(self):
573 repo = self.repo
573 repo = self.repo
574 ctx = repo['.']
574 ctx = repo['.']
575 rulectx = repo[self.node]
575 rulectx = repo[self.node]
576 parentctxnode = self.state.parentctxnode
576 parentctxnode = self.state.parentctxnode
577 if ctx.node() == parentctxnode:
577 if ctx.node() == parentctxnode:
578 repo.ui.warn(_('%s: empty changeset\n') %
578 repo.ui.warn(_('%s: empty changeset\n') %
579 node.short(self.node))
579 node.short(self.node))
580 return ctx, [(self.node, (parentctxnode,))]
580 return ctx, [(self.node, (parentctxnode,))]
581
581
582 parentctx = repo[parentctxnode]
582 parentctx = repo[parentctxnode]
583 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
583 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
584 parentctx))
584 parentctx))
585 if not newcommits:
585 if not newcommits:
586 repo.ui.warn(_('%s: cannot fold - working copy is not a '
586 repo.ui.warn(_('%s: cannot fold - working copy is not a '
587 'descendant of previous commit %s\n') %
587 'descendant of previous commit %s\n') %
588 (node.short(self.node), node.short(parentctxnode)))
588 (node.short(self.node), node.short(parentctxnode)))
589 return ctx, [(self.node, (ctx.node(),))]
589 return ctx, [(self.node, (ctx.node(),))]
590
590
591 middlecommits = newcommits.copy()
591 middlecommits = newcommits.copy()
592 middlecommits.discard(ctx.node())
592 middlecommits.discard(ctx.node())
593
593
594 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
594 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
595 middlecommits)
595 middlecommits)
596
596
597 def skipprompt(self):
597 def skipprompt(self):
598 """Returns true if the rule should skip the message editor.
598 """Returns true if the rule should skip the message editor.
599
599
600 For example, 'fold' wants to show an editor, but 'rollup'
600 For example, 'fold' wants to show an editor, but 'rollup'
601 doesn't want to.
601 doesn't want to.
602 """
602 """
603 return False
603 return False
604
604
605 def mergedescs(self):
605 def mergedescs(self):
606 """Returns true if the rule should merge messages of multiple changes.
606 """Returns true if the rule should merge messages of multiple changes.
607
607
608 This exists mainly so that 'rollup' rules can be a subclass of
608 This exists mainly so that 'rollup' rules can be a subclass of
609 'fold'.
609 'fold'.
610 """
610 """
611 return True
611 return True
612
612
613 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
613 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
614 parent = ctx.parents()[0].node()
614 parent = ctx.parents()[0].node()
615 hg.update(repo, parent)
615 hg.update(repo, parent)
616 ### prepare new commit data
616 ### prepare new commit data
617 commitopts = {}
617 commitopts = {}
618 commitopts['user'] = ctx.user()
618 commitopts['user'] = ctx.user()
619 # commit message
619 # commit message
620 if not self.mergedescs():
620 if not self.mergedescs():
621 newmessage = ctx.description()
621 newmessage = ctx.description()
622 else:
622 else:
623 newmessage = '\n***\n'.join(
623 newmessage = '\n***\n'.join(
624 [ctx.description()] +
624 [ctx.description()] +
625 [repo[r].description() for r in internalchanges] +
625 [repo[r].description() for r in internalchanges] +
626 [oldctx.description()]) + '\n'
626 [oldctx.description()]) + '\n'
627 commitopts['message'] = newmessage
627 commitopts['message'] = newmessage
628 # date
628 # date
629 commitopts['date'] = max(ctx.date(), oldctx.date())
629 commitopts['date'] = max(ctx.date(), oldctx.date())
630 extra = ctx.extra().copy()
630 extra = ctx.extra().copy()
631 # histedit_source
631 # histedit_source
632 # note: ctx is likely a temporary commit but that the best we can do
632 # note: ctx is likely a temporary commit but that the best we can do
633 # here. This is sufficient to solve issue3681 anyway.
633 # here. This is sufficient to solve issue3681 anyway.
634 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
634 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
635 commitopts['extra'] = extra
635 commitopts['extra'] = extra
636 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
636 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
637 try:
637 try:
638 phasemin = max(ctx.phase(), oldctx.phase())
638 phasemin = max(ctx.phase(), oldctx.phase())
639 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
639 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
640 n = collapse(repo, ctx, repo[newnode], commitopts,
640 n = collapse(repo, ctx, repo[newnode], commitopts,
641 skipprompt=self.skipprompt())
641 skipprompt=self.skipprompt())
642 finally:
642 finally:
643 repo.ui.restoreconfig(phasebackup)
643 repo.ui.restoreconfig(phasebackup)
644 if n is None:
644 if n is None:
645 return ctx, []
645 return ctx, []
646 hg.update(repo, n)
646 hg.update(repo, n)
647 replacements = [(oldctx.node(), (newnode,)),
647 replacements = [(oldctx.node(), (newnode,)),
648 (ctx.node(), (n,)),
648 (ctx.node(), (n,)),
649 (newnode, (n,)),
649 (newnode, (n,)),
650 ]
650 ]
651 for ich in internalchanges:
651 for ich in internalchanges:
652 replacements.append((ich, (n,)))
652 replacements.append((ich, (n,)))
653 return repo[n], replacements
653 return repo[n], replacements
654
654
655 class base(histeditaction):
655 class base(histeditaction):
656 def constraints(self):
656 def constraints(self):
657 return set([_constraints.forceother])
657 return set([_constraints.forceother])
658
658
659 def run(self):
659 def run(self):
660 if self.repo['.'].node() != self.node:
660 if self.repo['.'].node() != self.node:
661 mergemod.update(self.repo, self.node, False, True, False)
661 mergemod.update(self.repo, self.node, False, True, False)
662 # branchmerge, force, partial)
662 # branchmerge, force, partial)
663 return self.continueclean()
663 return self.continueclean()
664
664
665 def continuedirty(self):
665 def continuedirty(self):
666 abortdirty()
666 abortdirty()
667
667
668 def continueclean(self):
668 def continueclean(self):
669 basectx = self.repo['.']
669 basectx = self.repo['.']
670 return basectx, []
670 return basectx, []
671
671
672 class _multifold(fold):
672 class _multifold(fold):
673 """fold subclass used for when multiple folds happen in a row
673 """fold subclass used for when multiple folds happen in a row
674
674
675 We only want to fire the editor for the folded message once when
675 We only want to fire the editor for the folded message once when
676 (say) four changes are folded down into a single change. This is
676 (say) four changes are folded down into a single change. This is
677 similar to rollup, but we should preserve both messages so that
677 similar to rollup, but we should preserve both messages so that
678 when the last fold operation runs we can show the user all the
678 when the last fold operation runs we can show the user all the
679 commit messages in their editor.
679 commit messages in their editor.
680 """
680 """
681 def skipprompt(self):
681 def skipprompt(self):
682 return True
682 return True
683
683
684 class rollup(fold):
684 class rollup(fold):
685 def mergedescs(self):
685 def mergedescs(self):
686 return False
686 return False
687
687
688 def skipprompt(self):
688 def skipprompt(self):
689 return True
689 return True
690
690
691 class drop(histeditaction):
691 class drop(histeditaction):
692 def run(self):
692 def run(self):
693 parentctx = self.repo[self.state.parentctxnode]
693 parentctx = self.repo[self.state.parentctxnode]
694 return parentctx, [(self.node, tuple())]
694 return parentctx, [(self.node, tuple())]
695
695
696 class message(histeditaction):
696 class message(histeditaction):
697 def commiteditor(self):
697 def commiteditor(self):
698 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
698 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
699
699
700 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
700 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
701 """utility function to find the first outgoing changeset
701 """utility function to find the first outgoing changeset
702
702
703 Used by initialization code"""
703 Used by initialization code"""
704 if opts is None:
704 if opts is None:
705 opts = {}
705 opts = {}
706 dest = ui.expandpath(remote or 'default-push', remote or 'default')
706 dest = ui.expandpath(remote or 'default-push', remote or 'default')
707 dest, revs = hg.parseurl(dest, None)[:2]
707 dest, revs = hg.parseurl(dest, None)[:2]
708 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
708 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
709
709
710 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
710 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
711 other = hg.peer(repo, opts, dest)
711 other = hg.peer(repo, opts, dest)
712
712
713 if revs:
713 if revs:
714 revs = [repo.lookup(rev) for rev in revs]
714 revs = [repo.lookup(rev) for rev in revs]
715
715
716 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
716 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
717 if not outgoing.missing:
717 if not outgoing.missing:
718 raise error.Abort(_('no outgoing ancestors'))
718 raise error.Abort(_('no outgoing ancestors'))
719 roots = list(repo.revs("roots(%ln)", outgoing.missing))
719 roots = list(repo.revs("roots(%ln)", outgoing.missing))
720 if 1 < len(roots):
720 if 1 < len(roots):
721 msg = _('there are ambiguous outgoing revisions')
721 msg = _('there are ambiguous outgoing revisions')
722 hint = _('see "hg help histedit" for more detail')
722 hint = _('see "hg help histedit" for more detail')
723 raise error.Abort(msg, hint=hint)
723 raise error.Abort(msg, hint=hint)
724 return repo.lookup(roots[0])
724 return repo.lookup(roots[0])
725
725
726 actiontable = {'p': pick,
726 actiontable = {'p': pick,
727 'pick': pick,
727 'pick': pick,
728 'e': edit,
728 'e': edit,
729 'edit': edit,
729 'edit': edit,
730 'f': fold,
730 'f': fold,
731 'fold': fold,
731 'fold': fold,
732 '_multifold': _multifold,
732 '_multifold': _multifold,
733 'r': rollup,
733 'r': rollup,
734 'roll': rollup,
734 'roll': rollup,
735 'd': drop,
735 'd': drop,
736 'drop': drop,
736 'drop': drop,
737 'm': message,
737 'm': message,
738 'mess': message,
738 'mess': message,
739 }
739 }
740
740
741 @command('histedit',
741 @command('histedit',
742 [('', 'commands', '',
742 [('', 'commands', '',
743 _('read history edits from the specified file'), _('FILE')),
743 _('read history edits from the specified file'), _('FILE')),
744 ('c', 'continue', False, _('continue an edit already in progress')),
744 ('c', 'continue', False, _('continue an edit already in progress')),
745 ('', 'edit-plan', False, _('edit remaining actions list')),
745 ('', 'edit-plan', False, _('edit remaining actions list')),
746 ('k', 'keep', False,
746 ('k', 'keep', False,
747 _("don't strip old nodes after edit is complete")),
747 _("don't strip old nodes after edit is complete")),
748 ('', 'abort', False, _('abort an edit in progress')),
748 ('', 'abort', False, _('abort an edit in progress')),
749 ('o', 'outgoing', False, _('changesets not found in destination')),
749 ('o', 'outgoing', False, _('changesets not found in destination')),
750 ('f', 'force', False,
750 ('f', 'force', False,
751 _('force outgoing even for unrelated repositories')),
751 _('force outgoing even for unrelated repositories')),
752 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
752 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
753 _("ANCESTOR | --outgoing [URL]"))
753 _("ANCESTOR | --outgoing [URL]"))
754 def histedit(ui, repo, *freeargs, **opts):
754 def histedit(ui, repo, *freeargs, **opts):
755 """interactively edit changeset history
755 """interactively edit changeset history
756
756
757 This command edits changesets between ANCESTOR and the parent of
757 This command edits changesets between ANCESTOR and the parent of
758 the working directory.
758 the working directory.
759
759
760 With --outgoing, this edits changesets not found in the
760 With --outgoing, this edits changesets not found in the
761 destination repository. If URL of the destination is omitted, the
761 destination repository. If URL of the destination is omitted, the
762 'default-push' (or 'default') path will be used.
762 'default-push' (or 'default') path will be used.
763
763
764 For safety, this command is also aborted if there are ambiguous
764 For safety, this command is also aborted if there are ambiguous
765 outgoing revisions which may confuse users: for example, if there
765 outgoing revisions which may confuse users: for example, if there
766 are multiple branches containing outgoing revisions.
766 are multiple branches containing outgoing revisions.
767
767
768 Use "min(outgoing() and ::.)" or similar revset specification
768 Use "min(outgoing() and ::.)" or similar revset specification
769 instead of --outgoing to specify edit target revision exactly in
769 instead of --outgoing to specify edit target revision exactly in
770 such ambiguous situation. See :hg:`help revsets` for detail about
770 such ambiguous situation. See :hg:`help revsets` for detail about
771 selecting revisions.
771 selecting revisions.
772
772
773 .. container:: verbose
773 .. container:: verbose
774
774
775 Examples:
775 Examples:
776
776
777 - A number of changes have been made.
777 - A number of changes have been made.
778 Revision 3 is no longer needed.
778 Revision 3 is no longer needed.
779
779
780 Start history editing from revision 3::
780 Start history editing from revision 3::
781
781
782 hg histedit -r 3
782 hg histedit -r 3
783
783
784 An editor opens, containing the list of revisions,
784 An editor opens, containing the list of revisions,
785 with specific actions specified::
785 with specific actions specified::
786
786
787 pick 5339bf82f0ca 3 Zworgle the foobar
787 pick 5339bf82f0ca 3 Zworgle the foobar
788 pick 8ef592ce7cc4 4 Bedazzle the zerlog
788 pick 8ef592ce7cc4 4 Bedazzle the zerlog
789 pick 0a9639fcda9d 5 Morgify the cromulancy
789 pick 0a9639fcda9d 5 Morgify the cromulancy
790
790
791 Additional information about the possible actions
791 Additional information about the possible actions
792 to take appears below the list of revisions.
792 to take appears below the list of revisions.
793
793
794 To remove revision 3 from the history,
794 To remove revision 3 from the history,
795 its action (at the beginning of the relevant line)
795 its action (at the beginning of the relevant line)
796 is changed to 'drop'::
796 is changed to 'drop'::
797
797
798 drop 5339bf82f0ca 3 Zworgle the foobar
798 drop 5339bf82f0ca 3 Zworgle the foobar
799 pick 8ef592ce7cc4 4 Bedazzle the zerlog
799 pick 8ef592ce7cc4 4 Bedazzle the zerlog
800 pick 0a9639fcda9d 5 Morgify the cromulancy
800 pick 0a9639fcda9d 5 Morgify the cromulancy
801
801
802 - A number of changes have been made.
802 - A number of changes have been made.
803 Revision 2 and 4 need to be swapped.
803 Revision 2 and 4 need to be swapped.
804
804
805 Start history editing from revision 2::
805 Start history editing from revision 2::
806
806
807 hg histedit -r 2
807 hg histedit -r 2
808
808
809 An editor opens, containing the list of revisions,
809 An editor opens, containing the list of revisions,
810 with specific actions specified::
810 with specific actions specified::
811
811
812 pick 252a1af424ad 2 Blorb a morgwazzle
812 pick 252a1af424ad 2 Blorb a morgwazzle
813 pick 5339bf82f0ca 3 Zworgle the foobar
813 pick 5339bf82f0ca 3 Zworgle the foobar
814 pick 8ef592ce7cc4 4 Bedazzle the zerlog
814 pick 8ef592ce7cc4 4 Bedazzle the zerlog
815
815
816 To swap revision 2 and 4, its lines are swapped
816 To swap revision 2 and 4, its lines are swapped
817 in the editor::
817 in the editor::
818
818
819 pick 8ef592ce7cc4 4 Bedazzle the zerlog
819 pick 8ef592ce7cc4 4 Bedazzle the zerlog
820 pick 5339bf82f0ca 3 Zworgle the foobar
820 pick 5339bf82f0ca 3 Zworgle the foobar
821 pick 252a1af424ad 2 Blorb a morgwazzle
821 pick 252a1af424ad 2 Blorb a morgwazzle
822
822
823 Returns 0 on success, 1 if user intervention is required (not only
823 Returns 0 on success, 1 if user intervention is required (not only
824 for intentional "edit" command, but also for resolving unexpected
824 for intentional "edit" command, but also for resolving unexpected
825 conflicts).
825 conflicts).
826 """
826 """
827 state = histeditstate(repo)
827 state = histeditstate(repo)
828 try:
828 try:
829 state.wlock = repo.wlock()
829 state.wlock = repo.wlock()
830 state.lock = repo.lock()
830 state.lock = repo.lock()
831 _histedit(ui, repo, state, *freeargs, **opts)
831 _histedit(ui, repo, state, *freeargs, **opts)
832 except error.Abort:
833 if repo.vfs.exists('histedit-last-edit.txt'):
834 ui.warn(_('warning: histedit rules saved '
835 'to: .hg/histedit-last-edit.txt\n'))
836 raise
832 finally:
837 finally:
833 release(state.lock, state.wlock)
838 release(state.lock, state.wlock)
834
839
835 def _histedit(ui, repo, state, *freeargs, **opts):
840 def _histedit(ui, repo, state, *freeargs, **opts):
836 # TODO only abort if we try to histedit mq patches, not just
841 # TODO only abort if we try to histedit mq patches, not just
837 # blanket if mq patches are applied somewhere
842 # blanket if mq patches are applied somewhere
838 mq = getattr(repo, 'mq', None)
843 mq = getattr(repo, 'mq', None)
839 if mq and mq.applied:
844 if mq and mq.applied:
840 raise error.Abort(_('source has mq patches applied'))
845 raise error.Abort(_('source has mq patches applied'))
841
846
842 # basic argument incompatibility processing
847 # basic argument incompatibility processing
843 outg = opts.get('outgoing')
848 outg = opts.get('outgoing')
844 cont = opts.get('continue')
849 cont = opts.get('continue')
845 editplan = opts.get('edit_plan')
850 editplan = opts.get('edit_plan')
846 abort = opts.get('abort')
851 abort = opts.get('abort')
847 force = opts.get('force')
852 force = opts.get('force')
848 rules = opts.get('commands', '')
853 rules = opts.get('commands', '')
849 revs = opts.get('rev', [])
854 revs = opts.get('rev', [])
850 goal = 'new' # This invocation goal, in new, continue, abort
855 goal = 'new' # This invocation goal, in new, continue, abort
851 if force and not outg:
856 if force and not outg:
852 raise error.Abort(_('--force only allowed with --outgoing'))
857 raise error.Abort(_('--force only allowed with --outgoing'))
853 if cont:
858 if cont:
854 if any((outg, abort, revs, freeargs, rules, editplan)):
859 if any((outg, abort, revs, freeargs, rules, editplan)):
855 raise error.Abort(_('no arguments allowed with --continue'))
860 raise error.Abort(_('no arguments allowed with --continue'))
856 goal = 'continue'
861 goal = 'continue'
857 elif abort:
862 elif abort:
858 if any((outg, revs, freeargs, rules, editplan)):
863 if any((outg, revs, freeargs, rules, editplan)):
859 raise error.Abort(_('no arguments allowed with --abort'))
864 raise error.Abort(_('no arguments allowed with --abort'))
860 goal = 'abort'
865 goal = 'abort'
861 elif editplan:
866 elif editplan:
862 if any((outg, revs, freeargs)):
867 if any((outg, revs, freeargs)):
863 raise error.Abort(_('only --commands argument allowed with '
868 raise error.Abort(_('only --commands argument allowed with '
864 '--edit-plan'))
869 '--edit-plan'))
865 goal = 'edit-plan'
870 goal = 'edit-plan'
866 else:
871 else:
867 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
872 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
868 raise error.Abort(_('history edit already in progress, try '
873 raise error.Abort(_('history edit already in progress, try '
869 '--continue or --abort'))
874 '--continue or --abort'))
870 if outg:
875 if outg:
871 if revs:
876 if revs:
872 raise error.Abort(_('no revisions allowed with --outgoing'))
877 raise error.Abort(_('no revisions allowed with --outgoing'))
873 if len(freeargs) > 1:
878 if len(freeargs) > 1:
874 raise error.Abort(
879 raise error.Abort(
875 _('only one repo argument allowed with --outgoing'))
880 _('only one repo argument allowed with --outgoing'))
876 else:
881 else:
877 revs.extend(freeargs)
882 revs.extend(freeargs)
878 if len(revs) == 0:
883 if len(revs) == 0:
879 # experimental config: histedit.defaultrev
884 # experimental config: histedit.defaultrev
880 histeditdefault = ui.config('histedit', 'defaultrev')
885 histeditdefault = ui.config('histedit', 'defaultrev')
881 if histeditdefault:
886 if histeditdefault:
882 revs.append(histeditdefault)
887 revs.append(histeditdefault)
883 if len(revs) != 1:
888 if len(revs) != 1:
884 raise error.Abort(
889 raise error.Abort(
885 _('histedit requires exactly one ancestor revision'))
890 _('histedit requires exactly one ancestor revision'))
886
891
887
892
888 replacements = []
893 replacements = []
889 state.keep = opts.get('keep', False)
894 state.keep = opts.get('keep', False)
890 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
895 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
891
896
892 # rebuild state
897 # rebuild state
893 if goal == 'continue':
898 if goal == 'continue':
894 state.read()
899 state.read()
895 state = bootstrapcontinue(ui, state, opts)
900 state = bootstrapcontinue(ui, state, opts)
896 elif goal == 'edit-plan':
901 elif goal == 'edit-plan':
897 state.read()
902 state.read()
898 if not rules:
903 if not rules:
899 comment = editcomment % (node.short(state.parentctxnode),
904 comment = editcomment % (node.short(state.parentctxnode),
900 node.short(state.topmost))
905 node.short(state.topmost))
901 rules = ruleeditor(repo, ui, state.rules, comment)
906 rules = ruleeditor(repo, ui, state.rules, comment)
902 else:
907 else:
903 if rules == '-':
908 if rules == '-':
904 f = sys.stdin
909 f = sys.stdin
905 else:
910 else:
906 f = open(rules)
911 f = open(rules)
907 rules = f.read()
912 rules = f.read()
908 f.close()
913 f.close()
909 rules = [l for l in (r.strip() for r in rules.splitlines())
914 rules = [l for l in (r.strip() for r in rules.splitlines())
910 if l and not l.startswith('#')]
915 if l and not l.startswith('#')]
911 rules = verifyrules(rules, state, [repo[c] for [_a, c] in state.rules])
916 rules = verifyrules(rules, state, [repo[c] for [_a, c] in state.rules])
912 state.rules = rules
917 state.rules = rules
913 state.write()
918 state.write()
914 return
919 return
915 elif goal == 'abort':
920 elif goal == 'abort':
916 try:
921 try:
917 state.read()
922 state.read()
918 tmpnodes, leafs = newnodestoabort(state)
923 tmpnodes, leafs = newnodestoabort(state)
919 ui.debug('restore wc to old parent %s\n'
924 ui.debug('restore wc to old parent %s\n'
920 % node.short(state.topmost))
925 % node.short(state.topmost))
921
926
922 # Recover our old commits if necessary
927 # Recover our old commits if necessary
923 if not state.topmost in repo and state.backupfile:
928 if not state.topmost in repo and state.backupfile:
924 backupfile = repo.join(state.backupfile)
929 backupfile = repo.join(state.backupfile)
925 f = hg.openpath(ui, backupfile)
930 f = hg.openpath(ui, backupfile)
926 gen = exchange.readbundle(ui, f, backupfile)
931 gen = exchange.readbundle(ui, f, backupfile)
927 tr = repo.transaction('histedit.abort')
932 tr = repo.transaction('histedit.abort')
928 try:
933 try:
929 if not isinstance(gen, bundle2.unbundle20):
934 if not isinstance(gen, bundle2.unbundle20):
930 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
935 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
931 if isinstance(gen, bundle2.unbundle20):
936 if isinstance(gen, bundle2.unbundle20):
932 bundle2.applybundle(repo, gen, tr,
937 bundle2.applybundle(repo, gen, tr,
933 source='histedit',
938 source='histedit',
934 url='bundle:' + backupfile)
939 url='bundle:' + backupfile)
935 tr.close()
940 tr.close()
936 finally:
941 finally:
937 tr.release()
942 tr.release()
938
943
939 os.remove(backupfile)
944 os.remove(backupfile)
940
945
941 # check whether we should update away
946 # check whether we should update away
942 if repo.unfiltered().revs('parents() and (%n or %ln::)',
947 if repo.unfiltered().revs('parents() and (%n or %ln::)',
943 state.parentctxnode, leafs | tmpnodes):
948 state.parentctxnode, leafs | tmpnodes):
944 hg.clean(repo, state.topmost)
949 hg.clean(repo, state.topmost)
945 cleanupnode(ui, repo, 'created', tmpnodes)
950 cleanupnode(ui, repo, 'created', tmpnodes)
946 cleanupnode(ui, repo, 'temp', leafs)
951 cleanupnode(ui, repo, 'temp', leafs)
947 except Exception:
952 except Exception:
948 if state.inprogress():
953 if state.inprogress():
949 ui.warn(_('warning: encountered an exception during histedit '
954 ui.warn(_('warning: encountered an exception during histedit '
950 '--abort; the repository may not have been completely '
955 '--abort; the repository may not have been completely '
951 'cleaned up\n'))
956 'cleaned up\n'))
952 raise
957 raise
953 finally:
958 finally:
954 state.clear()
959 state.clear()
955 return
960 return
956 else:
961 else:
957 cmdutil.checkunfinished(repo)
962 cmdutil.checkunfinished(repo)
958 cmdutil.bailifchanged(repo)
963 cmdutil.bailifchanged(repo)
959
964
965 if repo.vfs.exists('histedit-last-edit.txt'):
966 repo.vfs.unlink('histedit-last-edit.txt')
960 topmost, empty = repo.dirstate.parents()
967 topmost, empty = repo.dirstate.parents()
961 if outg:
968 if outg:
962 if freeargs:
969 if freeargs:
963 remote = freeargs[0]
970 remote = freeargs[0]
964 else:
971 else:
965 remote = None
972 remote = None
966 root = findoutgoing(ui, repo, remote, force, opts)
973 root = findoutgoing(ui, repo, remote, force, opts)
967 else:
974 else:
968 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
975 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
969 if len(rr) != 1:
976 if len(rr) != 1:
970 raise error.Abort(_('The specified revisions must have '
977 raise error.Abort(_('The specified revisions must have '
971 'exactly one common root'))
978 'exactly one common root'))
972 root = rr[0].node()
979 root = rr[0].node()
973
980
974 revs = between(repo, root, topmost, state.keep)
981 revs = between(repo, root, topmost, state.keep)
975 if not revs:
982 if not revs:
976 raise error.Abort(_('%s is not an ancestor of working directory') %
983 raise error.Abort(_('%s is not an ancestor of working directory') %
977 node.short(root))
984 node.short(root))
978
985
979 ctxs = [repo[r] for r in revs]
986 ctxs = [repo[r] for r in revs]
980 if not rules:
987 if not rules:
981 comment = editcomment % (node.short(root), node.short(topmost))
988 comment = editcomment % (node.short(root), node.short(topmost))
982 rules = ruleeditor(repo, ui, [['pick', c] for c in ctxs], comment)
989 rules = ruleeditor(repo, ui, [['pick', c] for c in ctxs], comment)
983 else:
990 else:
984 if rules == '-':
991 if rules == '-':
985 f = sys.stdin
992 f = sys.stdin
986 else:
993 else:
987 f = open(rules)
994 f = open(rules)
988 rules = f.read()
995 rules = f.read()
989 f.close()
996 f.close()
990 rules = [l for l in (r.strip() for r in rules.splitlines())
997 rules = [l for l in (r.strip() for r in rules.splitlines())
991 if l and not l.startswith('#')]
998 if l and not l.startswith('#')]
992 rules = verifyrules(rules, state, ctxs)
999 rules = verifyrules(rules, state, ctxs)
993
1000
994 parentctxnode = repo[root].parents()[0].node()
1001 parentctxnode = repo[root].parents()[0].node()
995
1002
996 state.parentctxnode = parentctxnode
1003 state.parentctxnode = parentctxnode
997 state.rules = rules
1004 state.rules = rules
998 state.topmost = topmost
1005 state.topmost = topmost
999 state.replacements = replacements
1006 state.replacements = replacements
1000
1007
1001 # Create a backup so we can always abort completely.
1008 # Create a backup so we can always abort completely.
1002 backupfile = None
1009 backupfile = None
1003 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1010 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1004 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1011 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1005 'histedit')
1012 'histedit')
1006 state.backupfile = backupfile
1013 state.backupfile = backupfile
1007
1014
1008 # preprocess rules so that we can hide inner folds from the user
1015 # preprocess rules so that we can hide inner folds from the user
1009 # and only show one editor
1016 # and only show one editor
1010 rules = state.rules[:]
1017 rules = state.rules[:]
1011 for idx, ((action, ha), (nextact, unused)) in enumerate(
1018 for idx, ((action, ha), (nextact, unused)) in enumerate(
1012 zip(rules, rules[1:] + [(None, None)])):
1019 zip(rules, rules[1:] + [(None, None)])):
1013 if action == 'fold' and nextact == 'fold':
1020 if action == 'fold' and nextact == 'fold':
1014 state.rules[idx] = '_multifold', ha
1021 state.rules[idx] = '_multifold', ha
1015
1022
1016 while state.rules:
1023 while state.rules:
1017 state.write()
1024 state.write()
1018 action, ha = state.rules.pop(0)
1025 action, ha = state.rules.pop(0)
1019 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
1026 ui.debug('histedit: processing %s %s\n' % (action, ha[:12]))
1020 actobj = actiontable[action].fromrule(state, ha)
1027 actobj = actiontable[action].fromrule(state, ha)
1021 parentctx, replacement_ = actobj.run()
1028 parentctx, replacement_ = actobj.run()
1022 state.parentctxnode = parentctx.node()
1029 state.parentctxnode = parentctx.node()
1023 state.replacements.extend(replacement_)
1030 state.replacements.extend(replacement_)
1024 state.write()
1031 state.write()
1025
1032
1026 hg.update(repo, state.parentctxnode)
1033 hg.update(repo, state.parentctxnode)
1027
1034
1028 mapping, tmpnodes, created, ntm = processreplacement(state)
1035 mapping, tmpnodes, created, ntm = processreplacement(state)
1029 if mapping:
1036 if mapping:
1030 for prec, succs in mapping.iteritems():
1037 for prec, succs in mapping.iteritems():
1031 if not succs:
1038 if not succs:
1032 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1039 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1033 else:
1040 else:
1034 ui.debug('histedit: %s is replaced by %s\n' % (
1041 ui.debug('histedit: %s is replaced by %s\n' % (
1035 node.short(prec), node.short(succs[0])))
1042 node.short(prec), node.short(succs[0])))
1036 if len(succs) > 1:
1043 if len(succs) > 1:
1037 m = 'histedit: %s'
1044 m = 'histedit: %s'
1038 for n in succs[1:]:
1045 for n in succs[1:]:
1039 ui.debug(m % node.short(n))
1046 ui.debug(m % node.short(n))
1040
1047
1041 if supportsmarkers:
1048 if supportsmarkers:
1042 # Only create markers if the temp nodes weren't already removed.
1049 # Only create markers if the temp nodes weren't already removed.
1043 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1050 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1044 if t in repo))
1051 if t in repo))
1045 else:
1052 else:
1046 cleanupnode(ui, repo, 'temp', tmpnodes)
1053 cleanupnode(ui, repo, 'temp', tmpnodes)
1047
1054
1048 if not state.keep:
1055 if not state.keep:
1049 if mapping:
1056 if mapping:
1050 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1057 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1051 # TODO update mq state
1058 # TODO update mq state
1052 if supportsmarkers:
1059 if supportsmarkers:
1053 markers = []
1060 markers = []
1054 # sort by revision number because it sound "right"
1061 # sort by revision number because it sound "right"
1055 for prec in sorted(mapping, key=repo.changelog.rev):
1062 for prec in sorted(mapping, key=repo.changelog.rev):
1056 succs = mapping[prec]
1063 succs = mapping[prec]
1057 markers.append((repo[prec],
1064 markers.append((repo[prec],
1058 tuple(repo[s] for s in succs)))
1065 tuple(repo[s] for s in succs)))
1059 if markers:
1066 if markers:
1060 obsolete.createmarkers(repo, markers)
1067 obsolete.createmarkers(repo, markers)
1061 else:
1068 else:
1062 cleanupnode(ui, repo, 'replaced', mapping)
1069 cleanupnode(ui, repo, 'replaced', mapping)
1063
1070
1064 state.clear()
1071 state.clear()
1065 if os.path.exists(repo.sjoin('undo')):
1072 if os.path.exists(repo.sjoin('undo')):
1066 os.unlink(repo.sjoin('undo'))
1073 os.unlink(repo.sjoin('undo'))
1067
1074
1068 def bootstrapcontinue(ui, state, opts):
1075 def bootstrapcontinue(ui, state, opts):
1069 repo = state.repo
1076 repo = state.repo
1070 if state.rules:
1077 if state.rules:
1071 action, currentnode = state.rules.pop(0)
1078 action, currentnode = state.rules.pop(0)
1072
1079
1073 actobj = actiontable[action].fromrule(state, currentnode)
1080 actobj = actiontable[action].fromrule(state, currentnode)
1074
1081
1075 if _isdirtywc(repo):
1082 if _isdirtywc(repo):
1076 actobj.continuedirty()
1083 actobj.continuedirty()
1077 if _isdirtywc(repo):
1084 if _isdirtywc(repo):
1078 abortdirty()
1085 abortdirty()
1079
1086
1080 parentctx, replacements = actobj.continueclean()
1087 parentctx, replacements = actobj.continueclean()
1081
1088
1082 state.parentctxnode = parentctx.node()
1089 state.parentctxnode = parentctx.node()
1083 state.replacements.extend(replacements)
1090 state.replacements.extend(replacements)
1084
1091
1085 return state
1092 return state
1086
1093
1087 def between(repo, old, new, keep):
1094 def between(repo, old, new, keep):
1088 """select and validate the set of revision to edit
1095 """select and validate the set of revision to edit
1089
1096
1090 When keep is false, the specified set can't have children."""
1097 When keep is false, the specified set can't have children."""
1091 ctxs = list(repo.set('%n::%n', old, new))
1098 ctxs = list(repo.set('%n::%n', old, new))
1092 if ctxs and not keep:
1099 if ctxs and not keep:
1093 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1100 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1094 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1101 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1095 raise error.Abort(_('cannot edit history that would orphan nodes'))
1102 raise error.Abort(_('cannot edit history that would orphan nodes'))
1096 if repo.revs('(%ld) and merge()', ctxs):
1103 if repo.revs('(%ld) and merge()', ctxs):
1097 raise error.Abort(_('cannot edit history that contains merges'))
1104 raise error.Abort(_('cannot edit history that contains merges'))
1098 root = ctxs[0] # list is already sorted by repo.set
1105 root = ctxs[0] # list is already sorted by repo.set
1099 if not root.mutable():
1106 if not root.mutable():
1100 raise error.Abort(_('cannot edit public changeset: %s') % root,
1107 raise error.Abort(_('cannot edit public changeset: %s') % root,
1101 hint=_('see "hg help phases" for details'))
1108 hint=_('see "hg help phases" for details'))
1102 return [c.node() for c in ctxs]
1109 return [c.node() for c in ctxs]
1103
1110
1104 def makedesc(repo, action, rev):
1111 def makedesc(repo, action, rev):
1105 """build a initial action line for a ctx
1112 """build a initial action line for a ctx
1106
1113
1107 line are in the form:
1114 line are in the form:
1108
1115
1109 <action> <hash> <rev> <summary>
1116 <action> <hash> <rev> <summary>
1110 """
1117 """
1111 ctx = repo[rev]
1118 ctx = repo[rev]
1112 summary = ''
1119 summary = ''
1113 if ctx.description():
1120 if ctx.description():
1114 summary = ctx.description().splitlines()[0]
1121 summary = ctx.description().splitlines()[0]
1115 line = '%s %s %d %s' % (action, ctx, ctx.rev(), summary)
1122 line = '%s %s %d %s' % (action, ctx, ctx.rev(), summary)
1116 # trim to 80 columns so it's not stupidly wide in my editor
1123 # trim to 80 columns so it's not stupidly wide in my editor
1117 maxlen = repo.ui.configint('histedit', 'linelen', default=80)
1124 maxlen = repo.ui.configint('histedit', 'linelen', default=80)
1118 maxlen = max(maxlen, 22) # avoid truncating hash
1125 maxlen = max(maxlen, 22) # avoid truncating hash
1119 return util.ellipsis(line, maxlen)
1126 return util.ellipsis(line, maxlen)
1120
1127
1121 def ruleeditor(repo, ui, rules, editcomment=""):
1128 def ruleeditor(repo, ui, rules, editcomment=""):
1122 """open an editor to edit rules
1129 """open an editor to edit rules
1123
1130
1124 rules are in the format [ [act, ctx], ...] like in state.rules
1131 rules are in the format [ [act, ctx], ...] like in state.rules
1125 """
1132 """
1126 rules = '\n'.join([makedesc(repo, act, rev) for [act, rev] in rules])
1133 rules = '\n'.join([makedesc(repo, act, rev) for [act, rev] in rules])
1127 rules += '\n\n'
1134 rules += '\n\n'
1128 rules += editcomment
1135 rules += editcomment
1129 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1136 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1130
1137
1131 # Save edit rules in .hg/histedit-last-edit.txt in case
1138 # Save edit rules in .hg/histedit-last-edit.txt in case
1132 # the user needs to ask for help after something
1139 # the user needs to ask for help after something
1133 # surprising happens.
1140 # surprising happens.
1134 f = open(repo.join('histedit-last-edit.txt'), 'w')
1141 f = open(repo.join('histedit-last-edit.txt'), 'w')
1135 f.write(rules)
1142 f.write(rules)
1136 f.close()
1143 f.close()
1137
1144
1138 return rules
1145 return rules
1139
1146
1140 def verifyrules(rules, state, ctxs):
1147 def verifyrules(rules, state, ctxs):
1141 """Verify that there exists exactly one edit rule per given changeset.
1148 """Verify that there exists exactly one edit rule per given changeset.
1142
1149
1143 Will abort if there are to many or too few rules, a malformed rule,
1150 Will abort if there are to many or too few rules, a malformed rule,
1144 or a rule on a changeset outside of the user-given range.
1151 or a rule on a changeset outside of the user-given range.
1145 """
1152 """
1146 parsed = []
1153 parsed = []
1147 expected = set(c.hex() for c in ctxs)
1154 expected = set(c.hex() for c in ctxs)
1148 seen = set()
1155 seen = set()
1149 for r in rules:
1156 for r in rules:
1150 if ' ' not in r:
1157 if ' ' not in r:
1151 raise error.Abort(_('malformed line "%s"') % r)
1158 raise error.Abort(_('malformed line "%s"') % r)
1152 verb, rest = r.split(' ', 1)
1159 verb, rest = r.split(' ', 1)
1153
1160
1154 if verb not in actiontable or verb.startswith('_'):
1161 if verb not in actiontable or verb.startswith('_'):
1155 raise error.Abort(_('unknown action "%s"') % verb)
1162 raise error.Abort(_('unknown action "%s"') % verb)
1156 action = actiontable[verb].fromrule(state, rest)
1163 action = actiontable[verb].fromrule(state, rest)
1157 constraints = action.constraints()
1164 constraints = action.constraints()
1158 for constraint in constraints:
1165 for constraint in constraints:
1159 if constraint not in _constraints.known():
1166 if constraint not in _constraints.known():
1160 raise error.Abort(_('unknown constraint "%s"') % constraint)
1167 raise error.Abort(_('unknown constraint "%s"') % constraint)
1161
1168
1162 nodetoverify = action.nodetoverify()
1169 nodetoverify = action.nodetoverify()
1163 if nodetoverify is not None:
1170 if nodetoverify is not None:
1164 ha = node.hex(nodetoverify)
1171 ha = node.hex(nodetoverify)
1165 if _constraints.noother in constraints and ha not in expected:
1172 if _constraints.noother in constraints and ha not in expected:
1166 raise error.Abort(
1173 raise error.Abort(
1167 _('may not use "%s" with changesets '
1174 _('may not use "%s" with changesets '
1168 'other than the ones listed') % verb)
1175 'other than the ones listed') % verb)
1169 if _constraints.forceother in constraints and ha in expected:
1176 if _constraints.forceother in constraints and ha in expected:
1170 raise error.Abort(
1177 raise error.Abort(
1171 _('may not use "%s" with changesets '
1178 _('may not use "%s" with changesets '
1172 'within the edited list') % verb)
1179 'within the edited list') % verb)
1173 if _constraints.noduplicates in constraints and ha in seen:
1180 if _constraints.noduplicates in constraints and ha in seen:
1174 raise error.Abort(_('duplicated command for changeset %s') %
1181 raise error.Abort(_('duplicated command for changeset %s') %
1175 ha[:12])
1182 ha[:12])
1176 seen.add(ha)
1183 seen.add(ha)
1177 rest = ha
1184 rest = ha
1178 parsed.append([verb, rest])
1185 parsed.append([verb, rest])
1179 missing = sorted(expected - seen) # sort to stabilize output
1186 missing = sorted(expected - seen) # sort to stabilize output
1180 if missing:
1187 if missing:
1181 raise error.Abort(_('missing rules for changeset %s') %
1188 raise error.Abort(_('missing rules for changeset %s') %
1182 missing[0][:12],
1189 missing[0][:12],
1183 hint=_('do you want to use the drop action?'))
1190 hint=_('do you want to use the drop action?'))
1184 return parsed
1191 return parsed
1185
1192
1186 def newnodestoabort(state):
1193 def newnodestoabort(state):
1187 """process the list of replacements to return
1194 """process the list of replacements to return
1188
1195
1189 1) the list of final node
1196 1) the list of final node
1190 2) the list of temporary node
1197 2) the list of temporary node
1191
1198
1192 This meant to be used on abort as less data are required in this case.
1199 This meant to be used on abort as less data are required in this case.
1193 """
1200 """
1194 replacements = state.replacements
1201 replacements = state.replacements
1195 allsuccs = set()
1202 allsuccs = set()
1196 replaced = set()
1203 replaced = set()
1197 for rep in replacements:
1204 for rep in replacements:
1198 allsuccs.update(rep[1])
1205 allsuccs.update(rep[1])
1199 replaced.add(rep[0])
1206 replaced.add(rep[0])
1200 newnodes = allsuccs - replaced
1207 newnodes = allsuccs - replaced
1201 tmpnodes = allsuccs & replaced
1208 tmpnodes = allsuccs & replaced
1202 return newnodes, tmpnodes
1209 return newnodes, tmpnodes
1203
1210
1204
1211
1205 def processreplacement(state):
1212 def processreplacement(state):
1206 """process the list of replacements to return
1213 """process the list of replacements to return
1207
1214
1208 1) the final mapping between original and created nodes
1215 1) the final mapping between original and created nodes
1209 2) the list of temporary node created by histedit
1216 2) the list of temporary node created by histedit
1210 3) the list of new commit created by histedit"""
1217 3) the list of new commit created by histedit"""
1211 replacements = state.replacements
1218 replacements = state.replacements
1212 allsuccs = set()
1219 allsuccs = set()
1213 replaced = set()
1220 replaced = set()
1214 fullmapping = {}
1221 fullmapping = {}
1215 # initialize basic set
1222 # initialize basic set
1216 # fullmapping records all operations recorded in replacement
1223 # fullmapping records all operations recorded in replacement
1217 for rep in replacements:
1224 for rep in replacements:
1218 allsuccs.update(rep[1])
1225 allsuccs.update(rep[1])
1219 replaced.add(rep[0])
1226 replaced.add(rep[0])
1220 fullmapping.setdefault(rep[0], set()).update(rep[1])
1227 fullmapping.setdefault(rep[0], set()).update(rep[1])
1221 new = allsuccs - replaced
1228 new = allsuccs - replaced
1222 tmpnodes = allsuccs & replaced
1229 tmpnodes = allsuccs & replaced
1223 # Reduce content fullmapping into direct relation between original nodes
1230 # Reduce content fullmapping into direct relation between original nodes
1224 # and final node created during history edition
1231 # and final node created during history edition
1225 # Dropped changeset are replaced by an empty list
1232 # Dropped changeset are replaced by an empty list
1226 toproceed = set(fullmapping)
1233 toproceed = set(fullmapping)
1227 final = {}
1234 final = {}
1228 while toproceed:
1235 while toproceed:
1229 for x in list(toproceed):
1236 for x in list(toproceed):
1230 succs = fullmapping[x]
1237 succs = fullmapping[x]
1231 for s in list(succs):
1238 for s in list(succs):
1232 if s in toproceed:
1239 if s in toproceed:
1233 # non final node with unknown closure
1240 # non final node with unknown closure
1234 # We can't process this now
1241 # We can't process this now
1235 break
1242 break
1236 elif s in final:
1243 elif s in final:
1237 # non final node, replace with closure
1244 # non final node, replace with closure
1238 succs.remove(s)
1245 succs.remove(s)
1239 succs.update(final[s])
1246 succs.update(final[s])
1240 else:
1247 else:
1241 final[x] = succs
1248 final[x] = succs
1242 toproceed.remove(x)
1249 toproceed.remove(x)
1243 # remove tmpnodes from final mapping
1250 # remove tmpnodes from final mapping
1244 for n in tmpnodes:
1251 for n in tmpnodes:
1245 del final[n]
1252 del final[n]
1246 # we expect all changes involved in final to exist in the repo
1253 # we expect all changes involved in final to exist in the repo
1247 # turn `final` into list (topologically sorted)
1254 # turn `final` into list (topologically sorted)
1248 nm = state.repo.changelog.nodemap
1255 nm = state.repo.changelog.nodemap
1249 for prec, succs in final.items():
1256 for prec, succs in final.items():
1250 final[prec] = sorted(succs, key=nm.get)
1257 final[prec] = sorted(succs, key=nm.get)
1251
1258
1252 # computed topmost element (necessary for bookmark)
1259 # computed topmost element (necessary for bookmark)
1253 if new:
1260 if new:
1254 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1261 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1255 elif not final:
1262 elif not final:
1256 # Nothing rewritten at all. we won't need `newtopmost`
1263 # Nothing rewritten at all. we won't need `newtopmost`
1257 # It is the same as `oldtopmost` and `processreplacement` know it
1264 # It is the same as `oldtopmost` and `processreplacement` know it
1258 newtopmost = None
1265 newtopmost = None
1259 else:
1266 else:
1260 # every body died. The newtopmost is the parent of the root.
1267 # every body died. The newtopmost is the parent of the root.
1261 r = state.repo.changelog.rev
1268 r = state.repo.changelog.rev
1262 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1269 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1263
1270
1264 return final, tmpnodes, new, newtopmost
1271 return final, tmpnodes, new, newtopmost
1265
1272
1266 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1273 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1267 """Move bookmark from old to newly created node"""
1274 """Move bookmark from old to newly created node"""
1268 if not mapping:
1275 if not mapping:
1269 # if nothing got rewritten there is not purpose for this function
1276 # if nothing got rewritten there is not purpose for this function
1270 return
1277 return
1271 moves = []
1278 moves = []
1272 for bk, old in sorted(repo._bookmarks.iteritems()):
1279 for bk, old in sorted(repo._bookmarks.iteritems()):
1273 if old == oldtopmost:
1280 if old == oldtopmost:
1274 # special case ensure bookmark stay on tip.
1281 # special case ensure bookmark stay on tip.
1275 #
1282 #
1276 # This is arguably a feature and we may only want that for the
1283 # This is arguably a feature and we may only want that for the
1277 # active bookmark. But the behavior is kept compatible with the old
1284 # active bookmark. But the behavior is kept compatible with the old
1278 # version for now.
1285 # version for now.
1279 moves.append((bk, newtopmost))
1286 moves.append((bk, newtopmost))
1280 continue
1287 continue
1281 base = old
1288 base = old
1282 new = mapping.get(base, None)
1289 new = mapping.get(base, None)
1283 if new is None:
1290 if new is None:
1284 continue
1291 continue
1285 while not new:
1292 while not new:
1286 # base is killed, trying with parent
1293 # base is killed, trying with parent
1287 base = repo[base].p1().node()
1294 base = repo[base].p1().node()
1288 new = mapping.get(base, (base,))
1295 new = mapping.get(base, (base,))
1289 # nothing to move
1296 # nothing to move
1290 moves.append((bk, new[-1]))
1297 moves.append((bk, new[-1]))
1291 if moves:
1298 if moves:
1292 lock = tr = None
1299 lock = tr = None
1293 try:
1300 try:
1294 lock = repo.lock()
1301 lock = repo.lock()
1295 tr = repo.transaction('histedit')
1302 tr = repo.transaction('histedit')
1296 marks = repo._bookmarks
1303 marks = repo._bookmarks
1297 for mark, new in moves:
1304 for mark, new in moves:
1298 old = marks[mark]
1305 old = marks[mark]
1299 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1306 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1300 % (mark, node.short(old), node.short(new)))
1307 % (mark, node.short(old), node.short(new)))
1301 marks[mark] = new
1308 marks[mark] = new
1302 marks.recordchange(tr)
1309 marks.recordchange(tr)
1303 tr.close()
1310 tr.close()
1304 finally:
1311 finally:
1305 release(tr, lock)
1312 release(tr, lock)
1306
1313
1307 def cleanupnode(ui, repo, name, nodes):
1314 def cleanupnode(ui, repo, name, nodes):
1308 """strip a group of nodes from the repository
1315 """strip a group of nodes from the repository
1309
1316
1310 The set of node to strip may contains unknown nodes."""
1317 The set of node to strip may contains unknown nodes."""
1311 ui.debug('should strip %s nodes %s\n' %
1318 ui.debug('should strip %s nodes %s\n' %
1312 (name, ', '.join([node.short(n) for n in nodes])))
1319 (name, ', '.join([node.short(n) for n in nodes])))
1313 lock = None
1320 lock = None
1314 try:
1321 try:
1315 lock = repo.lock()
1322 lock = repo.lock()
1316 # do not let filtering get in the way of the cleanse
1323 # do not let filtering get in the way of the cleanse
1317 # we should probably get rid of obsolescence marker created during the
1324 # we should probably get rid of obsolescence marker created during the
1318 # histedit, but we currently do not have such information.
1325 # histedit, but we currently do not have such information.
1319 repo = repo.unfiltered()
1326 repo = repo.unfiltered()
1320 # Find all nodes that need to be stripped
1327 # Find all nodes that need to be stripped
1321 # (we use %lr instead of %ln to silently ignore unknown items)
1328 # (we use %lr instead of %ln to silently ignore unknown items)
1322 nm = repo.changelog.nodemap
1329 nm = repo.changelog.nodemap
1323 nodes = sorted(n for n in nodes if n in nm)
1330 nodes = sorted(n for n in nodes if n in nm)
1324 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1331 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1325 for c in roots:
1332 for c in roots:
1326 # We should process node in reverse order to strip tip most first.
1333 # We should process node in reverse order to strip tip most first.
1327 # but this trigger a bug in changegroup hook.
1334 # but this trigger a bug in changegroup hook.
1328 # This would reduce bundle overhead
1335 # This would reduce bundle overhead
1329 repair.strip(ui, repo, c)
1336 repair.strip(ui, repo, c)
1330 finally:
1337 finally:
1331 release(lock)
1338 release(lock)
1332
1339
1333 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1340 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1334 if isinstance(nodelist, str):
1341 if isinstance(nodelist, str):
1335 nodelist = [nodelist]
1342 nodelist = [nodelist]
1336 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1343 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1337 state = histeditstate(repo)
1344 state = histeditstate(repo)
1338 state.read()
1345 state.read()
1339 histedit_nodes = set([repo[rulehash].node() for (action, rulehash)
1346 histedit_nodes = set([repo[rulehash].node() for (action, rulehash)
1340 in state.rules if rulehash in repo])
1347 in state.rules if rulehash in repo])
1341 strip_nodes = set([repo[n].node() for n in nodelist])
1348 strip_nodes = set([repo[n].node() for n in nodelist])
1342 common_nodes = histedit_nodes & strip_nodes
1349 common_nodes = histedit_nodes & strip_nodes
1343 if common_nodes:
1350 if common_nodes:
1344 raise error.Abort(_("histedit in progress, can't strip %s")
1351 raise error.Abort(_("histedit in progress, can't strip %s")
1345 % ', '.join(node.short(x) for x in common_nodes))
1352 % ', '.join(node.short(x) for x in common_nodes))
1346 return orig(ui, repo, nodelist, *args, **kwargs)
1353 return orig(ui, repo, nodelist, *args, **kwargs)
1347
1354
1348 extensions.wrapfunction(repair, 'strip', stripwrapper)
1355 extensions.wrapfunction(repair, 'strip', stripwrapper)
1349
1356
1350 def summaryhook(ui, repo):
1357 def summaryhook(ui, repo):
1351 if not os.path.exists(repo.join('histedit-state')):
1358 if not os.path.exists(repo.join('histedit-state')):
1352 return
1359 return
1353 state = histeditstate(repo)
1360 state = histeditstate(repo)
1354 state.read()
1361 state.read()
1355 if state.rules:
1362 if state.rules:
1356 # i18n: column positioning for "hg summary"
1363 # i18n: column positioning for "hg summary"
1357 ui.write(_('hist: %s (histedit --continue)\n') %
1364 ui.write(_('hist: %s (histedit --continue)\n') %
1358 (ui.label(_('%d remaining'), 'histedit.remaining') %
1365 (ui.label(_('%d remaining'), 'histedit.remaining') %
1359 len(state.rules)))
1366 len(state.rules)))
1360
1367
1361 def extsetup(ui):
1368 def extsetup(ui):
1362 cmdutil.summaryhooks.add('histedit', summaryhook)
1369 cmdutil.summaryhooks.add('histedit', summaryhook)
1363 cmdutil.unfinishedstates.append(
1370 cmdutil.unfinishedstates.append(
1364 ['histedit-state', False, True, _('histedit in progress'),
1371 ['histedit-state', False, True, _('histedit in progress'),
1365 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1372 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1366 if ui.configbool("experimental", "histeditng"):
1373 if ui.configbool("experimental", "histeditng"):
1367 actiontable.update({'b': base, 'base': base})
1374 actiontable.update({'b': base, 'base': base})
@@ -1,467 +1,483 b''
1 $ . "$TESTDIR/histedit-helpers.sh"
1 $ . "$TESTDIR/histedit-helpers.sh"
2
2
3 $ cat >> $HGRCPATH <<EOF
3 $ cat >> $HGRCPATH <<EOF
4 > [extensions]
4 > [extensions]
5 > histedit=
5 > histedit=
6 > strip=
6 > strip=
7 > EOF
7 > EOF
8
8
9 $ initrepo ()
9 $ initrepo ()
10 > {
10 > {
11 > hg init r
11 > hg init r
12 > cd r
12 > cd r
13 > for x in a b c d e f g; do
13 > for x in a b c d e f g; do
14 > echo $x > $x
14 > echo $x > $x
15 > hg add $x
15 > hg add $x
16 > hg ci -m $x
16 > hg ci -m $x
17 > done
17 > done
18 > }
18 > }
19
19
20 $ initrepo
20 $ initrepo
21
21
22 log before edit
22 log before edit
23 $ hg log --graph
23 $ hg log --graph
24 @ changeset: 6:3c6a8ed2ebe8
24 @ changeset: 6:3c6a8ed2ebe8
25 | tag: tip
25 | tag: tip
26 | user: test
26 | user: test
27 | date: Thu Jan 01 00:00:00 1970 +0000
27 | date: Thu Jan 01 00:00:00 1970 +0000
28 | summary: g
28 | summary: g
29 |
29 |
30 o changeset: 5:652413bf663e
30 o changeset: 5:652413bf663e
31 | user: test
31 | user: test
32 | date: Thu Jan 01 00:00:00 1970 +0000
32 | date: Thu Jan 01 00:00:00 1970 +0000
33 | summary: f
33 | summary: f
34 |
34 |
35 o changeset: 4:e860deea161a
35 o changeset: 4:e860deea161a
36 | user: test
36 | user: test
37 | date: Thu Jan 01 00:00:00 1970 +0000
37 | date: Thu Jan 01 00:00:00 1970 +0000
38 | summary: e
38 | summary: e
39 |
39 |
40 o changeset: 3:055a42cdd887
40 o changeset: 3:055a42cdd887
41 | user: test
41 | user: test
42 | date: Thu Jan 01 00:00:00 1970 +0000
42 | date: Thu Jan 01 00:00:00 1970 +0000
43 | summary: d
43 | summary: d
44 |
44 |
45 o changeset: 2:177f92b77385
45 o changeset: 2:177f92b77385
46 | user: test
46 | user: test
47 | date: Thu Jan 01 00:00:00 1970 +0000
47 | date: Thu Jan 01 00:00:00 1970 +0000
48 | summary: c
48 | summary: c
49 |
49 |
50 o changeset: 1:d2ae7f538514
50 o changeset: 1:d2ae7f538514
51 | user: test
51 | user: test
52 | date: Thu Jan 01 00:00:00 1970 +0000
52 | date: Thu Jan 01 00:00:00 1970 +0000
53 | summary: b
53 | summary: b
54 |
54 |
55 o changeset: 0:cb9a9f314b8b
55 o changeset: 0:cb9a9f314b8b
56 user: test
56 user: test
57 date: Thu Jan 01 00:00:00 1970 +0000
57 date: Thu Jan 01 00:00:00 1970 +0000
58 summary: a
58 summary: a
59
59
60
60
61 edit the history
61 edit the history
62 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
62 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
63 > pick 177f92b77385 c
63 > pick 177f92b77385 c
64 > pick 055a42cdd887 d
64 > pick 055a42cdd887 d
65 > edit e860deea161a e
65 > edit e860deea161a e
66 > pick 652413bf663e f
66 > pick 652413bf663e f
67 > pick 3c6a8ed2ebe8 g
67 > pick 3c6a8ed2ebe8 g
68 > EOF
68 > EOF
69 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
69 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
70 Make changes as needed, you may commit or record as needed now.
70 Make changes as needed, you may commit or record as needed now.
71 When you are finished, run hg histedit --continue to resume.
71 When you are finished, run hg histedit --continue to resume.
72
72
73 edit the plan via the editor
73 edit the plan via the editor
74 $ cat >> $TESTTMP/editplan.sh <<EOF
74 $ cat >> $TESTTMP/editplan.sh <<EOF
75 > cat > \$1 <<EOF2
75 > cat > \$1 <<EOF2
76 > drop e860deea161a e
76 > drop e860deea161a e
77 > drop 652413bf663e f
77 > drop 652413bf663e f
78 > drop 3c6a8ed2ebe8 g
78 > drop 3c6a8ed2ebe8 g
79 > EOF2
79 > EOF2
80 > EOF
80 > EOF
81 $ HGEDITOR="sh $TESTTMP/editplan.sh" hg histedit --edit-plan
81 $ HGEDITOR="sh $TESTTMP/editplan.sh" hg histedit --edit-plan
82 $ cat .hg/histedit-state
82 $ cat .hg/histedit-state
83 v1
83 v1
84 055a42cdd88768532f9cf79daa407fc8d138de9b
84 055a42cdd88768532f9cf79daa407fc8d138de9b
85 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
85 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
86 False
86 False
87 3
87 3
88 drop
88 drop
89 e860deea161a2f77de56603b340ebbb4536308ae
89 e860deea161a2f77de56603b340ebbb4536308ae
90 drop
90 drop
91 652413bf663ef2a641cab26574e46d5f5a64a55a
91 652413bf663ef2a641cab26574e46d5f5a64a55a
92 drop
92 drop
93 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
93 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
94 0
94 0
95 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
95 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
96
96
97 edit the plan via --commands
97 edit the plan via --commands
98 $ hg histedit --edit-plan --commands - 2>&1 << EOF
98 $ hg histedit --edit-plan --commands - 2>&1 << EOF
99 > edit e860deea161a e
99 > edit e860deea161a e
100 > pick 652413bf663e f
100 > pick 652413bf663e f
101 > drop 3c6a8ed2ebe8 g
101 > drop 3c6a8ed2ebe8 g
102 > EOF
102 > EOF
103 $ cat .hg/histedit-state
103 $ cat .hg/histedit-state
104 v1
104 v1
105 055a42cdd88768532f9cf79daa407fc8d138de9b
105 055a42cdd88768532f9cf79daa407fc8d138de9b
106 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
106 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
107 False
107 False
108 3
108 3
109 edit
109 edit
110 e860deea161a2f77de56603b340ebbb4536308ae
110 e860deea161a2f77de56603b340ebbb4536308ae
111 pick
111 pick
112 652413bf663ef2a641cab26574e46d5f5a64a55a
112 652413bf663ef2a641cab26574e46d5f5a64a55a
113 drop
113 drop
114 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
114 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
115 0
115 0
116 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
116 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
117
117
118 Go at a random point and try to continue
118 Go at a random point and try to continue
119
119
120 $ hg id -n
120 $ hg id -n
121 3+
121 3+
122 $ hg up 0
122 $ hg up 0
123 abort: histedit in progress
123 abort: histedit in progress
124 (use 'hg histedit --continue' or 'hg histedit --abort')
124 (use 'hg histedit --continue' or 'hg histedit --abort')
125 [255]
125 [255]
126
126
127 Try to delete necessary commit
127 Try to delete necessary commit
128 $ hg strip -r 652413b
128 $ hg strip -r 652413b
129 abort: histedit in progress, can't strip 652413bf663e
129 abort: histedit in progress, can't strip 652413bf663e
130 [255]
130 [255]
131
131
132 commit, then edit the revision
132 commit, then edit the revision
133 $ hg ci -m 'wat'
133 $ hg ci -m 'wat'
134 created new head
134 created new head
135 $ echo a > e
135 $ echo a > e
136
136
137 qnew should fail while we're in the middle of the edit step
137 qnew should fail while we're in the middle of the edit step
138
138
139 $ hg --config extensions.mq= qnew please-fail
139 $ hg --config extensions.mq= qnew please-fail
140 abort: histedit in progress
140 abort: histedit in progress
141 (use 'hg histedit --continue' or 'hg histedit --abort')
141 (use 'hg histedit --continue' or 'hg histedit --abort')
142 [255]
142 [255]
143 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
143 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
144 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
144 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
145 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
145 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
146
146
147 $ hg log --graph
147 $ hg log --graph
148 @ changeset: 6:b5f70786f9b0
148 @ changeset: 6:b5f70786f9b0
149 | tag: tip
149 | tag: tip
150 | user: test
150 | user: test
151 | date: Thu Jan 01 00:00:00 1970 +0000
151 | date: Thu Jan 01 00:00:00 1970 +0000
152 | summary: f
152 | summary: f
153 |
153 |
154 o changeset: 5:a5e1ba2f7afb
154 o changeset: 5:a5e1ba2f7afb
155 | user: test
155 | user: test
156 | date: Thu Jan 01 00:00:00 1970 +0000
156 | date: Thu Jan 01 00:00:00 1970 +0000
157 | summary: foobaz
157 | summary: foobaz
158 |
158 |
159 o changeset: 4:1a60820cd1f6
159 o changeset: 4:1a60820cd1f6
160 | user: test
160 | user: test
161 | date: Thu Jan 01 00:00:00 1970 +0000
161 | date: Thu Jan 01 00:00:00 1970 +0000
162 | summary: wat
162 | summary: wat
163 |
163 |
164 o changeset: 3:055a42cdd887
164 o changeset: 3:055a42cdd887
165 | user: test
165 | user: test
166 | date: Thu Jan 01 00:00:00 1970 +0000
166 | date: Thu Jan 01 00:00:00 1970 +0000
167 | summary: d
167 | summary: d
168 |
168 |
169 o changeset: 2:177f92b77385
169 o changeset: 2:177f92b77385
170 | user: test
170 | user: test
171 | date: Thu Jan 01 00:00:00 1970 +0000
171 | date: Thu Jan 01 00:00:00 1970 +0000
172 | summary: c
172 | summary: c
173 |
173 |
174 o changeset: 1:d2ae7f538514
174 o changeset: 1:d2ae7f538514
175 | user: test
175 | user: test
176 | date: Thu Jan 01 00:00:00 1970 +0000
176 | date: Thu Jan 01 00:00:00 1970 +0000
177 | summary: b
177 | summary: b
178 |
178 |
179 o changeset: 0:cb9a9f314b8b
179 o changeset: 0:cb9a9f314b8b
180 user: test
180 user: test
181 date: Thu Jan 01 00:00:00 1970 +0000
181 date: Thu Jan 01 00:00:00 1970 +0000
182 summary: a
182 summary: a
183
183
184
184
185 $ hg cat e
185 $ hg cat e
186 a
186 a
187
187
188 Stripping necessary commits should not break --abort
188 Stripping necessary commits should not break --abort
189
189
190 $ hg histedit 1a60820cd1f6 --commands - 2>&1 << EOF| fixbundle
190 $ hg histedit 1a60820cd1f6 --commands - 2>&1 << EOF| fixbundle
191 > edit 1a60820cd1f6 wat
191 > edit 1a60820cd1f6 wat
192 > pick a5e1ba2f7afb foobaz
192 > pick a5e1ba2f7afb foobaz
193 > pick b5f70786f9b0 g
193 > pick b5f70786f9b0 g
194 > EOF
194 > EOF
195 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
195 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
196 Make changes as needed, you may commit or record as needed now.
196 Make changes as needed, you may commit or record as needed now.
197 When you are finished, run hg histedit --continue to resume.
197 When you are finished, run hg histedit --continue to resume.
198
198
199 $ mv .hg/histedit-state .hg/histedit-state.bak
199 $ mv .hg/histedit-state .hg/histedit-state.bak
200 $ hg strip -q -r b5f70786f9b0
200 $ hg strip -q -r b5f70786f9b0
201 $ mv .hg/histedit-state.bak .hg/histedit-state
201 $ mv .hg/histedit-state.bak .hg/histedit-state
202 $ hg histedit --abort
202 $ hg histedit --abort
203 adding changesets
203 adding changesets
204 adding manifests
204 adding manifests
205 adding file changes
205 adding file changes
206 added 1 changesets with 1 changes to 3 files
206 added 1 changesets with 1 changes to 3 files
207 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
207 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
208 $ hg log -r .
208 $ hg log -r .
209 changeset: 6:b5f70786f9b0
209 changeset: 6:b5f70786f9b0
210 tag: tip
210 tag: tip
211 user: test
211 user: test
212 date: Thu Jan 01 00:00:00 1970 +0000
212 date: Thu Jan 01 00:00:00 1970 +0000
213 summary: f
213 summary: f
214
214
215
215
216 check histedit_source
216 check histedit_source
217
217
218 $ hg log --debug --rev 5
218 $ hg log --debug --rev 5
219 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
219 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
220 phase: draft
220 phase: draft
221 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
221 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
222 parent: -1:0000000000000000000000000000000000000000
222 parent: -1:0000000000000000000000000000000000000000
223 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
223 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
224 user: test
224 user: test
225 date: Thu Jan 01 00:00:00 1970 +0000
225 date: Thu Jan 01 00:00:00 1970 +0000
226 files: e
226 files: e
227 extra: branch=default
227 extra: branch=default
228 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
228 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
229 description:
229 description:
230 foobaz
230 foobaz
231
231
232
232
233
233
234 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
234 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
235 > edit b5f70786f9b0 f
235 > edit b5f70786f9b0 f
236 > EOF
236 > EOF
237 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
237 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
238 Make changes as needed, you may commit or record as needed now.
238 Make changes as needed, you may commit or record as needed now.
239 When you are finished, run hg histedit --continue to resume.
239 When you are finished, run hg histedit --continue to resume.
240 $ hg status
240 $ hg status
241 A f
241 A f
242
242
243 $ hg summary
243 $ hg summary
244 parent: 5:a5e1ba2f7afb
244 parent: 5:a5e1ba2f7afb
245 foobaz
245 foobaz
246 branch: default
246 branch: default
247 commit: 1 added (new branch head)
247 commit: 1 added (new branch head)
248 update: 1 new changesets (update)
248 update: 1 new changesets (update)
249 phases: 7 draft
249 phases: 7 draft
250 hist: 1 remaining (histedit --continue)
250 hist: 1 remaining (histedit --continue)
251
251
252 (test also that editor is invoked if histedit is continued for
252 (test also that editor is invoked if histedit is continued for
253 "edit" action)
253 "edit" action)
254
254
255 $ HGEDITOR='cat' hg histedit --continue
255 $ HGEDITOR='cat' hg histedit --continue
256 f
256 f
257
257
258
258
259 HG: Enter commit message. Lines beginning with 'HG:' are removed.
259 HG: Enter commit message. Lines beginning with 'HG:' are removed.
260 HG: Leave message empty to abort commit.
260 HG: Leave message empty to abort commit.
261 HG: --
261 HG: --
262 HG: user: test
262 HG: user: test
263 HG: branch 'default'
263 HG: branch 'default'
264 HG: added f
264 HG: added f
265 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
265 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
266 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-backup.hg (glob)
266 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-backup.hg (glob)
267
267
268 $ hg status
268 $ hg status
269
269
270 log after edit
270 log after edit
271 $ hg log --limit 1
271 $ hg log --limit 1
272 changeset: 6:a107ee126658
272 changeset: 6:a107ee126658
273 tag: tip
273 tag: tip
274 user: test
274 user: test
275 date: Thu Jan 01 00:00:00 1970 +0000
275 date: Thu Jan 01 00:00:00 1970 +0000
276 summary: f
276 summary: f
277
277
278
278
279 say we'll change the message, but don't.
279 say we'll change the message, but don't.
280 $ cat > ../edit.sh <<EOF
280 $ cat > ../edit.sh <<EOF
281 > cat "\$1" | sed s/pick/mess/ > tmp
281 > cat "\$1" | sed s/pick/mess/ > tmp
282 > mv tmp "\$1"
282 > mv tmp "\$1"
283 > EOF
283 > EOF
284 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
284 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
285 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
285 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
286 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
286 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
287 $ hg status
287 $ hg status
288 $ hg log --limit 1
288 $ hg log --limit 1
289 changeset: 6:1fd3b2fe7754
289 changeset: 6:1fd3b2fe7754
290 tag: tip
290 tag: tip
291 user: test
291 user: test
292 date: Thu Jan 01 00:00:00 1970 +0000
292 date: Thu Jan 01 00:00:00 1970 +0000
293 summary: f
293 summary: f
294
294
295
295
296 modify the message
296 modify the message
297
297
298 check saving last-message.txt, at first
298 check saving last-message.txt, at first
299
299
300 $ cat > $TESTTMP/commitfailure.py <<EOF
300 $ cat > $TESTTMP/commitfailure.py <<EOF
301 > from mercurial import error
301 > from mercurial import error
302 > def reposetup(ui, repo):
302 > def reposetup(ui, repo):
303 > class commitfailure(repo.__class__):
303 > class commitfailure(repo.__class__):
304 > def commit(self, *args, **kwargs):
304 > def commit(self, *args, **kwargs):
305 > raise error.Abort('emulating unexpected abort')
305 > raise error.Abort('emulating unexpected abort')
306 > repo.__class__ = commitfailure
306 > repo.__class__ = commitfailure
307 > EOF
307 > EOF
308 $ cat >> .hg/hgrc <<EOF
308 $ cat >> .hg/hgrc <<EOF
309 > [extensions]
309 > [extensions]
310 > # this failure occurs before editor invocation
310 > # this failure occurs before editor invocation
311 > commitfailure = $TESTTMP/commitfailure.py
311 > commitfailure = $TESTTMP/commitfailure.py
312 > EOF
312 > EOF
313
313
314 $ cat > $TESTTMP/editor.sh <<EOF
314 $ cat > $TESTTMP/editor.sh <<EOF
315 > echo "==== before editing"
315 > echo "==== before editing"
316 > cat \$1
316 > cat \$1
317 > echo "===="
317 > echo "===="
318 > echo "check saving last-message.txt" >> \$1
318 > echo "check saving last-message.txt" >> \$1
319 > EOF
319 > EOF
320
320
321 (test that editor is not invoked before transaction starting)
321 (test that editor is not invoked before transaction starting)
322
322
323 $ rm -f .hg/last-message.txt
323 $ rm -f .hg/last-message.txt
324 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
324 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
325 > mess 1fd3b2fe7754 f
325 > mess 1fd3b2fe7754 f
326 > EOF
326 > EOF
327 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
327 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
328 abort: emulating unexpected abort
328 abort: emulating unexpected abort
329 $ test -f .hg/last-message.txt
329 $ test -f .hg/last-message.txt
330 [1]
330 [1]
331
331
332 $ cat >> .hg/hgrc <<EOF
332 $ cat >> .hg/hgrc <<EOF
333 > [extensions]
333 > [extensions]
334 > commitfailure = !
334 > commitfailure = !
335 > EOF
335 > EOF
336 $ hg histedit --abort -q
336 $ hg histedit --abort -q
337
337
338 (test that editor is invoked and commit message is saved into
338 (test that editor is invoked and commit message is saved into
339 "last-message.txt")
339 "last-message.txt")
340
340
341 $ cat >> .hg/hgrc <<EOF
341 $ cat >> .hg/hgrc <<EOF
342 > [hooks]
342 > [hooks]
343 > # this failure occurs after editor invocation
343 > # this failure occurs after editor invocation
344 > pretxncommit.unexpectedabort = false
344 > pretxncommit.unexpectedabort = false
345 > EOF
345 > EOF
346
346
347 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
347 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
348 A f
348 A f
349
349
350 $ rm -f .hg/last-message.txt
350 $ rm -f .hg/last-message.txt
351 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
351 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
352 > mess 1fd3b2fe7754 f
352 > mess 1fd3b2fe7754 f
353 > EOF
353 > EOF
354 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
354 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
355 adding f
355 adding f
356 ==== before editing
356 ==== before editing
357 f
357 f
358
358
359
359
360 HG: Enter commit message. Lines beginning with 'HG:' are removed.
360 HG: Enter commit message. Lines beginning with 'HG:' are removed.
361 HG: Leave message empty to abort commit.
361 HG: Leave message empty to abort commit.
362 HG: --
362 HG: --
363 HG: user: test
363 HG: user: test
364 HG: branch 'default'
364 HG: branch 'default'
365 HG: added f
365 HG: added f
366 ====
366 ====
367 note: commit message saved in .hg/last-message.txt
367 note: commit message saved in .hg/last-message.txt
368 transaction abort!
368 transaction abort!
369 rollback completed
369 rollback completed
370 abort: pretxncommit.unexpectedabort hook exited with status 1
370 abort: pretxncommit.unexpectedabort hook exited with status 1
371 [255]
371 [255]
372 $ cat .hg/last-message.txt
372 $ cat .hg/last-message.txt
373 f
373 f
374
374
375
375
376 check saving last-message.txt
376 check saving last-message.txt
377
377
378 (test also that editor is invoked if histedit is continued for "message"
378 (test also that editor is invoked if histedit is continued for "message"
379 action)
379 action)
380
380
381 $ HGEDITOR=cat hg histedit --continue
381 $ HGEDITOR=cat hg histedit --continue
382 f
382 f
383
383
384
384
385 HG: Enter commit message. Lines beginning with 'HG:' are removed.
385 HG: Enter commit message. Lines beginning with 'HG:' are removed.
386 HG: Leave message empty to abort commit.
386 HG: Leave message empty to abort commit.
387 HG: --
387 HG: --
388 HG: user: test
388 HG: user: test
389 HG: branch 'default'
389 HG: branch 'default'
390 HG: added f
390 HG: added f
391 note: commit message saved in .hg/last-message.txt
391 note: commit message saved in .hg/last-message.txt
392 transaction abort!
392 transaction abort!
393 rollback completed
393 rollback completed
394 abort: pretxncommit.unexpectedabort hook exited with status 1
394 abort: pretxncommit.unexpectedabort hook exited with status 1
395 [255]
395 [255]
396
396
397 $ cat >> .hg/hgrc <<EOF
397 $ cat >> .hg/hgrc <<EOF
398 > [hooks]
398 > [hooks]
399 > pretxncommit.unexpectedabort =
399 > pretxncommit.unexpectedabort =
400 > EOF
400 > EOF
401 $ hg histedit --abort -q
401 $ hg histedit --abort -q
402
402
403 then, check "modify the message" itself
403 then, check "modify the message" itself
404
404
405 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
405 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
406 > mess 1fd3b2fe7754 f
406 > mess 1fd3b2fe7754 f
407 > EOF
407 > EOF
408 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
408 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
409 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
409 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
410 $ hg status
410 $ hg status
411 $ hg log --limit 1
411 $ hg log --limit 1
412 changeset: 6:62feedb1200e
412 changeset: 6:62feedb1200e
413 tag: tip
413 tag: tip
414 user: test
414 user: test
415 date: Thu Jan 01 00:00:00 1970 +0000
415 date: Thu Jan 01 00:00:00 1970 +0000
416 summary: f
416 summary: f
417
417
418
418
419 rollback should not work after a histedit
419 rollback should not work after a histedit
420 $ hg rollback
420 $ hg rollback
421 no rollback information available
421 no rollback information available
422 [1]
422 [1]
423
423
424 $ cd ..
424 $ cd ..
425 $ hg clone -qr0 r r0
425 $ hg clone -qr0 r r0
426 $ cd r0
426 $ cd r0
427 $ hg phase -fdr0
427 $ hg phase -fdr0
428 $ hg histedit --commands - 0 2>&1 << EOF
428 $ hg histedit --commands - 0 2>&1 << EOF
429 > edit cb9a9f314b8b a > $EDITED
429 > edit cb9a9f314b8b a > $EDITED
430 > EOF
430 > EOF
431 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
431 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
432 adding a
432 adding a
433 Make changes as needed, you may commit or record as needed now.
433 Make changes as needed, you may commit or record as needed now.
434 When you are finished, run hg histedit --continue to resume.
434 When you are finished, run hg histedit --continue to resume.
435 [1]
435 [1]
436 $ HGEDITOR=true hg histedit --continue
436 $ HGEDITOR=true hg histedit --continue
437 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
437 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
438 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-backup.hg (glob)
438 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-backup.hg (glob)
439
439
440 $ hg log -G
440 $ hg log -G
441 @ changeset: 0:0efcea34f18a
441 @ changeset: 0:0efcea34f18a
442 tag: tip
442 tag: tip
443 user: test
443 user: test
444 date: Thu Jan 01 00:00:00 1970 +0000
444 date: Thu Jan 01 00:00:00 1970 +0000
445 summary: a
445 summary: a
446
446
447 $ echo foo >> b
447 $ echo foo >> b
448 $ hg addr
448 $ hg addr
449 adding b
449 adding b
450 $ hg ci -m 'add b'
450 $ hg ci -m 'add b'
451 $ echo foo >> a
451 $ echo foo >> a
452 $ hg ci -m 'extend a'
452 $ hg ci -m 'extend a'
453 $ hg phase --public 1
453 $ hg phase --public 1
454 Attempting to fold a change into a public change should not work:
454 Attempting to fold a change into a public change should not work:
455 $ cat > ../edit.sh <<EOF
455 $ cat > ../edit.sh <<EOF
456 > cat "\$1" | sed s/pick/fold/ > tmp
456 > cat "\$1" | sed s/pick/fold/ > tmp
457 > mv tmp "\$1"
457 > mv tmp "\$1"
458 > EOF
458 > EOF
459 $ HGEDITOR="sh ../edit.sh" hg histedit 2
459 $ HGEDITOR="sh ../edit.sh" hg histedit 2
460 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
460 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
461 reverting a
461 reverting a
462 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
462 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
463 warning: histedit rules saved to: .hg/histedit-last-edit.txt
463 abort: cannot fold into public change 18aa70c8ad22
464 abort: cannot fold into public change 18aa70c8ad22
464 [255]
465 [255]
466 $ cat .hg/histedit-last-edit.txt
467 fold 0012be4a27ea 2 extend a
468
469 # Edit history between 0012be4a27ea and 0012be4a27ea
470 #
471 # Commits are listed from least to most recent
472 #
473 # Commands:
474 # p, fold = use commit
475 # e, edit = use commit, but stop for amending
476 # f, fold = use commit, but combine it with the one above
477 # r, roll = like fold, but discard this commit's description
478 # d, drop = remove commit from history
479 # m, mess = edit commit message without changing commit content
480 #
465 TODO: this abort shouldn't be required, but it is for now to leave the repo in
481 TODO: this abort shouldn't be required, but it is for now to leave the repo in
466 a clean state.
482 a clean state.
467 $ hg histedit --abort
483 $ hg histedit --abort
General Comments 0
You need to be logged in to leave comments. Login now