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