##// END OF EJS Templates
curses: do not initialize LC_ALL to user settings (issue6358)...
Manuel Jacob -
r45539:d2227d4c default
parent child Browse files
Show More
@@ -1,2645 +1,2640
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::
159 The summary of a change can be customized as well::
160
160
161 [histedit]
161 [histedit]
162 summary-template = '{rev} {bookmarks} {desc|firstline}'
162 summary-template = '{rev} {bookmarks} {desc|firstline}'
163
163
164 The customized summary should be kept short enough that rule lines
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
165 will fit in the configured line length. See above if that requires
166 customization.
166 customization.
167
167
168 ``hg histedit`` attempts to automatically choose an appropriate base
168 ``hg histedit`` attempts to automatically choose an appropriate base
169 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
170 revset in your configuration file::
170 revset in your configuration file::
171
171
172 [histedit]
172 [histedit]
173 defaultrev = only(.) & draft()
173 defaultrev = only(.) & draft()
174
174
175 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.
176 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
177 the drop to be implicit for missing commits by adding::
177 the drop to be implicit for missing commits by adding::
178
178
179 [histedit]
179 [histedit]
180 dropmissing = True
180 dropmissing = True
181
181
182 By default, histedit will close the transaction after each action. For
182 By default, histedit will close the transaction after each action. For
183 performance purposes, you can configure histedit to use a single transaction
183 performance purposes, you can configure histedit to use a single transaction
184 across the entire histedit. WARNING: This setting introduces a significant risk
184 across the entire histedit. WARNING: This setting introduces a significant risk
185 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
186 unexpectedly::
186 unexpectedly::
187
187
188 [histedit]
188 [histedit]
189 singletransaction = True
189 singletransaction = True
190
190
191 """
191 """
192
192
193 from __future__ import absolute_import
193 from __future__ import absolute_import
194
194
195 # chistedit dependencies that are not available everywhere
195 # chistedit dependencies that are not available everywhere
196 try:
196 try:
197 import fcntl
197 import fcntl
198 import termios
198 import termios
199 except ImportError:
199 except ImportError:
200 fcntl = None
200 fcntl = None
201 termios = None
201 termios = None
202
202
203 import functools
203 import functools
204 import locale
205 import os
204 import os
206 import struct
205 import struct
207
206
208 from mercurial.i18n import _
207 from mercurial.i18n import _
209 from mercurial.pycompat import (
208 from mercurial.pycompat import (
210 getattr,
209 getattr,
211 open,
210 open,
212 )
211 )
213 from mercurial import (
212 from mercurial import (
214 bundle2,
213 bundle2,
215 cmdutil,
214 cmdutil,
216 context,
215 context,
217 copies,
216 copies,
218 destutil,
217 destutil,
219 discovery,
218 discovery,
220 encoding,
219 encoding,
221 error,
220 error,
222 exchange,
221 exchange,
223 extensions,
222 extensions,
224 hg,
223 hg,
225 logcmdutil,
224 logcmdutil,
226 merge as mergemod,
225 merge as mergemod,
227 mergestate as mergestatemod,
226 mergestate as mergestatemod,
228 mergeutil,
227 mergeutil,
229 node,
228 node,
230 obsolete,
229 obsolete,
231 pycompat,
230 pycompat,
232 registrar,
231 registrar,
233 repair,
232 repair,
234 rewriteutil,
233 rewriteutil,
235 scmutil,
234 scmutil,
236 state as statemod,
235 state as statemod,
237 util,
236 util,
238 )
237 )
239 from mercurial.utils import (
238 from mercurial.utils import (
240 dateutil,
239 dateutil,
241 stringutil,
240 stringutil,
242 )
241 )
243
242
244 pickle = util.pickle
243 pickle = util.pickle
245 cmdtable = {}
244 cmdtable = {}
246 command = registrar.command(cmdtable)
245 command = registrar.command(cmdtable)
247
246
248 configtable = {}
247 configtable = {}
249 configitem = registrar.configitem(configtable)
248 configitem = registrar.configitem(configtable)
250 configitem(
249 configitem(
251 b'experimental', b'histedit.autoverb', default=False,
250 b'experimental', b'histedit.autoverb', default=False,
252 )
251 )
253 configitem(
252 configitem(
254 b'histedit', b'defaultrev', default=None,
253 b'histedit', b'defaultrev', default=None,
255 )
254 )
256 configitem(
255 configitem(
257 b'histedit', b'dropmissing', default=False,
256 b'histedit', b'dropmissing', default=False,
258 )
257 )
259 configitem(
258 configitem(
260 b'histedit', b'linelen', default=80,
259 b'histedit', b'linelen', default=80,
261 )
260 )
262 configitem(
261 configitem(
263 b'histedit', b'singletransaction', default=False,
262 b'histedit', b'singletransaction', default=False,
264 )
263 )
265 configitem(
264 configitem(
266 b'ui', b'interface.histedit', default=None,
265 b'ui', b'interface.histedit', default=None,
267 )
266 )
268 configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}')
267 configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}')
269
268
270 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
269 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
271 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
270 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
272 # be specifying the version(s) of Mercurial they are tested with, or
271 # be specifying the version(s) of Mercurial they are tested with, or
273 # leave the attribute unspecified.
272 # leave the attribute unspecified.
274 testedwith = b'ships-with-hg-core'
273 testedwith = b'ships-with-hg-core'
275
274
276 actiontable = {}
275 actiontable = {}
277 primaryactions = set()
276 primaryactions = set()
278 secondaryactions = set()
277 secondaryactions = set()
279 tertiaryactions = set()
278 tertiaryactions = set()
280 internalactions = set()
279 internalactions = set()
281
280
282
281
283 def geteditcomment(ui, first, last):
282 def geteditcomment(ui, first, last):
284 """ construct the editor comment
283 """ construct the editor comment
285 The comment includes::
284 The comment includes::
286 - an intro
285 - an intro
287 - sorted primary commands
286 - sorted primary commands
288 - sorted short commands
287 - sorted short commands
289 - sorted long commands
288 - sorted long commands
290 - additional hints
289 - additional hints
291
290
292 Commands are only included once.
291 Commands are only included once.
293 """
292 """
294 intro = _(
293 intro = _(
295 b"""Edit history between %s and %s
294 b"""Edit history between %s and %s
296
295
297 Commits are listed from least to most recent
296 Commits are listed from least to most recent
298
297
299 You can reorder changesets by reordering the lines
298 You can reorder changesets by reordering the lines
300
299
301 Commands:
300 Commands:
302 """
301 """
303 )
302 )
304 actions = []
303 actions = []
305
304
306 def addverb(v):
305 def addverb(v):
307 a = actiontable[v]
306 a = actiontable[v]
308 lines = a.message.split(b"\n")
307 lines = a.message.split(b"\n")
309 if len(a.verbs):
308 if len(a.verbs):
310 v = b', '.join(sorted(a.verbs, key=lambda v: len(v)))
309 v = b', '.join(sorted(a.verbs, key=lambda v: len(v)))
311 actions.append(b" %s = %s" % (v, lines[0]))
310 actions.append(b" %s = %s" % (v, lines[0]))
312 actions.extend([b' %s'] * (len(lines) - 1))
311 actions.extend([b' %s'] * (len(lines) - 1))
313
312
314 for v in (
313 for v in (
315 sorted(primaryactions)
314 sorted(primaryactions)
316 + sorted(secondaryactions)
315 + sorted(secondaryactions)
317 + sorted(tertiaryactions)
316 + sorted(tertiaryactions)
318 ):
317 ):
319 addverb(v)
318 addverb(v)
320 actions.append(b'')
319 actions.append(b'')
321
320
322 hints = []
321 hints = []
323 if ui.configbool(b'histedit', b'dropmissing'):
322 if ui.configbool(b'histedit', b'dropmissing'):
324 hints.append(
323 hints.append(
325 b"Deleting a changeset from the list "
324 b"Deleting a changeset from the list "
326 b"will DISCARD it from the edited history!"
325 b"will DISCARD it from the edited history!"
327 )
326 )
328
327
329 lines = (intro % (first, last)).split(b'\n') + actions + hints
328 lines = (intro % (first, last)).split(b'\n') + actions + hints
330
329
331 return b''.join([b'# %s\n' % l if l else b'#\n' for l in lines])
330 return b''.join([b'# %s\n' % l if l else b'#\n' for l in lines])
332
331
333
332
334 class histeditstate(object):
333 class histeditstate(object):
335 def __init__(self, repo):
334 def __init__(self, repo):
336 self.repo = repo
335 self.repo = repo
337 self.actions = None
336 self.actions = None
338 self.keep = None
337 self.keep = None
339 self.topmost = None
338 self.topmost = None
340 self.parentctxnode = None
339 self.parentctxnode = None
341 self.lock = None
340 self.lock = None
342 self.wlock = None
341 self.wlock = None
343 self.backupfile = None
342 self.backupfile = None
344 self.stateobj = statemod.cmdstate(repo, b'histedit-state')
343 self.stateobj = statemod.cmdstate(repo, b'histedit-state')
345 self.replacements = []
344 self.replacements = []
346
345
347 def read(self):
346 def read(self):
348 """Load histedit state from disk and set fields appropriately."""
347 """Load histedit state from disk and set fields appropriately."""
349 if not self.stateobj.exists():
348 if not self.stateobj.exists():
350 cmdutil.wrongtooltocontinue(self.repo, _(b'histedit'))
349 cmdutil.wrongtooltocontinue(self.repo, _(b'histedit'))
351
350
352 data = self._read()
351 data = self._read()
353
352
354 self.parentctxnode = data[b'parentctxnode']
353 self.parentctxnode = data[b'parentctxnode']
355 actions = parserules(data[b'rules'], self)
354 actions = parserules(data[b'rules'], self)
356 self.actions = actions
355 self.actions = actions
357 self.keep = data[b'keep']
356 self.keep = data[b'keep']
358 self.topmost = data[b'topmost']
357 self.topmost = data[b'topmost']
359 self.replacements = data[b'replacements']
358 self.replacements = data[b'replacements']
360 self.backupfile = data[b'backupfile']
359 self.backupfile = data[b'backupfile']
361
360
362 def _read(self):
361 def _read(self):
363 fp = self.repo.vfs.read(b'histedit-state')
362 fp = self.repo.vfs.read(b'histedit-state')
364 if fp.startswith(b'v1\n'):
363 if fp.startswith(b'v1\n'):
365 data = self._load()
364 data = self._load()
366 parentctxnode, rules, keep, topmost, replacements, backupfile = data
365 parentctxnode, rules, keep, topmost, replacements, backupfile = data
367 else:
366 else:
368 data = pickle.loads(fp)
367 data = pickle.loads(fp)
369 parentctxnode, rules, keep, topmost, replacements = data
368 parentctxnode, rules, keep, topmost, replacements = data
370 backupfile = None
369 backupfile = None
371 rules = b"\n".join([b"%s %s" % (verb, rest) for [verb, rest] in rules])
370 rules = b"\n".join([b"%s %s" % (verb, rest) for [verb, rest] in rules])
372
371
373 return {
372 return {
374 b'parentctxnode': parentctxnode,
373 b'parentctxnode': parentctxnode,
375 b"rules": rules,
374 b"rules": rules,
376 b"keep": keep,
375 b"keep": keep,
377 b"topmost": topmost,
376 b"topmost": topmost,
378 b"replacements": replacements,
377 b"replacements": replacements,
379 b"backupfile": backupfile,
378 b"backupfile": backupfile,
380 }
379 }
381
380
382 def write(self, tr=None):
381 def write(self, tr=None):
383 if tr:
382 if tr:
384 tr.addfilegenerator(
383 tr.addfilegenerator(
385 b'histedit-state',
384 b'histedit-state',
386 (b'histedit-state',),
385 (b'histedit-state',),
387 self._write,
386 self._write,
388 location=b'plain',
387 location=b'plain',
389 )
388 )
390 else:
389 else:
391 with self.repo.vfs(b"histedit-state", b"w") as f:
390 with self.repo.vfs(b"histedit-state", b"w") as f:
392 self._write(f)
391 self._write(f)
393
392
394 def _write(self, fp):
393 def _write(self, fp):
395 fp.write(b'v1\n')
394 fp.write(b'v1\n')
396 fp.write(b'%s\n' % node.hex(self.parentctxnode))
395 fp.write(b'%s\n' % node.hex(self.parentctxnode))
397 fp.write(b'%s\n' % node.hex(self.topmost))
396 fp.write(b'%s\n' % node.hex(self.topmost))
398 fp.write(b'%s\n' % (b'True' if self.keep else b'False'))
397 fp.write(b'%s\n' % (b'True' if self.keep else b'False'))
399 fp.write(b'%d\n' % len(self.actions))
398 fp.write(b'%d\n' % len(self.actions))
400 for action in self.actions:
399 for action in self.actions:
401 fp.write(b'%s\n' % action.tostate())
400 fp.write(b'%s\n' % action.tostate())
402 fp.write(b'%d\n' % len(self.replacements))
401 fp.write(b'%d\n' % len(self.replacements))
403 for replacement in self.replacements:
402 for replacement in self.replacements:
404 fp.write(
403 fp.write(
405 b'%s%s\n'
404 b'%s%s\n'
406 % (
405 % (
407 node.hex(replacement[0]),
406 node.hex(replacement[0]),
408 b''.join(node.hex(r) for r in replacement[1]),
407 b''.join(node.hex(r) for r in replacement[1]),
409 )
408 )
410 )
409 )
411 backupfile = self.backupfile
410 backupfile = self.backupfile
412 if not backupfile:
411 if not backupfile:
413 backupfile = b''
412 backupfile = b''
414 fp.write(b'%s\n' % backupfile)
413 fp.write(b'%s\n' % backupfile)
415
414
416 def _load(self):
415 def _load(self):
417 fp = self.repo.vfs(b'histedit-state', b'r')
416 fp = self.repo.vfs(b'histedit-state', b'r')
418 lines = [l[:-1] for l in fp.readlines()]
417 lines = [l[:-1] for l in fp.readlines()]
419
418
420 index = 0
419 index = 0
421 lines[index] # version number
420 lines[index] # version number
422 index += 1
421 index += 1
423
422
424 parentctxnode = node.bin(lines[index])
423 parentctxnode = node.bin(lines[index])
425 index += 1
424 index += 1
426
425
427 topmost = node.bin(lines[index])
426 topmost = node.bin(lines[index])
428 index += 1
427 index += 1
429
428
430 keep = lines[index] == b'True'
429 keep = lines[index] == b'True'
431 index += 1
430 index += 1
432
431
433 # Rules
432 # Rules
434 rules = []
433 rules = []
435 rulelen = int(lines[index])
434 rulelen = int(lines[index])
436 index += 1
435 index += 1
437 for i in pycompat.xrange(rulelen):
436 for i in pycompat.xrange(rulelen):
438 ruleaction = lines[index]
437 ruleaction = lines[index]
439 index += 1
438 index += 1
440 rule = lines[index]
439 rule = lines[index]
441 index += 1
440 index += 1
442 rules.append((ruleaction, rule))
441 rules.append((ruleaction, rule))
443
442
444 # Replacements
443 # Replacements
445 replacements = []
444 replacements = []
446 replacementlen = int(lines[index])
445 replacementlen = int(lines[index])
447 index += 1
446 index += 1
448 for i in pycompat.xrange(replacementlen):
447 for i in pycompat.xrange(replacementlen):
449 replacement = lines[index]
448 replacement = lines[index]
450 original = node.bin(replacement[:40])
449 original = node.bin(replacement[:40])
451 succ = [
450 succ = [
452 node.bin(replacement[i : i + 40])
451 node.bin(replacement[i : i + 40])
453 for i in range(40, len(replacement), 40)
452 for i in range(40, len(replacement), 40)
454 ]
453 ]
455 replacements.append((original, succ))
454 replacements.append((original, succ))
456 index += 1
455 index += 1
457
456
458 backupfile = lines[index]
457 backupfile = lines[index]
459 index += 1
458 index += 1
460
459
461 fp.close()
460 fp.close()
462
461
463 return parentctxnode, rules, keep, topmost, replacements, backupfile
462 return parentctxnode, rules, keep, topmost, replacements, backupfile
464
463
465 def clear(self):
464 def clear(self):
466 if self.inprogress():
465 if self.inprogress():
467 self.repo.vfs.unlink(b'histedit-state')
466 self.repo.vfs.unlink(b'histedit-state')
468
467
469 def inprogress(self):
468 def inprogress(self):
470 return self.repo.vfs.exists(b'histedit-state')
469 return self.repo.vfs.exists(b'histedit-state')
471
470
472
471
473 class histeditaction(object):
472 class histeditaction(object):
474 def __init__(self, state, node):
473 def __init__(self, state, node):
475 self.state = state
474 self.state = state
476 self.repo = state.repo
475 self.repo = state.repo
477 self.node = node
476 self.node = node
478
477
479 @classmethod
478 @classmethod
480 def fromrule(cls, state, rule):
479 def fromrule(cls, state, rule):
481 """Parses the given rule, returning an instance of the histeditaction.
480 """Parses the given rule, returning an instance of the histeditaction.
482 """
481 """
483 ruleid = rule.strip().split(b' ', 1)[0]
482 ruleid = rule.strip().split(b' ', 1)[0]
484 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
483 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
485 # Check for validation of rule ids and get the rulehash
484 # Check for validation of rule ids and get the rulehash
486 try:
485 try:
487 rev = node.bin(ruleid)
486 rev = node.bin(ruleid)
488 except TypeError:
487 except TypeError:
489 try:
488 try:
490 _ctx = scmutil.revsingle(state.repo, ruleid)
489 _ctx = scmutil.revsingle(state.repo, ruleid)
491 rulehash = _ctx.hex()
490 rulehash = _ctx.hex()
492 rev = node.bin(rulehash)
491 rev = node.bin(rulehash)
493 except error.RepoLookupError:
492 except error.RepoLookupError:
494 raise error.ParseError(_(b"invalid changeset %s") % ruleid)
493 raise error.ParseError(_(b"invalid changeset %s") % ruleid)
495 return cls(state, rev)
494 return cls(state, rev)
496
495
497 def verify(self, prev, expected, seen):
496 def verify(self, prev, expected, seen):
498 """ Verifies semantic correctness of the rule"""
497 """ Verifies semantic correctness of the rule"""
499 repo = self.repo
498 repo = self.repo
500 ha = node.hex(self.node)
499 ha = node.hex(self.node)
501 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
500 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
502 if self.node is None:
501 if self.node is None:
503 raise error.ParseError(_(b'unknown changeset %s listed') % ha[:12])
502 raise error.ParseError(_(b'unknown changeset %s listed') % ha[:12])
504 self._verifynodeconstraints(prev, expected, seen)
503 self._verifynodeconstraints(prev, expected, seen)
505
504
506 def _verifynodeconstraints(self, prev, expected, seen):
505 def _verifynodeconstraints(self, prev, expected, seen):
507 # by default command need a node in the edited list
506 # by default command need a node in the edited list
508 if self.node not in expected:
507 if self.node not in expected:
509 raise error.ParseError(
508 raise error.ParseError(
510 _(b'%s "%s" changeset was not a candidate')
509 _(b'%s "%s" changeset was not a candidate')
511 % (self.verb, node.short(self.node)),
510 % (self.verb, node.short(self.node)),
512 hint=_(b'only use listed changesets'),
511 hint=_(b'only use listed changesets'),
513 )
512 )
514 # and only one command per node
513 # and only one command per node
515 if self.node in seen:
514 if self.node in seen:
516 raise error.ParseError(
515 raise error.ParseError(
517 _(b'duplicated command for changeset %s')
516 _(b'duplicated command for changeset %s')
518 % node.short(self.node)
517 % node.short(self.node)
519 )
518 )
520
519
521 def torule(self):
520 def torule(self):
522 """build a histedit rule line for an action
521 """build a histedit rule line for an action
523
522
524 by default lines are in the form:
523 by default lines are in the form:
525 <hash> <rev> <summary>
524 <hash> <rev> <summary>
526 """
525 """
527 ctx = self.repo[self.node]
526 ctx = self.repo[self.node]
528 ui = self.repo.ui
527 ui = self.repo.ui
529 summary = (
528 summary = (
530 cmdutil.rendertemplate(
529 cmdutil.rendertemplate(
531 ctx, ui.config(b'histedit', b'summary-template')
530 ctx, ui.config(b'histedit', b'summary-template')
532 )
531 )
533 or b''
532 or b''
534 )
533 )
535 summary = summary.splitlines()[0]
534 summary = summary.splitlines()[0]
536 line = b'%s %s %s' % (self.verb, ctx, summary)
535 line = b'%s %s %s' % (self.verb, ctx, summary)
537 # trim to 75 columns by default so it's not stupidly wide in my editor
536 # trim to 75 columns by default so it's not stupidly wide in my editor
538 # (the 5 more are left for verb)
537 # (the 5 more are left for verb)
539 maxlen = self.repo.ui.configint(b'histedit', b'linelen')
538 maxlen = self.repo.ui.configint(b'histedit', b'linelen')
540 maxlen = max(maxlen, 22) # avoid truncating hash
539 maxlen = max(maxlen, 22) # avoid truncating hash
541 return stringutil.ellipsis(line, maxlen)
540 return stringutil.ellipsis(line, maxlen)
542
541
543 def tostate(self):
542 def tostate(self):
544 """Print an action in format used by histedit state files
543 """Print an action in format used by histedit state files
545 (the first line is a verb, the remainder is the second)
544 (the first line is a verb, the remainder is the second)
546 """
545 """
547 return b"%s\n%s" % (self.verb, node.hex(self.node))
546 return b"%s\n%s" % (self.verb, node.hex(self.node))
548
547
549 def run(self):
548 def run(self):
550 """Runs the action. The default behavior is simply apply the action's
549 """Runs the action. The default behavior is simply apply the action's
551 rulectx onto the current parentctx."""
550 rulectx onto the current parentctx."""
552 self.applychange()
551 self.applychange()
553 self.continuedirty()
552 self.continuedirty()
554 return self.continueclean()
553 return self.continueclean()
555
554
556 def applychange(self):
555 def applychange(self):
557 """Applies the changes from this action's rulectx onto the current
556 """Applies the changes from this action's rulectx onto the current
558 parentctx, but does not commit them."""
557 parentctx, but does not commit them."""
559 repo = self.repo
558 repo = self.repo
560 rulectx = repo[self.node]
559 rulectx = repo[self.node]
561 repo.ui.pushbuffer(error=True, labeled=True)
560 repo.ui.pushbuffer(error=True, labeled=True)
562 hg.update(repo, self.state.parentctxnode, quietempty=True)
561 hg.update(repo, self.state.parentctxnode, quietempty=True)
563 repo.ui.popbuffer()
562 repo.ui.popbuffer()
564 stats = applychanges(repo.ui, repo, rulectx, {})
563 stats = applychanges(repo.ui, repo, rulectx, {})
565 repo.dirstate.setbranch(rulectx.branch())
564 repo.dirstate.setbranch(rulectx.branch())
566 if stats.unresolvedcount:
565 if stats.unresolvedcount:
567 raise error.InterventionRequired(
566 raise error.InterventionRequired(
568 _(b'Fix up the change (%s %s)')
567 _(b'Fix up the change (%s %s)')
569 % (self.verb, node.short(self.node)),
568 % (self.verb, node.short(self.node)),
570 hint=_(b'hg histedit --continue to resume'),
569 hint=_(b'hg histedit --continue to resume'),
571 )
570 )
572
571
573 def continuedirty(self):
572 def continuedirty(self):
574 """Continues the action when changes have been applied to the working
573 """Continues the action when changes have been applied to the working
575 copy. The default behavior is to commit the dirty changes."""
574 copy. The default behavior is to commit the dirty changes."""
576 repo = self.repo
575 repo = self.repo
577 rulectx = repo[self.node]
576 rulectx = repo[self.node]
578
577
579 editor = self.commiteditor()
578 editor = self.commiteditor()
580 commit = commitfuncfor(repo, rulectx)
579 commit = commitfuncfor(repo, rulectx)
581 if repo.ui.configbool(b'rewrite', b'update-timestamp'):
580 if repo.ui.configbool(b'rewrite', b'update-timestamp'):
582 date = dateutil.makedate()
581 date = dateutil.makedate()
583 else:
582 else:
584 date = rulectx.date()
583 date = rulectx.date()
585 commit(
584 commit(
586 text=rulectx.description(),
585 text=rulectx.description(),
587 user=rulectx.user(),
586 user=rulectx.user(),
588 date=date,
587 date=date,
589 extra=rulectx.extra(),
588 extra=rulectx.extra(),
590 editor=editor,
589 editor=editor,
591 )
590 )
592
591
593 def commiteditor(self):
592 def commiteditor(self):
594 """The editor to be used to edit the commit message."""
593 """The editor to be used to edit the commit message."""
595 return False
594 return False
596
595
597 def continueclean(self):
596 def continueclean(self):
598 """Continues the action when the working copy is clean. The default
597 """Continues the action when the working copy is clean. The default
599 behavior is to accept the current commit as the new version of the
598 behavior is to accept the current commit as the new version of the
600 rulectx."""
599 rulectx."""
601 ctx = self.repo[b'.']
600 ctx = self.repo[b'.']
602 if ctx.node() == self.state.parentctxnode:
601 if ctx.node() == self.state.parentctxnode:
603 self.repo.ui.warn(
602 self.repo.ui.warn(
604 _(b'%s: skipping changeset (no changes)\n')
603 _(b'%s: skipping changeset (no changes)\n')
605 % node.short(self.node)
604 % node.short(self.node)
606 )
605 )
607 return ctx, [(self.node, tuple())]
606 return ctx, [(self.node, tuple())]
608 if ctx.node() == self.node:
607 if ctx.node() == self.node:
609 # Nothing changed
608 # Nothing changed
610 return ctx, []
609 return ctx, []
611 return ctx, [(self.node, (ctx.node(),))]
610 return ctx, [(self.node, (ctx.node(),))]
612
611
613
612
614 def commitfuncfor(repo, src):
613 def commitfuncfor(repo, src):
615 """Build a commit function for the replacement of <src>
614 """Build a commit function for the replacement of <src>
616
615
617 This function ensure we apply the same treatment to all changesets.
616 This function ensure we apply the same treatment to all changesets.
618
617
619 - Add a 'histedit_source' entry in extra.
618 - Add a 'histedit_source' entry in extra.
620
619
621 Note that fold has its own separated logic because its handling is a bit
620 Note that fold has its own separated logic because its handling is a bit
622 different and not easily factored out of the fold method.
621 different and not easily factored out of the fold method.
623 """
622 """
624 phasemin = src.phase()
623 phasemin = src.phase()
625
624
626 def commitfunc(**kwargs):
625 def commitfunc(**kwargs):
627 overrides = {(b'phases', b'new-commit'): phasemin}
626 overrides = {(b'phases', b'new-commit'): phasemin}
628 with repo.ui.configoverride(overrides, b'histedit'):
627 with repo.ui.configoverride(overrides, b'histedit'):
629 extra = kwargs.get('extra', {}).copy()
628 extra = kwargs.get('extra', {}).copy()
630 extra[b'histedit_source'] = src.hex()
629 extra[b'histedit_source'] = src.hex()
631 kwargs['extra'] = extra
630 kwargs['extra'] = extra
632 return repo.commit(**kwargs)
631 return repo.commit(**kwargs)
633
632
634 return commitfunc
633 return commitfunc
635
634
636
635
637 def applychanges(ui, repo, ctx, opts):
636 def applychanges(ui, repo, ctx, opts):
638 """Merge changeset from ctx (only) in the current working directory"""
637 """Merge changeset from ctx (only) in the current working directory"""
639 wcpar = repo.dirstate.p1()
638 wcpar = repo.dirstate.p1()
640 if ctx.p1().node() == wcpar:
639 if ctx.p1().node() == wcpar:
641 # edits are "in place" we do not need to make any merge,
640 # edits are "in place" we do not need to make any merge,
642 # just applies changes on parent for editing
641 # just applies changes on parent for editing
643 ui.pushbuffer()
642 ui.pushbuffer()
644 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
643 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
645 stats = mergemod.updateresult(0, 0, 0, 0)
644 stats = mergemod.updateresult(0, 0, 0, 0)
646 ui.popbuffer()
645 ui.popbuffer()
647 else:
646 else:
648 try:
647 try:
649 # ui.forcemerge is an internal variable, do not document
648 # ui.forcemerge is an internal variable, do not document
650 repo.ui.setconfig(
649 repo.ui.setconfig(
651 b'ui', b'forcemerge', opts.get(b'tool', b''), b'histedit'
650 b'ui', b'forcemerge', opts.get(b'tool', b''), b'histedit'
652 )
651 )
653 stats = mergemod.graft(repo, ctx, labels=[b'local', b'histedit'])
652 stats = mergemod.graft(repo, ctx, labels=[b'local', b'histedit'])
654 finally:
653 finally:
655 repo.ui.setconfig(b'ui', b'forcemerge', b'', b'histedit')
654 repo.ui.setconfig(b'ui', b'forcemerge', b'', b'histedit')
656 return stats
655 return stats
657
656
658
657
659 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
658 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
660 """collapse the set of revisions from first to last as new one.
659 """collapse the set of revisions from first to last as new one.
661
660
662 Expected commit options are:
661 Expected commit options are:
663 - message
662 - message
664 - date
663 - date
665 - username
664 - username
666 Commit message is edited in all cases.
665 Commit message is edited in all cases.
667
666
668 This function works in memory."""
667 This function works in memory."""
669 ctxs = list(repo.set(b'%d::%d', firstctx.rev(), lastctx.rev()))
668 ctxs = list(repo.set(b'%d::%d', firstctx.rev(), lastctx.rev()))
670 if not ctxs:
669 if not ctxs:
671 return None
670 return None
672 for c in ctxs:
671 for c in ctxs:
673 if not c.mutable():
672 if not c.mutable():
674 raise error.ParseError(
673 raise error.ParseError(
675 _(b"cannot fold into public change %s") % node.short(c.node())
674 _(b"cannot fold into public change %s") % node.short(c.node())
676 )
675 )
677 base = firstctx.p1()
676 base = firstctx.p1()
678
677
679 # commit a new version of the old changeset, including the update
678 # commit a new version of the old changeset, including the update
680 # collect all files which might be affected
679 # collect all files which might be affected
681 files = set()
680 files = set()
682 for ctx in ctxs:
681 for ctx in ctxs:
683 files.update(ctx.files())
682 files.update(ctx.files())
684
683
685 # Recompute copies (avoid recording a -> b -> a)
684 # Recompute copies (avoid recording a -> b -> a)
686 copied = copies.pathcopies(base, lastctx)
685 copied = copies.pathcopies(base, lastctx)
687
686
688 # prune files which were reverted by the updates
687 # prune files which were reverted by the updates
689 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
688 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
690 # commit version of these files as defined by head
689 # commit version of these files as defined by head
691 headmf = lastctx.manifest()
690 headmf = lastctx.manifest()
692
691
693 def filectxfn(repo, ctx, path):
692 def filectxfn(repo, ctx, path):
694 if path in headmf:
693 if path in headmf:
695 fctx = lastctx[path]
694 fctx = lastctx[path]
696 flags = fctx.flags()
695 flags = fctx.flags()
697 mctx = context.memfilectx(
696 mctx = context.memfilectx(
698 repo,
697 repo,
699 ctx,
698 ctx,
700 fctx.path(),
699 fctx.path(),
701 fctx.data(),
700 fctx.data(),
702 islink=b'l' in flags,
701 islink=b'l' in flags,
703 isexec=b'x' in flags,
702 isexec=b'x' in flags,
704 copysource=copied.get(path),
703 copysource=copied.get(path),
705 )
704 )
706 return mctx
705 return mctx
707 return None
706 return None
708
707
709 if commitopts.get(b'message'):
708 if commitopts.get(b'message'):
710 message = commitopts[b'message']
709 message = commitopts[b'message']
711 else:
710 else:
712 message = firstctx.description()
711 message = firstctx.description()
713 user = commitopts.get(b'user')
712 user = commitopts.get(b'user')
714 date = commitopts.get(b'date')
713 date = commitopts.get(b'date')
715 extra = commitopts.get(b'extra')
714 extra = commitopts.get(b'extra')
716
715
717 parents = (firstctx.p1().node(), firstctx.p2().node())
716 parents = (firstctx.p1().node(), firstctx.p2().node())
718 editor = None
717 editor = None
719 if not skipprompt:
718 if not skipprompt:
720 editor = cmdutil.getcommiteditor(edit=True, editform=b'histedit.fold')
719 editor = cmdutil.getcommiteditor(edit=True, editform=b'histedit.fold')
721 new = context.memctx(
720 new = context.memctx(
722 repo,
721 repo,
723 parents=parents,
722 parents=parents,
724 text=message,
723 text=message,
725 files=files,
724 files=files,
726 filectxfn=filectxfn,
725 filectxfn=filectxfn,
727 user=user,
726 user=user,
728 date=date,
727 date=date,
729 extra=extra,
728 extra=extra,
730 editor=editor,
729 editor=editor,
731 )
730 )
732 return repo.commitctx(new)
731 return repo.commitctx(new)
733
732
734
733
735 def _isdirtywc(repo):
734 def _isdirtywc(repo):
736 return repo[None].dirty(missing=True)
735 return repo[None].dirty(missing=True)
737
736
738
737
739 def abortdirty():
738 def abortdirty():
740 raise error.Abort(
739 raise error.Abort(
741 _(b'working copy has pending changes'),
740 _(b'working copy has pending changes'),
742 hint=_(
741 hint=_(
743 b'amend, commit, or revert them and run histedit '
742 b'amend, commit, or revert them and run histedit '
744 b'--continue, or abort with histedit --abort'
743 b'--continue, or abort with histedit --abort'
745 ),
744 ),
746 )
745 )
747
746
748
747
749 def action(verbs, message, priority=False, internal=False):
748 def action(verbs, message, priority=False, internal=False):
750 def wrap(cls):
749 def wrap(cls):
751 assert not priority or not internal
750 assert not priority or not internal
752 verb = verbs[0]
751 verb = verbs[0]
753 if priority:
752 if priority:
754 primaryactions.add(verb)
753 primaryactions.add(verb)
755 elif internal:
754 elif internal:
756 internalactions.add(verb)
755 internalactions.add(verb)
757 elif len(verbs) > 1:
756 elif len(verbs) > 1:
758 secondaryactions.add(verb)
757 secondaryactions.add(verb)
759 else:
758 else:
760 tertiaryactions.add(verb)
759 tertiaryactions.add(verb)
761
760
762 cls.verb = verb
761 cls.verb = verb
763 cls.verbs = verbs
762 cls.verbs = verbs
764 cls.message = message
763 cls.message = message
765 for verb in verbs:
764 for verb in verbs:
766 actiontable[verb] = cls
765 actiontable[verb] = cls
767 return cls
766 return cls
768
767
769 return wrap
768 return wrap
770
769
771
770
772 @action([b'pick', b'p'], _(b'use commit'), priority=True)
771 @action([b'pick', b'p'], _(b'use commit'), priority=True)
773 class pick(histeditaction):
772 class pick(histeditaction):
774 def run(self):
773 def run(self):
775 rulectx = self.repo[self.node]
774 rulectx = self.repo[self.node]
776 if rulectx.p1().node() == self.state.parentctxnode:
775 if rulectx.p1().node() == self.state.parentctxnode:
777 self.repo.ui.debug(b'node %s unchanged\n' % node.short(self.node))
776 self.repo.ui.debug(b'node %s unchanged\n' % node.short(self.node))
778 return rulectx, []
777 return rulectx, []
779
778
780 return super(pick, self).run()
779 return super(pick, self).run()
781
780
782
781
783 @action([b'edit', b'e'], _(b'use commit, but stop for amending'), priority=True)
782 @action([b'edit', b'e'], _(b'use commit, but stop for amending'), priority=True)
784 class edit(histeditaction):
783 class edit(histeditaction):
785 def run(self):
784 def run(self):
786 repo = self.repo
785 repo = self.repo
787 rulectx = repo[self.node]
786 rulectx = repo[self.node]
788 hg.update(repo, self.state.parentctxnode, quietempty=True)
787 hg.update(repo, self.state.parentctxnode, quietempty=True)
789 applychanges(repo.ui, repo, rulectx, {})
788 applychanges(repo.ui, repo, rulectx, {})
790 raise error.InterventionRequired(
789 raise error.InterventionRequired(
791 _(b'Editing (%s), you may commit or record as needed now.')
790 _(b'Editing (%s), you may commit or record as needed now.')
792 % node.short(self.node),
791 % node.short(self.node),
793 hint=_(b'hg histedit --continue to resume'),
792 hint=_(b'hg histedit --continue to resume'),
794 )
793 )
795
794
796 def commiteditor(self):
795 def commiteditor(self):
797 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.edit')
796 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.edit')
798
797
799
798
800 @action([b'fold', b'f'], _(b'use commit, but combine it with the one above'))
799 @action([b'fold', b'f'], _(b'use commit, but combine it with the one above'))
801 class fold(histeditaction):
800 class fold(histeditaction):
802 def verify(self, prev, expected, seen):
801 def verify(self, prev, expected, seen):
803 """ Verifies semantic correctness of the fold rule"""
802 """ Verifies semantic correctness of the fold rule"""
804 super(fold, self).verify(prev, expected, seen)
803 super(fold, self).verify(prev, expected, seen)
805 repo = self.repo
804 repo = self.repo
806 if not prev:
805 if not prev:
807 c = repo[self.node].p1()
806 c = repo[self.node].p1()
808 elif not prev.verb in (b'pick', b'base'):
807 elif not prev.verb in (b'pick', b'base'):
809 return
808 return
810 else:
809 else:
811 c = repo[prev.node]
810 c = repo[prev.node]
812 if not c.mutable():
811 if not c.mutable():
813 raise error.ParseError(
812 raise error.ParseError(
814 _(b"cannot fold into public change %s") % node.short(c.node())
813 _(b"cannot fold into public change %s") % node.short(c.node())
815 )
814 )
816
815
817 def continuedirty(self):
816 def continuedirty(self):
818 repo = self.repo
817 repo = self.repo
819 rulectx = repo[self.node]
818 rulectx = repo[self.node]
820
819
821 commit = commitfuncfor(repo, rulectx)
820 commit = commitfuncfor(repo, rulectx)
822 commit(
821 commit(
823 text=b'fold-temp-revision %s' % node.short(self.node),
822 text=b'fold-temp-revision %s' % node.short(self.node),
824 user=rulectx.user(),
823 user=rulectx.user(),
825 date=rulectx.date(),
824 date=rulectx.date(),
826 extra=rulectx.extra(),
825 extra=rulectx.extra(),
827 )
826 )
828
827
829 def continueclean(self):
828 def continueclean(self):
830 repo = self.repo
829 repo = self.repo
831 ctx = repo[b'.']
830 ctx = repo[b'.']
832 rulectx = repo[self.node]
831 rulectx = repo[self.node]
833 parentctxnode = self.state.parentctxnode
832 parentctxnode = self.state.parentctxnode
834 if ctx.node() == parentctxnode:
833 if ctx.node() == parentctxnode:
835 repo.ui.warn(_(b'%s: empty changeset\n') % node.short(self.node))
834 repo.ui.warn(_(b'%s: empty changeset\n') % node.short(self.node))
836 return ctx, [(self.node, (parentctxnode,))]
835 return ctx, [(self.node, (parentctxnode,))]
837
836
838 parentctx = repo[parentctxnode]
837 parentctx = repo[parentctxnode]
839 newcommits = {
838 newcommits = {
840 c.node()
839 c.node()
841 for c in repo.set(b'(%d::. - %d)', parentctx.rev(), parentctx.rev())
840 for c in repo.set(b'(%d::. - %d)', parentctx.rev(), parentctx.rev())
842 }
841 }
843 if not newcommits:
842 if not newcommits:
844 repo.ui.warn(
843 repo.ui.warn(
845 _(
844 _(
846 b'%s: cannot fold - working copy is not a '
845 b'%s: cannot fold - working copy is not a '
847 b'descendant of previous commit %s\n'
846 b'descendant of previous commit %s\n'
848 )
847 )
849 % (node.short(self.node), node.short(parentctxnode))
848 % (node.short(self.node), node.short(parentctxnode))
850 )
849 )
851 return ctx, [(self.node, (ctx.node(),))]
850 return ctx, [(self.node, (ctx.node(),))]
852
851
853 middlecommits = newcommits.copy()
852 middlecommits = newcommits.copy()
854 middlecommits.discard(ctx.node())
853 middlecommits.discard(ctx.node())
855
854
856 return self.finishfold(
855 return self.finishfold(
857 repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
856 repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
858 )
857 )
859
858
860 def skipprompt(self):
859 def skipprompt(self):
861 """Returns true if the rule should skip the message editor.
860 """Returns true if the rule should skip the message editor.
862
861
863 For example, 'fold' wants to show an editor, but 'rollup'
862 For example, 'fold' wants to show an editor, but 'rollup'
864 doesn't want to.
863 doesn't want to.
865 """
864 """
866 return False
865 return False
867
866
868 def mergedescs(self):
867 def mergedescs(self):
869 """Returns true if the rule should merge messages of multiple changes.
868 """Returns true if the rule should merge messages of multiple changes.
870
869
871 This exists mainly so that 'rollup' rules can be a subclass of
870 This exists mainly so that 'rollup' rules can be a subclass of
872 'fold'.
871 'fold'.
873 """
872 """
874 return True
873 return True
875
874
876 def firstdate(self):
875 def firstdate(self):
877 """Returns true if the rule should preserve the date of the first
876 """Returns true if the rule should preserve the date of the first
878 change.
877 change.
879
878
880 This exists mainly so that 'rollup' rules can be a subclass of
879 This exists mainly so that 'rollup' rules can be a subclass of
881 'fold'.
880 'fold'.
882 """
881 """
883 return False
882 return False
884
883
885 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
884 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
886 parent = ctx.p1().node()
885 parent = ctx.p1().node()
887 hg.updaterepo(repo, parent, overwrite=False)
886 hg.updaterepo(repo, parent, overwrite=False)
888 ### prepare new commit data
887 ### prepare new commit data
889 commitopts = {}
888 commitopts = {}
890 commitopts[b'user'] = ctx.user()
889 commitopts[b'user'] = ctx.user()
891 # commit message
890 # commit message
892 if not self.mergedescs():
891 if not self.mergedescs():
893 newmessage = ctx.description()
892 newmessage = ctx.description()
894 else:
893 else:
895 newmessage = (
894 newmessage = (
896 b'\n***\n'.join(
895 b'\n***\n'.join(
897 [ctx.description()]
896 [ctx.description()]
898 + [repo[r].description() for r in internalchanges]
897 + [repo[r].description() for r in internalchanges]
899 + [oldctx.description()]
898 + [oldctx.description()]
900 )
899 )
901 + b'\n'
900 + b'\n'
902 )
901 )
903 commitopts[b'message'] = newmessage
902 commitopts[b'message'] = newmessage
904 # date
903 # date
905 if self.firstdate():
904 if self.firstdate():
906 commitopts[b'date'] = ctx.date()
905 commitopts[b'date'] = ctx.date()
907 else:
906 else:
908 commitopts[b'date'] = max(ctx.date(), oldctx.date())
907 commitopts[b'date'] = max(ctx.date(), oldctx.date())
909 # if date is to be updated to current
908 # if date is to be updated to current
910 if ui.configbool(b'rewrite', b'update-timestamp'):
909 if ui.configbool(b'rewrite', b'update-timestamp'):
911 commitopts[b'date'] = dateutil.makedate()
910 commitopts[b'date'] = dateutil.makedate()
912
911
913 extra = ctx.extra().copy()
912 extra = ctx.extra().copy()
914 # histedit_source
913 # histedit_source
915 # note: ctx is likely a temporary commit but that the best we can do
914 # note: ctx is likely a temporary commit but that the best we can do
916 # here. This is sufficient to solve issue3681 anyway.
915 # here. This is sufficient to solve issue3681 anyway.
917 extra[b'histedit_source'] = b'%s,%s' % (ctx.hex(), oldctx.hex())
916 extra[b'histedit_source'] = b'%s,%s' % (ctx.hex(), oldctx.hex())
918 commitopts[b'extra'] = extra
917 commitopts[b'extra'] = extra
919 phasemin = max(ctx.phase(), oldctx.phase())
918 phasemin = max(ctx.phase(), oldctx.phase())
920 overrides = {(b'phases', b'new-commit'): phasemin}
919 overrides = {(b'phases', b'new-commit'): phasemin}
921 with repo.ui.configoverride(overrides, b'histedit'):
920 with repo.ui.configoverride(overrides, b'histedit'):
922 n = collapse(
921 n = collapse(
923 repo,
922 repo,
924 ctx,
923 ctx,
925 repo[newnode],
924 repo[newnode],
926 commitopts,
925 commitopts,
927 skipprompt=self.skipprompt(),
926 skipprompt=self.skipprompt(),
928 )
927 )
929 if n is None:
928 if n is None:
930 return ctx, []
929 return ctx, []
931 hg.updaterepo(repo, n, overwrite=False)
930 hg.updaterepo(repo, n, overwrite=False)
932 replacements = [
931 replacements = [
933 (oldctx.node(), (newnode,)),
932 (oldctx.node(), (newnode,)),
934 (ctx.node(), (n,)),
933 (ctx.node(), (n,)),
935 (newnode, (n,)),
934 (newnode, (n,)),
936 ]
935 ]
937 for ich in internalchanges:
936 for ich in internalchanges:
938 replacements.append((ich, (n,)))
937 replacements.append((ich, (n,)))
939 return repo[n], replacements
938 return repo[n], replacements
940
939
941
940
942 @action(
941 @action(
943 [b'base', b'b'],
942 [b'base', b'b'],
944 _(b'checkout changeset and apply further changesets from there'),
943 _(b'checkout changeset and apply further changesets from there'),
945 )
944 )
946 class base(histeditaction):
945 class base(histeditaction):
947 def run(self):
946 def run(self):
948 if self.repo[b'.'].node() != self.node:
947 if self.repo[b'.'].node() != self.node:
949 mergemod.clean_update(self.repo[self.node])
948 mergemod.clean_update(self.repo[self.node])
950 return self.continueclean()
949 return self.continueclean()
951
950
952 def continuedirty(self):
951 def continuedirty(self):
953 abortdirty()
952 abortdirty()
954
953
955 def continueclean(self):
954 def continueclean(self):
956 basectx = self.repo[b'.']
955 basectx = self.repo[b'.']
957 return basectx, []
956 return basectx, []
958
957
959 def _verifynodeconstraints(self, prev, expected, seen):
958 def _verifynodeconstraints(self, prev, expected, seen):
960 # base can only be use with a node not in the edited set
959 # base can only be use with a node not in the edited set
961 if self.node in expected:
960 if self.node in expected:
962 msg = _(b'%s "%s" changeset was an edited list candidate')
961 msg = _(b'%s "%s" changeset was an edited list candidate')
963 raise error.ParseError(
962 raise error.ParseError(
964 msg % (self.verb, node.short(self.node)),
963 msg % (self.verb, node.short(self.node)),
965 hint=_(b'base must only use unlisted changesets'),
964 hint=_(b'base must only use unlisted changesets'),
966 )
965 )
967
966
968
967
969 @action(
968 @action(
970 [b'_multifold'],
969 [b'_multifold'],
971 _(
970 _(
972 """fold subclass used for when multiple folds happen in a row
971 """fold subclass used for when multiple folds happen in a row
973
972
974 We only want to fire the editor for the folded message once when
973 We only want to fire the editor for the folded message once when
975 (say) four changes are folded down into a single change. This is
974 (say) four changes are folded down into a single change. This is
976 similar to rollup, but we should preserve both messages so that
975 similar to rollup, but we should preserve both messages so that
977 when the last fold operation runs we can show the user all the
976 when the last fold operation runs we can show the user all the
978 commit messages in their editor.
977 commit messages in their editor.
979 """
978 """
980 ),
979 ),
981 internal=True,
980 internal=True,
982 )
981 )
983 class _multifold(fold):
982 class _multifold(fold):
984 def skipprompt(self):
983 def skipprompt(self):
985 return True
984 return True
986
985
987
986
988 @action(
987 @action(
989 [b"roll", b"r"],
988 [b"roll", b"r"],
990 _(b"like fold, but discard this commit's description and date"),
989 _(b"like fold, but discard this commit's description and date"),
991 )
990 )
992 class rollup(fold):
991 class rollup(fold):
993 def mergedescs(self):
992 def mergedescs(self):
994 return False
993 return False
995
994
996 def skipprompt(self):
995 def skipprompt(self):
997 return True
996 return True
998
997
999 def firstdate(self):
998 def firstdate(self):
1000 return True
999 return True
1001
1000
1002
1001
1003 @action([b"drop", b"d"], _(b'remove commit from history'))
1002 @action([b"drop", b"d"], _(b'remove commit from history'))
1004 class drop(histeditaction):
1003 class drop(histeditaction):
1005 def run(self):
1004 def run(self):
1006 parentctx = self.repo[self.state.parentctxnode]
1005 parentctx = self.repo[self.state.parentctxnode]
1007 return parentctx, [(self.node, tuple())]
1006 return parentctx, [(self.node, tuple())]
1008
1007
1009
1008
1010 @action(
1009 @action(
1011 [b"mess", b"m"],
1010 [b"mess", b"m"],
1012 _(b'edit commit message without changing commit content'),
1011 _(b'edit commit message without changing commit content'),
1013 priority=True,
1012 priority=True,
1014 )
1013 )
1015 class message(histeditaction):
1014 class message(histeditaction):
1016 def commiteditor(self):
1015 def commiteditor(self):
1017 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.mess')
1016 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.mess')
1018
1017
1019
1018
1020 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
1019 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
1021 """utility function to find the first outgoing changeset
1020 """utility function to find the first outgoing changeset
1022
1021
1023 Used by initialization code"""
1022 Used by initialization code"""
1024 if opts is None:
1023 if opts is None:
1025 opts = {}
1024 opts = {}
1026 dest = ui.expandpath(remote or b'default-push', remote or b'default')
1025 dest = ui.expandpath(remote or b'default-push', remote or b'default')
1027 dest, branches = hg.parseurl(dest, None)[:2]
1026 dest, branches = hg.parseurl(dest, None)[:2]
1028 ui.status(_(b'comparing with %s\n') % util.hidepassword(dest))
1027 ui.status(_(b'comparing with %s\n') % util.hidepassword(dest))
1029
1028
1030 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
1029 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
1031 other = hg.peer(repo, opts, dest)
1030 other = hg.peer(repo, opts, dest)
1032
1031
1033 if revs:
1032 if revs:
1034 revs = [repo.lookup(rev) for rev in revs]
1033 revs = [repo.lookup(rev) for rev in revs]
1035
1034
1036 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
1035 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
1037 if not outgoing.missing:
1036 if not outgoing.missing:
1038 raise error.Abort(_(b'no outgoing ancestors'))
1037 raise error.Abort(_(b'no outgoing ancestors'))
1039 roots = list(repo.revs(b"roots(%ln)", outgoing.missing))
1038 roots = list(repo.revs(b"roots(%ln)", outgoing.missing))
1040 if len(roots) > 1:
1039 if len(roots) > 1:
1041 msg = _(b'there are ambiguous outgoing revisions')
1040 msg = _(b'there are ambiguous outgoing revisions')
1042 hint = _(b"see 'hg help histedit' for more detail")
1041 hint = _(b"see 'hg help histedit' for more detail")
1043 raise error.Abort(msg, hint=hint)
1042 raise error.Abort(msg, hint=hint)
1044 return repo[roots[0]].node()
1043 return repo[roots[0]].node()
1045
1044
1046
1045
1047 # Curses Support
1046 # Curses Support
1048 try:
1047 try:
1049 import curses
1048 import curses
1050 except ImportError:
1049 except ImportError:
1051 curses = None
1050 curses = None
1052
1051
1053 KEY_LIST = [b'pick', b'edit', b'fold', b'drop', b'mess', b'roll']
1052 KEY_LIST = [b'pick', b'edit', b'fold', b'drop', b'mess', b'roll']
1054 ACTION_LABELS = {
1053 ACTION_LABELS = {
1055 b'fold': b'^fold',
1054 b'fold': b'^fold',
1056 b'roll': b'^roll',
1055 b'roll': b'^roll',
1057 }
1056 }
1058
1057
1059 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
1058 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN, COLOR_CURRENT = 1, 2, 3, 4, 5
1060 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
1059 COLOR_DIFF_ADD_LINE, COLOR_DIFF_DEL_LINE, COLOR_DIFF_OFFSET = 6, 7, 8
1061 COLOR_ROLL, COLOR_ROLL_CURRENT, COLOR_ROLL_SELECTED = 9, 10, 11
1060 COLOR_ROLL, COLOR_ROLL_CURRENT, COLOR_ROLL_SELECTED = 9, 10, 11
1062
1061
1063 E_QUIT, E_HISTEDIT = 1, 2
1062 E_QUIT, E_HISTEDIT = 1, 2
1064 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
1063 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
1065 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
1064 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
1066
1065
1067 KEYTABLE = {
1066 KEYTABLE = {
1068 b'global': {
1067 b'global': {
1069 b'h': b'next-action',
1068 b'h': b'next-action',
1070 b'KEY_RIGHT': b'next-action',
1069 b'KEY_RIGHT': b'next-action',
1071 b'l': b'prev-action',
1070 b'l': b'prev-action',
1072 b'KEY_LEFT': b'prev-action',
1071 b'KEY_LEFT': b'prev-action',
1073 b'q': b'quit',
1072 b'q': b'quit',
1074 b'c': b'histedit',
1073 b'c': b'histedit',
1075 b'C': b'histedit',
1074 b'C': b'histedit',
1076 b'v': b'showpatch',
1075 b'v': b'showpatch',
1077 b'?': b'help',
1076 b'?': b'help',
1078 },
1077 },
1079 MODE_RULES: {
1078 MODE_RULES: {
1080 b'd': b'action-drop',
1079 b'd': b'action-drop',
1081 b'e': b'action-edit',
1080 b'e': b'action-edit',
1082 b'f': b'action-fold',
1081 b'f': b'action-fold',
1083 b'm': b'action-mess',
1082 b'm': b'action-mess',
1084 b'p': b'action-pick',
1083 b'p': b'action-pick',
1085 b'r': b'action-roll',
1084 b'r': b'action-roll',
1086 b' ': b'select',
1085 b' ': b'select',
1087 b'j': b'down',
1086 b'j': b'down',
1088 b'k': b'up',
1087 b'k': b'up',
1089 b'KEY_DOWN': b'down',
1088 b'KEY_DOWN': b'down',
1090 b'KEY_UP': b'up',
1089 b'KEY_UP': b'up',
1091 b'J': b'move-down',
1090 b'J': b'move-down',
1092 b'K': b'move-up',
1091 b'K': b'move-up',
1093 b'KEY_NPAGE': b'move-down',
1092 b'KEY_NPAGE': b'move-down',
1094 b'KEY_PPAGE': b'move-up',
1093 b'KEY_PPAGE': b'move-up',
1095 b'0': b'goto', # Used for 0..9
1094 b'0': b'goto', # Used for 0..9
1096 },
1095 },
1097 MODE_PATCH: {
1096 MODE_PATCH: {
1098 b' ': b'page-down',
1097 b' ': b'page-down',
1099 b'KEY_NPAGE': b'page-down',
1098 b'KEY_NPAGE': b'page-down',
1100 b'KEY_PPAGE': b'page-up',
1099 b'KEY_PPAGE': b'page-up',
1101 b'j': b'line-down',
1100 b'j': b'line-down',
1102 b'k': b'line-up',
1101 b'k': b'line-up',
1103 b'KEY_DOWN': b'line-down',
1102 b'KEY_DOWN': b'line-down',
1104 b'KEY_UP': b'line-up',
1103 b'KEY_UP': b'line-up',
1105 b'J': b'down',
1104 b'J': b'down',
1106 b'K': b'up',
1105 b'K': b'up',
1107 },
1106 },
1108 MODE_HELP: {},
1107 MODE_HELP: {},
1109 }
1108 }
1110
1109
1111
1110
1112 def screen_size():
1111 def screen_size():
1113 return struct.unpack(b'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, b' '))
1112 return struct.unpack(b'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, b' '))
1114
1113
1115
1114
1116 class histeditrule(object):
1115 class histeditrule(object):
1117 def __init__(self, ui, ctx, pos, action=b'pick'):
1116 def __init__(self, ui, ctx, pos, action=b'pick'):
1118 self.ui = ui
1117 self.ui = ui
1119 self.ctx = ctx
1118 self.ctx = ctx
1120 self.action = action
1119 self.action = action
1121 self.origpos = pos
1120 self.origpos = pos
1122 self.pos = pos
1121 self.pos = pos
1123 self.conflicts = []
1122 self.conflicts = []
1124
1123
1125 def __bytes__(self):
1124 def __bytes__(self):
1126 # Example display of several histeditrules:
1125 # Example display of several histeditrules:
1127 #
1126 #
1128 # #10 pick 316392:06a16c25c053 add option to skip tests
1127 # #10 pick 316392:06a16c25c053 add option to skip tests
1129 # #11 ^roll 316393:71313c964cc5 <RED>oops a fixup commit</RED>
1128 # #11 ^roll 316393:71313c964cc5 <RED>oops a fixup commit</RED>
1130 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1129 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1131 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1130 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1132 #
1131 #
1133 # The carets point to the changeset being folded into ("roll this
1132 # The carets point to the changeset being folded into ("roll this
1134 # changeset into the changeset above").
1133 # changeset into the changeset above").
1135 return b'%s%s' % (self.prefix, self.desc)
1134 return b'%s%s' % (self.prefix, self.desc)
1136
1135
1137 __str__ = encoding.strmethod(__bytes__)
1136 __str__ = encoding.strmethod(__bytes__)
1138
1137
1139 @property
1138 @property
1140 def prefix(self):
1139 def prefix(self):
1141 # Some actions ('fold' and 'roll') combine a patch with a
1140 # Some actions ('fold' and 'roll') combine a patch with a
1142 # previous one. Add a marker showing which patch they apply
1141 # previous one. Add a marker showing which patch they apply
1143 # to.
1142 # to.
1144 action = ACTION_LABELS.get(self.action, self.action)
1143 action = ACTION_LABELS.get(self.action, self.action)
1145
1144
1146 h = self.ctx.hex()[0:12]
1145 h = self.ctx.hex()[0:12]
1147 r = self.ctx.rev()
1146 r = self.ctx.rev()
1148
1147
1149 return b"#%s %s %d:%s " % (
1148 return b"#%s %s %d:%s " % (
1150 (b'%d' % self.origpos).ljust(2),
1149 (b'%d' % self.origpos).ljust(2),
1151 action.ljust(6),
1150 action.ljust(6),
1152 r,
1151 r,
1153 h,
1152 h,
1154 )
1153 )
1155
1154
1156 @property
1155 @property
1157 def desc(self):
1156 def desc(self):
1158 summary = (
1157 summary = (
1159 cmdutil.rendertemplate(
1158 cmdutil.rendertemplate(
1160 self.ctx, self.ui.config(b'histedit', b'summary-template')
1159 self.ctx, self.ui.config(b'histedit', b'summary-template')
1161 )
1160 )
1162 or b''
1161 or b''
1163 )
1162 )
1164 if summary:
1163 if summary:
1165 return summary
1164 return summary
1166 # This is split off from the prefix property so that we can
1165 # This is split off from the prefix property so that we can
1167 # separately make the description for 'roll' red (since it
1166 # separately make the description for 'roll' red (since it
1168 # will get discarded).
1167 # will get discarded).
1169 return self.ctx.description().splitlines()[0].strip()
1168 return self.ctx.description().splitlines()[0].strip()
1170
1169
1171 def checkconflicts(self, other):
1170 def checkconflicts(self, other):
1172 if other.pos > self.pos and other.origpos <= self.origpos:
1171 if other.pos > self.pos and other.origpos <= self.origpos:
1173 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1172 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1174 self.conflicts.append(other)
1173 self.conflicts.append(other)
1175 return self.conflicts
1174 return self.conflicts
1176
1175
1177 if other in self.conflicts:
1176 if other in self.conflicts:
1178 self.conflicts.remove(other)
1177 self.conflicts.remove(other)
1179 return self.conflicts
1178 return self.conflicts
1180
1179
1181
1180
1182 # ============ EVENTS ===============
1181 # ============ EVENTS ===============
1183 def movecursor(state, oldpos, newpos):
1182 def movecursor(state, oldpos, newpos):
1184 '''Change the rule/changeset that the cursor is pointing to, regardless of
1183 '''Change the rule/changeset that the cursor is pointing to, regardless of
1185 current mode (you can switch between patches from the view patch window).'''
1184 current mode (you can switch between patches from the view patch window).'''
1186 state[b'pos'] = newpos
1185 state[b'pos'] = newpos
1187
1186
1188 mode, _ = state[b'mode']
1187 mode, _ = state[b'mode']
1189 if mode == MODE_RULES:
1188 if mode == MODE_RULES:
1190 # Scroll through the list by updating the view for MODE_RULES, so that
1189 # Scroll through the list by updating the view for MODE_RULES, so that
1191 # even if we are not currently viewing the rules, switching back will
1190 # even if we are not currently viewing the rules, switching back will
1192 # result in the cursor's rule being visible.
1191 # result in the cursor's rule being visible.
1193 modestate = state[b'modes'][MODE_RULES]
1192 modestate = state[b'modes'][MODE_RULES]
1194 if newpos < modestate[b'line_offset']:
1193 if newpos < modestate[b'line_offset']:
1195 modestate[b'line_offset'] = newpos
1194 modestate[b'line_offset'] = newpos
1196 elif newpos > modestate[b'line_offset'] + state[b'page_height'] - 1:
1195 elif newpos > modestate[b'line_offset'] + state[b'page_height'] - 1:
1197 modestate[b'line_offset'] = newpos - state[b'page_height'] + 1
1196 modestate[b'line_offset'] = newpos - state[b'page_height'] + 1
1198
1197
1199 # Reset the patch view region to the top of the new patch.
1198 # Reset the patch view region to the top of the new patch.
1200 state[b'modes'][MODE_PATCH][b'line_offset'] = 0
1199 state[b'modes'][MODE_PATCH][b'line_offset'] = 0
1201
1200
1202
1201
1203 def changemode(state, mode):
1202 def changemode(state, mode):
1204 curmode, _ = state[b'mode']
1203 curmode, _ = state[b'mode']
1205 state[b'mode'] = (mode, curmode)
1204 state[b'mode'] = (mode, curmode)
1206 if mode == MODE_PATCH:
1205 if mode == MODE_PATCH:
1207 state[b'modes'][MODE_PATCH][b'patchcontents'] = patchcontents(state)
1206 state[b'modes'][MODE_PATCH][b'patchcontents'] = patchcontents(state)
1208
1207
1209
1208
1210 def makeselection(state, pos):
1209 def makeselection(state, pos):
1211 state[b'selected'] = pos
1210 state[b'selected'] = pos
1212
1211
1213
1212
1214 def swap(state, oldpos, newpos):
1213 def swap(state, oldpos, newpos):
1215 """Swap two positions and calculate necessary conflicts in
1214 """Swap two positions and calculate necessary conflicts in
1216 O(|newpos-oldpos|) time"""
1215 O(|newpos-oldpos|) time"""
1217
1216
1218 rules = state[b'rules']
1217 rules = state[b'rules']
1219 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1218 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1220
1219
1221 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1220 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1222
1221
1223 # TODO: swap should not know about histeditrule's internals
1222 # TODO: swap should not know about histeditrule's internals
1224 rules[newpos].pos = newpos
1223 rules[newpos].pos = newpos
1225 rules[oldpos].pos = oldpos
1224 rules[oldpos].pos = oldpos
1226
1225
1227 start = min(oldpos, newpos)
1226 start = min(oldpos, newpos)
1228 end = max(oldpos, newpos)
1227 end = max(oldpos, newpos)
1229 for r in pycompat.xrange(start, end + 1):
1228 for r in pycompat.xrange(start, end + 1):
1230 rules[newpos].checkconflicts(rules[r])
1229 rules[newpos].checkconflicts(rules[r])
1231 rules[oldpos].checkconflicts(rules[r])
1230 rules[oldpos].checkconflicts(rules[r])
1232
1231
1233 if state[b'selected']:
1232 if state[b'selected']:
1234 makeselection(state, newpos)
1233 makeselection(state, newpos)
1235
1234
1236
1235
1237 def changeaction(state, pos, action):
1236 def changeaction(state, pos, action):
1238 """Change the action state on the given position to the new action"""
1237 """Change the action state on the given position to the new action"""
1239 rules = state[b'rules']
1238 rules = state[b'rules']
1240 assert 0 <= pos < len(rules)
1239 assert 0 <= pos < len(rules)
1241 rules[pos].action = action
1240 rules[pos].action = action
1242
1241
1243
1242
1244 def cycleaction(state, pos, next=False):
1243 def cycleaction(state, pos, next=False):
1245 """Changes the action state the next or the previous action from
1244 """Changes the action state the next or the previous action from
1246 the action list"""
1245 the action list"""
1247 rules = state[b'rules']
1246 rules = state[b'rules']
1248 assert 0 <= pos < len(rules)
1247 assert 0 <= pos < len(rules)
1249 current = rules[pos].action
1248 current = rules[pos].action
1250
1249
1251 assert current in KEY_LIST
1250 assert current in KEY_LIST
1252
1251
1253 index = KEY_LIST.index(current)
1252 index = KEY_LIST.index(current)
1254 if next:
1253 if next:
1255 index += 1
1254 index += 1
1256 else:
1255 else:
1257 index -= 1
1256 index -= 1
1258 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1257 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1259
1258
1260
1259
1261 def changeview(state, delta, unit):
1260 def changeview(state, delta, unit):
1262 '''Change the region of whatever is being viewed (a patch or the list of
1261 '''Change the region of whatever is being viewed (a patch or the list of
1263 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1262 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1264 mode, _ = state[b'mode']
1263 mode, _ = state[b'mode']
1265 if mode != MODE_PATCH:
1264 if mode != MODE_PATCH:
1266 return
1265 return
1267 mode_state = state[b'modes'][mode]
1266 mode_state = state[b'modes'][mode]
1268 num_lines = len(mode_state[b'patchcontents'])
1267 num_lines = len(mode_state[b'patchcontents'])
1269 page_height = state[b'page_height']
1268 page_height = state[b'page_height']
1270 unit = page_height if unit == b'page' else 1
1269 unit = page_height if unit == b'page' else 1
1271 num_pages = 1 + (num_lines - 1) // page_height
1270 num_pages = 1 + (num_lines - 1) // page_height
1272 max_offset = (num_pages - 1) * page_height
1271 max_offset = (num_pages - 1) * page_height
1273 newline = mode_state[b'line_offset'] + delta * unit
1272 newline = mode_state[b'line_offset'] + delta * unit
1274 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1273 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1275
1274
1276
1275
1277 def event(state, ch):
1276 def event(state, ch):
1278 """Change state based on the current character input
1277 """Change state based on the current character input
1279
1278
1280 This takes the current state and based on the current character input from
1279 This takes the current state and based on the current character input from
1281 the user we change the state.
1280 the user we change the state.
1282 """
1281 """
1283 selected = state[b'selected']
1282 selected = state[b'selected']
1284 oldpos = state[b'pos']
1283 oldpos = state[b'pos']
1285 rules = state[b'rules']
1284 rules = state[b'rules']
1286
1285
1287 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1286 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1288 return E_RESIZE
1287 return E_RESIZE
1289
1288
1290 lookup_ch = ch
1289 lookup_ch = ch
1291 if ch is not None and b'0' <= ch <= b'9':
1290 if ch is not None and b'0' <= ch <= b'9':
1292 lookup_ch = b'0'
1291 lookup_ch = b'0'
1293
1292
1294 curmode, prevmode = state[b'mode']
1293 curmode, prevmode = state[b'mode']
1295 action = KEYTABLE[curmode].get(
1294 action = KEYTABLE[curmode].get(
1296 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1295 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1297 )
1296 )
1298 if action is None:
1297 if action is None:
1299 return
1298 return
1300 if action in (b'down', b'move-down'):
1299 if action in (b'down', b'move-down'):
1301 newpos = min(oldpos + 1, len(rules) - 1)
1300 newpos = min(oldpos + 1, len(rules) - 1)
1302 movecursor(state, oldpos, newpos)
1301 movecursor(state, oldpos, newpos)
1303 if selected is not None or action == b'move-down':
1302 if selected is not None or action == b'move-down':
1304 swap(state, oldpos, newpos)
1303 swap(state, oldpos, newpos)
1305 elif action in (b'up', b'move-up'):
1304 elif action in (b'up', b'move-up'):
1306 newpos = max(0, oldpos - 1)
1305 newpos = max(0, oldpos - 1)
1307 movecursor(state, oldpos, newpos)
1306 movecursor(state, oldpos, newpos)
1308 if selected is not None or action == b'move-up':
1307 if selected is not None or action == b'move-up':
1309 swap(state, oldpos, newpos)
1308 swap(state, oldpos, newpos)
1310 elif action == b'next-action':
1309 elif action == b'next-action':
1311 cycleaction(state, oldpos, next=True)
1310 cycleaction(state, oldpos, next=True)
1312 elif action == b'prev-action':
1311 elif action == b'prev-action':
1313 cycleaction(state, oldpos, next=False)
1312 cycleaction(state, oldpos, next=False)
1314 elif action == b'select':
1313 elif action == b'select':
1315 selected = oldpos if selected is None else None
1314 selected = oldpos if selected is None else None
1316 makeselection(state, selected)
1315 makeselection(state, selected)
1317 elif action == b'goto' and int(ch) < len(rules) and len(rules) <= 10:
1316 elif action == b'goto' and int(ch) < len(rules) and len(rules) <= 10:
1318 newrule = next((r for r in rules if r.origpos == int(ch)))
1317 newrule = next((r for r in rules if r.origpos == int(ch)))
1319 movecursor(state, oldpos, newrule.pos)
1318 movecursor(state, oldpos, newrule.pos)
1320 if selected is not None:
1319 if selected is not None:
1321 swap(state, oldpos, newrule.pos)
1320 swap(state, oldpos, newrule.pos)
1322 elif action.startswith(b'action-'):
1321 elif action.startswith(b'action-'):
1323 changeaction(state, oldpos, action[7:])
1322 changeaction(state, oldpos, action[7:])
1324 elif action == b'showpatch':
1323 elif action == b'showpatch':
1325 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1324 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1326 elif action == b'help':
1325 elif action == b'help':
1327 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1326 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1328 elif action == b'quit':
1327 elif action == b'quit':
1329 return E_QUIT
1328 return E_QUIT
1330 elif action == b'histedit':
1329 elif action == b'histedit':
1331 return E_HISTEDIT
1330 return E_HISTEDIT
1332 elif action == b'page-down':
1331 elif action == b'page-down':
1333 return E_PAGEDOWN
1332 return E_PAGEDOWN
1334 elif action == b'page-up':
1333 elif action == b'page-up':
1335 return E_PAGEUP
1334 return E_PAGEUP
1336 elif action == b'line-down':
1335 elif action == b'line-down':
1337 return E_LINEDOWN
1336 return E_LINEDOWN
1338 elif action == b'line-up':
1337 elif action == b'line-up':
1339 return E_LINEUP
1338 return E_LINEUP
1340
1339
1341
1340
1342 def makecommands(rules):
1341 def makecommands(rules):
1343 """Returns a list of commands consumable by histedit --commands based on
1342 """Returns a list of commands consumable by histedit --commands based on
1344 our list of rules"""
1343 our list of rules"""
1345 commands = []
1344 commands = []
1346 for rules in rules:
1345 for rules in rules:
1347 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1346 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1348 return commands
1347 return commands
1349
1348
1350
1349
1351 def addln(win, y, x, line, color=None):
1350 def addln(win, y, x, line, color=None):
1352 """Add a line to the given window left padding but 100% filled with
1351 """Add a line to the given window left padding but 100% filled with
1353 whitespace characters, so that the color appears on the whole line"""
1352 whitespace characters, so that the color appears on the whole line"""
1354 maxy, maxx = win.getmaxyx()
1353 maxy, maxx = win.getmaxyx()
1355 length = maxx - 1 - x
1354 length = maxx - 1 - x
1356 line = bytes(line).ljust(length)[:length]
1355 line = bytes(line).ljust(length)[:length]
1357 if y < 0:
1356 if y < 0:
1358 y = maxy + y
1357 y = maxy + y
1359 if x < 0:
1358 if x < 0:
1360 x = maxx + x
1359 x = maxx + x
1361 if color:
1360 if color:
1362 win.addstr(y, x, line, color)
1361 win.addstr(y, x, line, color)
1363 else:
1362 else:
1364 win.addstr(y, x, line)
1363 win.addstr(y, x, line)
1365
1364
1366
1365
1367 def _trunc_head(line, n):
1366 def _trunc_head(line, n):
1368 if len(line) <= n:
1367 if len(line) <= n:
1369 return line
1368 return line
1370 return b'> ' + line[-(n - 2) :]
1369 return b'> ' + line[-(n - 2) :]
1371
1370
1372
1371
1373 def _trunc_tail(line, n):
1372 def _trunc_tail(line, n):
1374 if len(line) <= n:
1373 if len(line) <= n:
1375 return line
1374 return line
1376 return line[: n - 2] + b' >'
1375 return line[: n - 2] + b' >'
1377
1376
1378
1377
1379 def patchcontents(state):
1378 def patchcontents(state):
1380 repo = state[b'repo']
1379 repo = state[b'repo']
1381 rule = state[b'rules'][state[b'pos']]
1380 rule = state[b'rules'][state[b'pos']]
1382 displayer = logcmdutil.changesetdisplayer(
1381 displayer = logcmdutil.changesetdisplayer(
1383 repo.ui, repo, {b"patch": True, b"template": b"status"}, buffered=True
1382 repo.ui, repo, {b"patch": True, b"template": b"status"}, buffered=True
1384 )
1383 )
1385 overrides = {(b'ui', b'verbose'): True}
1384 overrides = {(b'ui', b'verbose'): True}
1386 with repo.ui.configoverride(overrides, source=b'histedit'):
1385 with repo.ui.configoverride(overrides, source=b'histedit'):
1387 displayer.show(rule.ctx)
1386 displayer.show(rule.ctx)
1388 displayer.close()
1387 displayer.close()
1389 return displayer.hunk[rule.ctx.rev()].splitlines()
1388 return displayer.hunk[rule.ctx.rev()].splitlines()
1390
1389
1391
1390
1392 def _chisteditmain(repo, rules, stdscr):
1391 def _chisteditmain(repo, rules, stdscr):
1393 try:
1392 try:
1394 curses.use_default_colors()
1393 curses.use_default_colors()
1395 except curses.error:
1394 except curses.error:
1396 pass
1395 pass
1397
1396
1398 # initialize color pattern
1397 # initialize color pattern
1399 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1398 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1400 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1399 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1401 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1400 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1402 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1401 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1403 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1402 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1404 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1403 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1405 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1404 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1406 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1405 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1407 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1406 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1408 curses.init_pair(
1407 curses.init_pair(
1409 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1408 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1410 )
1409 )
1411 curses.init_pair(COLOR_ROLL_SELECTED, curses.COLOR_RED, curses.COLOR_WHITE)
1410 curses.init_pair(COLOR_ROLL_SELECTED, curses.COLOR_RED, curses.COLOR_WHITE)
1412
1411
1413 # don't display the cursor
1412 # don't display the cursor
1414 try:
1413 try:
1415 curses.curs_set(0)
1414 curses.curs_set(0)
1416 except curses.error:
1415 except curses.error:
1417 pass
1416 pass
1418
1417
1419 def rendercommit(win, state):
1418 def rendercommit(win, state):
1420 """Renders the commit window that shows the log of the current selected
1419 """Renders the commit window that shows the log of the current selected
1421 commit"""
1420 commit"""
1422 pos = state[b'pos']
1421 pos = state[b'pos']
1423 rules = state[b'rules']
1422 rules = state[b'rules']
1424 rule = rules[pos]
1423 rule = rules[pos]
1425
1424
1426 ctx = rule.ctx
1425 ctx = rule.ctx
1427 win.box()
1426 win.box()
1428
1427
1429 maxy, maxx = win.getmaxyx()
1428 maxy, maxx = win.getmaxyx()
1430 length = maxx - 3
1429 length = maxx - 3
1431
1430
1432 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1431 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1433 win.addstr(1, 1, line[:length])
1432 win.addstr(1, 1, line[:length])
1434
1433
1435 line = b"user: %s" % ctx.user()
1434 line = b"user: %s" % ctx.user()
1436 win.addstr(2, 1, line[:length])
1435 win.addstr(2, 1, line[:length])
1437
1436
1438 bms = repo.nodebookmarks(ctx.node())
1437 bms = repo.nodebookmarks(ctx.node())
1439 line = b"bookmark: %s" % b' '.join(bms)
1438 line = b"bookmark: %s" % b' '.join(bms)
1440 win.addstr(3, 1, line[:length])
1439 win.addstr(3, 1, line[:length])
1441
1440
1442 line = b"summary: %s" % (ctx.description().splitlines()[0])
1441 line = b"summary: %s" % (ctx.description().splitlines()[0])
1443 win.addstr(4, 1, line[:length])
1442 win.addstr(4, 1, line[:length])
1444
1443
1445 line = b"files: "
1444 line = b"files: "
1446 win.addstr(5, 1, line)
1445 win.addstr(5, 1, line)
1447 fnx = 1 + len(line)
1446 fnx = 1 + len(line)
1448 fnmaxx = length - fnx + 1
1447 fnmaxx = length - fnx + 1
1449 y = 5
1448 y = 5
1450 fnmaxn = maxy - (1 + y) - 1
1449 fnmaxn = maxy - (1 + y) - 1
1451 files = ctx.files()
1450 files = ctx.files()
1452 for i, line1 in enumerate(files):
1451 for i, line1 in enumerate(files):
1453 if len(files) > fnmaxn and i == fnmaxn - 1:
1452 if len(files) > fnmaxn and i == fnmaxn - 1:
1454 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1453 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1455 y = y + 1
1454 y = y + 1
1456 break
1455 break
1457 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1456 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1458 y = y + 1
1457 y = y + 1
1459
1458
1460 conflicts = rule.conflicts
1459 conflicts = rule.conflicts
1461 if len(conflicts) > 0:
1460 if len(conflicts) > 0:
1462 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1461 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1463 conflictstr = b"changed files overlap with %s" % conflictstr
1462 conflictstr = b"changed files overlap with %s" % conflictstr
1464 else:
1463 else:
1465 conflictstr = b'no overlap'
1464 conflictstr = b'no overlap'
1466
1465
1467 win.addstr(y, 1, conflictstr[:length])
1466 win.addstr(y, 1, conflictstr[:length])
1468 win.noutrefresh()
1467 win.noutrefresh()
1469
1468
1470 def helplines(mode):
1469 def helplines(mode):
1471 if mode == MODE_PATCH:
1470 if mode == MODE_PATCH:
1472 help = b"""\
1471 help = b"""\
1473 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1472 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1474 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1473 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1475 """
1474 """
1476 else:
1475 else:
1477 help = b"""\
1476 help = b"""\
1478 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1477 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1479 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1478 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1480 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1479 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1481 """
1480 """
1482 return help.splitlines()
1481 return help.splitlines()
1483
1482
1484 def renderhelp(win, state):
1483 def renderhelp(win, state):
1485 maxy, maxx = win.getmaxyx()
1484 maxy, maxx = win.getmaxyx()
1486 mode, _ = state[b'mode']
1485 mode, _ = state[b'mode']
1487 for y, line in enumerate(helplines(mode)):
1486 for y, line in enumerate(helplines(mode)):
1488 if y >= maxy:
1487 if y >= maxy:
1489 break
1488 break
1490 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1489 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1491 win.noutrefresh()
1490 win.noutrefresh()
1492
1491
1493 def renderrules(rulesscr, state):
1492 def renderrules(rulesscr, state):
1494 rules = state[b'rules']
1493 rules = state[b'rules']
1495 pos = state[b'pos']
1494 pos = state[b'pos']
1496 selected = state[b'selected']
1495 selected = state[b'selected']
1497 start = state[b'modes'][MODE_RULES][b'line_offset']
1496 start = state[b'modes'][MODE_RULES][b'line_offset']
1498
1497
1499 conflicts = [r.ctx for r in rules if r.conflicts]
1498 conflicts = [r.ctx for r in rules if r.conflicts]
1500 if len(conflicts) > 0:
1499 if len(conflicts) > 0:
1501 line = b"potential conflict in %s" % b','.join(
1500 line = b"potential conflict in %s" % b','.join(
1502 map(pycompat.bytestr, conflicts)
1501 map(pycompat.bytestr, conflicts)
1503 )
1502 )
1504 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1503 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1505
1504
1506 for y, rule in enumerate(rules[start:]):
1505 for y, rule in enumerate(rules[start:]):
1507 if y >= state[b'page_height']:
1506 if y >= state[b'page_height']:
1508 break
1507 break
1509 if len(rule.conflicts) > 0:
1508 if len(rule.conflicts) > 0:
1510 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1509 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1511 else:
1510 else:
1512 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1511 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1513
1512
1514 if y + start == selected:
1513 if y + start == selected:
1515 rollcolor = COLOR_ROLL_SELECTED
1514 rollcolor = COLOR_ROLL_SELECTED
1516 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1515 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1517 elif y + start == pos:
1516 elif y + start == pos:
1518 rollcolor = COLOR_ROLL_CURRENT
1517 rollcolor = COLOR_ROLL_CURRENT
1519 addln(
1518 addln(
1520 rulesscr,
1519 rulesscr,
1521 y,
1520 y,
1522 2,
1521 2,
1523 rule,
1522 rule,
1524 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1523 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1525 )
1524 )
1526 else:
1525 else:
1527 rollcolor = COLOR_ROLL
1526 rollcolor = COLOR_ROLL
1528 addln(rulesscr, y, 2, rule)
1527 addln(rulesscr, y, 2, rule)
1529
1528
1530 if rule.action == b'roll':
1529 if rule.action == b'roll':
1531 rulesscr.addstr(
1530 rulesscr.addstr(
1532 y,
1531 y,
1533 2 + len(rule.prefix),
1532 2 + len(rule.prefix),
1534 rule.desc,
1533 rule.desc,
1535 curses.color_pair(rollcolor),
1534 curses.color_pair(rollcolor),
1536 )
1535 )
1537
1536
1538 rulesscr.noutrefresh()
1537 rulesscr.noutrefresh()
1539
1538
1540 def renderstring(win, state, output, diffcolors=False):
1539 def renderstring(win, state, output, diffcolors=False):
1541 maxy, maxx = win.getmaxyx()
1540 maxy, maxx = win.getmaxyx()
1542 length = min(maxy - 1, len(output))
1541 length = min(maxy - 1, len(output))
1543 for y in range(0, length):
1542 for y in range(0, length):
1544 line = output[y]
1543 line = output[y]
1545 if diffcolors:
1544 if diffcolors:
1546 if line and line[0] == b'+':
1545 if line and line[0] == b'+':
1547 win.addstr(
1546 win.addstr(
1548 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1547 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1549 )
1548 )
1550 elif line and line[0] == b'-':
1549 elif line and line[0] == b'-':
1551 win.addstr(
1550 win.addstr(
1552 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1551 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1553 )
1552 )
1554 elif line.startswith(b'@@ '):
1553 elif line.startswith(b'@@ '):
1555 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1554 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1556 else:
1555 else:
1557 win.addstr(y, 0, line)
1556 win.addstr(y, 0, line)
1558 else:
1557 else:
1559 win.addstr(y, 0, line)
1558 win.addstr(y, 0, line)
1560 win.noutrefresh()
1559 win.noutrefresh()
1561
1560
1562 def renderpatch(win, state):
1561 def renderpatch(win, state):
1563 start = state[b'modes'][MODE_PATCH][b'line_offset']
1562 start = state[b'modes'][MODE_PATCH][b'line_offset']
1564 content = state[b'modes'][MODE_PATCH][b'patchcontents']
1563 content = state[b'modes'][MODE_PATCH][b'patchcontents']
1565 renderstring(win, state, content[start:], diffcolors=True)
1564 renderstring(win, state, content[start:], diffcolors=True)
1566
1565
1567 def layout(mode):
1566 def layout(mode):
1568 maxy, maxx = stdscr.getmaxyx()
1567 maxy, maxx = stdscr.getmaxyx()
1569 helplen = len(helplines(mode))
1568 helplen = len(helplines(mode))
1570 return {
1569 return {
1571 b'commit': (12, maxx),
1570 b'commit': (12, maxx),
1572 b'help': (helplen, maxx),
1571 b'help': (helplen, maxx),
1573 b'main': (maxy - helplen - 12, maxx),
1572 b'main': (maxy - helplen - 12, maxx),
1574 }
1573 }
1575
1574
1576 def drawvertwin(size, y, x):
1575 def drawvertwin(size, y, x):
1577 win = curses.newwin(size[0], size[1], y, x)
1576 win = curses.newwin(size[0], size[1], y, x)
1578 y += size[0]
1577 y += size[0]
1579 return win, y, x
1578 return win, y, x
1580
1579
1581 state = {
1580 state = {
1582 b'pos': 0,
1581 b'pos': 0,
1583 b'rules': rules,
1582 b'rules': rules,
1584 b'selected': None,
1583 b'selected': None,
1585 b'mode': (MODE_INIT, MODE_INIT),
1584 b'mode': (MODE_INIT, MODE_INIT),
1586 b'page_height': None,
1585 b'page_height': None,
1587 b'modes': {
1586 b'modes': {
1588 MODE_RULES: {b'line_offset': 0,},
1587 MODE_RULES: {b'line_offset': 0,},
1589 MODE_PATCH: {b'line_offset': 0,},
1588 MODE_PATCH: {b'line_offset': 0,},
1590 },
1589 },
1591 b'repo': repo,
1590 b'repo': repo,
1592 }
1591 }
1593
1592
1594 # eventloop
1593 # eventloop
1595 ch = None
1594 ch = None
1596 stdscr.clear()
1595 stdscr.clear()
1597 stdscr.refresh()
1596 stdscr.refresh()
1598 while True:
1597 while True:
1599 try:
1598 try:
1600 oldmode, _ = state[b'mode']
1599 oldmode, _ = state[b'mode']
1601 if oldmode == MODE_INIT:
1600 if oldmode == MODE_INIT:
1602 changemode(state, MODE_RULES)
1601 changemode(state, MODE_RULES)
1603 e = event(state, ch)
1602 e = event(state, ch)
1604
1603
1605 if e == E_QUIT:
1604 if e == E_QUIT:
1606 return False
1605 return False
1607 if e == E_HISTEDIT:
1606 if e == E_HISTEDIT:
1608 return state[b'rules']
1607 return state[b'rules']
1609 else:
1608 else:
1610 if e == E_RESIZE:
1609 if e == E_RESIZE:
1611 size = screen_size()
1610 size = screen_size()
1612 if size != stdscr.getmaxyx():
1611 if size != stdscr.getmaxyx():
1613 curses.resizeterm(*size)
1612 curses.resizeterm(*size)
1614
1613
1615 curmode, _ = state[b'mode']
1614 curmode, _ = state[b'mode']
1616 sizes = layout(curmode)
1615 sizes = layout(curmode)
1617 if curmode != oldmode:
1616 if curmode != oldmode:
1618 state[b'page_height'] = sizes[b'main'][0]
1617 state[b'page_height'] = sizes[b'main'][0]
1619 # Adjust the view to fit the current screen size.
1618 # Adjust the view to fit the current screen size.
1620 movecursor(state, state[b'pos'], state[b'pos'])
1619 movecursor(state, state[b'pos'], state[b'pos'])
1621
1620
1622 # Pack the windows against the top, each pane spread across the
1621 # Pack the windows against the top, each pane spread across the
1623 # full width of the screen.
1622 # full width of the screen.
1624 y, x = (0, 0)
1623 y, x = (0, 0)
1625 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1624 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1626 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1625 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1627 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1626 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1628
1627
1629 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1628 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1630 if e == E_PAGEDOWN:
1629 if e == E_PAGEDOWN:
1631 changeview(state, +1, b'page')
1630 changeview(state, +1, b'page')
1632 elif e == E_PAGEUP:
1631 elif e == E_PAGEUP:
1633 changeview(state, -1, b'page')
1632 changeview(state, -1, b'page')
1634 elif e == E_LINEDOWN:
1633 elif e == E_LINEDOWN:
1635 changeview(state, +1, b'line')
1634 changeview(state, +1, b'line')
1636 elif e == E_LINEUP:
1635 elif e == E_LINEUP:
1637 changeview(state, -1, b'line')
1636 changeview(state, -1, b'line')
1638
1637
1639 # start rendering
1638 # start rendering
1640 commitwin.erase()
1639 commitwin.erase()
1641 helpwin.erase()
1640 helpwin.erase()
1642 mainwin.erase()
1641 mainwin.erase()
1643 if curmode == MODE_PATCH:
1642 if curmode == MODE_PATCH:
1644 renderpatch(mainwin, state)
1643 renderpatch(mainwin, state)
1645 elif curmode == MODE_HELP:
1644 elif curmode == MODE_HELP:
1646 renderstring(mainwin, state, __doc__.strip().splitlines())
1645 renderstring(mainwin, state, __doc__.strip().splitlines())
1647 else:
1646 else:
1648 renderrules(mainwin, state)
1647 renderrules(mainwin, state)
1649 rendercommit(commitwin, state)
1648 rendercommit(commitwin, state)
1650 renderhelp(helpwin, state)
1649 renderhelp(helpwin, state)
1651 curses.doupdate()
1650 curses.doupdate()
1652 # done rendering
1651 # done rendering
1653 ch = encoding.strtolocal(stdscr.getkey())
1652 ch = encoding.strtolocal(stdscr.getkey())
1654 except curses.error:
1653 except curses.error:
1655 pass
1654 pass
1656
1655
1657
1656
1658 def _chistedit(ui, repo, freeargs, opts):
1657 def _chistedit(ui, repo, freeargs, opts):
1659 """interactively edit changeset history via a curses interface
1658 """interactively edit changeset history via a curses interface
1660
1659
1661 Provides a ncurses interface to histedit. Press ? in chistedit mode
1660 Provides a ncurses interface to histedit. Press ? in chistedit mode
1662 to see an extensive help. Requires python-curses to be installed."""
1661 to see an extensive help. Requires python-curses to be installed."""
1663
1662
1664 if curses is None:
1663 if curses is None:
1665 raise error.Abort(_(b"Python curses library required"))
1664 raise error.Abort(_(b"Python curses library required"))
1666
1665
1667 # disable color
1666 # disable color
1668 ui._colormode = None
1667 ui._colormode = None
1669
1668
1670 try:
1669 try:
1671 keep = opts.get(b'keep')
1670 keep = opts.get(b'keep')
1672 revs = opts.get(b'rev', [])[:]
1671 revs = opts.get(b'rev', [])[:]
1673 cmdutil.checkunfinished(repo)
1672 cmdutil.checkunfinished(repo)
1674 cmdutil.bailifchanged(repo)
1673 cmdutil.bailifchanged(repo)
1675
1674
1676 if os.path.exists(os.path.join(repo.path, b'histedit-state')):
1675 if os.path.exists(os.path.join(repo.path, b'histedit-state')):
1677 raise error.Abort(
1676 raise error.Abort(
1678 _(
1677 _(
1679 b'history edit already in progress, try '
1678 b'history edit already in progress, try '
1680 b'--continue or --abort'
1679 b'--continue or --abort'
1681 )
1680 )
1682 )
1681 )
1683 revs.extend(freeargs)
1682 revs.extend(freeargs)
1684 if not revs:
1683 if not revs:
1685 defaultrev = destutil.desthistedit(ui, repo)
1684 defaultrev = destutil.desthistedit(ui, repo)
1686 if defaultrev is not None:
1685 if defaultrev is not None:
1687 revs.append(defaultrev)
1686 revs.append(defaultrev)
1688 if len(revs) != 1:
1687 if len(revs) != 1:
1689 raise error.Abort(
1688 raise error.Abort(
1690 _(b'histedit requires exactly one ancestor revision')
1689 _(b'histedit requires exactly one ancestor revision')
1691 )
1690 )
1692
1691
1693 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
1692 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
1694 if len(rr) != 1:
1693 if len(rr) != 1:
1695 raise error.Abort(
1694 raise error.Abort(
1696 _(
1695 _(
1697 b'The specified revisions must have '
1696 b'The specified revisions must have '
1698 b'exactly one common root'
1697 b'exactly one common root'
1699 )
1698 )
1700 )
1699 )
1701 root = rr[0].node()
1700 root = rr[0].node()
1702
1701
1703 topmost = repo.dirstate.p1()
1702 topmost = repo.dirstate.p1()
1704 revs = between(repo, root, topmost, keep)
1703 revs = between(repo, root, topmost, keep)
1705 if not revs:
1704 if not revs:
1706 raise error.Abort(
1705 raise error.Abort(
1707 _(b'%s is not an ancestor of working directory')
1706 _(b'%s is not an ancestor of working directory')
1708 % node.short(root)
1707 % node.short(root)
1709 )
1708 )
1710
1709
1711 ctxs = []
1710 ctxs = []
1712 for i, r in enumerate(revs):
1711 for i, r in enumerate(revs):
1713 ctxs.append(histeditrule(ui, repo[r], i))
1712 ctxs.append(histeditrule(ui, repo[r], i))
1714 # Curses requires setting the locale or it will default to the C
1715 # locale. This sets the locale to the user's default system
1716 # locale.
1717 locale.setlocale(locale.LC_ALL, '')
1718 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1713 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1719 curses.echo()
1714 curses.echo()
1720 curses.endwin()
1715 curses.endwin()
1721 if rc is False:
1716 if rc is False:
1722 ui.write(_(b"histedit aborted\n"))
1717 ui.write(_(b"histedit aborted\n"))
1723 return 0
1718 return 0
1724 if type(rc) is list:
1719 if type(rc) is list:
1725 ui.status(_(b"performing changes\n"))
1720 ui.status(_(b"performing changes\n"))
1726 rules = makecommands(rc)
1721 rules = makecommands(rc)
1727 with repo.vfs(b'chistedit', b'w+') as fp:
1722 with repo.vfs(b'chistedit', b'w+') as fp:
1728 for r in rules:
1723 for r in rules:
1729 fp.write(r)
1724 fp.write(r)
1730 opts[b'commands'] = fp.name
1725 opts[b'commands'] = fp.name
1731 return _texthistedit(ui, repo, freeargs, opts)
1726 return _texthistedit(ui, repo, freeargs, opts)
1732 except KeyboardInterrupt:
1727 except KeyboardInterrupt:
1733 pass
1728 pass
1734 return -1
1729 return -1
1735
1730
1736
1731
1737 @command(
1732 @command(
1738 b'histedit',
1733 b'histedit',
1739 [
1734 [
1740 (
1735 (
1741 b'',
1736 b'',
1742 b'commands',
1737 b'commands',
1743 b'',
1738 b'',
1744 _(b'read history edits from the specified file'),
1739 _(b'read history edits from the specified file'),
1745 _(b'FILE'),
1740 _(b'FILE'),
1746 ),
1741 ),
1747 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1742 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1748 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1743 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1749 (
1744 (
1750 b'k',
1745 b'k',
1751 b'keep',
1746 b'keep',
1752 False,
1747 False,
1753 _(b"don't strip old nodes after edit is complete"),
1748 _(b"don't strip old nodes after edit is complete"),
1754 ),
1749 ),
1755 (b'', b'abort', False, _(b'abort an edit in progress')),
1750 (b'', b'abort', False, _(b'abort an edit in progress')),
1756 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1751 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1757 (
1752 (
1758 b'f',
1753 b'f',
1759 b'force',
1754 b'force',
1760 False,
1755 False,
1761 _(b'force outgoing even for unrelated repositories'),
1756 _(b'force outgoing even for unrelated repositories'),
1762 ),
1757 ),
1763 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1758 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1764 ]
1759 ]
1765 + cmdutil.formatteropts,
1760 + cmdutil.formatteropts,
1766 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1761 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1767 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1762 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1768 )
1763 )
1769 def histedit(ui, repo, *freeargs, **opts):
1764 def histedit(ui, repo, *freeargs, **opts):
1770 """interactively edit changeset history
1765 """interactively edit changeset history
1771
1766
1772 This command lets you edit a linear series of changesets (up to
1767 This command lets you edit a linear series of changesets (up to
1773 and including the working directory, which should be clean).
1768 and including the working directory, which should be clean).
1774 You can:
1769 You can:
1775
1770
1776 - `pick` to [re]order a changeset
1771 - `pick` to [re]order a changeset
1777
1772
1778 - `drop` to omit changeset
1773 - `drop` to omit changeset
1779
1774
1780 - `mess` to reword the changeset commit message
1775 - `mess` to reword the changeset commit message
1781
1776
1782 - `fold` to combine it with the preceding changeset (using the later date)
1777 - `fold` to combine it with the preceding changeset (using the later date)
1783
1778
1784 - `roll` like fold, but discarding this commit's description and date
1779 - `roll` like fold, but discarding this commit's description and date
1785
1780
1786 - `edit` to edit this changeset (preserving date)
1781 - `edit` to edit this changeset (preserving date)
1787
1782
1788 - `base` to checkout changeset and apply further changesets from there
1783 - `base` to checkout changeset and apply further changesets from there
1789
1784
1790 There are a number of ways to select the root changeset:
1785 There are a number of ways to select the root changeset:
1791
1786
1792 - Specify ANCESTOR directly
1787 - Specify ANCESTOR directly
1793
1788
1794 - Use --outgoing -- it will be the first linear changeset not
1789 - Use --outgoing -- it will be the first linear changeset not
1795 included in destination. (See :hg:`help config.paths.default-push`)
1790 included in destination. (See :hg:`help config.paths.default-push`)
1796
1791
1797 - Otherwise, the value from the "histedit.defaultrev" config option
1792 - Otherwise, the value from the "histedit.defaultrev" config option
1798 is used as a revset to select the base revision when ANCESTOR is not
1793 is used as a revset to select the base revision when ANCESTOR is not
1799 specified. The first revision returned by the revset is used. By
1794 specified. The first revision returned by the revset is used. By
1800 default, this selects the editable history that is unique to the
1795 default, this selects the editable history that is unique to the
1801 ancestry of the working directory.
1796 ancestry of the working directory.
1802
1797
1803 .. container:: verbose
1798 .. container:: verbose
1804
1799
1805 If you use --outgoing, this command will abort if there are ambiguous
1800 If you use --outgoing, this command will abort if there are ambiguous
1806 outgoing revisions. For example, if there are multiple branches
1801 outgoing revisions. For example, if there are multiple branches
1807 containing outgoing revisions.
1802 containing outgoing revisions.
1808
1803
1809 Use "min(outgoing() and ::.)" or similar revset specification
1804 Use "min(outgoing() and ::.)" or similar revset specification
1810 instead of --outgoing to specify edit target revision exactly in
1805 instead of --outgoing to specify edit target revision exactly in
1811 such ambiguous situation. See :hg:`help revsets` for detail about
1806 such ambiguous situation. See :hg:`help revsets` for detail about
1812 selecting revisions.
1807 selecting revisions.
1813
1808
1814 .. container:: verbose
1809 .. container:: verbose
1815
1810
1816 Examples:
1811 Examples:
1817
1812
1818 - A number of changes have been made.
1813 - A number of changes have been made.
1819 Revision 3 is no longer needed.
1814 Revision 3 is no longer needed.
1820
1815
1821 Start history editing from revision 3::
1816 Start history editing from revision 3::
1822
1817
1823 hg histedit -r 3
1818 hg histedit -r 3
1824
1819
1825 An editor opens, containing the list of revisions,
1820 An editor opens, containing the list of revisions,
1826 with specific actions specified::
1821 with specific actions specified::
1827
1822
1828 pick 5339bf82f0ca 3 Zworgle the foobar
1823 pick 5339bf82f0ca 3 Zworgle the foobar
1829 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1824 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1830 pick 0a9639fcda9d 5 Morgify the cromulancy
1825 pick 0a9639fcda9d 5 Morgify the cromulancy
1831
1826
1832 Additional information about the possible actions
1827 Additional information about the possible actions
1833 to take appears below the list of revisions.
1828 to take appears below the list of revisions.
1834
1829
1835 To remove revision 3 from the history,
1830 To remove revision 3 from the history,
1836 its action (at the beginning of the relevant line)
1831 its action (at the beginning of the relevant line)
1837 is changed to 'drop'::
1832 is changed to 'drop'::
1838
1833
1839 drop 5339bf82f0ca 3 Zworgle the foobar
1834 drop 5339bf82f0ca 3 Zworgle the foobar
1840 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1835 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1841 pick 0a9639fcda9d 5 Morgify the cromulancy
1836 pick 0a9639fcda9d 5 Morgify the cromulancy
1842
1837
1843 - A number of changes have been made.
1838 - A number of changes have been made.
1844 Revision 2 and 4 need to be swapped.
1839 Revision 2 and 4 need to be swapped.
1845
1840
1846 Start history editing from revision 2::
1841 Start history editing from revision 2::
1847
1842
1848 hg histedit -r 2
1843 hg histedit -r 2
1849
1844
1850 An editor opens, containing the list of revisions,
1845 An editor opens, containing the list of revisions,
1851 with specific actions specified::
1846 with specific actions specified::
1852
1847
1853 pick 252a1af424ad 2 Blorb a morgwazzle
1848 pick 252a1af424ad 2 Blorb a morgwazzle
1854 pick 5339bf82f0ca 3 Zworgle the foobar
1849 pick 5339bf82f0ca 3 Zworgle the foobar
1855 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1850 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1856
1851
1857 To swap revision 2 and 4, its lines are swapped
1852 To swap revision 2 and 4, its lines are swapped
1858 in the editor::
1853 in the editor::
1859
1854
1860 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1855 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1861 pick 5339bf82f0ca 3 Zworgle the foobar
1856 pick 5339bf82f0ca 3 Zworgle the foobar
1862 pick 252a1af424ad 2 Blorb a morgwazzle
1857 pick 252a1af424ad 2 Blorb a morgwazzle
1863
1858
1864 Returns 0 on success, 1 if user intervention is required (not only
1859 Returns 0 on success, 1 if user intervention is required (not only
1865 for intentional "edit" command, but also for resolving unexpected
1860 for intentional "edit" command, but also for resolving unexpected
1866 conflicts).
1861 conflicts).
1867 """
1862 """
1868 opts = pycompat.byteskwargs(opts)
1863 opts = pycompat.byteskwargs(opts)
1869
1864
1870 # kludge: _chistedit only works for starting an edit, not aborting
1865 # kludge: _chistedit only works for starting an edit, not aborting
1871 # or continuing, so fall back to regular _texthistedit for those
1866 # or continuing, so fall back to regular _texthistedit for those
1872 # operations.
1867 # operations.
1873 if ui.interface(b'histedit') == b'curses' and _getgoal(opts) == goalnew:
1868 if ui.interface(b'histedit') == b'curses' and _getgoal(opts) == goalnew:
1874 return _chistedit(ui, repo, freeargs, opts)
1869 return _chistedit(ui, repo, freeargs, opts)
1875 return _texthistedit(ui, repo, freeargs, opts)
1870 return _texthistedit(ui, repo, freeargs, opts)
1876
1871
1877
1872
1878 def _texthistedit(ui, repo, freeargs, opts):
1873 def _texthistedit(ui, repo, freeargs, opts):
1879 state = histeditstate(repo)
1874 state = histeditstate(repo)
1880 with repo.wlock() as wlock, repo.lock() as lock:
1875 with repo.wlock() as wlock, repo.lock() as lock:
1881 state.wlock = wlock
1876 state.wlock = wlock
1882 state.lock = lock
1877 state.lock = lock
1883 _histedit(ui, repo, state, freeargs, opts)
1878 _histedit(ui, repo, state, freeargs, opts)
1884
1879
1885
1880
1886 goalcontinue = b'continue'
1881 goalcontinue = b'continue'
1887 goalabort = b'abort'
1882 goalabort = b'abort'
1888 goaleditplan = b'edit-plan'
1883 goaleditplan = b'edit-plan'
1889 goalnew = b'new'
1884 goalnew = b'new'
1890
1885
1891
1886
1892 def _getgoal(opts):
1887 def _getgoal(opts):
1893 if opts.get(b'continue'):
1888 if opts.get(b'continue'):
1894 return goalcontinue
1889 return goalcontinue
1895 if opts.get(b'abort'):
1890 if opts.get(b'abort'):
1896 return goalabort
1891 return goalabort
1897 if opts.get(b'edit_plan'):
1892 if opts.get(b'edit_plan'):
1898 return goaleditplan
1893 return goaleditplan
1899 return goalnew
1894 return goalnew
1900
1895
1901
1896
1902 def _readfile(ui, path):
1897 def _readfile(ui, path):
1903 if path == b'-':
1898 if path == b'-':
1904 with ui.timeblockedsection(b'histedit'):
1899 with ui.timeblockedsection(b'histedit'):
1905 return ui.fin.read()
1900 return ui.fin.read()
1906 else:
1901 else:
1907 with open(path, b'rb') as f:
1902 with open(path, b'rb') as f:
1908 return f.read()
1903 return f.read()
1909
1904
1910
1905
1911 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1906 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1912 # TODO only abort if we try to histedit mq patches, not just
1907 # TODO only abort if we try to histedit mq patches, not just
1913 # blanket if mq patches are applied somewhere
1908 # blanket if mq patches are applied somewhere
1914 mq = getattr(repo, 'mq', None)
1909 mq = getattr(repo, 'mq', None)
1915 if mq and mq.applied:
1910 if mq and mq.applied:
1916 raise error.Abort(_(b'source has mq patches applied'))
1911 raise error.Abort(_(b'source has mq patches applied'))
1917
1912
1918 # basic argument incompatibility processing
1913 # basic argument incompatibility processing
1919 outg = opts.get(b'outgoing')
1914 outg = opts.get(b'outgoing')
1920 editplan = opts.get(b'edit_plan')
1915 editplan = opts.get(b'edit_plan')
1921 abort = opts.get(b'abort')
1916 abort = opts.get(b'abort')
1922 force = opts.get(b'force')
1917 force = opts.get(b'force')
1923 if force and not outg:
1918 if force and not outg:
1924 raise error.Abort(_(b'--force only allowed with --outgoing'))
1919 raise error.Abort(_(b'--force only allowed with --outgoing'))
1925 if goal == b'continue':
1920 if goal == b'continue':
1926 if any((outg, abort, revs, freeargs, rules, editplan)):
1921 if any((outg, abort, revs, freeargs, rules, editplan)):
1927 raise error.Abort(_(b'no arguments allowed with --continue'))
1922 raise error.Abort(_(b'no arguments allowed with --continue'))
1928 elif goal == b'abort':
1923 elif goal == b'abort':
1929 if any((outg, revs, freeargs, rules, editplan)):
1924 if any((outg, revs, freeargs, rules, editplan)):
1930 raise error.Abort(_(b'no arguments allowed with --abort'))
1925 raise error.Abort(_(b'no arguments allowed with --abort'))
1931 elif goal == b'edit-plan':
1926 elif goal == b'edit-plan':
1932 if any((outg, revs, freeargs)):
1927 if any((outg, revs, freeargs)):
1933 raise error.Abort(
1928 raise error.Abort(
1934 _(b'only --commands argument allowed with --edit-plan')
1929 _(b'only --commands argument allowed with --edit-plan')
1935 )
1930 )
1936 else:
1931 else:
1937 if state.inprogress():
1932 if state.inprogress():
1938 raise error.Abort(
1933 raise error.Abort(
1939 _(
1934 _(
1940 b'history edit already in progress, try '
1935 b'history edit already in progress, try '
1941 b'--continue or --abort'
1936 b'--continue or --abort'
1942 )
1937 )
1943 )
1938 )
1944 if outg:
1939 if outg:
1945 if revs:
1940 if revs:
1946 raise error.Abort(_(b'no revisions allowed with --outgoing'))
1941 raise error.Abort(_(b'no revisions allowed with --outgoing'))
1947 if len(freeargs) > 1:
1942 if len(freeargs) > 1:
1948 raise error.Abort(
1943 raise error.Abort(
1949 _(b'only one repo argument allowed with --outgoing')
1944 _(b'only one repo argument allowed with --outgoing')
1950 )
1945 )
1951 else:
1946 else:
1952 revs.extend(freeargs)
1947 revs.extend(freeargs)
1953 if len(revs) == 0:
1948 if len(revs) == 0:
1954 defaultrev = destutil.desthistedit(ui, repo)
1949 defaultrev = destutil.desthistedit(ui, repo)
1955 if defaultrev is not None:
1950 if defaultrev is not None:
1956 revs.append(defaultrev)
1951 revs.append(defaultrev)
1957
1952
1958 if len(revs) != 1:
1953 if len(revs) != 1:
1959 raise error.Abort(
1954 raise error.Abort(
1960 _(b'histedit requires exactly one ancestor revision')
1955 _(b'histedit requires exactly one ancestor revision')
1961 )
1956 )
1962
1957
1963
1958
1964 def _histedit(ui, repo, state, freeargs, opts):
1959 def _histedit(ui, repo, state, freeargs, opts):
1965 fm = ui.formatter(b'histedit', opts)
1960 fm = ui.formatter(b'histedit', opts)
1966 fm.startitem()
1961 fm.startitem()
1967 goal = _getgoal(opts)
1962 goal = _getgoal(opts)
1968 revs = opts.get(b'rev', [])
1963 revs = opts.get(b'rev', [])
1969 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
1964 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
1970 rules = opts.get(b'commands', b'')
1965 rules = opts.get(b'commands', b'')
1971 state.keep = opts.get(b'keep', False)
1966 state.keep = opts.get(b'keep', False)
1972
1967
1973 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1968 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1974
1969
1975 hastags = False
1970 hastags = False
1976 if revs:
1971 if revs:
1977 revs = scmutil.revrange(repo, revs)
1972 revs = scmutil.revrange(repo, revs)
1978 ctxs = [repo[rev] for rev in revs]
1973 ctxs = [repo[rev] for rev in revs]
1979 for ctx in ctxs:
1974 for ctx in ctxs:
1980 tags = [tag for tag in ctx.tags() if tag != b'tip']
1975 tags = [tag for tag in ctx.tags() if tag != b'tip']
1981 if not hastags:
1976 if not hastags:
1982 hastags = len(tags)
1977 hastags = len(tags)
1983 if hastags:
1978 if hastags:
1984 if ui.promptchoice(
1979 if ui.promptchoice(
1985 _(
1980 _(
1986 b'warning: tags associated with the given'
1981 b'warning: tags associated with the given'
1987 b' changeset will be lost after histedit.\n'
1982 b' changeset will be lost after histedit.\n'
1988 b'do you want to continue (yN)? $$ &Yes $$ &No'
1983 b'do you want to continue (yN)? $$ &Yes $$ &No'
1989 ),
1984 ),
1990 default=1,
1985 default=1,
1991 ):
1986 ):
1992 raise error.Abort(_(b'histedit cancelled\n'))
1987 raise error.Abort(_(b'histedit cancelled\n'))
1993 # rebuild state
1988 # rebuild state
1994 if goal == goalcontinue:
1989 if goal == goalcontinue:
1995 state.read()
1990 state.read()
1996 state = bootstrapcontinue(ui, state, opts)
1991 state = bootstrapcontinue(ui, state, opts)
1997 elif goal == goaleditplan:
1992 elif goal == goaleditplan:
1998 _edithisteditplan(ui, repo, state, rules)
1993 _edithisteditplan(ui, repo, state, rules)
1999 return
1994 return
2000 elif goal == goalabort:
1995 elif goal == goalabort:
2001 _aborthistedit(ui, repo, state, nobackup=nobackup)
1996 _aborthistedit(ui, repo, state, nobackup=nobackup)
2002 return
1997 return
2003 else:
1998 else:
2004 # goal == goalnew
1999 # goal == goalnew
2005 _newhistedit(ui, repo, state, revs, freeargs, opts)
2000 _newhistedit(ui, repo, state, revs, freeargs, opts)
2006
2001
2007 _continuehistedit(ui, repo, state)
2002 _continuehistedit(ui, repo, state)
2008 _finishhistedit(ui, repo, state, fm)
2003 _finishhistedit(ui, repo, state, fm)
2009 fm.end()
2004 fm.end()
2010
2005
2011
2006
2012 def _continuehistedit(ui, repo, state):
2007 def _continuehistedit(ui, repo, state):
2013 """This function runs after either:
2008 """This function runs after either:
2014 - bootstrapcontinue (if the goal is 'continue')
2009 - bootstrapcontinue (if the goal is 'continue')
2015 - _newhistedit (if the goal is 'new')
2010 - _newhistedit (if the goal is 'new')
2016 """
2011 """
2017 # preprocess rules so that we can hide inner folds from the user
2012 # preprocess rules so that we can hide inner folds from the user
2018 # and only show one editor
2013 # and only show one editor
2019 actions = state.actions[:]
2014 actions = state.actions[:]
2020 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2015 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2021 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2016 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2022 state.actions[idx].__class__ = _multifold
2017 state.actions[idx].__class__ = _multifold
2023
2018
2024 # Force an initial state file write, so the user can run --abort/continue
2019 # Force an initial state file write, so the user can run --abort/continue
2025 # even if there's an exception before the first transaction serialize.
2020 # even if there's an exception before the first transaction serialize.
2026 state.write()
2021 state.write()
2027
2022
2028 tr = None
2023 tr = None
2029 # Don't use singletransaction by default since it rolls the entire
2024 # Don't use singletransaction by default since it rolls the entire
2030 # transaction back if an unexpected exception happens (like a
2025 # transaction back if an unexpected exception happens (like a
2031 # pretxncommit hook throws, or the user aborts the commit msg editor).
2026 # pretxncommit hook throws, or the user aborts the commit msg editor).
2032 if ui.configbool(b"histedit", b"singletransaction"):
2027 if ui.configbool(b"histedit", b"singletransaction"):
2033 # Don't use a 'with' for the transaction, since actions may close
2028 # Don't use a 'with' for the transaction, since actions may close
2034 # and reopen a transaction. For example, if the action executes an
2029 # and reopen a transaction. For example, if the action executes an
2035 # external process it may choose to commit the transaction first.
2030 # external process it may choose to commit the transaction first.
2036 tr = repo.transaction(b'histedit')
2031 tr = repo.transaction(b'histedit')
2037 progress = ui.makeprogress(
2032 progress = ui.makeprogress(
2038 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2033 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2039 )
2034 )
2040 with progress, util.acceptintervention(tr):
2035 with progress, util.acceptintervention(tr):
2041 while state.actions:
2036 while state.actions:
2042 state.write(tr=tr)
2037 state.write(tr=tr)
2043 actobj = state.actions[0]
2038 actobj = state.actions[0]
2044 progress.increment(item=actobj.torule())
2039 progress.increment(item=actobj.torule())
2045 ui.debug(
2040 ui.debug(
2046 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2041 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2047 )
2042 )
2048 parentctx, replacement_ = actobj.run()
2043 parentctx, replacement_ = actobj.run()
2049 state.parentctxnode = parentctx.node()
2044 state.parentctxnode = parentctx.node()
2050 state.replacements.extend(replacement_)
2045 state.replacements.extend(replacement_)
2051 state.actions.pop(0)
2046 state.actions.pop(0)
2052
2047
2053 state.write()
2048 state.write()
2054
2049
2055
2050
2056 def _finishhistedit(ui, repo, state, fm):
2051 def _finishhistedit(ui, repo, state, fm):
2057 """This action runs when histedit is finishing its session"""
2052 """This action runs when histedit is finishing its session"""
2058 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
2053 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
2059
2054
2060 mapping, tmpnodes, created, ntm = processreplacement(state)
2055 mapping, tmpnodes, created, ntm = processreplacement(state)
2061 if mapping:
2056 if mapping:
2062 for prec, succs in pycompat.iteritems(mapping):
2057 for prec, succs in pycompat.iteritems(mapping):
2063 if not succs:
2058 if not succs:
2064 ui.debug(b'histedit: %s is dropped\n' % node.short(prec))
2059 ui.debug(b'histedit: %s is dropped\n' % node.short(prec))
2065 else:
2060 else:
2066 ui.debug(
2061 ui.debug(
2067 b'histedit: %s is replaced by %s\n'
2062 b'histedit: %s is replaced by %s\n'
2068 % (node.short(prec), node.short(succs[0]))
2063 % (node.short(prec), node.short(succs[0]))
2069 )
2064 )
2070 if len(succs) > 1:
2065 if len(succs) > 1:
2071 m = b'histedit: %s'
2066 m = b'histedit: %s'
2072 for n in succs[1:]:
2067 for n in succs[1:]:
2073 ui.debug(m % node.short(n))
2068 ui.debug(m % node.short(n))
2074
2069
2075 if not state.keep:
2070 if not state.keep:
2076 if mapping:
2071 if mapping:
2077 movetopmostbookmarks(repo, state.topmost, ntm)
2072 movetopmostbookmarks(repo, state.topmost, ntm)
2078 # TODO update mq state
2073 # TODO update mq state
2079 else:
2074 else:
2080 mapping = {}
2075 mapping = {}
2081
2076
2082 for n in tmpnodes:
2077 for n in tmpnodes:
2083 if n in repo:
2078 if n in repo:
2084 mapping[n] = ()
2079 mapping[n] = ()
2085
2080
2086 # remove entries about unknown nodes
2081 # remove entries about unknown nodes
2087 has_node = repo.unfiltered().changelog.index.has_node
2082 has_node = repo.unfiltered().changelog.index.has_node
2088 mapping = {
2083 mapping = {
2089 k: v
2084 k: v
2090 for k, v in mapping.items()
2085 for k, v in mapping.items()
2091 if has_node(k) and all(has_node(n) for n in v)
2086 if has_node(k) and all(has_node(n) for n in v)
2092 }
2087 }
2093 scmutil.cleanupnodes(repo, mapping, b'histedit')
2088 scmutil.cleanupnodes(repo, mapping, b'histedit')
2094 hf = fm.hexfunc
2089 hf = fm.hexfunc
2095 fl = fm.formatlist
2090 fl = fm.formatlist
2096 fd = fm.formatdict
2091 fd = fm.formatdict
2097 nodechanges = fd(
2092 nodechanges = fd(
2098 {
2093 {
2099 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2094 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2100 for oldn, newn in pycompat.iteritems(mapping)
2095 for oldn, newn in pycompat.iteritems(mapping)
2101 },
2096 },
2102 key=b"oldnode",
2097 key=b"oldnode",
2103 value=b"newnodes",
2098 value=b"newnodes",
2104 )
2099 )
2105 fm.data(nodechanges=nodechanges)
2100 fm.data(nodechanges=nodechanges)
2106
2101
2107 state.clear()
2102 state.clear()
2108 if os.path.exists(repo.sjoin(b'undo')):
2103 if os.path.exists(repo.sjoin(b'undo')):
2109 os.unlink(repo.sjoin(b'undo'))
2104 os.unlink(repo.sjoin(b'undo'))
2110 if repo.vfs.exists(b'histedit-last-edit.txt'):
2105 if repo.vfs.exists(b'histedit-last-edit.txt'):
2111 repo.vfs.unlink(b'histedit-last-edit.txt')
2106 repo.vfs.unlink(b'histedit-last-edit.txt')
2112
2107
2113
2108
2114 def _aborthistedit(ui, repo, state, nobackup=False):
2109 def _aborthistedit(ui, repo, state, nobackup=False):
2115 try:
2110 try:
2116 state.read()
2111 state.read()
2117 __, leafs, tmpnodes, __ = processreplacement(state)
2112 __, leafs, tmpnodes, __ = processreplacement(state)
2118 ui.debug(b'restore wc to old parent %s\n' % node.short(state.topmost))
2113 ui.debug(b'restore wc to old parent %s\n' % node.short(state.topmost))
2119
2114
2120 # Recover our old commits if necessary
2115 # Recover our old commits if necessary
2121 if not state.topmost in repo and state.backupfile:
2116 if not state.topmost in repo and state.backupfile:
2122 backupfile = repo.vfs.join(state.backupfile)
2117 backupfile = repo.vfs.join(state.backupfile)
2123 f = hg.openpath(ui, backupfile)
2118 f = hg.openpath(ui, backupfile)
2124 gen = exchange.readbundle(ui, f, backupfile)
2119 gen = exchange.readbundle(ui, f, backupfile)
2125 with repo.transaction(b'histedit.abort') as tr:
2120 with repo.transaction(b'histedit.abort') as tr:
2126 bundle2.applybundle(
2121 bundle2.applybundle(
2127 repo,
2122 repo,
2128 gen,
2123 gen,
2129 tr,
2124 tr,
2130 source=b'histedit',
2125 source=b'histedit',
2131 url=b'bundle:' + backupfile,
2126 url=b'bundle:' + backupfile,
2132 )
2127 )
2133
2128
2134 os.remove(backupfile)
2129 os.remove(backupfile)
2135
2130
2136 # check whether we should update away
2131 # check whether we should update away
2137 if repo.unfiltered().revs(
2132 if repo.unfiltered().revs(
2138 b'parents() and (%n or %ln::)',
2133 b'parents() and (%n or %ln::)',
2139 state.parentctxnode,
2134 state.parentctxnode,
2140 leafs | tmpnodes,
2135 leafs | tmpnodes,
2141 ):
2136 ):
2142 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2137 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2143 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2138 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2144 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2139 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2145 except Exception:
2140 except Exception:
2146 if state.inprogress():
2141 if state.inprogress():
2147 ui.warn(
2142 ui.warn(
2148 _(
2143 _(
2149 b'warning: encountered an exception during histedit '
2144 b'warning: encountered an exception during histedit '
2150 b'--abort; the repository may not have been completely '
2145 b'--abort; the repository may not have been completely '
2151 b'cleaned up\n'
2146 b'cleaned up\n'
2152 )
2147 )
2153 )
2148 )
2154 raise
2149 raise
2155 finally:
2150 finally:
2156 state.clear()
2151 state.clear()
2157
2152
2158
2153
2159 def hgaborthistedit(ui, repo):
2154 def hgaborthistedit(ui, repo):
2160 state = histeditstate(repo)
2155 state = histeditstate(repo)
2161 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2156 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2162 with repo.wlock() as wlock, repo.lock() as lock:
2157 with repo.wlock() as wlock, repo.lock() as lock:
2163 state.wlock = wlock
2158 state.wlock = wlock
2164 state.lock = lock
2159 state.lock = lock
2165 _aborthistedit(ui, repo, state, nobackup=nobackup)
2160 _aborthistedit(ui, repo, state, nobackup=nobackup)
2166
2161
2167
2162
2168 def _edithisteditplan(ui, repo, state, rules):
2163 def _edithisteditplan(ui, repo, state, rules):
2169 state.read()
2164 state.read()
2170 if not rules:
2165 if not rules:
2171 comment = geteditcomment(
2166 comment = geteditcomment(
2172 ui, node.short(state.parentctxnode), node.short(state.topmost)
2167 ui, node.short(state.parentctxnode), node.short(state.topmost)
2173 )
2168 )
2174 rules = ruleeditor(repo, ui, state.actions, comment)
2169 rules = ruleeditor(repo, ui, state.actions, comment)
2175 else:
2170 else:
2176 rules = _readfile(ui, rules)
2171 rules = _readfile(ui, rules)
2177 actions = parserules(rules, state)
2172 actions = parserules(rules, state)
2178 ctxs = [repo[act.node] for act in state.actions if act.node]
2173 ctxs = [repo[act.node] for act in state.actions if act.node]
2179 warnverifyactions(ui, repo, actions, state, ctxs)
2174 warnverifyactions(ui, repo, actions, state, ctxs)
2180 state.actions = actions
2175 state.actions = actions
2181 state.write()
2176 state.write()
2182
2177
2183
2178
2184 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2179 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2185 outg = opts.get(b'outgoing')
2180 outg = opts.get(b'outgoing')
2186 rules = opts.get(b'commands', b'')
2181 rules = opts.get(b'commands', b'')
2187 force = opts.get(b'force')
2182 force = opts.get(b'force')
2188
2183
2189 cmdutil.checkunfinished(repo)
2184 cmdutil.checkunfinished(repo)
2190 cmdutil.bailifchanged(repo)
2185 cmdutil.bailifchanged(repo)
2191
2186
2192 topmost = repo.dirstate.p1()
2187 topmost = repo.dirstate.p1()
2193 if outg:
2188 if outg:
2194 if freeargs:
2189 if freeargs:
2195 remote = freeargs[0]
2190 remote = freeargs[0]
2196 else:
2191 else:
2197 remote = None
2192 remote = None
2198 root = findoutgoing(ui, repo, remote, force, opts)
2193 root = findoutgoing(ui, repo, remote, force, opts)
2199 else:
2194 else:
2200 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
2195 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
2201 if len(rr) != 1:
2196 if len(rr) != 1:
2202 raise error.Abort(
2197 raise error.Abort(
2203 _(
2198 _(
2204 b'The specified revisions must have '
2199 b'The specified revisions must have '
2205 b'exactly one common root'
2200 b'exactly one common root'
2206 )
2201 )
2207 )
2202 )
2208 root = rr[0].node()
2203 root = rr[0].node()
2209
2204
2210 revs = between(repo, root, topmost, state.keep)
2205 revs = between(repo, root, topmost, state.keep)
2211 if not revs:
2206 if not revs:
2212 raise error.Abort(
2207 raise error.Abort(
2213 _(b'%s is not an ancestor of working directory') % node.short(root)
2208 _(b'%s is not an ancestor of working directory') % node.short(root)
2214 )
2209 )
2215
2210
2216 ctxs = [repo[r] for r in revs]
2211 ctxs = [repo[r] for r in revs]
2217
2212
2218 wctx = repo[None]
2213 wctx = repo[None]
2219 # Please don't ask me why `ancestors` is this value. I figured it
2214 # Please don't ask me why `ancestors` is this value. I figured it
2220 # out with print-debugging, not by actually understanding what the
2215 # out with print-debugging, not by actually understanding what the
2221 # merge code is doing. :(
2216 # merge code is doing. :(
2222 ancs = [repo[b'.']]
2217 ancs = [repo[b'.']]
2223 # Sniff-test to make sure we won't collide with untracked files in
2218 # Sniff-test to make sure we won't collide with untracked files in
2224 # the working directory. If we don't do this, we can get a
2219 # the working directory. If we don't do this, we can get a
2225 # collision after we've started histedit and backing out gets ugly
2220 # collision after we've started histedit and backing out gets ugly
2226 # for everyone, especially the user.
2221 # for everyone, especially the user.
2227 for c in [ctxs[0].p1()] + ctxs:
2222 for c in [ctxs[0].p1()] + ctxs:
2228 try:
2223 try:
2229 mergemod.calculateupdates(
2224 mergemod.calculateupdates(
2230 repo,
2225 repo,
2231 wctx,
2226 wctx,
2232 c,
2227 c,
2233 ancs,
2228 ancs,
2234 # These parameters were determined by print-debugging
2229 # These parameters were determined by print-debugging
2235 # what happens later on inside histedit.
2230 # what happens later on inside histedit.
2236 branchmerge=False,
2231 branchmerge=False,
2237 force=False,
2232 force=False,
2238 acceptremote=False,
2233 acceptremote=False,
2239 followcopies=False,
2234 followcopies=False,
2240 )
2235 )
2241 except error.Abort:
2236 except error.Abort:
2242 raise error.Abort(
2237 raise error.Abort(
2243 _(
2238 _(
2244 b"untracked files in working directory conflict with files in %s"
2239 b"untracked files in working directory conflict with files in %s"
2245 )
2240 )
2246 % c
2241 % c
2247 )
2242 )
2248
2243
2249 if not rules:
2244 if not rules:
2250 comment = geteditcomment(ui, node.short(root), node.short(topmost))
2245 comment = geteditcomment(ui, node.short(root), node.short(topmost))
2251 actions = [pick(state, r) for r in revs]
2246 actions = [pick(state, r) for r in revs]
2252 rules = ruleeditor(repo, ui, actions, comment)
2247 rules = ruleeditor(repo, ui, actions, comment)
2253 else:
2248 else:
2254 rules = _readfile(ui, rules)
2249 rules = _readfile(ui, rules)
2255 actions = parserules(rules, state)
2250 actions = parserules(rules, state)
2256 warnverifyactions(ui, repo, actions, state, ctxs)
2251 warnverifyactions(ui, repo, actions, state, ctxs)
2257
2252
2258 parentctxnode = repo[root].p1().node()
2253 parentctxnode = repo[root].p1().node()
2259
2254
2260 state.parentctxnode = parentctxnode
2255 state.parentctxnode = parentctxnode
2261 state.actions = actions
2256 state.actions = actions
2262 state.topmost = topmost
2257 state.topmost = topmost
2263 state.replacements = []
2258 state.replacements = []
2264
2259
2265 ui.log(
2260 ui.log(
2266 b"histedit",
2261 b"histedit",
2267 b"%d actions to histedit\n",
2262 b"%d actions to histedit\n",
2268 len(actions),
2263 len(actions),
2269 histedit_num_actions=len(actions),
2264 histedit_num_actions=len(actions),
2270 )
2265 )
2271
2266
2272 # Create a backup so we can always abort completely.
2267 # Create a backup so we can always abort completely.
2273 backupfile = None
2268 backupfile = None
2274 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2269 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2275 backupfile = repair.backupbundle(
2270 backupfile = repair.backupbundle(
2276 repo, [parentctxnode], [topmost], root, b'histedit'
2271 repo, [parentctxnode], [topmost], root, b'histedit'
2277 )
2272 )
2278 state.backupfile = backupfile
2273 state.backupfile = backupfile
2279
2274
2280
2275
2281 def _getsummary(ctx):
2276 def _getsummary(ctx):
2282 # a common pattern is to extract the summary but default to the empty
2277 # a common pattern is to extract the summary but default to the empty
2283 # string
2278 # string
2284 summary = ctx.description() or b''
2279 summary = ctx.description() or b''
2285 if summary:
2280 if summary:
2286 summary = summary.splitlines()[0]
2281 summary = summary.splitlines()[0]
2287 return summary
2282 return summary
2288
2283
2289
2284
2290 def bootstrapcontinue(ui, state, opts):
2285 def bootstrapcontinue(ui, state, opts):
2291 repo = state.repo
2286 repo = state.repo
2292
2287
2293 ms = mergestatemod.mergestate.read(repo)
2288 ms = mergestatemod.mergestate.read(repo)
2294 mergeutil.checkunresolved(ms)
2289 mergeutil.checkunresolved(ms)
2295
2290
2296 if state.actions:
2291 if state.actions:
2297 actobj = state.actions.pop(0)
2292 actobj = state.actions.pop(0)
2298
2293
2299 if _isdirtywc(repo):
2294 if _isdirtywc(repo):
2300 actobj.continuedirty()
2295 actobj.continuedirty()
2301 if _isdirtywc(repo):
2296 if _isdirtywc(repo):
2302 abortdirty()
2297 abortdirty()
2303
2298
2304 parentctx, replacements = actobj.continueclean()
2299 parentctx, replacements = actobj.continueclean()
2305
2300
2306 state.parentctxnode = parentctx.node()
2301 state.parentctxnode = parentctx.node()
2307 state.replacements.extend(replacements)
2302 state.replacements.extend(replacements)
2308
2303
2309 return state
2304 return state
2310
2305
2311
2306
2312 def between(repo, old, new, keep):
2307 def between(repo, old, new, keep):
2313 """select and validate the set of revision to edit
2308 """select and validate the set of revision to edit
2314
2309
2315 When keep is false, the specified set can't have children."""
2310 When keep is false, the specified set can't have children."""
2316 revs = repo.revs(b'%n::%n', old, new)
2311 revs = repo.revs(b'%n::%n', old, new)
2317 if revs and not keep:
2312 if revs and not keep:
2318 rewriteutil.precheck(repo, revs, b'edit')
2313 rewriteutil.precheck(repo, revs, b'edit')
2319 if repo.revs(b'(%ld) and merge()', revs):
2314 if repo.revs(b'(%ld) and merge()', revs):
2320 raise error.Abort(_(b'cannot edit history that contains merges'))
2315 raise error.Abort(_(b'cannot edit history that contains merges'))
2321 return pycompat.maplist(repo.changelog.node, revs)
2316 return pycompat.maplist(repo.changelog.node, revs)
2322
2317
2323
2318
2324 def ruleeditor(repo, ui, actions, editcomment=b""):
2319 def ruleeditor(repo, ui, actions, editcomment=b""):
2325 """open an editor to edit rules
2320 """open an editor to edit rules
2326
2321
2327 rules are in the format [ [act, ctx], ...] like in state.rules
2322 rules are in the format [ [act, ctx], ...] like in state.rules
2328 """
2323 """
2329 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2324 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2330 newact = util.sortdict()
2325 newact = util.sortdict()
2331 for act in actions:
2326 for act in actions:
2332 ctx = repo[act.node]
2327 ctx = repo[act.node]
2333 summary = _getsummary(ctx)
2328 summary = _getsummary(ctx)
2334 fword = summary.split(b' ', 1)[0].lower()
2329 fword = summary.split(b' ', 1)[0].lower()
2335 added = False
2330 added = False
2336
2331
2337 # if it doesn't end with the special character '!' just skip this
2332 # if it doesn't end with the special character '!' just skip this
2338 if fword.endswith(b'!'):
2333 if fword.endswith(b'!'):
2339 fword = fword[:-1]
2334 fword = fword[:-1]
2340 if fword in primaryactions | secondaryactions | tertiaryactions:
2335 if fword in primaryactions | secondaryactions | tertiaryactions:
2341 act.verb = fword
2336 act.verb = fword
2342 # get the target summary
2337 # get the target summary
2343 tsum = summary[len(fword) + 1 :].lstrip()
2338 tsum = summary[len(fword) + 1 :].lstrip()
2344 # safe but slow: reverse iterate over the actions so we
2339 # safe but slow: reverse iterate over the actions so we
2345 # don't clash on two commits having the same summary
2340 # don't clash on two commits having the same summary
2346 for na, l in reversed(list(pycompat.iteritems(newact))):
2341 for na, l in reversed(list(pycompat.iteritems(newact))):
2347 actx = repo[na.node]
2342 actx = repo[na.node]
2348 asum = _getsummary(actx)
2343 asum = _getsummary(actx)
2349 if asum == tsum:
2344 if asum == tsum:
2350 added = True
2345 added = True
2351 l.append(act)
2346 l.append(act)
2352 break
2347 break
2353
2348
2354 if not added:
2349 if not added:
2355 newact[act] = []
2350 newact[act] = []
2356
2351
2357 # copy over and flatten the new list
2352 # copy over and flatten the new list
2358 actions = []
2353 actions = []
2359 for na, l in pycompat.iteritems(newact):
2354 for na, l in pycompat.iteritems(newact):
2360 actions.append(na)
2355 actions.append(na)
2361 actions += l
2356 actions += l
2362
2357
2363 rules = b'\n'.join([act.torule() for act in actions])
2358 rules = b'\n'.join([act.torule() for act in actions])
2364 rules += b'\n\n'
2359 rules += b'\n\n'
2365 rules += editcomment
2360 rules += editcomment
2366 rules = ui.edit(
2361 rules = ui.edit(
2367 rules,
2362 rules,
2368 ui.username(),
2363 ui.username(),
2369 {b'prefix': b'histedit'},
2364 {b'prefix': b'histedit'},
2370 repopath=repo.path,
2365 repopath=repo.path,
2371 action=b'histedit',
2366 action=b'histedit',
2372 )
2367 )
2373
2368
2374 # Save edit rules in .hg/histedit-last-edit.txt in case
2369 # Save edit rules in .hg/histedit-last-edit.txt in case
2375 # the user needs to ask for help after something
2370 # the user needs to ask for help after something
2376 # surprising happens.
2371 # surprising happens.
2377 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2372 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2378 f.write(rules)
2373 f.write(rules)
2379
2374
2380 return rules
2375 return rules
2381
2376
2382
2377
2383 def parserules(rules, state):
2378 def parserules(rules, state):
2384 """Read the histedit rules string and return list of action objects """
2379 """Read the histedit rules string and return list of action objects """
2385 rules = [
2380 rules = [
2386 l
2381 l
2387 for l in (r.strip() for r in rules.splitlines())
2382 for l in (r.strip() for r in rules.splitlines())
2388 if l and not l.startswith(b'#')
2383 if l and not l.startswith(b'#')
2389 ]
2384 ]
2390 actions = []
2385 actions = []
2391 for r in rules:
2386 for r in rules:
2392 if b' ' not in r:
2387 if b' ' not in r:
2393 raise error.ParseError(_(b'malformed line "%s"') % r)
2388 raise error.ParseError(_(b'malformed line "%s"') % r)
2394 verb, rest = r.split(b' ', 1)
2389 verb, rest = r.split(b' ', 1)
2395
2390
2396 if verb not in actiontable:
2391 if verb not in actiontable:
2397 raise error.ParseError(_(b'unknown action "%s"') % verb)
2392 raise error.ParseError(_(b'unknown action "%s"') % verb)
2398
2393
2399 action = actiontable[verb].fromrule(state, rest)
2394 action = actiontable[verb].fromrule(state, rest)
2400 actions.append(action)
2395 actions.append(action)
2401 return actions
2396 return actions
2402
2397
2403
2398
2404 def warnverifyactions(ui, repo, actions, state, ctxs):
2399 def warnverifyactions(ui, repo, actions, state, ctxs):
2405 try:
2400 try:
2406 verifyactions(actions, state, ctxs)
2401 verifyactions(actions, state, ctxs)
2407 except error.ParseError:
2402 except error.ParseError:
2408 if repo.vfs.exists(b'histedit-last-edit.txt'):
2403 if repo.vfs.exists(b'histedit-last-edit.txt'):
2409 ui.warn(
2404 ui.warn(
2410 _(
2405 _(
2411 b'warning: histedit rules saved '
2406 b'warning: histedit rules saved '
2412 b'to: .hg/histedit-last-edit.txt\n'
2407 b'to: .hg/histedit-last-edit.txt\n'
2413 )
2408 )
2414 )
2409 )
2415 raise
2410 raise
2416
2411
2417
2412
2418 def verifyactions(actions, state, ctxs):
2413 def verifyactions(actions, state, ctxs):
2419 """Verify that there exists exactly one action per given changeset and
2414 """Verify that there exists exactly one action per given changeset and
2420 other constraints.
2415 other constraints.
2421
2416
2422 Will abort if there are to many or too few rules, a malformed rule,
2417 Will abort if there are to many or too few rules, a malformed rule,
2423 or a rule on a changeset outside of the user-given range.
2418 or a rule on a changeset outside of the user-given range.
2424 """
2419 """
2425 expected = {c.node() for c in ctxs}
2420 expected = {c.node() for c in ctxs}
2426 seen = set()
2421 seen = set()
2427 prev = None
2422 prev = None
2428
2423
2429 if actions and actions[0].verb in [b'roll', b'fold']:
2424 if actions and actions[0].verb in [b'roll', b'fold']:
2430 raise error.ParseError(
2425 raise error.ParseError(
2431 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2426 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2432 )
2427 )
2433
2428
2434 for action in actions:
2429 for action in actions:
2435 action.verify(prev, expected, seen)
2430 action.verify(prev, expected, seen)
2436 prev = action
2431 prev = action
2437 if action.node is not None:
2432 if action.node is not None:
2438 seen.add(action.node)
2433 seen.add(action.node)
2439 missing = sorted(expected - seen) # sort to stabilize output
2434 missing = sorted(expected - seen) # sort to stabilize output
2440
2435
2441 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2436 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2442 if len(actions) == 0:
2437 if len(actions) == 0:
2443 raise error.ParseError(
2438 raise error.ParseError(
2444 _(b'no rules provided'),
2439 _(b'no rules provided'),
2445 hint=_(b'use strip extension to remove commits'),
2440 hint=_(b'use strip extension to remove commits'),
2446 )
2441 )
2447
2442
2448 drops = [drop(state, n) for n in missing]
2443 drops = [drop(state, n) for n in missing]
2449 # put the in the beginning so they execute immediately and
2444 # put the in the beginning so they execute immediately and
2450 # don't show in the edit-plan in the future
2445 # don't show in the edit-plan in the future
2451 actions[:0] = drops
2446 actions[:0] = drops
2452 elif missing:
2447 elif missing:
2453 raise error.ParseError(
2448 raise error.ParseError(
2454 _(b'missing rules for changeset %s') % node.short(missing[0]),
2449 _(b'missing rules for changeset %s') % node.short(missing[0]),
2455 hint=_(
2450 hint=_(
2456 b'use "drop %s" to discard, see also: '
2451 b'use "drop %s" to discard, see also: '
2457 b"'hg help -e histedit.config'"
2452 b"'hg help -e histedit.config'"
2458 )
2453 )
2459 % node.short(missing[0]),
2454 % node.short(missing[0]),
2460 )
2455 )
2461
2456
2462
2457
2463 def adjustreplacementsfrommarkers(repo, oldreplacements):
2458 def adjustreplacementsfrommarkers(repo, oldreplacements):
2464 """Adjust replacements from obsolescence markers
2459 """Adjust replacements from obsolescence markers
2465
2460
2466 Replacements structure is originally generated based on
2461 Replacements structure is originally generated based on
2467 histedit's state and does not account for changes that are
2462 histedit's state and does not account for changes that are
2468 not recorded there. This function fixes that by adding
2463 not recorded there. This function fixes that by adding
2469 data read from obsolescence markers"""
2464 data read from obsolescence markers"""
2470 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2465 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2471 return oldreplacements
2466 return oldreplacements
2472
2467
2473 unfi = repo.unfiltered()
2468 unfi = repo.unfiltered()
2474 get_rev = unfi.changelog.index.get_rev
2469 get_rev = unfi.changelog.index.get_rev
2475 obsstore = repo.obsstore
2470 obsstore = repo.obsstore
2476 newreplacements = list(oldreplacements)
2471 newreplacements = list(oldreplacements)
2477 oldsuccs = [r[1] for r in oldreplacements]
2472 oldsuccs = [r[1] for r in oldreplacements]
2478 # successors that have already been added to succstocheck once
2473 # successors that have already been added to succstocheck once
2479 seensuccs = set().union(
2474 seensuccs = set().union(
2480 *oldsuccs
2475 *oldsuccs
2481 ) # create a set from an iterable of tuples
2476 ) # create a set from an iterable of tuples
2482 succstocheck = list(seensuccs)
2477 succstocheck = list(seensuccs)
2483 while succstocheck:
2478 while succstocheck:
2484 n = succstocheck.pop()
2479 n = succstocheck.pop()
2485 missing = get_rev(n) is None
2480 missing = get_rev(n) is None
2486 markers = obsstore.successors.get(n, ())
2481 markers = obsstore.successors.get(n, ())
2487 if missing and not markers:
2482 if missing and not markers:
2488 # dead end, mark it as such
2483 # dead end, mark it as such
2489 newreplacements.append((n, ()))
2484 newreplacements.append((n, ()))
2490 for marker in markers:
2485 for marker in markers:
2491 nsuccs = marker[1]
2486 nsuccs = marker[1]
2492 newreplacements.append((n, nsuccs))
2487 newreplacements.append((n, nsuccs))
2493 for nsucc in nsuccs:
2488 for nsucc in nsuccs:
2494 if nsucc not in seensuccs:
2489 if nsucc not in seensuccs:
2495 seensuccs.add(nsucc)
2490 seensuccs.add(nsucc)
2496 succstocheck.append(nsucc)
2491 succstocheck.append(nsucc)
2497
2492
2498 return newreplacements
2493 return newreplacements
2499
2494
2500
2495
2501 def processreplacement(state):
2496 def processreplacement(state):
2502 """process the list of replacements to return
2497 """process the list of replacements to return
2503
2498
2504 1) the final mapping between original and created nodes
2499 1) the final mapping between original and created nodes
2505 2) the list of temporary node created by histedit
2500 2) the list of temporary node created by histedit
2506 3) the list of new commit created by histedit"""
2501 3) the list of new commit created by histedit"""
2507 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2502 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2508 allsuccs = set()
2503 allsuccs = set()
2509 replaced = set()
2504 replaced = set()
2510 fullmapping = {}
2505 fullmapping = {}
2511 # initialize basic set
2506 # initialize basic set
2512 # fullmapping records all operations recorded in replacement
2507 # fullmapping records all operations recorded in replacement
2513 for rep in replacements:
2508 for rep in replacements:
2514 allsuccs.update(rep[1])
2509 allsuccs.update(rep[1])
2515 replaced.add(rep[0])
2510 replaced.add(rep[0])
2516 fullmapping.setdefault(rep[0], set()).update(rep[1])
2511 fullmapping.setdefault(rep[0], set()).update(rep[1])
2517 new = allsuccs - replaced
2512 new = allsuccs - replaced
2518 tmpnodes = allsuccs & replaced
2513 tmpnodes = allsuccs & replaced
2519 # Reduce content fullmapping into direct relation between original nodes
2514 # Reduce content fullmapping into direct relation between original nodes
2520 # and final node created during history edition
2515 # and final node created during history edition
2521 # Dropped changeset are replaced by an empty list
2516 # Dropped changeset are replaced by an empty list
2522 toproceed = set(fullmapping)
2517 toproceed = set(fullmapping)
2523 final = {}
2518 final = {}
2524 while toproceed:
2519 while toproceed:
2525 for x in list(toproceed):
2520 for x in list(toproceed):
2526 succs = fullmapping[x]
2521 succs = fullmapping[x]
2527 for s in list(succs):
2522 for s in list(succs):
2528 if s in toproceed:
2523 if s in toproceed:
2529 # non final node with unknown closure
2524 # non final node with unknown closure
2530 # We can't process this now
2525 # We can't process this now
2531 break
2526 break
2532 elif s in final:
2527 elif s in final:
2533 # non final node, replace with closure
2528 # non final node, replace with closure
2534 succs.remove(s)
2529 succs.remove(s)
2535 succs.update(final[s])
2530 succs.update(final[s])
2536 else:
2531 else:
2537 final[x] = succs
2532 final[x] = succs
2538 toproceed.remove(x)
2533 toproceed.remove(x)
2539 # remove tmpnodes from final mapping
2534 # remove tmpnodes from final mapping
2540 for n in tmpnodes:
2535 for n in tmpnodes:
2541 del final[n]
2536 del final[n]
2542 # we expect all changes involved in final to exist in the repo
2537 # we expect all changes involved in final to exist in the repo
2543 # turn `final` into list (topologically sorted)
2538 # turn `final` into list (topologically sorted)
2544 get_rev = state.repo.changelog.index.get_rev
2539 get_rev = state.repo.changelog.index.get_rev
2545 for prec, succs in final.items():
2540 for prec, succs in final.items():
2546 final[prec] = sorted(succs, key=get_rev)
2541 final[prec] = sorted(succs, key=get_rev)
2547
2542
2548 # computed topmost element (necessary for bookmark)
2543 # computed topmost element (necessary for bookmark)
2549 if new:
2544 if new:
2550 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2545 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2551 elif not final:
2546 elif not final:
2552 # Nothing rewritten at all. we won't need `newtopmost`
2547 # Nothing rewritten at all. we won't need `newtopmost`
2553 # It is the same as `oldtopmost` and `processreplacement` know it
2548 # It is the same as `oldtopmost` and `processreplacement` know it
2554 newtopmost = None
2549 newtopmost = None
2555 else:
2550 else:
2556 # every body died. The newtopmost is the parent of the root.
2551 # every body died. The newtopmost is the parent of the root.
2557 r = state.repo.changelog.rev
2552 r = state.repo.changelog.rev
2558 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2553 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2559
2554
2560 return final, tmpnodes, new, newtopmost
2555 return final, tmpnodes, new, newtopmost
2561
2556
2562
2557
2563 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2558 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2564 """Move bookmark from oldtopmost to newly created topmost
2559 """Move bookmark from oldtopmost to newly created topmost
2565
2560
2566 This is arguably a feature and we may only want that for the active
2561 This is arguably a feature and we may only want that for the active
2567 bookmark. But the behavior is kept compatible with the old version for now.
2562 bookmark. But the behavior is kept compatible with the old version for now.
2568 """
2563 """
2569 if not oldtopmost or not newtopmost:
2564 if not oldtopmost or not newtopmost:
2570 return
2565 return
2571 oldbmarks = repo.nodebookmarks(oldtopmost)
2566 oldbmarks = repo.nodebookmarks(oldtopmost)
2572 if oldbmarks:
2567 if oldbmarks:
2573 with repo.lock(), repo.transaction(b'histedit') as tr:
2568 with repo.lock(), repo.transaction(b'histedit') as tr:
2574 marks = repo._bookmarks
2569 marks = repo._bookmarks
2575 changes = []
2570 changes = []
2576 for name in oldbmarks:
2571 for name in oldbmarks:
2577 changes.append((name, newtopmost))
2572 changes.append((name, newtopmost))
2578 marks.applychanges(repo, tr, changes)
2573 marks.applychanges(repo, tr, changes)
2579
2574
2580
2575
2581 def cleanupnode(ui, repo, nodes, nobackup=False):
2576 def cleanupnode(ui, repo, nodes, nobackup=False):
2582 """strip a group of nodes from the repository
2577 """strip a group of nodes from the repository
2583
2578
2584 The set of node to strip may contains unknown nodes."""
2579 The set of node to strip may contains unknown nodes."""
2585 with repo.lock():
2580 with repo.lock():
2586 # do not let filtering get in the way of the cleanse
2581 # do not let filtering get in the way of the cleanse
2587 # we should probably get rid of obsolescence marker created during the
2582 # we should probably get rid of obsolescence marker created during the
2588 # histedit, but we currently do not have such information.
2583 # histedit, but we currently do not have such information.
2589 repo = repo.unfiltered()
2584 repo = repo.unfiltered()
2590 # Find all nodes that need to be stripped
2585 # Find all nodes that need to be stripped
2591 # (we use %lr instead of %ln to silently ignore unknown items)
2586 # (we use %lr instead of %ln to silently ignore unknown items)
2592 has_node = repo.changelog.index.has_node
2587 has_node = repo.changelog.index.has_node
2593 nodes = sorted(n for n in nodes if has_node(n))
2588 nodes = sorted(n for n in nodes if has_node(n))
2594 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2589 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2595 if roots:
2590 if roots:
2596 backup = not nobackup
2591 backup = not nobackup
2597 repair.strip(ui, repo, roots, backup=backup)
2592 repair.strip(ui, repo, roots, backup=backup)
2598
2593
2599
2594
2600 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2595 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2601 if isinstance(nodelist, bytes):
2596 if isinstance(nodelist, bytes):
2602 nodelist = [nodelist]
2597 nodelist = [nodelist]
2603 state = histeditstate(repo)
2598 state = histeditstate(repo)
2604 if state.inprogress():
2599 if state.inprogress():
2605 state.read()
2600 state.read()
2606 histedit_nodes = {
2601 histedit_nodes = {
2607 action.node for action in state.actions if action.node
2602 action.node for action in state.actions if action.node
2608 }
2603 }
2609 common_nodes = histedit_nodes & set(nodelist)
2604 common_nodes = histedit_nodes & set(nodelist)
2610 if common_nodes:
2605 if common_nodes:
2611 raise error.Abort(
2606 raise error.Abort(
2612 _(b"histedit in progress, can't strip %s")
2607 _(b"histedit in progress, can't strip %s")
2613 % b', '.join(node.short(x) for x in common_nodes)
2608 % b', '.join(node.short(x) for x in common_nodes)
2614 )
2609 )
2615 return orig(ui, repo, nodelist, *args, **kwargs)
2610 return orig(ui, repo, nodelist, *args, **kwargs)
2616
2611
2617
2612
2618 extensions.wrapfunction(repair, b'strip', stripwrapper)
2613 extensions.wrapfunction(repair, b'strip', stripwrapper)
2619
2614
2620
2615
2621 def summaryhook(ui, repo):
2616 def summaryhook(ui, repo):
2622 state = histeditstate(repo)
2617 state = histeditstate(repo)
2623 if not state.inprogress():
2618 if not state.inprogress():
2624 return
2619 return
2625 state.read()
2620 state.read()
2626 if state.actions:
2621 if state.actions:
2627 # i18n: column positioning for "hg summary"
2622 # i18n: column positioning for "hg summary"
2628 ui.write(
2623 ui.write(
2629 _(b'hist: %s (histedit --continue)\n')
2624 _(b'hist: %s (histedit --continue)\n')
2630 % (
2625 % (
2631 ui.label(_(b'%d remaining'), b'histedit.remaining')
2626 ui.label(_(b'%d remaining'), b'histedit.remaining')
2632 % len(state.actions)
2627 % len(state.actions)
2633 )
2628 )
2634 )
2629 )
2635
2630
2636
2631
2637 def extsetup(ui):
2632 def extsetup(ui):
2638 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2633 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2639 statemod.addunfinished(
2634 statemod.addunfinished(
2640 b'histedit',
2635 b'histedit',
2641 fname=b'histedit-state',
2636 fname=b'histedit-state',
2642 allowcommit=True,
2637 allowcommit=True,
2643 continueflag=True,
2638 continueflag=True,
2644 abortfunc=hgaborthistedit,
2639 abortfunc=hgaborthistedit,
2645 )
2640 )
@@ -1,2037 +1,2033
1 # stuff related specifically to patch manipulation / parsing
1 # stuff related specifically to patch manipulation / parsing
2 #
2 #
3 # Copyright 2008 Mark Edgington <edgimar@gmail.com>
3 # Copyright 2008 Mark Edgington <edgimar@gmail.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 #
7 #
8 # This code is based on the Mark Edgington's crecord extension.
8 # This code is based on the Mark Edgington's crecord extension.
9 # (Itself based on Bryan O'Sullivan's record extension.)
9 # (Itself based on Bryan O'Sullivan's record extension.)
10
10
11 from __future__ import absolute_import
11 from __future__ import absolute_import
12
12
13 import locale
14 import os
13 import os
15 import re
14 import re
16 import signal
15 import signal
17
16
18 from .i18n import _
17 from .i18n import _
19 from .pycompat import (
18 from .pycompat import (
20 getattr,
19 getattr,
21 open,
20 open,
22 )
21 )
23 from . import (
22 from . import (
24 encoding,
23 encoding,
25 error,
24 error,
26 patch as patchmod,
25 patch as patchmod,
27 pycompat,
26 pycompat,
28 scmutil,
27 scmutil,
29 util,
28 util,
30 )
29 )
31 from .utils import stringutil
30 from .utils import stringutil
32
31
33 stringio = util.stringio
32 stringio = util.stringio
34
33
35 # patch comments based on the git one
34 # patch comments based on the git one
36 diffhelptext = _(
35 diffhelptext = _(
37 """# To remove '-' lines, make them ' ' lines (context).
36 """# To remove '-' lines, make them ' ' lines (context).
38 # To remove '+' lines, delete them.
37 # To remove '+' lines, delete them.
39 # Lines starting with # will be removed from the patch.
38 # Lines starting with # will be removed from the patch.
40 """
39 """
41 )
40 )
42
41
43 hunkhelptext = _(
42 hunkhelptext = _(
44 """#
43 """#
45 # If the patch applies cleanly, the edited hunk will immediately be
44 # If the patch applies cleanly, the edited hunk will immediately be
46 # added to the record list. If it does not apply cleanly, a rejects file
45 # added to the record list. If it does not apply cleanly, a rejects file
47 # will be generated. You can use that when you try again. If all lines
46 # will be generated. You can use that when you try again. If all lines
48 # of the hunk are removed, then the edit is aborted and the hunk is left
47 # of the hunk are removed, then the edit is aborted and the hunk is left
49 # unchanged.
48 # unchanged.
50 """
49 """
51 )
50 )
52
51
53 patchhelptext = _(
52 patchhelptext = _(
54 """#
53 """#
55 # If the patch applies cleanly, the edited patch will immediately
54 # If the patch applies cleanly, the edited patch will immediately
56 # be finalised. If it does not apply cleanly, rejects files will be
55 # be finalised. If it does not apply cleanly, rejects files will be
57 # generated. You can use those when you try again.
56 # generated. You can use those when you try again.
58 """
57 """
59 )
58 )
60
59
61 try:
60 try:
62 import curses
61 import curses
63 import curses.ascii
62 import curses.ascii
64
63
65 curses.error
64 curses.error
66 except (ImportError, AttributeError):
65 except (ImportError, AttributeError):
67 # I have no idea if wcurses works with crecord...
66 # I have no idea if wcurses works with crecord...
68 try:
67 try:
69 import wcurses as curses
68 import wcurses as curses
70
69
71 curses.error
70 curses.error
72 except (ImportError, AttributeError):
71 except (ImportError, AttributeError):
73 # wcurses is not shipped on Windows by default, or python is not
72 # wcurses is not shipped on Windows by default, or python is not
74 # compiled with curses
73 # compiled with curses
75 curses = False
74 curses = False
76
75
77
76
78 class fallbackerror(error.Abort):
77 class fallbackerror(error.Abort):
79 """Error that indicates the client should try to fallback to text mode."""
78 """Error that indicates the client should try to fallback to text mode."""
80
79
81 # Inherits from error.Abort so that existing behavior is preserved if the
80 # Inherits from error.Abort so that existing behavior is preserved if the
82 # calling code does not know how to fallback.
81 # calling code does not know how to fallback.
83
82
84
83
85 def checkcurses(ui):
84 def checkcurses(ui):
86 """Return True if the user wants to use curses
85 """Return True if the user wants to use curses
87
86
88 This method returns True if curses is found (and that python is built with
87 This method returns True if curses is found (and that python is built with
89 it) and that the user has the correct flag for the ui.
88 it) and that the user has the correct flag for the ui.
90 """
89 """
91 return curses and ui.interface(b"chunkselector") == b"curses"
90 return curses and ui.interface(b"chunkselector") == b"curses"
92
91
93
92
94 class patchnode(object):
93 class patchnode(object):
95 """abstract class for patch graph nodes
94 """abstract class for patch graph nodes
96 (i.e. patchroot, header, hunk, hunkline)
95 (i.e. patchroot, header, hunk, hunkline)
97 """
96 """
98
97
99 def firstchild(self):
98 def firstchild(self):
100 raise NotImplementedError(b"method must be implemented by subclass")
99 raise NotImplementedError(b"method must be implemented by subclass")
101
100
102 def lastchild(self):
101 def lastchild(self):
103 raise NotImplementedError(b"method must be implemented by subclass")
102 raise NotImplementedError(b"method must be implemented by subclass")
104
103
105 def allchildren(self):
104 def allchildren(self):
106 """Return a list of all of the direct children of this node"""
105 """Return a list of all of the direct children of this node"""
107 raise NotImplementedError(b"method must be implemented by subclass")
106 raise NotImplementedError(b"method must be implemented by subclass")
108
107
109 def nextsibling(self):
108 def nextsibling(self):
110 """
109 """
111 Return the closest next item of the same type where there are no items
110 Return the closest next item of the same type where there are no items
112 of different types between the current item and this closest item.
111 of different types between the current item and this closest item.
113 If no such item exists, return None.
112 If no such item exists, return None.
114 """
113 """
115 raise NotImplementedError(b"method must be implemented by subclass")
114 raise NotImplementedError(b"method must be implemented by subclass")
116
115
117 def prevsibling(self):
116 def prevsibling(self):
118 """
117 """
119 Return the closest previous item of the same type where there are no
118 Return the closest previous item of the same type where there are no
120 items of different types between the current item and this closest item.
119 items of different types between the current item and this closest item.
121 If no such item exists, return None.
120 If no such item exists, return None.
122 """
121 """
123 raise NotImplementedError(b"method must be implemented by subclass")
122 raise NotImplementedError(b"method must be implemented by subclass")
124
123
125 def parentitem(self):
124 def parentitem(self):
126 raise NotImplementedError(b"method must be implemented by subclass")
125 raise NotImplementedError(b"method must be implemented by subclass")
127
126
128 def nextitem(self, skipfolded=True):
127 def nextitem(self, skipfolded=True):
129 """
128 """
130 Try to return the next item closest to this item, regardless of item's
129 Try to return the next item closest to this item, regardless of item's
131 type (header, hunk, or hunkline).
130 type (header, hunk, or hunkline).
132
131
133 If skipfolded == True, and the current item is folded, then the child
132 If skipfolded == True, and the current item is folded, then the child
134 items that are hidden due to folding will be skipped when determining
133 items that are hidden due to folding will be skipped when determining
135 the next item.
134 the next item.
136
135
137 If it is not possible to get the next item, return None.
136 If it is not possible to get the next item, return None.
138 """
137 """
139 try:
138 try:
140 itemfolded = self.folded
139 itemfolded = self.folded
141 except AttributeError:
140 except AttributeError:
142 itemfolded = False
141 itemfolded = False
143 if skipfolded and itemfolded:
142 if skipfolded and itemfolded:
144 nextitem = self.nextsibling()
143 nextitem = self.nextsibling()
145 if nextitem is None:
144 if nextitem is None:
146 try:
145 try:
147 nextitem = self.parentitem().nextsibling()
146 nextitem = self.parentitem().nextsibling()
148 except AttributeError:
147 except AttributeError:
149 nextitem = None
148 nextitem = None
150 return nextitem
149 return nextitem
151 else:
150 else:
152 # try child
151 # try child
153 item = self.firstchild()
152 item = self.firstchild()
154 if item is not None:
153 if item is not None:
155 return item
154 return item
156
155
157 # else try next sibling
156 # else try next sibling
158 item = self.nextsibling()
157 item = self.nextsibling()
159 if item is not None:
158 if item is not None:
160 return item
159 return item
161
160
162 try:
161 try:
163 # else try parent's next sibling
162 # else try parent's next sibling
164 item = self.parentitem().nextsibling()
163 item = self.parentitem().nextsibling()
165 if item is not None:
164 if item is not None:
166 return item
165 return item
167
166
168 # else return grandparent's next sibling (or None)
167 # else return grandparent's next sibling (or None)
169 return self.parentitem().parentitem().nextsibling()
168 return self.parentitem().parentitem().nextsibling()
170
169
171 except AttributeError: # parent and/or grandparent was None
170 except AttributeError: # parent and/or grandparent was None
172 return None
171 return None
173
172
174 def previtem(self):
173 def previtem(self):
175 """
174 """
176 Try to return the previous item closest to this item, regardless of
175 Try to return the previous item closest to this item, regardless of
177 item's type (header, hunk, or hunkline).
176 item's type (header, hunk, or hunkline).
178
177
179 If it is not possible to get the previous item, return None.
178 If it is not possible to get the previous item, return None.
180 """
179 """
181 # try previous sibling's last child's last child,
180 # try previous sibling's last child's last child,
182 # else try previous sibling's last child, else try previous sibling
181 # else try previous sibling's last child, else try previous sibling
183 prevsibling = self.prevsibling()
182 prevsibling = self.prevsibling()
184 if prevsibling is not None:
183 if prevsibling is not None:
185 prevsiblinglastchild = prevsibling.lastchild()
184 prevsiblinglastchild = prevsibling.lastchild()
186 if (prevsiblinglastchild is not None) and not prevsibling.folded:
185 if (prevsiblinglastchild is not None) and not prevsibling.folded:
187 prevsiblinglclc = prevsiblinglastchild.lastchild()
186 prevsiblinglclc = prevsiblinglastchild.lastchild()
188 if (
187 if (
189 prevsiblinglclc is not None
188 prevsiblinglclc is not None
190 ) and not prevsiblinglastchild.folded:
189 ) and not prevsiblinglastchild.folded:
191 return prevsiblinglclc
190 return prevsiblinglclc
192 else:
191 else:
193 return prevsiblinglastchild
192 return prevsiblinglastchild
194 else:
193 else:
195 return prevsibling
194 return prevsibling
196
195
197 # try parent (or None)
196 # try parent (or None)
198 return self.parentitem()
197 return self.parentitem()
199
198
200
199
201 class patch(patchnode, list): # todo: rename patchroot
200 class patch(patchnode, list): # todo: rename patchroot
202 """
201 """
203 list of header objects representing the patch.
202 list of header objects representing the patch.
204 """
203 """
205
204
206 def __init__(self, headerlist):
205 def __init__(self, headerlist):
207 self.extend(headerlist)
206 self.extend(headerlist)
208 # add parent patch object reference to each header
207 # add parent patch object reference to each header
209 for header in self:
208 for header in self:
210 header.patch = self
209 header.patch = self
211
210
212
211
213 class uiheader(patchnode):
212 class uiheader(patchnode):
214 """patch header
213 """patch header
215
214
216 xxx shouldn't we move this to mercurial/patch.py ?
215 xxx shouldn't we move this to mercurial/patch.py ?
217 """
216 """
218
217
219 def __init__(self, header):
218 def __init__(self, header):
220 self.nonuiheader = header
219 self.nonuiheader = header
221 # flag to indicate whether to apply this chunk
220 # flag to indicate whether to apply this chunk
222 self.applied = True
221 self.applied = True
223 # flag which only affects the status display indicating if a node's
222 # flag which only affects the status display indicating if a node's
224 # children are partially applied (i.e. some applied, some not).
223 # children are partially applied (i.e. some applied, some not).
225 self.partial = False
224 self.partial = False
226
225
227 # flag to indicate whether to display as folded/unfolded to user
226 # flag to indicate whether to display as folded/unfolded to user
228 self.folded = True
227 self.folded = True
229
228
230 # list of all headers in patch
229 # list of all headers in patch
231 self.patch = None
230 self.patch = None
232
231
233 # flag is False if this header was ever unfolded from initial state
232 # flag is False if this header was ever unfolded from initial state
234 self.neverunfolded = True
233 self.neverunfolded = True
235 self.hunks = [uihunk(h, self) for h in self.hunks]
234 self.hunks = [uihunk(h, self) for h in self.hunks]
236
235
237 def prettystr(self):
236 def prettystr(self):
238 x = stringio()
237 x = stringio()
239 self.pretty(x)
238 self.pretty(x)
240 return x.getvalue()
239 return x.getvalue()
241
240
242 def nextsibling(self):
241 def nextsibling(self):
243 numheadersinpatch = len(self.patch)
242 numheadersinpatch = len(self.patch)
244 indexofthisheader = self.patch.index(self)
243 indexofthisheader = self.patch.index(self)
245
244
246 if indexofthisheader < numheadersinpatch - 1:
245 if indexofthisheader < numheadersinpatch - 1:
247 nextheader = self.patch[indexofthisheader + 1]
246 nextheader = self.patch[indexofthisheader + 1]
248 return nextheader
247 return nextheader
249 else:
248 else:
250 return None
249 return None
251
250
252 def prevsibling(self):
251 def prevsibling(self):
253 indexofthisheader = self.patch.index(self)
252 indexofthisheader = self.patch.index(self)
254 if indexofthisheader > 0:
253 if indexofthisheader > 0:
255 previousheader = self.patch[indexofthisheader - 1]
254 previousheader = self.patch[indexofthisheader - 1]
256 return previousheader
255 return previousheader
257 else:
256 else:
258 return None
257 return None
259
258
260 def parentitem(self):
259 def parentitem(self):
261 """
260 """
262 there is no 'real' parent item of a header that can be selected,
261 there is no 'real' parent item of a header that can be selected,
263 so return None.
262 so return None.
264 """
263 """
265 return None
264 return None
266
265
267 def firstchild(self):
266 def firstchild(self):
268 """return the first child of this item, if one exists. otherwise
267 """return the first child of this item, if one exists. otherwise
269 None."""
268 None."""
270 if len(self.hunks) > 0:
269 if len(self.hunks) > 0:
271 return self.hunks[0]
270 return self.hunks[0]
272 else:
271 else:
273 return None
272 return None
274
273
275 def lastchild(self):
274 def lastchild(self):
276 """return the last child of this item, if one exists. otherwise
275 """return the last child of this item, if one exists. otherwise
277 None."""
276 None."""
278 if len(self.hunks) > 0:
277 if len(self.hunks) > 0:
279 return self.hunks[-1]
278 return self.hunks[-1]
280 else:
279 else:
281 return None
280 return None
282
281
283 def allchildren(self):
282 def allchildren(self):
284 """return a list of all of the direct children of this node"""
283 """return a list of all of the direct children of this node"""
285 return self.hunks
284 return self.hunks
286
285
287 def __getattr__(self, name):
286 def __getattr__(self, name):
288 return getattr(self.nonuiheader, name)
287 return getattr(self.nonuiheader, name)
289
288
290
289
291 class uihunkline(patchnode):
290 class uihunkline(patchnode):
292 """represents a changed line in a hunk"""
291 """represents a changed line in a hunk"""
293
292
294 def __init__(self, linetext, hunk):
293 def __init__(self, linetext, hunk):
295 self.linetext = linetext
294 self.linetext = linetext
296 self.applied = True
295 self.applied = True
297 # the parent hunk to which this line belongs
296 # the parent hunk to which this line belongs
298 self.hunk = hunk
297 self.hunk = hunk
299 # folding lines currently is not used/needed, but this flag is needed
298 # folding lines currently is not used/needed, but this flag is needed
300 # in the previtem method.
299 # in the previtem method.
301 self.folded = False
300 self.folded = False
302
301
303 def prettystr(self):
302 def prettystr(self):
304 return self.linetext
303 return self.linetext
305
304
306 def nextsibling(self):
305 def nextsibling(self):
307 numlinesinhunk = len(self.hunk.changedlines)
306 numlinesinhunk = len(self.hunk.changedlines)
308 indexofthisline = self.hunk.changedlines.index(self)
307 indexofthisline = self.hunk.changedlines.index(self)
309
308
310 if indexofthisline < numlinesinhunk - 1:
309 if indexofthisline < numlinesinhunk - 1:
311 nextline = self.hunk.changedlines[indexofthisline + 1]
310 nextline = self.hunk.changedlines[indexofthisline + 1]
312 return nextline
311 return nextline
313 else:
312 else:
314 return None
313 return None
315
314
316 def prevsibling(self):
315 def prevsibling(self):
317 indexofthisline = self.hunk.changedlines.index(self)
316 indexofthisline = self.hunk.changedlines.index(self)
318 if indexofthisline > 0:
317 if indexofthisline > 0:
319 previousline = self.hunk.changedlines[indexofthisline - 1]
318 previousline = self.hunk.changedlines[indexofthisline - 1]
320 return previousline
319 return previousline
321 else:
320 else:
322 return None
321 return None
323
322
324 def parentitem(self):
323 def parentitem(self):
325 """return the parent to the current item"""
324 """return the parent to the current item"""
326 return self.hunk
325 return self.hunk
327
326
328 def firstchild(self):
327 def firstchild(self):
329 """return the first child of this item, if one exists. otherwise
328 """return the first child of this item, if one exists. otherwise
330 None."""
329 None."""
331 # hunk-lines don't have children
330 # hunk-lines don't have children
332 return None
331 return None
333
332
334 def lastchild(self):
333 def lastchild(self):
335 """return the last child of this item, if one exists. otherwise
334 """return the last child of this item, if one exists. otherwise
336 None."""
335 None."""
337 # hunk-lines don't have children
336 # hunk-lines don't have children
338 return None
337 return None
339
338
340
339
341 class uihunk(patchnode):
340 class uihunk(patchnode):
342 """ui patch hunk, wraps a hunk and keep track of ui behavior """
341 """ui patch hunk, wraps a hunk and keep track of ui behavior """
343
342
344 maxcontext = 3
343 maxcontext = 3
345
344
346 def __init__(self, hunk, header):
345 def __init__(self, hunk, header):
347 self._hunk = hunk
346 self._hunk = hunk
348 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
347 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
349 self.header = header
348 self.header = header
350 # used at end for detecting how many removed lines were un-applied
349 # used at end for detecting how many removed lines were un-applied
351 self.originalremoved = self.removed
350 self.originalremoved = self.removed
352
351
353 # flag to indicate whether to display as folded/unfolded to user
352 # flag to indicate whether to display as folded/unfolded to user
354 self.folded = True
353 self.folded = True
355 # flag to indicate whether to apply this chunk
354 # flag to indicate whether to apply this chunk
356 self.applied = True
355 self.applied = True
357 # flag which only affects the status display indicating if a node's
356 # flag which only affects the status display indicating if a node's
358 # children are partially applied (i.e. some applied, some not).
357 # children are partially applied (i.e. some applied, some not).
359 self.partial = False
358 self.partial = False
360
359
361 def nextsibling(self):
360 def nextsibling(self):
362 numhunksinheader = len(self.header.hunks)
361 numhunksinheader = len(self.header.hunks)
363 indexofthishunk = self.header.hunks.index(self)
362 indexofthishunk = self.header.hunks.index(self)
364
363
365 if indexofthishunk < numhunksinheader - 1:
364 if indexofthishunk < numhunksinheader - 1:
366 nexthunk = self.header.hunks[indexofthishunk + 1]
365 nexthunk = self.header.hunks[indexofthishunk + 1]
367 return nexthunk
366 return nexthunk
368 else:
367 else:
369 return None
368 return None
370
369
371 def prevsibling(self):
370 def prevsibling(self):
372 indexofthishunk = self.header.hunks.index(self)
371 indexofthishunk = self.header.hunks.index(self)
373 if indexofthishunk > 0:
372 if indexofthishunk > 0:
374 previoushunk = self.header.hunks[indexofthishunk - 1]
373 previoushunk = self.header.hunks[indexofthishunk - 1]
375 return previoushunk
374 return previoushunk
376 else:
375 else:
377 return None
376 return None
378
377
379 def parentitem(self):
378 def parentitem(self):
380 """return the parent to the current item"""
379 """return the parent to the current item"""
381 return self.header
380 return self.header
382
381
383 def firstchild(self):
382 def firstchild(self):
384 """return the first child of this item, if one exists. otherwise
383 """return the first child of this item, if one exists. otherwise
385 None."""
384 None."""
386 if len(self.changedlines) > 0:
385 if len(self.changedlines) > 0:
387 return self.changedlines[0]
386 return self.changedlines[0]
388 else:
387 else:
389 return None
388 return None
390
389
391 def lastchild(self):
390 def lastchild(self):
392 """return the last child of this item, if one exists. otherwise
391 """return the last child of this item, if one exists. otherwise
393 None."""
392 None."""
394 if len(self.changedlines) > 0:
393 if len(self.changedlines) > 0:
395 return self.changedlines[-1]
394 return self.changedlines[-1]
396 else:
395 else:
397 return None
396 return None
398
397
399 def allchildren(self):
398 def allchildren(self):
400 """return a list of all of the direct children of this node"""
399 """return a list of all of the direct children of this node"""
401 return self.changedlines
400 return self.changedlines
402
401
403 def countchanges(self):
402 def countchanges(self):
404 """changedlines -> (n+,n-)"""
403 """changedlines -> (n+,n-)"""
405 add = len(
404 add = len(
406 [
405 [
407 l
406 l
408 for l in self.changedlines
407 for l in self.changedlines
409 if l.applied and l.prettystr().startswith(b'+')
408 if l.applied and l.prettystr().startswith(b'+')
410 ]
409 ]
411 )
410 )
412 rem = len(
411 rem = len(
413 [
412 [
414 l
413 l
415 for l in self.changedlines
414 for l in self.changedlines
416 if l.applied and l.prettystr().startswith(b'-')
415 if l.applied and l.prettystr().startswith(b'-')
417 ]
416 ]
418 )
417 )
419 return add, rem
418 return add, rem
420
419
421 def getfromtoline(self):
420 def getfromtoline(self):
422 # calculate the number of removed lines converted to context lines
421 # calculate the number of removed lines converted to context lines
423 removedconvertedtocontext = self.originalremoved - self.removed
422 removedconvertedtocontext = self.originalremoved - self.removed
424
423
425 contextlen = (
424 contextlen = (
426 len(self.before) + len(self.after) + removedconvertedtocontext
425 len(self.before) + len(self.after) + removedconvertedtocontext
427 )
426 )
428 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
427 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
429 contextlen -= 1
428 contextlen -= 1
430 fromlen = contextlen + self.removed
429 fromlen = contextlen + self.removed
431 tolen = contextlen + self.added
430 tolen = contextlen + self.added
432
431
433 # diffutils manual, section "2.2.2.2 detailed description of unified
432 # diffutils manual, section "2.2.2.2 detailed description of unified
434 # format": "an empty hunk is considered to end at the line that
433 # format": "an empty hunk is considered to end at the line that
435 # precedes the hunk."
434 # precedes the hunk."
436 #
435 #
437 # so, if either of hunks is empty, decrease its line start. --immerrr
436 # so, if either of hunks is empty, decrease its line start. --immerrr
438 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
437 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
439 fromline, toline = self.fromline, self.toline
438 fromline, toline = self.fromline, self.toline
440 if fromline != 0:
439 if fromline != 0:
441 if fromlen == 0:
440 if fromlen == 0:
442 fromline -= 1
441 fromline -= 1
443 if tolen == 0 and toline > 0:
442 if tolen == 0 and toline > 0:
444 toline -= 1
443 toline -= 1
445
444
446 fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % (
445 fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % (
447 fromline,
446 fromline,
448 fromlen,
447 fromlen,
449 toline,
448 toline,
450 tolen,
449 tolen,
451 self.proc and (b' ' + self.proc),
450 self.proc and (b' ' + self.proc),
452 )
451 )
453 return fromtoline
452 return fromtoline
454
453
455 def write(self, fp):
454 def write(self, fp):
456 # updated self.added/removed, which are used by getfromtoline()
455 # updated self.added/removed, which are used by getfromtoline()
457 self.added, self.removed = self.countchanges()
456 self.added, self.removed = self.countchanges()
458 fp.write(self.getfromtoline())
457 fp.write(self.getfromtoline())
459
458
460 hunklinelist = []
459 hunklinelist = []
461 # add the following to the list: (1) all applied lines, and
460 # add the following to the list: (1) all applied lines, and
462 # (2) all unapplied removal lines (convert these to context lines)
461 # (2) all unapplied removal lines (convert these to context lines)
463 for changedline in self.changedlines:
462 for changedline in self.changedlines:
464 changedlinestr = changedline.prettystr()
463 changedlinestr = changedline.prettystr()
465 if changedline.applied:
464 if changedline.applied:
466 hunklinelist.append(changedlinestr)
465 hunklinelist.append(changedlinestr)
467 elif changedlinestr.startswith(b"-"):
466 elif changedlinestr.startswith(b"-"):
468 hunklinelist.append(b" " + changedlinestr[1:])
467 hunklinelist.append(b" " + changedlinestr[1:])
469
468
470 fp.write(b''.join(self.before + hunklinelist + self.after))
469 fp.write(b''.join(self.before + hunklinelist + self.after))
471
470
472 pretty = write
471 pretty = write
473
472
474 def prettystr(self):
473 def prettystr(self):
475 x = stringio()
474 x = stringio()
476 self.pretty(x)
475 self.pretty(x)
477 return x.getvalue()
476 return x.getvalue()
478
477
479 def reversehunk(self):
478 def reversehunk(self):
480 """return a recordhunk which is the reverse of the hunk
479 """return a recordhunk which is the reverse of the hunk
481
480
482 Assuming the displayed patch is diff(A, B) result. The returned hunk is
481 Assuming the displayed patch is diff(A, B) result. The returned hunk is
483 intended to be applied to B, instead of A.
482 intended to be applied to B, instead of A.
484
483
485 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
484 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
486 the user made the following selection:
485 the user made the following selection:
487
486
488 0
487 0
489 [x] -1 [x]: selected
488 [x] -1 [x]: selected
490 [ ] -2 [ ]: not selected
489 [ ] -2 [ ]: not selected
491 [x] +3
490 [x] +3
492 [ ] +4
491 [ ] +4
493 [x] +5
492 [x] +5
494 6
493 6
495
494
496 This function returns a hunk like:
495 This function returns a hunk like:
497
496
498 0
497 0
499 -3
498 -3
500 -4
499 -4
501 -5
500 -5
502 +1
501 +1
503 +4
502 +4
504 6
503 6
505
504
506 Note "4" was first deleted then added. That's because "4" exists in B
505 Note "4" was first deleted then added. That's because "4" exists in B
507 side and "-4" must exist between "-3" and "-5" to make the patch
506 side and "-4" must exist between "-3" and "-5" to make the patch
508 applicable to B.
507 applicable to B.
509 """
508 """
510 dels = []
509 dels = []
511 adds = []
510 adds = []
512 for line in self.changedlines:
511 for line in self.changedlines:
513 text = line.linetext
512 text = line.linetext
514 if line.applied:
513 if line.applied:
515 if text.startswith(b'+'):
514 if text.startswith(b'+'):
516 dels.append(text[1:])
515 dels.append(text[1:])
517 elif text.startswith(b'-'):
516 elif text.startswith(b'-'):
518 adds.append(text[1:])
517 adds.append(text[1:])
519 elif text.startswith(b'+'):
518 elif text.startswith(b'+'):
520 dels.append(text[1:])
519 dels.append(text[1:])
521 adds.append(text[1:])
520 adds.append(text[1:])
522 hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds]
521 hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds]
523 h = self._hunk
522 h = self._hunk
524 return patchmod.recordhunk(
523 return patchmod.recordhunk(
525 h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
524 h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
526 )
525 )
527
526
528 def __getattr__(self, name):
527 def __getattr__(self, name):
529 return getattr(self._hunk, name)
528 return getattr(self._hunk, name)
530
529
531 def __repr__(self):
530 def __repr__(self):
532 return '<hunk %r@%d>' % (self.filename(), self.fromline)
531 return '<hunk %r@%d>' % (self.filename(), self.fromline)
533
532
534
533
535 def filterpatch(ui, chunks, chunkselector, operation=None):
534 def filterpatch(ui, chunks, chunkselector, operation=None):
536 """interactively filter patch chunks into applied-only chunks"""
535 """interactively filter patch chunks into applied-only chunks"""
537 chunks = list(chunks)
536 chunks = list(chunks)
538 # convert chunks list into structure suitable for displaying/modifying
537 # convert chunks list into structure suitable for displaying/modifying
539 # with curses. create a list of headers only.
538 # with curses. create a list of headers only.
540 headers = [c for c in chunks if isinstance(c, patchmod.header)]
539 headers = [c for c in chunks if isinstance(c, patchmod.header)]
541
540
542 # if there are no changed files
541 # if there are no changed files
543 if len(headers) == 0:
542 if len(headers) == 0:
544 return [], {}
543 return [], {}
545 uiheaders = [uiheader(h) for h in headers]
544 uiheaders = [uiheader(h) for h in headers]
546 # let user choose headers/hunks/lines, and mark their applied flags
545 # let user choose headers/hunks/lines, and mark their applied flags
547 # accordingly
546 # accordingly
548 ret = chunkselector(ui, uiheaders, operation=operation)
547 ret = chunkselector(ui, uiheaders, operation=operation)
549 appliedhunklist = []
548 appliedhunklist = []
550 for hdr in uiheaders:
549 for hdr in uiheaders:
551 if hdr.applied and (
550 if hdr.applied and (
552 hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
551 hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
553 ):
552 ):
554 appliedhunklist.append(hdr)
553 appliedhunklist.append(hdr)
555 fixoffset = 0
554 fixoffset = 0
556 for hnk in hdr.hunks:
555 for hnk in hdr.hunks:
557 if hnk.applied:
556 if hnk.applied:
558 appliedhunklist.append(hnk)
557 appliedhunklist.append(hnk)
559 # adjust the 'to'-line offset of the hunk to be correct
558 # adjust the 'to'-line offset of the hunk to be correct
560 # after de-activating some of the other hunks for this file
559 # after de-activating some of the other hunks for this file
561 if fixoffset:
560 if fixoffset:
562 # hnk = copy.copy(hnk) # necessary??
561 # hnk = copy.copy(hnk) # necessary??
563 hnk.toline += fixoffset
562 hnk.toline += fixoffset
564 else:
563 else:
565 fixoffset += hnk.removed - hnk.added
564 fixoffset += hnk.removed - hnk.added
566
565
567 return (appliedhunklist, ret)
566 return (appliedhunklist, ret)
568
567
569
568
570 def chunkselector(ui, headerlist, operation=None):
569 def chunkselector(ui, headerlist, operation=None):
571 """
570 """
572 curses interface to get selection of chunks, and mark the applied flags
571 curses interface to get selection of chunks, and mark the applied flags
573 of the chosen chunks.
572 of the chosen chunks.
574 """
573 """
575 ui.write(_(b'starting interactive selection\n'))
574 ui.write(_(b'starting interactive selection\n'))
576 chunkselector = curseschunkselector(headerlist, ui, operation)
575 chunkselector = curseschunkselector(headerlist, ui, operation)
577 # This is required for ncurses to display non-ASCII characters in
578 # default user locale encoding correctly. --immerrr
579 locale.setlocale(locale.LC_ALL, '')
580 origsigtstp = sentinel = object()
576 origsigtstp = sentinel = object()
581 if util.safehasattr(signal, b'SIGTSTP'):
577 if util.safehasattr(signal, b'SIGTSTP'):
582 origsigtstp = signal.getsignal(signal.SIGTSTP)
578 origsigtstp = signal.getsignal(signal.SIGTSTP)
583 try:
579 try:
584 curses.wrapper(chunkselector.main)
580 curses.wrapper(chunkselector.main)
585 if chunkselector.initexc is not None:
581 if chunkselector.initexc is not None:
586 raise chunkselector.initexc
582 raise chunkselector.initexc
587 # ncurses does not restore signal handler for SIGTSTP
583 # ncurses does not restore signal handler for SIGTSTP
588 finally:
584 finally:
589 if origsigtstp is not sentinel:
585 if origsigtstp is not sentinel:
590 signal.signal(signal.SIGTSTP, origsigtstp)
586 signal.signal(signal.SIGTSTP, origsigtstp)
591 return chunkselector.opts
587 return chunkselector.opts
592
588
593
589
594 def testdecorator(testfn, f):
590 def testdecorator(testfn, f):
595 def u(*args, **kwargs):
591 def u(*args, **kwargs):
596 return f(testfn, *args, **kwargs)
592 return f(testfn, *args, **kwargs)
597
593
598 return u
594 return u
599
595
600
596
601 def testchunkselector(testfn, ui, headerlist, operation=None):
597 def testchunkselector(testfn, ui, headerlist, operation=None):
602 """
598 """
603 test interface to get selection of chunks, and mark the applied flags
599 test interface to get selection of chunks, and mark the applied flags
604 of the chosen chunks.
600 of the chosen chunks.
605 """
601 """
606 chunkselector = curseschunkselector(headerlist, ui, operation)
602 chunkselector = curseschunkselector(headerlist, ui, operation)
607
603
608 class dummystdscr(object):
604 class dummystdscr(object):
609 def clear(self):
605 def clear(self):
610 pass
606 pass
611
607
612 def refresh(self):
608 def refresh(self):
613 pass
609 pass
614
610
615 chunkselector.stdscr = dummystdscr()
611 chunkselector.stdscr = dummystdscr()
616 if testfn and os.path.exists(testfn):
612 if testfn and os.path.exists(testfn):
617 testf = open(testfn, 'r')
613 testf = open(testfn, 'r')
618 testcommands = [x.rstrip('\n') for x in testf.readlines()]
614 testcommands = [x.rstrip('\n') for x in testf.readlines()]
619 testf.close()
615 testf.close()
620 while True:
616 while True:
621 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
617 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
622 break
618 break
623 return chunkselector.opts
619 return chunkselector.opts
624
620
625
621
626 _headermessages = { # {operation: text}
622 _headermessages = { # {operation: text}
627 b'apply': _(b'Select hunks to apply'),
623 b'apply': _(b'Select hunks to apply'),
628 b'discard': _(b'Select hunks to discard'),
624 b'discard': _(b'Select hunks to discard'),
629 b'keep': _(b'Select hunks to keep'),
625 b'keep': _(b'Select hunks to keep'),
630 None: _(b'Select hunks to record'),
626 None: _(b'Select hunks to record'),
631 }
627 }
632
628
633
629
634 class curseschunkselector(object):
630 class curseschunkselector(object):
635 def __init__(self, headerlist, ui, operation=None):
631 def __init__(self, headerlist, ui, operation=None):
636 # put the headers into a patch object
632 # put the headers into a patch object
637 self.headerlist = patch(headerlist)
633 self.headerlist = patch(headerlist)
638
634
639 self.ui = ui
635 self.ui = ui
640 self.opts = {}
636 self.opts = {}
641
637
642 self.errorstr = None
638 self.errorstr = None
643 # list of all chunks
639 # list of all chunks
644 self.chunklist = []
640 self.chunklist = []
645 for h in headerlist:
641 for h in headerlist:
646 self.chunklist.append(h)
642 self.chunklist.append(h)
647 self.chunklist.extend(h.hunks)
643 self.chunklist.extend(h.hunks)
648
644
649 # dictionary mapping (fgcolor, bgcolor) pairs to the
645 # dictionary mapping (fgcolor, bgcolor) pairs to the
650 # corresponding curses color-pair value.
646 # corresponding curses color-pair value.
651 self.colorpairs = {}
647 self.colorpairs = {}
652 # maps custom nicknames of color-pairs to curses color-pair values
648 # maps custom nicknames of color-pairs to curses color-pair values
653 self.colorpairnames = {}
649 self.colorpairnames = {}
654
650
655 # Honor color setting of ui section. Keep colored setup as
651 # Honor color setting of ui section. Keep colored setup as
656 # long as not explicitly set to a falsy value - especially,
652 # long as not explicitly set to a falsy value - especially,
657 # when not set at all. This is to stay most compatible with
653 # when not set at all. This is to stay most compatible with
658 # previous (color only) behaviour.
654 # previous (color only) behaviour.
659 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
655 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
660 self.usecolor = uicolor is not False
656 self.usecolor = uicolor is not False
661
657
662 # the currently selected header, hunk, or hunk-line
658 # the currently selected header, hunk, or hunk-line
663 self.currentselecteditem = self.headerlist[0]
659 self.currentselecteditem = self.headerlist[0]
664 self.lastapplieditem = None
660 self.lastapplieditem = None
665
661
666 # updated when printing out patch-display -- the 'lines' here are the
662 # updated when printing out patch-display -- the 'lines' here are the
667 # line positions *in the pad*, not on the screen.
663 # line positions *in the pad*, not on the screen.
668 self.selecteditemstartline = 0
664 self.selecteditemstartline = 0
669 self.selecteditemendline = None
665 self.selecteditemendline = None
670
666
671 # define indentation levels
667 # define indentation levels
672 self.headerindentnumchars = 0
668 self.headerindentnumchars = 0
673 self.hunkindentnumchars = 3
669 self.hunkindentnumchars = 3
674 self.hunklineindentnumchars = 6
670 self.hunklineindentnumchars = 6
675
671
676 # the first line of the pad to print to the screen
672 # the first line of the pad to print to the screen
677 self.firstlineofpadtoprint = 0
673 self.firstlineofpadtoprint = 0
678
674
679 # keeps track of the number of lines in the pad
675 # keeps track of the number of lines in the pad
680 self.numpadlines = None
676 self.numpadlines = None
681
677
682 self.numstatuslines = 1
678 self.numstatuslines = 1
683
679
684 # keep a running count of the number of lines printed to the pad
680 # keep a running count of the number of lines printed to the pad
685 # (used for determining when the selected item begins/ends)
681 # (used for determining when the selected item begins/ends)
686 self.linesprintedtopadsofar = 0
682 self.linesprintedtopadsofar = 0
687
683
688 # stores optional text for a commit comment provided by the user
684 # stores optional text for a commit comment provided by the user
689 self.commenttext = b""
685 self.commenttext = b""
690
686
691 # if the last 'toggle all' command caused all changes to be applied
687 # if the last 'toggle all' command caused all changes to be applied
692 self.waslasttoggleallapplied = True
688 self.waslasttoggleallapplied = True
693
689
694 # affects some ui text
690 # affects some ui text
695 if operation not in _headermessages:
691 if operation not in _headermessages:
696 raise error.ProgrammingError(
692 raise error.ProgrammingError(
697 b'unexpected operation: %s' % operation
693 b'unexpected operation: %s' % operation
698 )
694 )
699 self.operation = operation
695 self.operation = operation
700
696
701 def uparrowevent(self):
697 def uparrowevent(self):
702 """
698 """
703 try to select the previous item to the current item that has the
699 try to select the previous item to the current item that has the
704 most-indented level. for example, if a hunk is selected, try to select
700 most-indented level. for example, if a hunk is selected, try to select
705 the last hunkline of the hunk prior to the selected hunk. or, if
701 the last hunkline of the hunk prior to the selected hunk. or, if
706 the first hunkline of a hunk is currently selected, then select the
702 the first hunkline of a hunk is currently selected, then select the
707 hunk itself.
703 hunk itself.
708 """
704 """
709 currentitem = self.currentselecteditem
705 currentitem = self.currentselecteditem
710
706
711 nextitem = currentitem.previtem()
707 nextitem = currentitem.previtem()
712
708
713 if nextitem is None:
709 if nextitem is None:
714 # if no parent item (i.e. currentitem is the first header), then
710 # if no parent item (i.e. currentitem is the first header), then
715 # no change...
711 # no change...
716 nextitem = currentitem
712 nextitem = currentitem
717
713
718 self.currentselecteditem = nextitem
714 self.currentselecteditem = nextitem
719
715
720 def uparrowshiftevent(self):
716 def uparrowshiftevent(self):
721 """
717 """
722 select (if possible) the previous item on the same level as the
718 select (if possible) the previous item on the same level as the
723 currently selected item. otherwise, select (if possible) the
719 currently selected item. otherwise, select (if possible) the
724 parent-item of the currently selected item.
720 parent-item of the currently selected item.
725 """
721 """
726 currentitem = self.currentselecteditem
722 currentitem = self.currentselecteditem
727 nextitem = currentitem.prevsibling()
723 nextitem = currentitem.prevsibling()
728 # if there's no previous sibling, try choosing the parent
724 # if there's no previous sibling, try choosing the parent
729 if nextitem is None:
725 if nextitem is None:
730 nextitem = currentitem.parentitem()
726 nextitem = currentitem.parentitem()
731 if nextitem is None:
727 if nextitem is None:
732 # if no parent item (i.e. currentitem is the first header), then
728 # if no parent item (i.e. currentitem is the first header), then
733 # no change...
729 # no change...
734 nextitem = currentitem
730 nextitem = currentitem
735
731
736 self.currentselecteditem = nextitem
732 self.currentselecteditem = nextitem
737 self.recenterdisplayedarea()
733 self.recenterdisplayedarea()
738
734
739 def downarrowevent(self):
735 def downarrowevent(self):
740 """
736 """
741 try to select the next item to the current item that has the
737 try to select the next item to the current item that has the
742 most-indented level. for example, if a hunk is selected, select
738 most-indented level. for example, if a hunk is selected, select
743 the first hunkline of the selected hunk. or, if the last hunkline of
739 the first hunkline of the selected hunk. or, if the last hunkline of
744 a hunk is currently selected, then select the next hunk, if one exists,
740 a hunk is currently selected, then select the next hunk, if one exists,
745 or if not, the next header if one exists.
741 or if not, the next header if one exists.
746 """
742 """
747 # self.startprintline += 1 #debug
743 # self.startprintline += 1 #debug
748 currentitem = self.currentselecteditem
744 currentitem = self.currentselecteditem
749
745
750 nextitem = currentitem.nextitem()
746 nextitem = currentitem.nextitem()
751 # if there's no next item, keep the selection as-is
747 # if there's no next item, keep the selection as-is
752 if nextitem is None:
748 if nextitem is None:
753 nextitem = currentitem
749 nextitem = currentitem
754
750
755 self.currentselecteditem = nextitem
751 self.currentselecteditem = nextitem
756
752
757 def downarrowshiftevent(self):
753 def downarrowshiftevent(self):
758 """
754 """
759 select (if possible) the next item on the same level as the currently
755 select (if possible) the next item on the same level as the currently
760 selected item. otherwise, select (if possible) the next item on the
756 selected item. otherwise, select (if possible) the next item on the
761 same level as the parent item of the currently selected item.
757 same level as the parent item of the currently selected item.
762 """
758 """
763 currentitem = self.currentselecteditem
759 currentitem = self.currentselecteditem
764 nextitem = currentitem.nextsibling()
760 nextitem = currentitem.nextsibling()
765 # if there's no next sibling, try choosing the parent's nextsibling
761 # if there's no next sibling, try choosing the parent's nextsibling
766 if nextitem is None:
762 if nextitem is None:
767 try:
763 try:
768 nextitem = currentitem.parentitem().nextsibling()
764 nextitem = currentitem.parentitem().nextsibling()
769 except AttributeError:
765 except AttributeError:
770 # parentitem returned None, so nextsibling() can't be called
766 # parentitem returned None, so nextsibling() can't be called
771 nextitem = None
767 nextitem = None
772 if nextitem is None:
768 if nextitem is None:
773 # if parent has no next sibling, then no change...
769 # if parent has no next sibling, then no change...
774 nextitem = currentitem
770 nextitem = currentitem
775
771
776 self.currentselecteditem = nextitem
772 self.currentselecteditem = nextitem
777 self.recenterdisplayedarea()
773 self.recenterdisplayedarea()
778
774
779 def nextsametype(self, test=False):
775 def nextsametype(self, test=False):
780 currentitem = self.currentselecteditem
776 currentitem = self.currentselecteditem
781 sametype = lambda item: isinstance(item, type(currentitem))
777 sametype = lambda item: isinstance(item, type(currentitem))
782 nextitem = currentitem.nextitem()
778 nextitem = currentitem.nextitem()
783
779
784 while nextitem is not None and not sametype(nextitem):
780 while nextitem is not None and not sametype(nextitem):
785 nextitem = nextitem.nextitem()
781 nextitem = nextitem.nextitem()
786
782
787 if nextitem is None:
783 if nextitem is None:
788 nextitem = currentitem
784 nextitem = currentitem
789 else:
785 else:
790 parent = nextitem.parentitem()
786 parent = nextitem.parentitem()
791 if parent is not None and parent.folded:
787 if parent is not None and parent.folded:
792 self.togglefolded(parent)
788 self.togglefolded(parent)
793
789
794 self.currentselecteditem = nextitem
790 self.currentselecteditem = nextitem
795 if not test:
791 if not test:
796 self.recenterdisplayedarea()
792 self.recenterdisplayedarea()
797
793
798 def rightarrowevent(self):
794 def rightarrowevent(self):
799 """
795 """
800 select (if possible) the first of this item's child-items.
796 select (if possible) the first of this item's child-items.
801 """
797 """
802 currentitem = self.currentselecteditem
798 currentitem = self.currentselecteditem
803 nextitem = currentitem.firstchild()
799 nextitem = currentitem.firstchild()
804
800
805 # turn off folding if we want to show a child-item
801 # turn off folding if we want to show a child-item
806 if currentitem.folded:
802 if currentitem.folded:
807 self.togglefolded(currentitem)
803 self.togglefolded(currentitem)
808
804
809 if nextitem is None:
805 if nextitem is None:
810 # if no next item on parent-level, then no change...
806 # if no next item on parent-level, then no change...
811 nextitem = currentitem
807 nextitem = currentitem
812
808
813 self.currentselecteditem = nextitem
809 self.currentselecteditem = nextitem
814
810
815 def leftarrowevent(self):
811 def leftarrowevent(self):
816 """
812 """
817 if the current item can be folded (i.e. it is an unfolded header or
813 if the current item can be folded (i.e. it is an unfolded header or
818 hunk), then fold it. otherwise try select (if possible) the parent
814 hunk), then fold it. otherwise try select (if possible) the parent
819 of this item.
815 of this item.
820 """
816 """
821 currentitem = self.currentselecteditem
817 currentitem = self.currentselecteditem
822
818
823 # try to fold the item
819 # try to fold the item
824 if not isinstance(currentitem, uihunkline):
820 if not isinstance(currentitem, uihunkline):
825 if not currentitem.folded:
821 if not currentitem.folded:
826 self.togglefolded(item=currentitem)
822 self.togglefolded(item=currentitem)
827 return
823 return
828
824
829 # if it can't be folded, try to select the parent item
825 # if it can't be folded, try to select the parent item
830 nextitem = currentitem.parentitem()
826 nextitem = currentitem.parentitem()
831
827
832 if nextitem is None:
828 if nextitem is None:
833 # if no item on parent-level, then no change...
829 # if no item on parent-level, then no change...
834 nextitem = currentitem
830 nextitem = currentitem
835 if not nextitem.folded:
831 if not nextitem.folded:
836 self.togglefolded(item=nextitem)
832 self.togglefolded(item=nextitem)
837
833
838 self.currentselecteditem = nextitem
834 self.currentselecteditem = nextitem
839
835
840 def leftarrowshiftevent(self):
836 def leftarrowshiftevent(self):
841 """
837 """
842 select the header of the current item (or fold current item if the
838 select the header of the current item (or fold current item if the
843 current item is already a header).
839 current item is already a header).
844 """
840 """
845 currentitem = self.currentselecteditem
841 currentitem = self.currentselecteditem
846
842
847 if isinstance(currentitem, uiheader):
843 if isinstance(currentitem, uiheader):
848 if not currentitem.folded:
844 if not currentitem.folded:
849 self.togglefolded(item=currentitem)
845 self.togglefolded(item=currentitem)
850 return
846 return
851
847
852 # select the parent item recursively until we're at a header
848 # select the parent item recursively until we're at a header
853 while True:
849 while True:
854 nextitem = currentitem.parentitem()
850 nextitem = currentitem.parentitem()
855 if nextitem is None:
851 if nextitem is None:
856 break
852 break
857 else:
853 else:
858 currentitem = nextitem
854 currentitem = nextitem
859
855
860 self.currentselecteditem = currentitem
856 self.currentselecteditem = currentitem
861
857
862 def updatescroll(self):
858 def updatescroll(self):
863 """scroll the screen to fully show the currently-selected"""
859 """scroll the screen to fully show the currently-selected"""
864 selstart = self.selecteditemstartline
860 selstart = self.selecteditemstartline
865 selend = self.selecteditemendline
861 selend = self.selecteditemendline
866
862
867 padstart = self.firstlineofpadtoprint
863 padstart = self.firstlineofpadtoprint
868 padend = padstart + self.yscreensize - self.numstatuslines - 1
864 padend = padstart + self.yscreensize - self.numstatuslines - 1
869 # 'buffered' pad start/end values which scroll with a certain
865 # 'buffered' pad start/end values which scroll with a certain
870 # top/bottom context margin
866 # top/bottom context margin
871 padstartbuffered = padstart + 3
867 padstartbuffered = padstart + 3
872 padendbuffered = padend - 3
868 padendbuffered = padend - 3
873
869
874 if selend > padendbuffered:
870 if selend > padendbuffered:
875 self.scrolllines(selend - padendbuffered)
871 self.scrolllines(selend - padendbuffered)
876 elif selstart < padstartbuffered:
872 elif selstart < padstartbuffered:
877 # negative values scroll in pgup direction
873 # negative values scroll in pgup direction
878 self.scrolllines(selstart - padstartbuffered)
874 self.scrolllines(selstart - padstartbuffered)
879
875
880 def scrolllines(self, numlines):
876 def scrolllines(self, numlines):
881 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
877 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
882 self.firstlineofpadtoprint += numlines
878 self.firstlineofpadtoprint += numlines
883 if self.firstlineofpadtoprint < 0:
879 if self.firstlineofpadtoprint < 0:
884 self.firstlineofpadtoprint = 0
880 self.firstlineofpadtoprint = 0
885 if self.firstlineofpadtoprint > self.numpadlines - 1:
881 if self.firstlineofpadtoprint > self.numpadlines - 1:
886 self.firstlineofpadtoprint = self.numpadlines - 1
882 self.firstlineofpadtoprint = self.numpadlines - 1
887
883
888 def toggleapply(self, item=None):
884 def toggleapply(self, item=None):
889 """
885 """
890 toggle the applied flag of the specified item. if no item is specified,
886 toggle the applied flag of the specified item. if no item is specified,
891 toggle the flag of the currently selected item.
887 toggle the flag of the currently selected item.
892 """
888 """
893 if item is None:
889 if item is None:
894 item = self.currentselecteditem
890 item = self.currentselecteditem
895 # Only set this when NOT using 'toggleall'
891 # Only set this when NOT using 'toggleall'
896 self.lastapplieditem = item
892 self.lastapplieditem = item
897
893
898 item.applied = not item.applied
894 item.applied = not item.applied
899
895
900 if isinstance(item, uiheader):
896 if isinstance(item, uiheader):
901 item.partial = False
897 item.partial = False
902 if item.applied:
898 if item.applied:
903 # apply all its hunks
899 # apply all its hunks
904 for hnk in item.hunks:
900 for hnk in item.hunks:
905 hnk.applied = True
901 hnk.applied = True
906 # apply all their hunklines
902 # apply all their hunklines
907 for hunkline in hnk.changedlines:
903 for hunkline in hnk.changedlines:
908 hunkline.applied = True
904 hunkline.applied = True
909 else:
905 else:
910 # un-apply all its hunks
906 # un-apply all its hunks
911 for hnk in item.hunks:
907 for hnk in item.hunks:
912 hnk.applied = False
908 hnk.applied = False
913 hnk.partial = False
909 hnk.partial = False
914 # un-apply all their hunklines
910 # un-apply all their hunklines
915 for hunkline in hnk.changedlines:
911 for hunkline in hnk.changedlines:
916 hunkline.applied = False
912 hunkline.applied = False
917 elif isinstance(item, uihunk):
913 elif isinstance(item, uihunk):
918 item.partial = False
914 item.partial = False
919 # apply all it's hunklines
915 # apply all it's hunklines
920 for hunkline in item.changedlines:
916 for hunkline in item.changedlines:
921 hunkline.applied = item.applied
917 hunkline.applied = item.applied
922
918
923 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
919 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
924 allsiblingsapplied = not (False in siblingappliedstatus)
920 allsiblingsapplied = not (False in siblingappliedstatus)
925 nosiblingsapplied = not (True in siblingappliedstatus)
921 nosiblingsapplied = not (True in siblingappliedstatus)
926
922
927 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
923 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
928 somesiblingspartial = True in siblingspartialstatus
924 somesiblingspartial = True in siblingspartialstatus
929
925
930 # cases where applied or partial should be removed from header
926 # cases where applied or partial should be removed from header
931
927
932 # if no 'sibling' hunks are applied (including this hunk)
928 # if no 'sibling' hunks are applied (including this hunk)
933 if nosiblingsapplied:
929 if nosiblingsapplied:
934 if not item.header.special():
930 if not item.header.special():
935 item.header.applied = False
931 item.header.applied = False
936 item.header.partial = False
932 item.header.partial = False
937 else: # some/all parent siblings are applied
933 else: # some/all parent siblings are applied
938 item.header.applied = True
934 item.header.applied = True
939 item.header.partial = (
935 item.header.partial = (
940 somesiblingspartial or not allsiblingsapplied
936 somesiblingspartial or not allsiblingsapplied
941 )
937 )
942
938
943 elif isinstance(item, uihunkline):
939 elif isinstance(item, uihunkline):
944 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
940 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
945 allsiblingsapplied = not (False in siblingappliedstatus)
941 allsiblingsapplied = not (False in siblingappliedstatus)
946 nosiblingsapplied = not (True in siblingappliedstatus)
942 nosiblingsapplied = not (True in siblingappliedstatus)
947
943
948 # if no 'sibling' lines are applied
944 # if no 'sibling' lines are applied
949 if nosiblingsapplied:
945 if nosiblingsapplied:
950 item.hunk.applied = False
946 item.hunk.applied = False
951 item.hunk.partial = False
947 item.hunk.partial = False
952 elif allsiblingsapplied:
948 elif allsiblingsapplied:
953 item.hunk.applied = True
949 item.hunk.applied = True
954 item.hunk.partial = False
950 item.hunk.partial = False
955 else: # some siblings applied
951 else: # some siblings applied
956 item.hunk.applied = True
952 item.hunk.applied = True
957 item.hunk.partial = True
953 item.hunk.partial = True
958
954
959 parentsiblingsapplied = [
955 parentsiblingsapplied = [
960 hnk.applied for hnk in item.hunk.header.hunks
956 hnk.applied for hnk in item.hunk.header.hunks
961 ]
957 ]
962 noparentsiblingsapplied = not (True in parentsiblingsapplied)
958 noparentsiblingsapplied = not (True in parentsiblingsapplied)
963 allparentsiblingsapplied = not (False in parentsiblingsapplied)
959 allparentsiblingsapplied = not (False in parentsiblingsapplied)
964
960
965 parentsiblingspartial = [
961 parentsiblingspartial = [
966 hnk.partial for hnk in item.hunk.header.hunks
962 hnk.partial for hnk in item.hunk.header.hunks
967 ]
963 ]
968 someparentsiblingspartial = True in parentsiblingspartial
964 someparentsiblingspartial = True in parentsiblingspartial
969
965
970 # if all parent hunks are not applied, un-apply header
966 # if all parent hunks are not applied, un-apply header
971 if noparentsiblingsapplied:
967 if noparentsiblingsapplied:
972 if not item.hunk.header.special():
968 if not item.hunk.header.special():
973 item.hunk.header.applied = False
969 item.hunk.header.applied = False
974 item.hunk.header.partial = False
970 item.hunk.header.partial = False
975 # set the applied and partial status of the header if needed
971 # set the applied and partial status of the header if needed
976 else: # some/all parent siblings are applied
972 else: # some/all parent siblings are applied
977 item.hunk.header.applied = True
973 item.hunk.header.applied = True
978 item.hunk.header.partial = (
974 item.hunk.header.partial = (
979 someparentsiblingspartial or not allparentsiblingsapplied
975 someparentsiblingspartial or not allparentsiblingsapplied
980 )
976 )
981
977
982 def toggleall(self):
978 def toggleall(self):
983 """toggle the applied flag of all items."""
979 """toggle the applied flag of all items."""
984 if self.waslasttoggleallapplied: # then unapply them this time
980 if self.waslasttoggleallapplied: # then unapply them this time
985 for item in self.headerlist:
981 for item in self.headerlist:
986 if item.applied:
982 if item.applied:
987 self.toggleapply(item)
983 self.toggleapply(item)
988 else:
984 else:
989 for item in self.headerlist:
985 for item in self.headerlist:
990 if not item.applied:
986 if not item.applied:
991 self.toggleapply(item)
987 self.toggleapply(item)
992 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
988 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
993
989
994 def flipselections(self):
990 def flipselections(self):
995 """
991 """
996 Flip all selections. Every selected line is unselected and vice
992 Flip all selections. Every selected line is unselected and vice
997 versa.
993 versa.
998 """
994 """
999 for header in self.headerlist:
995 for header in self.headerlist:
1000 for hunk in header.allchildren():
996 for hunk in header.allchildren():
1001 for line in hunk.allchildren():
997 for line in hunk.allchildren():
1002 self.toggleapply(line)
998 self.toggleapply(line)
1003
999
1004 def toggleallbetween(self):
1000 def toggleallbetween(self):
1005 """toggle applied on or off for all items in range [lastapplied,
1001 """toggle applied on or off for all items in range [lastapplied,
1006 current]. """
1002 current]. """
1007 if (
1003 if (
1008 not self.lastapplieditem
1004 not self.lastapplieditem
1009 or self.currentselecteditem == self.lastapplieditem
1005 or self.currentselecteditem == self.lastapplieditem
1010 ):
1006 ):
1011 # Treat this like a normal 'x'/' '
1007 # Treat this like a normal 'x'/' '
1012 self.toggleapply()
1008 self.toggleapply()
1013 return
1009 return
1014
1010
1015 startitem = self.lastapplieditem
1011 startitem = self.lastapplieditem
1016 enditem = self.currentselecteditem
1012 enditem = self.currentselecteditem
1017 # Verify that enditem is "after" startitem, otherwise swap them.
1013 # Verify that enditem is "after" startitem, otherwise swap them.
1018 for direction in [b'forward', b'reverse']:
1014 for direction in [b'forward', b'reverse']:
1019 nextitem = startitem.nextitem()
1015 nextitem = startitem.nextitem()
1020 while nextitem and nextitem != enditem:
1016 while nextitem and nextitem != enditem:
1021 nextitem = nextitem.nextitem()
1017 nextitem = nextitem.nextitem()
1022 if nextitem:
1018 if nextitem:
1023 break
1019 break
1024 # Looks like we went the wrong direction :)
1020 # Looks like we went the wrong direction :)
1025 startitem, enditem = enditem, startitem
1021 startitem, enditem = enditem, startitem
1026
1022
1027 if not nextitem:
1023 if not nextitem:
1028 # We didn't find a path going either forward or backward? Don't know
1024 # We didn't find a path going either forward or backward? Don't know
1029 # how this can happen, let's not crash though.
1025 # how this can happen, let's not crash though.
1030 return
1026 return
1031
1027
1032 nextitem = startitem
1028 nextitem = startitem
1033 # Switch all items to be the opposite state of the currently selected
1029 # Switch all items to be the opposite state of the currently selected
1034 # item. Specifically:
1030 # item. Specifically:
1035 # [ ] startitem
1031 # [ ] startitem
1036 # [x] middleitem
1032 # [x] middleitem
1037 # [ ] enditem <-- currently selected
1033 # [ ] enditem <-- currently selected
1038 # This will turn all three on, since the currently selected item is off.
1034 # This will turn all three on, since the currently selected item is off.
1039 # This does *not* invert each item (i.e. middleitem stays marked/on)
1035 # This does *not* invert each item (i.e. middleitem stays marked/on)
1040 desiredstate = not self.currentselecteditem.applied
1036 desiredstate = not self.currentselecteditem.applied
1041 while nextitem != enditem.nextitem():
1037 while nextitem != enditem.nextitem():
1042 if nextitem.applied != desiredstate:
1038 if nextitem.applied != desiredstate:
1043 self.toggleapply(item=nextitem)
1039 self.toggleapply(item=nextitem)
1044 nextitem = nextitem.nextitem()
1040 nextitem = nextitem.nextitem()
1045
1041
1046 def togglefolded(self, item=None, foldparent=False):
1042 def togglefolded(self, item=None, foldparent=False):
1047 """toggle folded flag of specified item (defaults to currently
1043 """toggle folded flag of specified item (defaults to currently
1048 selected)"""
1044 selected)"""
1049 if item is None:
1045 if item is None:
1050 item = self.currentselecteditem
1046 item = self.currentselecteditem
1051 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1047 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1052 if not isinstance(item, uiheader):
1048 if not isinstance(item, uiheader):
1053 # we need to select the parent item in this case
1049 # we need to select the parent item in this case
1054 self.currentselecteditem = item = item.parentitem()
1050 self.currentselecteditem = item = item.parentitem()
1055 elif item.neverunfolded:
1051 elif item.neverunfolded:
1056 item.neverunfolded = False
1052 item.neverunfolded = False
1057
1053
1058 # also fold any foldable children of the parent/current item
1054 # also fold any foldable children of the parent/current item
1059 if isinstance(item, uiheader): # the original or 'new' item
1055 if isinstance(item, uiheader): # the original or 'new' item
1060 for child in item.allchildren():
1056 for child in item.allchildren():
1061 child.folded = not item.folded
1057 child.folded = not item.folded
1062
1058
1063 if isinstance(item, (uiheader, uihunk)):
1059 if isinstance(item, (uiheader, uihunk)):
1064 item.folded = not item.folded
1060 item.folded = not item.folded
1065
1061
1066 def alignstring(self, instr, window):
1062 def alignstring(self, instr, window):
1067 """
1063 """
1068 add whitespace to the end of a string in order to make it fill
1064 add whitespace to the end of a string in order to make it fill
1069 the screen in the x direction. the current cursor position is
1065 the screen in the x direction. the current cursor position is
1070 taken into account when making this calculation. the string can span
1066 taken into account when making this calculation. the string can span
1071 multiple lines.
1067 multiple lines.
1072 """
1068 """
1073 y, xstart = window.getyx()
1069 y, xstart = window.getyx()
1074 width = self.xscreensize
1070 width = self.xscreensize
1075 # turn tabs into spaces
1071 # turn tabs into spaces
1076 instr = instr.expandtabs(4)
1072 instr = instr.expandtabs(4)
1077 strwidth = encoding.colwidth(instr)
1073 strwidth = encoding.colwidth(instr)
1078 numspaces = width - ((strwidth + xstart) % width)
1074 numspaces = width - ((strwidth + xstart) % width)
1079 return instr + b" " * numspaces
1075 return instr + b" " * numspaces
1080
1076
1081 def printstring(
1077 def printstring(
1082 self,
1078 self,
1083 window,
1079 window,
1084 text,
1080 text,
1085 fgcolor=None,
1081 fgcolor=None,
1086 bgcolor=None,
1082 bgcolor=None,
1087 pair=None,
1083 pair=None,
1088 pairname=None,
1084 pairname=None,
1089 attrlist=None,
1085 attrlist=None,
1090 towin=True,
1086 towin=True,
1091 align=True,
1087 align=True,
1092 showwhtspc=False,
1088 showwhtspc=False,
1093 ):
1089 ):
1094 """
1090 """
1095 print the string, text, with the specified colors and attributes, to
1091 print the string, text, with the specified colors and attributes, to
1096 the specified curses window object.
1092 the specified curses window object.
1097
1093
1098 the foreground and background colors are of the form
1094 the foreground and background colors are of the form
1099 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1095 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1100 magenta, red, white, yellow]. if pairname is provided, a color
1096 magenta, red, white, yellow]. if pairname is provided, a color
1101 pair will be looked up in the self.colorpairnames dictionary.
1097 pair will be looked up in the self.colorpairnames dictionary.
1102
1098
1103 attrlist is a list containing text attributes in the form of
1099 attrlist is a list containing text attributes in the form of
1104 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1100 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1105 underline].
1101 underline].
1106
1102
1107 if align == True, whitespace is added to the printed string such that
1103 if align == True, whitespace is added to the printed string such that
1108 the string stretches to the right border of the window.
1104 the string stretches to the right border of the window.
1109
1105
1110 if showwhtspc == True, trailing whitespace of a string is highlighted.
1106 if showwhtspc == True, trailing whitespace of a string is highlighted.
1111 """
1107 """
1112 # preprocess the text, converting tabs to spaces
1108 # preprocess the text, converting tabs to spaces
1113 text = text.expandtabs(4)
1109 text = text.expandtabs(4)
1114 # strip \n, and convert control characters to ^[char] representation
1110 # strip \n, and convert control characters to ^[char] representation
1115 text = re.sub(
1111 text = re.sub(
1116 br'[\x00-\x08\x0a-\x1f]',
1112 br'[\x00-\x08\x0a-\x1f]',
1117 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1113 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1118 text.strip(b'\n'),
1114 text.strip(b'\n'),
1119 )
1115 )
1120
1116
1121 if pair is not None:
1117 if pair is not None:
1122 colorpair = pair
1118 colorpair = pair
1123 elif pairname is not None:
1119 elif pairname is not None:
1124 colorpair = self.colorpairnames[pairname]
1120 colorpair = self.colorpairnames[pairname]
1125 else:
1121 else:
1126 if fgcolor is None:
1122 if fgcolor is None:
1127 fgcolor = -1
1123 fgcolor = -1
1128 if bgcolor is None:
1124 if bgcolor is None:
1129 bgcolor = -1
1125 bgcolor = -1
1130 if (fgcolor, bgcolor) in self.colorpairs:
1126 if (fgcolor, bgcolor) in self.colorpairs:
1131 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1127 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1132 else:
1128 else:
1133 colorpair = self.getcolorpair(fgcolor, bgcolor)
1129 colorpair = self.getcolorpair(fgcolor, bgcolor)
1134 # add attributes if possible
1130 # add attributes if possible
1135 if attrlist is None:
1131 if attrlist is None:
1136 attrlist = []
1132 attrlist = []
1137 if colorpair < 256:
1133 if colorpair < 256:
1138 # then it is safe to apply all attributes
1134 # then it is safe to apply all attributes
1139 for textattr in attrlist:
1135 for textattr in attrlist:
1140 colorpair |= textattr
1136 colorpair |= textattr
1141 else:
1137 else:
1142 # just apply a select few (safe?) attributes
1138 # just apply a select few (safe?) attributes
1143 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1139 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1144 if textattr in attrlist:
1140 if textattr in attrlist:
1145 colorpair |= textattr
1141 colorpair |= textattr
1146
1142
1147 y, xstart = self.chunkpad.getyx()
1143 y, xstart = self.chunkpad.getyx()
1148 t = b"" # variable for counting lines printed
1144 t = b"" # variable for counting lines printed
1149 # if requested, show trailing whitespace
1145 # if requested, show trailing whitespace
1150 if showwhtspc:
1146 if showwhtspc:
1151 origlen = len(text)
1147 origlen = len(text)
1152 text = text.rstrip(b' \n') # tabs have already been expanded
1148 text = text.rstrip(b' \n') # tabs have already been expanded
1153 strippedlen = len(text)
1149 strippedlen = len(text)
1154 numtrailingspaces = origlen - strippedlen
1150 numtrailingspaces = origlen - strippedlen
1155
1151
1156 if towin:
1152 if towin:
1157 window.addstr(text, colorpair)
1153 window.addstr(text, colorpair)
1158 t += text
1154 t += text
1159
1155
1160 if showwhtspc:
1156 if showwhtspc:
1161 wscolorpair = colorpair | curses.A_REVERSE
1157 wscolorpair = colorpair | curses.A_REVERSE
1162 if towin:
1158 if towin:
1163 for i in range(numtrailingspaces):
1159 for i in range(numtrailingspaces):
1164 window.addch(curses.ACS_CKBOARD, wscolorpair)
1160 window.addch(curses.ACS_CKBOARD, wscolorpair)
1165 t += b" " * numtrailingspaces
1161 t += b" " * numtrailingspaces
1166
1162
1167 if align:
1163 if align:
1168 if towin:
1164 if towin:
1169 extrawhitespace = self.alignstring(b"", window)
1165 extrawhitespace = self.alignstring(b"", window)
1170 window.addstr(extrawhitespace, colorpair)
1166 window.addstr(extrawhitespace, colorpair)
1171 else:
1167 else:
1172 # need to use t, since the x position hasn't incremented
1168 # need to use t, since the x position hasn't incremented
1173 extrawhitespace = self.alignstring(t, window)
1169 extrawhitespace = self.alignstring(t, window)
1174 t += extrawhitespace
1170 t += extrawhitespace
1175
1171
1176 # is reset to 0 at the beginning of printitem()
1172 # is reset to 0 at the beginning of printitem()
1177
1173
1178 linesprinted = (xstart + len(t)) // self.xscreensize
1174 linesprinted = (xstart + len(t)) // self.xscreensize
1179 self.linesprintedtopadsofar += linesprinted
1175 self.linesprintedtopadsofar += linesprinted
1180 return t
1176 return t
1181
1177
1182 def _getstatuslinesegments(self):
1178 def _getstatuslinesegments(self):
1183 """-> [str]. return segments"""
1179 """-> [str]. return segments"""
1184 selected = self.currentselecteditem.applied
1180 selected = self.currentselecteditem.applied
1185 spaceselect = _(b'space/enter: select')
1181 spaceselect = _(b'space/enter: select')
1186 spacedeselect = _(b'space/enter: deselect')
1182 spacedeselect = _(b'space/enter: deselect')
1187 # Format the selected label into a place as long as the longer of the
1183 # Format the selected label into a place as long as the longer of the
1188 # two possible labels. This may vary by language.
1184 # two possible labels. This may vary by language.
1189 spacelen = max(len(spaceselect), len(spacedeselect))
1185 spacelen = max(len(spaceselect), len(spacedeselect))
1190 selectedlabel = b'%-*s' % (
1186 selectedlabel = b'%-*s' % (
1191 spacelen,
1187 spacelen,
1192 spacedeselect if selected else spaceselect,
1188 spacedeselect if selected else spaceselect,
1193 )
1189 )
1194 segments = [
1190 segments = [
1195 _headermessages[self.operation],
1191 _headermessages[self.operation],
1196 b'-',
1192 b'-',
1197 _(b'[x]=selected **=collapsed'),
1193 _(b'[x]=selected **=collapsed'),
1198 _(b'c: confirm'),
1194 _(b'c: confirm'),
1199 _(b'q: abort'),
1195 _(b'q: abort'),
1200 _(b'arrow keys: move/expand/collapse'),
1196 _(b'arrow keys: move/expand/collapse'),
1201 selectedlabel,
1197 selectedlabel,
1202 _(b'?: help'),
1198 _(b'?: help'),
1203 ]
1199 ]
1204 return segments
1200 return segments
1205
1201
1206 def _getstatuslines(self):
1202 def _getstatuslines(self):
1207 """() -> [str]. return short help used in the top status window"""
1203 """() -> [str]. return short help used in the top status window"""
1208 if self.errorstr is not None:
1204 if self.errorstr is not None:
1209 lines = [self.errorstr, _(b'Press any key to continue')]
1205 lines = [self.errorstr, _(b'Press any key to continue')]
1210 else:
1206 else:
1211 # wrap segments to lines
1207 # wrap segments to lines
1212 segments = self._getstatuslinesegments()
1208 segments = self._getstatuslinesegments()
1213 width = self.xscreensize
1209 width = self.xscreensize
1214 lines = []
1210 lines = []
1215 lastwidth = width
1211 lastwidth = width
1216 for s in segments:
1212 for s in segments:
1217 w = encoding.colwidth(s)
1213 w = encoding.colwidth(s)
1218 sep = b' ' * (1 + (s and s[0] not in b'-['))
1214 sep = b' ' * (1 + (s and s[0] not in b'-['))
1219 if lastwidth + w + len(sep) >= width:
1215 if lastwidth + w + len(sep) >= width:
1220 lines.append(s)
1216 lines.append(s)
1221 lastwidth = w
1217 lastwidth = w
1222 else:
1218 else:
1223 lines[-1] += sep + s
1219 lines[-1] += sep + s
1224 lastwidth += w + len(sep)
1220 lastwidth += w + len(sep)
1225 if len(lines) != self.numstatuslines:
1221 if len(lines) != self.numstatuslines:
1226 self.numstatuslines = len(lines)
1222 self.numstatuslines = len(lines)
1227 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1223 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1228 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1224 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1229
1225
1230 def updatescreen(self):
1226 def updatescreen(self):
1231 self.statuswin.erase()
1227 self.statuswin.erase()
1232 self.chunkpad.erase()
1228 self.chunkpad.erase()
1233
1229
1234 printstring = self.printstring
1230 printstring = self.printstring
1235
1231
1236 # print out the status lines at the top
1232 # print out the status lines at the top
1237 try:
1233 try:
1238 for line in self._getstatuslines():
1234 for line in self._getstatuslines():
1239 printstring(self.statuswin, line, pairname=b"legend")
1235 printstring(self.statuswin, line, pairname=b"legend")
1240 self.statuswin.refresh()
1236 self.statuswin.refresh()
1241 except curses.error:
1237 except curses.error:
1242 pass
1238 pass
1243 if self.errorstr is not None:
1239 if self.errorstr is not None:
1244 return
1240 return
1245
1241
1246 # print out the patch in the remaining part of the window
1242 # print out the patch in the remaining part of the window
1247 try:
1243 try:
1248 self.printitem()
1244 self.printitem()
1249 self.updatescroll()
1245 self.updatescroll()
1250 self.chunkpad.refresh(
1246 self.chunkpad.refresh(
1251 self.firstlineofpadtoprint,
1247 self.firstlineofpadtoprint,
1252 0,
1248 0,
1253 self.numstatuslines,
1249 self.numstatuslines,
1254 0,
1250 0,
1255 self.yscreensize - self.numstatuslines,
1251 self.yscreensize - self.numstatuslines,
1256 self.xscreensize,
1252 self.xscreensize,
1257 )
1253 )
1258 except curses.error:
1254 except curses.error:
1259 pass
1255 pass
1260
1256
1261 def getstatusprefixstring(self, item):
1257 def getstatusprefixstring(self, item):
1262 """
1258 """
1263 create a string to prefix a line with which indicates whether 'item'
1259 create a string to prefix a line with which indicates whether 'item'
1264 is applied and/or folded.
1260 is applied and/or folded.
1265 """
1261 """
1266
1262
1267 # create checkbox string
1263 # create checkbox string
1268 if item.applied:
1264 if item.applied:
1269 if not isinstance(item, uihunkline) and item.partial:
1265 if not isinstance(item, uihunkline) and item.partial:
1270 checkbox = b"[~]"
1266 checkbox = b"[~]"
1271 else:
1267 else:
1272 checkbox = b"[x]"
1268 checkbox = b"[x]"
1273 else:
1269 else:
1274 checkbox = b"[ ]"
1270 checkbox = b"[ ]"
1275
1271
1276 try:
1272 try:
1277 if item.folded:
1273 if item.folded:
1278 checkbox += b"**"
1274 checkbox += b"**"
1279 if isinstance(item, uiheader):
1275 if isinstance(item, uiheader):
1280 # one of "m", "a", or "d" (modified, added, deleted)
1276 # one of "m", "a", or "d" (modified, added, deleted)
1281 filestatus = item.changetype
1277 filestatus = item.changetype
1282
1278
1283 checkbox += filestatus + b" "
1279 checkbox += filestatus + b" "
1284 else:
1280 else:
1285 checkbox += b" "
1281 checkbox += b" "
1286 if isinstance(item, uiheader):
1282 if isinstance(item, uiheader):
1287 # add two more spaces for headers
1283 # add two more spaces for headers
1288 checkbox += b" "
1284 checkbox += b" "
1289 except AttributeError: # not foldable
1285 except AttributeError: # not foldable
1290 checkbox += b" "
1286 checkbox += b" "
1291
1287
1292 return checkbox
1288 return checkbox
1293
1289
1294 def printheader(
1290 def printheader(
1295 self, header, selected=False, towin=True, ignorefolding=False
1291 self, header, selected=False, towin=True, ignorefolding=False
1296 ):
1292 ):
1297 """
1293 """
1298 print the header to the pad. if countlines is True, don't print
1294 print the header to the pad. if countlines is True, don't print
1299 anything, but just count the number of lines which would be printed.
1295 anything, but just count the number of lines which would be printed.
1300 """
1296 """
1301
1297
1302 outstr = b""
1298 outstr = b""
1303 text = header.prettystr()
1299 text = header.prettystr()
1304 chunkindex = self.chunklist.index(header)
1300 chunkindex = self.chunklist.index(header)
1305
1301
1306 if chunkindex != 0 and not header.folded:
1302 if chunkindex != 0 and not header.folded:
1307 # add separating line before headers
1303 # add separating line before headers
1308 outstr += self.printstring(
1304 outstr += self.printstring(
1309 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1305 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1310 )
1306 )
1311 # select color-pair based on if the header is selected
1307 # select color-pair based on if the header is selected
1312 colorpair = self.getcolorpair(
1308 colorpair = self.getcolorpair(
1313 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1309 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1314 )
1310 )
1315
1311
1316 # print out each line of the chunk, expanding it to screen width
1312 # print out each line of the chunk, expanding it to screen width
1317
1313
1318 # number of characters to indent lines on this level by
1314 # number of characters to indent lines on this level by
1319 indentnumchars = 0
1315 indentnumchars = 0
1320 checkbox = self.getstatusprefixstring(header)
1316 checkbox = self.getstatusprefixstring(header)
1321 if not header.folded or ignorefolding:
1317 if not header.folded or ignorefolding:
1322 textlist = text.split(b"\n")
1318 textlist = text.split(b"\n")
1323 linestr = checkbox + textlist[0]
1319 linestr = checkbox + textlist[0]
1324 else:
1320 else:
1325 linestr = checkbox + header.filename()
1321 linestr = checkbox + header.filename()
1326 outstr += self.printstring(
1322 outstr += self.printstring(
1327 self.chunkpad, linestr, pair=colorpair, towin=towin
1323 self.chunkpad, linestr, pair=colorpair, towin=towin
1328 )
1324 )
1329 if not header.folded or ignorefolding:
1325 if not header.folded or ignorefolding:
1330 if len(textlist) > 1:
1326 if len(textlist) > 1:
1331 for line in textlist[1:]:
1327 for line in textlist[1:]:
1332 linestr = b" " * (indentnumchars + len(checkbox)) + line
1328 linestr = b" " * (indentnumchars + len(checkbox)) + line
1333 outstr += self.printstring(
1329 outstr += self.printstring(
1334 self.chunkpad, linestr, pair=colorpair, towin=towin
1330 self.chunkpad, linestr, pair=colorpair, towin=towin
1335 )
1331 )
1336
1332
1337 return outstr
1333 return outstr
1338
1334
1339 def printhunklinesbefore(
1335 def printhunklinesbefore(
1340 self, hunk, selected=False, towin=True, ignorefolding=False
1336 self, hunk, selected=False, towin=True, ignorefolding=False
1341 ):
1337 ):
1342 """includes start/end line indicator"""
1338 """includes start/end line indicator"""
1343 outstr = b""
1339 outstr = b""
1344 # where hunk is in list of siblings
1340 # where hunk is in list of siblings
1345 hunkindex = hunk.header.hunks.index(hunk)
1341 hunkindex = hunk.header.hunks.index(hunk)
1346
1342
1347 if hunkindex != 0:
1343 if hunkindex != 0:
1348 # add separating line before headers
1344 # add separating line before headers
1349 outstr += self.printstring(
1345 outstr += self.printstring(
1350 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1346 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1351 )
1347 )
1352
1348
1353 colorpair = self.getcolorpair(
1349 colorpair = self.getcolorpair(
1354 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1350 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1355 )
1351 )
1356
1352
1357 # print out from-to line with checkbox
1353 # print out from-to line with checkbox
1358 checkbox = self.getstatusprefixstring(hunk)
1354 checkbox = self.getstatusprefixstring(hunk)
1359
1355
1360 lineprefix = b" " * self.hunkindentnumchars + checkbox
1356 lineprefix = b" " * self.hunkindentnumchars + checkbox
1361 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1357 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1362
1358
1363 outstr += self.printstring(
1359 outstr += self.printstring(
1364 self.chunkpad, lineprefix, towin=towin, align=False
1360 self.chunkpad, lineprefix, towin=towin, align=False
1365 ) # add uncolored checkbox/indent
1361 ) # add uncolored checkbox/indent
1366 outstr += self.printstring(
1362 outstr += self.printstring(
1367 self.chunkpad, frtoline, pair=colorpair, towin=towin
1363 self.chunkpad, frtoline, pair=colorpair, towin=towin
1368 )
1364 )
1369
1365
1370 if hunk.folded and not ignorefolding:
1366 if hunk.folded and not ignorefolding:
1371 # skip remainder of output
1367 # skip remainder of output
1372 return outstr
1368 return outstr
1373
1369
1374 # print out lines of the chunk preceeding changed-lines
1370 # print out lines of the chunk preceeding changed-lines
1375 for line in hunk.before:
1371 for line in hunk.before:
1376 linestr = (
1372 linestr = (
1377 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1373 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1378 )
1374 )
1379 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1375 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1380
1376
1381 return outstr
1377 return outstr
1382
1378
1383 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1379 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1384 outstr = b""
1380 outstr = b""
1385 if hunk.folded and not ignorefolding:
1381 if hunk.folded and not ignorefolding:
1386 return outstr
1382 return outstr
1387
1383
1388 # a bit superfluous, but to avoid hard-coding indent amount
1384 # a bit superfluous, but to avoid hard-coding indent amount
1389 checkbox = self.getstatusprefixstring(hunk)
1385 checkbox = self.getstatusprefixstring(hunk)
1390 for line in hunk.after:
1386 for line in hunk.after:
1391 linestr = (
1387 linestr = (
1392 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1388 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1393 )
1389 )
1394 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1390 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1395
1391
1396 return outstr
1392 return outstr
1397
1393
1398 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1394 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1399 outstr = b""
1395 outstr = b""
1400 checkbox = self.getstatusprefixstring(hunkline)
1396 checkbox = self.getstatusprefixstring(hunkline)
1401
1397
1402 linestr = hunkline.prettystr().strip(b"\n")
1398 linestr = hunkline.prettystr().strip(b"\n")
1403
1399
1404 # select color-pair based on whether line is an addition/removal
1400 # select color-pair based on whether line is an addition/removal
1405 if selected:
1401 if selected:
1406 colorpair = self.getcolorpair(name=b"selected")
1402 colorpair = self.getcolorpair(name=b"selected")
1407 elif linestr.startswith(b"+"):
1403 elif linestr.startswith(b"+"):
1408 colorpair = self.getcolorpair(name=b"addition")
1404 colorpair = self.getcolorpair(name=b"addition")
1409 elif linestr.startswith(b"-"):
1405 elif linestr.startswith(b"-"):
1410 colorpair = self.getcolorpair(name=b"deletion")
1406 colorpair = self.getcolorpair(name=b"deletion")
1411 elif linestr.startswith(b"\\"):
1407 elif linestr.startswith(b"\\"):
1412 colorpair = self.getcolorpair(name=b"normal")
1408 colorpair = self.getcolorpair(name=b"normal")
1413
1409
1414 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1410 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1415 outstr += self.printstring(
1411 outstr += self.printstring(
1416 self.chunkpad, lineprefix, towin=towin, align=False
1412 self.chunkpad, lineprefix, towin=towin, align=False
1417 ) # add uncolored checkbox/indent
1413 ) # add uncolored checkbox/indent
1418 outstr += self.printstring(
1414 outstr += self.printstring(
1419 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1415 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1420 )
1416 )
1421 return outstr
1417 return outstr
1422
1418
1423 def printitem(
1419 def printitem(
1424 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1420 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1425 ):
1421 ):
1426 """
1422 """
1427 use __printitem() to print the the specified item.applied.
1423 use __printitem() to print the the specified item.applied.
1428 if item is not specified, then print the entire patch.
1424 if item is not specified, then print the entire patch.
1429 (hiding folded elements, etc. -- see __printitem() docstring)
1425 (hiding folded elements, etc. -- see __printitem() docstring)
1430 """
1426 """
1431
1427
1432 if item is None:
1428 if item is None:
1433 item = self.headerlist
1429 item = self.headerlist
1434 if recursechildren:
1430 if recursechildren:
1435 self.linesprintedtopadsofar = 0
1431 self.linesprintedtopadsofar = 0
1436
1432
1437 outstr = []
1433 outstr = []
1438 self.__printitem(
1434 self.__printitem(
1439 item, ignorefolding, recursechildren, outstr, towin=towin
1435 item, ignorefolding, recursechildren, outstr, towin=towin
1440 )
1436 )
1441 return b''.join(outstr)
1437 return b''.join(outstr)
1442
1438
1443 def outofdisplayedarea(self):
1439 def outofdisplayedarea(self):
1444 y, _ = self.chunkpad.getyx() # cursor location
1440 y, _ = self.chunkpad.getyx() # cursor location
1445 # * 2 here works but an optimization would be the max number of
1441 # * 2 here works but an optimization would be the max number of
1446 # consecutive non selectable lines
1442 # consecutive non selectable lines
1447 # i.e the max number of context line for any hunk in the patch
1443 # i.e the max number of context line for any hunk in the patch
1448 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1444 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1449 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1445 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1450 return y < miny or y > maxy
1446 return y < miny or y > maxy
1451
1447
1452 def handleselection(self, item, recursechildren):
1448 def handleselection(self, item, recursechildren):
1453 selected = item is self.currentselecteditem
1449 selected = item is self.currentselecteditem
1454 if selected and recursechildren:
1450 if selected and recursechildren:
1455 # assumes line numbering starting from line 0
1451 # assumes line numbering starting from line 0
1456 self.selecteditemstartline = self.linesprintedtopadsofar
1452 self.selecteditemstartline = self.linesprintedtopadsofar
1457 selecteditemlines = self.getnumlinesdisplayed(
1453 selecteditemlines = self.getnumlinesdisplayed(
1458 item, recursechildren=False
1454 item, recursechildren=False
1459 )
1455 )
1460 self.selecteditemendline = (
1456 self.selecteditemendline = (
1461 self.selecteditemstartline + selecteditemlines - 1
1457 self.selecteditemstartline + selecteditemlines - 1
1462 )
1458 )
1463 return selected
1459 return selected
1464
1460
1465 def __printitem(
1461 def __printitem(
1466 self, item, ignorefolding, recursechildren, outstr, towin=True
1462 self, item, ignorefolding, recursechildren, outstr, towin=True
1467 ):
1463 ):
1468 """
1464 """
1469 recursive method for printing out patch/header/hunk/hunk-line data to
1465 recursive method for printing out patch/header/hunk/hunk-line data to
1470 screen. also returns a string with all of the content of the displayed
1466 screen. also returns a string with all of the content of the displayed
1471 patch (not including coloring, etc.).
1467 patch (not including coloring, etc.).
1472
1468
1473 if ignorefolding is True, then folded items are printed out.
1469 if ignorefolding is True, then folded items are printed out.
1474
1470
1475 if recursechildren is False, then only print the item without its
1471 if recursechildren is False, then only print the item without its
1476 child items.
1472 child items.
1477 """
1473 """
1478
1474
1479 if towin and self.outofdisplayedarea():
1475 if towin and self.outofdisplayedarea():
1480 return
1476 return
1481
1477
1482 selected = self.handleselection(item, recursechildren)
1478 selected = self.handleselection(item, recursechildren)
1483
1479
1484 # patch object is a list of headers
1480 # patch object is a list of headers
1485 if isinstance(item, patch):
1481 if isinstance(item, patch):
1486 if recursechildren:
1482 if recursechildren:
1487 for hdr in item:
1483 for hdr in item:
1488 self.__printitem(
1484 self.__printitem(
1489 hdr, ignorefolding, recursechildren, outstr, towin
1485 hdr, ignorefolding, recursechildren, outstr, towin
1490 )
1486 )
1491 # todo: eliminate all isinstance() calls
1487 # todo: eliminate all isinstance() calls
1492 if isinstance(item, uiheader):
1488 if isinstance(item, uiheader):
1493 outstr.append(
1489 outstr.append(
1494 self.printheader(
1490 self.printheader(
1495 item, selected, towin=towin, ignorefolding=ignorefolding
1491 item, selected, towin=towin, ignorefolding=ignorefolding
1496 )
1492 )
1497 )
1493 )
1498 if recursechildren:
1494 if recursechildren:
1499 for hnk in item.hunks:
1495 for hnk in item.hunks:
1500 self.__printitem(
1496 self.__printitem(
1501 hnk, ignorefolding, recursechildren, outstr, towin
1497 hnk, ignorefolding, recursechildren, outstr, towin
1502 )
1498 )
1503 elif isinstance(item, uihunk) and (
1499 elif isinstance(item, uihunk) and (
1504 (not item.header.folded) or ignorefolding
1500 (not item.header.folded) or ignorefolding
1505 ):
1501 ):
1506 # print the hunk data which comes before the changed-lines
1502 # print the hunk data which comes before the changed-lines
1507 outstr.append(
1503 outstr.append(
1508 self.printhunklinesbefore(
1504 self.printhunklinesbefore(
1509 item, selected, towin=towin, ignorefolding=ignorefolding
1505 item, selected, towin=towin, ignorefolding=ignorefolding
1510 )
1506 )
1511 )
1507 )
1512 if recursechildren:
1508 if recursechildren:
1513 for l in item.changedlines:
1509 for l in item.changedlines:
1514 self.__printitem(
1510 self.__printitem(
1515 l, ignorefolding, recursechildren, outstr, towin
1511 l, ignorefolding, recursechildren, outstr, towin
1516 )
1512 )
1517 outstr.append(
1513 outstr.append(
1518 self.printhunklinesafter(
1514 self.printhunklinesafter(
1519 item, towin=towin, ignorefolding=ignorefolding
1515 item, towin=towin, ignorefolding=ignorefolding
1520 )
1516 )
1521 )
1517 )
1522 elif isinstance(item, uihunkline) and (
1518 elif isinstance(item, uihunkline) and (
1523 (not item.hunk.folded) or ignorefolding
1519 (not item.hunk.folded) or ignorefolding
1524 ):
1520 ):
1525 outstr.append(
1521 outstr.append(
1526 self.printhunkchangedline(item, selected, towin=towin)
1522 self.printhunkchangedline(item, selected, towin=towin)
1527 )
1523 )
1528
1524
1529 return outstr
1525 return outstr
1530
1526
1531 def getnumlinesdisplayed(
1527 def getnumlinesdisplayed(
1532 self, item=None, ignorefolding=False, recursechildren=True
1528 self, item=None, ignorefolding=False, recursechildren=True
1533 ):
1529 ):
1534 """
1530 """
1535 return the number of lines which would be displayed if the item were
1531 return the number of lines which would be displayed if the item were
1536 to be printed to the display. the item will not be printed to the
1532 to be printed to the display. the item will not be printed to the
1537 display (pad).
1533 display (pad).
1538 if no item is given, assume the entire patch.
1534 if no item is given, assume the entire patch.
1539 if ignorefolding is True, folded items will be unfolded when counting
1535 if ignorefolding is True, folded items will be unfolded when counting
1540 the number of lines.
1536 the number of lines.
1541 """
1537 """
1542
1538
1543 # temporarily disable printing to windows by printstring
1539 # temporarily disable printing to windows by printstring
1544 patchdisplaystring = self.printitem(
1540 patchdisplaystring = self.printitem(
1545 item, ignorefolding, recursechildren, towin=False
1541 item, ignorefolding, recursechildren, towin=False
1546 )
1542 )
1547 numlines = len(patchdisplaystring) // self.xscreensize
1543 numlines = len(patchdisplaystring) // self.xscreensize
1548 return numlines
1544 return numlines
1549
1545
1550 def sigwinchhandler(self, n, frame):
1546 def sigwinchhandler(self, n, frame):
1551 """handle window resizing"""
1547 """handle window resizing"""
1552 try:
1548 try:
1553 curses.endwin()
1549 curses.endwin()
1554 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1550 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1555 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1551 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1556 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1552 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1557 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1553 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1558 except curses.error:
1554 except curses.error:
1559 pass
1555 pass
1560
1556
1561 def getcolorpair(
1557 def getcolorpair(
1562 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1558 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1563 ):
1559 ):
1564 """
1560 """
1565 get a curses color pair, adding it to self.colorpairs if it is not
1561 get a curses color pair, adding it to self.colorpairs if it is not
1566 already defined. an optional string, name, can be passed as a shortcut
1562 already defined. an optional string, name, can be passed as a shortcut
1567 for referring to the color-pair. by default, if no arguments are
1563 for referring to the color-pair. by default, if no arguments are
1568 specified, the white foreground / black background color-pair is
1564 specified, the white foreground / black background color-pair is
1569 returned.
1565 returned.
1570
1566
1571 it is expected that this function will be used exclusively for
1567 it is expected that this function will be used exclusively for
1572 initializing color pairs, and not curses.init_pair().
1568 initializing color pairs, and not curses.init_pair().
1573
1569
1574 attrlist is used to 'flavor' the returned color-pair. this information
1570 attrlist is used to 'flavor' the returned color-pair. this information
1575 is not stored in self.colorpairs. it contains attribute values like
1571 is not stored in self.colorpairs. it contains attribute values like
1576 curses.A_BOLD.
1572 curses.A_BOLD.
1577 """
1573 """
1578
1574
1579 if (name is not None) and name in self.colorpairnames:
1575 if (name is not None) and name in self.colorpairnames:
1580 # then get the associated color pair and return it
1576 # then get the associated color pair and return it
1581 colorpair = self.colorpairnames[name]
1577 colorpair = self.colorpairnames[name]
1582 else:
1578 else:
1583 if fgcolor is None:
1579 if fgcolor is None:
1584 fgcolor = -1
1580 fgcolor = -1
1585 if bgcolor is None:
1581 if bgcolor is None:
1586 bgcolor = -1
1582 bgcolor = -1
1587 if (fgcolor, bgcolor) in self.colorpairs:
1583 if (fgcolor, bgcolor) in self.colorpairs:
1588 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1584 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1589 else:
1585 else:
1590 pairindex = len(self.colorpairs) + 1
1586 pairindex = len(self.colorpairs) + 1
1591 if self.usecolor:
1587 if self.usecolor:
1592 curses.init_pair(pairindex, fgcolor, bgcolor)
1588 curses.init_pair(pairindex, fgcolor, bgcolor)
1593 colorpair = self.colorpairs[
1589 colorpair = self.colorpairs[
1594 (fgcolor, bgcolor)
1590 (fgcolor, bgcolor)
1595 ] = curses.color_pair(pairindex)
1591 ] = curses.color_pair(pairindex)
1596 if name is not None:
1592 if name is not None:
1597 self.colorpairnames[name] = curses.color_pair(pairindex)
1593 self.colorpairnames[name] = curses.color_pair(pairindex)
1598 else:
1594 else:
1599 cval = 0
1595 cval = 0
1600 if name is not None:
1596 if name is not None:
1601 if name == b'selected':
1597 if name == b'selected':
1602 cval = curses.A_REVERSE
1598 cval = curses.A_REVERSE
1603 self.colorpairnames[name] = cval
1599 self.colorpairnames[name] = cval
1604 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1600 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1605
1601
1606 # add attributes if possible
1602 # add attributes if possible
1607 if attrlist is None:
1603 if attrlist is None:
1608 attrlist = []
1604 attrlist = []
1609 if colorpair < 256:
1605 if colorpair < 256:
1610 # then it is safe to apply all attributes
1606 # then it is safe to apply all attributes
1611 for textattr in attrlist:
1607 for textattr in attrlist:
1612 colorpair |= textattr
1608 colorpair |= textattr
1613 else:
1609 else:
1614 # just apply a select few (safe?) attributes
1610 # just apply a select few (safe?) attributes
1615 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1611 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1616 if textattrib in attrlist:
1612 if textattrib in attrlist:
1617 colorpair |= textattrib
1613 colorpair |= textattrib
1618 return colorpair
1614 return colorpair
1619
1615
1620 def initcolorpair(self, *args, **kwargs):
1616 def initcolorpair(self, *args, **kwargs):
1621 """same as getcolorpair."""
1617 """same as getcolorpair."""
1622 self.getcolorpair(*args, **kwargs)
1618 self.getcolorpair(*args, **kwargs)
1623
1619
1624 def helpwindow(self):
1620 def helpwindow(self):
1625 """print a help window to the screen. exit after any keypress."""
1621 """print a help window to the screen. exit after any keypress."""
1626 helptext = _(
1622 helptext = _(
1627 """ [press any key to return to the patch-display]
1623 """ [press any key to return to the patch-display]
1628
1624
1629 The curses hunk selector allows you to interactively choose among the
1625 The curses hunk selector allows you to interactively choose among the
1630 changes you have made, and confirm only those changes you select for
1626 changes you have made, and confirm only those changes you select for
1631 further processing by the command you are running (such as commit,
1627 further processing by the command you are running (such as commit,
1632 shelve, or revert). After confirming the selected changes, the
1628 shelve, or revert). After confirming the selected changes, the
1633 unselected changes are still present in your working copy, so you can
1629 unselected changes are still present in your working copy, so you can
1634 use the hunk selector multiple times to split large changes into
1630 use the hunk selector multiple times to split large changes into
1635 smaller changesets. the following are valid keystrokes:
1631 smaller changesets. the following are valid keystrokes:
1636
1632
1637 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1633 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1638 [enter] : (un-)select item and go to next item of same type
1634 [enter] : (un-)select item and go to next item of same type
1639 A : (un-)select all items
1635 A : (un-)select all items
1640 X : (un-)select all items between current and most-recent
1636 X : (un-)select all items between current and most-recent
1641 up/down-arrow [k/j] : go to previous/next unfolded item
1637 up/down-arrow [k/j] : go to previous/next unfolded item
1642 pgup/pgdn [K/J] : go to previous/next item of same type
1638 pgup/pgdn [K/J] : go to previous/next item of same type
1643 right/left-arrow [l/h] : go to child item / parent item
1639 right/left-arrow [l/h] : go to child item / parent item
1644 shift-left-arrow [H] : go to parent header / fold selected header
1640 shift-left-arrow [H] : go to parent header / fold selected header
1645 g : go to the top
1641 g : go to the top
1646 G : go to the bottom
1642 G : go to the bottom
1647 f : fold / unfold item, hiding/revealing its children
1643 f : fold / unfold item, hiding/revealing its children
1648 F : fold / unfold parent item and all of its ancestors
1644 F : fold / unfold parent item and all of its ancestors
1649 ctrl-l : scroll the selected line to the top of the screen
1645 ctrl-l : scroll the selected line to the top of the screen
1650 m : edit / resume editing the commit message
1646 m : edit / resume editing the commit message
1651 e : edit the currently selected hunk
1647 e : edit the currently selected hunk
1652 a : toggle all selections
1648 a : toggle all selections
1653 c : confirm selected changes
1649 c : confirm selected changes
1654 r : review/edit and confirm selected changes
1650 r : review/edit and confirm selected changes
1655 q : quit without confirming (no changes will be made)
1651 q : quit without confirming (no changes will be made)
1656 ? : help (what you're currently reading)"""
1652 ? : help (what you're currently reading)"""
1657 )
1653 )
1658
1654
1659 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1655 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1660 helplines = helptext.split(b"\n")
1656 helplines = helptext.split(b"\n")
1661 helplines = helplines + [b" "] * (
1657 helplines = helplines + [b" "] * (
1662 self.yscreensize - self.numstatuslines - len(helplines) - 1
1658 self.yscreensize - self.numstatuslines - len(helplines) - 1
1663 )
1659 )
1664 try:
1660 try:
1665 for line in helplines:
1661 for line in helplines:
1666 self.printstring(helpwin, line, pairname=b"legend")
1662 self.printstring(helpwin, line, pairname=b"legend")
1667 except curses.error:
1663 except curses.error:
1668 pass
1664 pass
1669 helpwin.refresh()
1665 helpwin.refresh()
1670 try:
1666 try:
1671 with self.ui.timeblockedsection(b'crecord'):
1667 with self.ui.timeblockedsection(b'crecord'):
1672 helpwin.getkey()
1668 helpwin.getkey()
1673 except curses.error:
1669 except curses.error:
1674 pass
1670 pass
1675
1671
1676 def commitMessageWindow(self):
1672 def commitMessageWindow(self):
1677 """Create a temporary commit message editing window on the screen."""
1673 """Create a temporary commit message editing window on the screen."""
1678
1674
1679 curses.raw()
1675 curses.raw()
1680 curses.def_prog_mode()
1676 curses.def_prog_mode()
1681 curses.endwin()
1677 curses.endwin()
1682 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1678 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1683 curses.cbreak()
1679 curses.cbreak()
1684 self.stdscr.refresh()
1680 self.stdscr.refresh()
1685 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1681 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1686
1682
1687 def handlefirstlineevent(self):
1683 def handlefirstlineevent(self):
1688 """
1684 """
1689 Handle 'g' to navigate to the top most file in the ncurses window.
1685 Handle 'g' to navigate to the top most file in the ncurses window.
1690 """
1686 """
1691 self.currentselecteditem = self.headerlist[0]
1687 self.currentselecteditem = self.headerlist[0]
1692 currentitem = self.currentselecteditem
1688 currentitem = self.currentselecteditem
1693 # select the parent item recursively until we're at a header
1689 # select the parent item recursively until we're at a header
1694 while True:
1690 while True:
1695 nextitem = currentitem.parentitem()
1691 nextitem = currentitem.parentitem()
1696 if nextitem is None:
1692 if nextitem is None:
1697 break
1693 break
1698 else:
1694 else:
1699 currentitem = nextitem
1695 currentitem = nextitem
1700
1696
1701 self.currentselecteditem = currentitem
1697 self.currentselecteditem = currentitem
1702
1698
1703 def handlelastlineevent(self):
1699 def handlelastlineevent(self):
1704 """
1700 """
1705 Handle 'G' to navigate to the bottom most file/hunk/line depending
1701 Handle 'G' to navigate to the bottom most file/hunk/line depending
1706 on the whether the fold is active or not.
1702 on the whether the fold is active or not.
1707
1703
1708 If the bottom most file is folded, it navigates to that file and
1704 If the bottom most file is folded, it navigates to that file and
1709 stops there. If the bottom most file is unfolded, it navigates to
1705 stops there. If the bottom most file is unfolded, it navigates to
1710 the bottom most hunk in that file and stops there. If the bottom most
1706 the bottom most hunk in that file and stops there. If the bottom most
1711 hunk is unfolded, it navigates to the bottom most line in that hunk.
1707 hunk is unfolded, it navigates to the bottom most line in that hunk.
1712 """
1708 """
1713 currentitem = self.currentselecteditem
1709 currentitem = self.currentselecteditem
1714 nextitem = currentitem.nextitem()
1710 nextitem = currentitem.nextitem()
1715 # select the child item recursively until we're at a footer
1711 # select the child item recursively until we're at a footer
1716 while nextitem is not None:
1712 while nextitem is not None:
1717 nextitem = currentitem.nextitem()
1713 nextitem = currentitem.nextitem()
1718 if nextitem is None:
1714 if nextitem is None:
1719 break
1715 break
1720 else:
1716 else:
1721 currentitem = nextitem
1717 currentitem = nextitem
1722
1718
1723 self.currentselecteditem = currentitem
1719 self.currentselecteditem = currentitem
1724 self.recenterdisplayedarea()
1720 self.recenterdisplayedarea()
1725
1721
1726 def confirmationwindow(self, windowtext):
1722 def confirmationwindow(self, windowtext):
1727 """display an informational window, then wait for and return a
1723 """display an informational window, then wait for and return a
1728 keypress."""
1724 keypress."""
1729
1725
1730 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1726 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1731 try:
1727 try:
1732 lines = windowtext.split(b"\n")
1728 lines = windowtext.split(b"\n")
1733 for line in lines:
1729 for line in lines:
1734 self.printstring(confirmwin, line, pairname=b"selected")
1730 self.printstring(confirmwin, line, pairname=b"selected")
1735 except curses.error:
1731 except curses.error:
1736 pass
1732 pass
1737 self.stdscr.refresh()
1733 self.stdscr.refresh()
1738 confirmwin.refresh()
1734 confirmwin.refresh()
1739 try:
1735 try:
1740 with self.ui.timeblockedsection(b'crecord'):
1736 with self.ui.timeblockedsection(b'crecord'):
1741 response = chr(self.stdscr.getch())
1737 response = chr(self.stdscr.getch())
1742 except ValueError:
1738 except ValueError:
1743 response = None
1739 response = None
1744
1740
1745 return response
1741 return response
1746
1742
1747 def reviewcommit(self):
1743 def reviewcommit(self):
1748 """ask for 'y' to be pressed to confirm selected. return True if
1744 """ask for 'y' to be pressed to confirm selected. return True if
1749 confirmed."""
1745 confirmed."""
1750 confirmtext = _(
1746 confirmtext = _(
1751 """If you answer yes to the following, your currently chosen patch chunks
1747 """If you answer yes to the following, your currently chosen patch chunks
1752 will be loaded into an editor. To modify the patch, make the changes in your
1748 will be loaded into an editor. To modify the patch, make the changes in your
1753 editor and save. To accept the current patch as-is, close the editor without
1749 editor and save. To accept the current patch as-is, close the editor without
1754 saving.
1750 saving.
1755
1751
1756 note: don't add/remove lines unless you also modify the range information.
1752 note: don't add/remove lines unless you also modify the range information.
1757 failing to follow this rule will result in the commit aborting.
1753 failing to follow this rule will result in the commit aborting.
1758
1754
1759 are you sure you want to review/edit and confirm the selected changes [yn]?
1755 are you sure you want to review/edit and confirm the selected changes [yn]?
1760 """
1756 """
1761 )
1757 )
1762 with self.ui.timeblockedsection(b'crecord'):
1758 with self.ui.timeblockedsection(b'crecord'):
1763 response = self.confirmationwindow(confirmtext)
1759 response = self.confirmationwindow(confirmtext)
1764 if response is None:
1760 if response is None:
1765 response = "n"
1761 response = "n"
1766 if response.lower().startswith("y"):
1762 if response.lower().startswith("y"):
1767 return True
1763 return True
1768 else:
1764 else:
1769 return False
1765 return False
1770
1766
1771 def recenterdisplayedarea(self):
1767 def recenterdisplayedarea(self):
1772 """
1768 """
1773 once we scrolled with pg up pg down we can be pointing outside of the
1769 once we scrolled with pg up pg down we can be pointing outside of the
1774 display zone. we print the patch with towin=False to compute the
1770 display zone. we print the patch with towin=False to compute the
1775 location of the selected item even though it is outside of the displayed
1771 location of the selected item even though it is outside of the displayed
1776 zone and then update the scroll.
1772 zone and then update the scroll.
1777 """
1773 """
1778 self.printitem(towin=False)
1774 self.printitem(towin=False)
1779 self.updatescroll()
1775 self.updatescroll()
1780
1776
1781 def toggleedit(self, item=None, test=False):
1777 def toggleedit(self, item=None, test=False):
1782 """
1778 """
1783 edit the currently selected chunk
1779 edit the currently selected chunk
1784 """
1780 """
1785
1781
1786 def updateui(self):
1782 def updateui(self):
1787 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1783 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1788 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1784 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1789 self.updatescroll()
1785 self.updatescroll()
1790 self.stdscr.refresh()
1786 self.stdscr.refresh()
1791 self.statuswin.refresh()
1787 self.statuswin.refresh()
1792 self.stdscr.keypad(1)
1788 self.stdscr.keypad(1)
1793
1789
1794 def editpatchwitheditor(self, chunk):
1790 def editpatchwitheditor(self, chunk):
1795 if chunk is None:
1791 if chunk is None:
1796 self.ui.write(_(b'cannot edit patch for whole file'))
1792 self.ui.write(_(b'cannot edit patch for whole file'))
1797 self.ui.write(b"\n")
1793 self.ui.write(b"\n")
1798 return None
1794 return None
1799 if chunk.header.binary():
1795 if chunk.header.binary():
1800 self.ui.write(_(b'cannot edit patch for binary file'))
1796 self.ui.write(_(b'cannot edit patch for binary file'))
1801 self.ui.write(b"\n")
1797 self.ui.write(b"\n")
1802 return None
1798 return None
1803
1799
1804 # write the initial patch
1800 # write the initial patch
1805 patch = stringio()
1801 patch = stringio()
1806 patch.write(diffhelptext + hunkhelptext)
1802 patch.write(diffhelptext + hunkhelptext)
1807 chunk.header.write(patch)
1803 chunk.header.write(patch)
1808 chunk.write(patch)
1804 chunk.write(patch)
1809
1805
1810 # start the editor and wait for it to complete
1806 # start the editor and wait for it to complete
1811 try:
1807 try:
1812 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1808 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1813 except error.Abort as exc:
1809 except error.Abort as exc:
1814 self.errorstr = stringutil.forcebytestr(exc)
1810 self.errorstr = stringutil.forcebytestr(exc)
1815 return None
1811 return None
1816 finally:
1812 finally:
1817 self.stdscr.clear()
1813 self.stdscr.clear()
1818 self.stdscr.refresh()
1814 self.stdscr.refresh()
1819
1815
1820 # remove comment lines
1816 # remove comment lines
1821 patch = [
1817 patch = [
1822 line + b'\n'
1818 line + b'\n'
1823 for line in patch.splitlines()
1819 for line in patch.splitlines()
1824 if not line.startswith(b'#')
1820 if not line.startswith(b'#')
1825 ]
1821 ]
1826 return patchmod.parsepatch(patch)
1822 return patchmod.parsepatch(patch)
1827
1823
1828 if item is None:
1824 if item is None:
1829 item = self.currentselecteditem
1825 item = self.currentselecteditem
1830 if isinstance(item, uiheader):
1826 if isinstance(item, uiheader):
1831 return
1827 return
1832 if isinstance(item, uihunkline):
1828 if isinstance(item, uihunkline):
1833 item = item.parentitem()
1829 item = item.parentitem()
1834 if not isinstance(item, uihunk):
1830 if not isinstance(item, uihunk):
1835 return
1831 return
1836
1832
1837 # To go back to that hunk or its replacement at the end of the edit
1833 # To go back to that hunk or its replacement at the end of the edit
1838 itemindex = item.parentitem().hunks.index(item)
1834 itemindex = item.parentitem().hunks.index(item)
1839
1835
1840 beforeadded, beforeremoved = item.added, item.removed
1836 beforeadded, beforeremoved = item.added, item.removed
1841 newpatches = editpatchwitheditor(self, item)
1837 newpatches = editpatchwitheditor(self, item)
1842 if newpatches is None:
1838 if newpatches is None:
1843 if not test:
1839 if not test:
1844 updateui(self)
1840 updateui(self)
1845 return
1841 return
1846 header = item.header
1842 header = item.header
1847 editedhunkindex = header.hunks.index(item)
1843 editedhunkindex = header.hunks.index(item)
1848 hunksbefore = header.hunks[:editedhunkindex]
1844 hunksbefore = header.hunks[:editedhunkindex]
1849 hunksafter = header.hunks[editedhunkindex + 1 :]
1845 hunksafter = header.hunks[editedhunkindex + 1 :]
1850 newpatchheader = newpatches[0]
1846 newpatchheader = newpatches[0]
1851 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1847 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1852 newadded = sum([h.added for h in newhunks])
1848 newadded = sum([h.added for h in newhunks])
1853 newremoved = sum([h.removed for h in newhunks])
1849 newremoved = sum([h.removed for h in newhunks])
1854 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1850 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1855
1851
1856 for h in hunksafter:
1852 for h in hunksafter:
1857 h.toline += offset
1853 h.toline += offset
1858 for h in newhunks:
1854 for h in newhunks:
1859 h.folded = False
1855 h.folded = False
1860 header.hunks = hunksbefore + newhunks + hunksafter
1856 header.hunks = hunksbefore + newhunks + hunksafter
1861 if self.emptypatch():
1857 if self.emptypatch():
1862 header.hunks = hunksbefore + [item] + hunksafter
1858 header.hunks = hunksbefore + [item] + hunksafter
1863 self.currentselecteditem = header
1859 self.currentselecteditem = header
1864 if len(header.hunks) > itemindex:
1860 if len(header.hunks) > itemindex:
1865 self.currentselecteditem = header.hunks[itemindex]
1861 self.currentselecteditem = header.hunks[itemindex]
1866
1862
1867 if not test:
1863 if not test:
1868 updateui(self)
1864 updateui(self)
1869
1865
1870 def emptypatch(self):
1866 def emptypatch(self):
1871 item = self.headerlist
1867 item = self.headerlist
1872 if not item:
1868 if not item:
1873 return True
1869 return True
1874 for header in item:
1870 for header in item:
1875 if header.hunks:
1871 if header.hunks:
1876 return False
1872 return False
1877 return True
1873 return True
1878
1874
1879 def handlekeypressed(self, keypressed, test=False):
1875 def handlekeypressed(self, keypressed, test=False):
1880 """
1876 """
1881 Perform actions based on pressed keys.
1877 Perform actions based on pressed keys.
1882
1878
1883 Return true to exit the main loop.
1879 Return true to exit the main loop.
1884 """
1880 """
1885 if keypressed in ["k", "KEY_UP"]:
1881 if keypressed in ["k", "KEY_UP"]:
1886 self.uparrowevent()
1882 self.uparrowevent()
1887 elif keypressed in ["K", "KEY_PPAGE"]:
1883 elif keypressed in ["K", "KEY_PPAGE"]:
1888 self.uparrowshiftevent()
1884 self.uparrowshiftevent()
1889 elif keypressed in ["j", "KEY_DOWN"]:
1885 elif keypressed in ["j", "KEY_DOWN"]:
1890 self.downarrowevent()
1886 self.downarrowevent()
1891 elif keypressed in ["J", "KEY_NPAGE"]:
1887 elif keypressed in ["J", "KEY_NPAGE"]:
1892 self.downarrowshiftevent()
1888 self.downarrowshiftevent()
1893 elif keypressed in ["l", "KEY_RIGHT"]:
1889 elif keypressed in ["l", "KEY_RIGHT"]:
1894 self.rightarrowevent()
1890 self.rightarrowevent()
1895 elif keypressed in ["h", "KEY_LEFT"]:
1891 elif keypressed in ["h", "KEY_LEFT"]:
1896 self.leftarrowevent()
1892 self.leftarrowevent()
1897 elif keypressed in ["H", "KEY_SLEFT"]:
1893 elif keypressed in ["H", "KEY_SLEFT"]:
1898 self.leftarrowshiftevent()
1894 self.leftarrowshiftevent()
1899 elif keypressed in ["q"]:
1895 elif keypressed in ["q"]:
1900 raise error.Abort(_(b'user quit'))
1896 raise error.Abort(_(b'user quit'))
1901 elif keypressed in ['a']:
1897 elif keypressed in ['a']:
1902 self.flipselections()
1898 self.flipselections()
1903 elif keypressed in ["c"]:
1899 elif keypressed in ["c"]:
1904 return True
1900 return True
1905 elif keypressed in ["r"]:
1901 elif keypressed in ["r"]:
1906 if self.reviewcommit():
1902 if self.reviewcommit():
1907 self.opts[b'review'] = True
1903 self.opts[b'review'] = True
1908 return True
1904 return True
1909 elif test and keypressed in ["R"]:
1905 elif test and keypressed in ["R"]:
1910 self.opts[b'review'] = True
1906 self.opts[b'review'] = True
1911 return True
1907 return True
1912 elif keypressed in [" ", "x"]:
1908 elif keypressed in [" ", "x"]:
1913 self.toggleapply()
1909 self.toggleapply()
1914 elif keypressed in ["\n", "KEY_ENTER"]:
1910 elif keypressed in ["\n", "KEY_ENTER"]:
1915 self.toggleapply()
1911 self.toggleapply()
1916 self.nextsametype(test=test)
1912 self.nextsametype(test=test)
1917 elif keypressed in ["X"]:
1913 elif keypressed in ["X"]:
1918 self.toggleallbetween()
1914 self.toggleallbetween()
1919 elif keypressed in ["A"]:
1915 elif keypressed in ["A"]:
1920 self.toggleall()
1916 self.toggleall()
1921 elif keypressed in ["e"]:
1917 elif keypressed in ["e"]:
1922 self.toggleedit(test=test)
1918 self.toggleedit(test=test)
1923 elif keypressed in ["f"]:
1919 elif keypressed in ["f"]:
1924 self.togglefolded()
1920 self.togglefolded()
1925 elif keypressed in ["F"]:
1921 elif keypressed in ["F"]:
1926 self.togglefolded(foldparent=True)
1922 self.togglefolded(foldparent=True)
1927 elif keypressed in ["m"]:
1923 elif keypressed in ["m"]:
1928 self.commitMessageWindow()
1924 self.commitMessageWindow()
1929 elif keypressed in ["g", "KEY_HOME"]:
1925 elif keypressed in ["g", "KEY_HOME"]:
1930 self.handlefirstlineevent()
1926 self.handlefirstlineevent()
1931 elif keypressed in ["G", "KEY_END"]:
1927 elif keypressed in ["G", "KEY_END"]:
1932 self.handlelastlineevent()
1928 self.handlelastlineevent()
1933 elif keypressed in ["?"]:
1929 elif keypressed in ["?"]:
1934 self.helpwindow()
1930 self.helpwindow()
1935 self.stdscr.clear()
1931 self.stdscr.clear()
1936 self.stdscr.refresh()
1932 self.stdscr.refresh()
1937 elif keypressed in [curses.ascii.ctrl("L")]:
1933 elif keypressed in [curses.ascii.ctrl("L")]:
1938 # scroll the current line to the top of the screen, and redraw
1934 # scroll the current line to the top of the screen, and redraw
1939 # everything
1935 # everything
1940 self.scrolllines(self.selecteditemstartline)
1936 self.scrolllines(self.selecteditemstartline)
1941 self.stdscr.clear()
1937 self.stdscr.clear()
1942 self.stdscr.refresh()
1938 self.stdscr.refresh()
1943
1939
1944 def main(self, stdscr):
1940 def main(self, stdscr):
1945 """
1941 """
1946 method to be wrapped by curses.wrapper() for selecting chunks.
1942 method to be wrapped by curses.wrapper() for selecting chunks.
1947 """
1943 """
1948
1944
1949 origsigwinch = sentinel = object()
1945 origsigwinch = sentinel = object()
1950 if util.safehasattr(signal, b'SIGWINCH'):
1946 if util.safehasattr(signal, b'SIGWINCH'):
1951 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1947 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1952 try:
1948 try:
1953 return self._main(stdscr)
1949 return self._main(stdscr)
1954 finally:
1950 finally:
1955 if origsigwinch is not sentinel:
1951 if origsigwinch is not sentinel:
1956 signal.signal(signal.SIGWINCH, origsigwinch)
1952 signal.signal(signal.SIGWINCH, origsigwinch)
1957
1953
1958 def _main(self, stdscr):
1954 def _main(self, stdscr):
1959 self.stdscr = stdscr
1955 self.stdscr = stdscr
1960 # error during initialization, cannot be printed in the curses
1956 # error during initialization, cannot be printed in the curses
1961 # interface, it should be printed by the calling code
1957 # interface, it should be printed by the calling code
1962 self.initexc = None
1958 self.initexc = None
1963 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1959 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1964
1960
1965 curses.start_color()
1961 curses.start_color()
1966 try:
1962 try:
1967 curses.use_default_colors()
1963 curses.use_default_colors()
1968 except curses.error:
1964 except curses.error:
1969 self.usecolor = False
1965 self.usecolor = False
1970
1966
1971 # In some situations we may have some cruft left on the "alternate
1967 # In some situations we may have some cruft left on the "alternate
1972 # screen" from another program (or previous iterations of ourself), and
1968 # screen" from another program (or previous iterations of ourself), and
1973 # we won't clear it if the scroll region is small enough to comfortably
1969 # we won't clear it if the scroll region is small enough to comfortably
1974 # fit on the terminal.
1970 # fit on the terminal.
1975 self.stdscr.clear()
1971 self.stdscr.clear()
1976
1972
1977 # don't display the cursor
1973 # don't display the cursor
1978 try:
1974 try:
1979 curses.curs_set(0)
1975 curses.curs_set(0)
1980 except curses.error:
1976 except curses.error:
1981 pass
1977 pass
1982
1978
1983 # available colors: black, blue, cyan, green, magenta, white, yellow
1979 # available colors: black, blue, cyan, green, magenta, white, yellow
1984 # init_pair(color_id, foreground_color, background_color)
1980 # init_pair(color_id, foreground_color, background_color)
1985 self.initcolorpair(None, None, name=b"normal")
1981 self.initcolorpair(None, None, name=b"normal")
1986 self.initcolorpair(
1982 self.initcolorpair(
1987 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1983 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1988 )
1984 )
1989 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1985 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1990 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1986 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1991 self.initcolorpair(
1987 self.initcolorpair(
1992 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1988 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1993 )
1989 )
1994 # newwin([height, width,] begin_y, begin_x)
1990 # newwin([height, width,] begin_y, begin_x)
1995 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1991 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1996 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1992 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1997
1993
1998 # figure out how much space to allocate for the chunk-pad which is
1994 # figure out how much space to allocate for the chunk-pad which is
1999 # used for displaying the patch
1995 # used for displaying the patch
2000
1996
2001 # stupid hack to prevent getnumlinesdisplayed from failing
1997 # stupid hack to prevent getnumlinesdisplayed from failing
2002 self.chunkpad = curses.newpad(1, self.xscreensize)
1998 self.chunkpad = curses.newpad(1, self.xscreensize)
2003
1999
2004 # add 1 so to account for last line text reaching end of line
2000 # add 1 so to account for last line text reaching end of line
2005 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
2001 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
2006
2002
2007 try:
2003 try:
2008 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
2004 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
2009 except curses.error:
2005 except curses.error:
2010 self.initexc = fallbackerror(
2006 self.initexc = fallbackerror(
2011 _(b'this diff is too large to be displayed')
2007 _(b'this diff is too large to be displayed')
2012 )
2008 )
2013 return
2009 return
2014 # initialize selecteditemendline (initial start-line is 0)
2010 # initialize selecteditemendline (initial start-line is 0)
2015 self.selecteditemendline = self.getnumlinesdisplayed(
2011 self.selecteditemendline = self.getnumlinesdisplayed(
2016 self.currentselecteditem, recursechildren=False
2012 self.currentselecteditem, recursechildren=False
2017 )
2013 )
2018
2014
2019 while True:
2015 while True:
2020 self.updatescreen()
2016 self.updatescreen()
2021 try:
2017 try:
2022 with self.ui.timeblockedsection(b'crecord'):
2018 with self.ui.timeblockedsection(b'crecord'):
2023 keypressed = self.statuswin.getkey()
2019 keypressed = self.statuswin.getkey()
2024 if self.errorstr is not None:
2020 if self.errorstr is not None:
2025 self.errorstr = None
2021 self.errorstr = None
2026 continue
2022 continue
2027 except curses.error:
2023 except curses.error:
2028 keypressed = b"foobar"
2024 keypressed = b"foobar"
2029 if self.handlekeypressed(keypressed):
2025 if self.handlekeypressed(keypressed):
2030 break
2026 break
2031
2027
2032 if self.commenttext != b"":
2028 if self.commenttext != b"":
2033 whitespaceremoved = re.sub(
2029 whitespaceremoved = re.sub(
2034 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2030 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2035 )
2031 )
2036 if whitespaceremoved != b"":
2032 if whitespaceremoved != b"":
2037 self.opts[b'message'] = self.commenttext
2033 self.opts[b'message'] = self.commenttext
General Comments 0
You need to be logged in to leave comments. Login now