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