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