##// END OF EJS Templates
histedit: add rewrite.update-timestamp support to fold and mess...
Taapas Agrawal -
r41249:704a3aa3 default
parent child Browse files
Show More
@@ -0,0 +1,21 b''
1 # mock out util.makedate() to supply testable values
2
3 from __future__ import absolute_import
4
5 import os
6
7 from mercurial import pycompat
8 from mercurial.utils import dateutil
9
10 def mockmakedate():
11 filename = os.path.join(os.environ['TESTTMP'], 'testtime')
12 try:
13 with open(filename, 'rb') as timef:
14 time = float(timef.read()) + 1
15 except IOError:
16 time = 0.0
17 with open(filename, 'wb') as timef:
18 timef.write(pycompat.bytestr(time))
19 return (time, 0)
20
21 dateutil.makedate = mockmakedate
@@ -1,2241 +1,2249 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 # chistedit dependencies that are not available everywhere
186 # chistedit dependencies that are not available everywhere
187 try:
187 try:
188 import fcntl
188 import fcntl
189 import termios
189 import termios
190 except ImportError:
190 except ImportError:
191 fcntl = None
191 fcntl = None
192 termios = None
192 termios = None
193
193
194 import functools
194 import functools
195 import os
195 import os
196 import struct
196 import struct
197
197
198 from mercurial.i18n import _
198 from mercurial.i18n import _
199 from mercurial import (
199 from mercurial import (
200 bundle2,
200 bundle2,
201 cmdutil,
201 cmdutil,
202 context,
202 context,
203 copies,
203 copies,
204 destutil,
204 destutil,
205 discovery,
205 discovery,
206 error,
206 error,
207 exchange,
207 exchange,
208 extensions,
208 extensions,
209 hg,
209 hg,
210 logcmdutil,
210 logcmdutil,
211 merge as mergemod,
211 merge as mergemod,
212 mergeutil,
212 mergeutil,
213 node,
213 node,
214 obsolete,
214 obsolete,
215 pycompat,
215 pycompat,
216 registrar,
216 registrar,
217 repair,
217 repair,
218 scmutil,
218 scmutil,
219 state as statemod,
219 state as statemod,
220 util,
220 util,
221 )
221 )
222 from mercurial.utils import (
222 from mercurial.utils import (
223 dateutil,
223 stringutil,
224 stringutil,
224 )
225 )
225
226
226 pickle = util.pickle
227 pickle = util.pickle
227 cmdtable = {}
228 cmdtable = {}
228 command = registrar.command(cmdtable)
229 command = registrar.command(cmdtable)
229
230
230 configtable = {}
231 configtable = {}
231 configitem = registrar.configitem(configtable)
232 configitem = registrar.configitem(configtable)
232 configitem('experimental', 'histedit.autoverb',
233 configitem('experimental', 'histedit.autoverb',
233 default=False,
234 default=False,
234 )
235 )
235 configitem('histedit', 'defaultrev',
236 configitem('histedit', 'defaultrev',
236 default=None,
237 default=None,
237 )
238 )
238 configitem('histedit', 'dropmissing',
239 configitem('histedit', 'dropmissing',
239 default=False,
240 default=False,
240 )
241 )
241 configitem('histedit', 'linelen',
242 configitem('histedit', 'linelen',
242 default=80,
243 default=80,
243 )
244 )
244 configitem('histedit', 'singletransaction',
245 configitem('histedit', 'singletransaction',
245 default=False,
246 default=False,
246 )
247 )
247 configitem('ui', 'interface.histedit',
248 configitem('ui', 'interface.histedit',
248 default=None,
249 default=None,
249 )
250 )
250
251
251 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
252 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
252 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
253 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
253 # be specifying the version(s) of Mercurial they are tested with, or
254 # be specifying the version(s) of Mercurial they are tested with, or
254 # leave the attribute unspecified.
255 # leave the attribute unspecified.
255 testedwith = 'ships-with-hg-core'
256 testedwith = 'ships-with-hg-core'
256
257
257 actiontable = {}
258 actiontable = {}
258 primaryactions = set()
259 primaryactions = set()
259 secondaryactions = set()
260 secondaryactions = set()
260 tertiaryactions = set()
261 tertiaryactions = set()
261 internalactions = set()
262 internalactions = set()
262
263
263 def geteditcomment(ui, first, last):
264 def geteditcomment(ui, first, last):
264 """ construct the editor comment
265 """ construct the editor comment
265 The comment includes::
266 The comment includes::
266 - an intro
267 - an intro
267 - sorted primary commands
268 - sorted primary commands
268 - sorted short commands
269 - sorted short commands
269 - sorted long commands
270 - sorted long commands
270 - additional hints
271 - additional hints
271
272
272 Commands are only included once.
273 Commands are only included once.
273 """
274 """
274 intro = _("""Edit history between %s and %s
275 intro = _("""Edit history between %s and %s
275
276
276 Commits are listed from least to most recent
277 Commits are listed from least to most recent
277
278
278 You can reorder changesets by reordering the lines
279 You can reorder changesets by reordering the lines
279
280
280 Commands:
281 Commands:
281 """)
282 """)
282 actions = []
283 actions = []
283 def addverb(v):
284 def addverb(v):
284 a = actiontable[v]
285 a = actiontable[v]
285 lines = a.message.split("\n")
286 lines = a.message.split("\n")
286 if len(a.verbs):
287 if len(a.verbs):
287 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
288 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
288 actions.append(" %s = %s" % (v, lines[0]))
289 actions.append(" %s = %s" % (v, lines[0]))
289 actions.extend([' %s' for l in lines[1:]])
290 actions.extend([' %s' for l in lines[1:]])
290
291
291 for v in (
292 for v in (
292 sorted(primaryactions) +
293 sorted(primaryactions) +
293 sorted(secondaryactions) +
294 sorted(secondaryactions) +
294 sorted(tertiaryactions)
295 sorted(tertiaryactions)
295 ):
296 ):
296 addverb(v)
297 addverb(v)
297 actions.append('')
298 actions.append('')
298
299
299 hints = []
300 hints = []
300 if ui.configbool('histedit', 'dropmissing'):
301 if ui.configbool('histedit', 'dropmissing'):
301 hints.append("Deleting a changeset from the list "
302 hints.append("Deleting a changeset from the list "
302 "will DISCARD it from the edited history!")
303 "will DISCARD it from the edited history!")
303
304
304 lines = (intro % (first, last)).split('\n') + actions + hints
305 lines = (intro % (first, last)).split('\n') + actions + hints
305
306
306 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
307 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
307
308
308 class histeditstate(object):
309 class histeditstate(object):
309 def __init__(self, repo):
310 def __init__(self, repo):
310 self.repo = repo
311 self.repo = repo
311 self.actions = None
312 self.actions = None
312 self.keep = None
313 self.keep = None
313 self.topmost = None
314 self.topmost = None
314 self.parentctxnode = None
315 self.parentctxnode = None
315 self.lock = None
316 self.lock = None
316 self.wlock = None
317 self.wlock = None
317 self.backupfile = None
318 self.backupfile = None
318 self.stateobj = statemod.cmdstate(repo, 'histedit-state')
319 self.stateobj = statemod.cmdstate(repo, 'histedit-state')
319 self.replacements = []
320 self.replacements = []
320
321
321 def read(self):
322 def read(self):
322 """Load histedit state from disk and set fields appropriately."""
323 """Load histedit state from disk and set fields appropriately."""
323 if not self.stateobj.exists():
324 if not self.stateobj.exists():
324 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
325 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
325
326
326 data = self._read()
327 data = self._read()
327
328
328 self.parentctxnode = data['parentctxnode']
329 self.parentctxnode = data['parentctxnode']
329 actions = parserules(data['rules'], self)
330 actions = parserules(data['rules'], self)
330 self.actions = actions
331 self.actions = actions
331 self.keep = data['keep']
332 self.keep = data['keep']
332 self.topmost = data['topmost']
333 self.topmost = data['topmost']
333 self.replacements = data['replacements']
334 self.replacements = data['replacements']
334 self.backupfile = data['backupfile']
335 self.backupfile = data['backupfile']
335
336
336 def _read(self):
337 def _read(self):
337 fp = self.repo.vfs.read('histedit-state')
338 fp = self.repo.vfs.read('histedit-state')
338 if fp.startswith('v1\n'):
339 if fp.startswith('v1\n'):
339 data = self._load()
340 data = self._load()
340 parentctxnode, rules, keep, topmost, replacements, backupfile = data
341 parentctxnode, rules, keep, topmost, replacements, backupfile = data
341 else:
342 else:
342 data = pickle.loads(fp)
343 data = pickle.loads(fp)
343 parentctxnode, rules, keep, topmost, replacements = data
344 parentctxnode, rules, keep, topmost, replacements = data
344 backupfile = None
345 backupfile = None
345 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
346 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
346
347
347 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
348 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
348 "topmost": topmost, "replacements": replacements,
349 "topmost": topmost, "replacements": replacements,
349 "backupfile": backupfile}
350 "backupfile": backupfile}
350
351
351 def write(self, tr=None):
352 def write(self, tr=None):
352 if tr:
353 if tr:
353 tr.addfilegenerator('histedit-state', ('histedit-state',),
354 tr.addfilegenerator('histedit-state', ('histedit-state',),
354 self._write, location='plain')
355 self._write, location='plain')
355 else:
356 else:
356 with self.repo.vfs("histedit-state", "w") as f:
357 with self.repo.vfs("histedit-state", "w") as f:
357 self._write(f)
358 self._write(f)
358
359
359 def _write(self, fp):
360 def _write(self, fp):
360 fp.write('v1\n')
361 fp.write('v1\n')
361 fp.write('%s\n' % node.hex(self.parentctxnode))
362 fp.write('%s\n' % node.hex(self.parentctxnode))
362 fp.write('%s\n' % node.hex(self.topmost))
363 fp.write('%s\n' % node.hex(self.topmost))
363 fp.write('%s\n' % ('True' if self.keep else 'False'))
364 fp.write('%s\n' % ('True' if self.keep else 'False'))
364 fp.write('%d\n' % len(self.actions))
365 fp.write('%d\n' % len(self.actions))
365 for action in self.actions:
366 for action in self.actions:
366 fp.write('%s\n' % action.tostate())
367 fp.write('%s\n' % action.tostate())
367 fp.write('%d\n' % len(self.replacements))
368 fp.write('%d\n' % len(self.replacements))
368 for replacement in self.replacements:
369 for replacement in self.replacements:
369 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
370 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
370 for r in replacement[1])))
371 for r in replacement[1])))
371 backupfile = self.backupfile
372 backupfile = self.backupfile
372 if not backupfile:
373 if not backupfile:
373 backupfile = ''
374 backupfile = ''
374 fp.write('%s\n' % backupfile)
375 fp.write('%s\n' % backupfile)
375
376
376 def _load(self):
377 def _load(self):
377 fp = self.repo.vfs('histedit-state', 'r')
378 fp = self.repo.vfs('histedit-state', 'r')
378 lines = [l[:-1] for l in fp.readlines()]
379 lines = [l[:-1] for l in fp.readlines()]
379
380
380 index = 0
381 index = 0
381 lines[index] # version number
382 lines[index] # version number
382 index += 1
383 index += 1
383
384
384 parentctxnode = node.bin(lines[index])
385 parentctxnode = node.bin(lines[index])
385 index += 1
386 index += 1
386
387
387 topmost = node.bin(lines[index])
388 topmost = node.bin(lines[index])
388 index += 1
389 index += 1
389
390
390 keep = lines[index] == 'True'
391 keep = lines[index] == 'True'
391 index += 1
392 index += 1
392
393
393 # Rules
394 # Rules
394 rules = []
395 rules = []
395 rulelen = int(lines[index])
396 rulelen = int(lines[index])
396 index += 1
397 index += 1
397 for i in pycompat.xrange(rulelen):
398 for i in pycompat.xrange(rulelen):
398 ruleaction = lines[index]
399 ruleaction = lines[index]
399 index += 1
400 index += 1
400 rule = lines[index]
401 rule = lines[index]
401 index += 1
402 index += 1
402 rules.append((ruleaction, rule))
403 rules.append((ruleaction, rule))
403
404
404 # Replacements
405 # Replacements
405 replacements = []
406 replacements = []
406 replacementlen = int(lines[index])
407 replacementlen = int(lines[index])
407 index += 1
408 index += 1
408 for i in pycompat.xrange(replacementlen):
409 for i in pycompat.xrange(replacementlen):
409 replacement = lines[index]
410 replacement = lines[index]
410 original = node.bin(replacement[:40])
411 original = node.bin(replacement[:40])
411 succ = [node.bin(replacement[i:i + 40]) for i in
412 succ = [node.bin(replacement[i:i + 40]) for i in
412 range(40, len(replacement), 40)]
413 range(40, len(replacement), 40)]
413 replacements.append((original, succ))
414 replacements.append((original, succ))
414 index += 1
415 index += 1
415
416
416 backupfile = lines[index]
417 backupfile = lines[index]
417 index += 1
418 index += 1
418
419
419 fp.close()
420 fp.close()
420
421
421 return parentctxnode, rules, keep, topmost, replacements, backupfile
422 return parentctxnode, rules, keep, topmost, replacements, backupfile
422
423
423 def clear(self):
424 def clear(self):
424 if self.inprogress():
425 if self.inprogress():
425 self.repo.vfs.unlink('histedit-state')
426 self.repo.vfs.unlink('histedit-state')
426
427
427 def inprogress(self):
428 def inprogress(self):
428 return self.repo.vfs.exists('histedit-state')
429 return self.repo.vfs.exists('histedit-state')
429
430
430
431
431 class histeditaction(object):
432 class histeditaction(object):
432 def __init__(self, state, node):
433 def __init__(self, state, node):
433 self.state = state
434 self.state = state
434 self.repo = state.repo
435 self.repo = state.repo
435 self.node = node
436 self.node = node
436
437
437 @classmethod
438 @classmethod
438 def fromrule(cls, state, rule):
439 def fromrule(cls, state, rule):
439 """Parses the given rule, returning an instance of the histeditaction.
440 """Parses the given rule, returning an instance of the histeditaction.
440 """
441 """
441 ruleid = rule.strip().split(' ', 1)[0]
442 ruleid = rule.strip().split(' ', 1)[0]
442 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
443 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
443 # Check for validation of rule ids and get the rulehash
444 # Check for validation of rule ids and get the rulehash
444 try:
445 try:
445 rev = node.bin(ruleid)
446 rev = node.bin(ruleid)
446 except TypeError:
447 except TypeError:
447 try:
448 try:
448 _ctx = scmutil.revsingle(state.repo, ruleid)
449 _ctx = scmutil.revsingle(state.repo, ruleid)
449 rulehash = _ctx.hex()
450 rulehash = _ctx.hex()
450 rev = node.bin(rulehash)
451 rev = node.bin(rulehash)
451 except error.RepoLookupError:
452 except error.RepoLookupError:
452 raise error.ParseError(_("invalid changeset %s") % ruleid)
453 raise error.ParseError(_("invalid changeset %s") % ruleid)
453 return cls(state, rev)
454 return cls(state, rev)
454
455
455 def verify(self, prev, expected, seen):
456 def verify(self, prev, expected, seen):
456 """ Verifies semantic correctness of the rule"""
457 """ Verifies semantic correctness of the rule"""
457 repo = self.repo
458 repo = self.repo
458 ha = node.hex(self.node)
459 ha = node.hex(self.node)
459 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
460 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
460 if self.node is None:
461 if self.node is None:
461 raise error.ParseError(_('unknown changeset %s listed') % ha[:12])
462 raise error.ParseError(_('unknown changeset %s listed') % ha[:12])
462 self._verifynodeconstraints(prev, expected, seen)
463 self._verifynodeconstraints(prev, expected, seen)
463
464
464 def _verifynodeconstraints(self, prev, expected, seen):
465 def _verifynodeconstraints(self, prev, expected, seen):
465 # by default command need a node in the edited list
466 # by default command need a node in the edited list
466 if self.node not in expected:
467 if self.node not in expected:
467 raise error.ParseError(_('%s "%s" changeset was not a candidate')
468 raise error.ParseError(_('%s "%s" changeset was not a candidate')
468 % (self.verb, node.short(self.node)),
469 % (self.verb, node.short(self.node)),
469 hint=_('only use listed changesets'))
470 hint=_('only use listed changesets'))
470 # and only one command per node
471 # and only one command per node
471 if self.node in seen:
472 if self.node in seen:
472 raise error.ParseError(_('duplicated command for changeset %s') %
473 raise error.ParseError(_('duplicated command for changeset %s') %
473 node.short(self.node))
474 node.short(self.node))
474
475
475 def torule(self):
476 def torule(self):
476 """build a histedit rule line for an action
477 """build a histedit rule line for an action
477
478
478 by default lines are in the form:
479 by default lines are in the form:
479 <hash> <rev> <summary>
480 <hash> <rev> <summary>
480 """
481 """
481 ctx = self.repo[self.node]
482 ctx = self.repo[self.node]
482 summary = _getsummary(ctx)
483 summary = _getsummary(ctx)
483 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
484 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
484 # trim to 75 columns by default so it's not stupidly wide in my editor
485 # trim to 75 columns by default so it's not stupidly wide in my editor
485 # (the 5 more are left for verb)
486 # (the 5 more are left for verb)
486 maxlen = self.repo.ui.configint('histedit', 'linelen')
487 maxlen = self.repo.ui.configint('histedit', 'linelen')
487 maxlen = max(maxlen, 22) # avoid truncating hash
488 maxlen = max(maxlen, 22) # avoid truncating hash
488 return stringutil.ellipsis(line, maxlen)
489 return stringutil.ellipsis(line, maxlen)
489
490
490 def tostate(self):
491 def tostate(self):
491 """Print an action in format used by histedit state files
492 """Print an action in format used by histedit state files
492 (the first line is a verb, the remainder is the second)
493 (the first line is a verb, the remainder is the second)
493 """
494 """
494 return "%s\n%s" % (self.verb, node.hex(self.node))
495 return "%s\n%s" % (self.verb, node.hex(self.node))
495
496
496 def run(self):
497 def run(self):
497 """Runs the action. The default behavior is simply apply the action's
498 """Runs the action. The default behavior is simply apply the action's
498 rulectx onto the current parentctx."""
499 rulectx onto the current parentctx."""
499 self.applychange()
500 self.applychange()
500 self.continuedirty()
501 self.continuedirty()
501 return self.continueclean()
502 return self.continueclean()
502
503
503 def applychange(self):
504 def applychange(self):
504 """Applies the changes from this action's rulectx onto the current
505 """Applies the changes from this action's rulectx onto the current
505 parentctx, but does not commit them."""
506 parentctx, but does not commit them."""
506 repo = self.repo
507 repo = self.repo
507 rulectx = repo[self.node]
508 rulectx = repo[self.node]
508 repo.ui.pushbuffer(error=True, labeled=True)
509 repo.ui.pushbuffer(error=True, labeled=True)
509 hg.update(repo, self.state.parentctxnode, quietempty=True)
510 hg.update(repo, self.state.parentctxnode, quietempty=True)
510 stats = applychanges(repo.ui, repo, rulectx, {})
511 stats = applychanges(repo.ui, repo, rulectx, {})
511 repo.dirstate.setbranch(rulectx.branch())
512 repo.dirstate.setbranch(rulectx.branch())
512 if stats.unresolvedcount:
513 if stats.unresolvedcount:
513 buf = repo.ui.popbuffer()
514 buf = repo.ui.popbuffer()
514 repo.ui.write(buf)
515 repo.ui.write(buf)
515 raise error.InterventionRequired(
516 raise error.InterventionRequired(
516 _('Fix up the change (%s %s)') %
517 _('Fix up the change (%s %s)') %
517 (self.verb, node.short(self.node)),
518 (self.verb, node.short(self.node)),
518 hint=_('hg histedit --continue to resume'))
519 hint=_('hg histedit --continue to resume'))
519 else:
520 else:
520 repo.ui.popbuffer()
521 repo.ui.popbuffer()
521
522
522 def continuedirty(self):
523 def continuedirty(self):
523 """Continues the action when changes have been applied to the working
524 """Continues the action when changes have been applied to the working
524 copy. The default behavior is to commit the dirty changes."""
525 copy. The default behavior is to commit the dirty changes."""
525 repo = self.repo
526 repo = self.repo
526 rulectx = repo[self.node]
527 rulectx = repo[self.node]
527
528
528 editor = self.commiteditor()
529 editor = self.commiteditor()
529 commit = commitfuncfor(repo, rulectx)
530 commit = commitfuncfor(repo, rulectx)
530
531 if repo.ui.configbool('rewrite', 'update-timestamp'):
532 date = dateutil.makedate()
533 else:
534 date = rulectx.date()
531 commit(text=rulectx.description(), user=rulectx.user(),
535 commit(text=rulectx.description(), user=rulectx.user(),
532 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
536 date=date, extra=rulectx.extra(), editor=editor)
533
537
534 def commiteditor(self):
538 def commiteditor(self):
535 """The editor to be used to edit the commit message."""
539 """The editor to be used to edit the commit message."""
536 return False
540 return False
537
541
538 def continueclean(self):
542 def continueclean(self):
539 """Continues the action when the working copy is clean. The default
543 """Continues the action when the working copy is clean. The default
540 behavior is to accept the current commit as the new version of the
544 behavior is to accept the current commit as the new version of the
541 rulectx."""
545 rulectx."""
542 ctx = self.repo['.']
546 ctx = self.repo['.']
543 if ctx.node() == self.state.parentctxnode:
547 if ctx.node() == self.state.parentctxnode:
544 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
548 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
545 node.short(self.node))
549 node.short(self.node))
546 return ctx, [(self.node, tuple())]
550 return ctx, [(self.node, tuple())]
547 if ctx.node() == self.node:
551 if ctx.node() == self.node:
548 # Nothing changed
552 # Nothing changed
549 return ctx, []
553 return ctx, []
550 return ctx, [(self.node, (ctx.node(),))]
554 return ctx, [(self.node, (ctx.node(),))]
551
555
552 def commitfuncfor(repo, src):
556 def commitfuncfor(repo, src):
553 """Build a commit function for the replacement of <src>
557 """Build a commit function for the replacement of <src>
554
558
555 This function ensure we apply the same treatment to all changesets.
559 This function ensure we apply the same treatment to all changesets.
556
560
557 - Add a 'histedit_source' entry in extra.
561 - Add a 'histedit_source' entry in extra.
558
562
559 Note that fold has its own separated logic because its handling is a bit
563 Note that fold has its own separated logic because its handling is a bit
560 different and not easily factored out of the fold method.
564 different and not easily factored out of the fold method.
561 """
565 """
562 phasemin = src.phase()
566 phasemin = src.phase()
563 def commitfunc(**kwargs):
567 def commitfunc(**kwargs):
564 overrides = {('phases', 'new-commit'): phasemin}
568 overrides = {('phases', 'new-commit'): phasemin}
565 with repo.ui.configoverride(overrides, 'histedit'):
569 with repo.ui.configoverride(overrides, 'histedit'):
566 extra = kwargs.get(r'extra', {}).copy()
570 extra = kwargs.get(r'extra', {}).copy()
567 extra['histedit_source'] = src.hex()
571 extra['histedit_source'] = src.hex()
568 kwargs[r'extra'] = extra
572 kwargs[r'extra'] = extra
569 return repo.commit(**kwargs)
573 return repo.commit(**kwargs)
570 return commitfunc
574 return commitfunc
571
575
572 def applychanges(ui, repo, ctx, opts):
576 def applychanges(ui, repo, ctx, opts):
573 """Merge changeset from ctx (only) in the current working directory"""
577 """Merge changeset from ctx (only) in the current working directory"""
574 wcpar = repo.dirstate.parents()[0]
578 wcpar = repo.dirstate.parents()[0]
575 if ctx.p1().node() == wcpar:
579 if ctx.p1().node() == wcpar:
576 # edits are "in place" we do not need to make any merge,
580 # edits are "in place" we do not need to make any merge,
577 # just applies changes on parent for editing
581 # just applies changes on parent for editing
578 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
582 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
579 stats = mergemod.updateresult(0, 0, 0, 0)
583 stats = mergemod.updateresult(0, 0, 0, 0)
580 else:
584 else:
581 try:
585 try:
582 # ui.forcemerge is an internal variable, do not document
586 # ui.forcemerge is an internal variable, do not document
583 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
587 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
584 'histedit')
588 'histedit')
585 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
589 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
586 finally:
590 finally:
587 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
591 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
588 return stats
592 return stats
589
593
590 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
594 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
591 """collapse the set of revisions from first to last as new one.
595 """collapse the set of revisions from first to last as new one.
592
596
593 Expected commit options are:
597 Expected commit options are:
594 - message
598 - message
595 - date
599 - date
596 - username
600 - username
597 Commit message is edited in all cases.
601 Commit message is edited in all cases.
598
602
599 This function works in memory."""
603 This function works in memory."""
600 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
604 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
601 if not ctxs:
605 if not ctxs:
602 return None
606 return None
603 for c in ctxs:
607 for c in ctxs:
604 if not c.mutable():
608 if not c.mutable():
605 raise error.ParseError(
609 raise error.ParseError(
606 _("cannot fold into public change %s") % node.short(c.node()))
610 _("cannot fold into public change %s") % node.short(c.node()))
607 base = firstctx.parents()[0]
611 base = firstctx.parents()[0]
608
612
609 # commit a new version of the old changeset, including the update
613 # commit a new version of the old changeset, including the update
610 # collect all files which might be affected
614 # collect all files which might be affected
611 files = set()
615 files = set()
612 for ctx in ctxs:
616 for ctx in ctxs:
613 files.update(ctx.files())
617 files.update(ctx.files())
614
618
615 # Recompute copies (avoid recording a -> b -> a)
619 # Recompute copies (avoid recording a -> b -> a)
616 copied = copies.pathcopies(base, lastctx)
620 copied = copies.pathcopies(base, lastctx)
617
621
618 # prune files which were reverted by the updates
622 # prune files which were reverted by the updates
619 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
623 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
620 # commit version of these files as defined by head
624 # commit version of these files as defined by head
621 headmf = lastctx.manifest()
625 headmf = lastctx.manifest()
622 def filectxfn(repo, ctx, path):
626 def filectxfn(repo, ctx, path):
623 if path in headmf:
627 if path in headmf:
624 fctx = lastctx[path]
628 fctx = lastctx[path]
625 flags = fctx.flags()
629 flags = fctx.flags()
626 mctx = context.memfilectx(repo, ctx,
630 mctx = context.memfilectx(repo, ctx,
627 fctx.path(), fctx.data(),
631 fctx.path(), fctx.data(),
628 islink='l' in flags,
632 islink='l' in flags,
629 isexec='x' in flags,
633 isexec='x' in flags,
630 copied=copied.get(path))
634 copied=copied.get(path))
631 return mctx
635 return mctx
632 return None
636 return None
633
637
634 if commitopts.get('message'):
638 if commitopts.get('message'):
635 message = commitopts['message']
639 message = commitopts['message']
636 else:
640 else:
637 message = firstctx.description()
641 message = firstctx.description()
638 user = commitopts.get('user')
642 user = commitopts.get('user')
639 date = commitopts.get('date')
643 date = commitopts.get('date')
640 extra = commitopts.get('extra')
644 extra = commitopts.get('extra')
641
645
642 parents = (firstctx.p1().node(), firstctx.p2().node())
646 parents = (firstctx.p1().node(), firstctx.p2().node())
643 editor = None
647 editor = None
644 if not skipprompt:
648 if not skipprompt:
645 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
649 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
646 new = context.memctx(repo,
650 new = context.memctx(repo,
647 parents=parents,
651 parents=parents,
648 text=message,
652 text=message,
649 files=files,
653 files=files,
650 filectxfn=filectxfn,
654 filectxfn=filectxfn,
651 user=user,
655 user=user,
652 date=date,
656 date=date,
653 extra=extra,
657 extra=extra,
654 editor=editor)
658 editor=editor)
655 return repo.commitctx(new)
659 return repo.commitctx(new)
656
660
657 def _isdirtywc(repo):
661 def _isdirtywc(repo):
658 return repo[None].dirty(missing=True)
662 return repo[None].dirty(missing=True)
659
663
660 def abortdirty():
664 def abortdirty():
661 raise error.Abort(_('working copy has pending changes'),
665 raise error.Abort(_('working copy has pending changes'),
662 hint=_('amend, commit, or revert them and run histedit '
666 hint=_('amend, commit, or revert them and run histedit '
663 '--continue, or abort with histedit --abort'))
667 '--continue, or abort with histedit --abort'))
664
668
665 def action(verbs, message, priority=False, internal=False):
669 def action(verbs, message, priority=False, internal=False):
666 def wrap(cls):
670 def wrap(cls):
667 assert not priority or not internal
671 assert not priority or not internal
668 verb = verbs[0]
672 verb = verbs[0]
669 if priority:
673 if priority:
670 primaryactions.add(verb)
674 primaryactions.add(verb)
671 elif internal:
675 elif internal:
672 internalactions.add(verb)
676 internalactions.add(verb)
673 elif len(verbs) > 1:
677 elif len(verbs) > 1:
674 secondaryactions.add(verb)
678 secondaryactions.add(verb)
675 else:
679 else:
676 tertiaryactions.add(verb)
680 tertiaryactions.add(verb)
677
681
678 cls.verb = verb
682 cls.verb = verb
679 cls.verbs = verbs
683 cls.verbs = verbs
680 cls.message = message
684 cls.message = message
681 for verb in verbs:
685 for verb in verbs:
682 actiontable[verb] = cls
686 actiontable[verb] = cls
683 return cls
687 return cls
684 return wrap
688 return wrap
685
689
686 @action(['pick', 'p'],
690 @action(['pick', 'p'],
687 _('use commit'),
691 _('use commit'),
688 priority=True)
692 priority=True)
689 class pick(histeditaction):
693 class pick(histeditaction):
690 def run(self):
694 def run(self):
691 rulectx = self.repo[self.node]
695 rulectx = self.repo[self.node]
692 if rulectx.parents()[0].node() == self.state.parentctxnode:
696 if rulectx.parents()[0].node() == self.state.parentctxnode:
693 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
697 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
694 return rulectx, []
698 return rulectx, []
695
699
696 return super(pick, self).run()
700 return super(pick, self).run()
697
701
698 @action(['edit', 'e'],
702 @action(['edit', 'e'],
699 _('use commit, but stop for amending'),
703 _('use commit, but stop for amending'),
700 priority=True)
704 priority=True)
701 class edit(histeditaction):
705 class edit(histeditaction):
702 def run(self):
706 def run(self):
703 repo = self.repo
707 repo = self.repo
704 rulectx = repo[self.node]
708 rulectx = repo[self.node]
705 hg.update(repo, self.state.parentctxnode, quietempty=True)
709 hg.update(repo, self.state.parentctxnode, quietempty=True)
706 applychanges(repo.ui, repo, rulectx, {})
710 applychanges(repo.ui, repo, rulectx, {})
707 raise error.InterventionRequired(
711 raise error.InterventionRequired(
708 _('Editing (%s), you may commit or record as needed now.')
712 _('Editing (%s), you may commit or record as needed now.')
709 % node.short(self.node),
713 % node.short(self.node),
710 hint=_('hg histedit --continue to resume'))
714 hint=_('hg histedit --continue to resume'))
711
715
712 def commiteditor(self):
716 def commiteditor(self):
713 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
717 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
714
718
715 @action(['fold', 'f'],
719 @action(['fold', 'f'],
716 _('use commit, but combine it with the one above'))
720 _('use commit, but combine it with the one above'))
717 class fold(histeditaction):
721 class fold(histeditaction):
718 def verify(self, prev, expected, seen):
722 def verify(self, prev, expected, seen):
719 """ Verifies semantic correctness of the fold rule"""
723 """ Verifies semantic correctness of the fold rule"""
720 super(fold, self).verify(prev, expected, seen)
724 super(fold, self).verify(prev, expected, seen)
721 repo = self.repo
725 repo = self.repo
722 if not prev:
726 if not prev:
723 c = repo[self.node].parents()[0]
727 c = repo[self.node].parents()[0]
724 elif not prev.verb in ('pick', 'base'):
728 elif not prev.verb in ('pick', 'base'):
725 return
729 return
726 else:
730 else:
727 c = repo[prev.node]
731 c = repo[prev.node]
728 if not c.mutable():
732 if not c.mutable():
729 raise error.ParseError(
733 raise error.ParseError(
730 _("cannot fold into public change %s") % node.short(c.node()))
734 _("cannot fold into public change %s") % node.short(c.node()))
731
735
732
736
733 def continuedirty(self):
737 def continuedirty(self):
734 repo = self.repo
738 repo = self.repo
735 rulectx = repo[self.node]
739 rulectx = repo[self.node]
736
740
737 commit = commitfuncfor(repo, rulectx)
741 commit = commitfuncfor(repo, rulectx)
738 commit(text='fold-temp-revision %s' % node.short(self.node),
742 commit(text='fold-temp-revision %s' % node.short(self.node),
739 user=rulectx.user(), date=rulectx.date(),
743 user=rulectx.user(), date=rulectx.date(),
740 extra=rulectx.extra())
744 extra=rulectx.extra())
741
745
742 def continueclean(self):
746 def continueclean(self):
743 repo = self.repo
747 repo = self.repo
744 ctx = repo['.']
748 ctx = repo['.']
745 rulectx = repo[self.node]
749 rulectx = repo[self.node]
746 parentctxnode = self.state.parentctxnode
750 parentctxnode = self.state.parentctxnode
747 if ctx.node() == parentctxnode:
751 if ctx.node() == parentctxnode:
748 repo.ui.warn(_('%s: empty changeset\n') %
752 repo.ui.warn(_('%s: empty changeset\n') %
749 node.short(self.node))
753 node.short(self.node))
750 return ctx, [(self.node, (parentctxnode,))]
754 return ctx, [(self.node, (parentctxnode,))]
751
755
752 parentctx = repo[parentctxnode]
756 parentctx = repo[parentctxnode]
753 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
757 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
754 parentctx.rev(),
758 parentctx.rev(),
755 parentctx.rev()))
759 parentctx.rev()))
756 if not newcommits:
760 if not newcommits:
757 repo.ui.warn(_('%s: cannot fold - working copy is not a '
761 repo.ui.warn(_('%s: cannot fold - working copy is not a '
758 'descendant of previous commit %s\n') %
762 'descendant of previous commit %s\n') %
759 (node.short(self.node), node.short(parentctxnode)))
763 (node.short(self.node), node.short(parentctxnode)))
760 return ctx, [(self.node, (ctx.node(),))]
764 return ctx, [(self.node, (ctx.node(),))]
761
765
762 middlecommits = newcommits.copy()
766 middlecommits = newcommits.copy()
763 middlecommits.discard(ctx.node())
767 middlecommits.discard(ctx.node())
764
768
765 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
769 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
766 middlecommits)
770 middlecommits)
767
771
768 def skipprompt(self):
772 def skipprompt(self):
769 """Returns true if the rule should skip the message editor.
773 """Returns true if the rule should skip the message editor.
770
774
771 For example, 'fold' wants to show an editor, but 'rollup'
775 For example, 'fold' wants to show an editor, but 'rollup'
772 doesn't want to.
776 doesn't want to.
773 """
777 """
774 return False
778 return False
775
779
776 def mergedescs(self):
780 def mergedescs(self):
777 """Returns true if the rule should merge messages of multiple changes.
781 """Returns true if the rule should merge messages of multiple changes.
778
782
779 This exists mainly so that 'rollup' rules can be a subclass of
783 This exists mainly so that 'rollup' rules can be a subclass of
780 'fold'.
784 'fold'.
781 """
785 """
782 return True
786 return True
783
787
784 def firstdate(self):
788 def firstdate(self):
785 """Returns true if the rule should preserve the date of the first
789 """Returns true if the rule should preserve the date of the first
786 change.
790 change.
787
791
788 This exists mainly so that 'rollup' rules can be a subclass of
792 This exists mainly so that 'rollup' rules can be a subclass of
789 'fold'.
793 'fold'.
790 """
794 """
791 return False
795 return False
792
796
793 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
797 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
794 parent = ctx.parents()[0].node()
798 parent = ctx.parents()[0].node()
795 hg.updaterepo(repo, parent, overwrite=False)
799 hg.updaterepo(repo, parent, overwrite=False)
796 ### prepare new commit data
800 ### prepare new commit data
797 commitopts = {}
801 commitopts = {}
798 commitopts['user'] = ctx.user()
802 commitopts['user'] = ctx.user()
799 # commit message
803 # commit message
800 if not self.mergedescs():
804 if not self.mergedescs():
801 newmessage = ctx.description()
805 newmessage = ctx.description()
802 else:
806 else:
803 newmessage = '\n***\n'.join(
807 newmessage = '\n***\n'.join(
804 [ctx.description()] +
808 [ctx.description()] +
805 [repo[r].description() for r in internalchanges] +
809 [repo[r].description() for r in internalchanges] +
806 [oldctx.description()]) + '\n'
810 [oldctx.description()]) + '\n'
807 commitopts['message'] = newmessage
811 commitopts['message'] = newmessage
808 # date
812 # date
809 if self.firstdate():
813 if self.firstdate():
810 commitopts['date'] = ctx.date()
814 commitopts['date'] = ctx.date()
811 else:
815 else:
812 commitopts['date'] = max(ctx.date(), oldctx.date())
816 commitopts['date'] = max(ctx.date(), oldctx.date())
817 # if date is to be updated to current
818 if ui.configbool('rewrite', 'update-timestamp'):
819 commitopts['date'] = dateutil.makedate()
820
813 extra = ctx.extra().copy()
821 extra = ctx.extra().copy()
814 # histedit_source
822 # histedit_source
815 # note: ctx is likely a temporary commit but that the best we can do
823 # note: ctx is likely a temporary commit but that the best we can do
816 # here. This is sufficient to solve issue3681 anyway.
824 # here. This is sufficient to solve issue3681 anyway.
817 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
825 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
818 commitopts['extra'] = extra
826 commitopts['extra'] = extra
819 phasemin = max(ctx.phase(), oldctx.phase())
827 phasemin = max(ctx.phase(), oldctx.phase())
820 overrides = {('phases', 'new-commit'): phasemin}
828 overrides = {('phases', 'new-commit'): phasemin}
821 with repo.ui.configoverride(overrides, 'histedit'):
829 with repo.ui.configoverride(overrides, 'histedit'):
822 n = collapse(repo, ctx, repo[newnode], commitopts,
830 n = collapse(repo, ctx, repo[newnode], commitopts,
823 skipprompt=self.skipprompt())
831 skipprompt=self.skipprompt())
824 if n is None:
832 if n is None:
825 return ctx, []
833 return ctx, []
826 hg.updaterepo(repo, n, overwrite=False)
834 hg.updaterepo(repo, n, overwrite=False)
827 replacements = [(oldctx.node(), (newnode,)),
835 replacements = [(oldctx.node(), (newnode,)),
828 (ctx.node(), (n,)),
836 (ctx.node(), (n,)),
829 (newnode, (n,)),
837 (newnode, (n,)),
830 ]
838 ]
831 for ich in internalchanges:
839 for ich in internalchanges:
832 replacements.append((ich, (n,)))
840 replacements.append((ich, (n,)))
833 return repo[n], replacements
841 return repo[n], replacements
834
842
835 @action(['base', 'b'],
843 @action(['base', 'b'],
836 _('checkout changeset and apply further changesets from there'))
844 _('checkout changeset and apply further changesets from there'))
837 class base(histeditaction):
845 class base(histeditaction):
838
846
839 def run(self):
847 def run(self):
840 if self.repo['.'].node() != self.node:
848 if self.repo['.'].node() != self.node:
841 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
849 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
842 return self.continueclean()
850 return self.continueclean()
843
851
844 def continuedirty(self):
852 def continuedirty(self):
845 abortdirty()
853 abortdirty()
846
854
847 def continueclean(self):
855 def continueclean(self):
848 basectx = self.repo['.']
856 basectx = self.repo['.']
849 return basectx, []
857 return basectx, []
850
858
851 def _verifynodeconstraints(self, prev, expected, seen):
859 def _verifynodeconstraints(self, prev, expected, seen):
852 # base can only be use with a node not in the edited set
860 # base can only be use with a node not in the edited set
853 if self.node in expected:
861 if self.node in expected:
854 msg = _('%s "%s" changeset was an edited list candidate')
862 msg = _('%s "%s" changeset was an edited list candidate')
855 raise error.ParseError(
863 raise error.ParseError(
856 msg % (self.verb, node.short(self.node)),
864 msg % (self.verb, node.short(self.node)),
857 hint=_('base must only use unlisted changesets'))
865 hint=_('base must only use unlisted changesets'))
858
866
859 @action(['_multifold'],
867 @action(['_multifold'],
860 _(
868 _(
861 """fold subclass used for when multiple folds happen in a row
869 """fold subclass used for when multiple folds happen in a row
862
870
863 We only want to fire the editor for the folded message once when
871 We only want to fire the editor for the folded message once when
864 (say) four changes are folded down into a single change. This is
872 (say) four changes are folded down into a single change. This is
865 similar to rollup, but we should preserve both messages so that
873 similar to rollup, but we should preserve both messages so that
866 when the last fold operation runs we can show the user all the
874 when the last fold operation runs we can show the user all the
867 commit messages in their editor.
875 commit messages in their editor.
868 """),
876 """),
869 internal=True)
877 internal=True)
870 class _multifold(fold):
878 class _multifold(fold):
871 def skipprompt(self):
879 def skipprompt(self):
872 return True
880 return True
873
881
874 @action(["roll", "r"],
882 @action(["roll", "r"],
875 _("like fold, but discard this commit's description and date"))
883 _("like fold, but discard this commit's description and date"))
876 class rollup(fold):
884 class rollup(fold):
877 def mergedescs(self):
885 def mergedescs(self):
878 return False
886 return False
879
887
880 def skipprompt(self):
888 def skipprompt(self):
881 return True
889 return True
882
890
883 def firstdate(self):
891 def firstdate(self):
884 return True
892 return True
885
893
886 @action(["drop", "d"],
894 @action(["drop", "d"],
887 _('remove commit from history'))
895 _('remove commit from history'))
888 class drop(histeditaction):
896 class drop(histeditaction):
889 def run(self):
897 def run(self):
890 parentctx = self.repo[self.state.parentctxnode]
898 parentctx = self.repo[self.state.parentctxnode]
891 return parentctx, [(self.node, tuple())]
899 return parentctx, [(self.node, tuple())]
892
900
893 @action(["mess", "m"],
901 @action(["mess", "m"],
894 _('edit commit message without changing commit content'),
902 _('edit commit message without changing commit content'),
895 priority=True)
903 priority=True)
896 class message(histeditaction):
904 class message(histeditaction):
897 def commiteditor(self):
905 def commiteditor(self):
898 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
906 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
899
907
900 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
908 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
901 """utility function to find the first outgoing changeset
909 """utility function to find the first outgoing changeset
902
910
903 Used by initialization code"""
911 Used by initialization code"""
904 if opts is None:
912 if opts is None:
905 opts = {}
913 opts = {}
906 dest = ui.expandpath(remote or 'default-push', remote or 'default')
914 dest = ui.expandpath(remote or 'default-push', remote or 'default')
907 dest, branches = hg.parseurl(dest, None)[:2]
915 dest, branches = hg.parseurl(dest, None)[:2]
908 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
916 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
909
917
910 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
918 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
911 other = hg.peer(repo, opts, dest)
919 other = hg.peer(repo, opts, dest)
912
920
913 if revs:
921 if revs:
914 revs = [repo.lookup(rev) for rev in revs]
922 revs = [repo.lookup(rev) for rev in revs]
915
923
916 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
924 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
917 if not outgoing.missing:
925 if not outgoing.missing:
918 raise error.Abort(_('no outgoing ancestors'))
926 raise error.Abort(_('no outgoing ancestors'))
919 roots = list(repo.revs("roots(%ln)", outgoing.missing))
927 roots = list(repo.revs("roots(%ln)", outgoing.missing))
920 if len(roots) > 1:
928 if len(roots) > 1:
921 msg = _('there are ambiguous outgoing revisions')
929 msg = _('there are ambiguous outgoing revisions')
922 hint = _("see 'hg help histedit' for more detail")
930 hint = _("see 'hg help histedit' for more detail")
923 raise error.Abort(msg, hint=hint)
931 raise error.Abort(msg, hint=hint)
924 return repo[roots[0]].node()
932 return repo[roots[0]].node()
925
933
926 # Curses Support
934 # Curses Support
927 try:
935 try:
928 import curses
936 import curses
929 except ImportError:
937 except ImportError:
930 curses = None
938 curses = None
931
939
932 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
940 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
933 ACTION_LABELS = {
941 ACTION_LABELS = {
934 'fold': '^fold',
942 'fold': '^fold',
935 'roll': '^roll',
943 'roll': '^roll',
936 }
944 }
937
945
938 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN = 1, 2, 3, 4
946 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN = 1, 2, 3, 4
939
947
940 E_QUIT, E_HISTEDIT = 1, 2
948 E_QUIT, E_HISTEDIT = 1, 2
941 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
949 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
942 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
950 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
943
951
944 KEYTABLE = {
952 KEYTABLE = {
945 'global': {
953 'global': {
946 'h': 'next-action',
954 'h': 'next-action',
947 'KEY_RIGHT': 'next-action',
955 'KEY_RIGHT': 'next-action',
948 'l': 'prev-action',
956 'l': 'prev-action',
949 'KEY_LEFT': 'prev-action',
957 'KEY_LEFT': 'prev-action',
950 'q': 'quit',
958 'q': 'quit',
951 'c': 'histedit',
959 'c': 'histedit',
952 'C': 'histedit',
960 'C': 'histedit',
953 'v': 'showpatch',
961 'v': 'showpatch',
954 '?': 'help',
962 '?': 'help',
955 },
963 },
956 MODE_RULES: {
964 MODE_RULES: {
957 'd': 'action-drop',
965 'd': 'action-drop',
958 'e': 'action-edit',
966 'e': 'action-edit',
959 'f': 'action-fold',
967 'f': 'action-fold',
960 'm': 'action-mess',
968 'm': 'action-mess',
961 'p': 'action-pick',
969 'p': 'action-pick',
962 'r': 'action-roll',
970 'r': 'action-roll',
963 ' ': 'select',
971 ' ': 'select',
964 'j': 'down',
972 'j': 'down',
965 'k': 'up',
973 'k': 'up',
966 'KEY_DOWN': 'down',
974 'KEY_DOWN': 'down',
967 'KEY_UP': 'up',
975 'KEY_UP': 'up',
968 'J': 'move-down',
976 'J': 'move-down',
969 'K': 'move-up',
977 'K': 'move-up',
970 'KEY_NPAGE': 'move-down',
978 'KEY_NPAGE': 'move-down',
971 'KEY_PPAGE': 'move-up',
979 'KEY_PPAGE': 'move-up',
972 '0': 'goto', # Used for 0..9
980 '0': 'goto', # Used for 0..9
973 },
981 },
974 MODE_PATCH: {
982 MODE_PATCH: {
975 ' ': 'page-down',
983 ' ': 'page-down',
976 'KEY_NPAGE': 'page-down',
984 'KEY_NPAGE': 'page-down',
977 'KEY_PPAGE': 'page-up',
985 'KEY_PPAGE': 'page-up',
978 'j': 'line-down',
986 'j': 'line-down',
979 'k': 'line-up',
987 'k': 'line-up',
980 'KEY_DOWN': 'line-down',
988 'KEY_DOWN': 'line-down',
981 'KEY_UP': 'line-up',
989 'KEY_UP': 'line-up',
982 'J': 'down',
990 'J': 'down',
983 'K': 'up',
991 'K': 'up',
984 },
992 },
985 MODE_HELP: {
993 MODE_HELP: {
986 },
994 },
987 }
995 }
988
996
989 def screen_size():
997 def screen_size():
990 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' '))
998 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' '))
991
999
992 class histeditrule(object):
1000 class histeditrule(object):
993 def __init__(self, ctx, pos, action='pick'):
1001 def __init__(self, ctx, pos, action='pick'):
994 self.ctx = ctx
1002 self.ctx = ctx
995 self.action = action
1003 self.action = action
996 self.origpos = pos
1004 self.origpos = pos
997 self.pos = pos
1005 self.pos = pos
998 self.conflicts = []
1006 self.conflicts = []
999
1007
1000 def __str__(self):
1008 def __str__(self):
1001 # Some actions ('fold' and 'roll') combine a patch with a previous one.
1009 # Some actions ('fold' and 'roll') combine a patch with a previous one.
1002 # Add a marker showing which patch they apply to, and also omit the
1010 # Add a marker showing which patch they apply to, and also omit the
1003 # description for 'roll' (since it will get discarded). Example display:
1011 # description for 'roll' (since it will get discarded). Example display:
1004 #
1012 #
1005 # #10 pick 316392:06a16c25c053 add option to skip tests
1013 # #10 pick 316392:06a16c25c053 add option to skip tests
1006 # #11 ^roll 316393:71313c964cc5
1014 # #11 ^roll 316393:71313c964cc5
1007 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1015 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1008 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1016 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1009 #
1017 #
1010 # The carets point to the changeset being folded into ("roll this
1018 # The carets point to the changeset being folded into ("roll this
1011 # changeset into the changeset above").
1019 # changeset into the changeset above").
1012 action = ACTION_LABELS.get(self.action, self.action)
1020 action = ACTION_LABELS.get(self.action, self.action)
1013 h = self.ctx.hex()[0:12]
1021 h = self.ctx.hex()[0:12]
1014 r = self.ctx.rev()
1022 r = self.ctx.rev()
1015 desc = self.ctx.description().splitlines()[0].strip()
1023 desc = self.ctx.description().splitlines()[0].strip()
1016 if self.action == 'roll':
1024 if self.action == 'roll':
1017 desc = ''
1025 desc = ''
1018 return "#{0:<2} {1:<6} {2}:{3} {4}".format(
1026 return "#{0:<2} {1:<6} {2}:{3} {4}".format(
1019 self.origpos, action, r, h, desc)
1027 self.origpos, action, r, h, desc)
1020
1028
1021 def checkconflicts(self, other):
1029 def checkconflicts(self, other):
1022 if other.pos > self.pos and other.origpos <= self.origpos:
1030 if other.pos > self.pos and other.origpos <= self.origpos:
1023 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1031 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1024 self.conflicts.append(other)
1032 self.conflicts.append(other)
1025 return self.conflicts
1033 return self.conflicts
1026
1034
1027 if other in self.conflicts:
1035 if other in self.conflicts:
1028 self.conflicts.remove(other)
1036 self.conflicts.remove(other)
1029 return self.conflicts
1037 return self.conflicts
1030
1038
1031 # ============ EVENTS ===============
1039 # ============ EVENTS ===============
1032 def movecursor(state, oldpos, newpos):
1040 def movecursor(state, oldpos, newpos):
1033 '''Change the rule/changeset that the cursor is pointing to, regardless of
1041 '''Change the rule/changeset that the cursor is pointing to, regardless of
1034 current mode (you can switch between patches from the view patch window).'''
1042 current mode (you can switch between patches from the view patch window).'''
1035 state['pos'] = newpos
1043 state['pos'] = newpos
1036
1044
1037 mode, _ = state['mode']
1045 mode, _ = state['mode']
1038 if mode == MODE_RULES:
1046 if mode == MODE_RULES:
1039 # Scroll through the list by updating the view for MODE_RULES, so that
1047 # Scroll through the list by updating the view for MODE_RULES, so that
1040 # even if we are not currently viewing the rules, switching back will
1048 # even if we are not currently viewing the rules, switching back will
1041 # result in the cursor's rule being visible.
1049 # result in the cursor's rule being visible.
1042 modestate = state['modes'][MODE_RULES]
1050 modestate = state['modes'][MODE_RULES]
1043 if newpos < modestate['line_offset']:
1051 if newpos < modestate['line_offset']:
1044 modestate['line_offset'] = newpos
1052 modestate['line_offset'] = newpos
1045 elif newpos > modestate['line_offset'] + state['page_height'] - 1:
1053 elif newpos > modestate['line_offset'] + state['page_height'] - 1:
1046 modestate['line_offset'] = newpos - state['page_height'] + 1
1054 modestate['line_offset'] = newpos - state['page_height'] + 1
1047
1055
1048 # Reset the patch view region to the top of the new patch.
1056 # Reset the patch view region to the top of the new patch.
1049 state['modes'][MODE_PATCH]['line_offset'] = 0
1057 state['modes'][MODE_PATCH]['line_offset'] = 0
1050
1058
1051 def changemode(state, mode):
1059 def changemode(state, mode):
1052 curmode, _ = state['mode']
1060 curmode, _ = state['mode']
1053 state['mode'] = (mode, curmode)
1061 state['mode'] = (mode, curmode)
1054
1062
1055 def makeselection(state, pos):
1063 def makeselection(state, pos):
1056 state['selected'] = pos
1064 state['selected'] = pos
1057
1065
1058 def swap(state, oldpos, newpos):
1066 def swap(state, oldpos, newpos):
1059 """Swap two positions and calculate necessary conflicts in
1067 """Swap two positions and calculate necessary conflicts in
1060 O(|newpos-oldpos|) time"""
1068 O(|newpos-oldpos|) time"""
1061
1069
1062 rules = state['rules']
1070 rules = state['rules']
1063 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1071 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1064
1072
1065 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1073 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1066
1074
1067 # TODO: swap should not know about histeditrule's internals
1075 # TODO: swap should not know about histeditrule's internals
1068 rules[newpos].pos = newpos
1076 rules[newpos].pos = newpos
1069 rules[oldpos].pos = oldpos
1077 rules[oldpos].pos = oldpos
1070
1078
1071 start = min(oldpos, newpos)
1079 start = min(oldpos, newpos)
1072 end = max(oldpos, newpos)
1080 end = max(oldpos, newpos)
1073 for r in pycompat.xrange(start, end + 1):
1081 for r in pycompat.xrange(start, end + 1):
1074 rules[newpos].checkconflicts(rules[r])
1082 rules[newpos].checkconflicts(rules[r])
1075 rules[oldpos].checkconflicts(rules[r])
1083 rules[oldpos].checkconflicts(rules[r])
1076
1084
1077 if state['selected']:
1085 if state['selected']:
1078 makeselection(state, newpos)
1086 makeselection(state, newpos)
1079
1087
1080 def changeaction(state, pos, action):
1088 def changeaction(state, pos, action):
1081 """Change the action state on the given position to the new action"""
1089 """Change the action state on the given position to the new action"""
1082 rules = state['rules']
1090 rules = state['rules']
1083 assert 0 <= pos < len(rules)
1091 assert 0 <= pos < len(rules)
1084 rules[pos].action = action
1092 rules[pos].action = action
1085
1093
1086 def cycleaction(state, pos, next=False):
1094 def cycleaction(state, pos, next=False):
1087 """Changes the action state the next or the previous action from
1095 """Changes the action state the next or the previous action from
1088 the action list"""
1096 the action list"""
1089 rules = state['rules']
1097 rules = state['rules']
1090 assert 0 <= pos < len(rules)
1098 assert 0 <= pos < len(rules)
1091 current = rules[pos].action
1099 current = rules[pos].action
1092
1100
1093 assert current in KEY_LIST
1101 assert current in KEY_LIST
1094
1102
1095 index = KEY_LIST.index(current)
1103 index = KEY_LIST.index(current)
1096 if next:
1104 if next:
1097 index += 1
1105 index += 1
1098 else:
1106 else:
1099 index -= 1
1107 index -= 1
1100 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1108 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1101
1109
1102 def changeview(state, delta, unit):
1110 def changeview(state, delta, unit):
1103 '''Change the region of whatever is being viewed (a patch or the list of
1111 '''Change the region of whatever is being viewed (a patch or the list of
1104 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1112 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1105 mode, _ = state['mode']
1113 mode, _ = state['mode']
1106 if mode != MODE_PATCH:
1114 if mode != MODE_PATCH:
1107 return
1115 return
1108 mode_state = state['modes'][mode]
1116 mode_state = state['modes'][mode]
1109 num_lines = len(patchcontents(state))
1117 num_lines = len(patchcontents(state))
1110 page_height = state['page_height']
1118 page_height = state['page_height']
1111 unit = page_height if unit == 'page' else 1
1119 unit = page_height if unit == 'page' else 1
1112 num_pages = 1 + (num_lines - 1) / page_height
1120 num_pages = 1 + (num_lines - 1) / page_height
1113 max_offset = (num_pages - 1) * page_height
1121 max_offset = (num_pages - 1) * page_height
1114 newline = mode_state['line_offset'] + delta * unit
1122 newline = mode_state['line_offset'] + delta * unit
1115 mode_state['line_offset'] = max(0, min(max_offset, newline))
1123 mode_state['line_offset'] = max(0, min(max_offset, newline))
1116
1124
1117 def event(state, ch):
1125 def event(state, ch):
1118 """Change state based on the current character input
1126 """Change state based on the current character input
1119
1127
1120 This takes the current state and based on the current character input from
1128 This takes the current state and based on the current character input from
1121 the user we change the state.
1129 the user we change the state.
1122 """
1130 """
1123 selected = state['selected']
1131 selected = state['selected']
1124 oldpos = state['pos']
1132 oldpos = state['pos']
1125 rules = state['rules']
1133 rules = state['rules']
1126
1134
1127 if ch in (curses.KEY_RESIZE, "KEY_RESIZE"):
1135 if ch in (curses.KEY_RESIZE, "KEY_RESIZE"):
1128 return E_RESIZE
1136 return E_RESIZE
1129
1137
1130 lookup_ch = ch
1138 lookup_ch = ch
1131 if '0' <= ch <= '9':
1139 if '0' <= ch <= '9':
1132 lookup_ch = '0'
1140 lookup_ch = '0'
1133
1141
1134 curmode, prevmode = state['mode']
1142 curmode, prevmode = state['mode']
1135 action = KEYTABLE[curmode].get(lookup_ch, KEYTABLE['global'].get(lookup_ch))
1143 action = KEYTABLE[curmode].get(lookup_ch, KEYTABLE['global'].get(lookup_ch))
1136 if action is None:
1144 if action is None:
1137 return
1145 return
1138 if action in ('down', 'move-down'):
1146 if action in ('down', 'move-down'):
1139 newpos = min(oldpos + 1, len(rules) - 1)
1147 newpos = min(oldpos + 1, len(rules) - 1)
1140 movecursor(state, oldpos, newpos)
1148 movecursor(state, oldpos, newpos)
1141 if selected is not None or action == 'move-down':
1149 if selected is not None or action == 'move-down':
1142 swap(state, oldpos, newpos)
1150 swap(state, oldpos, newpos)
1143 elif action in ('up', 'move-up'):
1151 elif action in ('up', 'move-up'):
1144 newpos = max(0, oldpos - 1)
1152 newpos = max(0, oldpos - 1)
1145 movecursor(state, oldpos, newpos)
1153 movecursor(state, oldpos, newpos)
1146 if selected is not None or action == 'move-up':
1154 if selected is not None or action == 'move-up':
1147 swap(state, oldpos, newpos)
1155 swap(state, oldpos, newpos)
1148 elif action == 'next-action':
1156 elif action == 'next-action':
1149 cycleaction(state, oldpos, next=True)
1157 cycleaction(state, oldpos, next=True)
1150 elif action == 'prev-action':
1158 elif action == 'prev-action':
1151 cycleaction(state, oldpos, next=False)
1159 cycleaction(state, oldpos, next=False)
1152 elif action == 'select':
1160 elif action == 'select':
1153 selected = oldpos if selected is None else None
1161 selected = oldpos if selected is None else None
1154 makeselection(state, selected)
1162 makeselection(state, selected)
1155 elif action == 'goto' and int(ch) < len(rules) and len(rules) <= 10:
1163 elif action == 'goto' and int(ch) < len(rules) and len(rules) <= 10:
1156 newrule = next((r for r in rules if r.origpos == int(ch)))
1164 newrule = next((r for r in rules if r.origpos == int(ch)))
1157 movecursor(state, oldpos, newrule.pos)
1165 movecursor(state, oldpos, newrule.pos)
1158 if selected is not None:
1166 if selected is not None:
1159 swap(state, oldpos, newrule.pos)
1167 swap(state, oldpos, newrule.pos)
1160 elif action.startswith('action-'):
1168 elif action.startswith('action-'):
1161 changeaction(state, oldpos, action[7:])
1169 changeaction(state, oldpos, action[7:])
1162 elif action == 'showpatch':
1170 elif action == 'showpatch':
1163 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1171 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1164 elif action == 'help':
1172 elif action == 'help':
1165 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1173 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1166 elif action == 'quit':
1174 elif action == 'quit':
1167 return E_QUIT
1175 return E_QUIT
1168 elif action == 'histedit':
1176 elif action == 'histedit':
1169 return E_HISTEDIT
1177 return E_HISTEDIT
1170 elif action == 'page-down':
1178 elif action == 'page-down':
1171 return E_PAGEDOWN
1179 return E_PAGEDOWN
1172 elif action == 'page-up':
1180 elif action == 'page-up':
1173 return E_PAGEUP
1181 return E_PAGEUP
1174 elif action == 'line-down':
1182 elif action == 'line-down':
1175 return E_LINEDOWN
1183 return E_LINEDOWN
1176 elif action == 'line-up':
1184 elif action == 'line-up':
1177 return E_LINEUP
1185 return E_LINEUP
1178
1186
1179 def makecommands(rules):
1187 def makecommands(rules):
1180 """Returns a list of commands consumable by histedit --commands based on
1188 """Returns a list of commands consumable by histedit --commands based on
1181 our list of rules"""
1189 our list of rules"""
1182 commands = []
1190 commands = []
1183 for rules in rules:
1191 for rules in rules:
1184 commands.append("{0} {1}\n".format(rules.action, rules.ctx))
1192 commands.append("{0} {1}\n".format(rules.action, rules.ctx))
1185 return commands
1193 return commands
1186
1194
1187 def addln(win, y, x, line, color=None):
1195 def addln(win, y, x, line, color=None):
1188 """Add a line to the given window left padding but 100% filled with
1196 """Add a line to the given window left padding but 100% filled with
1189 whitespace characters, so that the color appears on the whole line"""
1197 whitespace characters, so that the color appears on the whole line"""
1190 maxy, maxx = win.getmaxyx()
1198 maxy, maxx = win.getmaxyx()
1191 length = maxx - 1 - x
1199 length = maxx - 1 - x
1192 line = ("{0:<%d}" % length).format(str(line).strip())[:length]
1200 line = ("{0:<%d}" % length).format(str(line).strip())[:length]
1193 if y < 0:
1201 if y < 0:
1194 y = maxy + y
1202 y = maxy + y
1195 if x < 0:
1203 if x < 0:
1196 x = maxx + x
1204 x = maxx + x
1197 if color:
1205 if color:
1198 win.addstr(y, x, line, color)
1206 win.addstr(y, x, line, color)
1199 else:
1207 else:
1200 win.addstr(y, x, line)
1208 win.addstr(y, x, line)
1201
1209
1202 def patchcontents(state):
1210 def patchcontents(state):
1203 repo = state['repo']
1211 repo = state['repo']
1204 rule = state['rules'][state['pos']]
1212 rule = state['rules'][state['pos']]
1205 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1213 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1206 'patch': True, 'verbose': True
1214 'patch': True, 'verbose': True
1207 }, buffered=True)
1215 }, buffered=True)
1208 displayer.show(rule.ctx)
1216 displayer.show(rule.ctx)
1209 displayer.close()
1217 displayer.close()
1210 return displayer.hunk[rule.ctx.rev()].splitlines()
1218 return displayer.hunk[rule.ctx.rev()].splitlines()
1211
1219
1212 def _chisteditmain(repo, rules, stdscr):
1220 def _chisteditmain(repo, rules, stdscr):
1213 # initialize color pattern
1221 # initialize color pattern
1214 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1222 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1215 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1223 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1216 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1224 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1217 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1225 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1218
1226
1219 # don't display the cursor
1227 # don't display the cursor
1220 try:
1228 try:
1221 curses.curs_set(0)
1229 curses.curs_set(0)
1222 except curses.error:
1230 except curses.error:
1223 pass
1231 pass
1224
1232
1225 def rendercommit(win, state):
1233 def rendercommit(win, state):
1226 """Renders the commit window that shows the log of the current selected
1234 """Renders the commit window that shows the log of the current selected
1227 commit"""
1235 commit"""
1228 pos = state['pos']
1236 pos = state['pos']
1229 rules = state['rules']
1237 rules = state['rules']
1230 rule = rules[pos]
1238 rule = rules[pos]
1231
1239
1232 ctx = rule.ctx
1240 ctx = rule.ctx
1233 win.box()
1241 win.box()
1234
1242
1235 maxy, maxx = win.getmaxyx()
1243 maxy, maxx = win.getmaxyx()
1236 length = maxx - 3
1244 length = maxx - 3
1237
1245
1238 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1246 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1239 win.addstr(1, 1, line[:length])
1247 win.addstr(1, 1, line[:length])
1240
1248
1241 line = "user: {0}".format(stringutil.shortuser(ctx.user()))
1249 line = "user: {0}".format(stringutil.shortuser(ctx.user()))
1242 win.addstr(2, 1, line[:length])
1250 win.addstr(2, 1, line[:length])
1243
1251
1244 bms = repo.nodebookmarks(ctx.node())
1252 bms = repo.nodebookmarks(ctx.node())
1245 line = "bookmark: {0}".format(' '.join(bms))
1253 line = "bookmark: {0}".format(' '.join(bms))
1246 win.addstr(3, 1, line[:length])
1254 win.addstr(3, 1, line[:length])
1247
1255
1248 line = "files: {0}".format(','.join(ctx.files()))
1256 line = "files: {0}".format(','.join(ctx.files()))
1249 win.addstr(4, 1, line[:length])
1257 win.addstr(4, 1, line[:length])
1250
1258
1251 line = "summary: {0}".format(ctx.description().splitlines()[0])
1259 line = "summary: {0}".format(ctx.description().splitlines()[0])
1252 win.addstr(5, 1, line[:length])
1260 win.addstr(5, 1, line[:length])
1253
1261
1254 conflicts = rule.conflicts
1262 conflicts = rule.conflicts
1255 if len(conflicts) > 0:
1263 if len(conflicts) > 0:
1256 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1264 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1257 conflictstr = "changed files overlap with {0}".format(conflictstr)
1265 conflictstr = "changed files overlap with {0}".format(conflictstr)
1258 else:
1266 else:
1259 conflictstr = 'no overlap'
1267 conflictstr = 'no overlap'
1260
1268
1261 win.addstr(6, 1, conflictstr[:length])
1269 win.addstr(6, 1, conflictstr[:length])
1262 win.noutrefresh()
1270 win.noutrefresh()
1263
1271
1264 def helplines(mode):
1272 def helplines(mode):
1265 if mode == MODE_PATCH:
1273 if mode == MODE_PATCH:
1266 help = """\
1274 help = """\
1267 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1275 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1268 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1276 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1269 """
1277 """
1270 else:
1278 else:
1271 help = """\
1279 help = """\
1272 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1280 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1273 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1281 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1274 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1282 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1275 """
1283 """
1276 return help.splitlines()
1284 return help.splitlines()
1277
1285
1278 def renderhelp(win, state):
1286 def renderhelp(win, state):
1279 maxy, maxx = win.getmaxyx()
1287 maxy, maxx = win.getmaxyx()
1280 mode, _ = state['mode']
1288 mode, _ = state['mode']
1281 for y, line in enumerate(helplines(mode)):
1289 for y, line in enumerate(helplines(mode)):
1282 if y >= maxy:
1290 if y >= maxy:
1283 break
1291 break
1284 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1292 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1285 win.noutrefresh()
1293 win.noutrefresh()
1286
1294
1287 def renderrules(rulesscr, state):
1295 def renderrules(rulesscr, state):
1288 rules = state['rules']
1296 rules = state['rules']
1289 pos = state['pos']
1297 pos = state['pos']
1290 selected = state['selected']
1298 selected = state['selected']
1291 start = state['modes'][MODE_RULES]['line_offset']
1299 start = state['modes'][MODE_RULES]['line_offset']
1292
1300
1293 conflicts = [r.ctx for r in rules if r.conflicts]
1301 conflicts = [r.ctx for r in rules if r.conflicts]
1294 if len(conflicts) > 0:
1302 if len(conflicts) > 0:
1295 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1303 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1296 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1304 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1297
1305
1298 for y, rule in enumerate(rules[start:]):
1306 for y, rule in enumerate(rules[start:]):
1299 if y >= state['page_height']:
1307 if y >= state['page_height']:
1300 break
1308 break
1301 if len(rule.conflicts) > 0:
1309 if len(rule.conflicts) > 0:
1302 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1310 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1303 else:
1311 else:
1304 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1312 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1305 if y + start == selected:
1313 if y + start == selected:
1306 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1314 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1307 elif y + start == pos:
1315 elif y + start == pos:
1308 addln(rulesscr, y, 2, rule, curses.A_BOLD)
1316 addln(rulesscr, y, 2, rule, curses.A_BOLD)
1309 else:
1317 else:
1310 addln(rulesscr, y, 2, rule)
1318 addln(rulesscr, y, 2, rule)
1311 rulesscr.noutrefresh()
1319 rulesscr.noutrefresh()
1312
1320
1313 def renderstring(win, state, output):
1321 def renderstring(win, state, output):
1314 maxy, maxx = win.getmaxyx()
1322 maxy, maxx = win.getmaxyx()
1315 length = min(maxy - 1, len(output))
1323 length = min(maxy - 1, len(output))
1316 for y in range(0, length):
1324 for y in range(0, length):
1317 win.addstr(y, 0, output[y])
1325 win.addstr(y, 0, output[y])
1318 win.noutrefresh()
1326 win.noutrefresh()
1319
1327
1320 def renderpatch(win, state):
1328 def renderpatch(win, state):
1321 start = state['modes'][MODE_PATCH]['line_offset']
1329 start = state['modes'][MODE_PATCH]['line_offset']
1322 renderstring(win, state, patchcontents(state)[start:])
1330 renderstring(win, state, patchcontents(state)[start:])
1323
1331
1324 def layout(mode):
1332 def layout(mode):
1325 maxy, maxx = stdscr.getmaxyx()
1333 maxy, maxx = stdscr.getmaxyx()
1326 helplen = len(helplines(mode))
1334 helplen = len(helplines(mode))
1327 return {
1335 return {
1328 'commit': (8, maxx),
1336 'commit': (8, maxx),
1329 'help': (helplen, maxx),
1337 'help': (helplen, maxx),
1330 'main': (maxy - helplen - 8, maxx),
1338 'main': (maxy - helplen - 8, maxx),
1331 }
1339 }
1332
1340
1333 def drawvertwin(size, y, x):
1341 def drawvertwin(size, y, x):
1334 win = curses.newwin(size[0], size[1], y, x)
1342 win = curses.newwin(size[0], size[1], y, x)
1335 y += size[0]
1343 y += size[0]
1336 return win, y, x
1344 return win, y, x
1337
1345
1338 state = {
1346 state = {
1339 'pos': 0,
1347 'pos': 0,
1340 'rules': rules,
1348 'rules': rules,
1341 'selected': None,
1349 'selected': None,
1342 'mode': (MODE_INIT, MODE_INIT),
1350 'mode': (MODE_INIT, MODE_INIT),
1343 'page_height': None,
1351 'page_height': None,
1344 'modes': {
1352 'modes': {
1345 MODE_RULES: {
1353 MODE_RULES: {
1346 'line_offset': 0,
1354 'line_offset': 0,
1347 },
1355 },
1348 MODE_PATCH: {
1356 MODE_PATCH: {
1349 'line_offset': 0,
1357 'line_offset': 0,
1350 }
1358 }
1351 },
1359 },
1352 'repo': repo,
1360 'repo': repo,
1353 }
1361 }
1354
1362
1355 # eventloop
1363 # eventloop
1356 ch = None
1364 ch = None
1357 stdscr.clear()
1365 stdscr.clear()
1358 stdscr.refresh()
1366 stdscr.refresh()
1359 while True:
1367 while True:
1360 try:
1368 try:
1361 oldmode, _ = state['mode']
1369 oldmode, _ = state['mode']
1362 if oldmode == MODE_INIT:
1370 if oldmode == MODE_INIT:
1363 changemode(state, MODE_RULES)
1371 changemode(state, MODE_RULES)
1364 e = event(state, ch)
1372 e = event(state, ch)
1365
1373
1366 if e == E_QUIT:
1374 if e == E_QUIT:
1367 return False
1375 return False
1368 if e == E_HISTEDIT:
1376 if e == E_HISTEDIT:
1369 return state['rules']
1377 return state['rules']
1370 else:
1378 else:
1371 if e == E_RESIZE:
1379 if e == E_RESIZE:
1372 size = screen_size()
1380 size = screen_size()
1373 if size != stdscr.getmaxyx():
1381 if size != stdscr.getmaxyx():
1374 curses.resizeterm(*size)
1382 curses.resizeterm(*size)
1375
1383
1376 curmode, _ = state['mode']
1384 curmode, _ = state['mode']
1377 sizes = layout(curmode)
1385 sizes = layout(curmode)
1378 if curmode != oldmode:
1386 if curmode != oldmode:
1379 state['page_height'] = sizes['main'][0]
1387 state['page_height'] = sizes['main'][0]
1380 # Adjust the view to fit the current screen size.
1388 # Adjust the view to fit the current screen size.
1381 movecursor(state, state['pos'], state['pos'])
1389 movecursor(state, state['pos'], state['pos'])
1382
1390
1383 # Pack the windows against the top, each pane spread across the
1391 # Pack the windows against the top, each pane spread across the
1384 # full width of the screen.
1392 # full width of the screen.
1385 y, x = (0, 0)
1393 y, x = (0, 0)
1386 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1394 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1387 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1395 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1388 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1396 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1389
1397
1390 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1398 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1391 if e == E_PAGEDOWN:
1399 if e == E_PAGEDOWN:
1392 changeview(state, +1, 'page')
1400 changeview(state, +1, 'page')
1393 elif e == E_PAGEUP:
1401 elif e == E_PAGEUP:
1394 changeview(state, -1, 'page')
1402 changeview(state, -1, 'page')
1395 elif e == E_LINEDOWN:
1403 elif e == E_LINEDOWN:
1396 changeview(state, +1, 'line')
1404 changeview(state, +1, 'line')
1397 elif e == E_LINEUP:
1405 elif e == E_LINEUP:
1398 changeview(state, -1, 'line')
1406 changeview(state, -1, 'line')
1399
1407
1400 # start rendering
1408 # start rendering
1401 commitwin.erase()
1409 commitwin.erase()
1402 helpwin.erase()
1410 helpwin.erase()
1403 mainwin.erase()
1411 mainwin.erase()
1404 if curmode == MODE_PATCH:
1412 if curmode == MODE_PATCH:
1405 renderpatch(mainwin, state)
1413 renderpatch(mainwin, state)
1406 elif curmode == MODE_HELP:
1414 elif curmode == MODE_HELP:
1407 renderstring(mainwin, state, __doc__.strip().splitlines())
1415 renderstring(mainwin, state, __doc__.strip().splitlines())
1408 else:
1416 else:
1409 renderrules(mainwin, state)
1417 renderrules(mainwin, state)
1410 rendercommit(commitwin, state)
1418 rendercommit(commitwin, state)
1411 renderhelp(helpwin, state)
1419 renderhelp(helpwin, state)
1412 curses.doupdate()
1420 curses.doupdate()
1413 # done rendering
1421 # done rendering
1414 ch = stdscr.getkey()
1422 ch = stdscr.getkey()
1415 except curses.error:
1423 except curses.error:
1416 pass
1424 pass
1417
1425
1418 def _chistedit(ui, repo, *freeargs, **opts):
1426 def _chistedit(ui, repo, *freeargs, **opts):
1419 """interactively edit changeset history via a curses interface
1427 """interactively edit changeset history via a curses interface
1420
1428
1421 Provides a ncurses interface to histedit. Press ? in chistedit mode
1429 Provides a ncurses interface to histedit. Press ? in chistedit mode
1422 to see an extensive help. Requires python-curses to be installed."""
1430 to see an extensive help. Requires python-curses to be installed."""
1423
1431
1424 if curses is None:
1432 if curses is None:
1425 raise error.Abort(_("Python curses library required"))
1433 raise error.Abort(_("Python curses library required"))
1426
1434
1427 # disable color
1435 # disable color
1428 ui._colormode = None
1436 ui._colormode = None
1429
1437
1430 try:
1438 try:
1431 keep = opts.get('keep')
1439 keep = opts.get('keep')
1432 revs = opts.get('rev', [])[:]
1440 revs = opts.get('rev', [])[:]
1433 cmdutil.checkunfinished(repo)
1441 cmdutil.checkunfinished(repo)
1434 cmdutil.bailifchanged(repo)
1442 cmdutil.bailifchanged(repo)
1435
1443
1436 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1444 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1437 raise error.Abort(_('history edit already in progress, try '
1445 raise error.Abort(_('history edit already in progress, try '
1438 '--continue or --abort'))
1446 '--continue or --abort'))
1439 revs.extend(freeargs)
1447 revs.extend(freeargs)
1440 if not revs:
1448 if not revs:
1441 defaultrev = destutil.desthistedit(ui, repo)
1449 defaultrev = destutil.desthistedit(ui, repo)
1442 if defaultrev is not None:
1450 if defaultrev is not None:
1443 revs.append(defaultrev)
1451 revs.append(defaultrev)
1444 if len(revs) != 1:
1452 if len(revs) != 1:
1445 raise error.Abort(
1453 raise error.Abort(
1446 _('histedit requires exactly one ancestor revision'))
1454 _('histedit requires exactly one ancestor revision'))
1447
1455
1448 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1456 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1449 if len(rr) != 1:
1457 if len(rr) != 1:
1450 raise error.Abort(_('The specified revisions must have '
1458 raise error.Abort(_('The specified revisions must have '
1451 'exactly one common root'))
1459 'exactly one common root'))
1452 root = rr[0].node()
1460 root = rr[0].node()
1453
1461
1454 topmost, empty = repo.dirstate.parents()
1462 topmost, empty = repo.dirstate.parents()
1455 revs = between(repo, root, topmost, keep)
1463 revs = between(repo, root, topmost, keep)
1456 if not revs:
1464 if not revs:
1457 raise error.Abort(_('%s is not an ancestor of working directory') %
1465 raise error.Abort(_('%s is not an ancestor of working directory') %
1458 node.short(root))
1466 node.short(root))
1459
1467
1460 ctxs = []
1468 ctxs = []
1461 for i, r in enumerate(revs):
1469 for i, r in enumerate(revs):
1462 ctxs.append(histeditrule(repo[r], i))
1470 ctxs.append(histeditrule(repo[r], i))
1463 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1471 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1464 curses.echo()
1472 curses.echo()
1465 curses.endwin()
1473 curses.endwin()
1466 if rc is False:
1474 if rc is False:
1467 ui.write(_("chistedit aborted\n"))
1475 ui.write(_("chistedit aborted\n"))
1468 return 0
1476 return 0
1469 if type(rc) is list:
1477 if type(rc) is list:
1470 ui.status(_("running histedit\n"))
1478 ui.status(_("running histedit\n"))
1471 rules = makecommands(rc)
1479 rules = makecommands(rc)
1472 filename = repo.vfs.join('chistedit')
1480 filename = repo.vfs.join('chistedit')
1473 with open(filename, 'w+') as fp:
1481 with open(filename, 'w+') as fp:
1474 for r in rules:
1482 for r in rules:
1475 fp.write(r)
1483 fp.write(r)
1476 opts['commands'] = filename
1484 opts['commands'] = filename
1477 return _texthistedit(ui, repo, *freeargs, **opts)
1485 return _texthistedit(ui, repo, *freeargs, **opts)
1478 except KeyboardInterrupt:
1486 except KeyboardInterrupt:
1479 pass
1487 pass
1480 return -1
1488 return -1
1481
1489
1482 @command('histedit',
1490 @command('histedit',
1483 [('', 'commands', '',
1491 [('', 'commands', '',
1484 _('read history edits from the specified file'), _('FILE')),
1492 _('read history edits from the specified file'), _('FILE')),
1485 ('c', 'continue', False, _('continue an edit already in progress')),
1493 ('c', 'continue', False, _('continue an edit already in progress')),
1486 ('', 'edit-plan', False, _('edit remaining actions list')),
1494 ('', 'edit-plan', False, _('edit remaining actions list')),
1487 ('k', 'keep', False,
1495 ('k', 'keep', False,
1488 _("don't strip old nodes after edit is complete")),
1496 _("don't strip old nodes after edit is complete")),
1489 ('', 'abort', False, _('abort an edit in progress')),
1497 ('', 'abort', False, _('abort an edit in progress')),
1490 ('o', 'outgoing', False, _('changesets not found in destination')),
1498 ('o', 'outgoing', False, _('changesets not found in destination')),
1491 ('f', 'force', False,
1499 ('f', 'force', False,
1492 _('force outgoing even for unrelated repositories')),
1500 _('force outgoing even for unrelated repositories')),
1493 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1501 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1494 cmdutil.formatteropts,
1502 cmdutil.formatteropts,
1495 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1503 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1496 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1504 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1497 def histedit(ui, repo, *freeargs, **opts):
1505 def histedit(ui, repo, *freeargs, **opts):
1498 """interactively edit changeset history
1506 """interactively edit changeset history
1499
1507
1500 This command lets you edit a linear series of changesets (up to
1508 This command lets you edit a linear series of changesets (up to
1501 and including the working directory, which should be clean).
1509 and including the working directory, which should be clean).
1502 You can:
1510 You can:
1503
1511
1504 - `pick` to [re]order a changeset
1512 - `pick` to [re]order a changeset
1505
1513
1506 - `drop` to omit changeset
1514 - `drop` to omit changeset
1507
1515
1508 - `mess` to reword the changeset commit message
1516 - `mess` to reword the changeset commit message
1509
1517
1510 - `fold` to combine it with the preceding changeset (using the later date)
1518 - `fold` to combine it with the preceding changeset (using the later date)
1511
1519
1512 - `roll` like fold, but discarding this commit's description and date
1520 - `roll` like fold, but discarding this commit's description and date
1513
1521
1514 - `edit` to edit this changeset (preserving date)
1522 - `edit` to edit this changeset (preserving date)
1515
1523
1516 - `base` to checkout changeset and apply further changesets from there
1524 - `base` to checkout changeset and apply further changesets from there
1517
1525
1518 There are a number of ways to select the root changeset:
1526 There are a number of ways to select the root changeset:
1519
1527
1520 - Specify ANCESTOR directly
1528 - Specify ANCESTOR directly
1521
1529
1522 - Use --outgoing -- it will be the first linear changeset not
1530 - Use --outgoing -- it will be the first linear changeset not
1523 included in destination. (See :hg:`help config.paths.default-push`)
1531 included in destination. (See :hg:`help config.paths.default-push`)
1524
1532
1525 - Otherwise, the value from the "histedit.defaultrev" config option
1533 - Otherwise, the value from the "histedit.defaultrev" config option
1526 is used as a revset to select the base revision when ANCESTOR is not
1534 is used as a revset to select the base revision when ANCESTOR is not
1527 specified. The first revision returned by the revset is used. By
1535 specified. The first revision returned by the revset is used. By
1528 default, this selects the editable history that is unique to the
1536 default, this selects the editable history that is unique to the
1529 ancestry of the working directory.
1537 ancestry of the working directory.
1530
1538
1531 .. container:: verbose
1539 .. container:: verbose
1532
1540
1533 If you use --outgoing, this command will abort if there are ambiguous
1541 If you use --outgoing, this command will abort if there are ambiguous
1534 outgoing revisions. For example, if there are multiple branches
1542 outgoing revisions. For example, if there are multiple branches
1535 containing outgoing revisions.
1543 containing outgoing revisions.
1536
1544
1537 Use "min(outgoing() and ::.)" or similar revset specification
1545 Use "min(outgoing() and ::.)" or similar revset specification
1538 instead of --outgoing to specify edit target revision exactly in
1546 instead of --outgoing to specify edit target revision exactly in
1539 such ambiguous situation. See :hg:`help revsets` for detail about
1547 such ambiguous situation. See :hg:`help revsets` for detail about
1540 selecting revisions.
1548 selecting revisions.
1541
1549
1542 .. container:: verbose
1550 .. container:: verbose
1543
1551
1544 Examples:
1552 Examples:
1545
1553
1546 - A number of changes have been made.
1554 - A number of changes have been made.
1547 Revision 3 is no longer needed.
1555 Revision 3 is no longer needed.
1548
1556
1549 Start history editing from revision 3::
1557 Start history editing from revision 3::
1550
1558
1551 hg histedit -r 3
1559 hg histedit -r 3
1552
1560
1553 An editor opens, containing the list of revisions,
1561 An editor opens, containing the list of revisions,
1554 with specific actions specified::
1562 with specific actions specified::
1555
1563
1556 pick 5339bf82f0ca 3 Zworgle the foobar
1564 pick 5339bf82f0ca 3 Zworgle the foobar
1557 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1565 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1558 pick 0a9639fcda9d 5 Morgify the cromulancy
1566 pick 0a9639fcda9d 5 Morgify the cromulancy
1559
1567
1560 Additional information about the possible actions
1568 Additional information about the possible actions
1561 to take appears below the list of revisions.
1569 to take appears below the list of revisions.
1562
1570
1563 To remove revision 3 from the history,
1571 To remove revision 3 from the history,
1564 its action (at the beginning of the relevant line)
1572 its action (at the beginning of the relevant line)
1565 is changed to 'drop'::
1573 is changed to 'drop'::
1566
1574
1567 drop 5339bf82f0ca 3 Zworgle the foobar
1575 drop 5339bf82f0ca 3 Zworgle the foobar
1568 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1576 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1569 pick 0a9639fcda9d 5 Morgify the cromulancy
1577 pick 0a9639fcda9d 5 Morgify the cromulancy
1570
1578
1571 - A number of changes have been made.
1579 - A number of changes have been made.
1572 Revision 2 and 4 need to be swapped.
1580 Revision 2 and 4 need to be swapped.
1573
1581
1574 Start history editing from revision 2::
1582 Start history editing from revision 2::
1575
1583
1576 hg histedit -r 2
1584 hg histedit -r 2
1577
1585
1578 An editor opens, containing the list of revisions,
1586 An editor opens, containing the list of revisions,
1579 with specific actions specified::
1587 with specific actions specified::
1580
1588
1581 pick 252a1af424ad 2 Blorb a morgwazzle
1589 pick 252a1af424ad 2 Blorb a morgwazzle
1582 pick 5339bf82f0ca 3 Zworgle the foobar
1590 pick 5339bf82f0ca 3 Zworgle the foobar
1583 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1591 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1584
1592
1585 To swap revision 2 and 4, its lines are swapped
1593 To swap revision 2 and 4, its lines are swapped
1586 in the editor::
1594 in the editor::
1587
1595
1588 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1596 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1589 pick 5339bf82f0ca 3 Zworgle the foobar
1597 pick 5339bf82f0ca 3 Zworgle the foobar
1590 pick 252a1af424ad 2 Blorb a morgwazzle
1598 pick 252a1af424ad 2 Blorb a morgwazzle
1591
1599
1592 Returns 0 on success, 1 if user intervention is required (not only
1600 Returns 0 on success, 1 if user intervention is required (not only
1593 for intentional "edit" command, but also for resolving unexpected
1601 for intentional "edit" command, but also for resolving unexpected
1594 conflicts).
1602 conflicts).
1595 """
1603 """
1596 # kludge: _chistedit only works for starting an edit, not aborting
1604 # kludge: _chistedit only works for starting an edit, not aborting
1597 # or continuing, so fall back to regular _texthistedit for those
1605 # or continuing, so fall back to regular _texthistedit for those
1598 # operations.
1606 # operations.
1599 if ui.interface('histedit') == 'curses' and _getgoal(opts) == goalnew:
1607 if ui.interface('histedit') == 'curses' and _getgoal(opts) == goalnew:
1600 return _chistedit(ui, repo, *freeargs, **opts)
1608 return _chistedit(ui, repo, *freeargs, **opts)
1601 return _texthistedit(ui, repo, *freeargs, **opts)
1609 return _texthistedit(ui, repo, *freeargs, **opts)
1602
1610
1603 def _texthistedit(ui, repo, *freeargs, **opts):
1611 def _texthistedit(ui, repo, *freeargs, **opts):
1604 state = histeditstate(repo)
1612 state = histeditstate(repo)
1605 with repo.wlock() as wlock, repo.lock() as lock:
1613 with repo.wlock() as wlock, repo.lock() as lock:
1606 state.wlock = wlock
1614 state.wlock = wlock
1607 state.lock = lock
1615 state.lock = lock
1608 _histedit(ui, repo, state, *freeargs, **opts)
1616 _histedit(ui, repo, state, *freeargs, **opts)
1609
1617
1610 goalcontinue = 'continue'
1618 goalcontinue = 'continue'
1611 goalabort = 'abort'
1619 goalabort = 'abort'
1612 goaleditplan = 'edit-plan'
1620 goaleditplan = 'edit-plan'
1613 goalnew = 'new'
1621 goalnew = 'new'
1614
1622
1615 def _getgoal(opts):
1623 def _getgoal(opts):
1616 if opts.get('continue'):
1624 if opts.get('continue'):
1617 return goalcontinue
1625 return goalcontinue
1618 if opts.get('abort'):
1626 if opts.get('abort'):
1619 return goalabort
1627 return goalabort
1620 if opts.get('edit_plan'):
1628 if opts.get('edit_plan'):
1621 return goaleditplan
1629 return goaleditplan
1622 return goalnew
1630 return goalnew
1623
1631
1624 def _readfile(ui, path):
1632 def _readfile(ui, path):
1625 if path == '-':
1633 if path == '-':
1626 with ui.timeblockedsection('histedit'):
1634 with ui.timeblockedsection('histedit'):
1627 return ui.fin.read()
1635 return ui.fin.read()
1628 else:
1636 else:
1629 with open(path, 'rb') as f:
1637 with open(path, 'rb') as f:
1630 return f.read()
1638 return f.read()
1631
1639
1632 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1640 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1633 # TODO only abort if we try to histedit mq patches, not just
1641 # TODO only abort if we try to histedit mq patches, not just
1634 # blanket if mq patches are applied somewhere
1642 # blanket if mq patches are applied somewhere
1635 mq = getattr(repo, 'mq', None)
1643 mq = getattr(repo, 'mq', None)
1636 if mq and mq.applied:
1644 if mq and mq.applied:
1637 raise error.Abort(_('source has mq patches applied'))
1645 raise error.Abort(_('source has mq patches applied'))
1638
1646
1639 # basic argument incompatibility processing
1647 # basic argument incompatibility processing
1640 outg = opts.get('outgoing')
1648 outg = opts.get('outgoing')
1641 editplan = opts.get('edit_plan')
1649 editplan = opts.get('edit_plan')
1642 abort = opts.get('abort')
1650 abort = opts.get('abort')
1643 force = opts.get('force')
1651 force = opts.get('force')
1644 if force and not outg:
1652 if force and not outg:
1645 raise error.Abort(_('--force only allowed with --outgoing'))
1653 raise error.Abort(_('--force only allowed with --outgoing'))
1646 if goal == 'continue':
1654 if goal == 'continue':
1647 if any((outg, abort, revs, freeargs, rules, editplan)):
1655 if any((outg, abort, revs, freeargs, rules, editplan)):
1648 raise error.Abort(_('no arguments allowed with --continue'))
1656 raise error.Abort(_('no arguments allowed with --continue'))
1649 elif goal == 'abort':
1657 elif goal == 'abort':
1650 if any((outg, revs, freeargs, rules, editplan)):
1658 if any((outg, revs, freeargs, rules, editplan)):
1651 raise error.Abort(_('no arguments allowed with --abort'))
1659 raise error.Abort(_('no arguments allowed with --abort'))
1652 elif goal == 'edit-plan':
1660 elif goal == 'edit-plan':
1653 if any((outg, revs, freeargs)):
1661 if any((outg, revs, freeargs)):
1654 raise error.Abort(_('only --commands argument allowed with '
1662 raise error.Abort(_('only --commands argument allowed with '
1655 '--edit-plan'))
1663 '--edit-plan'))
1656 else:
1664 else:
1657 if state.inprogress():
1665 if state.inprogress():
1658 raise error.Abort(_('history edit already in progress, try '
1666 raise error.Abort(_('history edit already in progress, try '
1659 '--continue or --abort'))
1667 '--continue or --abort'))
1660 if outg:
1668 if outg:
1661 if revs:
1669 if revs:
1662 raise error.Abort(_('no revisions allowed with --outgoing'))
1670 raise error.Abort(_('no revisions allowed with --outgoing'))
1663 if len(freeargs) > 1:
1671 if len(freeargs) > 1:
1664 raise error.Abort(
1672 raise error.Abort(
1665 _('only one repo argument allowed with --outgoing'))
1673 _('only one repo argument allowed with --outgoing'))
1666 else:
1674 else:
1667 revs.extend(freeargs)
1675 revs.extend(freeargs)
1668 if len(revs) == 0:
1676 if len(revs) == 0:
1669 defaultrev = destutil.desthistedit(ui, repo)
1677 defaultrev = destutil.desthistedit(ui, repo)
1670 if defaultrev is not None:
1678 if defaultrev is not None:
1671 revs.append(defaultrev)
1679 revs.append(defaultrev)
1672
1680
1673 if len(revs) != 1:
1681 if len(revs) != 1:
1674 raise error.Abort(
1682 raise error.Abort(
1675 _('histedit requires exactly one ancestor revision'))
1683 _('histedit requires exactly one ancestor revision'))
1676
1684
1677 def _histedit(ui, repo, state, *freeargs, **opts):
1685 def _histedit(ui, repo, state, *freeargs, **opts):
1678 opts = pycompat.byteskwargs(opts)
1686 opts = pycompat.byteskwargs(opts)
1679 fm = ui.formatter('histedit', opts)
1687 fm = ui.formatter('histedit', opts)
1680 fm.startitem()
1688 fm.startitem()
1681 goal = _getgoal(opts)
1689 goal = _getgoal(opts)
1682 revs = opts.get('rev', [])
1690 revs = opts.get('rev', [])
1683 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1691 nobackup = not ui.configbool('rewrite', 'backup-bundle')
1684 rules = opts.get('commands', '')
1692 rules = opts.get('commands', '')
1685 state.keep = opts.get('keep', False)
1693 state.keep = opts.get('keep', False)
1686
1694
1687 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1695 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1688
1696
1689 hastags = False
1697 hastags = False
1690 if revs:
1698 if revs:
1691 revs = scmutil.revrange(repo, revs)
1699 revs = scmutil.revrange(repo, revs)
1692 ctxs = [repo[rev] for rev in revs]
1700 ctxs = [repo[rev] for rev in revs]
1693 for ctx in ctxs:
1701 for ctx in ctxs:
1694 tags = [tag for tag in ctx.tags() if tag != 'tip']
1702 tags = [tag for tag in ctx.tags() if tag != 'tip']
1695 if not hastags:
1703 if not hastags:
1696 hastags = len(tags)
1704 hastags = len(tags)
1697 if hastags:
1705 if hastags:
1698 if ui.promptchoice(_('warning: tags associated with the given'
1706 if ui.promptchoice(_('warning: tags associated with the given'
1699 ' changeset will be lost after histedit.\n'
1707 ' changeset will be lost after histedit.\n'
1700 'do you want to continue (yN)? $$ &Yes $$ &No'),
1708 'do you want to continue (yN)? $$ &Yes $$ &No'),
1701 default=1):
1709 default=1):
1702 raise error.Abort(_('histedit cancelled\n'))
1710 raise error.Abort(_('histedit cancelled\n'))
1703 # rebuild state
1711 # rebuild state
1704 if goal == goalcontinue:
1712 if goal == goalcontinue:
1705 state.read()
1713 state.read()
1706 state = bootstrapcontinue(ui, state, opts)
1714 state = bootstrapcontinue(ui, state, opts)
1707 elif goal == goaleditplan:
1715 elif goal == goaleditplan:
1708 _edithisteditplan(ui, repo, state, rules)
1716 _edithisteditplan(ui, repo, state, rules)
1709 return
1717 return
1710 elif goal == goalabort:
1718 elif goal == goalabort:
1711 _aborthistedit(ui, repo, state, nobackup=nobackup)
1719 _aborthistedit(ui, repo, state, nobackup=nobackup)
1712 return
1720 return
1713 else:
1721 else:
1714 # goal == goalnew
1722 # goal == goalnew
1715 _newhistedit(ui, repo, state, revs, freeargs, opts)
1723 _newhistedit(ui, repo, state, revs, freeargs, opts)
1716
1724
1717 _continuehistedit(ui, repo, state)
1725 _continuehistedit(ui, repo, state)
1718 _finishhistedit(ui, repo, state, fm)
1726 _finishhistedit(ui, repo, state, fm)
1719 fm.end()
1727 fm.end()
1720
1728
1721 def _continuehistedit(ui, repo, state):
1729 def _continuehistedit(ui, repo, state):
1722 """This function runs after either:
1730 """This function runs after either:
1723 - bootstrapcontinue (if the goal is 'continue')
1731 - bootstrapcontinue (if the goal is 'continue')
1724 - _newhistedit (if the goal is 'new')
1732 - _newhistedit (if the goal is 'new')
1725 """
1733 """
1726 # preprocess rules so that we can hide inner folds from the user
1734 # preprocess rules so that we can hide inner folds from the user
1727 # and only show one editor
1735 # and only show one editor
1728 actions = state.actions[:]
1736 actions = state.actions[:]
1729 for idx, (action, nextact) in enumerate(
1737 for idx, (action, nextact) in enumerate(
1730 zip(actions, actions[1:] + [None])):
1738 zip(actions, actions[1:] + [None])):
1731 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1739 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1732 state.actions[idx].__class__ = _multifold
1740 state.actions[idx].__class__ = _multifold
1733
1741
1734 # Force an initial state file write, so the user can run --abort/continue
1742 # Force an initial state file write, so the user can run --abort/continue
1735 # even if there's an exception before the first transaction serialize.
1743 # even if there's an exception before the first transaction serialize.
1736 state.write()
1744 state.write()
1737
1745
1738 tr = None
1746 tr = None
1739 # Don't use singletransaction by default since it rolls the entire
1747 # Don't use singletransaction by default since it rolls the entire
1740 # transaction back if an unexpected exception happens (like a
1748 # transaction back if an unexpected exception happens (like a
1741 # pretxncommit hook throws, or the user aborts the commit msg editor).
1749 # pretxncommit hook throws, or the user aborts the commit msg editor).
1742 if ui.configbool("histedit", "singletransaction"):
1750 if ui.configbool("histedit", "singletransaction"):
1743 # Don't use a 'with' for the transaction, since actions may close
1751 # Don't use a 'with' for the transaction, since actions may close
1744 # and reopen a transaction. For example, if the action executes an
1752 # and reopen a transaction. For example, if the action executes an
1745 # external process it may choose to commit the transaction first.
1753 # external process it may choose to commit the transaction first.
1746 tr = repo.transaction('histedit')
1754 tr = repo.transaction('histedit')
1747 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1755 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1748 total=len(state.actions))
1756 total=len(state.actions))
1749 with progress, util.acceptintervention(tr):
1757 with progress, util.acceptintervention(tr):
1750 while state.actions:
1758 while state.actions:
1751 state.write(tr=tr)
1759 state.write(tr=tr)
1752 actobj = state.actions[0]
1760 actobj = state.actions[0]
1753 progress.increment(item=actobj.torule())
1761 progress.increment(item=actobj.torule())
1754 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1762 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1755 actobj.torule()))
1763 actobj.torule()))
1756 parentctx, replacement_ = actobj.run()
1764 parentctx, replacement_ = actobj.run()
1757 state.parentctxnode = parentctx.node()
1765 state.parentctxnode = parentctx.node()
1758 state.replacements.extend(replacement_)
1766 state.replacements.extend(replacement_)
1759 state.actions.pop(0)
1767 state.actions.pop(0)
1760
1768
1761 state.write()
1769 state.write()
1762
1770
1763 def _finishhistedit(ui, repo, state, fm):
1771 def _finishhistedit(ui, repo, state, fm):
1764 """This action runs when histedit is finishing its session"""
1772 """This action runs when histedit is finishing its session"""
1765 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1773 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1766
1774
1767 mapping, tmpnodes, created, ntm = processreplacement(state)
1775 mapping, tmpnodes, created, ntm = processreplacement(state)
1768 if mapping:
1776 if mapping:
1769 for prec, succs in mapping.iteritems():
1777 for prec, succs in mapping.iteritems():
1770 if not succs:
1778 if not succs:
1771 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1779 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1772 else:
1780 else:
1773 ui.debug('histedit: %s is replaced by %s\n' % (
1781 ui.debug('histedit: %s is replaced by %s\n' % (
1774 node.short(prec), node.short(succs[0])))
1782 node.short(prec), node.short(succs[0])))
1775 if len(succs) > 1:
1783 if len(succs) > 1:
1776 m = 'histedit: %s'
1784 m = 'histedit: %s'
1777 for n in succs[1:]:
1785 for n in succs[1:]:
1778 ui.debug(m % node.short(n))
1786 ui.debug(m % node.short(n))
1779
1787
1780 if not state.keep:
1788 if not state.keep:
1781 if mapping:
1789 if mapping:
1782 movetopmostbookmarks(repo, state.topmost, ntm)
1790 movetopmostbookmarks(repo, state.topmost, ntm)
1783 # TODO update mq state
1791 # TODO update mq state
1784 else:
1792 else:
1785 mapping = {}
1793 mapping = {}
1786
1794
1787 for n in tmpnodes:
1795 for n in tmpnodes:
1788 if n in repo:
1796 if n in repo:
1789 mapping[n] = ()
1797 mapping[n] = ()
1790
1798
1791 # remove entries about unknown nodes
1799 # remove entries about unknown nodes
1792 nodemap = repo.unfiltered().changelog.nodemap
1800 nodemap = repo.unfiltered().changelog.nodemap
1793 mapping = {k: v for k, v in mapping.items()
1801 mapping = {k: v for k, v in mapping.items()
1794 if k in nodemap and all(n in nodemap for n in v)}
1802 if k in nodemap and all(n in nodemap for n in v)}
1795 scmutil.cleanupnodes(repo, mapping, 'histedit')
1803 scmutil.cleanupnodes(repo, mapping, 'histedit')
1796 hf = fm.hexfunc
1804 hf = fm.hexfunc
1797 fl = fm.formatlist
1805 fl = fm.formatlist
1798 fd = fm.formatdict
1806 fd = fm.formatdict
1799 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1807 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1800 for oldn, newn in mapping.iteritems()},
1808 for oldn, newn in mapping.iteritems()},
1801 key="oldnode", value="newnodes")
1809 key="oldnode", value="newnodes")
1802 fm.data(nodechanges=nodechanges)
1810 fm.data(nodechanges=nodechanges)
1803
1811
1804 state.clear()
1812 state.clear()
1805 if os.path.exists(repo.sjoin('undo')):
1813 if os.path.exists(repo.sjoin('undo')):
1806 os.unlink(repo.sjoin('undo'))
1814 os.unlink(repo.sjoin('undo'))
1807 if repo.vfs.exists('histedit-last-edit.txt'):
1815 if repo.vfs.exists('histedit-last-edit.txt'):
1808 repo.vfs.unlink('histedit-last-edit.txt')
1816 repo.vfs.unlink('histedit-last-edit.txt')
1809
1817
1810 def _aborthistedit(ui, repo, state, nobackup=False):
1818 def _aborthistedit(ui, repo, state, nobackup=False):
1811 try:
1819 try:
1812 state.read()
1820 state.read()
1813 __, leafs, tmpnodes, __ = processreplacement(state)
1821 __, leafs, tmpnodes, __ = processreplacement(state)
1814 ui.debug('restore wc to old parent %s\n'
1822 ui.debug('restore wc to old parent %s\n'
1815 % node.short(state.topmost))
1823 % node.short(state.topmost))
1816
1824
1817 # Recover our old commits if necessary
1825 # Recover our old commits if necessary
1818 if not state.topmost in repo and state.backupfile:
1826 if not state.topmost in repo and state.backupfile:
1819 backupfile = repo.vfs.join(state.backupfile)
1827 backupfile = repo.vfs.join(state.backupfile)
1820 f = hg.openpath(ui, backupfile)
1828 f = hg.openpath(ui, backupfile)
1821 gen = exchange.readbundle(ui, f, backupfile)
1829 gen = exchange.readbundle(ui, f, backupfile)
1822 with repo.transaction('histedit.abort') as tr:
1830 with repo.transaction('histedit.abort') as tr:
1823 bundle2.applybundle(repo, gen, tr, source='histedit',
1831 bundle2.applybundle(repo, gen, tr, source='histedit',
1824 url='bundle:' + backupfile)
1832 url='bundle:' + backupfile)
1825
1833
1826 os.remove(backupfile)
1834 os.remove(backupfile)
1827
1835
1828 # check whether we should update away
1836 # check whether we should update away
1829 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1837 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1830 state.parentctxnode, leafs | tmpnodes):
1838 state.parentctxnode, leafs | tmpnodes):
1831 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1839 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1832 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1840 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1833 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1841 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1834 except Exception:
1842 except Exception:
1835 if state.inprogress():
1843 if state.inprogress():
1836 ui.warn(_('warning: encountered an exception during histedit '
1844 ui.warn(_('warning: encountered an exception during histedit '
1837 '--abort; the repository may not have been completely '
1845 '--abort; the repository may not have been completely '
1838 'cleaned up\n'))
1846 'cleaned up\n'))
1839 raise
1847 raise
1840 finally:
1848 finally:
1841 state.clear()
1849 state.clear()
1842
1850
1843 def _edithisteditplan(ui, repo, state, rules):
1851 def _edithisteditplan(ui, repo, state, rules):
1844 state.read()
1852 state.read()
1845 if not rules:
1853 if not rules:
1846 comment = geteditcomment(ui,
1854 comment = geteditcomment(ui,
1847 node.short(state.parentctxnode),
1855 node.short(state.parentctxnode),
1848 node.short(state.topmost))
1856 node.short(state.topmost))
1849 rules = ruleeditor(repo, ui, state.actions, comment)
1857 rules = ruleeditor(repo, ui, state.actions, comment)
1850 else:
1858 else:
1851 rules = _readfile(ui, rules)
1859 rules = _readfile(ui, rules)
1852 actions = parserules(rules, state)
1860 actions = parserules(rules, state)
1853 ctxs = [repo[act.node] \
1861 ctxs = [repo[act.node] \
1854 for act in state.actions if act.node]
1862 for act in state.actions if act.node]
1855 warnverifyactions(ui, repo, actions, state, ctxs)
1863 warnverifyactions(ui, repo, actions, state, ctxs)
1856 state.actions = actions
1864 state.actions = actions
1857 state.write()
1865 state.write()
1858
1866
1859 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1867 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1860 outg = opts.get('outgoing')
1868 outg = opts.get('outgoing')
1861 rules = opts.get('commands', '')
1869 rules = opts.get('commands', '')
1862 force = opts.get('force')
1870 force = opts.get('force')
1863
1871
1864 cmdutil.checkunfinished(repo)
1872 cmdutil.checkunfinished(repo)
1865 cmdutil.bailifchanged(repo)
1873 cmdutil.bailifchanged(repo)
1866
1874
1867 topmost, empty = repo.dirstate.parents()
1875 topmost, empty = repo.dirstate.parents()
1868 if outg:
1876 if outg:
1869 if freeargs:
1877 if freeargs:
1870 remote = freeargs[0]
1878 remote = freeargs[0]
1871 else:
1879 else:
1872 remote = None
1880 remote = None
1873 root = findoutgoing(ui, repo, remote, force, opts)
1881 root = findoutgoing(ui, repo, remote, force, opts)
1874 else:
1882 else:
1875 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1883 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1876 if len(rr) != 1:
1884 if len(rr) != 1:
1877 raise error.Abort(_('The specified revisions must have '
1885 raise error.Abort(_('The specified revisions must have '
1878 'exactly one common root'))
1886 'exactly one common root'))
1879 root = rr[0].node()
1887 root = rr[0].node()
1880
1888
1881 revs = between(repo, root, topmost, state.keep)
1889 revs = between(repo, root, topmost, state.keep)
1882 if not revs:
1890 if not revs:
1883 raise error.Abort(_('%s is not an ancestor of working directory') %
1891 raise error.Abort(_('%s is not an ancestor of working directory') %
1884 node.short(root))
1892 node.short(root))
1885
1893
1886 ctxs = [repo[r] for r in revs]
1894 ctxs = [repo[r] for r in revs]
1887 if not rules:
1895 if not rules:
1888 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1896 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1889 actions = [pick(state, r) for r in revs]
1897 actions = [pick(state, r) for r in revs]
1890 rules = ruleeditor(repo, ui, actions, comment)
1898 rules = ruleeditor(repo, ui, actions, comment)
1891 else:
1899 else:
1892 rules = _readfile(ui, rules)
1900 rules = _readfile(ui, rules)
1893 actions = parserules(rules, state)
1901 actions = parserules(rules, state)
1894 warnverifyactions(ui, repo, actions, state, ctxs)
1902 warnverifyactions(ui, repo, actions, state, ctxs)
1895
1903
1896 parentctxnode = repo[root].parents()[0].node()
1904 parentctxnode = repo[root].parents()[0].node()
1897
1905
1898 state.parentctxnode = parentctxnode
1906 state.parentctxnode = parentctxnode
1899 state.actions = actions
1907 state.actions = actions
1900 state.topmost = topmost
1908 state.topmost = topmost
1901 state.replacements = []
1909 state.replacements = []
1902
1910
1903 ui.log("histedit", "%d actions to histedit\n", len(actions),
1911 ui.log("histedit", "%d actions to histedit\n", len(actions),
1904 histedit_num_actions=len(actions))
1912 histedit_num_actions=len(actions))
1905
1913
1906 # Create a backup so we can always abort completely.
1914 # Create a backup so we can always abort completely.
1907 backupfile = None
1915 backupfile = None
1908 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1916 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1909 backupfile = repair.backupbundle(repo, [parentctxnode],
1917 backupfile = repair.backupbundle(repo, [parentctxnode],
1910 [topmost], root, 'histedit')
1918 [topmost], root, 'histedit')
1911 state.backupfile = backupfile
1919 state.backupfile = backupfile
1912
1920
1913 def _getsummary(ctx):
1921 def _getsummary(ctx):
1914 # a common pattern is to extract the summary but default to the empty
1922 # a common pattern is to extract the summary but default to the empty
1915 # string
1923 # string
1916 summary = ctx.description() or ''
1924 summary = ctx.description() or ''
1917 if summary:
1925 if summary:
1918 summary = summary.splitlines()[0]
1926 summary = summary.splitlines()[0]
1919 return summary
1927 return summary
1920
1928
1921 def bootstrapcontinue(ui, state, opts):
1929 def bootstrapcontinue(ui, state, opts):
1922 repo = state.repo
1930 repo = state.repo
1923
1931
1924 ms = mergemod.mergestate.read(repo)
1932 ms = mergemod.mergestate.read(repo)
1925 mergeutil.checkunresolved(ms)
1933 mergeutil.checkunresolved(ms)
1926
1934
1927 if state.actions:
1935 if state.actions:
1928 actobj = state.actions.pop(0)
1936 actobj = state.actions.pop(0)
1929
1937
1930 if _isdirtywc(repo):
1938 if _isdirtywc(repo):
1931 actobj.continuedirty()
1939 actobj.continuedirty()
1932 if _isdirtywc(repo):
1940 if _isdirtywc(repo):
1933 abortdirty()
1941 abortdirty()
1934
1942
1935 parentctx, replacements = actobj.continueclean()
1943 parentctx, replacements = actobj.continueclean()
1936
1944
1937 state.parentctxnode = parentctx.node()
1945 state.parentctxnode = parentctx.node()
1938 state.replacements.extend(replacements)
1946 state.replacements.extend(replacements)
1939
1947
1940 return state
1948 return state
1941
1949
1942 def between(repo, old, new, keep):
1950 def between(repo, old, new, keep):
1943 """select and validate the set of revision to edit
1951 """select and validate the set of revision to edit
1944
1952
1945 When keep is false, the specified set can't have children."""
1953 When keep is false, the specified set can't have children."""
1946 revs = repo.revs('%n::%n', old, new)
1954 revs = repo.revs('%n::%n', old, new)
1947 if revs and not keep:
1955 if revs and not keep:
1948 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1956 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1949 repo.revs('(%ld::) - (%ld)', revs, revs)):
1957 repo.revs('(%ld::) - (%ld)', revs, revs)):
1950 raise error.Abort(_('can only histedit a changeset together '
1958 raise error.Abort(_('can only histedit a changeset together '
1951 'with all its descendants'))
1959 'with all its descendants'))
1952 if repo.revs('(%ld) and merge()', revs):
1960 if repo.revs('(%ld) and merge()', revs):
1953 raise error.Abort(_('cannot edit history that contains merges'))
1961 raise error.Abort(_('cannot edit history that contains merges'))
1954 root = repo[revs.first()] # list is already sorted by repo.revs()
1962 root = repo[revs.first()] # list is already sorted by repo.revs()
1955 if not root.mutable():
1963 if not root.mutable():
1956 raise error.Abort(_('cannot edit public changeset: %s') % root,
1964 raise error.Abort(_('cannot edit public changeset: %s') % root,
1957 hint=_("see 'hg help phases' for details"))
1965 hint=_("see 'hg help phases' for details"))
1958 return pycompat.maplist(repo.changelog.node, revs)
1966 return pycompat.maplist(repo.changelog.node, revs)
1959
1967
1960 def ruleeditor(repo, ui, actions, editcomment=""):
1968 def ruleeditor(repo, ui, actions, editcomment=""):
1961 """open an editor to edit rules
1969 """open an editor to edit rules
1962
1970
1963 rules are in the format [ [act, ctx], ...] like in state.rules
1971 rules are in the format [ [act, ctx], ...] like in state.rules
1964 """
1972 """
1965 if repo.ui.configbool("experimental", "histedit.autoverb"):
1973 if repo.ui.configbool("experimental", "histedit.autoverb"):
1966 newact = util.sortdict()
1974 newact = util.sortdict()
1967 for act in actions:
1975 for act in actions:
1968 ctx = repo[act.node]
1976 ctx = repo[act.node]
1969 summary = _getsummary(ctx)
1977 summary = _getsummary(ctx)
1970 fword = summary.split(' ', 1)[0].lower()
1978 fword = summary.split(' ', 1)[0].lower()
1971 added = False
1979 added = False
1972
1980
1973 # if it doesn't end with the special character '!' just skip this
1981 # if it doesn't end with the special character '!' just skip this
1974 if fword.endswith('!'):
1982 if fword.endswith('!'):
1975 fword = fword[:-1]
1983 fword = fword[:-1]
1976 if fword in primaryactions | secondaryactions | tertiaryactions:
1984 if fword in primaryactions | secondaryactions | tertiaryactions:
1977 act.verb = fword
1985 act.verb = fword
1978 # get the target summary
1986 # get the target summary
1979 tsum = summary[len(fword) + 1:].lstrip()
1987 tsum = summary[len(fword) + 1:].lstrip()
1980 # safe but slow: reverse iterate over the actions so we
1988 # safe but slow: reverse iterate over the actions so we
1981 # don't clash on two commits having the same summary
1989 # don't clash on two commits having the same summary
1982 for na, l in reversed(list(newact.iteritems())):
1990 for na, l in reversed(list(newact.iteritems())):
1983 actx = repo[na.node]
1991 actx = repo[na.node]
1984 asum = _getsummary(actx)
1992 asum = _getsummary(actx)
1985 if asum == tsum:
1993 if asum == tsum:
1986 added = True
1994 added = True
1987 l.append(act)
1995 l.append(act)
1988 break
1996 break
1989
1997
1990 if not added:
1998 if not added:
1991 newact[act] = []
1999 newact[act] = []
1992
2000
1993 # copy over and flatten the new list
2001 # copy over and flatten the new list
1994 actions = []
2002 actions = []
1995 for na, l in newact.iteritems():
2003 for na, l in newact.iteritems():
1996 actions.append(na)
2004 actions.append(na)
1997 actions += l
2005 actions += l
1998
2006
1999 rules = '\n'.join([act.torule() for act in actions])
2007 rules = '\n'.join([act.torule() for act in actions])
2000 rules += '\n\n'
2008 rules += '\n\n'
2001 rules += editcomment
2009 rules += editcomment
2002 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2010 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
2003 repopath=repo.path, action='histedit')
2011 repopath=repo.path, action='histedit')
2004
2012
2005 # Save edit rules in .hg/histedit-last-edit.txt in case
2013 # Save edit rules in .hg/histedit-last-edit.txt in case
2006 # the user needs to ask for help after something
2014 # the user needs to ask for help after something
2007 # surprising happens.
2015 # surprising happens.
2008 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2016 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
2009 f.write(rules)
2017 f.write(rules)
2010
2018
2011 return rules
2019 return rules
2012
2020
2013 def parserules(rules, state):
2021 def parserules(rules, state):
2014 """Read the histedit rules string and return list of action objects """
2022 """Read the histedit rules string and return list of action objects """
2015 rules = [l for l in (r.strip() for r in rules.splitlines())
2023 rules = [l for l in (r.strip() for r in rules.splitlines())
2016 if l and not l.startswith('#')]
2024 if l and not l.startswith('#')]
2017 actions = []
2025 actions = []
2018 for r in rules:
2026 for r in rules:
2019 if ' ' not in r:
2027 if ' ' not in r:
2020 raise error.ParseError(_('malformed line "%s"') % r)
2028 raise error.ParseError(_('malformed line "%s"') % r)
2021 verb, rest = r.split(' ', 1)
2029 verb, rest = r.split(' ', 1)
2022
2030
2023 if verb not in actiontable:
2031 if verb not in actiontable:
2024 raise error.ParseError(_('unknown action "%s"') % verb)
2032 raise error.ParseError(_('unknown action "%s"') % verb)
2025
2033
2026 action = actiontable[verb].fromrule(state, rest)
2034 action = actiontable[verb].fromrule(state, rest)
2027 actions.append(action)
2035 actions.append(action)
2028 return actions
2036 return actions
2029
2037
2030 def warnverifyactions(ui, repo, actions, state, ctxs):
2038 def warnverifyactions(ui, repo, actions, state, ctxs):
2031 try:
2039 try:
2032 verifyactions(actions, state, ctxs)
2040 verifyactions(actions, state, ctxs)
2033 except error.ParseError:
2041 except error.ParseError:
2034 if repo.vfs.exists('histedit-last-edit.txt'):
2042 if repo.vfs.exists('histedit-last-edit.txt'):
2035 ui.warn(_('warning: histedit rules saved '
2043 ui.warn(_('warning: histedit rules saved '
2036 'to: .hg/histedit-last-edit.txt\n'))
2044 'to: .hg/histedit-last-edit.txt\n'))
2037 raise
2045 raise
2038
2046
2039 def verifyactions(actions, state, ctxs):
2047 def verifyactions(actions, state, ctxs):
2040 """Verify that there exists exactly one action per given changeset and
2048 """Verify that there exists exactly one action per given changeset and
2041 other constraints.
2049 other constraints.
2042
2050
2043 Will abort if there are to many or too few rules, a malformed rule,
2051 Will abort if there are to many or too few rules, a malformed rule,
2044 or a rule on a changeset outside of the user-given range.
2052 or a rule on a changeset outside of the user-given range.
2045 """
2053 """
2046 expected = set(c.node() for c in ctxs)
2054 expected = set(c.node() for c in ctxs)
2047 seen = set()
2055 seen = set()
2048 prev = None
2056 prev = None
2049
2057
2050 if actions and actions[0].verb in ['roll', 'fold']:
2058 if actions and actions[0].verb in ['roll', 'fold']:
2051 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2059 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2052 actions[0].verb)
2060 actions[0].verb)
2053
2061
2054 for action in actions:
2062 for action in actions:
2055 action.verify(prev, expected, seen)
2063 action.verify(prev, expected, seen)
2056 prev = action
2064 prev = action
2057 if action.node is not None:
2065 if action.node is not None:
2058 seen.add(action.node)
2066 seen.add(action.node)
2059 missing = sorted(expected - seen) # sort to stabilize output
2067 missing = sorted(expected - seen) # sort to stabilize output
2060
2068
2061 if state.repo.ui.configbool('histedit', 'dropmissing'):
2069 if state.repo.ui.configbool('histedit', 'dropmissing'):
2062 if len(actions) == 0:
2070 if len(actions) == 0:
2063 raise error.ParseError(_('no rules provided'),
2071 raise error.ParseError(_('no rules provided'),
2064 hint=_('use strip extension to remove commits'))
2072 hint=_('use strip extension to remove commits'))
2065
2073
2066 drops = [drop(state, n) for n in missing]
2074 drops = [drop(state, n) for n in missing]
2067 # put the in the beginning so they execute immediately and
2075 # put the in the beginning so they execute immediately and
2068 # don't show in the edit-plan in the future
2076 # don't show in the edit-plan in the future
2069 actions[:0] = drops
2077 actions[:0] = drops
2070 elif missing:
2078 elif missing:
2071 raise error.ParseError(_('missing rules for changeset %s') %
2079 raise error.ParseError(_('missing rules for changeset %s') %
2072 node.short(missing[0]),
2080 node.short(missing[0]),
2073 hint=_('use "drop %s" to discard, see also: '
2081 hint=_('use "drop %s" to discard, see also: '
2074 "'hg help -e histedit.config'")
2082 "'hg help -e histedit.config'")
2075 % node.short(missing[0]))
2083 % node.short(missing[0]))
2076
2084
2077 def adjustreplacementsfrommarkers(repo, oldreplacements):
2085 def adjustreplacementsfrommarkers(repo, oldreplacements):
2078 """Adjust replacements from obsolescence markers
2086 """Adjust replacements from obsolescence markers
2079
2087
2080 Replacements structure is originally generated based on
2088 Replacements structure is originally generated based on
2081 histedit's state and does not account for changes that are
2089 histedit's state and does not account for changes that are
2082 not recorded there. This function fixes that by adding
2090 not recorded there. This function fixes that by adding
2083 data read from obsolescence markers"""
2091 data read from obsolescence markers"""
2084 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2092 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2085 return oldreplacements
2093 return oldreplacements
2086
2094
2087 unfi = repo.unfiltered()
2095 unfi = repo.unfiltered()
2088 nm = unfi.changelog.nodemap
2096 nm = unfi.changelog.nodemap
2089 obsstore = repo.obsstore
2097 obsstore = repo.obsstore
2090 newreplacements = list(oldreplacements)
2098 newreplacements = list(oldreplacements)
2091 oldsuccs = [r[1] for r in oldreplacements]
2099 oldsuccs = [r[1] for r in oldreplacements]
2092 # successors that have already been added to succstocheck once
2100 # successors that have already been added to succstocheck once
2093 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2101 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2094 succstocheck = list(seensuccs)
2102 succstocheck = list(seensuccs)
2095 while succstocheck:
2103 while succstocheck:
2096 n = succstocheck.pop()
2104 n = succstocheck.pop()
2097 missing = nm.get(n) is None
2105 missing = nm.get(n) is None
2098 markers = obsstore.successors.get(n, ())
2106 markers = obsstore.successors.get(n, ())
2099 if missing and not markers:
2107 if missing and not markers:
2100 # dead end, mark it as such
2108 # dead end, mark it as such
2101 newreplacements.append((n, ()))
2109 newreplacements.append((n, ()))
2102 for marker in markers:
2110 for marker in markers:
2103 nsuccs = marker[1]
2111 nsuccs = marker[1]
2104 newreplacements.append((n, nsuccs))
2112 newreplacements.append((n, nsuccs))
2105 for nsucc in nsuccs:
2113 for nsucc in nsuccs:
2106 if nsucc not in seensuccs:
2114 if nsucc not in seensuccs:
2107 seensuccs.add(nsucc)
2115 seensuccs.add(nsucc)
2108 succstocheck.append(nsucc)
2116 succstocheck.append(nsucc)
2109
2117
2110 return newreplacements
2118 return newreplacements
2111
2119
2112 def processreplacement(state):
2120 def processreplacement(state):
2113 """process the list of replacements to return
2121 """process the list of replacements to return
2114
2122
2115 1) the final mapping between original and created nodes
2123 1) the final mapping between original and created nodes
2116 2) the list of temporary node created by histedit
2124 2) the list of temporary node created by histedit
2117 3) the list of new commit created by histedit"""
2125 3) the list of new commit created by histedit"""
2118 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2126 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2119 allsuccs = set()
2127 allsuccs = set()
2120 replaced = set()
2128 replaced = set()
2121 fullmapping = {}
2129 fullmapping = {}
2122 # initialize basic set
2130 # initialize basic set
2123 # fullmapping records all operations recorded in replacement
2131 # fullmapping records all operations recorded in replacement
2124 for rep in replacements:
2132 for rep in replacements:
2125 allsuccs.update(rep[1])
2133 allsuccs.update(rep[1])
2126 replaced.add(rep[0])
2134 replaced.add(rep[0])
2127 fullmapping.setdefault(rep[0], set()).update(rep[1])
2135 fullmapping.setdefault(rep[0], set()).update(rep[1])
2128 new = allsuccs - replaced
2136 new = allsuccs - replaced
2129 tmpnodes = allsuccs & replaced
2137 tmpnodes = allsuccs & replaced
2130 # Reduce content fullmapping into direct relation between original nodes
2138 # Reduce content fullmapping into direct relation between original nodes
2131 # and final node created during history edition
2139 # and final node created during history edition
2132 # Dropped changeset are replaced by an empty list
2140 # Dropped changeset are replaced by an empty list
2133 toproceed = set(fullmapping)
2141 toproceed = set(fullmapping)
2134 final = {}
2142 final = {}
2135 while toproceed:
2143 while toproceed:
2136 for x in list(toproceed):
2144 for x in list(toproceed):
2137 succs = fullmapping[x]
2145 succs = fullmapping[x]
2138 for s in list(succs):
2146 for s in list(succs):
2139 if s in toproceed:
2147 if s in toproceed:
2140 # non final node with unknown closure
2148 # non final node with unknown closure
2141 # We can't process this now
2149 # We can't process this now
2142 break
2150 break
2143 elif s in final:
2151 elif s in final:
2144 # non final node, replace with closure
2152 # non final node, replace with closure
2145 succs.remove(s)
2153 succs.remove(s)
2146 succs.update(final[s])
2154 succs.update(final[s])
2147 else:
2155 else:
2148 final[x] = succs
2156 final[x] = succs
2149 toproceed.remove(x)
2157 toproceed.remove(x)
2150 # remove tmpnodes from final mapping
2158 # remove tmpnodes from final mapping
2151 for n in tmpnodes:
2159 for n in tmpnodes:
2152 del final[n]
2160 del final[n]
2153 # we expect all changes involved in final to exist in the repo
2161 # we expect all changes involved in final to exist in the repo
2154 # turn `final` into list (topologically sorted)
2162 # turn `final` into list (topologically sorted)
2155 nm = state.repo.changelog.nodemap
2163 nm = state.repo.changelog.nodemap
2156 for prec, succs in final.items():
2164 for prec, succs in final.items():
2157 final[prec] = sorted(succs, key=nm.get)
2165 final[prec] = sorted(succs, key=nm.get)
2158
2166
2159 # computed topmost element (necessary for bookmark)
2167 # computed topmost element (necessary for bookmark)
2160 if new:
2168 if new:
2161 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2169 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2162 elif not final:
2170 elif not final:
2163 # Nothing rewritten at all. we won't need `newtopmost`
2171 # Nothing rewritten at all. we won't need `newtopmost`
2164 # It is the same as `oldtopmost` and `processreplacement` know it
2172 # It is the same as `oldtopmost` and `processreplacement` know it
2165 newtopmost = None
2173 newtopmost = None
2166 else:
2174 else:
2167 # every body died. The newtopmost is the parent of the root.
2175 # every body died. The newtopmost is the parent of the root.
2168 r = state.repo.changelog.rev
2176 r = state.repo.changelog.rev
2169 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2177 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2170
2178
2171 return final, tmpnodes, new, newtopmost
2179 return final, tmpnodes, new, newtopmost
2172
2180
2173 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2181 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2174 """Move bookmark from oldtopmost to newly created topmost
2182 """Move bookmark from oldtopmost to newly created topmost
2175
2183
2176 This is arguably a feature and we may only want that for the active
2184 This is arguably a feature and we may only want that for the active
2177 bookmark. But the behavior is kept compatible with the old version for now.
2185 bookmark. But the behavior is kept compatible with the old version for now.
2178 """
2186 """
2179 if not oldtopmost or not newtopmost:
2187 if not oldtopmost or not newtopmost:
2180 return
2188 return
2181 oldbmarks = repo.nodebookmarks(oldtopmost)
2189 oldbmarks = repo.nodebookmarks(oldtopmost)
2182 if oldbmarks:
2190 if oldbmarks:
2183 with repo.lock(), repo.transaction('histedit') as tr:
2191 with repo.lock(), repo.transaction('histedit') as tr:
2184 marks = repo._bookmarks
2192 marks = repo._bookmarks
2185 changes = []
2193 changes = []
2186 for name in oldbmarks:
2194 for name in oldbmarks:
2187 changes.append((name, newtopmost))
2195 changes.append((name, newtopmost))
2188 marks.applychanges(repo, tr, changes)
2196 marks.applychanges(repo, tr, changes)
2189
2197
2190 def cleanupnode(ui, repo, nodes, nobackup=False):
2198 def cleanupnode(ui, repo, nodes, nobackup=False):
2191 """strip a group of nodes from the repository
2199 """strip a group of nodes from the repository
2192
2200
2193 The set of node to strip may contains unknown nodes."""
2201 The set of node to strip may contains unknown nodes."""
2194 with repo.lock():
2202 with repo.lock():
2195 # do not let filtering get in the way of the cleanse
2203 # do not let filtering get in the way of the cleanse
2196 # we should probably get rid of obsolescence marker created during the
2204 # we should probably get rid of obsolescence marker created during the
2197 # histedit, but we currently do not have such information.
2205 # histedit, but we currently do not have such information.
2198 repo = repo.unfiltered()
2206 repo = repo.unfiltered()
2199 # Find all nodes that need to be stripped
2207 # Find all nodes that need to be stripped
2200 # (we use %lr instead of %ln to silently ignore unknown items)
2208 # (we use %lr instead of %ln to silently ignore unknown items)
2201 nm = repo.changelog.nodemap
2209 nm = repo.changelog.nodemap
2202 nodes = sorted(n for n in nodes if n in nm)
2210 nodes = sorted(n for n in nodes if n in nm)
2203 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2211 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2204 if roots:
2212 if roots:
2205 backup = not nobackup
2213 backup = not nobackup
2206 repair.strip(ui, repo, roots, backup=backup)
2214 repair.strip(ui, repo, roots, backup=backup)
2207
2215
2208 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2216 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2209 if isinstance(nodelist, str):
2217 if isinstance(nodelist, str):
2210 nodelist = [nodelist]
2218 nodelist = [nodelist]
2211 state = histeditstate(repo)
2219 state = histeditstate(repo)
2212 if state.inprogress():
2220 if state.inprogress():
2213 state.read()
2221 state.read()
2214 histedit_nodes = {action.node for action
2222 histedit_nodes = {action.node for action
2215 in state.actions if action.node}
2223 in state.actions if action.node}
2216 common_nodes = histedit_nodes & set(nodelist)
2224 common_nodes = histedit_nodes & set(nodelist)
2217 if common_nodes:
2225 if common_nodes:
2218 raise error.Abort(_("histedit in progress, can't strip %s")
2226 raise error.Abort(_("histedit in progress, can't strip %s")
2219 % ', '.join(node.short(x) for x in common_nodes))
2227 % ', '.join(node.short(x) for x in common_nodes))
2220 return orig(ui, repo, nodelist, *args, **kwargs)
2228 return orig(ui, repo, nodelist, *args, **kwargs)
2221
2229
2222 extensions.wrapfunction(repair, 'strip', stripwrapper)
2230 extensions.wrapfunction(repair, 'strip', stripwrapper)
2223
2231
2224 def summaryhook(ui, repo):
2232 def summaryhook(ui, repo):
2225 state = histeditstate(repo)
2233 state = histeditstate(repo)
2226 if not state.inprogress():
2234 if not state.inprogress():
2227 return
2235 return
2228 state.read()
2236 state.read()
2229 if state.actions:
2237 if state.actions:
2230 # i18n: column positioning for "hg summary"
2238 # i18n: column positioning for "hg summary"
2231 ui.write(_('hist: %s (histedit --continue)\n') %
2239 ui.write(_('hist: %s (histedit --continue)\n') %
2232 (ui.label(_('%d remaining'), 'histedit.remaining') %
2240 (ui.label(_('%d remaining'), 'histedit.remaining') %
2233 len(state.actions)))
2241 len(state.actions)))
2234
2242
2235 def extsetup(ui):
2243 def extsetup(ui):
2236 cmdutil.summaryhooks.add('histedit', summaryhook)
2244 cmdutil.summaryhooks.add('histedit', summaryhook)
2237 cmdutil.unfinishedstates.append(
2245 cmdutil.unfinishedstates.append(
2238 ['histedit-state', False, True, _('histedit in progress'),
2246 ['histedit-state', False, True, _('histedit in progress'),
2239 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2247 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2240 cmdutil.afterresolvedstates.append(
2248 cmdutil.afterresolvedstates.append(
2241 ['histedit-state', _('hg histedit --continue')])
2249 ['histedit-state', _('hg histedit --continue')])
@@ -1,504 +1,555 b''
1 $ . "$TESTDIR/histedit-helpers.sh"
1 $ . "$TESTDIR/histedit-helpers.sh"
2
2
3 $ cat >> $HGRCPATH <<EOF
3 $ cat >> $HGRCPATH <<EOF
4 > [extensions]
4 > [extensions]
5 > histedit=
5 > histedit=
6 > strip=
6 > strip=
7 > mockmakedate = $TESTDIR/mockmakedate.py
7 > EOF
8 > EOF
8
9
9 $ initrepo ()
10 $ initrepo ()
10 > {
11 > {
11 > hg init r
12 > hg init r
12 > cd r
13 > cd r
13 > for x in a b c d e f g; do
14 > for x in a b c d e f g; do
14 > echo $x > $x
15 > echo $x > $x
15 > hg add $x
16 > hg add $x
16 > hg ci -m $x
17 > hg ci -m $x
17 > done
18 > done
18 > }
19 > }
19
20
20 $ initrepo
21 $ initrepo
21
22
22 log before edit
23 log before edit
23 $ hg log --graph
24 $ hg log --graph
24 @ changeset: 6:3c6a8ed2ebe8
25 @ changeset: 6:3c6a8ed2ebe8
25 | tag: tip
26 | tag: tip
26 | user: test
27 | user: test
27 | date: Thu Jan 01 00:00:00 1970 +0000
28 | date: Thu Jan 01 00:00:00 1970 +0000
28 | summary: g
29 | summary: g
29 |
30 |
30 o changeset: 5:652413bf663e
31 o changeset: 5:652413bf663e
31 | user: test
32 | user: test
32 | date: Thu Jan 01 00:00:00 1970 +0000
33 | date: Thu Jan 01 00:00:00 1970 +0000
33 | summary: f
34 | summary: f
34 |
35 |
35 o changeset: 4:e860deea161a
36 o changeset: 4:e860deea161a
36 | user: test
37 | user: test
37 | date: Thu Jan 01 00:00:00 1970 +0000
38 | date: Thu Jan 01 00:00:00 1970 +0000
38 | summary: e
39 | summary: e
39 |
40 |
40 o changeset: 3:055a42cdd887
41 o changeset: 3:055a42cdd887
41 | user: test
42 | user: test
42 | date: Thu Jan 01 00:00:00 1970 +0000
43 | date: Thu Jan 01 00:00:00 1970 +0000
43 | summary: d
44 | summary: d
44 |
45 |
45 o changeset: 2:177f92b77385
46 o changeset: 2:177f92b77385
46 | user: test
47 | user: test
47 | date: Thu Jan 01 00:00:00 1970 +0000
48 | date: Thu Jan 01 00:00:00 1970 +0000
48 | summary: c
49 | summary: c
49 |
50 |
50 o changeset: 1:d2ae7f538514
51 o changeset: 1:d2ae7f538514
51 | user: test
52 | user: test
52 | date: Thu Jan 01 00:00:00 1970 +0000
53 | date: Thu Jan 01 00:00:00 1970 +0000
53 | summary: b
54 | summary: b
54 |
55 |
55 o changeset: 0:cb9a9f314b8b
56 o changeset: 0:cb9a9f314b8b
56 user: test
57 user: test
57 date: Thu Jan 01 00:00:00 1970 +0000
58 date: Thu Jan 01 00:00:00 1970 +0000
58 summary: a
59 summary: a
59
60
60 dirty a file
61 dirty a file
61 $ echo a > g
62 $ echo a > g
62 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF
63 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF
63 > EOF
64 > EOF
64 abort: uncommitted changes
65 abort: uncommitted changes
65 [255]
66 [255]
66 $ echo g > g
67 $ echo g > g
67
68
68 edit the history
69 edit the history
69 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
70 $ hg histedit 177f92b77385 --commands - 2>&1 << EOF| fixbundle
70 > pick 177f92b77385 c
71 > pick 177f92b77385 c
71 > pick 055a42cdd887 d
72 > pick 055a42cdd887 d
72 > edit e860deea161a e
73 > edit e860deea161a e
73 > pick 652413bf663e f
74 > pick 652413bf663e f
74 > pick 3c6a8ed2ebe8 g
75 > pick 3c6a8ed2ebe8 g
75 > EOF
76 > EOF
76 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
77 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
77 Editing (e860deea161a), you may commit or record as needed now.
78 Editing (e860deea161a), you may commit or record as needed now.
78 (hg histedit --continue to resume)
79 (hg histedit --continue to resume)
79
80
80 try to update and get an error
81 try to update and get an error
81 $ hg update tip
82 $ hg update tip
82 abort: histedit in progress
83 abort: histedit in progress
83 (use 'hg histedit --continue' or 'hg histedit --abort')
84 (use 'hg histedit --continue' or 'hg histedit --abort')
84 [255]
85 [255]
85
86
86 edit the plan via the editor
87 edit the plan via the editor
87 $ cat >> $TESTTMP/editplan.sh <<EOF
88 $ cat >> $TESTTMP/editplan.sh <<EOF
88 > cat > \$1 <<EOF2
89 > cat > \$1 <<EOF2
89 > drop e860deea161a e
90 > drop e860deea161a e
90 > drop 652413bf663e f
91 > drop 652413bf663e f
91 > drop 3c6a8ed2ebe8 g
92 > drop 3c6a8ed2ebe8 g
92 > EOF2
93 > EOF2
93 > EOF
94 > EOF
94 $ HGEDITOR="sh $TESTTMP/editplan.sh" hg histedit --edit-plan
95 $ HGEDITOR="sh $TESTTMP/editplan.sh" hg histedit --edit-plan
95 $ cat .hg/histedit-state
96 $ cat .hg/histedit-state
96 v1
97 v1
97 055a42cdd88768532f9cf79daa407fc8d138de9b
98 055a42cdd88768532f9cf79daa407fc8d138de9b
98 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
99 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
99 False
100 False
100 3
101 3
101 drop
102 drop
102 e860deea161a2f77de56603b340ebbb4536308ae
103 e860deea161a2f77de56603b340ebbb4536308ae
103 drop
104 drop
104 652413bf663ef2a641cab26574e46d5f5a64a55a
105 652413bf663ef2a641cab26574e46d5f5a64a55a
105 drop
106 drop
106 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
107 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
107 0
108 0
108 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
109 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
109
110
110 edit the plan via --commands
111 edit the plan via --commands
111 $ hg histedit --edit-plan --commands - 2>&1 << EOF
112 $ hg histedit --edit-plan --commands - 2>&1 << EOF
112 > edit e860deea161a e
113 > edit e860deea161a e
113 > pick 652413bf663e f
114 > pick 652413bf663e f
114 > drop 3c6a8ed2ebe8 g
115 > drop 3c6a8ed2ebe8 g
115 > EOF
116 > EOF
116 $ cat .hg/histedit-state
117 $ cat .hg/histedit-state
117 v1
118 v1
118 055a42cdd88768532f9cf79daa407fc8d138de9b
119 055a42cdd88768532f9cf79daa407fc8d138de9b
119 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
120 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
120 False
121 False
121 3
122 3
122 edit
123 edit
123 e860deea161a2f77de56603b340ebbb4536308ae
124 e860deea161a2f77de56603b340ebbb4536308ae
124 pick
125 pick
125 652413bf663ef2a641cab26574e46d5f5a64a55a
126 652413bf663ef2a641cab26574e46d5f5a64a55a
126 drop
127 drop
127 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
128 3c6a8ed2ebe862cc949d2caa30775dd6f16fb799
128 0
129 0
129 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
130 strip-backup/177f92b77385-0ebe6a8f-histedit.hg
130
131
131 Go at a random point and try to continue
132 Go at a random point and try to continue
132
133
133 $ hg id -n
134 $ hg id -n
134 3+
135 3+
135 $ hg up 0
136 $ hg up 0
136 abort: histedit in progress
137 abort: histedit in progress
137 (use 'hg histedit --continue' or 'hg histedit --abort')
138 (use 'hg histedit --continue' or 'hg histedit --abort')
138 [255]
139 [255]
139
140
140 Try to delete necessary commit
141 Try to delete necessary commit
141 $ hg strip -r 652413b
142 $ hg strip -r 652413b
142 abort: histedit in progress, can't strip 652413bf663e
143 abort: histedit in progress, can't strip 652413bf663e
143 [255]
144 [255]
144
145
145 commit, then edit the revision
146 commit, then edit the revision
146 $ hg ci -m 'wat'
147 $ hg ci -m 'wat'
147 created new head
148 created new head
148 $ echo a > e
149 $ echo a > e
149
150
150 qnew should fail while we're in the middle of the edit step
151 qnew should fail while we're in the middle of the edit step
151
152
152 $ hg --config extensions.mq= qnew please-fail
153 $ hg --config extensions.mq= qnew please-fail
153 abort: histedit in progress
154 abort: histedit in progress
154 (use 'hg histedit --continue' or 'hg histedit --abort')
155 (use 'hg histedit --continue' or 'hg histedit --abort')
155 [255]
156 [255]
156 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
157 $ HGEDITOR='echo foobaz > ' hg histedit --continue 2>&1 | fixbundle
157
158
158 $ hg log --graph
159 $ hg log --graph
159 @ changeset: 6:b5f70786f9b0
160 @ changeset: 6:b5f70786f9b0
160 | tag: tip
161 | tag: tip
161 | user: test
162 | user: test
162 | date: Thu Jan 01 00:00:00 1970 +0000
163 | date: Thu Jan 01 00:00:00 1970 +0000
163 | summary: f
164 | summary: f
164 |
165 |
165 o changeset: 5:a5e1ba2f7afb
166 o changeset: 5:a5e1ba2f7afb
166 | user: test
167 | user: test
167 | date: Thu Jan 01 00:00:00 1970 +0000
168 | date: Thu Jan 01 00:00:00 1970 +0000
168 | summary: foobaz
169 | summary: foobaz
169 |
170 |
170 o changeset: 4:1a60820cd1f6
171 o changeset: 4:1a60820cd1f6
171 | user: test
172 | user: test
172 | date: Thu Jan 01 00:00:00 1970 +0000
173 | date: Thu Jan 01 00:00:00 1970 +0000
173 | summary: wat
174 | summary: wat
174 |
175 |
175 o changeset: 3:055a42cdd887
176 o changeset: 3:055a42cdd887
176 | user: test
177 | user: test
177 | date: Thu Jan 01 00:00:00 1970 +0000
178 | date: Thu Jan 01 00:00:00 1970 +0000
178 | summary: d
179 | summary: d
179 |
180 |
180 o changeset: 2:177f92b77385
181 o changeset: 2:177f92b77385
181 | user: test
182 | user: test
182 | date: Thu Jan 01 00:00:00 1970 +0000
183 | date: Thu Jan 01 00:00:00 1970 +0000
183 | summary: c
184 | summary: c
184 |
185 |
185 o changeset: 1:d2ae7f538514
186 o changeset: 1:d2ae7f538514
186 | user: test
187 | user: test
187 | date: Thu Jan 01 00:00:00 1970 +0000
188 | date: Thu Jan 01 00:00:00 1970 +0000
188 | summary: b
189 | summary: b
189 |
190 |
190 o changeset: 0:cb9a9f314b8b
191 o changeset: 0:cb9a9f314b8b
191 user: test
192 user: test
192 date: Thu Jan 01 00:00:00 1970 +0000
193 date: Thu Jan 01 00:00:00 1970 +0000
193 summary: a
194 summary: a
194
195
195
196
196 $ hg cat e
197 $ hg cat e
197 a
198 a
198
199
199 Stripping necessary commits should not break --abort
200 Stripping necessary commits should not break --abort
200
201
201 $ hg histedit 1a60820cd1f6 --commands - 2>&1 << EOF| fixbundle
202 $ hg histedit 1a60820cd1f6 --commands - 2>&1 << EOF| fixbundle
202 > edit 1a60820cd1f6 wat
203 > edit 1a60820cd1f6 wat
203 > pick a5e1ba2f7afb foobaz
204 > pick a5e1ba2f7afb foobaz
204 > pick b5f70786f9b0 g
205 > pick b5f70786f9b0 g
205 > EOF
206 > EOF
206 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
207 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
207 Editing (1a60820cd1f6), you may commit or record as needed now.
208 Editing (1a60820cd1f6), you may commit or record as needed now.
208 (hg histedit --continue to resume)
209 (hg histedit --continue to resume)
209
210
210 $ mv .hg/histedit-state .hg/histedit-state.bak
211 $ mv .hg/histedit-state .hg/histedit-state.bak
211 $ hg strip -q -r b5f70786f9b0
212 $ hg strip -q -r b5f70786f9b0
212 $ mv .hg/histedit-state.bak .hg/histedit-state
213 $ mv .hg/histedit-state.bak .hg/histedit-state
213 $ hg histedit --abort
214 $ hg histedit --abort
214 adding changesets
215 adding changesets
215 adding manifests
216 adding manifests
216 adding file changes
217 adding file changes
217 added 1 changesets with 1 changes to 3 files
218 added 1 changesets with 1 changes to 3 files
218 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
219 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
219 $ hg log -r .
220 $ hg log -r .
220 changeset: 6:b5f70786f9b0
221 changeset: 6:b5f70786f9b0
221 tag: tip
222 tag: tip
222 user: test
223 user: test
223 date: Thu Jan 01 00:00:00 1970 +0000
224 date: Thu Jan 01 00:00:00 1970 +0000
224 summary: f
225 summary: f
225
226
226
227
227 check histedit_source
228 check histedit_source
228
229
229 $ hg log --debug --rev 5
230 $ hg log --debug --rev 5
230 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
231 changeset: 5:a5e1ba2f7afb899ef1581cea528fd885d2fca70d
231 phase: draft
232 phase: draft
232 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
233 parent: 4:1a60820cd1f6004a362aa622ebc47d59bc48eb34
233 parent: -1:0000000000000000000000000000000000000000
234 parent: -1:0000000000000000000000000000000000000000
234 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
235 manifest: 5:5ad3be8791f39117565557781f5464363b918a45
235 user: test
236 user: test
236 date: Thu Jan 01 00:00:00 1970 +0000
237 date: Thu Jan 01 00:00:00 1970 +0000
237 files: e
238 files: e
238 extra: branch=default
239 extra: branch=default
239 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
240 extra: histedit_source=e860deea161a2f77de56603b340ebbb4536308ae
240 description:
241 description:
241 foobaz
242 foobaz
242
243
243
244
244
245
245 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
246 $ hg histedit tip --commands - 2>&1 <<EOF| fixbundle
246 > edit b5f70786f9b0 f
247 > edit b5f70786f9b0 f
247 > EOF
248 > EOF
248 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
249 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
249 Editing (b5f70786f9b0), you may commit or record as needed now.
250 Editing (b5f70786f9b0), you may commit or record as needed now.
250 (hg histedit --continue to resume)
251 (hg histedit --continue to resume)
251 $ hg status
252 $ hg status
252 A f
253 A f
253
254
254 $ hg summary
255 $ hg summary
255 parent: 5:a5e1ba2f7afb
256 parent: 5:a5e1ba2f7afb
256 foobaz
257 foobaz
257 branch: default
258 branch: default
258 commit: 1 added (new branch head)
259 commit: 1 added (new branch head)
259 update: 1 new changesets (update)
260 update: 1 new changesets (update)
260 phases: 7 draft
261 phases: 7 draft
261 hist: 1 remaining (histedit --continue)
262 hist: 1 remaining (histedit --continue)
262
263
263 (test also that editor is invoked if histedit is continued for
264 (test also that editor is invoked if histedit is continued for
264 "edit" action)
265 "edit" action)
265
266
266 $ HGEDITOR='cat' hg histedit --continue
267 $ HGEDITOR='cat' hg histedit --continue
267 f
268 f
268
269
269
270
270 HG: Enter commit message. Lines beginning with 'HG:' are removed.
271 HG: Enter commit message. Lines beginning with 'HG:' are removed.
271 HG: Leave message empty to abort commit.
272 HG: Leave message empty to abort commit.
272 HG: --
273 HG: --
273 HG: user: test
274 HG: user: test
274 HG: branch 'default'
275 HG: branch 'default'
275 HG: added f
276 HG: added f
276 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-histedit.hg
277 saved backup bundle to $TESTTMP/r/.hg/strip-backup/b5f70786f9b0-c28d9c86-histedit.hg
277
278
278 $ hg status
279 $ hg status
279
280
280 log after edit
281 log after edit
281 $ hg log --limit 1
282 $ hg log --limit 1
282 changeset: 6:a107ee126658
283 changeset: 6:a107ee126658
283 tag: tip
284 tag: tip
284 user: test
285 user: test
285 date: Thu Jan 01 00:00:00 1970 +0000
286 date: Thu Jan 01 00:00:00 1970 +0000
286 summary: f
287 summary: f
287
288
288
289
289 say we'll change the message, but don't.
290 say we'll change the message, but don't.
290 $ cat > ../edit.sh <<EOF
291 $ cat > ../edit.sh <<EOF
291 > cat "\$1" | sed s/pick/mess/ > tmp
292 > cat "\$1" | sed s/pick/mess/ > tmp
292 > mv tmp "\$1"
293 > mv tmp "\$1"
293 > EOF
294 > EOF
294 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
295 $ HGEDITOR="sh ../edit.sh" hg histedit tip 2>&1 | fixbundle
295 $ hg status
296 $ hg status
296 $ hg log --limit 1
297 $ hg log --limit 1
297 changeset: 6:1fd3b2fe7754
298 changeset: 6:1fd3b2fe7754
298 tag: tip
299 tag: tip
299 user: test
300 user: test
300 date: Thu Jan 01 00:00:00 1970 +0000
301 date: Thu Jan 01 00:00:00 1970 +0000
301 summary: f
302 summary: f
302
303
303
304
304 modify the message
305 modify the message
305
306
306 check saving last-message.txt, at first
307 check saving last-message.txt, at first
307
308
308 $ cat > $TESTTMP/commitfailure.py <<EOF
309 $ cat > $TESTTMP/commitfailure.py <<EOF
309 > from mercurial import error
310 > from mercurial import error
310 > def reposetup(ui, repo):
311 > def reposetup(ui, repo):
311 > class commitfailure(repo.__class__):
312 > class commitfailure(repo.__class__):
312 > def commit(self, *args, **kwargs):
313 > def commit(self, *args, **kwargs):
313 > raise error.Abort('emulating unexpected abort')
314 > raise error.Abort('emulating unexpected abort')
314 > repo.__class__ = commitfailure
315 > repo.__class__ = commitfailure
315 > EOF
316 > EOF
316 $ cat >> .hg/hgrc <<EOF
317 $ cat >> .hg/hgrc <<EOF
317 > [extensions]
318 > [extensions]
318 > # this failure occurs before editor invocation
319 > # this failure occurs before editor invocation
319 > commitfailure = $TESTTMP/commitfailure.py
320 > commitfailure = $TESTTMP/commitfailure.py
320 > EOF
321 > EOF
321
322
322 $ cat > $TESTTMP/editor.sh <<EOF
323 $ cat > $TESTTMP/editor.sh <<EOF
323 > echo "==== before editing"
324 > echo "==== before editing"
324 > cat \$1
325 > cat \$1
325 > echo "===="
326 > echo "===="
326 > echo "check saving last-message.txt" >> \$1
327 > echo "check saving last-message.txt" >> \$1
327 > EOF
328 > EOF
328
329
329 (test that editor is not invoked before transaction starting)
330 (test that editor is not invoked before transaction starting)
330
331
331 $ rm -f .hg/last-message.txt
332 $ rm -f .hg/last-message.txt
332 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
333 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF | fixbundle
333 > mess 1fd3b2fe7754 f
334 > mess 1fd3b2fe7754 f
334 > EOF
335 > EOF
335 abort: emulating unexpected abort
336 abort: emulating unexpected abort
336 $ test -f .hg/last-message.txt
337 $ test -f .hg/last-message.txt
337 [1]
338 [1]
338
339
339 $ cat >> .hg/hgrc <<EOF
340 $ cat >> .hg/hgrc <<EOF
340 > [extensions]
341 > [extensions]
341 > commitfailure = !
342 > commitfailure = !
342 > EOF
343 > EOF
343 $ hg histedit --abort -q
344 $ hg histedit --abort -q
344
345
345 (test that editor is invoked and commit message is saved into
346 (test that editor is invoked and commit message is saved into
346 "last-message.txt")
347 "last-message.txt")
347
348
348 $ cat >> .hg/hgrc <<EOF
349 $ cat >> .hg/hgrc <<EOF
349 > [hooks]
350 > [hooks]
350 > # this failure occurs after editor invocation
351 > # this failure occurs after editor invocation
351 > pretxncommit.unexpectedabort = false
352 > pretxncommit.unexpectedabort = false
352 > EOF
353 > EOF
353
354
354 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
355 $ hg status --rev '1fd3b2fe7754^1' --rev 1fd3b2fe7754
355 A f
356 A f
356
357
357 $ rm -f .hg/last-message.txt
358 $ rm -f .hg/last-message.txt
358 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
359 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit tip --commands - 2>&1 << EOF
359 > mess 1fd3b2fe7754 f
360 > mess 1fd3b2fe7754 f
360 > EOF
361 > EOF
361 ==== before editing
362 ==== before editing
362 f
363 f
363
364
364
365
365 HG: Enter commit message. Lines beginning with 'HG:' are removed.
366 HG: Enter commit message. Lines beginning with 'HG:' are removed.
366 HG: Leave message empty to abort commit.
367 HG: Leave message empty to abort commit.
367 HG: --
368 HG: --
368 HG: user: test
369 HG: user: test
369 HG: branch 'default'
370 HG: branch 'default'
370 HG: added f
371 HG: added f
371 ====
372 ====
372 note: commit message saved in .hg/last-message.txt
373 note: commit message saved in .hg/last-message.txt
373 transaction abort!
374 transaction abort!
374 rollback completed
375 rollback completed
375 abort: pretxncommit.unexpectedabort hook exited with status 1
376 abort: pretxncommit.unexpectedabort hook exited with status 1
376 [255]
377 [255]
377 $ cat .hg/last-message.txt
378 $ cat .hg/last-message.txt
378 f
379 f
379
380
380
381
381 check saving last-message.txt
382 check saving last-message.txt
382
383
383 (test also that editor is invoked if histedit is continued for "message"
384 (test also that editor is invoked if histedit is continued for "message"
384 action)
385 action)
385
386
386 $ HGEDITOR=cat hg histedit --continue
387 $ HGEDITOR=cat hg histedit --continue
387 f
388 f
388
389
389
390
390 HG: Enter commit message. Lines beginning with 'HG:' are removed.
391 HG: Enter commit message. Lines beginning with 'HG:' are removed.
391 HG: Leave message empty to abort commit.
392 HG: Leave message empty to abort commit.
392 HG: --
393 HG: --
393 HG: user: test
394 HG: user: test
394 HG: branch 'default'
395 HG: branch 'default'
395 HG: added f
396 HG: added f
396 note: commit message saved in .hg/last-message.txt
397 note: commit message saved in .hg/last-message.txt
397 transaction abort!
398 transaction abort!
398 rollback completed
399 rollback completed
399 abort: pretxncommit.unexpectedabort hook exited with status 1
400 abort: pretxncommit.unexpectedabort hook exited with status 1
400 [255]
401 [255]
401
402
402 $ cat >> .hg/hgrc <<EOF
403 $ cat >> .hg/hgrc <<EOF
403 > [hooks]
404 > [hooks]
404 > pretxncommit.unexpectedabort =
405 > pretxncommit.unexpectedabort =
405 > EOF
406 > EOF
406 $ hg histedit --abort -q
407 $ hg histedit --abort -q
407
408
408 then, check "modify the message" itself
409 then, check "modify the message" itself
409
410
410 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
411 $ hg histedit tip --commands - 2>&1 << EOF | fixbundle
411 > mess 1fd3b2fe7754 f
412 > mess 1fd3b2fe7754 f
412 > EOF
413 > EOF
413 $ hg status
414 $ hg status
414 $ hg log --limit 1
415 $ hg log --limit 1
415 changeset: 6:62feedb1200e
416 changeset: 6:62feedb1200e
416 tag: tip
417 tag: tip
417 user: test
418 user: test
418 date: Thu Jan 01 00:00:00 1970 +0000
419 date: Thu Jan 01 00:00:00 1970 +0000
419 summary: f
420 summary: f
420
421
421
422
422 rollback should not work after a histedit
423 rollback should not work after a histedit
423 $ hg rollback
424 $ hg rollback
424 no rollback information available
425 no rollback information available
425 [1]
426 [1]
426
427
427 $ cd ..
428 $ cd ..
428 $ hg clone -qr0 r r0
429 $ hg clone -qr0 r r0
429 $ cd r0
430 $ cd r0
430 $ hg phase -fdr0
431 $ hg phase -fdr0
431 $ hg histedit --commands - 0 2>&1 << EOF
432 $ hg histedit --commands - 0 2>&1 << EOF
432 > edit cb9a9f314b8b a > $EDITED
433 > edit cb9a9f314b8b a > $EDITED
433 > EOF
434 > EOF
434 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
435 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
435 adding a
436 adding a
436 Editing (cb9a9f314b8b), you may commit or record as needed now.
437 Editing (cb9a9f314b8b), you may commit or record as needed now.
437 (hg histedit --continue to resume)
438 (hg histedit --continue to resume)
438 [1]
439 [1]
439 $ HGEDITOR=true hg histedit --continue
440 $ HGEDITOR=true hg histedit --continue
440 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-histedit.hg
441 saved backup bundle to $TESTTMP/r0/.hg/strip-backup/cb9a9f314b8b-cc5ccb0b-histedit.hg
441
442
442 $ hg log -G
443 $ hg log -G
443 @ changeset: 0:0efcea34f18a
444 @ changeset: 0:0efcea34f18a
444 tag: tip
445 tag: tip
445 user: test
446 user: test
446 date: Thu Jan 01 00:00:00 1970 +0000
447 date: Thu Jan 01 00:00:00 1970 +0000
447 summary: a
448 summary: a
448
449
449 $ echo foo >> b
450 $ echo foo >> b
450 $ hg addr
451 $ hg addr
451 adding b
452 adding b
452 $ hg ci -m 'add b'
453 $ hg ci -m 'add b'
453 $ echo foo >> a
454 $ echo foo >> a
454 $ hg ci -m 'extend a'
455 $ hg ci -m 'extend a'
455 $ hg phase --public 1
456 $ hg phase --public 1
456 Attempting to fold a change into a public change should not work:
457 Attempting to fold a change into a public change should not work:
457 $ cat > ../edit.sh <<EOF
458 $ cat > ../edit.sh <<EOF
458 > cat "\$1" | sed s/pick/fold/ > tmp
459 > cat "\$1" | sed s/pick/fold/ > tmp
459 > mv tmp "\$1"
460 > mv tmp "\$1"
460 > EOF
461 > EOF
461 $ HGEDITOR="sh ../edit.sh" hg histedit 2
462 $ HGEDITOR="sh ../edit.sh" hg histedit 2
462 warning: histedit rules saved to: .hg/histedit-last-edit.txt
463 warning: histedit rules saved to: .hg/histedit-last-edit.txt
463 hg: parse error: first changeset cannot use verb "fold"
464 hg: parse error: first changeset cannot use verb "fold"
464 [255]
465 [255]
465 $ cat .hg/histedit-last-edit.txt
466 $ cat .hg/histedit-last-edit.txt
466 fold 0012be4a27ea 2 extend a
467 fold 0012be4a27ea 2 extend a
467
468
468 # Edit history between 0012be4a27ea and 0012be4a27ea
469 # Edit history between 0012be4a27ea and 0012be4a27ea
469 #
470 #
470 # Commits are listed from least to most recent
471 # Commits are listed from least to most recent
471 #
472 #
472 # You can reorder changesets by reordering the lines
473 # You can reorder changesets by reordering the lines
473 #
474 #
474 # Commands:
475 # Commands:
475 #
476 #
476 # e, edit = use commit, but stop for amending
477 # e, edit = use commit, but stop for amending
477 # m, mess = edit commit message without changing commit content
478 # m, mess = edit commit message without changing commit content
478 # p, fold = use commit
479 # p, fold = use commit
479 # b, base = checkout changeset and apply further changesets from there
480 # b, base = checkout changeset and apply further changesets from there
480 # d, drop = remove commit from history
481 # d, drop = remove commit from history
481 # f, fold = use commit, but combine it with the one above
482 # f, fold = use commit, but combine it with the one above
482 # r, roll = like fold, but discard this commit's description and date
483 # r, roll = like fold, but discard this commit's description and date
483 #
484 #
484
485
485 $ cd ..
486 $ cd ..
486
487
488 ============================================
489 Test update-timestamp config option in mess|
490 ============================================
491
492 $ addwithdate ()
493 > {
494 > echo $1 > $1
495 > hg add $1
496 > hg ci -m $1 -d "$2 0"
497 > }
498
499 $ initrepo ()
500 > {
501 > hg init r2
502 > cd r2
503 > addwithdate a 1
504 > addwithdate b 2
505 > addwithdate c 3
506 > addwithdate d 4
507 > addwithdate e 5
508 > addwithdate f 6
509 > }
510
511 $ initrepo
512
513 log before edit
514
515 $ hg log --limit 1
516 changeset: 5:178e35e0ce73
517 tag: tip
518 user: test
519 date: Thu Jan 01 00:00:06 1970 +0000
520 summary: f
521
522 $ hg histedit tip --commands - 2>&1 --config rewrite.update-timestamp=True << EOF | fixbundle
523 > mess 178e35e0ce73 f
524 > EOF
525
526 log after edit
527
528 $ hg log --limit 1
529 changeset: 5:98bf456d476b
530 tag: tip
531 user: test
532 date: Thu Jan 01 00:00:00 1970 +0000
533 summary: f
534
535
536 $ cd ..
537
487 warn the user on editing tagged commits
538 warn the user on editing tagged commits
488
539
489 $ hg init issue4017
540 $ hg init issue4017
490 $ cd issue4017
541 $ cd issue4017
491 $ echo > a
542 $ echo > a
492 $ hg ci -Am 'add a'
543 $ hg ci -Am 'add a'
493 adding a
544 adding a
494 $ hg tag a
545 $ hg tag a
495 $ hg tags
546 $ hg tags
496 tip 1:bd7ee4f3939b
547 tip 1:bd7ee4f3939b
497 a 0:a8a82d372bb3
548 a 0:a8a82d372bb3
498 $ hg histedit
549 $ hg histedit
499 warning: tags associated with the given changeset will be lost after histedit.
550 warning: tags associated with the given changeset will be lost after histedit.
500 do you want to continue (yN)? n
551 do you want to continue (yN)? n
501 abort: histedit cancelled
552 abort: histedit cancelled
502
553
503 [255]
554 [255]
504 $ cd ..
555 $ cd ..
@@ -1,599 +1,707 b''
1 Test histedit extension: Fold commands
1 Test histedit extension: Fold commands
2 ======================================
2 ======================================
3
3
4 This test file is dedicated to testing the fold command in non conflicting
4 This test file is dedicated to testing the fold command in non conflicting
5 case.
5 case.
6
6
7 Initialization
7 Initialization
8 ---------------
8 ---------------
9
9
10
10
11 $ . "$TESTDIR/histedit-helpers.sh"
11 $ . "$TESTDIR/histedit-helpers.sh"
12
12
13 $ cat >> $HGRCPATH <<EOF
13 $ cat >> $HGRCPATH <<EOF
14 > [alias]
14 > [alias]
15 > logt = log --template '{rev}:{node|short} {desc|firstline}\n'
15 > logt = log --template '{rev}:{node|short} {desc|firstline}\n'
16 > [extensions]
16 > [extensions]
17 > histedit=
17 > histedit=
18 > mockmakedate = $TESTDIR/mockmakedate.py
18 > EOF
19 > EOF
19
20
20
21
21 Simple folding
22 Simple folding
22 --------------------
23 --------------------
23 $ addwithdate ()
24 $ addwithdate ()
24 > {
25 > {
25 > echo $1 > $1
26 > echo $1 > $1
26 > hg add $1
27 > hg add $1
27 > hg ci -m $1 -d "$2 0"
28 > hg ci -m $1 -d "$2 0"
28 > }
29 > }
29
30
30 $ initrepo ()
31 $ initrepo ()
31 > {
32 > {
32 > hg init r
33 > hg init r
33 > cd r
34 > cd r
34 > addwithdate a 1
35 > addwithdate a 1
35 > addwithdate b 2
36 > addwithdate b 2
36 > addwithdate c 3
37 > addwithdate c 3
37 > addwithdate d 4
38 > addwithdate d 4
38 > addwithdate e 5
39 > addwithdate e 5
39 > addwithdate f 6
40 > addwithdate f 6
40 > }
41 > }
41
42
42 $ initrepo
43 $ initrepo
43
44
44 log before edit
45 log before edit
45 $ hg logt --graph
46 $ hg logt --graph
46 @ 5:178e35e0ce73 f
47 @ 5:178e35e0ce73 f
47 |
48 |
48 o 4:1ddb6c90f2ee e
49 o 4:1ddb6c90f2ee e
49 |
50 |
50 o 3:532247a8969b d
51 o 3:532247a8969b d
51 |
52 |
52 o 2:ff2c9fa2018b c
53 o 2:ff2c9fa2018b c
53 |
54 |
54 o 1:97d72e5f12c7 b
55 o 1:97d72e5f12c7 b
55 |
56 |
56 o 0:8580ff50825a a
57 o 0:8580ff50825a a
57
58
58
59
59 $ hg histedit ff2c9fa2018b --commands - 2>&1 <<EOF | fixbundle
60 $ hg histedit ff2c9fa2018b --commands - 2>&1 <<EOF | fixbundle
60 > pick 1ddb6c90f2ee e
61 > pick 1ddb6c90f2ee e
61 > pick 178e35e0ce73 f
62 > pick 178e35e0ce73 f
62 > fold ff2c9fa2018b c
63 > fold ff2c9fa2018b c
63 > pick 532247a8969b d
64 > pick 532247a8969b d
64 > EOF
65 > EOF
65
66
66 log after edit
67 log after edit
67 $ hg logt --graph
68 $ hg logt --graph
68 @ 4:c4d7f3def76d d
69 @ 4:c4d7f3def76d d
69 |
70 |
70 o 3:575228819b7e f
71 o 3:575228819b7e f
71 |
72 |
72 o 2:505a591af19e e
73 o 2:505a591af19e e
73 |
74 |
74 o 1:97d72e5f12c7 b
75 o 1:97d72e5f12c7 b
75 |
76 |
76 o 0:8580ff50825a a
77 o 0:8580ff50825a a
77
78
78
79
79 post-fold manifest
80 post-fold manifest
80 $ hg manifest
81 $ hg manifest
81 a
82 a
82 b
83 b
83 c
84 c
84 d
85 d
85 e
86 e
86 f
87 f
87
88
88
89
89 check histedit_source, including that it uses the later date, from the first changeset
90 check histedit_source, including that it uses the later date, from the first changeset
90
91
91 $ hg log --debug --rev 3
92 $ hg log --debug --rev 3
92 changeset: 3:575228819b7e6ed69e8c0a6a383ee59a80db7358
93 changeset: 3:575228819b7e6ed69e8c0a6a383ee59a80db7358
93 phase: draft
94 phase: draft
94 parent: 2:505a591af19eed18f560af827b9e03d2076773dc
95 parent: 2:505a591af19eed18f560af827b9e03d2076773dc
95 parent: -1:0000000000000000000000000000000000000000
96 parent: -1:0000000000000000000000000000000000000000
96 manifest: 3:81eede616954057198ead0b2c73b41d1f392829a
97 manifest: 3:81eede616954057198ead0b2c73b41d1f392829a
97 user: test
98 user: test
98 date: Thu Jan 01 00:00:06 1970 +0000
99 date: Thu Jan 01 00:00:06 1970 +0000
99 files+: c f
100 files+: c f
100 extra: branch=default
101 extra: branch=default
101 extra: histedit_source=7cad1d7030207872dfd1c3a7cb430f24f2884086,ff2c9fa2018b15fa74b33363bda9527323e2a99f
102 extra: histedit_source=7cad1d7030207872dfd1c3a7cb430f24f2884086,ff2c9fa2018b15fa74b33363bda9527323e2a99f
102 description:
103 description:
103 f
104 f
104 ***
105 ***
105 c
106 c
106
107
107
108
108
109
109 rollup will fold without preserving the folded commit's message or date
110 rollup will fold without preserving the folded commit's message or date
110
111
111 $ OLDHGEDITOR=$HGEDITOR
112 $ OLDHGEDITOR=$HGEDITOR
112 $ HGEDITOR=false
113 $ HGEDITOR=false
113 $ hg histedit 97d72e5f12c7 --commands - 2>&1 <<EOF | fixbundle
114 $ hg histedit 97d72e5f12c7 --commands - 2>&1 <<EOF | fixbundle
114 > pick 97d72e5f12c7 b
115 > pick 97d72e5f12c7 b
115 > roll 505a591af19e e
116 > roll 505a591af19e e
116 > pick 575228819b7e f
117 > pick 575228819b7e f
117 > pick c4d7f3def76d d
118 > pick c4d7f3def76d d
118 > EOF
119 > EOF
119
120
120 $ HGEDITOR=$OLDHGEDITOR
121 $ HGEDITOR=$OLDHGEDITOR
121
122
122 log after edit
123 log after edit
123 $ hg logt --graph
124 $ hg logt --graph
124 @ 3:bab801520cec d
125 @ 3:bab801520cec d
125 |
126 |
126 o 2:58c8f2bfc151 f
127 o 2:58c8f2bfc151 f
127 |
128 |
128 o 1:5d939c56c72e b
129 o 1:5d939c56c72e b
129 |
130 |
130 o 0:8580ff50825a a
131 o 0:8580ff50825a a
131
132
132
133
133 description is taken from rollup target commit
134 description is taken from rollup target commit
134
135
135 $ hg log --debug --rev 1
136 $ hg log --debug --rev 1
136 changeset: 1:5d939c56c72e77e29f5167696218e2131a40f5cf
137 changeset: 1:5d939c56c72e77e29f5167696218e2131a40f5cf
137 phase: draft
138 phase: draft
138 parent: 0:8580ff50825a50c8f716709acdf8de0deddcd6ab
139 parent: 0:8580ff50825a50c8f716709acdf8de0deddcd6ab
139 parent: -1:0000000000000000000000000000000000000000
140 parent: -1:0000000000000000000000000000000000000000
140 manifest: 1:b5e112a3a8354e269b1524729f0918662d847c38
141 manifest: 1:b5e112a3a8354e269b1524729f0918662d847c38
141 user: test
142 user: test
142 date: Thu Jan 01 00:00:02 1970 +0000
143 date: Thu Jan 01 00:00:02 1970 +0000
143 files+: b e
144 files+: b e
144 extra: branch=default
145 extra: branch=default
145 extra: histedit_source=97d72e5f12c7e84f85064aa72e5a297142c36ed9,505a591af19eed18f560af827b9e03d2076773dc
146 extra: histedit_source=97d72e5f12c7e84f85064aa72e5a297142c36ed9,505a591af19eed18f560af827b9e03d2076773dc
146 description:
147 description:
147 b
148 b
148
149
149
150
150
151
151 check saving last-message.txt
152 check saving last-message.txt
152
153
153 $ cat > $TESTTMP/abortfolding.py <<EOF
154 $ cat > $TESTTMP/abortfolding.py <<EOF
154 > from mercurial import util
155 > from mercurial import util
155 > def abortfolding(ui, repo, hooktype, **kwargs):
156 > def abortfolding(ui, repo, hooktype, **kwargs):
156 > ctx = repo[kwargs.get('node')]
157 > ctx = repo[kwargs.get('node')]
157 > if set(ctx.files()) == {b'c', b'd', b'f'}:
158 > if set(ctx.files()) == {b'c', b'd', b'f'}:
158 > return True # abort folding commit only
159 > return True # abort folding commit only
159 > ui.warn(b'allow non-folding commit\\n')
160 > ui.warn(b'allow non-folding commit\\n')
160 > EOF
161 > EOF
161 $ cat > .hg/hgrc <<EOF
162 $ cat > .hg/hgrc <<EOF
162 > [hooks]
163 > [hooks]
163 > pretxncommit.abortfolding = python:$TESTTMP/abortfolding.py:abortfolding
164 > pretxncommit.abortfolding = python:$TESTTMP/abortfolding.py:abortfolding
164 > EOF
165 > EOF
165
166
166 $ cat > $TESTTMP/editor.sh << EOF
167 $ cat > $TESTTMP/editor.sh << EOF
167 > echo "==== before editing"
168 > echo "==== before editing"
168 > cat \$1
169 > cat \$1
169 > echo "===="
170 > echo "===="
170 > echo "check saving last-message.txt" >> \$1
171 > echo "check saving last-message.txt" >> \$1
171 > EOF
172 > EOF
172
173
173 $ rm -f .hg/last-message.txt
174 $ rm -f .hg/last-message.txt
174 $ hg status --rev '58c8f2bfc151^1::bab801520cec'
175 $ hg status --rev '58c8f2bfc151^1::bab801520cec'
175 A c
176 A c
176 A d
177 A d
177 A f
178 A f
178 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit 58c8f2bfc151 --commands - 2>&1 <<EOF
179 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit 58c8f2bfc151 --commands - 2>&1 <<EOF
179 > pick 58c8f2bfc151 f
180 > pick 58c8f2bfc151 f
180 > fold bab801520cec d
181 > fold bab801520cec d
181 > EOF
182 > EOF
182 allow non-folding commit
183 allow non-folding commit
183 ==== before editing
184 ==== before editing
184 f
185 f
185 ***
186 ***
186 c
187 c
187 ***
188 ***
188 d
189 d
189
190
190
191
191
192
192 HG: Enter commit message. Lines beginning with 'HG:' are removed.
193 HG: Enter commit message. Lines beginning with 'HG:' are removed.
193 HG: Leave message empty to abort commit.
194 HG: Leave message empty to abort commit.
194 HG: --
195 HG: --
195 HG: user: test
196 HG: user: test
196 HG: branch 'default'
197 HG: branch 'default'
197 HG: added c
198 HG: added c
198 HG: added d
199 HG: added d
199 HG: added f
200 HG: added f
200 ====
201 ====
201 transaction abort!
202 transaction abort!
202 rollback completed
203 rollback completed
203 abort: pretxncommit.abortfolding hook failed
204 abort: pretxncommit.abortfolding hook failed
204 [255]
205 [255]
205
206
206 $ cat .hg/last-message.txt
207 $ cat .hg/last-message.txt
207 f
208 f
208 ***
209 ***
209 c
210 c
210 ***
211 ***
211 d
212 d
212
213
213
214
214
215
215 check saving last-message.txt
216 check saving last-message.txt
216
217
217 $ cd ..
218 $ cd ..
218 $ rm -r r
219 $ rm -r r
219
220
220 folding preserves initial author but uses later date
221 folding preserves initial author but uses later date
221 ----------------------------------------------------
222 ----------------------------------------------------
222
223
223 $ initrepo
224 $ initrepo
224
225
225 $ hg ci -d '7 0' --user "someone else" --amend --quiet
226 $ hg ci -d '7 0' --user "someone else" --amend --quiet
226
227
227 tip before edit
228 tip before edit
228 $ hg log --rev .
229 $ hg log --rev .
229 changeset: 5:10c36dd37515
230 changeset: 5:10c36dd37515
230 tag: tip
231 tag: tip
231 user: someone else
232 user: someone else
232 date: Thu Jan 01 00:00:07 1970 +0000
233 date: Thu Jan 01 00:00:07 1970 +0000
233 summary: f
234 summary: f
234
235
235
236
236 $ hg --config progress.debug=1 --debug \
237 $ hg --config progress.debug=1 --debug \
237 > histedit 1ddb6c90f2ee --commands - 2>&1 <<EOF | \
238 > histedit 1ddb6c90f2ee --commands - 2>&1 <<EOF | \
238 > egrep 'editing|unresolved'
239 > egrep 'editing|unresolved'
239 > pick 1ddb6c90f2ee e
240 > pick 1ddb6c90f2ee e
240 > fold 10c36dd37515 f
241 > fold 10c36dd37515 f
241 > EOF
242 > EOF
242 editing: pick 1ddb6c90f2ee 4 e 1/2 changes (50.00%)
243 editing: pick 1ddb6c90f2ee 4 e 1/2 changes (50.00%)
243 editing: fold 10c36dd37515 5 f 2/2 changes (100.00%)
244 editing: fold 10c36dd37515 5 f 2/2 changes (100.00%)
244
245
245 tip after edit, which should use the later date, from the second changeset
246 tip after edit, which should use the later date, from the second changeset
246 $ hg log --rev .
247 $ hg log --rev .
247 changeset: 4:e4f3ec5d0b40
248 changeset: 4:e4f3ec5d0b40
248 tag: tip
249 tag: tip
249 user: test
250 user: test
250 date: Thu Jan 01 00:00:07 1970 +0000
251 date: Thu Jan 01 00:00:07 1970 +0000
251 summary: e
252 summary: e
252
253
253
254
254 $ cd ..
255 $ cd ..
255 $ rm -r r
256 $ rm -r r
256
257
257 folding and creating no new change doesn't break:
258 folding and creating no new change doesn't break:
258 -------------------------------------------------
259 -------------------------------------------------
259
260
260 folded content is dropped during a merge. The folded commit should properly disappear.
261 folded content is dropped during a merge. The folded commit should properly disappear.
261
262
262 $ mkdir fold-to-empty-test
263 $ mkdir fold-to-empty-test
263 $ cd fold-to-empty-test
264 $ cd fold-to-empty-test
264 $ hg init
265 $ hg init
265 $ printf "1\n2\n3\n" > file
266 $ printf "1\n2\n3\n" > file
266 $ hg add file
267 $ hg add file
267 $ hg commit -m '1+2+3'
268 $ hg commit -m '1+2+3'
268 $ echo 4 >> file
269 $ echo 4 >> file
269 $ hg commit -m '+4'
270 $ hg commit -m '+4'
270 $ echo 5 >> file
271 $ echo 5 >> file
271 $ hg commit -m '+5'
272 $ hg commit -m '+5'
272 $ echo 6 >> file
273 $ echo 6 >> file
273 $ hg commit -m '+6'
274 $ hg commit -m '+6'
274 $ hg logt --graph
275 $ hg logt --graph
275 @ 3:251d831eeec5 +6
276 @ 3:251d831eeec5 +6
276 |
277 |
277 o 2:888f9082bf99 +5
278 o 2:888f9082bf99 +5
278 |
279 |
279 o 1:617f94f13c0f +4
280 o 1:617f94f13c0f +4
280 |
281 |
281 o 0:0189ba417d34 1+2+3
282 o 0:0189ba417d34 1+2+3
282
283
283
284
284 $ hg histedit 1 --commands - << EOF
285 $ hg histedit 1 --commands - << EOF
285 > pick 617f94f13c0f 1 +4
286 > pick 617f94f13c0f 1 +4
286 > drop 888f9082bf99 2 +5
287 > drop 888f9082bf99 2 +5
287 > fold 251d831eeec5 3 +6
288 > fold 251d831eeec5 3 +6
288 > EOF
289 > EOF
289 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
290 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
290 merging file
291 merging file
291 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
292 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
292 Fix up the change (fold 251d831eeec5)
293 Fix up the change (fold 251d831eeec5)
293 (hg histedit --continue to resume)
294 (hg histedit --continue to resume)
294 [1]
295 [1]
295 There were conflicts, we keep P1 content. This
296 There were conflicts, we keep P1 content. This
296 should effectively drop the changes from +6.
297 should effectively drop the changes from +6.
297
298
298 $ hg status -v
299 $ hg status -v
299 M file
300 M file
300 ? file.orig
301 ? file.orig
301 # The repository is in an unfinished *histedit* state.
302 # The repository is in an unfinished *histedit* state.
302
303
303 # Unresolved merge conflicts:
304 # Unresolved merge conflicts:
304 #
305 #
305 # file
306 # file
306 #
307 #
307 # To mark files as resolved: hg resolve --mark FILE
308 # To mark files as resolved: hg resolve --mark FILE
308
309
309 # To continue: hg histedit --continue
310 # To continue: hg histedit --continue
310 # To abort: hg histedit --abort
311 # To abort: hg histedit --abort
311
312
312 $ hg resolve -l
313 $ hg resolve -l
313 U file
314 U file
314 $ hg revert -r 'p1()' file
315 $ hg revert -r 'p1()' file
315 $ hg resolve --mark file
316 $ hg resolve --mark file
316 (no more unresolved files)
317 (no more unresolved files)
317 continue: hg histedit --continue
318 continue: hg histedit --continue
318 $ hg histedit --continue
319 $ hg histedit --continue
319 251d831eeec5: empty changeset
320 251d831eeec5: empty changeset
320 saved backup bundle to $TESTTMP/fold-to-empty-test/.hg/strip-backup/888f9082bf99-daa0b8b3-histedit.hg
321 saved backup bundle to $TESTTMP/fold-to-empty-test/.hg/strip-backup/888f9082bf99-daa0b8b3-histedit.hg
321 $ hg logt --graph
322 $ hg logt --graph
322 @ 1:617f94f13c0f +4
323 @ 1:617f94f13c0f +4
323 |
324 |
324 o 0:0189ba417d34 1+2+3
325 o 0:0189ba417d34 1+2+3
325
326
326
327
327 $ cd ..
328 $ cd ..
328
329
329
330
330 Test fold through dropped
331 Test fold through dropped
331 -------------------------
332 -------------------------
332
333
333
334
334 Test corner case where folded revision is separated from its parent by a
335 Test corner case where folded revision is separated from its parent by a
335 dropped revision.
336 dropped revision.
336
337
337
338
338 $ hg init fold-with-dropped
339 $ hg init fold-with-dropped
339 $ cd fold-with-dropped
340 $ cd fold-with-dropped
340 $ printf "1\n2\n3\n" > file
341 $ printf "1\n2\n3\n" > file
341 $ hg commit -Am '1+2+3'
342 $ hg commit -Am '1+2+3'
342 adding file
343 adding file
343 $ echo 4 >> file
344 $ echo 4 >> file
344 $ hg commit -m '+4'
345 $ hg commit -m '+4'
345 $ echo 5 >> file
346 $ echo 5 >> file
346 $ hg commit -m '+5'
347 $ hg commit -m '+5'
347 $ echo 6 >> file
348 $ echo 6 >> file
348 $ hg commit -m '+6'
349 $ hg commit -m '+6'
349 $ hg logt -G
350 $ hg logt -G
350 @ 3:251d831eeec5 +6
351 @ 3:251d831eeec5 +6
351 |
352 |
352 o 2:888f9082bf99 +5
353 o 2:888f9082bf99 +5
353 |
354 |
354 o 1:617f94f13c0f +4
355 o 1:617f94f13c0f +4
355 |
356 |
356 o 0:0189ba417d34 1+2+3
357 o 0:0189ba417d34 1+2+3
357
358
358 $ hg histedit 1 --commands - << EOF
359 $ hg histedit 1 --commands - << EOF
359 > pick 617f94f13c0f 1 +4
360 > pick 617f94f13c0f 1 +4
360 > drop 888f9082bf99 2 +5
361 > drop 888f9082bf99 2 +5
361 > fold 251d831eeec5 3 +6
362 > fold 251d831eeec5 3 +6
362 > EOF
363 > EOF
363 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
364 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
364 merging file
365 merging file
365 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
366 warning: conflicts while merging file! (edit, then use 'hg resolve --mark')
366 Fix up the change (fold 251d831eeec5)
367 Fix up the change (fold 251d831eeec5)
367 (hg histedit --continue to resume)
368 (hg histedit --continue to resume)
368 [1]
369 [1]
369 $ cat > file << EOF
370 $ cat > file << EOF
370 > 1
371 > 1
371 > 2
372 > 2
372 > 3
373 > 3
373 > 4
374 > 4
374 > 5
375 > 5
375 > EOF
376 > EOF
376 $ hg resolve --mark file
377 $ hg resolve --mark file
377 (no more unresolved files)
378 (no more unresolved files)
378 continue: hg histedit --continue
379 continue: hg histedit --continue
379 $ hg commit -m '+5.2'
380 $ hg commit -m '+5.2'
380 created new head
381 created new head
381 $ echo 6 >> file
382 $ echo 6 >> file
382 $ HGEDITOR=cat hg histedit --continue
383 $ HGEDITOR=cat hg histedit --continue
383 +4
384 +4
384 ***
385 ***
385 +5.2
386 +5.2
386 ***
387 ***
387 +6
388 +6
388
389
389
390
390
391
391 HG: Enter commit message. Lines beginning with 'HG:' are removed.
392 HG: Enter commit message. Lines beginning with 'HG:' are removed.
392 HG: Leave message empty to abort commit.
393 HG: Leave message empty to abort commit.
393 HG: --
394 HG: --
394 HG: user: test
395 HG: user: test
395 HG: branch 'default'
396 HG: branch 'default'
396 HG: changed file
397 HG: changed file
397 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-3d69522c-histedit.hg
398 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-3d69522c-histedit.hg
398 $ hg logt -G
399 $ hg logt -G
399 @ 1:10c647b2cdd5 +4
400 @ 1:10c647b2cdd5 +4
400 |
401 |
401 o 0:0189ba417d34 1+2+3
402 o 0:0189ba417d34 1+2+3
402
403
403 $ hg export tip
404 $ hg export tip
404 # HG changeset patch
405 # HG changeset patch
405 # User test
406 # User test
406 # Date 0 0
407 # Date 0 0
407 # Thu Jan 01 00:00:00 1970 +0000
408 # Thu Jan 01 00:00:00 1970 +0000
408 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
409 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
409 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
410 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
410 +4
411 +4
411 ***
412 ***
412 +5.2
413 +5.2
413 ***
414 ***
414 +6
415 +6
415
416
416 diff -r 0189ba417d34 -r 10c647b2cdd5 file
417 diff -r 0189ba417d34 -r 10c647b2cdd5 file
417 --- a/file Thu Jan 01 00:00:00 1970 +0000
418 --- a/file Thu Jan 01 00:00:00 1970 +0000
418 +++ b/file Thu Jan 01 00:00:00 1970 +0000
419 +++ b/file Thu Jan 01 00:00:00 1970 +0000
419 @@ -1,3 +1,6 @@
420 @@ -1,3 +1,6 @@
420 1
421 1
421 2
422 2
422 3
423 3
423 +4
424 +4
424 +5
425 +5
425 +6
426 +6
426 $ cd ..
427 $ cd ..
427
428
428
429
429 Folding with initial rename (issue3729)
430 Folding with initial rename (issue3729)
430 ---------------------------------------
431 ---------------------------------------
431
432
432 $ hg init fold-rename
433 $ hg init fold-rename
433 $ cd fold-rename
434 $ cd fold-rename
434 $ echo a > a.txt
435 $ echo a > a.txt
435 $ hg add a.txt
436 $ hg add a.txt
436 $ hg commit -m a
437 $ hg commit -m a
437 $ hg rename a.txt b.txt
438 $ hg rename a.txt b.txt
438 $ hg commit -m rename
439 $ hg commit -m rename
439 $ echo b >> b.txt
440 $ echo b >> b.txt
440 $ hg commit -m b
441 $ hg commit -m b
441
442
442 $ hg logt --follow b.txt
443 $ hg logt --follow b.txt
443 2:e0371e0426bc b
444 2:e0371e0426bc b
444 1:1c4f440a8085 rename
445 1:1c4f440a8085 rename
445 0:6c795aa153cb a
446 0:6c795aa153cb a
446
447
447 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
448 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
448 > pick 1c4f440a8085 rename
449 > pick 1c4f440a8085 rename
449 > fold e0371e0426bc b
450 > fold e0371e0426bc b
450 > EOF
451 > EOF
451
452
452 $ hg logt --follow b.txt
453 $ hg logt --follow b.txt
453 1:cf858d235c76 rename
454 1:cf858d235c76 rename
454 0:6c795aa153cb a
455 0:6c795aa153cb a
455
456
456 $ cd ..
457 $ cd ..
457
458
458 Folding with swapping
459 Folding with swapping
459 ---------------------
460 ---------------------
460
461
461 This is an excuse to test hook with histedit temporary commit (issue4422)
462 This is an excuse to test hook with histedit temporary commit (issue4422)
462
463
463
464
464 $ hg init issue4422
465 $ hg init issue4422
465 $ cd issue4422
466 $ cd issue4422
466 $ echo a > a.txt
467 $ echo a > a.txt
467 $ hg add a.txt
468 $ hg add a.txt
468 $ hg commit -m a
469 $ hg commit -m a
469 $ echo b > b.txt
470 $ echo b > b.txt
470 $ hg add b.txt
471 $ hg add b.txt
471 $ hg commit -m b
472 $ hg commit -m b
472 $ echo c > c.txt
473 $ echo c > c.txt
473 $ hg add c.txt
474 $ hg add c.txt
474 $ hg commit -m c
475 $ hg commit -m c
475
476
476 $ hg logt
477 $ hg logt
477 2:a1a953ffb4b0 c
478 2:a1a953ffb4b0 c
478 1:199b6bb90248 b
479 1:199b6bb90248 b
479 0:6c795aa153cb a
480 0:6c795aa153cb a
480
481
481 $ hg histedit 6c795aa153cb --config hooks.commit='echo commit $HG_NODE' --config hooks.tonative.commit=True \
482 $ hg histedit 6c795aa153cb --config hooks.commit='echo commit $HG_NODE' --config hooks.tonative.commit=True \
482 > --commands - 2>&1 << EOF | fixbundle
483 > --commands - 2>&1 << EOF | fixbundle
483 > pick 199b6bb90248 b
484 > pick 199b6bb90248 b
484 > fold a1a953ffb4b0 c
485 > fold a1a953ffb4b0 c
485 > pick 6c795aa153cb a
486 > pick 6c795aa153cb a
486 > EOF
487 > EOF
487 commit 9599899f62c05f4377548c32bf1c9f1a39634b0c
488 commit 9599899f62c05f4377548c32bf1c9f1a39634b0c
488
489
489 $ hg logt
490 $ hg logt
490 1:9599899f62c0 a
491 1:9599899f62c0 a
491 0:79b99e9c8e49 b
492 0:79b99e9c8e49 b
492
493
493 Test unix -> windows style variable substitution in external hooks.
494 Test unix -> windows style variable substitution in external hooks.
494
495
495 $ cat > $TESTTMP/tmp.hgrc <<'EOF'
496 $ cat > $TESTTMP/tmp.hgrc <<'EOF'
496 > [hooks]
497 > [hooks]
497 > pre-add = echo no variables
498 > pre-add = echo no variables
498 > post-add = echo ran $HG_ARGS, literal \$non-var, 'also $non-var', $HG_RESULT
499 > post-add = echo ran $HG_ARGS, literal \$non-var, 'also $non-var', $HG_RESULT
499 > tonative.post-add = True
500 > tonative.post-add = True
500 > EOF
501 > EOF
501
502
502 $ echo "foo" > amended.txt
503 $ echo "foo" > amended.txt
503 $ HGRCPATH=$TESTTMP/tmp.hgrc hg add -v amended.txt
504 $ HGRCPATH=$TESTTMP/tmp.hgrc hg add -v amended.txt
504 running hook pre-add: echo no variables
505 running hook pre-add: echo no variables
505 no variables
506 no variables
506 adding amended.txt
507 adding amended.txt
507 converting hook "post-add" to native (windows !)
508 converting hook "post-add" to native (windows !)
508 running hook post-add: echo ran %HG_ARGS%, literal $non-var, "also $non-var", %HG_RESULT% (windows !)
509 running hook post-add: echo ran %HG_ARGS%, literal $non-var, "also $non-var", %HG_RESULT% (windows !)
509 running hook post-add: echo ran $HG_ARGS, literal \$non-var, 'also $non-var', $HG_RESULT (no-windows !)
510 running hook post-add: echo ran $HG_ARGS, literal \$non-var, 'also $non-var', $HG_RESULT (no-windows !)
510 ran add -v amended.txt, literal $non-var, "also $non-var", 0 (windows !)
511 ran add -v amended.txt, literal $non-var, "also $non-var", 0 (windows !)
511 ran add -v amended.txt, literal $non-var, also $non-var, 0 (no-windows !)
512 ran add -v amended.txt, literal $non-var, also $non-var, 0 (no-windows !)
512 $ hg ci -q --config extensions.largefiles= --amend -I amended.txt
513 $ hg ci -q --config extensions.largefiles= --amend -I amended.txt
513 The fsmonitor extension is incompatible with the largefiles extension and has been disabled. (fsmonitor !)
514 The fsmonitor extension is incompatible with the largefiles extension and has been disabled. (fsmonitor !)
514
515
515 Test that folding multiple changes in a row doesn't show multiple
516 Test that folding multiple changes in a row doesn't show multiple
516 editors.
517 editors.
517
518
518 $ echo foo >> foo
519 $ echo foo >> foo
519 $ hg add foo
520 $ hg add foo
520 $ hg ci -m foo1
521 $ hg ci -m foo1
521 $ echo foo >> foo
522 $ echo foo >> foo
522 $ hg ci -m foo2
523 $ hg ci -m foo2
523 $ echo foo >> foo
524 $ echo foo >> foo
524 $ hg ci -m foo3
525 $ hg ci -m foo3
525 $ hg logt
526 $ hg logt
526 4:21679ff7675c foo3
527 4:21679ff7675c foo3
527 3:b7389cc4d66e foo2
528 3:b7389cc4d66e foo2
528 2:0e01aeef5fa8 foo1
529 2:0e01aeef5fa8 foo1
529 1:578c7455730c a
530 1:578c7455730c a
530 0:79b99e9c8e49 b
531 0:79b99e9c8e49 b
531 $ cat > "$TESTTMP/editor.sh" <<EOF
532 $ cat > "$TESTTMP/editor.sh" <<EOF
532 > echo ran editor >> "$TESTTMP/editorlog.txt"
533 > echo ran editor >> "$TESTTMP/editorlog.txt"
533 > cat \$1 >> "$TESTTMP/editorlog.txt"
534 > cat \$1 >> "$TESTTMP/editorlog.txt"
534 > echo END >> "$TESTTMP/editorlog.txt"
535 > echo END >> "$TESTTMP/editorlog.txt"
535 > echo merged foos > \$1
536 > echo merged foos > \$1
536 > EOF
537 > EOF
537 $ HGEDITOR="sh \"$TESTTMP/editor.sh\"" hg histedit 1 --commands - 2>&1 <<EOF | fixbundle
538 $ HGEDITOR="sh \"$TESTTMP/editor.sh\"" hg histedit 1 --commands - 2>&1 <<EOF | fixbundle
538 > pick 578c7455730c 1 a
539 > pick 578c7455730c 1 a
539 > pick 0e01aeef5fa8 2 foo1
540 > pick 0e01aeef5fa8 2 foo1
540 > fold b7389cc4d66e 3 foo2
541 > fold b7389cc4d66e 3 foo2
541 > fold 21679ff7675c 4 foo3
542 > fold 21679ff7675c 4 foo3
542 > EOF
543 > EOF
543 $ hg logt
544 $ hg logt
544 2:e8bedbda72c1 merged foos
545 2:e8bedbda72c1 merged foos
545 1:578c7455730c a
546 1:578c7455730c a
546 0:79b99e9c8e49 b
547 0:79b99e9c8e49 b
547 Editor should have run only once
548 Editor should have run only once
548 $ cat $TESTTMP/editorlog.txt
549 $ cat $TESTTMP/editorlog.txt
549 ran editor
550 ran editor
550 foo1
551 foo1
551 ***
552 ***
552 foo2
553 foo2
553 ***
554 ***
554 foo3
555 foo3
555
556
556
557
557
558
558 HG: Enter commit message. Lines beginning with 'HG:' are removed.
559 HG: Enter commit message. Lines beginning with 'HG:' are removed.
559 HG: Leave message empty to abort commit.
560 HG: Leave message empty to abort commit.
560 HG: --
561 HG: --
561 HG: user: test
562 HG: user: test
562 HG: branch 'default'
563 HG: branch 'default'
563 HG: added foo
564 HG: added foo
564 END
565 END
565
566
566 $ cd ..
567 $ cd ..
567
568
568 Test rolling into a commit with multiple children (issue5498)
569 Test rolling into a commit with multiple children (issue5498)
569
570
570 $ hg init roll
571 $ hg init roll
571 $ cd roll
572 $ cd roll
572 $ echo a > a
573 $ echo a > a
573 $ hg commit -qAm aa
574 $ hg commit -qAm aa
574 $ echo b > b
575 $ echo b > b
575 $ hg commit -qAm bb
576 $ hg commit -qAm bb
576 $ hg up -q ".^"
577 $ hg up -q ".^"
577 $ echo c > c
578 $ echo c > c
578 $ hg commit -qAm cc
579 $ hg commit -qAm cc
579 $ hg log -G -T '{node|short} {desc}'
580 $ hg log -G -T '{node|short} {desc}'
580 @ 5db65b93a12b cc
581 @ 5db65b93a12b cc
581 |
582 |
582 | o 301d76bdc3ae bb
583 | o 301d76bdc3ae bb
583 |/
584 |/
584 o 8f0162e483d0 aa
585 o 8f0162e483d0 aa
585
586
586
587
587 $ hg histedit . --commands - << EOF
588 $ hg histedit . --commands - << EOF
588 > r 5db65b93a12b
589 > r 5db65b93a12b
589 > EOF
590 > EOF
590 hg: parse error: first changeset cannot use verb "roll"
591 hg: parse error: first changeset cannot use verb "roll"
591 [255]
592 [255]
592 $ hg log -G -T '{node|short} {desc}'
593 $ hg log -G -T '{node|short} {desc}'
593 @ 5db65b93a12b cc
594 @ 5db65b93a12b cc
594 |
595 |
595 | o 301d76bdc3ae bb
596 | o 301d76bdc3ae bb
596 |/
597 |/
597 o 8f0162e483d0 aa
598 o 8f0162e483d0 aa
598
599
599
600
601 $ cd ..
602
603 ====================================
604 Test update-timestamp config option|
605 ====================================
606
607 $ addwithdate ()
608 > {
609 > echo $1 > $1
610 > hg add $1
611 > hg ci -m $1 -d "$2 0"
612 > }
613
614 $ initrepo ()
615 > {
616 > hg init r
617 > cd r
618 > addwithdate a 1
619 > addwithdate b 2
620 > addwithdate c 3
621 > addwithdate d 4
622 > addwithdate e 5
623 > addwithdate f 6
624 > }
625
626 $ initrepo
627
628 log before edit
629
630 $ hg log
631 changeset: 5:178e35e0ce73
632 tag: tip
633 user: test
634 date: Thu Jan 01 00:00:06 1970 +0000
635 summary: f
636
637 changeset: 4:1ddb6c90f2ee
638 user: test
639 date: Thu Jan 01 00:00:05 1970 +0000
640 summary: e
641
642 changeset: 3:532247a8969b
643 user: test
644 date: Thu Jan 01 00:00:04 1970 +0000
645 summary: d
646
647 changeset: 2:ff2c9fa2018b
648 user: test
649 date: Thu Jan 01 00:00:03 1970 +0000
650 summary: c
651
652 changeset: 1:97d72e5f12c7
653 user: test
654 date: Thu Jan 01 00:00:02 1970 +0000
655 summary: b
656
657 changeset: 0:8580ff50825a
658 user: test
659 date: Thu Jan 01 00:00:01 1970 +0000
660 summary: a
661
662
663 $ hg histedit 1ddb6c90f2ee --commands - 2>&1 --config rewrite.update-timestamp=True <<EOF | fixbundle
664 > pick 178e35e0ce73 f
665 > fold 1ddb6c90f2ee e
666 > EOF
667
668 log after edit
669 observe time from f is updated
670
671 $ hg log
672 changeset: 4:f7909b1863a2
673 tag: tip
674 user: test
675 date: Thu Jan 01 00:00:01 1970 +0000
676 summary: f
677
678 changeset: 3:532247a8969b
679 user: test
680 date: Thu Jan 01 00:00:04 1970 +0000
681 summary: d
682
683 changeset: 2:ff2c9fa2018b
684 user: test
685 date: Thu Jan 01 00:00:03 1970 +0000
686 summary: c
687
688 changeset: 1:97d72e5f12c7
689 user: test
690 date: Thu Jan 01 00:00:02 1970 +0000
691 summary: b
692
693 changeset: 0:8580ff50825a
694 user: test
695 date: Thu Jan 01 00:00:01 1970 +0000
696 summary: a
697
698 post-fold manifest
699 $ hg manifest
700 a
701 b
702 c
703 d
704 e
705 f
706
707 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now