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