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