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