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