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