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