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