##// END OF EJS Templates
histedit: use samefile function from cmdutil...
Hannes Oldenburg -
r29820:e1a4015f default
parent child Browse files
Show More
@@ -1,1643 +1,1632 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commits are listed from least to most recent
33 # Commits are listed from least to most recent
34 #
34 #
35 # Commands:
35 # Commands:
36 # p, pick = use commit
36 # p, pick = use commit
37 # e, edit = use commit, but stop for amending
37 # e, edit = use commit, but stop for amending
38 # f, fold = use commit, but combine it with the one above
38 # f, fold = use commit, but combine it with the one above
39 # r, roll = like fold, but discard this commit's description
39 # r, roll = like fold, but discard this commit's description
40 # d, drop = remove commit from history
40 # d, drop = remove commit from history
41 # m, mess = edit commit message without changing commit content
41 # m, mess = edit commit message without changing commit content
42 #
42 #
43
43
44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 for each revision in your history. For example, if you had meant to add gamma
45 for each revision in your history. For example, if you had meant to add gamma
46 before beta, and then wanted to add delta in the same revision as beta, you
46 before beta, and then wanted to add delta in the same revision as beta, you
47 would reorganize the file to look like this::
47 would reorganize the file to look like this::
48
48
49 pick 030b686bedc4 Add gamma
49 pick 030b686bedc4 Add gamma
50 pick c561b4e977df Add beta
50 pick c561b4e977df Add beta
51 fold 7c2fd3b9020c Add delta
51 fold 7c2fd3b9020c Add delta
52
52
53 # Edit history between c561b4e977df and 7c2fd3b9020c
53 # Edit history between c561b4e977df and 7c2fd3b9020c
54 #
54 #
55 # Commits are listed from least to most recent
55 # Commits are listed from least to most recent
56 #
56 #
57 # Commands:
57 # Commands:
58 # p, pick = use commit
58 # p, pick = use commit
59 # e, edit = use commit, but stop for amending
59 # e, edit = use commit, but stop for amending
60 # f, fold = use commit, but combine it with the one above
60 # f, fold = use commit, but combine it with the one above
61 # r, roll = like fold, but discard this commit's description
61 # r, roll = like fold, but discard this commit's description
62 # d, drop = remove commit from history
62 # d, drop = remove commit from history
63 # m, mess = edit commit message without changing commit content
63 # m, mess = edit commit message without changing commit content
64 #
64 #
65
65
66 At which point you close the editor and ``histedit`` starts working. When you
66 At which point you close the editor and ``histedit`` starts working. When you
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 those revisions together, offering you a chance to clean up the commit message::
68 those revisions together, offering you a chance to clean up the commit message::
69
69
70 Add beta
70 Add beta
71 ***
71 ***
72 Add delta
72 Add delta
73
73
74 Edit the commit message to your liking, then close the editor. For
74 Edit the commit message to your liking, then close the editor. For
75 this example, let's assume that the commit message was changed to
75 this example, let's assume that the commit message was changed to
76 ``Add beta and delta.`` After histedit has run and had a chance to
76 ``Add beta and delta.`` After histedit has run and had a chance to
77 remove any old or temporary revisions it needed, the history looks
77 remove any old or temporary revisions it needed, the history looks
78 like this::
78 like this::
79
79
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 | Add beta and delta.
81 | Add beta and delta.
82 |
82 |
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 | Add gamma
84 | Add gamma
85 |
85 |
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 Add alpha
87 Add alpha
88
88
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 ones) until after it has completed all the editing operations, so it will
90 ones) until after it has completed all the editing operations, so it will
91 probably perform several strip operations when it's done. For the above example,
91 probably perform several strip operations when it's done. For the above example,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 so you might need to be a little patient. You can choose to keep the original
93 so you might need to be a little patient. You can choose to keep the original
94 revisions by passing the ``--keep`` flag.
94 revisions by passing the ``--keep`` flag.
95
95
96 The ``edit`` operation will drop you back to a command prompt,
96 The ``edit`` operation will drop you back to a command prompt,
97 allowing you to edit files freely, or even use ``hg record`` to commit
97 allowing you to edit files freely, or even use ``hg record`` to commit
98 some changes as a separate commit. When you're done, any remaining
98 some changes as a separate commit. When you're done, any remaining
99 uncommitted changes will be committed as well. When done, run ``hg
99 uncommitted changes will be committed as well. When done, run ``hg
100 histedit --continue`` to finish this step. You'll be prompted for a
100 histedit --continue`` to finish this step. You'll be prompted for a
101 new commit message, but the default commit message will be the
101 new commit message, but the default commit message will be the
102 original message for the ``edit`` ed revision.
102 original message for the ``edit`` ed revision.
103
103
104 The ``message`` operation will give you a chance to revise a commit
104 The ``message`` operation will give you a chance to revise a commit
105 message without changing the contents. It's a shortcut for doing
105 message without changing the contents. It's a shortcut for doing
106 ``edit`` immediately followed by `hg histedit --continue``.
106 ``edit`` immediately followed by `hg histedit --continue``.
107
107
108 If ``histedit`` encounters a conflict when moving a revision (while
108 If ``histedit`` encounters a conflict when moving a revision (while
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 ``edit`` with the difference that it won't prompt you for a commit
110 ``edit`` with the difference that it won't prompt you for a commit
111 message when done. If you decide at this point that you don't like how
111 message when done. If you decide at this point that you don't like how
112 much work it will be to rearrange history, or that you made a mistake,
112 much work it will be to rearrange history, or that you made a mistake,
113 you can use ``hg histedit --abort`` to abandon the new changes you
113 you can use ``hg histedit --abort`` to abandon the new changes you
114 have made and return to the state before you attempted to edit your
114 have made and return to the state before you attempted to edit your
115 history.
115 history.
116
116
117 If we clone the histedit-ed example repository above and add four more
117 If we clone the histedit-ed example repository above and add four more
118 changes, such that we have the following history::
118 changes, such that we have the following history::
119
119
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 | Add theta
121 | Add theta
122 |
122 |
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 | Add eta
124 | Add eta
125 |
125 |
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 | Add zeta
127 | Add zeta
128 |
128 |
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 | Add epsilon
130 | Add epsilon
131 |
131 |
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 | Add beta and delta.
133 | Add beta and delta.
134 |
134 |
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 | Add gamma
136 | Add gamma
137 |
137 |
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 Add alpha
139 Add alpha
140
140
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 as running ``hg histedit 836302820282``. If you need plan to push to a
142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 repository that Mercurial does not detect to be related to the source
143 repository that Mercurial does not detect to be related to the source
144 repo, you can add a ``--force`` option.
144 repo, you can add a ``--force`` option.
145
145
146 Config
146 Config
147 ------
147 ------
148
148
149 Histedit rule lines are truncated to 80 characters by default. You
149 Histedit rule lines are truncated to 80 characters by default. You
150 can customize this behavior by setting a different length in your
150 can customize this behavior by setting a different length in your
151 configuration file::
151 configuration file::
152
152
153 [histedit]
153 [histedit]
154 linelen = 120 # truncate rule lines at 120 characters
154 linelen = 120 # truncate rule lines at 120 characters
155
155
156 ``hg histedit`` attempts to automatically choose an appropriate base
156 ``hg histedit`` attempts to automatically choose an appropriate base
157 revision to use. To change which base revision is used, define a
157 revision to use. To change which base revision is used, define a
158 revset in your configuration file::
158 revset in your configuration file::
159
159
160 [histedit]
160 [histedit]
161 defaultrev = only(.) & draft()
161 defaultrev = only(.) & draft()
162
162
163 By default each edited revision needs to be present in histedit commands.
163 By default each edited revision needs to be present in histedit commands.
164 To remove revision you need to use ``drop`` operation. You can configure
164 To remove revision you need to use ``drop`` operation. You can configure
165 the drop to be implicit for missing commits by adding::
165 the drop to be implicit for missing commits by adding::
166
166
167 [histedit]
167 [histedit]
168 dropmissing = True
168 dropmissing = True
169
169
170 """
170 """
171
171
172 from __future__ import absolute_import
172 from __future__ import absolute_import
173
173
174 import errno
174 import errno
175 import os
175 import os
176 import sys
176 import sys
177
177
178 from mercurial.i18n import _
178 from mercurial.i18n import _
179 from mercurial import (
179 from mercurial import (
180 bundle2,
180 bundle2,
181 cmdutil,
181 cmdutil,
182 context,
182 context,
183 copies,
183 copies,
184 destutil,
184 destutil,
185 discovery,
185 discovery,
186 error,
186 error,
187 exchange,
187 exchange,
188 extensions,
188 extensions,
189 hg,
189 hg,
190 lock,
190 lock,
191 merge as mergemod,
191 merge as mergemod,
192 node,
192 node,
193 obsolete,
193 obsolete,
194 repair,
194 repair,
195 scmutil,
195 scmutil,
196 util,
196 util,
197 )
197 )
198
198
199 pickle = util.pickle
199 pickle = util.pickle
200 release = lock.release
200 release = lock.release
201 cmdtable = {}
201 cmdtable = {}
202 command = cmdutil.command(cmdtable)
202 command = cmdutil.command(cmdtable)
203
203
204 class _constraints(object):
204 class _constraints(object):
205 # aborts if there are multiple rules for one node
205 # aborts if there are multiple rules for one node
206 noduplicates = 'noduplicates'
206 noduplicates = 'noduplicates'
207 # abort if the node does belong to edited stack
207 # abort if the node does belong to edited stack
208 forceother = 'forceother'
208 forceother = 'forceother'
209 # abort if the node doesn't belong to edited stack
209 # abort if the node doesn't belong to edited stack
210 noother = 'noother'
210 noother = 'noother'
211
211
212 @classmethod
212 @classmethod
213 def known(cls):
213 def known(cls):
214 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
214 return set([v for k, v in cls.__dict__.items() if k[0] != '_'])
215
215
216 # Note for extension authors: ONLY specify testedwith = 'internal' for
216 # Note for extension authors: ONLY specify testedwith = 'internal' for
217 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
217 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
218 # be specifying the version(s) of Mercurial they are tested with, or
218 # be specifying the version(s) of Mercurial they are tested with, or
219 # leave the attribute unspecified.
219 # leave the attribute unspecified.
220 testedwith = 'internal'
220 testedwith = 'internal'
221
221
222 actiontable = {}
222 actiontable = {}
223 primaryactions = set()
223 primaryactions = set()
224 secondaryactions = set()
224 secondaryactions = set()
225 tertiaryactions = set()
225 tertiaryactions = set()
226 internalactions = set()
226 internalactions = set()
227
227
228 def geteditcomment(ui, first, last):
228 def geteditcomment(ui, first, last):
229 """ construct the editor comment
229 """ construct the editor comment
230 The comment includes::
230 The comment includes::
231 - an intro
231 - an intro
232 - sorted primary commands
232 - sorted primary commands
233 - sorted short commands
233 - sorted short commands
234 - sorted long commands
234 - sorted long commands
235 - additional hints
235 - additional hints
236
236
237 Commands are only included once.
237 Commands are only included once.
238 """
238 """
239 intro = _("""Edit history between %s and %s
239 intro = _("""Edit history between %s and %s
240
240
241 Commits are listed from least to most recent
241 Commits are listed from least to most recent
242
242
243 You can reorder changesets by reordering the lines
243 You can reorder changesets by reordering the lines
244
244
245 Commands:
245 Commands:
246 """)
246 """)
247 actions = []
247 actions = []
248 def addverb(v):
248 def addverb(v):
249 a = actiontable[v]
249 a = actiontable[v]
250 lines = a.message.split("\n")
250 lines = a.message.split("\n")
251 if len(a.verbs):
251 if len(a.verbs):
252 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
252 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
253 actions.append(" %s = %s" % (v, lines[0]))
253 actions.append(" %s = %s" % (v, lines[0]))
254 actions.extend([' %s' for l in lines[1:]])
254 actions.extend([' %s' for l in lines[1:]])
255
255
256 for v in (
256 for v in (
257 sorted(primaryactions) +
257 sorted(primaryactions) +
258 sorted(secondaryactions) +
258 sorted(secondaryactions) +
259 sorted(tertiaryactions)
259 sorted(tertiaryactions)
260 ):
260 ):
261 addverb(v)
261 addverb(v)
262 actions.append('')
262 actions.append('')
263
263
264 hints = []
264 hints = []
265 if ui.configbool('histedit', 'dropmissing'):
265 if ui.configbool('histedit', 'dropmissing'):
266 hints.append("Deleting a changeset from the list "
266 hints.append("Deleting a changeset from the list "
267 "will DISCARD it from the edited history!")
267 "will DISCARD it from the edited history!")
268
268
269 lines = (intro % (first, last)).split('\n') + actions + hints
269 lines = (intro % (first, last)).split('\n') + actions + hints
270
270
271 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
271 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
272
272
273 class histeditstate(object):
273 class histeditstate(object):
274 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
274 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
275 topmost=None, replacements=None, lock=None, wlock=None):
275 topmost=None, replacements=None, lock=None, wlock=None):
276 self.repo = repo
276 self.repo = repo
277 self.actions = actions
277 self.actions = actions
278 self.keep = keep
278 self.keep = keep
279 self.topmost = topmost
279 self.topmost = topmost
280 self.parentctxnode = parentctxnode
280 self.parentctxnode = parentctxnode
281 self.lock = lock
281 self.lock = lock
282 self.wlock = wlock
282 self.wlock = wlock
283 self.backupfile = None
283 self.backupfile = None
284 if replacements is None:
284 if replacements is None:
285 self.replacements = []
285 self.replacements = []
286 else:
286 else:
287 self.replacements = replacements
287 self.replacements = replacements
288
288
289 def read(self):
289 def read(self):
290 """Load histedit state from disk and set fields appropriately."""
290 """Load histedit state from disk and set fields appropriately."""
291 try:
291 try:
292 state = self.repo.vfs.read('histedit-state')
292 state = self.repo.vfs.read('histedit-state')
293 except IOError as err:
293 except IOError as err:
294 if err.errno != errno.ENOENT:
294 if err.errno != errno.ENOENT:
295 raise
295 raise
296 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
296 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
297
297
298 if state.startswith('v1\n'):
298 if state.startswith('v1\n'):
299 data = self._load()
299 data = self._load()
300 parentctxnode, rules, keep, topmost, replacements, backupfile = data
300 parentctxnode, rules, keep, topmost, replacements, backupfile = data
301 else:
301 else:
302 data = pickle.loads(state)
302 data = pickle.loads(state)
303 parentctxnode, rules, keep, topmost, replacements = data
303 parentctxnode, rules, keep, topmost, replacements = data
304 backupfile = None
304 backupfile = None
305
305
306 self.parentctxnode = parentctxnode
306 self.parentctxnode = parentctxnode
307 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
307 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
308 actions = parserules(rules, self)
308 actions = parserules(rules, self)
309 self.actions = actions
309 self.actions = actions
310 self.keep = keep
310 self.keep = keep
311 self.topmost = topmost
311 self.topmost = topmost
312 self.replacements = replacements
312 self.replacements = replacements
313 self.backupfile = backupfile
313 self.backupfile = backupfile
314
314
315 def write(self):
315 def write(self):
316 fp = self.repo.vfs('histedit-state', 'w')
316 fp = self.repo.vfs('histedit-state', 'w')
317 fp.write('v1\n')
317 fp.write('v1\n')
318 fp.write('%s\n' % node.hex(self.parentctxnode))
318 fp.write('%s\n' % node.hex(self.parentctxnode))
319 fp.write('%s\n' % node.hex(self.topmost))
319 fp.write('%s\n' % node.hex(self.topmost))
320 fp.write('%s\n' % self.keep)
320 fp.write('%s\n' % self.keep)
321 fp.write('%d\n' % len(self.actions))
321 fp.write('%d\n' % len(self.actions))
322 for action in self.actions:
322 for action in self.actions:
323 fp.write('%s\n' % action.tostate())
323 fp.write('%s\n' % action.tostate())
324 fp.write('%d\n' % len(self.replacements))
324 fp.write('%d\n' % len(self.replacements))
325 for replacement in self.replacements:
325 for replacement in self.replacements:
326 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
326 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
327 for r in replacement[1])))
327 for r in replacement[1])))
328 backupfile = self.backupfile
328 backupfile = self.backupfile
329 if not backupfile:
329 if not backupfile:
330 backupfile = ''
330 backupfile = ''
331 fp.write('%s\n' % backupfile)
331 fp.write('%s\n' % backupfile)
332 fp.close()
332 fp.close()
333
333
334 def _load(self):
334 def _load(self):
335 fp = self.repo.vfs('histedit-state', 'r')
335 fp = self.repo.vfs('histedit-state', 'r')
336 lines = [l[:-1] for l in fp.readlines()]
336 lines = [l[:-1] for l in fp.readlines()]
337
337
338 index = 0
338 index = 0
339 lines[index] # version number
339 lines[index] # version number
340 index += 1
340 index += 1
341
341
342 parentctxnode = node.bin(lines[index])
342 parentctxnode = node.bin(lines[index])
343 index += 1
343 index += 1
344
344
345 topmost = node.bin(lines[index])
345 topmost = node.bin(lines[index])
346 index += 1
346 index += 1
347
347
348 keep = lines[index] == 'True'
348 keep = lines[index] == 'True'
349 index += 1
349 index += 1
350
350
351 # Rules
351 # Rules
352 rules = []
352 rules = []
353 rulelen = int(lines[index])
353 rulelen = int(lines[index])
354 index += 1
354 index += 1
355 for i in xrange(rulelen):
355 for i in xrange(rulelen):
356 ruleaction = lines[index]
356 ruleaction = lines[index]
357 index += 1
357 index += 1
358 rule = lines[index]
358 rule = lines[index]
359 index += 1
359 index += 1
360 rules.append((ruleaction, rule))
360 rules.append((ruleaction, rule))
361
361
362 # Replacements
362 # Replacements
363 replacements = []
363 replacements = []
364 replacementlen = int(lines[index])
364 replacementlen = int(lines[index])
365 index += 1
365 index += 1
366 for i in xrange(replacementlen):
366 for i in xrange(replacementlen):
367 replacement = lines[index]
367 replacement = lines[index]
368 original = node.bin(replacement[:40])
368 original = node.bin(replacement[:40])
369 succ = [node.bin(replacement[i:i + 40]) for i in
369 succ = [node.bin(replacement[i:i + 40]) for i in
370 range(40, len(replacement), 40)]
370 range(40, len(replacement), 40)]
371 replacements.append((original, succ))
371 replacements.append((original, succ))
372 index += 1
372 index += 1
373
373
374 backupfile = lines[index]
374 backupfile = lines[index]
375 index += 1
375 index += 1
376
376
377 fp.close()
377 fp.close()
378
378
379 return parentctxnode, rules, keep, topmost, replacements, backupfile
379 return parentctxnode, rules, keep, topmost, replacements, backupfile
380
380
381 def clear(self):
381 def clear(self):
382 if self.inprogress():
382 if self.inprogress():
383 self.repo.vfs.unlink('histedit-state')
383 self.repo.vfs.unlink('histedit-state')
384
384
385 def inprogress(self):
385 def inprogress(self):
386 return self.repo.vfs.exists('histedit-state')
386 return self.repo.vfs.exists('histedit-state')
387
387
388
388
389 class histeditaction(object):
389 class histeditaction(object):
390 def __init__(self, state, node):
390 def __init__(self, state, node):
391 self.state = state
391 self.state = state
392 self.repo = state.repo
392 self.repo = state.repo
393 self.node = node
393 self.node = node
394
394
395 @classmethod
395 @classmethod
396 def fromrule(cls, state, rule):
396 def fromrule(cls, state, rule):
397 """Parses the given rule, returning an instance of the histeditaction.
397 """Parses the given rule, returning an instance of the histeditaction.
398 """
398 """
399 rulehash = rule.strip().split(' ', 1)[0]
399 rulehash = rule.strip().split(' ', 1)[0]
400 try:
400 try:
401 rev = node.bin(rulehash)
401 rev = node.bin(rulehash)
402 except TypeError:
402 except TypeError:
403 raise error.ParseError("invalid changeset %s" % rulehash)
403 raise error.ParseError("invalid changeset %s" % rulehash)
404 return cls(state, rev)
404 return cls(state, rev)
405
405
406 def verify(self, prev):
406 def verify(self, prev):
407 """ Verifies semantic correctness of the rule"""
407 """ Verifies semantic correctness of the rule"""
408 repo = self.repo
408 repo = self.repo
409 ha = node.hex(self.node)
409 ha = node.hex(self.node)
410 try:
410 try:
411 self.node = repo[ha].node()
411 self.node = repo[ha].node()
412 except error.RepoError:
412 except error.RepoError:
413 raise error.ParseError(_('unknown changeset %s listed')
413 raise error.ParseError(_('unknown changeset %s listed')
414 % ha[:12])
414 % ha[:12])
415
415
416 def torule(self):
416 def torule(self):
417 """build a histedit rule line for an action
417 """build a histedit rule line for an action
418
418
419 by default lines are in the form:
419 by default lines are in the form:
420 <hash> <rev> <summary>
420 <hash> <rev> <summary>
421 """
421 """
422 ctx = self.repo[self.node]
422 ctx = self.repo[self.node]
423 summary = _getsummary(ctx)
423 summary = _getsummary(ctx)
424 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
424 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
425 # trim to 75 columns by default so it's not stupidly wide in my editor
425 # trim to 75 columns by default so it's not stupidly wide in my editor
426 # (the 5 more are left for verb)
426 # (the 5 more are left for verb)
427 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
427 maxlen = self.repo.ui.configint('histedit', 'linelen', default=80)
428 maxlen = max(maxlen, 22) # avoid truncating hash
428 maxlen = max(maxlen, 22) # avoid truncating hash
429 return util.ellipsis(line, maxlen)
429 return util.ellipsis(line, maxlen)
430
430
431 def tostate(self):
431 def tostate(self):
432 """Print an action in format used by histedit state files
432 """Print an action in format used by histedit state files
433 (the first line is a verb, the remainder is the second)
433 (the first line is a verb, the remainder is the second)
434 """
434 """
435 return "%s\n%s" % (self.verb, node.hex(self.node))
435 return "%s\n%s" % (self.verb, node.hex(self.node))
436
436
437 def constraints(self):
437 def constraints(self):
438 """Return a set of constrains that this action should be verified for
438 """Return a set of constrains that this action should be verified for
439 """
439 """
440 return set([_constraints.noduplicates, _constraints.noother])
440 return set([_constraints.noduplicates, _constraints.noother])
441
441
442 def nodetoverify(self):
442 def nodetoverify(self):
443 """Returns a node associated with the action that will be used for
443 """Returns a node associated with the action that will be used for
444 verification purposes.
444 verification purposes.
445
445
446 If the action doesn't correspond to node it should return None
446 If the action doesn't correspond to node it should return None
447 """
447 """
448 return self.node
448 return self.node
449
449
450 def run(self):
450 def run(self):
451 """Runs the action. The default behavior is simply apply the action's
451 """Runs the action. The default behavior is simply apply the action's
452 rulectx onto the current parentctx."""
452 rulectx onto the current parentctx."""
453 self.applychange()
453 self.applychange()
454 self.continuedirty()
454 self.continuedirty()
455 return self.continueclean()
455 return self.continueclean()
456
456
457 def applychange(self):
457 def applychange(self):
458 """Applies the changes from this action's rulectx onto the current
458 """Applies the changes from this action's rulectx onto the current
459 parentctx, but does not commit them."""
459 parentctx, but does not commit them."""
460 repo = self.repo
460 repo = self.repo
461 rulectx = repo[self.node]
461 rulectx = repo[self.node]
462 repo.ui.pushbuffer(error=True, labeled=True)
462 repo.ui.pushbuffer(error=True, labeled=True)
463 hg.update(repo, self.state.parentctxnode, quietempty=True)
463 hg.update(repo, self.state.parentctxnode, quietempty=True)
464 stats = applychanges(repo.ui, repo, rulectx, {})
464 stats = applychanges(repo.ui, repo, rulectx, {})
465 if stats and stats[3] > 0:
465 if stats and stats[3] > 0:
466 buf = repo.ui.popbuffer()
466 buf = repo.ui.popbuffer()
467 repo.ui.write(*buf)
467 repo.ui.write(*buf)
468 raise error.InterventionRequired(
468 raise error.InterventionRequired(
469 _('Fix up the change (%s %s)') %
469 _('Fix up the change (%s %s)') %
470 (self.verb, node.short(self.node)),
470 (self.verb, node.short(self.node)),
471 hint=_('hg histedit --continue to resume'))
471 hint=_('hg histedit --continue to resume'))
472 else:
472 else:
473 repo.ui.popbuffer()
473 repo.ui.popbuffer()
474
474
475 def continuedirty(self):
475 def continuedirty(self):
476 """Continues the action when changes have been applied to the working
476 """Continues the action when changes have been applied to the working
477 copy. The default behavior is to commit the dirty changes."""
477 copy. The default behavior is to commit the dirty changes."""
478 repo = self.repo
478 repo = self.repo
479 rulectx = repo[self.node]
479 rulectx = repo[self.node]
480
480
481 editor = self.commiteditor()
481 editor = self.commiteditor()
482 commit = commitfuncfor(repo, rulectx)
482 commit = commitfuncfor(repo, rulectx)
483
483
484 commit(text=rulectx.description(), user=rulectx.user(),
484 commit(text=rulectx.description(), user=rulectx.user(),
485 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
485 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
486
486
487 def commiteditor(self):
487 def commiteditor(self):
488 """The editor to be used to edit the commit message."""
488 """The editor to be used to edit the commit message."""
489 return False
489 return False
490
490
491 def continueclean(self):
491 def continueclean(self):
492 """Continues the action when the working copy is clean. The default
492 """Continues the action when the working copy is clean. The default
493 behavior is to accept the current commit as the new version of the
493 behavior is to accept the current commit as the new version of the
494 rulectx."""
494 rulectx."""
495 ctx = self.repo['.']
495 ctx = self.repo['.']
496 if ctx.node() == self.state.parentctxnode:
496 if ctx.node() == self.state.parentctxnode:
497 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
497 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
498 node.short(self.node))
498 node.short(self.node))
499 return ctx, [(self.node, tuple())]
499 return ctx, [(self.node, tuple())]
500 if ctx.node() == self.node:
500 if ctx.node() == self.node:
501 # Nothing changed
501 # Nothing changed
502 return ctx, []
502 return ctx, []
503 return ctx, [(self.node, (ctx.node(),))]
503 return ctx, [(self.node, (ctx.node(),))]
504
504
505 def commitfuncfor(repo, src):
505 def commitfuncfor(repo, src):
506 """Build a commit function for the replacement of <src>
506 """Build a commit function for the replacement of <src>
507
507
508 This function ensure we apply the same treatment to all changesets.
508 This function ensure we apply the same treatment to all changesets.
509
509
510 - Add a 'histedit_source' entry in extra.
510 - Add a 'histedit_source' entry in extra.
511
511
512 Note that fold has its own separated logic because its handling is a bit
512 Note that fold has its own separated logic because its handling is a bit
513 different and not easily factored out of the fold method.
513 different and not easily factored out of the fold method.
514 """
514 """
515 phasemin = src.phase()
515 phasemin = src.phase()
516 def commitfunc(**kwargs):
516 def commitfunc(**kwargs):
517 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
517 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
518 try:
518 try:
519 repo.ui.setconfig('phases', 'new-commit', phasemin,
519 repo.ui.setconfig('phases', 'new-commit', phasemin,
520 'histedit')
520 'histedit')
521 extra = kwargs.get('extra', {}).copy()
521 extra = kwargs.get('extra', {}).copy()
522 extra['histedit_source'] = src.hex()
522 extra['histedit_source'] = src.hex()
523 kwargs['extra'] = extra
523 kwargs['extra'] = extra
524 return repo.commit(**kwargs)
524 return repo.commit(**kwargs)
525 finally:
525 finally:
526 repo.ui.restoreconfig(phasebackup)
526 repo.ui.restoreconfig(phasebackup)
527 return commitfunc
527 return commitfunc
528
528
529 def applychanges(ui, repo, ctx, opts):
529 def applychanges(ui, repo, ctx, opts):
530 """Merge changeset from ctx (only) in the current working directory"""
530 """Merge changeset from ctx (only) in the current working directory"""
531 wcpar = repo.dirstate.parents()[0]
531 wcpar = repo.dirstate.parents()[0]
532 if ctx.p1().node() == wcpar:
532 if ctx.p1().node() == wcpar:
533 # edits are "in place" we do not need to make any merge,
533 # edits are "in place" we do not need to make any merge,
534 # just applies changes on parent for editing
534 # just applies changes on parent for editing
535 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
535 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
536 stats = None
536 stats = None
537 else:
537 else:
538 try:
538 try:
539 # ui.forcemerge is an internal variable, do not document
539 # ui.forcemerge is an internal variable, do not document
540 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
540 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
541 'histedit')
541 'histedit')
542 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
542 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
543 finally:
543 finally:
544 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
544 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
545 return stats
545 return stats
546
546
547 def collapse(repo, first, last, commitopts, skipprompt=False):
547 def collapse(repo, first, last, commitopts, skipprompt=False):
548 """collapse the set of revisions from first to last as new one.
548 """collapse the set of revisions from first to last as new one.
549
549
550 Expected commit options are:
550 Expected commit options are:
551 - message
551 - message
552 - date
552 - date
553 - username
553 - username
554 Commit message is edited in all cases.
554 Commit message is edited in all cases.
555
555
556 This function works in memory."""
556 This function works in memory."""
557 ctxs = list(repo.set('%d::%d', first, last))
557 ctxs = list(repo.set('%d::%d', first, last))
558 if not ctxs:
558 if not ctxs:
559 return None
559 return None
560 for c in ctxs:
560 for c in ctxs:
561 if not c.mutable():
561 if not c.mutable():
562 raise error.ParseError(
562 raise error.ParseError(
563 _("cannot fold into public change %s") % node.short(c.node()))
563 _("cannot fold into public change %s") % node.short(c.node()))
564 base = first.parents()[0]
564 base = first.parents()[0]
565
565
566 # commit a new version of the old changeset, including the update
566 # commit a new version of the old changeset, including the update
567 # collect all files which might be affected
567 # collect all files which might be affected
568 files = set()
568 files = set()
569 for ctx in ctxs:
569 for ctx in ctxs:
570 files.update(ctx.files())
570 files.update(ctx.files())
571
571
572 # Recompute copies (avoid recording a -> b -> a)
572 # Recompute copies (avoid recording a -> b -> a)
573 copied = copies.pathcopies(base, last)
573 copied = copies.pathcopies(base, last)
574
574
575 # prune files which were reverted by the updates
575 # prune files which were reverted by the updates
576 def samefile(f):
576 files = [f for f in files if not cmdutil.samefile(f, last, base)]
577 if f in last.manifest():
578 a = last.filectx(f)
579 if f in base.manifest():
580 b = base.filectx(f)
581 return (a.data() == b.data()
582 and a.flags() == b.flags())
583 else:
584 return False
585 else:
586 return f not in base.manifest()
587 files = [f for f in files if not samefile(f)]
588 # commit version of these files as defined by head
577 # commit version of these files as defined by head
589 headmf = last.manifest()
578 headmf = last.manifest()
590 def filectxfn(repo, ctx, path):
579 def filectxfn(repo, ctx, path):
591 if path in headmf:
580 if path in headmf:
592 fctx = last[path]
581 fctx = last[path]
593 flags = fctx.flags()
582 flags = fctx.flags()
594 mctx = context.memfilectx(repo,
583 mctx = context.memfilectx(repo,
595 fctx.path(), fctx.data(),
584 fctx.path(), fctx.data(),
596 islink='l' in flags,
585 islink='l' in flags,
597 isexec='x' in flags,
586 isexec='x' in flags,
598 copied=copied.get(path))
587 copied=copied.get(path))
599 return mctx
588 return mctx
600 return None
589 return None
601
590
602 if commitopts.get('message'):
591 if commitopts.get('message'):
603 message = commitopts['message']
592 message = commitopts['message']
604 else:
593 else:
605 message = first.description()
594 message = first.description()
606 user = commitopts.get('user')
595 user = commitopts.get('user')
607 date = commitopts.get('date')
596 date = commitopts.get('date')
608 extra = commitopts.get('extra')
597 extra = commitopts.get('extra')
609
598
610 parents = (first.p1().node(), first.p2().node())
599 parents = (first.p1().node(), first.p2().node())
611 editor = None
600 editor = None
612 if not skipprompt:
601 if not skipprompt:
613 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
602 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
614 new = context.memctx(repo,
603 new = context.memctx(repo,
615 parents=parents,
604 parents=parents,
616 text=message,
605 text=message,
617 files=files,
606 files=files,
618 filectxfn=filectxfn,
607 filectxfn=filectxfn,
619 user=user,
608 user=user,
620 date=date,
609 date=date,
621 extra=extra,
610 extra=extra,
622 editor=editor)
611 editor=editor)
623 return repo.commitctx(new)
612 return repo.commitctx(new)
624
613
625 def _isdirtywc(repo):
614 def _isdirtywc(repo):
626 return repo[None].dirty(missing=True)
615 return repo[None].dirty(missing=True)
627
616
628 def abortdirty():
617 def abortdirty():
629 raise error.Abort(_('working copy has pending changes'),
618 raise error.Abort(_('working copy has pending changes'),
630 hint=_('amend, commit, or revert them and run histedit '
619 hint=_('amend, commit, or revert them and run histedit '
631 '--continue, or abort with histedit --abort'))
620 '--continue, or abort with histedit --abort'))
632
621
633 def action(verbs, message, priority=False, internal=False):
622 def action(verbs, message, priority=False, internal=False):
634 def wrap(cls):
623 def wrap(cls):
635 assert not priority or not internal
624 assert not priority or not internal
636 verb = verbs[0]
625 verb = verbs[0]
637 if priority:
626 if priority:
638 primaryactions.add(verb)
627 primaryactions.add(verb)
639 elif internal:
628 elif internal:
640 internalactions.add(verb)
629 internalactions.add(verb)
641 elif len(verbs) > 1:
630 elif len(verbs) > 1:
642 secondaryactions.add(verb)
631 secondaryactions.add(verb)
643 else:
632 else:
644 tertiaryactions.add(verb)
633 tertiaryactions.add(verb)
645
634
646 cls.verb = verb
635 cls.verb = verb
647 cls.verbs = verbs
636 cls.verbs = verbs
648 cls.message = message
637 cls.message = message
649 for verb in verbs:
638 for verb in verbs:
650 actiontable[verb] = cls
639 actiontable[verb] = cls
651 return cls
640 return cls
652 return wrap
641 return wrap
653
642
654 @action(['pick', 'p'],
643 @action(['pick', 'p'],
655 _('use commit'),
644 _('use commit'),
656 priority=True)
645 priority=True)
657 class pick(histeditaction):
646 class pick(histeditaction):
658 def run(self):
647 def run(self):
659 rulectx = self.repo[self.node]
648 rulectx = self.repo[self.node]
660 if rulectx.parents()[0].node() == self.state.parentctxnode:
649 if rulectx.parents()[0].node() == self.state.parentctxnode:
661 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
650 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
662 return rulectx, []
651 return rulectx, []
663
652
664 return super(pick, self).run()
653 return super(pick, self).run()
665
654
666 @action(['edit', 'e'],
655 @action(['edit', 'e'],
667 _('use commit, but stop for amending'),
656 _('use commit, but stop for amending'),
668 priority=True)
657 priority=True)
669 class edit(histeditaction):
658 class edit(histeditaction):
670 def run(self):
659 def run(self):
671 repo = self.repo
660 repo = self.repo
672 rulectx = repo[self.node]
661 rulectx = repo[self.node]
673 hg.update(repo, self.state.parentctxnode, quietempty=True)
662 hg.update(repo, self.state.parentctxnode, quietempty=True)
674 applychanges(repo.ui, repo, rulectx, {})
663 applychanges(repo.ui, repo, rulectx, {})
675 raise error.InterventionRequired(
664 raise error.InterventionRequired(
676 _('Editing (%s), you may commit or record as needed now.')
665 _('Editing (%s), you may commit or record as needed now.')
677 % node.short(self.node),
666 % node.short(self.node),
678 hint=_('hg histedit --continue to resume'))
667 hint=_('hg histedit --continue to resume'))
679
668
680 def commiteditor(self):
669 def commiteditor(self):
681 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
670 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
682
671
683 @action(['fold', 'f'],
672 @action(['fold', 'f'],
684 _('use commit, but combine it with the one above'))
673 _('use commit, but combine it with the one above'))
685 class fold(histeditaction):
674 class fold(histeditaction):
686 def verify(self, prev):
675 def verify(self, prev):
687 """ Verifies semantic correctness of the fold rule"""
676 """ Verifies semantic correctness of the fold rule"""
688 super(fold, self).verify(prev)
677 super(fold, self).verify(prev)
689 repo = self.repo
678 repo = self.repo
690 if not prev:
679 if not prev:
691 c = repo[self.node].parents()[0]
680 c = repo[self.node].parents()[0]
692 elif not prev.verb in ('pick', 'base'):
681 elif not prev.verb in ('pick', 'base'):
693 return
682 return
694 else:
683 else:
695 c = repo[prev.node]
684 c = repo[prev.node]
696 if not c.mutable():
685 if not c.mutable():
697 raise error.ParseError(
686 raise error.ParseError(
698 _("cannot fold into public change %s") % node.short(c.node()))
687 _("cannot fold into public change %s") % node.short(c.node()))
699
688
700
689
701 def continuedirty(self):
690 def continuedirty(self):
702 repo = self.repo
691 repo = self.repo
703 rulectx = repo[self.node]
692 rulectx = repo[self.node]
704
693
705 commit = commitfuncfor(repo, rulectx)
694 commit = commitfuncfor(repo, rulectx)
706 commit(text='fold-temp-revision %s' % node.short(self.node),
695 commit(text='fold-temp-revision %s' % node.short(self.node),
707 user=rulectx.user(), date=rulectx.date(),
696 user=rulectx.user(), date=rulectx.date(),
708 extra=rulectx.extra())
697 extra=rulectx.extra())
709
698
710 def continueclean(self):
699 def continueclean(self):
711 repo = self.repo
700 repo = self.repo
712 ctx = repo['.']
701 ctx = repo['.']
713 rulectx = repo[self.node]
702 rulectx = repo[self.node]
714 parentctxnode = self.state.parentctxnode
703 parentctxnode = self.state.parentctxnode
715 if ctx.node() == parentctxnode:
704 if ctx.node() == parentctxnode:
716 repo.ui.warn(_('%s: empty changeset\n') %
705 repo.ui.warn(_('%s: empty changeset\n') %
717 node.short(self.node))
706 node.short(self.node))
718 return ctx, [(self.node, (parentctxnode,))]
707 return ctx, [(self.node, (parentctxnode,))]
719
708
720 parentctx = repo[parentctxnode]
709 parentctx = repo[parentctxnode]
721 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
710 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
722 parentctx))
711 parentctx))
723 if not newcommits:
712 if not newcommits:
724 repo.ui.warn(_('%s: cannot fold - working copy is not a '
713 repo.ui.warn(_('%s: cannot fold - working copy is not a '
725 'descendant of previous commit %s\n') %
714 'descendant of previous commit %s\n') %
726 (node.short(self.node), node.short(parentctxnode)))
715 (node.short(self.node), node.short(parentctxnode)))
727 return ctx, [(self.node, (ctx.node(),))]
716 return ctx, [(self.node, (ctx.node(),))]
728
717
729 middlecommits = newcommits.copy()
718 middlecommits = newcommits.copy()
730 middlecommits.discard(ctx.node())
719 middlecommits.discard(ctx.node())
731
720
732 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
721 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
733 middlecommits)
722 middlecommits)
734
723
735 def skipprompt(self):
724 def skipprompt(self):
736 """Returns true if the rule should skip the message editor.
725 """Returns true if the rule should skip the message editor.
737
726
738 For example, 'fold' wants to show an editor, but 'rollup'
727 For example, 'fold' wants to show an editor, but 'rollup'
739 doesn't want to.
728 doesn't want to.
740 """
729 """
741 return False
730 return False
742
731
743 def mergedescs(self):
732 def mergedescs(self):
744 """Returns true if the rule should merge messages of multiple changes.
733 """Returns true if the rule should merge messages of multiple changes.
745
734
746 This exists mainly so that 'rollup' rules can be a subclass of
735 This exists mainly so that 'rollup' rules can be a subclass of
747 'fold'.
736 'fold'.
748 """
737 """
749 return True
738 return True
750
739
751 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
740 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
752 parent = ctx.parents()[0].node()
741 parent = ctx.parents()[0].node()
753 repo.ui.pushbuffer()
742 repo.ui.pushbuffer()
754 hg.update(repo, parent)
743 hg.update(repo, parent)
755 repo.ui.popbuffer()
744 repo.ui.popbuffer()
756 ### prepare new commit data
745 ### prepare new commit data
757 commitopts = {}
746 commitopts = {}
758 commitopts['user'] = ctx.user()
747 commitopts['user'] = ctx.user()
759 # commit message
748 # commit message
760 if not self.mergedescs():
749 if not self.mergedescs():
761 newmessage = ctx.description()
750 newmessage = ctx.description()
762 else:
751 else:
763 newmessage = '\n***\n'.join(
752 newmessage = '\n***\n'.join(
764 [ctx.description()] +
753 [ctx.description()] +
765 [repo[r].description() for r in internalchanges] +
754 [repo[r].description() for r in internalchanges] +
766 [oldctx.description()]) + '\n'
755 [oldctx.description()]) + '\n'
767 commitopts['message'] = newmessage
756 commitopts['message'] = newmessage
768 # date
757 # date
769 commitopts['date'] = max(ctx.date(), oldctx.date())
758 commitopts['date'] = max(ctx.date(), oldctx.date())
770 extra = ctx.extra().copy()
759 extra = ctx.extra().copy()
771 # histedit_source
760 # histedit_source
772 # note: ctx is likely a temporary commit but that the best we can do
761 # note: ctx is likely a temporary commit but that the best we can do
773 # here. This is sufficient to solve issue3681 anyway.
762 # here. This is sufficient to solve issue3681 anyway.
774 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
763 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
775 commitopts['extra'] = extra
764 commitopts['extra'] = extra
776 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
765 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
777 try:
766 try:
778 phasemin = max(ctx.phase(), oldctx.phase())
767 phasemin = max(ctx.phase(), oldctx.phase())
779 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
768 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
780 n = collapse(repo, ctx, repo[newnode], commitopts,
769 n = collapse(repo, ctx, repo[newnode], commitopts,
781 skipprompt=self.skipprompt())
770 skipprompt=self.skipprompt())
782 finally:
771 finally:
783 repo.ui.restoreconfig(phasebackup)
772 repo.ui.restoreconfig(phasebackup)
784 if n is None:
773 if n is None:
785 return ctx, []
774 return ctx, []
786 repo.ui.pushbuffer()
775 repo.ui.pushbuffer()
787 hg.update(repo, n)
776 hg.update(repo, n)
788 repo.ui.popbuffer()
777 repo.ui.popbuffer()
789 replacements = [(oldctx.node(), (newnode,)),
778 replacements = [(oldctx.node(), (newnode,)),
790 (ctx.node(), (n,)),
779 (ctx.node(), (n,)),
791 (newnode, (n,)),
780 (newnode, (n,)),
792 ]
781 ]
793 for ich in internalchanges:
782 for ich in internalchanges:
794 replacements.append((ich, (n,)))
783 replacements.append((ich, (n,)))
795 return repo[n], replacements
784 return repo[n], replacements
796
785
797 class base(histeditaction):
786 class base(histeditaction):
798 def constraints(self):
787 def constraints(self):
799 return set([_constraints.forceother])
788 return set([_constraints.forceother])
800
789
801 def run(self):
790 def run(self):
802 if self.repo['.'].node() != self.node:
791 if self.repo['.'].node() != self.node:
803 mergemod.update(self.repo, self.node, False, True)
792 mergemod.update(self.repo, self.node, False, True)
804 # branchmerge, force)
793 # branchmerge, force)
805 return self.continueclean()
794 return self.continueclean()
806
795
807 def continuedirty(self):
796 def continuedirty(self):
808 abortdirty()
797 abortdirty()
809
798
810 def continueclean(self):
799 def continueclean(self):
811 basectx = self.repo['.']
800 basectx = self.repo['.']
812 return basectx, []
801 return basectx, []
813
802
814 @action(['_multifold'],
803 @action(['_multifold'],
815 _(
804 _(
816 """fold subclass used for when multiple folds happen in a row
805 """fold subclass used for when multiple folds happen in a row
817
806
818 We only want to fire the editor for the folded message once when
807 We only want to fire the editor for the folded message once when
819 (say) four changes are folded down into a single change. This is
808 (say) four changes are folded down into a single change. This is
820 similar to rollup, but we should preserve both messages so that
809 similar to rollup, but we should preserve both messages so that
821 when the last fold operation runs we can show the user all the
810 when the last fold operation runs we can show the user all the
822 commit messages in their editor.
811 commit messages in their editor.
823 """),
812 """),
824 internal=True)
813 internal=True)
825 class _multifold(fold):
814 class _multifold(fold):
826 def skipprompt(self):
815 def skipprompt(self):
827 return True
816 return True
828
817
829 @action(["roll", "r"],
818 @action(["roll", "r"],
830 _("like fold, but discard this commit's description"))
819 _("like fold, but discard this commit's description"))
831 class rollup(fold):
820 class rollup(fold):
832 def mergedescs(self):
821 def mergedescs(self):
833 return False
822 return False
834
823
835 def skipprompt(self):
824 def skipprompt(self):
836 return True
825 return True
837
826
838 @action(["drop", "d"],
827 @action(["drop", "d"],
839 _('remove commit from history'))
828 _('remove commit from history'))
840 class drop(histeditaction):
829 class drop(histeditaction):
841 def run(self):
830 def run(self):
842 parentctx = self.repo[self.state.parentctxnode]
831 parentctx = self.repo[self.state.parentctxnode]
843 return parentctx, [(self.node, tuple())]
832 return parentctx, [(self.node, tuple())]
844
833
845 @action(["mess", "m"],
834 @action(["mess", "m"],
846 _('edit commit message without changing commit content'),
835 _('edit commit message without changing commit content'),
847 priority=True)
836 priority=True)
848 class message(histeditaction):
837 class message(histeditaction):
849 def commiteditor(self):
838 def commiteditor(self):
850 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
839 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
851
840
852 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
841 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
853 """utility function to find the first outgoing changeset
842 """utility function to find the first outgoing changeset
854
843
855 Used by initialization code"""
844 Used by initialization code"""
856 if opts is None:
845 if opts is None:
857 opts = {}
846 opts = {}
858 dest = ui.expandpath(remote or 'default-push', remote or 'default')
847 dest = ui.expandpath(remote or 'default-push', remote or 'default')
859 dest, revs = hg.parseurl(dest, None)[:2]
848 dest, revs = hg.parseurl(dest, None)[:2]
860 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
849 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
861
850
862 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
851 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
863 other = hg.peer(repo, opts, dest)
852 other = hg.peer(repo, opts, dest)
864
853
865 if revs:
854 if revs:
866 revs = [repo.lookup(rev) for rev in revs]
855 revs = [repo.lookup(rev) for rev in revs]
867
856
868 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
857 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
869 if not outgoing.missing:
858 if not outgoing.missing:
870 raise error.Abort(_('no outgoing ancestors'))
859 raise error.Abort(_('no outgoing ancestors'))
871 roots = list(repo.revs("roots(%ln)", outgoing.missing))
860 roots = list(repo.revs("roots(%ln)", outgoing.missing))
872 if 1 < len(roots):
861 if 1 < len(roots):
873 msg = _('there are ambiguous outgoing revisions')
862 msg = _('there are ambiguous outgoing revisions')
874 hint = _('see "hg help histedit" for more detail')
863 hint = _('see "hg help histedit" for more detail')
875 raise error.Abort(msg, hint=hint)
864 raise error.Abort(msg, hint=hint)
876 return repo.lookup(roots[0])
865 return repo.lookup(roots[0])
877
866
878
867
879 @command('histedit',
868 @command('histedit',
880 [('', 'commands', '',
869 [('', 'commands', '',
881 _('read history edits from the specified file'), _('FILE')),
870 _('read history edits from the specified file'), _('FILE')),
882 ('c', 'continue', False, _('continue an edit already in progress')),
871 ('c', 'continue', False, _('continue an edit already in progress')),
883 ('', 'edit-plan', False, _('edit remaining actions list')),
872 ('', 'edit-plan', False, _('edit remaining actions list')),
884 ('k', 'keep', False,
873 ('k', 'keep', False,
885 _("don't strip old nodes after edit is complete")),
874 _("don't strip old nodes after edit is complete")),
886 ('', 'abort', False, _('abort an edit in progress')),
875 ('', 'abort', False, _('abort an edit in progress')),
887 ('o', 'outgoing', False, _('changesets not found in destination')),
876 ('o', 'outgoing', False, _('changesets not found in destination')),
888 ('f', 'force', False,
877 ('f', 'force', False,
889 _('force outgoing even for unrelated repositories')),
878 _('force outgoing even for unrelated repositories')),
890 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
879 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
891 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
880 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
892 def histedit(ui, repo, *freeargs, **opts):
881 def histedit(ui, repo, *freeargs, **opts):
893 """interactively edit changeset history
882 """interactively edit changeset history
894
883
895 This command lets you edit a linear series of changesets (up to
884 This command lets you edit a linear series of changesets (up to
896 and including the working directory, which should be clean).
885 and including the working directory, which should be clean).
897 You can:
886 You can:
898
887
899 - `pick` to [re]order a changeset
888 - `pick` to [re]order a changeset
900
889
901 - `drop` to omit changeset
890 - `drop` to omit changeset
902
891
903 - `mess` to reword the changeset commit message
892 - `mess` to reword the changeset commit message
904
893
905 - `fold` to combine it with the preceding changeset
894 - `fold` to combine it with the preceding changeset
906
895
907 - `roll` like fold, but discarding this commit's description
896 - `roll` like fold, but discarding this commit's description
908
897
909 - `edit` to edit this changeset
898 - `edit` to edit this changeset
910
899
911 There are a number of ways to select the root changeset:
900 There are a number of ways to select the root changeset:
912
901
913 - Specify ANCESTOR directly
902 - Specify ANCESTOR directly
914
903
915 - Use --outgoing -- it will be the first linear changeset not
904 - Use --outgoing -- it will be the first linear changeset not
916 included in destination. (See :hg:`help config.paths.default-push`)
905 included in destination. (See :hg:`help config.paths.default-push`)
917
906
918 - Otherwise, the value from the "histedit.defaultrev" config option
907 - Otherwise, the value from the "histedit.defaultrev" config option
919 is used as a revset to select the base revision when ANCESTOR is not
908 is used as a revset to select the base revision when ANCESTOR is not
920 specified. The first revision returned by the revset is used. By
909 specified. The first revision returned by the revset is used. By
921 default, this selects the editable history that is unique to the
910 default, this selects the editable history that is unique to the
922 ancestry of the working directory.
911 ancestry of the working directory.
923
912
924 .. container:: verbose
913 .. container:: verbose
925
914
926 If you use --outgoing, this command will abort if there are ambiguous
915 If you use --outgoing, this command will abort if there are ambiguous
927 outgoing revisions. For example, if there are multiple branches
916 outgoing revisions. For example, if there are multiple branches
928 containing outgoing revisions.
917 containing outgoing revisions.
929
918
930 Use "min(outgoing() and ::.)" or similar revset specification
919 Use "min(outgoing() and ::.)" or similar revset specification
931 instead of --outgoing to specify edit target revision exactly in
920 instead of --outgoing to specify edit target revision exactly in
932 such ambiguous situation. See :hg:`help revsets` for detail about
921 such ambiguous situation. See :hg:`help revsets` for detail about
933 selecting revisions.
922 selecting revisions.
934
923
935 .. container:: verbose
924 .. container:: verbose
936
925
937 Examples:
926 Examples:
938
927
939 - A number of changes have been made.
928 - A number of changes have been made.
940 Revision 3 is no longer needed.
929 Revision 3 is no longer needed.
941
930
942 Start history editing from revision 3::
931 Start history editing from revision 3::
943
932
944 hg histedit -r 3
933 hg histedit -r 3
945
934
946 An editor opens, containing the list of revisions,
935 An editor opens, containing the list of revisions,
947 with specific actions specified::
936 with specific actions specified::
948
937
949 pick 5339bf82f0ca 3 Zworgle the foobar
938 pick 5339bf82f0ca 3 Zworgle the foobar
950 pick 8ef592ce7cc4 4 Bedazzle the zerlog
939 pick 8ef592ce7cc4 4 Bedazzle the zerlog
951 pick 0a9639fcda9d 5 Morgify the cromulancy
940 pick 0a9639fcda9d 5 Morgify the cromulancy
952
941
953 Additional information about the possible actions
942 Additional information about the possible actions
954 to take appears below the list of revisions.
943 to take appears below the list of revisions.
955
944
956 To remove revision 3 from the history,
945 To remove revision 3 from the history,
957 its action (at the beginning of the relevant line)
946 its action (at the beginning of the relevant line)
958 is changed to 'drop'::
947 is changed to 'drop'::
959
948
960 drop 5339bf82f0ca 3 Zworgle the foobar
949 drop 5339bf82f0ca 3 Zworgle the foobar
961 pick 8ef592ce7cc4 4 Bedazzle the zerlog
950 pick 8ef592ce7cc4 4 Bedazzle the zerlog
962 pick 0a9639fcda9d 5 Morgify the cromulancy
951 pick 0a9639fcda9d 5 Morgify the cromulancy
963
952
964 - A number of changes have been made.
953 - A number of changes have been made.
965 Revision 2 and 4 need to be swapped.
954 Revision 2 and 4 need to be swapped.
966
955
967 Start history editing from revision 2::
956 Start history editing from revision 2::
968
957
969 hg histedit -r 2
958 hg histedit -r 2
970
959
971 An editor opens, containing the list of revisions,
960 An editor opens, containing the list of revisions,
972 with specific actions specified::
961 with specific actions specified::
973
962
974 pick 252a1af424ad 2 Blorb a morgwazzle
963 pick 252a1af424ad 2 Blorb a morgwazzle
975 pick 5339bf82f0ca 3 Zworgle the foobar
964 pick 5339bf82f0ca 3 Zworgle the foobar
976 pick 8ef592ce7cc4 4 Bedazzle the zerlog
965 pick 8ef592ce7cc4 4 Bedazzle the zerlog
977
966
978 To swap revision 2 and 4, its lines are swapped
967 To swap revision 2 and 4, its lines are swapped
979 in the editor::
968 in the editor::
980
969
981 pick 8ef592ce7cc4 4 Bedazzle the zerlog
970 pick 8ef592ce7cc4 4 Bedazzle the zerlog
982 pick 5339bf82f0ca 3 Zworgle the foobar
971 pick 5339bf82f0ca 3 Zworgle the foobar
983 pick 252a1af424ad 2 Blorb a morgwazzle
972 pick 252a1af424ad 2 Blorb a morgwazzle
984
973
985 Returns 0 on success, 1 if user intervention is required (not only
974 Returns 0 on success, 1 if user intervention is required (not only
986 for intentional "edit" command, but also for resolving unexpected
975 for intentional "edit" command, but also for resolving unexpected
987 conflicts).
976 conflicts).
988 """
977 """
989 state = histeditstate(repo)
978 state = histeditstate(repo)
990 try:
979 try:
991 state.wlock = repo.wlock()
980 state.wlock = repo.wlock()
992 state.lock = repo.lock()
981 state.lock = repo.lock()
993 _histedit(ui, repo, state, *freeargs, **opts)
982 _histedit(ui, repo, state, *freeargs, **opts)
994 finally:
983 finally:
995 release(state.lock, state.wlock)
984 release(state.lock, state.wlock)
996
985
997 goalcontinue = 'continue'
986 goalcontinue = 'continue'
998 goalabort = 'abort'
987 goalabort = 'abort'
999 goaleditplan = 'edit-plan'
988 goaleditplan = 'edit-plan'
1000 goalnew = 'new'
989 goalnew = 'new'
1001
990
1002 def _getgoal(opts):
991 def _getgoal(opts):
1003 if opts.get('continue'):
992 if opts.get('continue'):
1004 return goalcontinue
993 return goalcontinue
1005 if opts.get('abort'):
994 if opts.get('abort'):
1006 return goalabort
995 return goalabort
1007 if opts.get('edit_plan'):
996 if opts.get('edit_plan'):
1008 return goaleditplan
997 return goaleditplan
1009 return goalnew
998 return goalnew
1010
999
1011 def _readfile(path):
1000 def _readfile(path):
1012 if path == '-':
1001 if path == '-':
1013 return sys.stdin.read()
1002 return sys.stdin.read()
1014 else:
1003 else:
1015 with open(path, 'rb') as f:
1004 with open(path, 'rb') as f:
1016 return f.read()
1005 return f.read()
1017
1006
1018 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1007 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1019 # TODO only abort if we try to histedit mq patches, not just
1008 # TODO only abort if we try to histedit mq patches, not just
1020 # blanket if mq patches are applied somewhere
1009 # blanket if mq patches are applied somewhere
1021 mq = getattr(repo, 'mq', None)
1010 mq = getattr(repo, 'mq', None)
1022 if mq and mq.applied:
1011 if mq and mq.applied:
1023 raise error.Abort(_('source has mq patches applied'))
1012 raise error.Abort(_('source has mq patches applied'))
1024
1013
1025 # basic argument incompatibility processing
1014 # basic argument incompatibility processing
1026 outg = opts.get('outgoing')
1015 outg = opts.get('outgoing')
1027 editplan = opts.get('edit_plan')
1016 editplan = opts.get('edit_plan')
1028 abort = opts.get('abort')
1017 abort = opts.get('abort')
1029 force = opts.get('force')
1018 force = opts.get('force')
1030 if force and not outg:
1019 if force and not outg:
1031 raise error.Abort(_('--force only allowed with --outgoing'))
1020 raise error.Abort(_('--force only allowed with --outgoing'))
1032 if goal == 'continue':
1021 if goal == 'continue':
1033 if any((outg, abort, revs, freeargs, rules, editplan)):
1022 if any((outg, abort, revs, freeargs, rules, editplan)):
1034 raise error.Abort(_('no arguments allowed with --continue'))
1023 raise error.Abort(_('no arguments allowed with --continue'))
1035 elif goal == 'abort':
1024 elif goal == 'abort':
1036 if any((outg, revs, freeargs, rules, editplan)):
1025 if any((outg, revs, freeargs, rules, editplan)):
1037 raise error.Abort(_('no arguments allowed with --abort'))
1026 raise error.Abort(_('no arguments allowed with --abort'))
1038 elif goal == 'edit-plan':
1027 elif goal == 'edit-plan':
1039 if any((outg, revs, freeargs)):
1028 if any((outg, revs, freeargs)):
1040 raise error.Abort(_('only --commands argument allowed with '
1029 raise error.Abort(_('only --commands argument allowed with '
1041 '--edit-plan'))
1030 '--edit-plan'))
1042 else:
1031 else:
1043 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1032 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1044 raise error.Abort(_('history edit already in progress, try '
1033 raise error.Abort(_('history edit already in progress, try '
1045 '--continue or --abort'))
1034 '--continue or --abort'))
1046 if outg:
1035 if outg:
1047 if revs:
1036 if revs:
1048 raise error.Abort(_('no revisions allowed with --outgoing'))
1037 raise error.Abort(_('no revisions allowed with --outgoing'))
1049 if len(freeargs) > 1:
1038 if len(freeargs) > 1:
1050 raise error.Abort(
1039 raise error.Abort(
1051 _('only one repo argument allowed with --outgoing'))
1040 _('only one repo argument allowed with --outgoing'))
1052 else:
1041 else:
1053 revs.extend(freeargs)
1042 revs.extend(freeargs)
1054 if len(revs) == 0:
1043 if len(revs) == 0:
1055 defaultrev = destutil.desthistedit(ui, repo)
1044 defaultrev = destutil.desthistedit(ui, repo)
1056 if defaultrev is not None:
1045 if defaultrev is not None:
1057 revs.append(defaultrev)
1046 revs.append(defaultrev)
1058
1047
1059 if len(revs) != 1:
1048 if len(revs) != 1:
1060 raise error.Abort(
1049 raise error.Abort(
1061 _('histedit requires exactly one ancestor revision'))
1050 _('histedit requires exactly one ancestor revision'))
1062
1051
1063 def _histedit(ui, repo, state, *freeargs, **opts):
1052 def _histedit(ui, repo, state, *freeargs, **opts):
1064 goal = _getgoal(opts)
1053 goal = _getgoal(opts)
1065 revs = opts.get('rev', [])
1054 revs = opts.get('rev', [])
1066 rules = opts.get('commands', '')
1055 rules = opts.get('commands', '')
1067 state.keep = opts.get('keep', False)
1056 state.keep = opts.get('keep', False)
1068
1057
1069 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1058 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1070
1059
1071 # rebuild state
1060 # rebuild state
1072 if goal == goalcontinue:
1061 if goal == goalcontinue:
1073 state.read()
1062 state.read()
1074 state = bootstrapcontinue(ui, state, opts)
1063 state = bootstrapcontinue(ui, state, opts)
1075 elif goal == goaleditplan:
1064 elif goal == goaleditplan:
1076 _edithisteditplan(ui, repo, state, rules)
1065 _edithisteditplan(ui, repo, state, rules)
1077 return
1066 return
1078 elif goal == goalabort:
1067 elif goal == goalabort:
1079 _aborthistedit(ui, repo, state)
1068 _aborthistedit(ui, repo, state)
1080 return
1069 return
1081 else:
1070 else:
1082 # goal == goalnew
1071 # goal == goalnew
1083 _newhistedit(ui, repo, state, revs, freeargs, opts)
1072 _newhistedit(ui, repo, state, revs, freeargs, opts)
1084
1073
1085 _continuehistedit(ui, repo, state)
1074 _continuehistedit(ui, repo, state)
1086 _finishhistedit(ui, repo, state)
1075 _finishhistedit(ui, repo, state)
1087
1076
1088 def _continuehistedit(ui, repo, state):
1077 def _continuehistedit(ui, repo, state):
1089 """This function runs after either:
1078 """This function runs after either:
1090 - bootstrapcontinue (if the goal is 'continue')
1079 - bootstrapcontinue (if the goal is 'continue')
1091 - _newhistedit (if the goal is 'new')
1080 - _newhistedit (if the goal is 'new')
1092 """
1081 """
1093 # preprocess rules so that we can hide inner folds from the user
1082 # preprocess rules so that we can hide inner folds from the user
1094 # and only show one editor
1083 # and only show one editor
1095 actions = state.actions[:]
1084 actions = state.actions[:]
1096 for idx, (action, nextact) in enumerate(
1085 for idx, (action, nextact) in enumerate(
1097 zip(actions, actions[1:] + [None])):
1086 zip(actions, actions[1:] + [None])):
1098 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1087 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1099 state.actions[idx].__class__ = _multifold
1088 state.actions[idx].__class__ = _multifold
1100
1089
1101 total = len(state.actions)
1090 total = len(state.actions)
1102 pos = 0
1091 pos = 0
1103 while state.actions:
1092 while state.actions:
1104 state.write()
1093 state.write()
1105 actobj = state.actions.pop(0)
1094 actobj = state.actions.pop(0)
1106 pos += 1
1095 pos += 1
1107 ui.progress(_("editing"), pos, actobj.torule(),
1096 ui.progress(_("editing"), pos, actobj.torule(),
1108 _('changes'), total)
1097 _('changes'), total)
1109 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1098 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1110 actobj.torule()))
1099 actobj.torule()))
1111 parentctx, replacement_ = actobj.run()
1100 parentctx, replacement_ = actobj.run()
1112 state.parentctxnode = parentctx.node()
1101 state.parentctxnode = parentctx.node()
1113 state.replacements.extend(replacement_)
1102 state.replacements.extend(replacement_)
1114 state.write()
1103 state.write()
1115 ui.progress(_("editing"), None)
1104 ui.progress(_("editing"), None)
1116
1105
1117 def _finishhistedit(ui, repo, state):
1106 def _finishhistedit(ui, repo, state):
1118 """This action runs when histedit is finishing its session"""
1107 """This action runs when histedit is finishing its session"""
1119 repo.ui.pushbuffer()
1108 repo.ui.pushbuffer()
1120 hg.update(repo, state.parentctxnode, quietempty=True)
1109 hg.update(repo, state.parentctxnode, quietempty=True)
1121 repo.ui.popbuffer()
1110 repo.ui.popbuffer()
1122
1111
1123 mapping, tmpnodes, created, ntm = processreplacement(state)
1112 mapping, tmpnodes, created, ntm = processreplacement(state)
1124 if mapping:
1113 if mapping:
1125 for prec, succs in mapping.iteritems():
1114 for prec, succs in mapping.iteritems():
1126 if not succs:
1115 if not succs:
1127 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1116 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1128 else:
1117 else:
1129 ui.debug('histedit: %s is replaced by %s\n' % (
1118 ui.debug('histedit: %s is replaced by %s\n' % (
1130 node.short(prec), node.short(succs[0])))
1119 node.short(prec), node.short(succs[0])))
1131 if len(succs) > 1:
1120 if len(succs) > 1:
1132 m = 'histedit: %s'
1121 m = 'histedit: %s'
1133 for n in succs[1:]:
1122 for n in succs[1:]:
1134 ui.debug(m % node.short(n))
1123 ui.debug(m % node.short(n))
1135
1124
1136 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
1125 supportsmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
1137 if supportsmarkers:
1126 if supportsmarkers:
1138 # Only create markers if the temp nodes weren't already removed.
1127 # Only create markers if the temp nodes weren't already removed.
1139 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1128 obsolete.createmarkers(repo, ((repo[t],()) for t in sorted(tmpnodes)
1140 if t in repo))
1129 if t in repo))
1141 else:
1130 else:
1142 cleanupnode(ui, repo, 'temp', tmpnodes)
1131 cleanupnode(ui, repo, 'temp', tmpnodes)
1143
1132
1144 if not state.keep:
1133 if not state.keep:
1145 if mapping:
1134 if mapping:
1146 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1135 movebookmarks(ui, repo, mapping, state.topmost, ntm)
1147 # TODO update mq state
1136 # TODO update mq state
1148 if supportsmarkers:
1137 if supportsmarkers:
1149 markers = []
1138 markers = []
1150 # sort by revision number because it sound "right"
1139 # sort by revision number because it sound "right"
1151 for prec in sorted(mapping, key=repo.changelog.rev):
1140 for prec in sorted(mapping, key=repo.changelog.rev):
1152 succs = mapping[prec]
1141 succs = mapping[prec]
1153 markers.append((repo[prec],
1142 markers.append((repo[prec],
1154 tuple(repo[s] for s in succs)))
1143 tuple(repo[s] for s in succs)))
1155 if markers:
1144 if markers:
1156 obsolete.createmarkers(repo, markers)
1145 obsolete.createmarkers(repo, markers)
1157 else:
1146 else:
1158 cleanupnode(ui, repo, 'replaced', mapping)
1147 cleanupnode(ui, repo, 'replaced', mapping)
1159
1148
1160 state.clear()
1149 state.clear()
1161 if os.path.exists(repo.sjoin('undo')):
1150 if os.path.exists(repo.sjoin('undo')):
1162 os.unlink(repo.sjoin('undo'))
1151 os.unlink(repo.sjoin('undo'))
1163 if repo.vfs.exists('histedit-last-edit.txt'):
1152 if repo.vfs.exists('histedit-last-edit.txt'):
1164 repo.vfs.unlink('histedit-last-edit.txt')
1153 repo.vfs.unlink('histedit-last-edit.txt')
1165
1154
1166 def _aborthistedit(ui, repo, state):
1155 def _aborthistedit(ui, repo, state):
1167 try:
1156 try:
1168 state.read()
1157 state.read()
1169 __, leafs, tmpnodes, __ = processreplacement(state)
1158 __, leafs, tmpnodes, __ = processreplacement(state)
1170 ui.debug('restore wc to old parent %s\n'
1159 ui.debug('restore wc to old parent %s\n'
1171 % node.short(state.topmost))
1160 % node.short(state.topmost))
1172
1161
1173 # Recover our old commits if necessary
1162 # Recover our old commits if necessary
1174 if not state.topmost in repo and state.backupfile:
1163 if not state.topmost in repo and state.backupfile:
1175 backupfile = repo.join(state.backupfile)
1164 backupfile = repo.join(state.backupfile)
1176 f = hg.openpath(ui, backupfile)
1165 f = hg.openpath(ui, backupfile)
1177 gen = exchange.readbundle(ui, f, backupfile)
1166 gen = exchange.readbundle(ui, f, backupfile)
1178 with repo.transaction('histedit.abort') as tr:
1167 with repo.transaction('histedit.abort') as tr:
1179 if not isinstance(gen, bundle2.unbundle20):
1168 if not isinstance(gen, bundle2.unbundle20):
1180 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1169 gen.apply(repo, 'histedit', 'bundle:' + backupfile)
1181 if isinstance(gen, bundle2.unbundle20):
1170 if isinstance(gen, bundle2.unbundle20):
1182 bundle2.applybundle(repo, gen, tr,
1171 bundle2.applybundle(repo, gen, tr,
1183 source='histedit',
1172 source='histedit',
1184 url='bundle:' + backupfile)
1173 url='bundle:' + backupfile)
1185
1174
1186 os.remove(backupfile)
1175 os.remove(backupfile)
1187
1176
1188 # check whether we should update away
1177 # check whether we should update away
1189 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1178 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1190 state.parentctxnode, leafs | tmpnodes):
1179 state.parentctxnode, leafs | tmpnodes):
1191 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1180 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1192 cleanupnode(ui, repo, 'created', tmpnodes)
1181 cleanupnode(ui, repo, 'created', tmpnodes)
1193 cleanupnode(ui, repo, 'temp', leafs)
1182 cleanupnode(ui, repo, 'temp', leafs)
1194 except Exception:
1183 except Exception:
1195 if state.inprogress():
1184 if state.inprogress():
1196 ui.warn(_('warning: encountered an exception during histedit '
1185 ui.warn(_('warning: encountered an exception during histedit '
1197 '--abort; the repository may not have been completely '
1186 '--abort; the repository may not have been completely '
1198 'cleaned up\n'))
1187 'cleaned up\n'))
1199 raise
1188 raise
1200 finally:
1189 finally:
1201 state.clear()
1190 state.clear()
1202
1191
1203 def _edithisteditplan(ui, repo, state, rules):
1192 def _edithisteditplan(ui, repo, state, rules):
1204 state.read()
1193 state.read()
1205 if not rules:
1194 if not rules:
1206 comment = geteditcomment(ui,
1195 comment = geteditcomment(ui,
1207 node.short(state.parentctxnode),
1196 node.short(state.parentctxnode),
1208 node.short(state.topmost))
1197 node.short(state.topmost))
1209 rules = ruleeditor(repo, ui, state.actions, comment)
1198 rules = ruleeditor(repo, ui, state.actions, comment)
1210 else:
1199 else:
1211 rules = _readfile(rules)
1200 rules = _readfile(rules)
1212 actions = parserules(rules, state)
1201 actions = parserules(rules, state)
1213 ctxs = [repo[act.nodetoverify()] \
1202 ctxs = [repo[act.nodetoverify()] \
1214 for act in state.actions if act.nodetoverify()]
1203 for act in state.actions if act.nodetoverify()]
1215 warnverifyactions(ui, repo, actions, state, ctxs)
1204 warnverifyactions(ui, repo, actions, state, ctxs)
1216 state.actions = actions
1205 state.actions = actions
1217 state.write()
1206 state.write()
1218
1207
1219 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1208 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1220 outg = opts.get('outgoing')
1209 outg = opts.get('outgoing')
1221 rules = opts.get('commands', '')
1210 rules = opts.get('commands', '')
1222 force = opts.get('force')
1211 force = opts.get('force')
1223
1212
1224 cmdutil.checkunfinished(repo)
1213 cmdutil.checkunfinished(repo)
1225 cmdutil.bailifchanged(repo)
1214 cmdutil.bailifchanged(repo)
1226
1215
1227 topmost, empty = repo.dirstate.parents()
1216 topmost, empty = repo.dirstate.parents()
1228 if outg:
1217 if outg:
1229 if freeargs:
1218 if freeargs:
1230 remote = freeargs[0]
1219 remote = freeargs[0]
1231 else:
1220 else:
1232 remote = None
1221 remote = None
1233 root = findoutgoing(ui, repo, remote, force, opts)
1222 root = findoutgoing(ui, repo, remote, force, opts)
1234 else:
1223 else:
1235 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1224 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1236 if len(rr) != 1:
1225 if len(rr) != 1:
1237 raise error.Abort(_('The specified revisions must have '
1226 raise error.Abort(_('The specified revisions must have '
1238 'exactly one common root'))
1227 'exactly one common root'))
1239 root = rr[0].node()
1228 root = rr[0].node()
1240
1229
1241 revs = between(repo, root, topmost, state.keep)
1230 revs = between(repo, root, topmost, state.keep)
1242 if not revs:
1231 if not revs:
1243 raise error.Abort(_('%s is not an ancestor of working directory') %
1232 raise error.Abort(_('%s is not an ancestor of working directory') %
1244 node.short(root))
1233 node.short(root))
1245
1234
1246 ctxs = [repo[r] for r in revs]
1235 ctxs = [repo[r] for r in revs]
1247 if not rules:
1236 if not rules:
1248 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1237 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1249 actions = [pick(state, r) for r in revs]
1238 actions = [pick(state, r) for r in revs]
1250 rules = ruleeditor(repo, ui, actions, comment)
1239 rules = ruleeditor(repo, ui, actions, comment)
1251 else:
1240 else:
1252 rules = _readfile(rules)
1241 rules = _readfile(rules)
1253 actions = parserules(rules, state)
1242 actions = parserules(rules, state)
1254 warnverifyactions(ui, repo, actions, state, ctxs)
1243 warnverifyactions(ui, repo, actions, state, ctxs)
1255
1244
1256 parentctxnode = repo[root].parents()[0].node()
1245 parentctxnode = repo[root].parents()[0].node()
1257
1246
1258 state.parentctxnode = parentctxnode
1247 state.parentctxnode = parentctxnode
1259 state.actions = actions
1248 state.actions = actions
1260 state.topmost = topmost
1249 state.topmost = topmost
1261 state.replacements = []
1250 state.replacements = []
1262
1251
1263 # Create a backup so we can always abort completely.
1252 # Create a backup so we can always abort completely.
1264 backupfile = None
1253 backupfile = None
1265 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1254 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1266 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1255 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1267 'histedit')
1256 'histedit')
1268 state.backupfile = backupfile
1257 state.backupfile = backupfile
1269
1258
1270 def _getsummary(ctx):
1259 def _getsummary(ctx):
1271 # a common pattern is to extract the summary but default to the empty
1260 # a common pattern is to extract the summary but default to the empty
1272 # string
1261 # string
1273 summary = ctx.description() or ''
1262 summary = ctx.description() or ''
1274 if summary:
1263 if summary:
1275 summary = summary.splitlines()[0]
1264 summary = summary.splitlines()[0]
1276 return summary
1265 return summary
1277
1266
1278 def bootstrapcontinue(ui, state, opts):
1267 def bootstrapcontinue(ui, state, opts):
1279 repo = state.repo
1268 repo = state.repo
1280 if state.actions:
1269 if state.actions:
1281 actobj = state.actions.pop(0)
1270 actobj = state.actions.pop(0)
1282
1271
1283 if _isdirtywc(repo):
1272 if _isdirtywc(repo):
1284 actobj.continuedirty()
1273 actobj.continuedirty()
1285 if _isdirtywc(repo):
1274 if _isdirtywc(repo):
1286 abortdirty()
1275 abortdirty()
1287
1276
1288 parentctx, replacements = actobj.continueclean()
1277 parentctx, replacements = actobj.continueclean()
1289
1278
1290 state.parentctxnode = parentctx.node()
1279 state.parentctxnode = parentctx.node()
1291 state.replacements.extend(replacements)
1280 state.replacements.extend(replacements)
1292
1281
1293 return state
1282 return state
1294
1283
1295 def between(repo, old, new, keep):
1284 def between(repo, old, new, keep):
1296 """select and validate the set of revision to edit
1285 """select and validate the set of revision to edit
1297
1286
1298 When keep is false, the specified set can't have children."""
1287 When keep is false, the specified set can't have children."""
1299 ctxs = list(repo.set('%n::%n', old, new))
1288 ctxs = list(repo.set('%n::%n', old, new))
1300 if ctxs and not keep:
1289 if ctxs and not keep:
1301 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1290 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1302 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1291 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1303 raise error.Abort(_('can only histedit a changeset together '
1292 raise error.Abort(_('can only histedit a changeset together '
1304 'with all its descendants'))
1293 'with all its descendants'))
1305 if repo.revs('(%ld) and merge()', ctxs):
1294 if repo.revs('(%ld) and merge()', ctxs):
1306 raise error.Abort(_('cannot edit history that contains merges'))
1295 raise error.Abort(_('cannot edit history that contains merges'))
1307 root = ctxs[0] # list is already sorted by repo.set
1296 root = ctxs[0] # list is already sorted by repo.set
1308 if not root.mutable():
1297 if not root.mutable():
1309 raise error.Abort(_('cannot edit public changeset: %s') % root,
1298 raise error.Abort(_('cannot edit public changeset: %s') % root,
1310 hint=_('see "hg help phases" for details'))
1299 hint=_('see "hg help phases" for details'))
1311 return [c.node() for c in ctxs]
1300 return [c.node() for c in ctxs]
1312
1301
1313 def ruleeditor(repo, ui, actions, editcomment=""):
1302 def ruleeditor(repo, ui, actions, editcomment=""):
1314 """open an editor to edit rules
1303 """open an editor to edit rules
1315
1304
1316 rules are in the format [ [act, ctx], ...] like in state.rules
1305 rules are in the format [ [act, ctx], ...] like in state.rules
1317 """
1306 """
1318 if repo.ui.configbool("experimental", "histedit.autoverb"):
1307 if repo.ui.configbool("experimental", "histedit.autoverb"):
1319 newact = util.sortdict()
1308 newact = util.sortdict()
1320 for act in actions:
1309 for act in actions:
1321 ctx = repo[act.node]
1310 ctx = repo[act.node]
1322 summary = _getsummary(ctx)
1311 summary = _getsummary(ctx)
1323 fword = summary.split(' ', 1)[0].lower()
1312 fword = summary.split(' ', 1)[0].lower()
1324 added = False
1313 added = False
1325
1314
1326 # if it doesn't end with the special character '!' just skip this
1315 # if it doesn't end with the special character '!' just skip this
1327 if fword.endswith('!'):
1316 if fword.endswith('!'):
1328 fword = fword[:-1]
1317 fword = fword[:-1]
1329 if fword in primaryactions | secondaryactions | tertiaryactions:
1318 if fword in primaryactions | secondaryactions | tertiaryactions:
1330 act.verb = fword
1319 act.verb = fword
1331 # get the target summary
1320 # get the target summary
1332 tsum = summary[len(fword) + 1:].lstrip()
1321 tsum = summary[len(fword) + 1:].lstrip()
1333 # safe but slow: reverse iterate over the actions so we
1322 # safe but slow: reverse iterate over the actions so we
1334 # don't clash on two commits having the same summary
1323 # don't clash on two commits having the same summary
1335 for na, l in reversed(list(newact.iteritems())):
1324 for na, l in reversed(list(newact.iteritems())):
1336 actx = repo[na.node]
1325 actx = repo[na.node]
1337 asum = _getsummary(actx)
1326 asum = _getsummary(actx)
1338 if asum == tsum:
1327 if asum == tsum:
1339 added = True
1328 added = True
1340 l.append(act)
1329 l.append(act)
1341 break
1330 break
1342
1331
1343 if not added:
1332 if not added:
1344 newact[act] = []
1333 newact[act] = []
1345
1334
1346 # copy over and flatten the new list
1335 # copy over and flatten the new list
1347 actions = []
1336 actions = []
1348 for na, l in newact.iteritems():
1337 for na, l in newact.iteritems():
1349 actions.append(na)
1338 actions.append(na)
1350 actions += l
1339 actions += l
1351
1340
1352 rules = '\n'.join([act.torule() for act in actions])
1341 rules = '\n'.join([act.torule() for act in actions])
1353 rules += '\n\n'
1342 rules += '\n\n'
1354 rules += editcomment
1343 rules += editcomment
1355 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1344 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'})
1356
1345
1357 # Save edit rules in .hg/histedit-last-edit.txt in case
1346 # Save edit rules in .hg/histedit-last-edit.txt in case
1358 # the user needs to ask for help after something
1347 # the user needs to ask for help after something
1359 # surprising happens.
1348 # surprising happens.
1360 f = open(repo.join('histedit-last-edit.txt'), 'w')
1349 f = open(repo.join('histedit-last-edit.txt'), 'w')
1361 f.write(rules)
1350 f.write(rules)
1362 f.close()
1351 f.close()
1363
1352
1364 return rules
1353 return rules
1365
1354
1366 def parserules(rules, state):
1355 def parserules(rules, state):
1367 """Read the histedit rules string and return list of action objects """
1356 """Read the histedit rules string and return list of action objects """
1368 rules = [l for l in (r.strip() for r in rules.splitlines())
1357 rules = [l for l in (r.strip() for r in rules.splitlines())
1369 if l and not l.startswith('#')]
1358 if l and not l.startswith('#')]
1370 actions = []
1359 actions = []
1371 for r in rules:
1360 for r in rules:
1372 if ' ' not in r:
1361 if ' ' not in r:
1373 raise error.ParseError(_('malformed line "%s"') % r)
1362 raise error.ParseError(_('malformed line "%s"') % r)
1374 verb, rest = r.split(' ', 1)
1363 verb, rest = r.split(' ', 1)
1375
1364
1376 if verb not in actiontable:
1365 if verb not in actiontable:
1377 raise error.ParseError(_('unknown action "%s"') % verb)
1366 raise error.ParseError(_('unknown action "%s"') % verb)
1378
1367
1379 action = actiontable[verb].fromrule(state, rest)
1368 action = actiontable[verb].fromrule(state, rest)
1380 actions.append(action)
1369 actions.append(action)
1381 return actions
1370 return actions
1382
1371
1383 def warnverifyactions(ui, repo, actions, state, ctxs):
1372 def warnverifyactions(ui, repo, actions, state, ctxs):
1384 try:
1373 try:
1385 verifyactions(actions, state, ctxs)
1374 verifyactions(actions, state, ctxs)
1386 except error.ParseError:
1375 except error.ParseError:
1387 if repo.vfs.exists('histedit-last-edit.txt'):
1376 if repo.vfs.exists('histedit-last-edit.txt'):
1388 ui.warn(_('warning: histedit rules saved '
1377 ui.warn(_('warning: histedit rules saved '
1389 'to: .hg/histedit-last-edit.txt\n'))
1378 'to: .hg/histedit-last-edit.txt\n'))
1390 raise
1379 raise
1391
1380
1392 def verifyactions(actions, state, ctxs):
1381 def verifyactions(actions, state, ctxs):
1393 """Verify that there exists exactly one action per given changeset and
1382 """Verify that there exists exactly one action per given changeset and
1394 other constraints.
1383 other constraints.
1395
1384
1396 Will abort if there are to many or too few rules, a malformed rule,
1385 Will abort if there are to many or too few rules, a malformed rule,
1397 or a rule on a changeset outside of the user-given range.
1386 or a rule on a changeset outside of the user-given range.
1398 """
1387 """
1399 expected = set(c.hex() for c in ctxs)
1388 expected = set(c.hex() for c in ctxs)
1400 seen = set()
1389 seen = set()
1401 prev = None
1390 prev = None
1402 for action in actions:
1391 for action in actions:
1403 action.verify(prev)
1392 action.verify(prev)
1404 prev = action
1393 prev = action
1405 constraints = action.constraints()
1394 constraints = action.constraints()
1406 for constraint in constraints:
1395 for constraint in constraints:
1407 if constraint not in _constraints.known():
1396 if constraint not in _constraints.known():
1408 raise error.ParseError(_('unknown constraint "%s"') %
1397 raise error.ParseError(_('unknown constraint "%s"') %
1409 constraint)
1398 constraint)
1410
1399
1411 nodetoverify = action.nodetoverify()
1400 nodetoverify = action.nodetoverify()
1412 if nodetoverify is not None:
1401 if nodetoverify is not None:
1413 ha = node.hex(nodetoverify)
1402 ha = node.hex(nodetoverify)
1414 if _constraints.noother in constraints and ha not in expected:
1403 if _constraints.noother in constraints and ha not in expected:
1415 raise error.ParseError(
1404 raise error.ParseError(
1416 _('%s "%s" changeset was not a candidate')
1405 _('%s "%s" changeset was not a candidate')
1417 % (action.verb, ha[:12]),
1406 % (action.verb, ha[:12]),
1418 hint=_('only use listed changesets'))
1407 hint=_('only use listed changesets'))
1419 if _constraints.forceother in constraints and ha in expected:
1408 if _constraints.forceother in constraints and ha in expected:
1420 raise error.ParseError(
1409 raise error.ParseError(
1421 _('%s "%s" changeset was not an edited list candidate')
1410 _('%s "%s" changeset was not an edited list candidate')
1422 % (action.verb, ha[:12]),
1411 % (action.verb, ha[:12]),
1423 hint=_('only use listed changesets'))
1412 hint=_('only use listed changesets'))
1424 if _constraints.noduplicates in constraints and ha in seen:
1413 if _constraints.noduplicates in constraints and ha in seen:
1425 raise error.ParseError(_(
1414 raise error.ParseError(_(
1426 'duplicated command for changeset %s') %
1415 'duplicated command for changeset %s') %
1427 ha[:12])
1416 ha[:12])
1428 seen.add(ha)
1417 seen.add(ha)
1429 missing = sorted(expected - seen) # sort to stabilize output
1418 missing = sorted(expected - seen) # sort to stabilize output
1430
1419
1431 if state.repo.ui.configbool('histedit', 'dropmissing'):
1420 if state.repo.ui.configbool('histedit', 'dropmissing'):
1432 if len(actions) == 0:
1421 if len(actions) == 0:
1433 raise error.ParseError(_('no rules provided'),
1422 raise error.ParseError(_('no rules provided'),
1434 hint=_('use strip extension to remove commits'))
1423 hint=_('use strip extension to remove commits'))
1435
1424
1436 drops = [drop(state, node.bin(n)) for n in missing]
1425 drops = [drop(state, node.bin(n)) for n in missing]
1437 # put the in the beginning so they execute immediately and
1426 # put the in the beginning so they execute immediately and
1438 # don't show in the edit-plan in the future
1427 # don't show in the edit-plan in the future
1439 actions[:0] = drops
1428 actions[:0] = drops
1440 elif missing:
1429 elif missing:
1441 raise error.ParseError(_('missing rules for changeset %s') %
1430 raise error.ParseError(_('missing rules for changeset %s') %
1442 missing[0][:12],
1431 missing[0][:12],
1443 hint=_('use "drop %s" to discard, see also: '
1432 hint=_('use "drop %s" to discard, see also: '
1444 '"hg help -e histedit.config"') % missing[0][:12])
1433 '"hg help -e histedit.config"') % missing[0][:12])
1445
1434
1446 def adjustreplacementsfrommarkers(repo, oldreplacements):
1435 def adjustreplacementsfrommarkers(repo, oldreplacements):
1447 """Adjust replacements from obsolescense markers
1436 """Adjust replacements from obsolescense markers
1448
1437
1449 Replacements structure is originally generated based on
1438 Replacements structure is originally generated based on
1450 histedit's state and does not account for changes that are
1439 histedit's state and does not account for changes that are
1451 not recorded there. This function fixes that by adding
1440 not recorded there. This function fixes that by adding
1452 data read from obsolescense markers"""
1441 data read from obsolescense markers"""
1453 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1442 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1454 return oldreplacements
1443 return oldreplacements
1455
1444
1456 unfi = repo.unfiltered()
1445 unfi = repo.unfiltered()
1457 nm = unfi.changelog.nodemap
1446 nm = unfi.changelog.nodemap
1458 obsstore = repo.obsstore
1447 obsstore = repo.obsstore
1459 newreplacements = list(oldreplacements)
1448 newreplacements = list(oldreplacements)
1460 oldsuccs = [r[1] for r in oldreplacements]
1449 oldsuccs = [r[1] for r in oldreplacements]
1461 # successors that have already been added to succstocheck once
1450 # successors that have already been added to succstocheck once
1462 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1451 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1463 succstocheck = list(seensuccs)
1452 succstocheck = list(seensuccs)
1464 while succstocheck:
1453 while succstocheck:
1465 n = succstocheck.pop()
1454 n = succstocheck.pop()
1466 missing = nm.get(n) is None
1455 missing = nm.get(n) is None
1467 markers = obsstore.successors.get(n, ())
1456 markers = obsstore.successors.get(n, ())
1468 if missing and not markers:
1457 if missing and not markers:
1469 # dead end, mark it as such
1458 # dead end, mark it as such
1470 newreplacements.append((n, ()))
1459 newreplacements.append((n, ()))
1471 for marker in markers:
1460 for marker in markers:
1472 nsuccs = marker[1]
1461 nsuccs = marker[1]
1473 newreplacements.append((n, nsuccs))
1462 newreplacements.append((n, nsuccs))
1474 for nsucc in nsuccs:
1463 for nsucc in nsuccs:
1475 if nsucc not in seensuccs:
1464 if nsucc not in seensuccs:
1476 seensuccs.add(nsucc)
1465 seensuccs.add(nsucc)
1477 succstocheck.append(nsucc)
1466 succstocheck.append(nsucc)
1478
1467
1479 return newreplacements
1468 return newreplacements
1480
1469
1481 def processreplacement(state):
1470 def processreplacement(state):
1482 """process the list of replacements to return
1471 """process the list of replacements to return
1483
1472
1484 1) the final mapping between original and created nodes
1473 1) the final mapping between original and created nodes
1485 2) the list of temporary node created by histedit
1474 2) the list of temporary node created by histedit
1486 3) the list of new commit created by histedit"""
1475 3) the list of new commit created by histedit"""
1487 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1476 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1488 allsuccs = set()
1477 allsuccs = set()
1489 replaced = set()
1478 replaced = set()
1490 fullmapping = {}
1479 fullmapping = {}
1491 # initialize basic set
1480 # initialize basic set
1492 # fullmapping records all operations recorded in replacement
1481 # fullmapping records all operations recorded in replacement
1493 for rep in replacements:
1482 for rep in replacements:
1494 allsuccs.update(rep[1])
1483 allsuccs.update(rep[1])
1495 replaced.add(rep[0])
1484 replaced.add(rep[0])
1496 fullmapping.setdefault(rep[0], set()).update(rep[1])
1485 fullmapping.setdefault(rep[0], set()).update(rep[1])
1497 new = allsuccs - replaced
1486 new = allsuccs - replaced
1498 tmpnodes = allsuccs & replaced
1487 tmpnodes = allsuccs & replaced
1499 # Reduce content fullmapping into direct relation between original nodes
1488 # Reduce content fullmapping into direct relation between original nodes
1500 # and final node created during history edition
1489 # and final node created during history edition
1501 # Dropped changeset are replaced by an empty list
1490 # Dropped changeset are replaced by an empty list
1502 toproceed = set(fullmapping)
1491 toproceed = set(fullmapping)
1503 final = {}
1492 final = {}
1504 while toproceed:
1493 while toproceed:
1505 for x in list(toproceed):
1494 for x in list(toproceed):
1506 succs = fullmapping[x]
1495 succs = fullmapping[x]
1507 for s in list(succs):
1496 for s in list(succs):
1508 if s in toproceed:
1497 if s in toproceed:
1509 # non final node with unknown closure
1498 # non final node with unknown closure
1510 # We can't process this now
1499 # We can't process this now
1511 break
1500 break
1512 elif s in final:
1501 elif s in final:
1513 # non final node, replace with closure
1502 # non final node, replace with closure
1514 succs.remove(s)
1503 succs.remove(s)
1515 succs.update(final[s])
1504 succs.update(final[s])
1516 else:
1505 else:
1517 final[x] = succs
1506 final[x] = succs
1518 toproceed.remove(x)
1507 toproceed.remove(x)
1519 # remove tmpnodes from final mapping
1508 # remove tmpnodes from final mapping
1520 for n in tmpnodes:
1509 for n in tmpnodes:
1521 del final[n]
1510 del final[n]
1522 # we expect all changes involved in final to exist in the repo
1511 # we expect all changes involved in final to exist in the repo
1523 # turn `final` into list (topologically sorted)
1512 # turn `final` into list (topologically sorted)
1524 nm = state.repo.changelog.nodemap
1513 nm = state.repo.changelog.nodemap
1525 for prec, succs in final.items():
1514 for prec, succs in final.items():
1526 final[prec] = sorted(succs, key=nm.get)
1515 final[prec] = sorted(succs, key=nm.get)
1527
1516
1528 # computed topmost element (necessary for bookmark)
1517 # computed topmost element (necessary for bookmark)
1529 if new:
1518 if new:
1530 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1519 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1531 elif not final:
1520 elif not final:
1532 # Nothing rewritten at all. we won't need `newtopmost`
1521 # Nothing rewritten at all. we won't need `newtopmost`
1533 # It is the same as `oldtopmost` and `processreplacement` know it
1522 # It is the same as `oldtopmost` and `processreplacement` know it
1534 newtopmost = None
1523 newtopmost = None
1535 else:
1524 else:
1536 # every body died. The newtopmost is the parent of the root.
1525 # every body died. The newtopmost is the parent of the root.
1537 r = state.repo.changelog.rev
1526 r = state.repo.changelog.rev
1538 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1527 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1539
1528
1540 return final, tmpnodes, new, newtopmost
1529 return final, tmpnodes, new, newtopmost
1541
1530
1542 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1531 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
1543 """Move bookmark from old to newly created node"""
1532 """Move bookmark from old to newly created node"""
1544 if not mapping:
1533 if not mapping:
1545 # if nothing got rewritten there is not purpose for this function
1534 # if nothing got rewritten there is not purpose for this function
1546 return
1535 return
1547 moves = []
1536 moves = []
1548 for bk, old in sorted(repo._bookmarks.iteritems()):
1537 for bk, old in sorted(repo._bookmarks.iteritems()):
1549 if old == oldtopmost:
1538 if old == oldtopmost:
1550 # special case ensure bookmark stay on tip.
1539 # special case ensure bookmark stay on tip.
1551 #
1540 #
1552 # This is arguably a feature and we may only want that for the
1541 # This is arguably a feature and we may only want that for the
1553 # active bookmark. But the behavior is kept compatible with the old
1542 # active bookmark. But the behavior is kept compatible with the old
1554 # version for now.
1543 # version for now.
1555 moves.append((bk, newtopmost))
1544 moves.append((bk, newtopmost))
1556 continue
1545 continue
1557 base = old
1546 base = old
1558 new = mapping.get(base, None)
1547 new = mapping.get(base, None)
1559 if new is None:
1548 if new is None:
1560 continue
1549 continue
1561 while not new:
1550 while not new:
1562 # base is killed, trying with parent
1551 # base is killed, trying with parent
1563 base = repo[base].p1().node()
1552 base = repo[base].p1().node()
1564 new = mapping.get(base, (base,))
1553 new = mapping.get(base, (base,))
1565 # nothing to move
1554 # nothing to move
1566 moves.append((bk, new[-1]))
1555 moves.append((bk, new[-1]))
1567 if moves:
1556 if moves:
1568 lock = tr = None
1557 lock = tr = None
1569 try:
1558 try:
1570 lock = repo.lock()
1559 lock = repo.lock()
1571 tr = repo.transaction('histedit')
1560 tr = repo.transaction('histedit')
1572 marks = repo._bookmarks
1561 marks = repo._bookmarks
1573 for mark, new in moves:
1562 for mark, new in moves:
1574 old = marks[mark]
1563 old = marks[mark]
1575 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1564 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
1576 % (mark, node.short(old), node.short(new)))
1565 % (mark, node.short(old), node.short(new)))
1577 marks[mark] = new
1566 marks[mark] = new
1578 marks.recordchange(tr)
1567 marks.recordchange(tr)
1579 tr.close()
1568 tr.close()
1580 finally:
1569 finally:
1581 release(tr, lock)
1570 release(tr, lock)
1582
1571
1583 def cleanupnode(ui, repo, name, nodes):
1572 def cleanupnode(ui, repo, name, nodes):
1584 """strip a group of nodes from the repository
1573 """strip a group of nodes from the repository
1585
1574
1586 The set of node to strip may contains unknown nodes."""
1575 The set of node to strip may contains unknown nodes."""
1587 ui.debug('should strip %s nodes %s\n' %
1576 ui.debug('should strip %s nodes %s\n' %
1588 (name, ', '.join([node.short(n) for n in nodes])))
1577 (name, ', '.join([node.short(n) for n in nodes])))
1589 with repo.lock():
1578 with repo.lock():
1590 # do not let filtering get in the way of the cleanse
1579 # do not let filtering get in the way of the cleanse
1591 # we should probably get rid of obsolescence marker created during the
1580 # we should probably get rid of obsolescence marker created during the
1592 # histedit, but we currently do not have such information.
1581 # histedit, but we currently do not have such information.
1593 repo = repo.unfiltered()
1582 repo = repo.unfiltered()
1594 # Find all nodes that need to be stripped
1583 # Find all nodes that need to be stripped
1595 # (we use %lr instead of %ln to silently ignore unknown items)
1584 # (we use %lr instead of %ln to silently ignore unknown items)
1596 nm = repo.changelog.nodemap
1585 nm = repo.changelog.nodemap
1597 nodes = sorted(n for n in nodes if n in nm)
1586 nodes = sorted(n for n in nodes if n in nm)
1598 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1587 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1599 for c in roots:
1588 for c in roots:
1600 # We should process node in reverse order to strip tip most first.
1589 # We should process node in reverse order to strip tip most first.
1601 # but this trigger a bug in changegroup hook.
1590 # but this trigger a bug in changegroup hook.
1602 # This would reduce bundle overhead
1591 # This would reduce bundle overhead
1603 repair.strip(ui, repo, c)
1592 repair.strip(ui, repo, c)
1604
1593
1605 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1594 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1606 if isinstance(nodelist, str):
1595 if isinstance(nodelist, str):
1607 nodelist = [nodelist]
1596 nodelist = [nodelist]
1608 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1597 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1609 state = histeditstate(repo)
1598 state = histeditstate(repo)
1610 state.read()
1599 state.read()
1611 histedit_nodes = set([action.nodetoverify() for action
1600 histedit_nodes = set([action.nodetoverify() for action
1612 in state.actions if action.nodetoverify()])
1601 in state.actions if action.nodetoverify()])
1613 strip_nodes = set([repo[n].node() for n in nodelist])
1602 strip_nodes = set([repo[n].node() for n in nodelist])
1614 common_nodes = histedit_nodes & strip_nodes
1603 common_nodes = histedit_nodes & strip_nodes
1615 if common_nodes:
1604 if common_nodes:
1616 raise error.Abort(_("histedit in progress, can't strip %s")
1605 raise error.Abort(_("histedit in progress, can't strip %s")
1617 % ', '.join(node.short(x) for x in common_nodes))
1606 % ', '.join(node.short(x) for x in common_nodes))
1618 return orig(ui, repo, nodelist, *args, **kwargs)
1607 return orig(ui, repo, nodelist, *args, **kwargs)
1619
1608
1620 extensions.wrapfunction(repair, 'strip', stripwrapper)
1609 extensions.wrapfunction(repair, 'strip', stripwrapper)
1621
1610
1622 def summaryhook(ui, repo):
1611 def summaryhook(ui, repo):
1623 if not os.path.exists(repo.join('histedit-state')):
1612 if not os.path.exists(repo.join('histedit-state')):
1624 return
1613 return
1625 state = histeditstate(repo)
1614 state = histeditstate(repo)
1626 state.read()
1615 state.read()
1627 if state.actions:
1616 if state.actions:
1628 # i18n: column positioning for "hg summary"
1617 # i18n: column positioning for "hg summary"
1629 ui.write(_('hist: %s (histedit --continue)\n') %
1618 ui.write(_('hist: %s (histedit --continue)\n') %
1630 (ui.label(_('%d remaining'), 'histedit.remaining') %
1619 (ui.label(_('%d remaining'), 'histedit.remaining') %
1631 len(state.actions)))
1620 len(state.actions)))
1632
1621
1633 def extsetup(ui):
1622 def extsetup(ui):
1634 cmdutil.summaryhooks.add('histedit', summaryhook)
1623 cmdutil.summaryhooks.add('histedit', summaryhook)
1635 cmdutil.unfinishedstates.append(
1624 cmdutil.unfinishedstates.append(
1636 ['histedit-state', False, True, _('histedit in progress'),
1625 ['histedit-state', False, True, _('histedit in progress'),
1637 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1626 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1638 cmdutil.afterresolvedstates.append(
1627 cmdutil.afterresolvedstates.append(
1639 ['histedit-state', _('hg histedit --continue')])
1628 ['histedit-state', _('hg histedit --continue')])
1640 if ui.configbool("experimental", "histeditng"):
1629 if ui.configbool("experimental", "histeditng"):
1641 globals()['base'] = action(['base', 'b'],
1630 globals()['base'] = action(['base', 'b'],
1642 _('checkout changeset and apply further changesets from there')
1631 _('checkout changeset and apply further changesets from there')
1643 )(base)
1632 )(base)
General Comments 0
You need to be logged in to leave comments. Login now