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