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