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