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