##// END OF EJS Templates
convert: stop using the `pycompat.open()` shim
Matt Harbison -
r53272:bb07b1ca default
parent child Browse files
Show More
@@ -1,620 +1,619
1 # common.py - common code for the convert extension
1 # common.py - common code for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
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 from __future__ import annotations
8 from __future__ import annotations
9
9
10 import base64
10 import base64
11 import os
11 import os
12 import pickle
12 import pickle
13 import re
13 import re
14 import shlex
14 import shlex
15 import subprocess
15 import subprocess
16 import typing
16 import typing
17
17
18 from typing import (
18 from typing import (
19 Any,
19 Any,
20 AnyStr,
20 AnyStr,
21 Optional,
21 Optional,
22 )
22 )
23
23
24 from mercurial.i18n import _
24 from mercurial.i18n import _
25 from mercurial.pycompat import open
26 from mercurial import (
25 from mercurial import (
27 encoding,
26 encoding,
28 error,
27 error,
29 phases,
28 phases,
30 pycompat,
29 pycompat,
31 util,
30 util,
32 )
31 )
33 from mercurial.utils import (
32 from mercurial.utils import (
34 dateutil,
33 dateutil,
35 procutil,
34 procutil,
36 )
35 )
37
36
38 if typing.TYPE_CHECKING:
37 if typing.TYPE_CHECKING:
39 from typing import (
38 from typing import (
40 overload,
39 overload,
41 )
40 )
42 from mercurial import (
41 from mercurial import (
43 ui as uimod,
42 ui as uimod,
44 )
43 )
45
44
46 propertycache = util.propertycache
45 propertycache = util.propertycache
47
46
48
47
49 if typing.TYPE_CHECKING:
48 if typing.TYPE_CHECKING:
50
49
51 @overload
50 @overload
52 def _encodeornone(d: str) -> bytes:
51 def _encodeornone(d: str) -> bytes:
53 pass
52 pass
54
53
55 @overload
54 @overload
56 def _encodeornone(d: None) -> None:
55 def _encodeornone(d: None) -> None:
57 pass
56 pass
58
57
59
58
60 def _encodeornone(d):
59 def _encodeornone(d):
61 if d is None:
60 if d is None:
62 return
61 return
63 return d.encode('latin1')
62 return d.encode('latin1')
64
63
65
64
66 class _shlexpy3proxy:
65 class _shlexpy3proxy:
67 def __init__(self, l: shlex.shlex) -> None:
66 def __init__(self, l: shlex.shlex) -> None:
68 self._l = l
67 self._l = l
69
68
70 def __iter__(self):
69 def __iter__(self):
71 return (_encodeornone(v) for v in self._l)
70 return (_encodeornone(v) for v in self._l)
72
71
73 def get_token(self):
72 def get_token(self):
74 return _encodeornone(self._l.get_token())
73 return _encodeornone(self._l.get_token())
75
74
76 @property
75 @property
77 def infile(self) -> bytes:
76 def infile(self) -> bytes:
78 if self._l.infile is not None:
77 if self._l.infile is not None:
79 return encoding.strtolocal(self._l.infile)
78 return encoding.strtolocal(self._l.infile)
80 return b'<unknown>'
79 return b'<unknown>'
81
80
82 @property
81 @property
83 def lineno(self) -> int:
82 def lineno(self) -> int:
84 return self._l.lineno
83 return self._l.lineno
85
84
86
85
87 def shlexer(
86 def shlexer(
88 data=None,
87 data=None,
89 filepath: Optional[bytes] = None,
88 filepath: Optional[bytes] = None,
90 wordchars: Optional[bytes] = None,
89 wordchars: Optional[bytes] = None,
91 whitespace: Optional[bytes] = None,
90 whitespace: Optional[bytes] = None,
92 ):
91 ):
93 if data is None:
92 if data is None:
94 data = open(filepath, b'r', encoding='latin1')
93 data = open(filepath, 'r', encoding='latin1')
95 else:
94 else:
96 if filepath is not None:
95 if filepath is not None:
97 raise error.ProgrammingError(
96 raise error.ProgrammingError(
98 b'shlexer only accepts data or filepath, not both'
97 b'shlexer only accepts data or filepath, not both'
99 )
98 )
100 data = data.decode('latin1')
99 data = data.decode('latin1')
101 infile = encoding.strfromlocal(filepath) if filepath is not None else None
100 infile = encoding.strfromlocal(filepath) if filepath is not None else None
102 l = shlex.shlex(data, infile=infile, posix=True)
101 l = shlex.shlex(data, infile=infile, posix=True)
103 if whitespace is not None:
102 if whitespace is not None:
104 l.whitespace_split = True
103 l.whitespace_split = True
105 l.whitespace += whitespace.decode('latin1')
104 l.whitespace += whitespace.decode('latin1')
106 if wordchars is not None:
105 if wordchars is not None:
107 l.wordchars += wordchars.decode('latin1')
106 l.wordchars += wordchars.decode('latin1')
108 return _shlexpy3proxy(l)
107 return _shlexpy3proxy(l)
109
108
110
109
111 def encodeargs(args: Any) -> bytes:
110 def encodeargs(args: Any) -> bytes:
112 def encodearg(s: bytes) -> bytes:
111 def encodearg(s: bytes) -> bytes:
113 lines = base64.encodebytes(s)
112 lines = base64.encodebytes(s)
114 lines = [l.splitlines()[0] for l in pycompat.iterbytestr(lines)]
113 lines = [l.splitlines()[0] for l in pycompat.iterbytestr(lines)]
115 return b''.join(lines)
114 return b''.join(lines)
116
115
117 s = pickle.dumps(args)
116 s = pickle.dumps(args)
118 return encodearg(s)
117 return encodearg(s)
119
118
120
119
121 def decodeargs(s: bytes) -> Any:
120 def decodeargs(s: bytes) -> Any:
122 s = base64.decodebytes(s)
121 s = base64.decodebytes(s)
123 return pickle.loads(s)
122 return pickle.loads(s)
124
123
125
124
126 class MissingTool(Exception):
125 class MissingTool(Exception):
127 pass
126 pass
128
127
129
128
130 def checktool(
129 def checktool(
131 exe: bytes, name: Optional[bytes] = None, abort: bool = True
130 exe: bytes, name: Optional[bytes] = None, abort: bool = True
132 ) -> None:
131 ) -> None:
133 name = name or exe
132 name = name or exe
134 if not procutil.findexe(exe):
133 if not procutil.findexe(exe):
135 if abort:
134 if abort:
136 exc = error.Abort
135 exc = error.Abort
137 else:
136 else:
138 exc = MissingTool
137 exc = MissingTool
139 raise exc(_(b'cannot find required "%s" tool') % name)
138 raise exc(_(b'cannot find required "%s" tool') % name)
140
139
141
140
142 class NoRepo(Exception):
141 class NoRepo(Exception):
143 pass
142 pass
144
143
145
144
146 SKIPREV: bytes = b'SKIP'
145 SKIPREV: bytes = b'SKIP'
147
146
148
147
149 class commit:
148 class commit:
150 def __init__(
149 def __init__(
151 self,
150 self,
152 author: bytes,
151 author: bytes,
153 date: bytes,
152 date: bytes,
154 desc: bytes,
153 desc: bytes,
155 parents,
154 parents,
156 branch: Optional[bytes] = None,
155 branch: Optional[bytes] = None,
157 rev=None,
156 rev=None,
158 extra=None,
157 extra=None,
159 sortkey=None,
158 sortkey=None,
160 saverev=True,
159 saverev=True,
161 phase: int = phases.draft,
160 phase: int = phases.draft,
162 optparents=None,
161 optparents=None,
163 ctx=None,
162 ctx=None,
164 ) -> None:
163 ) -> None:
165 self.author = author or b'unknown'
164 self.author = author or b'unknown'
166 self.date = date or b'0 0'
165 self.date = date or b'0 0'
167 self.desc = desc
166 self.desc = desc
168 self.parents = parents # will be converted and used as parents
167 self.parents = parents # will be converted and used as parents
169 self.optparents = optparents or [] # will be used if already converted
168 self.optparents = optparents or [] # will be used if already converted
170 self.branch = branch
169 self.branch = branch
171 self.rev = rev
170 self.rev = rev
172 self.extra = extra or {}
171 self.extra = extra or {}
173 self.sortkey = sortkey
172 self.sortkey = sortkey
174 self.saverev = saverev
173 self.saverev = saverev
175 self.phase = phase
174 self.phase = phase
176 self.ctx = ctx # for hg to hg conversions
175 self.ctx = ctx # for hg to hg conversions
177
176
178
177
179 class converter_source:
178 class converter_source:
180 """Conversion source interface"""
179 """Conversion source interface"""
181
180
182 def __init__(
181 def __init__(
183 self,
182 self,
184 ui: "uimod.ui",
183 ui: "uimod.ui",
185 repotype: bytes,
184 repotype: bytes,
186 path: Optional[bytes] = None,
185 path: Optional[bytes] = None,
187 revs=None,
186 revs=None,
188 ) -> None:
187 ) -> None:
189 """Initialize conversion source (or raise NoRepo("message")
188 """Initialize conversion source (or raise NoRepo("message")
190 exception if path is not a valid repository)"""
189 exception if path is not a valid repository)"""
191 self.ui = ui
190 self.ui = ui
192 self.path = path
191 self.path = path
193 self.revs = revs
192 self.revs = revs
194 self.repotype = repotype
193 self.repotype = repotype
195
194
196 self.encoding = b'utf-8'
195 self.encoding = b'utf-8'
197
196
198 def checkhexformat(
197 def checkhexformat(
199 self, revstr: bytes, mapname: bytes = b'splicemap'
198 self, revstr: bytes, mapname: bytes = b'splicemap'
200 ) -> None:
199 ) -> None:
201 """fails if revstr is not a 40 byte hex. mercurial and git both uses
200 """fails if revstr is not a 40 byte hex. mercurial and git both uses
202 such format for their revision numbering
201 such format for their revision numbering
203 """
202 """
204 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr):
203 if not re.match(br'[0-9a-fA-F]{40,40}$', revstr):
205 raise error.Abort(
204 raise error.Abort(
206 _(b'%s entry %s is not a valid revision identifier')
205 _(b'%s entry %s is not a valid revision identifier')
207 % (mapname, revstr)
206 % (mapname, revstr)
208 )
207 )
209
208
210 def before(self) -> None:
209 def before(self) -> None:
211 pass
210 pass
212
211
213 def after(self) -> None:
212 def after(self) -> None:
214 pass
213 pass
215
214
216 def targetfilebelongstosource(self, targetfilename):
215 def targetfilebelongstosource(self, targetfilename):
217 """Returns true if the given targetfile belongs to the source repo. This
216 """Returns true if the given targetfile belongs to the source repo. This
218 is useful when only a subdirectory of the target belongs to the source
217 is useful when only a subdirectory of the target belongs to the source
219 repo."""
218 repo."""
220 # For normal full repo converts, this is always True.
219 # For normal full repo converts, this is always True.
221 return True
220 return True
222
221
223 def setrevmap(self, revmap):
222 def setrevmap(self, revmap):
224 """set the map of already-converted revisions"""
223 """set the map of already-converted revisions"""
225
224
226 def getheads(self):
225 def getheads(self):
227 """Return a list of this repository's heads"""
226 """Return a list of this repository's heads"""
228 raise NotImplementedError
227 raise NotImplementedError
229
228
230 def getfile(self, name, rev):
229 def getfile(self, name, rev):
231 """Return a pair (data, mode) where data is the file content
230 """Return a pair (data, mode) where data is the file content
232 as a string and mode one of '', 'x' or 'l'. rev is the
231 as a string and mode one of '', 'x' or 'l'. rev is the
233 identifier returned by a previous call to getchanges().
232 identifier returned by a previous call to getchanges().
234 Data is None if file is missing/deleted in rev.
233 Data is None if file is missing/deleted in rev.
235 """
234 """
236 raise NotImplementedError
235 raise NotImplementedError
237
236
238 def getchanges(self, version, full):
237 def getchanges(self, version, full):
239 """Returns a tuple of (files, copies, cleanp2).
238 """Returns a tuple of (files, copies, cleanp2).
240
239
241 files is a sorted list of (filename, id) tuples for all files
240 files is a sorted list of (filename, id) tuples for all files
242 changed between version and its first parent returned by
241 changed between version and its first parent returned by
243 getcommit(). If full, all files in that revision is returned.
242 getcommit(). If full, all files in that revision is returned.
244 id is the source revision id of the file.
243 id is the source revision id of the file.
245
244
246 copies is a dictionary of dest: source
245 copies is a dictionary of dest: source
247
246
248 cleanp2 is the set of files filenames that are clean against p2.
247 cleanp2 is the set of files filenames that are clean against p2.
249 (Files that are clean against p1 are already not in files (unless
248 (Files that are clean against p1 are already not in files (unless
250 full). This makes it possible to handle p2 clean files similarly.)
249 full). This makes it possible to handle p2 clean files similarly.)
251 """
250 """
252 raise NotImplementedError
251 raise NotImplementedError
253
252
254 def getcommit(self, version):
253 def getcommit(self, version):
255 """Return the commit object for version"""
254 """Return the commit object for version"""
256 raise NotImplementedError
255 raise NotImplementedError
257
256
258 def numcommits(self):
257 def numcommits(self):
259 """Return the number of commits in this source.
258 """Return the number of commits in this source.
260
259
261 If unknown, return None.
260 If unknown, return None.
262 """
261 """
263 return None
262 return None
264
263
265 def gettags(self):
264 def gettags(self):
266 """Return the tags as a dictionary of name: revision
265 """Return the tags as a dictionary of name: revision
267
266
268 Tag names must be UTF-8 strings.
267 Tag names must be UTF-8 strings.
269 """
268 """
270 raise NotImplementedError
269 raise NotImplementedError
271
270
272 def recode(self, s: AnyStr, encoding: Optional[bytes] = None) -> bytes:
271 def recode(self, s: AnyStr, encoding: Optional[bytes] = None) -> bytes:
273 if not encoding:
272 if not encoding:
274 encoding = self.encoding or b'utf-8'
273 encoding = self.encoding or b'utf-8'
275
274
276 if isinstance(s, str):
275 if isinstance(s, str):
277 return s.encode("utf-8")
276 return s.encode("utf-8")
278 try:
277 try:
279 return s.decode(pycompat.sysstr(encoding)).encode("utf-8")
278 return s.decode(pycompat.sysstr(encoding)).encode("utf-8")
280 except UnicodeError:
279 except UnicodeError:
281 try:
280 try:
282 return s.decode("latin-1").encode("utf-8")
281 return s.decode("latin-1").encode("utf-8")
283 except UnicodeError:
282 except UnicodeError:
284 return s.decode(pycompat.sysstr(encoding), "replace").encode(
283 return s.decode(pycompat.sysstr(encoding), "replace").encode(
285 "utf-8"
284 "utf-8"
286 )
285 )
287
286
288 def getchangedfiles(self, rev, i):
287 def getchangedfiles(self, rev, i):
289 """Return the files changed by rev compared to parent[i].
288 """Return the files changed by rev compared to parent[i].
290
289
291 i is an index selecting one of the parents of rev. The return
290 i is an index selecting one of the parents of rev. The return
292 value should be the list of files that are different in rev and
291 value should be the list of files that are different in rev and
293 this parent.
292 this parent.
294
293
295 If rev has no parents, i is None.
294 If rev has no parents, i is None.
296
295
297 This function is only needed to support --filemap
296 This function is only needed to support --filemap
298 """
297 """
299 raise NotImplementedError
298 raise NotImplementedError
300
299
301 def converted(self, rev, sinkrev) -> None:
300 def converted(self, rev, sinkrev) -> None:
302 '''Notify the source that a revision has been converted.'''
301 '''Notify the source that a revision has been converted.'''
303
302
304 def hasnativeorder(self) -> bool:
303 def hasnativeorder(self) -> bool:
305 """Return true if this source has a meaningful, native revision
304 """Return true if this source has a meaningful, native revision
306 order. For instance, Mercurial revisions are store sequentially
305 order. For instance, Mercurial revisions are store sequentially
307 while there is no such global ordering with Darcs.
306 while there is no such global ordering with Darcs.
308 """
307 """
309 return False
308 return False
310
309
311 def hasnativeclose(self) -> bool:
310 def hasnativeclose(self) -> bool:
312 """Return true if this source has ability to close branch."""
311 """Return true if this source has ability to close branch."""
313 return False
312 return False
314
313
315 def lookuprev(self, rev):
314 def lookuprev(self, rev):
316 """If rev is a meaningful revision reference in source, return
315 """If rev is a meaningful revision reference in source, return
317 the referenced identifier in the same format used by getcommit().
316 the referenced identifier in the same format used by getcommit().
318 return None otherwise.
317 return None otherwise.
319 """
318 """
320 return None
319 return None
321
320
322 def getbookmarks(self):
321 def getbookmarks(self):
323 """Return the bookmarks as a dictionary of name: revision
322 """Return the bookmarks as a dictionary of name: revision
324
323
325 Bookmark names are to be UTF-8 strings.
324 Bookmark names are to be UTF-8 strings.
326 """
325 """
327 return {}
326 return {}
328
327
329 def checkrevformat(self, revstr, mapname: bytes = b'splicemap') -> bool:
328 def checkrevformat(self, revstr, mapname: bytes = b'splicemap') -> bool:
330 """revstr is a string that describes a revision in the given
329 """revstr is a string that describes a revision in the given
331 source control system. Return true if revstr has correct
330 source control system. Return true if revstr has correct
332 format.
331 format.
333 """
332 """
334 return True
333 return True
335
334
336
335
337 class converter_sink:
336 class converter_sink:
338 """Conversion sink (target) interface"""
337 """Conversion sink (target) interface"""
339
338
340 def __init__(self, ui: "uimod.ui", repotype: bytes, path: bytes) -> None:
339 def __init__(self, ui: "uimod.ui", repotype: bytes, path: bytes) -> None:
341 """Initialize conversion sink (or raise NoRepo("message")
340 """Initialize conversion sink (or raise NoRepo("message")
342 exception if path is not a valid repository)
341 exception if path is not a valid repository)
343
342
344 created is a list of paths to remove if a fatal error occurs
343 created is a list of paths to remove if a fatal error occurs
345 later"""
344 later"""
346 self.ui = ui
345 self.ui = ui
347 self.path = path
346 self.path = path
348 self.created = []
347 self.created = []
349 self.repotype = repotype
348 self.repotype = repotype
350
349
351 def revmapfile(self):
350 def revmapfile(self):
352 """Path to a file that will contain lines
351 """Path to a file that will contain lines
353 source_rev_id sink_rev_id
352 source_rev_id sink_rev_id
354 mapping equivalent revision identifiers for each system."""
353 mapping equivalent revision identifiers for each system."""
355 raise NotImplementedError
354 raise NotImplementedError
356
355
357 def authorfile(self):
356 def authorfile(self):
358 """Path to a file that will contain lines
357 """Path to a file that will contain lines
359 srcauthor=dstauthor
358 srcauthor=dstauthor
360 mapping equivalent authors identifiers for each system."""
359 mapping equivalent authors identifiers for each system."""
361 return None
360 return None
362
361
363 def putcommit(
362 def putcommit(
364 self, files, copies, parents, commit, source, revmap, full, cleanp2
363 self, files, copies, parents, commit, source, revmap, full, cleanp2
365 ):
364 ):
366 """Create a revision with all changed files listed in 'files'
365 """Create a revision with all changed files listed in 'files'
367 and having listed parents. 'commit' is a commit object
366 and having listed parents. 'commit' is a commit object
368 containing at a minimum the author, date, and message for this
367 containing at a minimum the author, date, and message for this
369 changeset. 'files' is a list of (path, version) tuples,
368 changeset. 'files' is a list of (path, version) tuples,
370 'copies' is a dictionary mapping destinations to sources,
369 'copies' is a dictionary mapping destinations to sources,
371 'source' is the source repository, and 'revmap' is a mapfile
370 'source' is the source repository, and 'revmap' is a mapfile
372 of source revisions to converted revisions. Only getfile() and
371 of source revisions to converted revisions. Only getfile() and
373 lookuprev() should be called on 'source'. 'full' means that 'files'
372 lookuprev() should be called on 'source'. 'full' means that 'files'
374 is complete and all other files should be removed.
373 is complete and all other files should be removed.
375 'cleanp2' is a set of the filenames that are unchanged from p2
374 'cleanp2' is a set of the filenames that are unchanged from p2
376 (only in the common merge case where there two parents).
375 (only in the common merge case where there two parents).
377
376
378 Note that the sink repository is not told to update itself to
377 Note that the sink repository is not told to update itself to
379 a particular revision (or even what that revision would be)
378 a particular revision (or even what that revision would be)
380 before it receives the file data.
379 before it receives the file data.
381 """
380 """
382 raise NotImplementedError
381 raise NotImplementedError
383
382
384 def puttags(self, tags):
383 def puttags(self, tags):
385 """Put tags into sink.
384 """Put tags into sink.
386
385
387 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
386 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
388 Return a pair (tag_revision, tag_parent_revision), or (None, None)
387 Return a pair (tag_revision, tag_parent_revision), or (None, None)
389 if nothing was changed.
388 if nothing was changed.
390 """
389 """
391 raise NotImplementedError
390 raise NotImplementedError
392
391
393 def setbranch(self, branch, pbranches):
392 def setbranch(self, branch, pbranches):
394 """Set the current branch name. Called before the first putcommit
393 """Set the current branch name. Called before the first putcommit
395 on the branch.
394 on the branch.
396 branch: branch name for subsequent commits
395 branch: branch name for subsequent commits
397 pbranches: (converted parent revision, parent branch) tuples"""
396 pbranches: (converted parent revision, parent branch) tuples"""
398
397
399 def setfilemapmode(self, active):
398 def setfilemapmode(self, active):
400 """Tell the destination that we're using a filemap
399 """Tell the destination that we're using a filemap
401
400
402 Some converter_sources (svn in particular) can claim that a file
401 Some converter_sources (svn in particular) can claim that a file
403 was changed in a revision, even if there was no change. This method
402 was changed in a revision, even if there was no change. This method
404 tells the destination that we're using a filemap and that it should
403 tells the destination that we're using a filemap and that it should
405 filter empty revisions.
404 filter empty revisions.
406 """
405 """
407
406
408 def before(self) -> None:
407 def before(self) -> None:
409 pass
408 pass
410
409
411 def after(self) -> None:
410 def after(self) -> None:
412 pass
411 pass
413
412
414 def putbookmarks(self, bookmarks):
413 def putbookmarks(self, bookmarks):
415 """Put bookmarks into sink.
414 """Put bookmarks into sink.
416
415
417 bookmarks: {bookmarkname: sink_rev_id, ...}
416 bookmarks: {bookmarkname: sink_rev_id, ...}
418 where bookmarkname is an UTF-8 string.
417 where bookmarkname is an UTF-8 string.
419 """
418 """
420
419
421 def hascommitfrommap(self, rev):
420 def hascommitfrommap(self, rev):
422 """Return False if a rev mentioned in a filemap is known to not be
421 """Return False if a rev mentioned in a filemap is known to not be
423 present."""
422 present."""
424 raise NotImplementedError
423 raise NotImplementedError
425
424
426 def hascommitforsplicemap(self, rev):
425 def hascommitforsplicemap(self, rev):
427 """This method is for the special needs for splicemap handling and not
426 """This method is for the special needs for splicemap handling and not
428 for general use. Returns True if the sink contains rev, aborts on some
427 for general use. Returns True if the sink contains rev, aborts on some
429 special cases."""
428 special cases."""
430 raise NotImplementedError
429 raise NotImplementedError
431
430
432
431
433 class commandline:
432 class commandline:
434 def __init__(self, ui: "uimod.ui", command: bytes) -> None:
433 def __init__(self, ui: "uimod.ui", command: bytes) -> None:
435 self.ui = ui
434 self.ui = ui
436 self.command = command
435 self.command = command
437
436
438 def prerun(self) -> None:
437 def prerun(self) -> None:
439 pass
438 pass
440
439
441 def postrun(self) -> None:
440 def postrun(self) -> None:
442 pass
441 pass
443
442
444 def _cmdline(self, cmd: bytes, *args: bytes, **kwargs) -> bytes:
443 def _cmdline(self, cmd: bytes, *args: bytes, **kwargs) -> bytes:
445 kwargs = pycompat.byteskwargs(kwargs)
444 kwargs = pycompat.byteskwargs(kwargs)
446 cmdline = [self.command, cmd] + list(args)
445 cmdline = [self.command, cmd] + list(args)
447 for k, v in kwargs.items():
446 for k, v in kwargs.items():
448 if len(k) == 1:
447 if len(k) == 1:
449 cmdline.append(b'-' + k)
448 cmdline.append(b'-' + k)
450 else:
449 else:
451 cmdline.append(b'--' + k.replace(b'_', b'-'))
450 cmdline.append(b'--' + k.replace(b'_', b'-'))
452 try:
451 try:
453 if len(k) == 1:
452 if len(k) == 1:
454 cmdline.append(b'' + v)
453 cmdline.append(b'' + v)
455 else:
454 else:
456 cmdline[-1] += b'=' + v
455 cmdline[-1] += b'=' + v
457 except TypeError:
456 except TypeError:
458 pass
457 pass
459 cmdline = [procutil.shellquote(arg) for arg in cmdline]
458 cmdline = [procutil.shellquote(arg) for arg in cmdline]
460 if not self.ui.debugflag:
459 if not self.ui.debugflag:
461 cmdline += [b'2>', pycompat.bytestr(os.devnull)]
460 cmdline += [b'2>', pycompat.bytestr(os.devnull)]
462 cmdline = b' '.join(cmdline)
461 cmdline = b' '.join(cmdline)
463 return cmdline
462 return cmdline
464
463
465 def _run(self, cmd: bytes, *args: bytes, **kwargs):
464 def _run(self, cmd: bytes, *args: bytes, **kwargs):
466 def popen(cmdline):
465 def popen(cmdline):
467 p = subprocess.Popen(
466 p = subprocess.Popen(
468 procutil.tonativestr(cmdline),
467 procutil.tonativestr(cmdline),
469 shell=True,
468 shell=True,
470 bufsize=-1,
469 bufsize=-1,
471 close_fds=procutil.closefds,
470 close_fds=procutil.closefds,
472 stdout=subprocess.PIPE,
471 stdout=subprocess.PIPE,
473 )
472 )
474 return p
473 return p
475
474
476 return self._dorun(popen, cmd, *args, **kwargs)
475 return self._dorun(popen, cmd, *args, **kwargs)
477
476
478 def _run2(self, cmd: bytes, *args: bytes, **kwargs):
477 def _run2(self, cmd: bytes, *args: bytes, **kwargs):
479 return self._dorun(procutil.popen2, cmd, *args, **kwargs)
478 return self._dorun(procutil.popen2, cmd, *args, **kwargs)
480
479
481 def _run3(self, cmd: bytes, *args: bytes, **kwargs):
480 def _run3(self, cmd: bytes, *args: bytes, **kwargs):
482 return self._dorun(procutil.popen3, cmd, *args, **kwargs)
481 return self._dorun(procutil.popen3, cmd, *args, **kwargs)
483
482
484 def _dorun(self, openfunc, cmd: bytes, *args: bytes, **kwargs):
483 def _dorun(self, openfunc, cmd: bytes, *args: bytes, **kwargs):
485 cmdline = self._cmdline(cmd, *args, **kwargs)
484 cmdline = self._cmdline(cmd, *args, **kwargs)
486 self.ui.debug(b'running: %s\n' % (cmdline,))
485 self.ui.debug(b'running: %s\n' % (cmdline,))
487 self.prerun()
486 self.prerun()
488 try:
487 try:
489 return openfunc(cmdline)
488 return openfunc(cmdline)
490 finally:
489 finally:
491 self.postrun()
490 self.postrun()
492
491
493 def run(self, cmd: bytes, *args: bytes, **kwargs):
492 def run(self, cmd: bytes, *args: bytes, **kwargs):
494 p = self._run(cmd, *args, **kwargs)
493 p = self._run(cmd, *args, **kwargs)
495 output = p.communicate()[0]
494 output = p.communicate()[0]
496 self.ui.debug(output)
495 self.ui.debug(output)
497 return output, p.returncode
496 return output, p.returncode
498
497
499 def runlines(self, cmd: bytes, *args: bytes, **kwargs):
498 def runlines(self, cmd: bytes, *args: bytes, **kwargs):
500 p = self._run(cmd, *args, **kwargs)
499 p = self._run(cmd, *args, **kwargs)
501 output = p.stdout.readlines()
500 output = p.stdout.readlines()
502 p.wait()
501 p.wait()
503 self.ui.debug(b''.join(output))
502 self.ui.debug(b''.join(output))
504 return output, p.returncode
503 return output, p.returncode
505
504
506 def checkexit(self, status, output: bytes = b'') -> None:
505 def checkexit(self, status, output: bytes = b'') -> None:
507 if status:
506 if status:
508 if output:
507 if output:
509 self.ui.warn(_(b'%s error:\n') % self.command)
508 self.ui.warn(_(b'%s error:\n') % self.command)
510 self.ui.warn(output)
509 self.ui.warn(output)
511 msg = procutil.explainexit(status)
510 msg = procutil.explainexit(status)
512 raise error.Abort(b'%s %s' % (self.command, msg))
511 raise error.Abort(b'%s %s' % (self.command, msg))
513
512
514 def run0(self, cmd: bytes, *args: bytes, **kwargs):
513 def run0(self, cmd: bytes, *args: bytes, **kwargs):
515 output, status = self.run(cmd, *args, **kwargs)
514 output, status = self.run(cmd, *args, **kwargs)
516 self.checkexit(status, output)
515 self.checkexit(status, output)
517 return output
516 return output
518
517
519 def runlines0(self, cmd: bytes, *args: bytes, **kwargs):
518 def runlines0(self, cmd: bytes, *args: bytes, **kwargs):
520 output, status = self.runlines(cmd, *args, **kwargs)
519 output, status = self.runlines(cmd, *args, **kwargs)
521 self.checkexit(status, b''.join(output))
520 self.checkexit(status, b''.join(output))
522 return output
521 return output
523
522
524 @propertycache
523 @propertycache
525 def argmax(self):
524 def argmax(self):
526 # POSIX requires at least 4096 bytes for ARG_MAX
525 # POSIX requires at least 4096 bytes for ARG_MAX
527 argmax = 4096
526 argmax = 4096
528 try:
527 try:
529 argmax = os.sysconf("SC_ARG_MAX")
528 argmax = os.sysconf("SC_ARG_MAX")
530 except (AttributeError, ValueError):
529 except (AttributeError, ValueError):
531 pass
530 pass
532
531
533 # Windows shells impose their own limits on command line length,
532 # Windows shells impose their own limits on command line length,
534 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
533 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
535 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
534 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
536 # details about cmd.exe limitations.
535 # details about cmd.exe limitations.
537
536
538 # Since ARG_MAX is for command line _and_ environment, lower our limit
537 # Since ARG_MAX is for command line _and_ environment, lower our limit
539 # (and make happy Windows shells while doing this).
538 # (and make happy Windows shells while doing this).
540 return argmax // 2 - 1
539 return argmax // 2 - 1
541
540
542 def _limit_arglist(self, arglist, cmd: bytes, *args: bytes, **kwargs):
541 def _limit_arglist(self, arglist, cmd: bytes, *args: bytes, **kwargs):
543 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
542 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
544 limit = self.argmax - cmdlen
543 limit = self.argmax - cmdlen
545 numbytes = 0
544 numbytes = 0
546 fl = []
545 fl = []
547 for fn in arglist:
546 for fn in arglist:
548 b = len(fn) + 3
547 b = len(fn) + 3
549 if numbytes + b < limit or len(fl) == 0:
548 if numbytes + b < limit or len(fl) == 0:
550 fl.append(fn)
549 fl.append(fn)
551 numbytes += b
550 numbytes += b
552 else:
551 else:
553 yield fl
552 yield fl
554 fl = [fn]
553 fl = [fn]
555 numbytes = b
554 numbytes = b
556 if fl:
555 if fl:
557 yield fl
556 yield fl
558
557
559 def xargs(self, arglist, cmd: bytes, *args: bytes, **kwargs):
558 def xargs(self, arglist, cmd: bytes, *args: bytes, **kwargs):
560 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
559 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
561 self.run0(cmd, *(list(args) + l), **kwargs)
560 self.run0(cmd, *(list(args) + l), **kwargs)
562
561
563
562
564 class mapfile(dict):
563 class mapfile(dict):
565 def __init__(self, ui: "uimod.ui", path: bytes) -> None:
564 def __init__(self, ui: "uimod.ui", path: bytes) -> None:
566 super(mapfile, self).__init__()
565 super(mapfile, self).__init__()
567 self.ui = ui
566 self.ui = ui
568 self.path = path
567 self.path = path
569 self.fp = None
568 self.fp = None
570 self.order = []
569 self.order = []
571 self._read()
570 self._read()
572
571
573 def _read(self) -> None:
572 def _read(self) -> None:
574 if not self.path:
573 if not self.path:
575 return
574 return
576 try:
575 try:
577 fp = open(self.path, b'rb')
576 fp = open(self.path, 'rb')
578 except FileNotFoundError:
577 except FileNotFoundError:
579 return
578 return
580
579
581 try:
580 try:
582 for i, line in enumerate(fp):
581 for i, line in enumerate(fp):
583 line = line.splitlines()[0].rstrip()
582 line = line.splitlines()[0].rstrip()
584 if not line:
583 if not line:
585 # Ignore blank lines
584 # Ignore blank lines
586 continue
585 continue
587 try:
586 try:
588 key, value = line.rsplit(b' ', 1)
587 key, value = line.rsplit(b' ', 1)
589 except ValueError:
588 except ValueError:
590 raise error.Abort(
589 raise error.Abort(
591 _(b'syntax error in %s(%d): key/value pair expected')
590 _(b'syntax error in %s(%d): key/value pair expected')
592 % (self.path, i + 1)
591 % (self.path, i + 1)
593 )
592 )
594 if key not in self:
593 if key not in self:
595 self.order.append(key)
594 self.order.append(key)
596 super(mapfile, self).__setitem__(key, value)
595 super(mapfile, self).__setitem__(key, value)
597 finally:
596 finally:
598 fp.close()
597 fp.close()
599
598
600 def __setitem__(self, key, value) -> None:
599 def __setitem__(self, key, value) -> None:
601 if self.fp is None:
600 if self.fp is None:
602 try:
601 try:
603 self.fp = open(self.path, b'ab')
602 self.fp = open(self.path, 'ab')
604 except IOError as err:
603 except IOError as err:
605 raise error.Abort(
604 raise error.Abort(
606 _(b'could not open map file %r: %s')
605 _(b'could not open map file %r: %s')
607 % (self.path, encoding.strtolocal(err.strerror))
606 % (self.path, encoding.strtolocal(err.strerror))
608 )
607 )
609 self.fp.write(util.tonativeeol(b'%s %s\n' % (key, value)))
608 self.fp.write(util.tonativeeol(b'%s %s\n' % (key, value)))
610 self.fp.flush()
609 self.fp.flush()
611 super(mapfile, self).__setitem__(key, value)
610 super(mapfile, self).__setitem__(key, value)
612
611
613 def close(self) -> None:
612 def close(self) -> None:
614 if self.fp:
613 if self.fp:
615 self.fp.close()
614 self.fp.close()
616 self.fp = None
615 self.fp = None
617
616
618
617
619 def makedatetimestamp(t: float) -> dateutil.hgdate:
618 def makedatetimestamp(t: float) -> dateutil.hgdate:
620 return dateutil.makedate(t)
619 return dateutil.makedate(t)
General Comments 0
You need to be logged in to leave comments. Login now