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