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