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