##// END OF EJS Templates
branching: merge stable into default
Raphaël Gomès -
r52493:ee1b648e merge default
parent child Browse files
Show More
@@ -1,559 +1,562 b''
1 # bundlecaches.py - utility to deal with pre-computed bundle for servers
1 # bundlecaches.py - utility to deal with pre-computed bundle for servers
2 #
2 #
3 # This software may be used and distributed according to the terms of the
3 # This software may be used and distributed according to the terms of the
4 # GNU General Public License version 2 or any later version.
4 # GNU General Public License version 2 or any later version.
5
5
6 import collections
6 import collections
7
7
8 from typing import (
8 from typing import (
9 Dict,
9 Dict,
10 Union,
10 Union,
11 cast,
11 cast,
12 )
12 )
13
13
14 from .i18n import _
14 from .i18n import _
15
15
16 from .thirdparty import attr
16 from .thirdparty import attr
17
17
18 from . import (
18 from . import (
19 error,
19 error,
20 requirements as requirementsmod,
20 requirements as requirementsmod,
21 sslutil,
21 sslutil,
22 util,
22 util,
23 )
23 )
24 from .utils import stringutil
24 from .utils import stringutil
25
25
26 urlreq = util.urlreq
26 urlreq = util.urlreq
27
27
28 BUNDLE_CACHE_DIR = b'bundle-cache'
28 BUNDLE_CACHE_DIR = b'bundle-cache'
29 CB_MANIFEST_FILE = b'clonebundles.manifest'
29 CB_MANIFEST_FILE = b'clonebundles.manifest'
30 CLONEBUNDLESCHEME = b"peer-bundle-cache://"
30 CLONEBUNDLESCHEME = b"peer-bundle-cache://"
31
31
32
32
33 def get_manifest(repo):
33 def get_manifest(repo):
34 """get the bundle manifest to be served to a client from a server"""
34 """get the bundle manifest to be served to a client from a server"""
35 raw_text = repo.vfs.tryread(CB_MANIFEST_FILE)
35 raw_text = repo.vfs.tryread(CB_MANIFEST_FILE)
36 entries = [e.split(b' ', 1) for e in raw_text.splitlines()]
36 entries = [e.split(b' ', 1) for e in raw_text.splitlines()]
37
37
38 new_lines = []
38 new_lines = []
39 for e in entries:
39 for e in entries:
40 url = alter_bundle_url(repo, e[0])
40 url = alter_bundle_url(repo, e[0])
41 if len(e) == 1:
41 if len(e) == 1:
42 line = url + b'\n'
42 line = url + b'\n'
43 else:
43 else:
44 line = b"%s %s\n" % (url, e[1])
44 line = b"%s %s\n" % (url, e[1])
45 new_lines.append(line)
45 new_lines.append(line)
46 return b''.join(new_lines)
46 return b''.join(new_lines)
47
47
48
48
49 def alter_bundle_url(repo, url):
49 def alter_bundle_url(repo, url):
50 """a function that exist to help extension and hosting to alter the url
50 """a function that exist to help extension and hosting to alter the url
51
51
52 This will typically be used to inject authentication information in the url
52 This will typically be used to inject authentication information in the url
53 of cached bundles."""
53 of cached bundles."""
54 return url
54 return url
55
55
56
56
57 SUPPORTED_CLONEBUNDLE_SCHEMES = [
57 SUPPORTED_CLONEBUNDLE_SCHEMES = [
58 b"http://",
58 b"http://",
59 b"https://",
59 b"https://",
60 b"largefile://",
60 b"largefile://",
61 CLONEBUNDLESCHEME,
61 CLONEBUNDLESCHEME,
62 ]
62 ]
63
63
64
64
65 @attr.s
65 @attr.s
66 class bundlespec:
66 class bundlespec:
67 compression = attr.ib()
67 compression = attr.ib()
68 wirecompression = attr.ib()
68 wirecompression = attr.ib()
69 version = attr.ib()
69 version = attr.ib()
70 wireversion = attr.ib()
70 wireversion = attr.ib()
71 # parameters explicitly overwritten by the config or the specification
71 # parameters explicitly overwritten by the config or the specification
72 _explicit_params = attr.ib()
72 _explicit_params = attr.ib()
73 # default parameter for the version
73 # default parameter for the version
74 #
74 #
75 # Keeping it separated is useful to check what was actually overwritten.
75 # Keeping it separated is useful to check what was actually overwritten.
76 _default_opts = attr.ib()
76 _default_opts = attr.ib()
77
77
78 @property
78 @property
79 def params(self):
79 def params(self):
80 return collections.ChainMap(self._explicit_params, self._default_opts)
80 return collections.ChainMap(self._explicit_params, self._default_opts)
81
81
82 @property
82 @property
83 def contentopts(self):
83 def contentopts(self):
84 # kept for Backward Compatibility concerns.
84 # kept for Backward Compatibility concerns.
85 return self.params
85 return self.params
86
86
87 def set_param(self, key, value, overwrite=True):
87 def set_param(self, key, value, overwrite=True):
88 """Set a bundle parameter value.
88 """Set a bundle parameter value.
89
89
90 Will only overwrite if overwrite is true"""
90 Will only overwrite if overwrite is true"""
91 if overwrite or key not in self._explicit_params:
91 if overwrite or key not in self._explicit_params:
92 self._explicit_params[key] = value
92 self._explicit_params[key] = value
93
93
94 def as_spec(self):
94 def as_spec(self):
95 parts = [b"%s-%s" % (self.compression, self.version)]
95 parts = [b"%s-%s" % (self.compression, self.version)]
96 for param in sorted(self._explicit_params.items()):
96 for param in sorted(self._explicit_params.items()):
97 parts.append(b'%s=%s' % param)
97 parts.append(b'%s=%s' % param)
98 return b';'.join(parts)
98 return b';'.join(parts)
99
99
100
100
101 # Maps bundle version human names to changegroup versions.
101 # Maps bundle version human names to changegroup versions.
102 _bundlespeccgversions = {
102 _bundlespeccgversions = {
103 b'v1': b'01',
103 b'v1': b'01',
104 b'v2': b'02',
104 b'v2': b'02',
105 b'v3': b'03',
105 b'v3': b'03',
106 b'packed1': b's1',
106 b'packed1': b's1',
107 b'bundle2': b'02', # legacy
107 b'bundle2': b'02', # legacy
108 }
108 }
109
109
110 # Maps bundle version with content opts to choose which part to bundle
110 # Maps bundle version with content opts to choose which part to bundle
111 _bundlespeccontentopts: Dict[bytes, Dict[bytes, Union[bool, bytes]]] = {
111 _bundlespeccontentopts: Dict[bytes, Dict[bytes, Union[bool, bytes]]] = {
112 b'v1': {
112 b'v1': {
113 b'changegroup': True,
113 b'changegroup': True,
114 b'cg.version': b'01',
114 b'cg.version': b'01',
115 b'obsolescence': False,
115 b'obsolescence': False,
116 b'phases': False,
116 b'phases': False,
117 b'tagsfnodescache': False,
117 b'tagsfnodescache': False,
118 b'revbranchcache': False,
118 b'revbranchcache': False,
119 },
119 },
120 b'v2': {
120 b'v2': {
121 b'changegroup': True,
121 b'changegroup': True,
122 b'cg.version': b'02',
122 b'cg.version': b'02',
123 b'obsolescence': False,
123 b'obsolescence': False,
124 b'phases': False,
124 b'phases': False,
125 b'tagsfnodescache': True,
125 b'tagsfnodescache': True,
126 b'revbranchcache': True,
126 b'revbranchcache': True,
127 },
127 },
128 b'v3': {
128 b'v3': {
129 b'changegroup': True,
129 b'changegroup': True,
130 b'cg.version': b'03',
130 b'cg.version': b'03',
131 b'obsolescence': False,
131 b'obsolescence': False,
132 b'phases': True,
132 b'phases': True,
133 b'tagsfnodescache': True,
133 b'tagsfnodescache': True,
134 b'revbranchcache': True,
134 b'revbranchcache': True,
135 },
135 },
136 b'streamv2': {
136 b'streamv2': {
137 b'changegroup': False,
137 b'changegroup': False,
138 b'cg.version': b'02',
138 b'cg.version': b'02',
139 b'obsolescence': False,
139 b'obsolescence': False,
140 b'phases': False,
140 b'phases': False,
141 b"stream": b"v2",
141 b"stream": b"v2",
142 b'tagsfnodescache': False,
142 b'tagsfnodescache': False,
143 b'revbranchcache': False,
143 b'revbranchcache': False,
144 },
144 },
145 b'streamv3-exp': {
145 b'streamv3-exp': {
146 b'changegroup': False,
146 b'changegroup': False,
147 b'cg.version': b'03',
147 b'cg.version': b'03',
148 b'obsolescence': False,
148 b'obsolescence': False,
149 b'phases': False,
149 b'phases': False,
150 b"stream": b"v3-exp",
150 b"stream": b"v3-exp",
151 b'tagsfnodescache': False,
151 b'tagsfnodescache': False,
152 b'revbranchcache': False,
152 b'revbranchcache': False,
153 },
153 },
154 b'packed1': {
154 b'packed1': {
155 b'cg.version': b's1',
155 b'cg.version': b's1',
156 },
156 },
157 b'bundle2': { # legacy
157 b'bundle2': { # legacy
158 b'cg.version': b'02',
158 b'cg.version': b'02',
159 },
159 },
160 }
160 }
161 _bundlespeccontentopts[b'bundle2'] = _bundlespeccontentopts[b'v2']
161 _bundlespeccontentopts[b'bundle2'] = _bundlespeccontentopts[b'v2']
162
162
163 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
163 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
164 _bundlespecv1compengines = {b'gzip', b'bzip2', b'none'}
164 _bundlespecv1compengines = {b'gzip', b'bzip2', b'none'}
165
165
166
166
167 def param_bool(key, value):
167 def param_bool(key, value):
168 """make a boolean out of a parameter value"""
168 """make a boolean out of a parameter value"""
169 b = stringutil.parsebool(value)
169 b = stringutil.parsebool(value)
170 if b is None:
170 if b is None:
171 msg = _(b"parameter %s should be a boolean ('%s')")
171 msg = _(b"parameter %s should be a boolean ('%s')")
172 msg %= (key, value)
172 msg %= (key, value)
173 raise error.InvalidBundleSpecification(msg)
173 raise error.InvalidBundleSpecification(msg)
174 return b
174 return b
175
175
176
176
177 # mapping of known parameter name need their value processed
177 # mapping of known parameter name need their value processed
178 bundle_spec_param_processing = {
178 bundle_spec_param_processing = {
179 b"obsolescence": param_bool,
179 b"obsolescence": param_bool,
180 b"obsolescence-mandatory": param_bool,
180 b"obsolescence-mandatory": param_bool,
181 b"phases": param_bool,
181 b"phases": param_bool,
182 b"changegroup": param_bool,
183 b"tagsfnodescache": param_bool,
184 b"revbranchcache": param_bool,
182 }
185 }
183
186
184
187
185 def _parseparams(s):
188 def _parseparams(s):
186 """parse bundlespec parameter section
189 """parse bundlespec parameter section
187
190
188 input: "comp-version;params" string
191 input: "comp-version;params" string
189
192
190 return: (spec; {param_key: param_value})
193 return: (spec; {param_key: param_value})
191 """
194 """
192 if b';' not in s:
195 if b';' not in s:
193 return s, {}
196 return s, {}
194
197
195 params = {}
198 params = {}
196 version, paramstr = s.split(b';', 1)
199 version, paramstr = s.split(b';', 1)
197
200
198 err = _(b'invalid bundle specification: missing "=" in parameter: %s')
201 err = _(b'invalid bundle specification: missing "=" in parameter: %s')
199 for p in paramstr.split(b';'):
202 for p in paramstr.split(b';'):
200 if b'=' not in p:
203 if b'=' not in p:
201 msg = err % p
204 msg = err % p
202 raise error.InvalidBundleSpecification(msg)
205 raise error.InvalidBundleSpecification(msg)
203
206
204 key, value = p.split(b'=', 1)
207 key, value = p.split(b'=', 1)
205 key = urlreq.unquote(key)
208 key = urlreq.unquote(key)
206 value = urlreq.unquote(value)
209 value = urlreq.unquote(value)
207 process = bundle_spec_param_processing.get(key)
210 process = bundle_spec_param_processing.get(key)
208 if process is not None:
211 if process is not None:
209 value = process(key, value)
212 value = process(key, value)
210 params[key] = value
213 params[key] = value
211
214
212 return version, params
215 return version, params
213
216
214
217
215 def parsebundlespec(repo, spec, strict=True):
218 def parsebundlespec(repo, spec, strict=True):
216 """Parse a bundle string specification into parts.
219 """Parse a bundle string specification into parts.
217
220
218 Bundle specifications denote a well-defined bundle/exchange format.
221 Bundle specifications denote a well-defined bundle/exchange format.
219 The content of a given specification should not change over time in
222 The content of a given specification should not change over time in
220 order to ensure that bundles produced by a newer version of Mercurial are
223 order to ensure that bundles produced by a newer version of Mercurial are
221 readable from an older version.
224 readable from an older version.
222
225
223 The string currently has the form:
226 The string currently has the form:
224
227
225 <compression>-<type>[;<parameter0>[;<parameter1>]]
228 <compression>-<type>[;<parameter0>[;<parameter1>]]
226
229
227 Where <compression> is one of the supported compression formats
230 Where <compression> is one of the supported compression formats
228 and <type> is (currently) a version string. A ";" can follow the type and
231 and <type> is (currently) a version string. A ";" can follow the type and
229 all text afterwards is interpreted as URI encoded, ";" delimited key=value
232 all text afterwards is interpreted as URI encoded, ";" delimited key=value
230 pairs.
233 pairs.
231
234
232 If ``strict`` is True (the default) <compression> is required. Otherwise,
235 If ``strict`` is True (the default) <compression> is required. Otherwise,
233 it is optional.
236 it is optional.
234
237
235 Returns a bundlespec object of (compression, version, parameters).
238 Returns a bundlespec object of (compression, version, parameters).
236 Compression will be ``None`` if not in strict mode and a compression isn't
239 Compression will be ``None`` if not in strict mode and a compression isn't
237 defined.
240 defined.
238
241
239 An ``InvalidBundleSpecification`` is raised when the specification is
242 An ``InvalidBundleSpecification`` is raised when the specification is
240 not syntactically well formed.
243 not syntactically well formed.
241
244
242 An ``UnsupportedBundleSpecification`` is raised when the compression or
245 An ``UnsupportedBundleSpecification`` is raised when the compression or
243 bundle type/version is not recognized.
246 bundle type/version is not recognized.
244
247
245 Note: this function will likely eventually return a more complex data
248 Note: this function will likely eventually return a more complex data
246 structure, including bundle2 part information.
249 structure, including bundle2 part information.
247 """
250 """
248 if strict and b'-' not in spec:
251 if strict and b'-' not in spec:
249 raise error.InvalidBundleSpecification(
252 raise error.InvalidBundleSpecification(
250 _(
253 _(
251 b'invalid bundle specification; '
254 b'invalid bundle specification; '
252 b'must be prefixed with compression: %s'
255 b'must be prefixed with compression: %s'
253 )
256 )
254 % spec
257 % spec
255 )
258 )
256
259
257 pre_args = spec.split(b';', 1)[0]
260 pre_args = spec.split(b';', 1)[0]
258 if b'-' in pre_args:
261 if b'-' in pre_args:
259 compression, version = spec.split(b'-', 1)
262 compression, version = spec.split(b'-', 1)
260
263
261 if compression not in util.compengines.supportedbundlenames:
264 if compression not in util.compengines.supportedbundlenames:
262 raise error.UnsupportedBundleSpecification(
265 raise error.UnsupportedBundleSpecification(
263 _(b'%s compression is not supported') % compression
266 _(b'%s compression is not supported') % compression
264 )
267 )
265
268
266 version, params = _parseparams(version)
269 version, params = _parseparams(version)
267
270
268 if version not in _bundlespeccontentopts:
271 if version not in _bundlespeccontentopts:
269 raise error.UnsupportedBundleSpecification(
272 raise error.UnsupportedBundleSpecification(
270 _(b'%s is not a recognized bundle version') % version
273 _(b'%s is not a recognized bundle version') % version
271 )
274 )
272 else:
275 else:
273 # Value could be just the compression or just the version, in which
276 # Value could be just the compression or just the version, in which
274 # case some defaults are assumed (but only when not in strict mode).
277 # case some defaults are assumed (but only when not in strict mode).
275 assert not strict
278 assert not strict
276
279
277 spec, params = _parseparams(spec)
280 spec, params = _parseparams(spec)
278
281
279 if spec in util.compengines.supportedbundlenames:
282 if spec in util.compengines.supportedbundlenames:
280 compression = spec
283 compression = spec
281 version = b'v1'
284 version = b'v1'
282 # Generaldelta repos require v2.
285 # Generaldelta repos require v2.
283 if requirementsmod.GENERALDELTA_REQUIREMENT in repo.requirements:
286 if requirementsmod.GENERALDELTA_REQUIREMENT in repo.requirements:
284 version = b'v2'
287 version = b'v2'
285 elif requirementsmod.REVLOGV2_REQUIREMENT in repo.requirements:
288 elif requirementsmod.REVLOGV2_REQUIREMENT in repo.requirements:
286 version = b'v2'
289 version = b'v2'
287 # Modern compression engines require v2.
290 # Modern compression engines require v2.
288 if compression not in _bundlespecv1compengines:
291 if compression not in _bundlespecv1compengines:
289 version = b'v2'
292 version = b'v2'
290 elif spec in _bundlespeccontentopts:
293 elif spec in _bundlespeccontentopts:
291 if spec == b'packed1':
294 if spec == b'packed1':
292 compression = b'none'
295 compression = b'none'
293 else:
296 else:
294 compression = b'bzip2'
297 compression = b'bzip2'
295 version = spec
298 version = spec
296 else:
299 else:
297 raise error.UnsupportedBundleSpecification(
300 raise error.UnsupportedBundleSpecification(
298 _(b'%s is not a recognized bundle specification') % spec
301 _(b'%s is not a recognized bundle specification') % spec
299 )
302 )
300
303
301 # Bundle version 1 only supports a known set of compression engines.
304 # Bundle version 1 only supports a known set of compression engines.
302 if version == b'v1' and compression not in _bundlespecv1compengines:
305 if version == b'v1' and compression not in _bundlespecv1compengines:
303 raise error.UnsupportedBundleSpecification(
306 raise error.UnsupportedBundleSpecification(
304 _(b'compression engine %s is not supported on v1 bundles')
307 _(b'compression engine %s is not supported on v1 bundles')
305 % compression
308 % compression
306 )
309 )
307
310
308 # The specification for packed1 can optionally declare the data formats
311 # The specification for packed1 can optionally declare the data formats
309 # required to apply it. If we see this metadata, compare against what the
312 # required to apply it. If we see this metadata, compare against what the
310 # repo supports and error if the bundle isn't compatible.
313 # repo supports and error if the bundle isn't compatible.
311 if version == b'packed1' and b'requirements' in params:
314 if version == b'packed1' and b'requirements' in params:
312 requirements = set(cast(bytes, params[b'requirements']).split(b','))
315 requirements = set(cast(bytes, params[b'requirements']).split(b','))
313 missingreqs = requirements - requirementsmod.STREAM_FIXED_REQUIREMENTS
316 missingreqs = requirements - requirementsmod.STREAM_FIXED_REQUIREMENTS
314 if missingreqs:
317 if missingreqs:
315 raise error.UnsupportedBundleSpecification(
318 raise error.UnsupportedBundleSpecification(
316 _(b'missing support for repository features: %s')
319 _(b'missing support for repository features: %s')
317 % b', '.join(sorted(missingreqs))
320 % b', '.join(sorted(missingreqs))
318 )
321 )
319
322
320 # Compute contentopts based on the version
323 # Compute contentopts based on the version
321 if b"stream" in params:
324 if b"stream" in params:
322 # This case is fishy as this mostly derails the version selection
325 # This case is fishy as this mostly derails the version selection
323 # mechanism. `stream` bundles are quite specific and used differently
326 # mechanism. `stream` bundles are quite specific and used differently
324 # as "normal" bundles.
327 # as "normal" bundles.
325 #
328 #
326 # (we should probably define a cleaner way to do this and raise a
329 # (we should probably define a cleaner way to do this and raise a
327 # warning when the old way is encountered)
330 # warning when the old way is encountered)
328 if params[b"stream"] == b"v2":
331 if params[b"stream"] == b"v2":
329 version = b"streamv2"
332 version = b"streamv2"
330 if params[b"stream"] == b"v3-exp":
333 if params[b"stream"] == b"v3-exp":
331 version = b"streamv3-exp"
334 version = b"streamv3-exp"
332 contentopts = _bundlespeccontentopts.get(version, {}).copy()
335 contentopts = _bundlespeccontentopts.get(version, {}).copy()
333 if version == b"streamv2" or version == b"streamv3-exp":
336 if version == b"streamv2" or version == b"streamv3-exp":
334 # streamv2 have been reported as "v2" for a while.
337 # streamv2 have been reported as "v2" for a while.
335 version = b"v2"
338 version = b"v2"
336
339
337 engine = util.compengines.forbundlename(compression)
340 engine = util.compengines.forbundlename(compression)
338 compression, wirecompression = engine.bundletype()
341 compression, wirecompression = engine.bundletype()
339 wireversion = _bundlespeccontentopts[version][b'cg.version']
342 wireversion = _bundlespeccontentopts[version][b'cg.version']
340
343
341 return bundlespec(
344 return bundlespec(
342 compression, wirecompression, version, wireversion, params, contentopts
345 compression, wirecompression, version, wireversion, params, contentopts
343 )
346 )
344
347
345
348
346 def parseclonebundlesmanifest(repo, s):
349 def parseclonebundlesmanifest(repo, s):
347 """Parses the raw text of a clone bundles manifest.
350 """Parses the raw text of a clone bundles manifest.
348
351
349 Returns a list of dicts. The dicts have a ``URL`` key corresponding
352 Returns a list of dicts. The dicts have a ``URL`` key corresponding
350 to the URL and other keys are the attributes for the entry.
353 to the URL and other keys are the attributes for the entry.
351 """
354 """
352 m = []
355 m = []
353 for line in s.splitlines():
356 for line in s.splitlines():
354 fields = line.split()
357 fields = line.split()
355 if not fields:
358 if not fields:
356 continue
359 continue
357 attrs = {b'URL': fields[0]}
360 attrs = {b'URL': fields[0]}
358 for rawattr in fields[1:]:
361 for rawattr in fields[1:]:
359 key, value = rawattr.split(b'=', 1)
362 key, value = rawattr.split(b'=', 1)
360 key = util.urlreq.unquote(key)
363 key = util.urlreq.unquote(key)
361 value = util.urlreq.unquote(value)
364 value = util.urlreq.unquote(value)
362 attrs[key] = value
365 attrs[key] = value
363
366
364 # Parse BUNDLESPEC into components. This makes client-side
367 # Parse BUNDLESPEC into components. This makes client-side
365 # preferences easier to specify since you can prefer a single
368 # preferences easier to specify since you can prefer a single
366 # component of the BUNDLESPEC.
369 # component of the BUNDLESPEC.
367 if key == b'BUNDLESPEC':
370 if key == b'BUNDLESPEC':
368 try:
371 try:
369 bundlespec = parsebundlespec(repo, value)
372 bundlespec = parsebundlespec(repo, value)
370 attrs[b'COMPRESSION'] = bundlespec.compression
373 attrs[b'COMPRESSION'] = bundlespec.compression
371 attrs[b'VERSION'] = bundlespec.version
374 attrs[b'VERSION'] = bundlespec.version
372 except error.InvalidBundleSpecification:
375 except error.InvalidBundleSpecification:
373 pass
376 pass
374 except error.UnsupportedBundleSpecification:
377 except error.UnsupportedBundleSpecification:
375 pass
378 pass
376
379
377 m.append(attrs)
380 m.append(attrs)
378
381
379 return m
382 return m
380
383
381
384
382 def isstreamclonespec(bundlespec):
385 def isstreamclonespec(bundlespec):
383 # Stream clone v1
386 # Stream clone v1
384 if bundlespec.wirecompression == b'UN' and bundlespec.wireversion == b's1':
387 if bundlespec.wirecompression == b'UN' and bundlespec.wireversion == b's1':
385 return True
388 return True
386
389
387 # Stream clone v2
390 # Stream clone v2
388 if (
391 if (
389 bundlespec.wirecompression == b'UN'
392 bundlespec.wirecompression == b'UN'
390 and bundlespec.wireversion == b'02'
393 and bundlespec.wireversion == b'02'
391 and bundlespec.contentopts.get(b'stream', None) in (b"v2", b"v3-exp")
394 and bundlespec.contentopts.get(b'stream', None) in (b"v2", b"v3-exp")
392 ):
395 ):
393 return True
396 return True
394
397
395 return False
398 return False
396
399
397
400
398 def filterclonebundleentries(
401 def filterclonebundleentries(
399 repo, entries, streamclonerequested=False, pullbundles=False
402 repo, entries, streamclonerequested=False, pullbundles=False
400 ):
403 ):
401 """Remove incompatible clone bundle manifest entries.
404 """Remove incompatible clone bundle manifest entries.
402
405
403 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
406 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
404 and returns a new list consisting of only the entries that this client
407 and returns a new list consisting of only the entries that this client
405 should be able to apply.
408 should be able to apply.
406
409
407 There is no guarantee we'll be able to apply all returned entries because
410 There is no guarantee we'll be able to apply all returned entries because
408 the metadata we use to filter on may be missing or wrong.
411 the metadata we use to filter on may be missing or wrong.
409 """
412 """
410 newentries = []
413 newentries = []
411 for entry in entries:
414 for entry in entries:
412 url = entry.get(b'URL')
415 url = entry.get(b'URL')
413 if not pullbundles and not any(
416 if not pullbundles and not any(
414 [url.startswith(scheme) for scheme in SUPPORTED_CLONEBUNDLE_SCHEMES]
417 [url.startswith(scheme) for scheme in SUPPORTED_CLONEBUNDLE_SCHEMES]
415 ):
418 ):
416 repo.ui.debug(
419 repo.ui.debug(
417 b'filtering %s because not a supported clonebundle scheme\n'
420 b'filtering %s because not a supported clonebundle scheme\n'
418 % url
421 % url
419 )
422 )
420 continue
423 continue
421
424
422 spec = entry.get(b'BUNDLESPEC')
425 spec = entry.get(b'BUNDLESPEC')
423 if spec:
426 if spec:
424 try:
427 try:
425 bundlespec = parsebundlespec(repo, spec, strict=True)
428 bundlespec = parsebundlespec(repo, spec, strict=True)
426
429
427 # If a stream clone was requested, filter out non-streamclone
430 # If a stream clone was requested, filter out non-streamclone
428 # entries.
431 # entries.
429 if streamclonerequested and not isstreamclonespec(bundlespec):
432 if streamclonerequested and not isstreamclonespec(bundlespec):
430 repo.ui.debug(
433 repo.ui.debug(
431 b'filtering %s because not a stream clone\n' % url
434 b'filtering %s because not a stream clone\n' % url
432 )
435 )
433 continue
436 continue
434
437
435 except error.InvalidBundleSpecification as e:
438 except error.InvalidBundleSpecification as e:
436 repo.ui.debug(stringutil.forcebytestr(e) + b'\n')
439 repo.ui.debug(stringutil.forcebytestr(e) + b'\n')
437 continue
440 continue
438 except error.UnsupportedBundleSpecification as e:
441 except error.UnsupportedBundleSpecification as e:
439 repo.ui.debug(
442 repo.ui.debug(
440 b'filtering %s because unsupported bundle '
443 b'filtering %s because unsupported bundle '
441 b'spec: %s\n' % (url, stringutil.forcebytestr(e))
444 b'spec: %s\n' % (url, stringutil.forcebytestr(e))
442 )
445 )
443 continue
446 continue
444 # If we don't have a spec and requested a stream clone, we don't know
447 # If we don't have a spec and requested a stream clone, we don't know
445 # what the entry is so don't attempt to apply it.
448 # what the entry is so don't attempt to apply it.
446 elif streamclonerequested:
449 elif streamclonerequested:
447 repo.ui.debug(
450 repo.ui.debug(
448 b'filtering %s because cannot determine if a stream '
451 b'filtering %s because cannot determine if a stream '
449 b'clone bundle\n' % url
452 b'clone bundle\n' % url
450 )
453 )
451 continue
454 continue
452
455
453 if b'REQUIRESNI' in entry and not sslutil.hassni:
456 if b'REQUIRESNI' in entry and not sslutil.hassni:
454 repo.ui.debug(b'filtering %s because SNI not supported\n' % url)
457 repo.ui.debug(b'filtering %s because SNI not supported\n' % url)
455 continue
458 continue
456
459
457 if b'REQUIREDRAM' in entry:
460 if b'REQUIREDRAM' in entry:
458 try:
461 try:
459 requiredram = util.sizetoint(entry[b'REQUIREDRAM'])
462 requiredram = util.sizetoint(entry[b'REQUIREDRAM'])
460 except error.ParseError:
463 except error.ParseError:
461 repo.ui.debug(
464 repo.ui.debug(
462 b'filtering %s due to a bad REQUIREDRAM attribute\n' % url
465 b'filtering %s due to a bad REQUIREDRAM attribute\n' % url
463 )
466 )
464 continue
467 continue
465 actualram = repo.ui.estimatememory()
468 actualram = repo.ui.estimatememory()
466 if actualram is not None and actualram * 0.66 < requiredram:
469 if actualram is not None and actualram * 0.66 < requiredram:
467 repo.ui.debug(
470 repo.ui.debug(
468 b'filtering %s as it needs more than 2/3 of system memory\n'
471 b'filtering %s as it needs more than 2/3 of system memory\n'
469 % url
472 % url
470 )
473 )
471 continue
474 continue
472
475
473 newentries.append(entry)
476 newentries.append(entry)
474
477
475 return newentries
478 return newentries
476
479
477
480
478 class clonebundleentry:
481 class clonebundleentry:
479 """Represents an item in a clone bundles manifest.
482 """Represents an item in a clone bundles manifest.
480
483
481 This rich class is needed to support sorting since sorted() in Python 3
484 This rich class is needed to support sorting since sorted() in Python 3
482 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
485 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
483 won't work.
486 won't work.
484 """
487 """
485
488
486 def __init__(self, value, prefers):
489 def __init__(self, value, prefers):
487 self.value = value
490 self.value = value
488 self.prefers = prefers
491 self.prefers = prefers
489
492
490 def _cmp(self, other):
493 def _cmp(self, other):
491 for prefkey, prefvalue in self.prefers:
494 for prefkey, prefvalue in self.prefers:
492 avalue = self.value.get(prefkey)
495 avalue = self.value.get(prefkey)
493 bvalue = other.value.get(prefkey)
496 bvalue = other.value.get(prefkey)
494
497
495 # Special case for b missing attribute and a matches exactly.
498 # Special case for b missing attribute and a matches exactly.
496 if avalue is not None and bvalue is None and avalue == prefvalue:
499 if avalue is not None and bvalue is None and avalue == prefvalue:
497 return -1
500 return -1
498
501
499 # Special case for a missing attribute and b matches exactly.
502 # Special case for a missing attribute and b matches exactly.
500 if bvalue is not None and avalue is None and bvalue == prefvalue:
503 if bvalue is not None and avalue is None and bvalue == prefvalue:
501 return 1
504 return 1
502
505
503 # We can't compare unless attribute present on both.
506 # We can't compare unless attribute present on both.
504 if avalue is None or bvalue is None:
507 if avalue is None or bvalue is None:
505 continue
508 continue
506
509
507 # Same values should fall back to next attribute.
510 # Same values should fall back to next attribute.
508 if avalue == bvalue:
511 if avalue == bvalue:
509 continue
512 continue
510
513
511 # Exact matches come first.
514 # Exact matches come first.
512 if avalue == prefvalue:
515 if avalue == prefvalue:
513 return -1
516 return -1
514 if bvalue == prefvalue:
517 if bvalue == prefvalue:
515 return 1
518 return 1
516
519
517 # Fall back to next attribute.
520 # Fall back to next attribute.
518 continue
521 continue
519
522
520 # If we got here we couldn't sort by attributes and prefers. Fall
523 # If we got here we couldn't sort by attributes and prefers. Fall
521 # back to index order.
524 # back to index order.
522 return 0
525 return 0
523
526
524 def __lt__(self, other):
527 def __lt__(self, other):
525 return self._cmp(other) < 0
528 return self._cmp(other) < 0
526
529
527 def __gt__(self, other):
530 def __gt__(self, other):
528 return self._cmp(other) > 0
531 return self._cmp(other) > 0
529
532
530 def __eq__(self, other):
533 def __eq__(self, other):
531 return self._cmp(other) == 0
534 return self._cmp(other) == 0
532
535
533 def __le__(self, other):
536 def __le__(self, other):
534 return self._cmp(other) <= 0
537 return self._cmp(other) <= 0
535
538
536 def __ge__(self, other):
539 def __ge__(self, other):
537 return self._cmp(other) >= 0
540 return self._cmp(other) >= 0
538
541
539 def __ne__(self, other):
542 def __ne__(self, other):
540 return self._cmp(other) != 0
543 return self._cmp(other) != 0
541
544
542
545
543 def sortclonebundleentries(ui, entries):
546 def sortclonebundleentries(ui, entries):
544 prefers = ui.configlist(b'ui', b'clonebundleprefers')
547 prefers = ui.configlist(b'ui', b'clonebundleprefers')
545 if not prefers:
548 if not prefers:
546 return list(entries)
549 return list(entries)
547
550
548 def _split(p):
551 def _split(p):
549 if b'=' not in p:
552 if b'=' not in p:
550 hint = _(b"each comma separated item should be key=value pairs")
553 hint = _(b"each comma separated item should be key=value pairs")
551 raise error.Abort(
554 raise error.Abort(
552 _(b"invalid ui.clonebundleprefers item: %s") % p, hint=hint
555 _(b"invalid ui.clonebundleprefers item: %s") % p, hint=hint
553 )
556 )
554 return p.split(b'=', 1)
557 return p.split(b'=', 1)
555
558
556 prefers = [_split(p) for p in prefers]
559 prefers = [_split(p) for p in prefers]
557
560
558 items = sorted(clonebundleentry(v, prefers) for v in entries)
561 items = sorted(clonebundleentry(v, prefers) for v in entries)
559 return [i.value for i in items]
562 return [i.value for i in items]
@@ -1,2944 +1,2943 b''
1 # exchange.py - utility to exchange data between repos.
1 # exchange.py - utility to exchange data between repos.
2 #
2 #
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8
8
9 import collections
9 import collections
10 import weakref
10 import weakref
11
11
12 from .i18n import _
12 from .i18n import _
13 from .node import (
13 from .node import (
14 hex,
14 hex,
15 nullrev,
15 nullrev,
16 )
16 )
17 from . import (
17 from . import (
18 bookmarks as bookmod,
18 bookmarks as bookmod,
19 bundle2,
19 bundle2,
20 bundlecaches,
20 bundlecaches,
21 changegroup,
21 changegroup,
22 discovery,
22 discovery,
23 error,
23 error,
24 lock as lockmod,
24 lock as lockmod,
25 logexchange,
25 logexchange,
26 narrowspec,
26 narrowspec,
27 obsolete,
27 obsolete,
28 obsutil,
28 obsutil,
29 phases,
29 phases,
30 pushkey,
30 pushkey,
31 pycompat,
31 pycompat,
32 requirements,
32 requirements,
33 scmutil,
33 scmutil,
34 streamclone,
34 streamclone,
35 url as urlmod,
35 url as urlmod,
36 util,
36 util,
37 wireprototypes,
37 wireprototypes,
38 )
38 )
39 from .utils import (
39 from .utils import (
40 hashutil,
40 hashutil,
41 stringutil,
41 stringutil,
42 urlutil,
42 urlutil,
43 )
43 )
44 from .interfaces import repository
44 from .interfaces import repository
45
45
46 urlerr = util.urlerr
46 urlerr = util.urlerr
47 urlreq = util.urlreq
47 urlreq = util.urlreq
48
48
49 _NARROWACL_SECTION = b'narrowacl'
49 _NARROWACL_SECTION = b'narrowacl'
50
50
51
51
52 def readbundle(ui, fh, fname, vfs=None):
52 def readbundle(ui, fh, fname, vfs=None):
53 header = changegroup.readexactly(fh, 4)
53 header = changegroup.readexactly(fh, 4)
54
54
55 alg = None
55 alg = None
56 if not fname:
56 if not fname:
57 fname = b"stream"
57 fname = b"stream"
58 if not header.startswith(b'HG') and header.startswith(b'\0'):
58 if not header.startswith(b'HG') and header.startswith(b'\0'):
59 fh = changegroup.headerlessfixup(fh, header)
59 fh = changegroup.headerlessfixup(fh, header)
60 header = b"HG10"
60 header = b"HG10"
61 alg = b'UN'
61 alg = b'UN'
62 elif vfs:
62 elif vfs:
63 fname = vfs.join(fname)
63 fname = vfs.join(fname)
64
64
65 magic, version = header[0:2], header[2:4]
65 magic, version = header[0:2], header[2:4]
66
66
67 if magic != b'HG':
67 if magic != b'HG':
68 raise error.Abort(_(b'%s: not a Mercurial bundle') % fname)
68 raise error.Abort(_(b'%s: not a Mercurial bundle') % fname)
69 if version == b'10':
69 if version == b'10':
70 if alg is None:
70 if alg is None:
71 alg = changegroup.readexactly(fh, 2)
71 alg = changegroup.readexactly(fh, 2)
72 return changegroup.cg1unpacker(fh, alg)
72 return changegroup.cg1unpacker(fh, alg)
73 elif version.startswith(b'2'):
73 elif version.startswith(b'2'):
74 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
74 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
75 elif version == b'S1':
75 elif version == b'S1':
76 return streamclone.streamcloneapplier(fh)
76 return streamclone.streamcloneapplier(fh)
77 else:
77 else:
78 raise error.Abort(
78 raise error.Abort(
79 _(b'%s: unknown bundle version %s') % (fname, version)
79 _(b'%s: unknown bundle version %s') % (fname, version)
80 )
80 )
81
81
82
82
83 def _format_params(params):
83 def _format_params(params):
84 parts = []
84 parts = []
85 for key, value in sorted(params.items()):
85 for key, value in sorted(params.items()):
86 value = urlreq.quote(value)
86 value = urlreq.quote(value)
87 parts.append(b"%s=%s" % (key, value))
87 parts.append(b"%s=%s" % (key, value))
88 return b';'.join(parts)
88 return b';'.join(parts)
89
89
90
90
91 def getbundlespec(ui, fh):
91 def getbundlespec(ui, fh):
92 """Infer the bundlespec from a bundle file handle.
92 """Infer the bundlespec from a bundle file handle.
93
93
94 The input file handle is seeked and the original seek position is not
94 The input file handle is seeked and the original seek position is not
95 restored.
95 restored.
96 """
96 """
97
97
98 def speccompression(alg):
98 def speccompression(alg):
99 try:
99 try:
100 return util.compengines.forbundletype(alg).bundletype()[0]
100 return util.compengines.forbundletype(alg).bundletype()[0]
101 except KeyError:
101 except KeyError:
102 return None
102 return None
103
103
104 params = {}
104 params = {}
105
105
106 b = readbundle(ui, fh, None)
106 b = readbundle(ui, fh, None)
107 if isinstance(b, changegroup.cg1unpacker):
107 if isinstance(b, changegroup.cg1unpacker):
108 alg = b._type
108 alg = b._type
109 if alg == b'_truncatedBZ':
109 if alg == b'_truncatedBZ':
110 alg = b'BZ'
110 alg = b'BZ'
111 comp = speccompression(alg)
111 comp = speccompression(alg)
112 if not comp:
112 if not comp:
113 raise error.Abort(_(b'unknown compression algorithm: %s') % alg)
113 raise error.Abort(_(b'unknown compression algorithm: %s') % alg)
114 return b'%s-v1' % comp
114 return b'%s-v1' % comp
115 elif isinstance(b, bundle2.unbundle20):
115 elif isinstance(b, bundle2.unbundle20):
116 if b'Compression' in b.params:
116 if b'Compression' in b.params:
117 comp = speccompression(b.params[b'Compression'])
117 comp = speccompression(b.params[b'Compression'])
118 if not comp:
118 if not comp:
119 raise error.Abort(
119 raise error.Abort(
120 _(b'unknown compression algorithm: %s') % comp
120 _(b'unknown compression algorithm: %s') % comp
121 )
121 )
122 else:
122 else:
123 comp = b'none'
123 comp = b'none'
124
124
125 version = None
125 version = None
126 for part in b.iterparts():
126 for part in b.iterparts():
127 if part.type == b'changegroup':
127 if part.type == b'changegroup':
128 cgversion = part.params[b'version']
128 cgversion = part.params[b'version']
129 if cgversion in (b'01', b'02'):
129 if cgversion in (b'01', b'02'):
130 version = b'v2'
130 version = b'v2'
131 elif cgversion in (b'03',):
131 elif cgversion in (b'03',):
132 version = b'v2'
132 version = b'v2'
133 params[b'cg.version'] = cgversion
133 params[b'cg.version'] = cgversion
134 else:
134 else:
135 raise error.Abort(
135 raise error.Abort(
136 _(
136 _(
137 b'changegroup version %s does not have '
137 b'changegroup version %s does not have '
138 b'a known bundlespec'
138 b'a known bundlespec'
139 )
139 )
140 % version,
140 % version,
141 hint=_(b'try upgrading your Mercurial client'),
141 hint=_(b'try upgrading your Mercurial client'),
142 )
142 )
143 elif part.type == b'stream2' and version is None:
143 elif part.type == b'stream2' and version is None:
144 # A stream2 part requires to be part of a v2 bundle
144 # A stream2 part requires to be part of a v2 bundle
145 requirements = urlreq.unquote(part.params[b'requirements'])
145 requirements = urlreq.unquote(part.params[b'requirements'])
146 splitted = requirements.split()
146 splitted = requirements.split()
147 params = bundle2._formatrequirementsparams(splitted)
147 params = bundle2._formatrequirementsparams(splitted)
148 return b'none-v2;stream=v2;%s' % params
148 return b'none-v2;stream=v2;%s' % params
149 elif part.type == b'stream3-exp' and version is None:
149 elif part.type == b'stream3-exp' and version is None:
150 # A stream3 part requires to be part of a v2 bundle
150 # A stream3 part requires to be part of a v2 bundle
151 requirements = urlreq.unquote(part.params[b'requirements'])
151 requirements = urlreq.unquote(part.params[b'requirements'])
152 splitted = requirements.split()
152 splitted = requirements.split()
153 params = bundle2._formatrequirementsparams(splitted)
153 params = bundle2._formatrequirementsparams(splitted)
154 return b'none-v2;stream=v3-exp;%s' % params
154 return b'none-v2;stream=v3-exp;%s' % params
155 elif part.type == b'obsmarkers':
155 elif part.type == b'obsmarkers':
156 params[b'obsolescence'] = b'yes'
156 params[b'obsolescence'] = b'yes'
157 if not part.mandatory:
157 if not part.mandatory:
158 params[b'obsolescence-mandatory'] = b'no'
158 params[b'obsolescence-mandatory'] = b'no'
159
159
160 if not version:
160 if not version:
161 raise error.Abort(
161 params[b'changegroup'] = b'no'
162 _(b'could not identify changegroup version in bundle')
162 version = b'v2'
163 )
164 spec = b'%s-%s' % (comp, version)
163 spec = b'%s-%s' % (comp, version)
165 if params:
164 if params:
166 spec += b';'
165 spec += b';'
167 spec += _format_params(params)
166 spec += _format_params(params)
168 return spec
167 return spec
169
168
170 elif isinstance(b, streamclone.streamcloneapplier):
169 elif isinstance(b, streamclone.streamcloneapplier):
171 requirements = streamclone.readbundle1header(fh)[2]
170 requirements = streamclone.readbundle1header(fh)[2]
172 formatted = bundle2._formatrequirementsparams(requirements)
171 formatted = bundle2._formatrequirementsparams(requirements)
173 return b'none-packed1;%s' % formatted
172 return b'none-packed1;%s' % formatted
174 else:
173 else:
175 raise error.Abort(_(b'unknown bundle type: %s') % b)
174 raise error.Abort(_(b'unknown bundle type: %s') % b)
176
175
177
176
178 def _computeoutgoing(repo, heads, common):
177 def _computeoutgoing(repo, heads, common):
179 """Computes which revs are outgoing given a set of common
178 """Computes which revs are outgoing given a set of common
180 and a set of heads.
179 and a set of heads.
181
180
182 This is a separate function so extensions can have access to
181 This is a separate function so extensions can have access to
183 the logic.
182 the logic.
184
183
185 Returns a discovery.outgoing object.
184 Returns a discovery.outgoing object.
186 """
185 """
187 cl = repo.changelog
186 cl = repo.changelog
188 if common:
187 if common:
189 hasnode = cl.hasnode
188 hasnode = cl.hasnode
190 common = [n for n in common if hasnode(n)]
189 common = [n for n in common if hasnode(n)]
191 else:
190 else:
192 common = [repo.nullid]
191 common = [repo.nullid]
193 if not heads:
192 if not heads:
194 heads = cl.heads()
193 heads = cl.heads()
195 return discovery.outgoing(repo, common, heads)
194 return discovery.outgoing(repo, common, heads)
196
195
197
196
198 def _checkpublish(pushop):
197 def _checkpublish(pushop):
199 repo = pushop.repo
198 repo = pushop.repo
200 ui = repo.ui
199 ui = repo.ui
201 behavior = ui.config(b'experimental', b'auto-publish')
200 behavior = ui.config(b'experimental', b'auto-publish')
202 if pushop.publish or behavior not in (b'warn', b'confirm', b'abort'):
201 if pushop.publish or behavior not in (b'warn', b'confirm', b'abort'):
203 return
202 return
204 remotephases = listkeys(pushop.remote, b'phases')
203 remotephases = listkeys(pushop.remote, b'phases')
205 if not remotephases.get(b'publishing', False):
204 if not remotephases.get(b'publishing', False):
206 return
205 return
207
206
208 if pushop.revs is None:
207 if pushop.revs is None:
209 published = repo.filtered(b'served').revs(b'not public()')
208 published = repo.filtered(b'served').revs(b'not public()')
210 else:
209 else:
211 published = repo.revs(b'::%ln - public()', pushop.revs)
210 published = repo.revs(b'::%ln - public()', pushop.revs)
212 # we want to use pushop.revs in the revset even if they themselves are
211 # we want to use pushop.revs in the revset even if they themselves are
213 # secret, but we don't want to have anything that the server won't see
212 # secret, but we don't want to have anything that the server won't see
214 # in the result of this expression
213 # in the result of this expression
215 published &= repo.filtered(b'served')
214 published &= repo.filtered(b'served')
216 if published:
215 if published:
217 if behavior == b'warn':
216 if behavior == b'warn':
218 ui.warn(
217 ui.warn(
219 _(b'%i changesets about to be published\n') % len(published)
218 _(b'%i changesets about to be published\n') % len(published)
220 )
219 )
221 elif behavior == b'confirm':
220 elif behavior == b'confirm':
222 if ui.promptchoice(
221 if ui.promptchoice(
223 _(b'push and publish %i changesets (yn)?$$ &Yes $$ &No')
222 _(b'push and publish %i changesets (yn)?$$ &Yes $$ &No')
224 % len(published)
223 % len(published)
225 ):
224 ):
226 raise error.CanceledError(_(b'user quit'))
225 raise error.CanceledError(_(b'user quit'))
227 elif behavior == b'abort':
226 elif behavior == b'abort':
228 msg = _(b'push would publish %i changesets') % len(published)
227 msg = _(b'push would publish %i changesets') % len(published)
229 hint = _(
228 hint = _(
230 b"use --publish or adjust 'experimental.auto-publish'"
229 b"use --publish or adjust 'experimental.auto-publish'"
231 b" config"
230 b" config"
232 )
231 )
233 raise error.Abort(msg, hint=hint)
232 raise error.Abort(msg, hint=hint)
234
233
235
234
236 def _forcebundle1(op):
235 def _forcebundle1(op):
237 """return true if a pull/push must use bundle1
236 """return true if a pull/push must use bundle1
238
237
239 This function is used to allow testing of the older bundle version"""
238 This function is used to allow testing of the older bundle version"""
240 ui = op.repo.ui
239 ui = op.repo.ui
241 # The goal is this config is to allow developer to choose the bundle
240 # The goal is this config is to allow developer to choose the bundle
242 # version used during exchanged. This is especially handy during test.
241 # version used during exchanged. This is especially handy during test.
243 # Value is a list of bundle version to be picked from, highest version
242 # Value is a list of bundle version to be picked from, highest version
244 # should be used.
243 # should be used.
245 #
244 #
246 # developer config: devel.legacy.exchange
245 # developer config: devel.legacy.exchange
247 exchange = ui.configlist(b'devel', b'legacy.exchange')
246 exchange = ui.configlist(b'devel', b'legacy.exchange')
248 forcebundle1 = b'bundle2' not in exchange and b'bundle1' in exchange
247 forcebundle1 = b'bundle2' not in exchange and b'bundle1' in exchange
249 return forcebundle1 or not op.remote.capable(b'bundle2')
248 return forcebundle1 or not op.remote.capable(b'bundle2')
250
249
251
250
252 class pushoperation:
251 class pushoperation:
253 """A object that represent a single push operation
252 """A object that represent a single push operation
254
253
255 Its purpose is to carry push related state and very common operations.
254 Its purpose is to carry push related state and very common operations.
256
255
257 A new pushoperation should be created at the beginning of each push and
256 A new pushoperation should be created at the beginning of each push and
258 discarded afterward.
257 discarded afterward.
259 """
258 """
260
259
261 def __init__(
260 def __init__(
262 self,
261 self,
263 repo,
262 repo,
264 remote,
263 remote,
265 force=False,
264 force=False,
266 revs=None,
265 revs=None,
267 newbranch=False,
266 newbranch=False,
268 bookmarks=(),
267 bookmarks=(),
269 publish=False,
268 publish=False,
270 pushvars=None,
269 pushvars=None,
271 ):
270 ):
272 # repo we push from
271 # repo we push from
273 self.repo = repo
272 self.repo = repo
274 self.ui = repo.ui
273 self.ui = repo.ui
275 # repo we push to
274 # repo we push to
276 self.remote = remote
275 self.remote = remote
277 # force option provided
276 # force option provided
278 self.force = force
277 self.force = force
279 # revs to be pushed (None is "all")
278 # revs to be pushed (None is "all")
280 self.revs = revs
279 self.revs = revs
281 # bookmark explicitly pushed
280 # bookmark explicitly pushed
282 self.bookmarks = bookmarks
281 self.bookmarks = bookmarks
283 # allow push of new branch
282 # allow push of new branch
284 self.newbranch = newbranch
283 self.newbranch = newbranch
285 # step already performed
284 # step already performed
286 # (used to check what steps have been already performed through bundle2)
285 # (used to check what steps have been already performed through bundle2)
287 self.stepsdone = set()
286 self.stepsdone = set()
288 # Integer version of the changegroup push result
287 # Integer version of the changegroup push result
289 # - None means nothing to push
288 # - None means nothing to push
290 # - 0 means HTTP error
289 # - 0 means HTTP error
291 # - 1 means we pushed and remote head count is unchanged *or*
290 # - 1 means we pushed and remote head count is unchanged *or*
292 # we have outgoing changesets but refused to push
291 # we have outgoing changesets but refused to push
293 # - other values as described by addchangegroup()
292 # - other values as described by addchangegroup()
294 self.cgresult = None
293 self.cgresult = None
295 # Boolean value for the bookmark push
294 # Boolean value for the bookmark push
296 self.bkresult = None
295 self.bkresult = None
297 # discover.outgoing object (contains common and outgoing data)
296 # discover.outgoing object (contains common and outgoing data)
298 self.outgoing = None
297 self.outgoing = None
299 # all remote topological heads before the push
298 # all remote topological heads before the push
300 self.remoteheads = None
299 self.remoteheads = None
301 # Details of the remote branch pre and post push
300 # Details of the remote branch pre and post push
302 #
301 #
303 # mapping: {'branch': ([remoteheads],
302 # mapping: {'branch': ([remoteheads],
304 # [newheads],
303 # [newheads],
305 # [unsyncedheads],
304 # [unsyncedheads],
306 # [discardedheads])}
305 # [discardedheads])}
307 # - branch: the branch name
306 # - branch: the branch name
308 # - remoteheads: the list of remote heads known locally
307 # - remoteheads: the list of remote heads known locally
309 # None if the branch is new
308 # None if the branch is new
310 # - newheads: the new remote heads (known locally) with outgoing pushed
309 # - newheads: the new remote heads (known locally) with outgoing pushed
311 # - unsyncedheads: the list of remote heads unknown locally.
310 # - unsyncedheads: the list of remote heads unknown locally.
312 # - discardedheads: the list of remote heads made obsolete by the push
311 # - discardedheads: the list of remote heads made obsolete by the push
313 self.pushbranchmap = None
312 self.pushbranchmap = None
314 # testable as a boolean indicating if any nodes are missing locally.
313 # testable as a boolean indicating if any nodes are missing locally.
315 self.incoming = None
314 self.incoming = None
316 # summary of the remote phase situation
315 # summary of the remote phase situation
317 self.remotephases = None
316 self.remotephases = None
318 # phases changes that must be pushed along side the changesets
317 # phases changes that must be pushed along side the changesets
319 self.outdatedphases = None
318 self.outdatedphases = None
320 # phases changes that must be pushed if changeset push fails
319 # phases changes that must be pushed if changeset push fails
321 self.fallbackoutdatedphases = None
320 self.fallbackoutdatedphases = None
322 # outgoing obsmarkers
321 # outgoing obsmarkers
323 self.outobsmarkers = set()
322 self.outobsmarkers = set()
324 # outgoing bookmarks, list of (bm, oldnode | '', newnode | '')
323 # outgoing bookmarks, list of (bm, oldnode | '', newnode | '')
325 self.outbookmarks = []
324 self.outbookmarks = []
326 # transaction manager
325 # transaction manager
327 self.trmanager = None
326 self.trmanager = None
328 # map { pushkey partid -> callback handling failure}
327 # map { pushkey partid -> callback handling failure}
329 # used to handle exception from mandatory pushkey part failure
328 # used to handle exception from mandatory pushkey part failure
330 self.pkfailcb = {}
329 self.pkfailcb = {}
331 # an iterable of pushvars or None
330 # an iterable of pushvars or None
332 self.pushvars = pushvars
331 self.pushvars = pushvars
333 # publish pushed changesets
332 # publish pushed changesets
334 self.publish = publish
333 self.publish = publish
335
334
336 @util.propertycache
335 @util.propertycache
337 def futureheads(self):
336 def futureheads(self):
338 """future remote heads if the changeset push succeeds"""
337 """future remote heads if the changeset push succeeds"""
339 return self.outgoing.ancestorsof
338 return self.outgoing.ancestorsof
340
339
341 @util.propertycache
340 @util.propertycache
342 def fallbackheads(self):
341 def fallbackheads(self):
343 """future remote heads if the changeset push fails"""
342 """future remote heads if the changeset push fails"""
344 if self.revs is None:
343 if self.revs is None:
345 # not target to push, all common are relevant
344 # not target to push, all common are relevant
346 return self.outgoing.commonheads
345 return self.outgoing.commonheads
347 unfi = self.repo.unfiltered()
346 unfi = self.repo.unfiltered()
348 # I want cheads = heads(::push_heads and ::commonheads)
347 # I want cheads = heads(::push_heads and ::commonheads)
349 #
348 #
350 # To push, we already computed
349 # To push, we already computed
351 # common = (::commonheads)
350 # common = (::commonheads)
352 # missing = ((commonheads::push_heads) - commonheads)
351 # missing = ((commonheads::push_heads) - commonheads)
353 #
352 #
354 # So we basically search
353 # So we basically search
355 #
354 #
356 # almost_heads = heads((parents(missing) + push_heads) & common)
355 # almost_heads = heads((parents(missing) + push_heads) & common)
357 #
356 #
358 # We use "almost" here as this can return revision that are ancestors
357 # We use "almost" here as this can return revision that are ancestors
359 # of other in the set and we need to explicitly turn it into an
358 # of other in the set and we need to explicitly turn it into an
360 # antichain later. We can do so using:
359 # antichain later. We can do so using:
361 #
360 #
362 # cheads = heads(almost_heads::almost_heads)
361 # cheads = heads(almost_heads::almost_heads)
363 #
362 #
364 # In pratice the code is a bit more convulted to avoid some extra
363 # In pratice the code is a bit more convulted to avoid some extra
365 # computation. It aims at doing the same computation as highlighted
364 # computation. It aims at doing the same computation as highlighted
366 # above however.
365 # above however.
367 common = self.outgoing.common
366 common = self.outgoing.common
368 unfi = self.repo.unfiltered()
367 unfi = self.repo.unfiltered()
369 cl = unfi.changelog
368 cl = unfi.changelog
370 to_rev = cl.index.rev
369 to_rev = cl.index.rev
371 to_node = cl.node
370 to_node = cl.node
372 parent_revs = cl.parentrevs
371 parent_revs = cl.parentrevs
373 unselected = []
372 unselected = []
374 cheads = set()
373 cheads = set()
375 # XXX-perf: `self.revs` and `outgoing.missing` could hold revs directly
374 # XXX-perf: `self.revs` and `outgoing.missing` could hold revs directly
376 for n in self.revs:
375 for n in self.revs:
377 r = to_rev(n)
376 r = to_rev(n)
378 if r in common:
377 if r in common:
379 cheads.add(r)
378 cheads.add(r)
380 else:
379 else:
381 unselected.append(r)
380 unselected.append(r)
382 known_non_heads = cl.ancestors(cheads, inclusive=True)
381 known_non_heads = cl.ancestors(cheads, inclusive=True)
383 if unselected:
382 if unselected:
384 missing_revs = {to_rev(n) for n in self.outgoing.missing}
383 missing_revs = {to_rev(n) for n in self.outgoing.missing}
385 missing_revs.add(nullrev)
384 missing_revs.add(nullrev)
386 root_points = set()
385 root_points = set()
387 for r in missing_revs:
386 for r in missing_revs:
388 p1, p2 = parent_revs(r)
387 p1, p2 = parent_revs(r)
389 if p1 not in missing_revs and p1 not in known_non_heads:
388 if p1 not in missing_revs and p1 not in known_non_heads:
390 root_points.add(p1)
389 root_points.add(p1)
391 if p2 not in missing_revs and p2 not in known_non_heads:
390 if p2 not in missing_revs and p2 not in known_non_heads:
392 root_points.add(p2)
391 root_points.add(p2)
393 if root_points:
392 if root_points:
394 heads = unfi.revs('heads(%ld::%ld)', root_points, root_points)
393 heads = unfi.revs('heads(%ld::%ld)', root_points, root_points)
395 cheads.update(heads)
394 cheads.update(heads)
396 # XXX-perf: could this be a set of revision?
395 # XXX-perf: could this be a set of revision?
397 return [to_node(r) for r in sorted(cheads)]
396 return [to_node(r) for r in sorted(cheads)]
398
397
399 @property
398 @property
400 def commonheads(self):
399 def commonheads(self):
401 """set of all common heads after changeset bundle push"""
400 """set of all common heads after changeset bundle push"""
402 if self.cgresult:
401 if self.cgresult:
403 return self.futureheads
402 return self.futureheads
404 else:
403 else:
405 return self.fallbackheads
404 return self.fallbackheads
406
405
407
406
408 # mapping of message used when pushing bookmark
407 # mapping of message used when pushing bookmark
409 bookmsgmap = {
408 bookmsgmap = {
410 b'update': (
409 b'update': (
411 _(b"updating bookmark %s\n"),
410 _(b"updating bookmark %s\n"),
412 _(b'updating bookmark %s failed\n'),
411 _(b'updating bookmark %s failed\n'),
413 ),
412 ),
414 b'export': (
413 b'export': (
415 _(b"exporting bookmark %s\n"),
414 _(b"exporting bookmark %s\n"),
416 _(b'exporting bookmark %s failed\n'),
415 _(b'exporting bookmark %s failed\n'),
417 ),
416 ),
418 b'delete': (
417 b'delete': (
419 _(b"deleting remote bookmark %s\n"),
418 _(b"deleting remote bookmark %s\n"),
420 _(b'deleting remote bookmark %s failed\n'),
419 _(b'deleting remote bookmark %s failed\n'),
421 ),
420 ),
422 }
421 }
423
422
424
423
425 def push(
424 def push(
426 repo,
425 repo,
427 remote,
426 remote,
428 force=False,
427 force=False,
429 revs=None,
428 revs=None,
430 newbranch=False,
429 newbranch=False,
431 bookmarks=(),
430 bookmarks=(),
432 publish=False,
431 publish=False,
433 opargs=None,
432 opargs=None,
434 ):
433 ):
435 """Push outgoing changesets (limited by revs) from a local
434 """Push outgoing changesets (limited by revs) from a local
436 repository to remote. Return an integer:
435 repository to remote. Return an integer:
437 - None means nothing to push
436 - None means nothing to push
438 - 0 means HTTP error
437 - 0 means HTTP error
439 - 1 means we pushed and remote head count is unchanged *or*
438 - 1 means we pushed and remote head count is unchanged *or*
440 we have outgoing changesets but refused to push
439 we have outgoing changesets but refused to push
441 - other values as described by addchangegroup()
440 - other values as described by addchangegroup()
442 """
441 """
443 if opargs is None:
442 if opargs is None:
444 opargs = {}
443 opargs = {}
445 pushop = pushoperation(
444 pushop = pushoperation(
446 repo,
445 repo,
447 remote,
446 remote,
448 force,
447 force,
449 revs,
448 revs,
450 newbranch,
449 newbranch,
451 bookmarks,
450 bookmarks,
452 publish,
451 publish,
453 **pycompat.strkwargs(opargs)
452 **pycompat.strkwargs(opargs)
454 )
453 )
455 if pushop.remote.local():
454 if pushop.remote.local():
456 missing = (
455 missing = (
457 set(pushop.repo.requirements) - pushop.remote.local().supported
456 set(pushop.repo.requirements) - pushop.remote.local().supported
458 )
457 )
459 if missing:
458 if missing:
460 msg = _(
459 msg = _(
461 b"required features are not"
460 b"required features are not"
462 b" supported in the destination:"
461 b" supported in the destination:"
463 b" %s"
462 b" %s"
464 ) % (b', '.join(sorted(missing)))
463 ) % (b', '.join(sorted(missing)))
465 raise error.Abort(msg)
464 raise error.Abort(msg)
466
465
467 if not pushop.remote.canpush():
466 if not pushop.remote.canpush():
468 raise error.Abort(_(b"destination does not support push"))
467 raise error.Abort(_(b"destination does not support push"))
469
468
470 if not pushop.remote.capable(b'unbundle'):
469 if not pushop.remote.capable(b'unbundle'):
471 raise error.Abort(
470 raise error.Abort(
472 _(
471 _(
473 b'cannot push: destination does not support the '
472 b'cannot push: destination does not support the '
474 b'unbundle wire protocol command'
473 b'unbundle wire protocol command'
475 )
474 )
476 )
475 )
477 for category in sorted(bundle2.read_remote_wanted_sidedata(pushop.remote)):
476 for category in sorted(bundle2.read_remote_wanted_sidedata(pushop.remote)):
478 # Check that a computer is registered for that category for at least
477 # Check that a computer is registered for that category for at least
479 # one revlog kind.
478 # one revlog kind.
480 for kind, computers in repo._sidedata_computers.items():
479 for kind, computers in repo._sidedata_computers.items():
481 if computers.get(category):
480 if computers.get(category):
482 break
481 break
483 else:
482 else:
484 raise error.Abort(
483 raise error.Abort(
485 _(
484 _(
486 b'cannot push: required sidedata category not supported'
485 b'cannot push: required sidedata category not supported'
487 b" by this client: '%s'"
486 b" by this client: '%s'"
488 )
487 )
489 % pycompat.bytestr(category)
488 % pycompat.bytestr(category)
490 )
489 )
491 # get lock as we might write phase data
490 # get lock as we might write phase data
492 wlock = lock = None
491 wlock = lock = None
493 try:
492 try:
494 # bundle2 push may receive a reply bundle touching bookmarks
493 # bundle2 push may receive a reply bundle touching bookmarks
495 # requiring the wlock. Take it now to ensure proper ordering.
494 # requiring the wlock. Take it now to ensure proper ordering.
496 maypushback = pushop.ui.configbool(b'experimental', b'bundle2.pushback')
495 maypushback = pushop.ui.configbool(b'experimental', b'bundle2.pushback')
497 if (
496 if (
498 (not _forcebundle1(pushop))
497 (not _forcebundle1(pushop))
499 and maypushback
498 and maypushback
500 and not bookmod.bookmarksinstore(repo)
499 and not bookmod.bookmarksinstore(repo)
501 ):
500 ):
502 wlock = pushop.repo.wlock()
501 wlock = pushop.repo.wlock()
503 lock = pushop.repo.lock()
502 lock = pushop.repo.lock()
504 pushop.trmanager = transactionmanager(
503 pushop.trmanager = transactionmanager(
505 pushop.repo, b'push-response', pushop.remote.url()
504 pushop.repo, b'push-response', pushop.remote.url()
506 )
505 )
507 except error.LockUnavailable as err:
506 except error.LockUnavailable as err:
508 # source repo cannot be locked.
507 # source repo cannot be locked.
509 # We do not abort the push, but just disable the local phase
508 # We do not abort the push, but just disable the local phase
510 # synchronisation.
509 # synchronisation.
511 msg = b'cannot lock source repository: %s\n' % stringutil.forcebytestr(
510 msg = b'cannot lock source repository: %s\n' % stringutil.forcebytestr(
512 err
511 err
513 )
512 )
514 pushop.ui.debug(msg)
513 pushop.ui.debug(msg)
515
514
516 with wlock or util.nullcontextmanager():
515 with wlock or util.nullcontextmanager():
517 with lock or util.nullcontextmanager():
516 with lock or util.nullcontextmanager():
518 with pushop.trmanager or util.nullcontextmanager():
517 with pushop.trmanager or util.nullcontextmanager():
519 pushop.repo.checkpush(pushop)
518 pushop.repo.checkpush(pushop)
520 _checkpublish(pushop)
519 _checkpublish(pushop)
521 _pushdiscovery(pushop)
520 _pushdiscovery(pushop)
522 if not pushop.force:
521 if not pushop.force:
523 _checksubrepostate(pushop)
522 _checksubrepostate(pushop)
524 if not _forcebundle1(pushop):
523 if not _forcebundle1(pushop):
525 _pushbundle2(pushop)
524 _pushbundle2(pushop)
526 _pushchangeset(pushop)
525 _pushchangeset(pushop)
527 _pushsyncphase(pushop)
526 _pushsyncphase(pushop)
528 _pushobsolete(pushop)
527 _pushobsolete(pushop)
529 _pushbookmark(pushop)
528 _pushbookmark(pushop)
530
529
531 if repo.ui.configbool(b'experimental', b'remotenames'):
530 if repo.ui.configbool(b'experimental', b'remotenames'):
532 logexchange.pullremotenames(repo, remote)
531 logexchange.pullremotenames(repo, remote)
533
532
534 return pushop
533 return pushop
535
534
536
535
537 # list of steps to perform discovery before push
536 # list of steps to perform discovery before push
538 pushdiscoveryorder = []
537 pushdiscoveryorder = []
539
538
540 # Mapping between step name and function
539 # Mapping between step name and function
541 #
540 #
542 # This exists to help extensions wrap steps if necessary
541 # This exists to help extensions wrap steps if necessary
543 pushdiscoverymapping = {}
542 pushdiscoverymapping = {}
544
543
545
544
546 def pushdiscovery(stepname):
545 def pushdiscovery(stepname):
547 """decorator for function performing discovery before push
546 """decorator for function performing discovery before push
548
547
549 The function is added to the step -> function mapping and appended to the
548 The function is added to the step -> function mapping and appended to the
550 list of steps. Beware that decorated function will be added in order (this
549 list of steps. Beware that decorated function will be added in order (this
551 may matter).
550 may matter).
552
551
553 You can only use this decorator for a new step, if you want to wrap a step
552 You can only use this decorator for a new step, if you want to wrap a step
554 from an extension, change the pushdiscovery dictionary directly."""
553 from an extension, change the pushdiscovery dictionary directly."""
555
554
556 def dec(func):
555 def dec(func):
557 assert stepname not in pushdiscoverymapping
556 assert stepname not in pushdiscoverymapping
558 pushdiscoverymapping[stepname] = func
557 pushdiscoverymapping[stepname] = func
559 pushdiscoveryorder.append(stepname)
558 pushdiscoveryorder.append(stepname)
560 return func
559 return func
561
560
562 return dec
561 return dec
563
562
564
563
565 def _pushdiscovery(pushop):
564 def _pushdiscovery(pushop):
566 """Run all discovery steps"""
565 """Run all discovery steps"""
567 for stepname in pushdiscoveryorder:
566 for stepname in pushdiscoveryorder:
568 step = pushdiscoverymapping[stepname]
567 step = pushdiscoverymapping[stepname]
569 step(pushop)
568 step(pushop)
570
569
571
570
572 def _checksubrepostate(pushop):
571 def _checksubrepostate(pushop):
573 """Ensure all outgoing referenced subrepo revisions are present locally"""
572 """Ensure all outgoing referenced subrepo revisions are present locally"""
574
573
575 repo = pushop.repo
574 repo = pushop.repo
576
575
577 # If the repository does not use subrepos, skip the expensive
576 # If the repository does not use subrepos, skip the expensive
578 # manifest checks.
577 # manifest checks.
579 if not len(repo.file(b'.hgsub')) or not len(repo.file(b'.hgsubstate')):
578 if not len(repo.file(b'.hgsub')) or not len(repo.file(b'.hgsubstate')):
580 return
579 return
581
580
582 for n in pushop.outgoing.missing:
581 for n in pushop.outgoing.missing:
583 ctx = repo[n]
582 ctx = repo[n]
584
583
585 if b'.hgsub' in ctx.manifest() and b'.hgsubstate' in ctx.files():
584 if b'.hgsub' in ctx.manifest() and b'.hgsubstate' in ctx.files():
586 for subpath in sorted(ctx.substate):
585 for subpath in sorted(ctx.substate):
587 sub = ctx.sub(subpath)
586 sub = ctx.sub(subpath)
588 sub.verify(onpush=True)
587 sub.verify(onpush=True)
589
588
590
589
591 @pushdiscovery(b'changeset')
590 @pushdiscovery(b'changeset')
592 def _pushdiscoverychangeset(pushop):
591 def _pushdiscoverychangeset(pushop):
593 """discover the changeset that need to be pushed"""
592 """discover the changeset that need to be pushed"""
594 fci = discovery.findcommonincoming
593 fci = discovery.findcommonincoming
595 if pushop.revs:
594 if pushop.revs:
596 commoninc = fci(
595 commoninc = fci(
597 pushop.repo,
596 pushop.repo,
598 pushop.remote,
597 pushop.remote,
599 force=pushop.force,
598 force=pushop.force,
600 ancestorsof=pushop.revs,
599 ancestorsof=pushop.revs,
601 )
600 )
602 else:
601 else:
603 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
602 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
604 common, inc, remoteheads = commoninc
603 common, inc, remoteheads = commoninc
605 fco = discovery.findcommonoutgoing
604 fco = discovery.findcommonoutgoing
606 outgoing = fco(
605 outgoing = fco(
607 pushop.repo,
606 pushop.repo,
608 pushop.remote,
607 pushop.remote,
609 onlyheads=pushop.revs,
608 onlyheads=pushop.revs,
610 commoninc=commoninc,
609 commoninc=commoninc,
611 force=pushop.force,
610 force=pushop.force,
612 )
611 )
613 pushop.outgoing = outgoing
612 pushop.outgoing = outgoing
614 pushop.remoteheads = remoteheads
613 pushop.remoteheads = remoteheads
615 pushop.incoming = inc
614 pushop.incoming = inc
616
615
617
616
618 @pushdiscovery(b'phase')
617 @pushdiscovery(b'phase')
619 def _pushdiscoveryphase(pushop):
618 def _pushdiscoveryphase(pushop):
620 """discover the phase that needs to be pushed
619 """discover the phase that needs to be pushed
621
620
622 (computed for both success and failure case for changesets push)"""
621 (computed for both success and failure case for changesets push)"""
623 outgoing = pushop.outgoing
622 outgoing = pushop.outgoing
624 repo = pushop.repo
623 repo = pushop.repo
625 unfi = repo.unfiltered()
624 unfi = repo.unfiltered()
626 cl = unfi.changelog
625 cl = unfi.changelog
627 to_rev = cl.index.rev
626 to_rev = cl.index.rev
628 remotephases = listkeys(pushop.remote, b'phases')
627 remotephases = listkeys(pushop.remote, b'phases')
629
628
630 if (
629 if (
631 pushop.ui.configbool(b'ui', b'_usedassubrepo')
630 pushop.ui.configbool(b'ui', b'_usedassubrepo')
632 and remotephases # server supports phases
631 and remotephases # server supports phases
633 and not pushop.outgoing.missing # no changesets to be pushed
632 and not pushop.outgoing.missing # no changesets to be pushed
634 and remotephases.get(b'publishing', False)
633 and remotephases.get(b'publishing', False)
635 ):
634 ):
636 # When:
635 # When:
637 # - this is a subrepo push
636 # - this is a subrepo push
638 # - and remote support phase
637 # - and remote support phase
639 # - and no changeset are to be pushed
638 # - and no changeset are to be pushed
640 # - and remote is publishing
639 # - and remote is publishing
641 # We may be in issue 3781 case!
640 # We may be in issue 3781 case!
642 # We drop the possible phase synchronisation done by
641 # We drop the possible phase synchronisation done by
643 # courtesy to publish changesets possibly locally draft
642 # courtesy to publish changesets possibly locally draft
644 # on the remote.
643 # on the remote.
645 pushop.outdatedphases = []
644 pushop.outdatedphases = []
646 pushop.fallbackoutdatedphases = []
645 pushop.fallbackoutdatedphases = []
647 return
646 return
648
647
649 fallbackheads_rev = {to_rev(n) for n in pushop.fallbackheads}
648 fallbackheads_rev = {to_rev(n) for n in pushop.fallbackheads}
650 pushop.remotephases = phases.RemotePhasesSummary(
649 pushop.remotephases = phases.RemotePhasesSummary(
651 pushop.repo,
650 pushop.repo,
652 fallbackheads_rev,
651 fallbackheads_rev,
653 remotephases,
652 remotephases,
654 )
653 )
655 droots = set(pushop.remotephases.draft_roots)
654 droots = set(pushop.remotephases.draft_roots)
656
655
657 fallback_publishing = pushop.remotephases.publishing
656 fallback_publishing = pushop.remotephases.publishing
658 push_publishing = pushop.remotephases.publishing or pushop.publish
657 push_publishing = pushop.remotephases.publishing or pushop.publish
659 missing_revs = {to_rev(n) for n in outgoing.missing}
658 missing_revs = {to_rev(n) for n in outgoing.missing}
660 drafts = unfi._phasecache.get_raw_set(unfi, phases.draft)
659 drafts = unfi._phasecache.get_raw_set(unfi, phases.draft)
661
660
662 if fallback_publishing:
661 if fallback_publishing:
663 fallback_roots = droots - missing_revs
662 fallback_roots = droots - missing_revs
664 revset = b'heads(%ld::%ld)'
663 revset = b'heads(%ld::%ld)'
665 else:
664 else:
666 fallback_roots = droots - drafts
665 fallback_roots = droots - drafts
667 fallback_roots -= missing_revs
666 fallback_roots -= missing_revs
668 # Get the list of all revs draft on remote but public here.
667 # Get the list of all revs draft on remote but public here.
669 revset = b'heads((%ld::%ld) and public())'
668 revset = b'heads((%ld::%ld) and public())'
670 if not fallback_roots:
669 if not fallback_roots:
671 fallback = fallback_rev = []
670 fallback = fallback_rev = []
672 else:
671 else:
673 fallback_rev = unfi.revs(revset, fallback_roots, fallbackheads_rev)
672 fallback_rev = unfi.revs(revset, fallback_roots, fallbackheads_rev)
674 fallback = [repo[r] for r in fallback_rev]
673 fallback = [repo[r] for r in fallback_rev]
675
674
676 if push_publishing:
675 if push_publishing:
677 published = missing_revs.copy()
676 published = missing_revs.copy()
678 else:
677 else:
679 published = missing_revs - drafts
678 published = missing_revs - drafts
680 if pushop.publish:
679 if pushop.publish:
681 published.update(fallbackheads_rev & drafts)
680 published.update(fallbackheads_rev & drafts)
682 elif fallback:
681 elif fallback:
683 published.update(fallback_rev)
682 published.update(fallback_rev)
684
683
685 pushop.outdatedphases = [repo[r] for r in cl.headrevs(published)]
684 pushop.outdatedphases = [repo[r] for r in cl.headrevs(published)]
686 pushop.fallbackoutdatedphases = fallback
685 pushop.fallbackoutdatedphases = fallback
687
686
688
687
689 @pushdiscovery(b'obsmarker')
688 @pushdiscovery(b'obsmarker')
690 def _pushdiscoveryobsmarkers(pushop):
689 def _pushdiscoveryobsmarkers(pushop):
691 if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt):
690 if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt):
692 return
691 return
693
692
694 if not pushop.repo.obsstore:
693 if not pushop.repo.obsstore:
695 return
694 return
696
695
697 if b'obsolete' not in listkeys(pushop.remote, b'namespaces'):
696 if b'obsolete' not in listkeys(pushop.remote, b'namespaces'):
698 return
697 return
699
698
700 repo = pushop.repo
699 repo = pushop.repo
701 # very naive computation, that can be quite expensive on big repo.
700 # very naive computation, that can be quite expensive on big repo.
702 # However: evolution is currently slow on them anyway.
701 # However: evolution is currently slow on them anyway.
703 nodes = (c.node() for c in repo.set(b'::%ln', pushop.futureheads))
702 nodes = (c.node() for c in repo.set(b'::%ln', pushop.futureheads))
704 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
703 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
705
704
706
705
707 @pushdiscovery(b'bookmarks')
706 @pushdiscovery(b'bookmarks')
708 def _pushdiscoverybookmarks(pushop):
707 def _pushdiscoverybookmarks(pushop):
709 ui = pushop.ui
708 ui = pushop.ui
710 repo = pushop.repo.unfiltered()
709 repo = pushop.repo.unfiltered()
711 remote = pushop.remote
710 remote = pushop.remote
712 ui.debug(b"checking for updated bookmarks\n")
711 ui.debug(b"checking for updated bookmarks\n")
713 ancestors = ()
712 ancestors = ()
714 if pushop.revs:
713 if pushop.revs:
715 revnums = pycompat.maplist(repo.changelog.rev, pushop.revs)
714 revnums = pycompat.maplist(repo.changelog.rev, pushop.revs)
716 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
715 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
717
716
718 remotebookmark = bookmod.unhexlifybookmarks(listkeys(remote, b'bookmarks'))
717 remotebookmark = bookmod.unhexlifybookmarks(listkeys(remote, b'bookmarks'))
719
718
720 explicit = {
719 explicit = {
721 repo._bookmarks.expandname(bookmark) for bookmark in pushop.bookmarks
720 repo._bookmarks.expandname(bookmark) for bookmark in pushop.bookmarks
722 }
721 }
723
722
724 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
723 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
725 return _processcompared(pushop, ancestors, explicit, remotebookmark, comp)
724 return _processcompared(pushop, ancestors, explicit, remotebookmark, comp)
726
725
727
726
728 def _processcompared(pushop, pushed, explicit, remotebms, comp):
727 def _processcompared(pushop, pushed, explicit, remotebms, comp):
729 """take decision on bookmarks to push to the remote repo
728 """take decision on bookmarks to push to the remote repo
730
729
731 Exists to help extensions alter this behavior.
730 Exists to help extensions alter this behavior.
732 """
731 """
733 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
732 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
734
733
735 repo = pushop.repo
734 repo = pushop.repo
736
735
737 for b, scid, dcid in advsrc:
736 for b, scid, dcid in advsrc:
738 if b in explicit:
737 if b in explicit:
739 explicit.remove(b)
738 explicit.remove(b)
740 if not pushed or repo[scid].rev() in pushed:
739 if not pushed or repo[scid].rev() in pushed:
741 pushop.outbookmarks.append((b, dcid, scid))
740 pushop.outbookmarks.append((b, dcid, scid))
742 # search added bookmark
741 # search added bookmark
743 for b, scid, dcid in addsrc:
742 for b, scid, dcid in addsrc:
744 if b in explicit:
743 if b in explicit:
745 explicit.remove(b)
744 explicit.remove(b)
746 if bookmod.isdivergent(b):
745 if bookmod.isdivergent(b):
747 pushop.ui.warn(_(b'cannot push divergent bookmark %s!\n') % b)
746 pushop.ui.warn(_(b'cannot push divergent bookmark %s!\n') % b)
748 pushop.bkresult = 2
747 pushop.bkresult = 2
749 else:
748 else:
750 pushop.outbookmarks.append((b, b'', scid))
749 pushop.outbookmarks.append((b, b'', scid))
751 # search for overwritten bookmark
750 # search for overwritten bookmark
752 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
751 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
753 if b in explicit:
752 if b in explicit:
754 explicit.remove(b)
753 explicit.remove(b)
755 pushop.outbookmarks.append((b, dcid, scid))
754 pushop.outbookmarks.append((b, dcid, scid))
756 # search for bookmark to delete
755 # search for bookmark to delete
757 for b, scid, dcid in adddst:
756 for b, scid, dcid in adddst:
758 if b in explicit:
757 if b in explicit:
759 explicit.remove(b)
758 explicit.remove(b)
760 # treat as "deleted locally"
759 # treat as "deleted locally"
761 pushop.outbookmarks.append((b, dcid, b''))
760 pushop.outbookmarks.append((b, dcid, b''))
762 # identical bookmarks shouldn't get reported
761 # identical bookmarks shouldn't get reported
763 for b, scid, dcid in same:
762 for b, scid, dcid in same:
764 if b in explicit:
763 if b in explicit:
765 explicit.remove(b)
764 explicit.remove(b)
766
765
767 if explicit:
766 if explicit:
768 explicit = sorted(explicit)
767 explicit = sorted(explicit)
769 # we should probably list all of them
768 # we should probably list all of them
770 pushop.ui.warn(
769 pushop.ui.warn(
771 _(
770 _(
772 b'bookmark %s does not exist on the local '
771 b'bookmark %s does not exist on the local '
773 b'or remote repository!\n'
772 b'or remote repository!\n'
774 )
773 )
775 % explicit[0]
774 % explicit[0]
776 )
775 )
777 pushop.bkresult = 2
776 pushop.bkresult = 2
778
777
779 pushop.outbookmarks.sort()
778 pushop.outbookmarks.sort()
780
779
781
780
782 def _pushcheckoutgoing(pushop):
781 def _pushcheckoutgoing(pushop):
783 outgoing = pushop.outgoing
782 outgoing = pushop.outgoing
784 unfi = pushop.repo.unfiltered()
783 unfi = pushop.repo.unfiltered()
785 if not outgoing.missing:
784 if not outgoing.missing:
786 # nothing to push
785 # nothing to push
787 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
786 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
788 return False
787 return False
789 # something to push
788 # something to push
790 if not pushop.force:
789 if not pushop.force:
791 # if repo.obsstore == False --> no obsolete
790 # if repo.obsstore == False --> no obsolete
792 # then, save the iteration
791 # then, save the iteration
793 if unfi.obsstore:
792 if unfi.obsstore:
794 # this message are here for 80 char limit reason
793 # this message are here for 80 char limit reason
795 mso = _(b"push includes obsolete changeset: %s!")
794 mso = _(b"push includes obsolete changeset: %s!")
796 mspd = _(b"push includes phase-divergent changeset: %s!")
795 mspd = _(b"push includes phase-divergent changeset: %s!")
797 mscd = _(b"push includes content-divergent changeset: %s!")
796 mscd = _(b"push includes content-divergent changeset: %s!")
798 mst = {
797 mst = {
799 b"orphan": _(b"push includes orphan changeset: %s!"),
798 b"orphan": _(b"push includes orphan changeset: %s!"),
800 b"phase-divergent": mspd,
799 b"phase-divergent": mspd,
801 b"content-divergent": mscd,
800 b"content-divergent": mscd,
802 }
801 }
803 # If we are to push if there is at least one
802 # If we are to push if there is at least one
804 # obsolete or unstable changeset in missing, at
803 # obsolete or unstable changeset in missing, at
805 # least one of the missinghead will be obsolete or
804 # least one of the missinghead will be obsolete or
806 # unstable. So checking heads only is ok
805 # unstable. So checking heads only is ok
807 for node in outgoing.ancestorsof:
806 for node in outgoing.ancestorsof:
808 ctx = unfi[node]
807 ctx = unfi[node]
809 if ctx.obsolete():
808 if ctx.obsolete():
810 raise error.Abort(mso % ctx)
809 raise error.Abort(mso % ctx)
811 elif ctx.isunstable():
810 elif ctx.isunstable():
812 # TODO print more than one instability in the abort
811 # TODO print more than one instability in the abort
813 # message
812 # message
814 raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
813 raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
815
814
816 discovery.checkheads(pushop)
815 discovery.checkheads(pushop)
817 return True
816 return True
818
817
819
818
820 # List of names of steps to perform for an outgoing bundle2, order matters.
819 # List of names of steps to perform for an outgoing bundle2, order matters.
821 b2partsgenorder = []
820 b2partsgenorder = []
822
821
823 # Mapping between step name and function
822 # Mapping between step name and function
824 #
823 #
825 # This exists to help extensions wrap steps if necessary
824 # This exists to help extensions wrap steps if necessary
826 b2partsgenmapping = {}
825 b2partsgenmapping = {}
827
826
828
827
829 def b2partsgenerator(stepname, idx=None):
828 def b2partsgenerator(stepname, idx=None):
830 """decorator for function generating bundle2 part
829 """decorator for function generating bundle2 part
831
830
832 The function is added to the step -> function mapping and appended to the
831 The function is added to the step -> function mapping and appended to the
833 list of steps. Beware that decorated functions will be added in order
832 list of steps. Beware that decorated functions will be added in order
834 (this may matter).
833 (this may matter).
835
834
836 You can only use this decorator for new steps, if you want to wrap a step
835 You can only use this decorator for new steps, if you want to wrap a step
837 from an extension, attack the b2partsgenmapping dictionary directly."""
836 from an extension, attack the b2partsgenmapping dictionary directly."""
838
837
839 def dec(func):
838 def dec(func):
840 assert stepname not in b2partsgenmapping
839 assert stepname not in b2partsgenmapping
841 b2partsgenmapping[stepname] = func
840 b2partsgenmapping[stepname] = func
842 if idx is None:
841 if idx is None:
843 b2partsgenorder.append(stepname)
842 b2partsgenorder.append(stepname)
844 else:
843 else:
845 b2partsgenorder.insert(idx, stepname)
844 b2partsgenorder.insert(idx, stepname)
846 return func
845 return func
847
846
848 return dec
847 return dec
849
848
850
849
851 def _pushb2ctxcheckheads(pushop, bundler):
850 def _pushb2ctxcheckheads(pushop, bundler):
852 """Generate race condition checking parts
851 """Generate race condition checking parts
853
852
854 Exists as an independent function to aid extensions
853 Exists as an independent function to aid extensions
855 """
854 """
856 # * 'force' do not check for push race,
855 # * 'force' do not check for push race,
857 # * if we don't push anything, there are nothing to check.
856 # * if we don't push anything, there are nothing to check.
858 if not pushop.force and pushop.outgoing.ancestorsof:
857 if not pushop.force and pushop.outgoing.ancestorsof:
859 allowunrelated = b'related' in bundler.capabilities.get(
858 allowunrelated = b'related' in bundler.capabilities.get(
860 b'checkheads', ()
859 b'checkheads', ()
861 )
860 )
862 emptyremote = pushop.pushbranchmap is None
861 emptyremote = pushop.pushbranchmap is None
863 if not allowunrelated or emptyremote:
862 if not allowunrelated or emptyremote:
864 bundler.newpart(b'check:heads', data=iter(pushop.remoteheads))
863 bundler.newpart(b'check:heads', data=iter(pushop.remoteheads))
865 else:
864 else:
866 affected = set()
865 affected = set()
867 for branch, heads in pushop.pushbranchmap.items():
866 for branch, heads in pushop.pushbranchmap.items():
868 remoteheads, newheads, unsyncedheads, discardedheads = heads
867 remoteheads, newheads, unsyncedheads, discardedheads = heads
869 if remoteheads is not None:
868 if remoteheads is not None:
870 remote = set(remoteheads)
869 remote = set(remoteheads)
871 affected |= set(discardedheads) & remote
870 affected |= set(discardedheads) & remote
872 affected |= remote - set(newheads)
871 affected |= remote - set(newheads)
873 if affected:
872 if affected:
874 data = iter(sorted(affected))
873 data = iter(sorted(affected))
875 bundler.newpart(b'check:updated-heads', data=data)
874 bundler.newpart(b'check:updated-heads', data=data)
876
875
877
876
878 def _pushing(pushop):
877 def _pushing(pushop):
879 """return True if we are pushing anything"""
878 """return True if we are pushing anything"""
880 return bool(
879 return bool(
881 pushop.outgoing.missing
880 pushop.outgoing.missing
882 or pushop.outdatedphases
881 or pushop.outdatedphases
883 or pushop.outobsmarkers
882 or pushop.outobsmarkers
884 or pushop.outbookmarks
883 or pushop.outbookmarks
885 )
884 )
886
885
887
886
888 @b2partsgenerator(b'check-bookmarks')
887 @b2partsgenerator(b'check-bookmarks')
889 def _pushb2checkbookmarks(pushop, bundler):
888 def _pushb2checkbookmarks(pushop, bundler):
890 """insert bookmark move checking"""
889 """insert bookmark move checking"""
891 if not _pushing(pushop) or pushop.force:
890 if not _pushing(pushop) or pushop.force:
892 return
891 return
893 b2caps = bundle2.bundle2caps(pushop.remote)
892 b2caps = bundle2.bundle2caps(pushop.remote)
894 hasbookmarkcheck = b'bookmarks' in b2caps
893 hasbookmarkcheck = b'bookmarks' in b2caps
895 if not (pushop.outbookmarks and hasbookmarkcheck):
894 if not (pushop.outbookmarks and hasbookmarkcheck):
896 return
895 return
897 data = []
896 data = []
898 for book, old, new in pushop.outbookmarks:
897 for book, old, new in pushop.outbookmarks:
899 data.append((book, old))
898 data.append((book, old))
900 checkdata = bookmod.binaryencode(pushop.repo, data)
899 checkdata = bookmod.binaryencode(pushop.repo, data)
901 bundler.newpart(b'check:bookmarks', data=checkdata)
900 bundler.newpart(b'check:bookmarks', data=checkdata)
902
901
903
902
904 @b2partsgenerator(b'check-phases')
903 @b2partsgenerator(b'check-phases')
905 def _pushb2checkphases(pushop, bundler):
904 def _pushb2checkphases(pushop, bundler):
906 """insert phase move checking"""
905 """insert phase move checking"""
907 if not _pushing(pushop) or pushop.force:
906 if not _pushing(pushop) or pushop.force:
908 return
907 return
909 b2caps = bundle2.bundle2caps(pushop.remote)
908 b2caps = bundle2.bundle2caps(pushop.remote)
910 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
909 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
911 if pushop.remotephases is not None and hasphaseheads:
910 if pushop.remotephases is not None and hasphaseheads:
912 # check that the remote phase has not changed
911 # check that the remote phase has not changed
913 checks = {p: [] for p in phases.allphases}
912 checks = {p: [] for p in phases.allphases}
914 to_node = pushop.repo.unfiltered().changelog.node
913 to_node = pushop.repo.unfiltered().changelog.node
915 checks[phases.public].extend(
914 checks[phases.public].extend(
916 to_node(r) for r in pushop.remotephases.public_heads
915 to_node(r) for r in pushop.remotephases.public_heads
917 )
916 )
918 checks[phases.draft].extend(
917 checks[phases.draft].extend(
919 to_node(r) for r in pushop.remotephases.draft_roots
918 to_node(r) for r in pushop.remotephases.draft_roots
920 )
919 )
921 if any(checks.values()):
920 if any(checks.values()):
922 for phase in checks:
921 for phase in checks:
923 checks[phase].sort()
922 checks[phase].sort()
924 checkdata = phases.binaryencode(checks)
923 checkdata = phases.binaryencode(checks)
925 bundler.newpart(b'check:phases', data=checkdata)
924 bundler.newpart(b'check:phases', data=checkdata)
926
925
927
926
928 @b2partsgenerator(b'changeset')
927 @b2partsgenerator(b'changeset')
929 def _pushb2ctx(pushop, bundler):
928 def _pushb2ctx(pushop, bundler):
930 """handle changegroup push through bundle2
929 """handle changegroup push through bundle2
931
930
932 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
931 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
933 """
932 """
934 if b'changesets' in pushop.stepsdone:
933 if b'changesets' in pushop.stepsdone:
935 return
934 return
936 pushop.stepsdone.add(b'changesets')
935 pushop.stepsdone.add(b'changesets')
937 # Send known heads to the server for race detection.
936 # Send known heads to the server for race detection.
938 if not _pushcheckoutgoing(pushop):
937 if not _pushcheckoutgoing(pushop):
939 return
938 return
940 pushop.repo.prepushoutgoinghooks(pushop)
939 pushop.repo.prepushoutgoinghooks(pushop)
941
940
942 _pushb2ctxcheckheads(pushop, bundler)
941 _pushb2ctxcheckheads(pushop, bundler)
943
942
944 b2caps = bundle2.bundle2caps(pushop.remote)
943 b2caps = bundle2.bundle2caps(pushop.remote)
945 version = b'01'
944 version = b'01'
946 cgversions = b2caps.get(b'changegroup')
945 cgversions = b2caps.get(b'changegroup')
947 if cgversions: # 3.1 and 3.2 ship with an empty value
946 if cgversions: # 3.1 and 3.2 ship with an empty value
948 cgversions = [
947 cgversions = [
949 v
948 v
950 for v in cgversions
949 for v in cgversions
951 if v in changegroup.supportedoutgoingversions(pushop.repo)
950 if v in changegroup.supportedoutgoingversions(pushop.repo)
952 ]
951 ]
953 if not cgversions:
952 if not cgversions:
954 raise error.Abort(_(b'no common changegroup version'))
953 raise error.Abort(_(b'no common changegroup version'))
955 version = max(cgversions)
954 version = max(cgversions)
956
955
957 remote_sidedata = bundle2.read_remote_wanted_sidedata(pushop.remote)
956 remote_sidedata = bundle2.read_remote_wanted_sidedata(pushop.remote)
958 cgstream = changegroup.makestream(
957 cgstream = changegroup.makestream(
959 pushop.repo,
958 pushop.repo,
960 pushop.outgoing,
959 pushop.outgoing,
961 version,
960 version,
962 b'push',
961 b'push',
963 bundlecaps=b2caps,
962 bundlecaps=b2caps,
964 remote_sidedata=remote_sidedata,
963 remote_sidedata=remote_sidedata,
965 )
964 )
966 cgpart = bundler.newpart(b'changegroup', data=cgstream)
965 cgpart = bundler.newpart(b'changegroup', data=cgstream)
967 if cgversions:
966 if cgversions:
968 cgpart.addparam(b'version', version)
967 cgpart.addparam(b'version', version)
969 if scmutil.istreemanifest(pushop.repo):
968 if scmutil.istreemanifest(pushop.repo):
970 cgpart.addparam(b'treemanifest', b'1')
969 cgpart.addparam(b'treemanifest', b'1')
971 if repository.REPO_FEATURE_SIDE_DATA in pushop.repo.features:
970 if repository.REPO_FEATURE_SIDE_DATA in pushop.repo.features:
972 cgpart.addparam(b'exp-sidedata', b'1')
971 cgpart.addparam(b'exp-sidedata', b'1')
973
972
974 def handlereply(op):
973 def handlereply(op):
975 """extract addchangegroup returns from server reply"""
974 """extract addchangegroup returns from server reply"""
976 cgreplies = op.records.getreplies(cgpart.id)
975 cgreplies = op.records.getreplies(cgpart.id)
977 assert len(cgreplies[b'changegroup']) == 1
976 assert len(cgreplies[b'changegroup']) == 1
978 pushop.cgresult = cgreplies[b'changegroup'][0][b'return']
977 pushop.cgresult = cgreplies[b'changegroup'][0][b'return']
979
978
980 return handlereply
979 return handlereply
981
980
982
981
983 @b2partsgenerator(b'phase')
982 @b2partsgenerator(b'phase')
984 def _pushb2phases(pushop, bundler):
983 def _pushb2phases(pushop, bundler):
985 """handle phase push through bundle2"""
984 """handle phase push through bundle2"""
986 if b'phases' in pushop.stepsdone:
985 if b'phases' in pushop.stepsdone:
987 return
986 return
988 b2caps = bundle2.bundle2caps(pushop.remote)
987 b2caps = bundle2.bundle2caps(pushop.remote)
989 ui = pushop.repo.ui
988 ui = pushop.repo.ui
990
989
991 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
990 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
992 haspushkey = b'pushkey' in b2caps
991 haspushkey = b'pushkey' in b2caps
993 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
992 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
994
993
995 if hasphaseheads and not legacyphase:
994 if hasphaseheads and not legacyphase:
996 return _pushb2phaseheads(pushop, bundler)
995 return _pushb2phaseheads(pushop, bundler)
997 elif haspushkey:
996 elif haspushkey:
998 return _pushb2phasespushkey(pushop, bundler)
997 return _pushb2phasespushkey(pushop, bundler)
999
998
1000
999
1001 def _pushb2phaseheads(pushop, bundler):
1000 def _pushb2phaseheads(pushop, bundler):
1002 """push phase information through a bundle2 - binary part"""
1001 """push phase information through a bundle2 - binary part"""
1003 pushop.stepsdone.add(b'phases')
1002 pushop.stepsdone.add(b'phases')
1004 if pushop.outdatedphases:
1003 if pushop.outdatedphases:
1005 updates = {p: [] for p in phases.allphases}
1004 updates = {p: [] for p in phases.allphases}
1006 updates[0].extend(h.node() for h in pushop.outdatedphases)
1005 updates[0].extend(h.node() for h in pushop.outdatedphases)
1007 phasedata = phases.binaryencode(updates)
1006 phasedata = phases.binaryencode(updates)
1008 bundler.newpart(b'phase-heads', data=phasedata)
1007 bundler.newpart(b'phase-heads', data=phasedata)
1009
1008
1010
1009
1011 def _pushb2phasespushkey(pushop, bundler):
1010 def _pushb2phasespushkey(pushop, bundler):
1012 """push phase information through a bundle2 - pushkey part"""
1011 """push phase information through a bundle2 - pushkey part"""
1013 pushop.stepsdone.add(b'phases')
1012 pushop.stepsdone.add(b'phases')
1014 part2node = []
1013 part2node = []
1015
1014
1016 def handlefailure(pushop, exc):
1015 def handlefailure(pushop, exc):
1017 targetid = int(exc.partid)
1016 targetid = int(exc.partid)
1018 for partid, node in part2node:
1017 for partid, node in part2node:
1019 if partid == targetid:
1018 if partid == targetid:
1020 raise error.Abort(_(b'updating %s to public failed') % node)
1019 raise error.Abort(_(b'updating %s to public failed') % node)
1021
1020
1022 enc = pushkey.encode
1021 enc = pushkey.encode
1023 for newremotehead in pushop.outdatedphases:
1022 for newremotehead in pushop.outdatedphases:
1024 part = bundler.newpart(b'pushkey')
1023 part = bundler.newpart(b'pushkey')
1025 part.addparam(b'namespace', enc(b'phases'))
1024 part.addparam(b'namespace', enc(b'phases'))
1026 part.addparam(b'key', enc(newremotehead.hex()))
1025 part.addparam(b'key', enc(newremotehead.hex()))
1027 part.addparam(b'old', enc(b'%d' % phases.draft))
1026 part.addparam(b'old', enc(b'%d' % phases.draft))
1028 part.addparam(b'new', enc(b'%d' % phases.public))
1027 part.addparam(b'new', enc(b'%d' % phases.public))
1029 part2node.append((part.id, newremotehead))
1028 part2node.append((part.id, newremotehead))
1030 pushop.pkfailcb[part.id] = handlefailure
1029 pushop.pkfailcb[part.id] = handlefailure
1031
1030
1032 def handlereply(op):
1031 def handlereply(op):
1033 for partid, node in part2node:
1032 for partid, node in part2node:
1034 partrep = op.records.getreplies(partid)
1033 partrep = op.records.getreplies(partid)
1035 results = partrep[b'pushkey']
1034 results = partrep[b'pushkey']
1036 assert len(results) <= 1
1035 assert len(results) <= 1
1037 msg = None
1036 msg = None
1038 if not results:
1037 if not results:
1039 msg = _(b'server ignored update of %s to public!\n') % node
1038 msg = _(b'server ignored update of %s to public!\n') % node
1040 elif not int(results[0][b'return']):
1039 elif not int(results[0][b'return']):
1041 msg = _(b'updating %s to public failed!\n') % node
1040 msg = _(b'updating %s to public failed!\n') % node
1042 if msg is not None:
1041 if msg is not None:
1043 pushop.ui.warn(msg)
1042 pushop.ui.warn(msg)
1044
1043
1045 return handlereply
1044 return handlereply
1046
1045
1047
1046
1048 @b2partsgenerator(b'obsmarkers')
1047 @b2partsgenerator(b'obsmarkers')
1049 def _pushb2obsmarkers(pushop, bundler):
1048 def _pushb2obsmarkers(pushop, bundler):
1050 if b'obsmarkers' in pushop.stepsdone:
1049 if b'obsmarkers' in pushop.stepsdone:
1051 return
1050 return
1052 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
1051 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
1053 if obsolete.commonversion(remoteversions) is None:
1052 if obsolete.commonversion(remoteversions) is None:
1054 return
1053 return
1055 pushop.stepsdone.add(b'obsmarkers')
1054 pushop.stepsdone.add(b'obsmarkers')
1056 if pushop.outobsmarkers:
1055 if pushop.outobsmarkers:
1057 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
1056 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
1058 bundle2.buildobsmarkerspart(bundler, markers)
1057 bundle2.buildobsmarkerspart(bundler, markers)
1059
1058
1060
1059
1061 @b2partsgenerator(b'bookmarks')
1060 @b2partsgenerator(b'bookmarks')
1062 def _pushb2bookmarks(pushop, bundler):
1061 def _pushb2bookmarks(pushop, bundler):
1063 """handle bookmark push through bundle2"""
1062 """handle bookmark push through bundle2"""
1064 if b'bookmarks' in pushop.stepsdone:
1063 if b'bookmarks' in pushop.stepsdone:
1065 return
1064 return
1066 b2caps = bundle2.bundle2caps(pushop.remote)
1065 b2caps = bundle2.bundle2caps(pushop.remote)
1067
1066
1068 legacy = pushop.repo.ui.configlist(b'devel', b'legacy.exchange')
1067 legacy = pushop.repo.ui.configlist(b'devel', b'legacy.exchange')
1069 legacybooks = b'bookmarks' in legacy
1068 legacybooks = b'bookmarks' in legacy
1070
1069
1071 if not legacybooks and b'bookmarks' in b2caps:
1070 if not legacybooks and b'bookmarks' in b2caps:
1072 return _pushb2bookmarkspart(pushop, bundler)
1071 return _pushb2bookmarkspart(pushop, bundler)
1073 elif b'pushkey' in b2caps:
1072 elif b'pushkey' in b2caps:
1074 return _pushb2bookmarkspushkey(pushop, bundler)
1073 return _pushb2bookmarkspushkey(pushop, bundler)
1075
1074
1076
1075
1077 def _bmaction(old, new):
1076 def _bmaction(old, new):
1078 """small utility for bookmark pushing"""
1077 """small utility for bookmark pushing"""
1079 if not old:
1078 if not old:
1080 return b'export'
1079 return b'export'
1081 elif not new:
1080 elif not new:
1082 return b'delete'
1081 return b'delete'
1083 return b'update'
1082 return b'update'
1084
1083
1085
1084
1086 def _abortonsecretctx(pushop, node, b):
1085 def _abortonsecretctx(pushop, node, b):
1087 """abort if a given bookmark points to a secret changeset"""
1086 """abort if a given bookmark points to a secret changeset"""
1088 if node and pushop.repo[node].phase() == phases.secret:
1087 if node and pushop.repo[node].phase() == phases.secret:
1089 raise error.Abort(
1088 raise error.Abort(
1090 _(b'cannot push bookmark %s as it points to a secret changeset') % b
1089 _(b'cannot push bookmark %s as it points to a secret changeset') % b
1091 )
1090 )
1092
1091
1093
1092
1094 def _pushb2bookmarkspart(pushop, bundler):
1093 def _pushb2bookmarkspart(pushop, bundler):
1095 pushop.stepsdone.add(b'bookmarks')
1094 pushop.stepsdone.add(b'bookmarks')
1096 if not pushop.outbookmarks:
1095 if not pushop.outbookmarks:
1097 return
1096 return
1098
1097
1099 allactions = []
1098 allactions = []
1100 data = []
1099 data = []
1101 for book, old, new in pushop.outbookmarks:
1100 for book, old, new in pushop.outbookmarks:
1102 _abortonsecretctx(pushop, new, book)
1101 _abortonsecretctx(pushop, new, book)
1103 data.append((book, new))
1102 data.append((book, new))
1104 allactions.append((book, _bmaction(old, new)))
1103 allactions.append((book, _bmaction(old, new)))
1105 checkdata = bookmod.binaryencode(pushop.repo, data)
1104 checkdata = bookmod.binaryencode(pushop.repo, data)
1106 bundler.newpart(b'bookmarks', data=checkdata)
1105 bundler.newpart(b'bookmarks', data=checkdata)
1107
1106
1108 def handlereply(op):
1107 def handlereply(op):
1109 ui = pushop.ui
1108 ui = pushop.ui
1110 # if success
1109 # if success
1111 for book, action in allactions:
1110 for book, action in allactions:
1112 ui.status(bookmsgmap[action][0] % book)
1111 ui.status(bookmsgmap[action][0] % book)
1113
1112
1114 return handlereply
1113 return handlereply
1115
1114
1116
1115
1117 def _pushb2bookmarkspushkey(pushop, bundler):
1116 def _pushb2bookmarkspushkey(pushop, bundler):
1118 pushop.stepsdone.add(b'bookmarks')
1117 pushop.stepsdone.add(b'bookmarks')
1119 part2book = []
1118 part2book = []
1120 enc = pushkey.encode
1119 enc = pushkey.encode
1121
1120
1122 def handlefailure(pushop, exc):
1121 def handlefailure(pushop, exc):
1123 targetid = int(exc.partid)
1122 targetid = int(exc.partid)
1124 for partid, book, action in part2book:
1123 for partid, book, action in part2book:
1125 if partid == targetid:
1124 if partid == targetid:
1126 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
1125 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
1127 # we should not be called for part we did not generated
1126 # we should not be called for part we did not generated
1128 assert False
1127 assert False
1129
1128
1130 for book, old, new in pushop.outbookmarks:
1129 for book, old, new in pushop.outbookmarks:
1131 _abortonsecretctx(pushop, new, book)
1130 _abortonsecretctx(pushop, new, book)
1132 part = bundler.newpart(b'pushkey')
1131 part = bundler.newpart(b'pushkey')
1133 part.addparam(b'namespace', enc(b'bookmarks'))
1132 part.addparam(b'namespace', enc(b'bookmarks'))
1134 part.addparam(b'key', enc(book))
1133 part.addparam(b'key', enc(book))
1135 part.addparam(b'old', enc(hex(old)))
1134 part.addparam(b'old', enc(hex(old)))
1136 part.addparam(b'new', enc(hex(new)))
1135 part.addparam(b'new', enc(hex(new)))
1137 action = b'update'
1136 action = b'update'
1138 if not old:
1137 if not old:
1139 action = b'export'
1138 action = b'export'
1140 elif not new:
1139 elif not new:
1141 action = b'delete'
1140 action = b'delete'
1142 part2book.append((part.id, book, action))
1141 part2book.append((part.id, book, action))
1143 pushop.pkfailcb[part.id] = handlefailure
1142 pushop.pkfailcb[part.id] = handlefailure
1144
1143
1145 def handlereply(op):
1144 def handlereply(op):
1146 ui = pushop.ui
1145 ui = pushop.ui
1147 for partid, book, action in part2book:
1146 for partid, book, action in part2book:
1148 partrep = op.records.getreplies(partid)
1147 partrep = op.records.getreplies(partid)
1149 results = partrep[b'pushkey']
1148 results = partrep[b'pushkey']
1150 assert len(results) <= 1
1149 assert len(results) <= 1
1151 if not results:
1150 if not results:
1152 pushop.ui.warn(_(b'server ignored bookmark %s update\n') % book)
1151 pushop.ui.warn(_(b'server ignored bookmark %s update\n') % book)
1153 else:
1152 else:
1154 ret = int(results[0][b'return'])
1153 ret = int(results[0][b'return'])
1155 if ret:
1154 if ret:
1156 ui.status(bookmsgmap[action][0] % book)
1155 ui.status(bookmsgmap[action][0] % book)
1157 else:
1156 else:
1158 ui.warn(bookmsgmap[action][1] % book)
1157 ui.warn(bookmsgmap[action][1] % book)
1159 if pushop.bkresult is not None:
1158 if pushop.bkresult is not None:
1160 pushop.bkresult = 1
1159 pushop.bkresult = 1
1161
1160
1162 return handlereply
1161 return handlereply
1163
1162
1164
1163
1165 @b2partsgenerator(b'pushvars', idx=0)
1164 @b2partsgenerator(b'pushvars', idx=0)
1166 def _getbundlesendvars(pushop, bundler):
1165 def _getbundlesendvars(pushop, bundler):
1167 '''send shellvars via bundle2'''
1166 '''send shellvars via bundle2'''
1168 pushvars = pushop.pushvars
1167 pushvars = pushop.pushvars
1169 if pushvars:
1168 if pushvars:
1170 shellvars = {}
1169 shellvars = {}
1171 for raw in pushvars:
1170 for raw in pushvars:
1172 if b'=' not in raw:
1171 if b'=' not in raw:
1173 msg = (
1172 msg = (
1174 b"unable to parse variable '%s', should follow "
1173 b"unable to parse variable '%s', should follow "
1175 b"'KEY=VALUE' or 'KEY=' format"
1174 b"'KEY=VALUE' or 'KEY=' format"
1176 )
1175 )
1177 raise error.Abort(msg % raw)
1176 raise error.Abort(msg % raw)
1178 k, v = raw.split(b'=', 1)
1177 k, v = raw.split(b'=', 1)
1179 shellvars[k] = v
1178 shellvars[k] = v
1180
1179
1181 part = bundler.newpart(b'pushvars')
1180 part = bundler.newpart(b'pushvars')
1182
1181
1183 for key, value in shellvars.items():
1182 for key, value in shellvars.items():
1184 part.addparam(key, value, mandatory=False)
1183 part.addparam(key, value, mandatory=False)
1185
1184
1186
1185
1187 def _pushbundle2(pushop):
1186 def _pushbundle2(pushop):
1188 """push data to the remote using bundle2
1187 """push data to the remote using bundle2
1189
1188
1190 The only currently supported type of data is changegroup but this will
1189 The only currently supported type of data is changegroup but this will
1191 evolve in the future."""
1190 evolve in the future."""
1192 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
1191 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
1193 pushback = pushop.trmanager and pushop.ui.configbool(
1192 pushback = pushop.trmanager and pushop.ui.configbool(
1194 b'experimental', b'bundle2.pushback'
1193 b'experimental', b'bundle2.pushback'
1195 )
1194 )
1196
1195
1197 # create reply capability
1196 # create reply capability
1198 capsblob = bundle2.encodecaps(
1197 capsblob = bundle2.encodecaps(
1199 bundle2.getrepocaps(pushop.repo, allowpushback=pushback, role=b'client')
1198 bundle2.getrepocaps(pushop.repo, allowpushback=pushback, role=b'client')
1200 )
1199 )
1201 bundler.newpart(b'replycaps', data=capsblob)
1200 bundler.newpart(b'replycaps', data=capsblob)
1202 replyhandlers = []
1201 replyhandlers = []
1203 for partgenname in b2partsgenorder:
1202 for partgenname in b2partsgenorder:
1204 partgen = b2partsgenmapping[partgenname]
1203 partgen = b2partsgenmapping[partgenname]
1205 ret = partgen(pushop, bundler)
1204 ret = partgen(pushop, bundler)
1206 if callable(ret):
1205 if callable(ret):
1207 replyhandlers.append(ret)
1206 replyhandlers.append(ret)
1208 # do not push if nothing to push
1207 # do not push if nothing to push
1209 if bundler.nbparts <= 1:
1208 if bundler.nbparts <= 1:
1210 return
1209 return
1211 stream = util.chunkbuffer(bundler.getchunks())
1210 stream = util.chunkbuffer(bundler.getchunks())
1212 try:
1211 try:
1213 try:
1212 try:
1214 with pushop.remote.commandexecutor() as e:
1213 with pushop.remote.commandexecutor() as e:
1215 reply = e.callcommand(
1214 reply = e.callcommand(
1216 b'unbundle',
1215 b'unbundle',
1217 {
1216 {
1218 b'bundle': stream,
1217 b'bundle': stream,
1219 b'heads': [b'force'],
1218 b'heads': [b'force'],
1220 b'url': pushop.remote.url(),
1219 b'url': pushop.remote.url(),
1221 },
1220 },
1222 ).result()
1221 ).result()
1223 except error.BundleValueError as exc:
1222 except error.BundleValueError as exc:
1224 raise error.RemoteError(_(b'missing support for %s') % exc)
1223 raise error.RemoteError(_(b'missing support for %s') % exc)
1225 try:
1224 try:
1226 trgetter = None
1225 trgetter = None
1227 if pushback:
1226 if pushback:
1228 trgetter = pushop.trmanager.transaction
1227 trgetter = pushop.trmanager.transaction
1229 op = bundle2.processbundle(
1228 op = bundle2.processbundle(
1230 pushop.repo,
1229 pushop.repo,
1231 reply,
1230 reply,
1232 trgetter,
1231 trgetter,
1233 remote=pushop.remote,
1232 remote=pushop.remote,
1234 )
1233 )
1235 except error.BundleValueError as exc:
1234 except error.BundleValueError as exc:
1236 raise error.RemoteError(_(b'missing support for %s') % exc)
1235 raise error.RemoteError(_(b'missing support for %s') % exc)
1237 except bundle2.AbortFromPart as exc:
1236 except bundle2.AbortFromPart as exc:
1238 pushop.ui.error(_(b'remote: %s\n') % exc)
1237 pushop.ui.error(_(b'remote: %s\n') % exc)
1239 if exc.hint is not None:
1238 if exc.hint is not None:
1240 pushop.ui.error(_(b'remote: %s\n') % (b'(%s)' % exc.hint))
1239 pushop.ui.error(_(b'remote: %s\n') % (b'(%s)' % exc.hint))
1241 raise error.RemoteError(_(b'push failed on remote'))
1240 raise error.RemoteError(_(b'push failed on remote'))
1242 except error.PushkeyFailed as exc:
1241 except error.PushkeyFailed as exc:
1243 partid = int(exc.partid)
1242 partid = int(exc.partid)
1244 if partid not in pushop.pkfailcb:
1243 if partid not in pushop.pkfailcb:
1245 raise
1244 raise
1246 pushop.pkfailcb[partid](pushop, exc)
1245 pushop.pkfailcb[partid](pushop, exc)
1247 for rephand in replyhandlers:
1246 for rephand in replyhandlers:
1248 rephand(op)
1247 rephand(op)
1249
1248
1250
1249
1251 def _pushchangeset(pushop):
1250 def _pushchangeset(pushop):
1252 """Make the actual push of changeset bundle to remote repo"""
1251 """Make the actual push of changeset bundle to remote repo"""
1253 if b'changesets' in pushop.stepsdone:
1252 if b'changesets' in pushop.stepsdone:
1254 return
1253 return
1255 pushop.stepsdone.add(b'changesets')
1254 pushop.stepsdone.add(b'changesets')
1256 if not _pushcheckoutgoing(pushop):
1255 if not _pushcheckoutgoing(pushop):
1257 return
1256 return
1258
1257
1259 # Should have verified this in push().
1258 # Should have verified this in push().
1260 assert pushop.remote.capable(b'unbundle')
1259 assert pushop.remote.capable(b'unbundle')
1261
1260
1262 pushop.repo.prepushoutgoinghooks(pushop)
1261 pushop.repo.prepushoutgoinghooks(pushop)
1263 outgoing = pushop.outgoing
1262 outgoing = pushop.outgoing
1264 # TODO: get bundlecaps from remote
1263 # TODO: get bundlecaps from remote
1265 bundlecaps = None
1264 bundlecaps = None
1266 # create a changegroup from local
1265 # create a changegroup from local
1267 if pushop.revs is None and not (
1266 if pushop.revs is None and not (
1268 outgoing.excluded or pushop.repo.changelog.filteredrevs
1267 outgoing.excluded or pushop.repo.changelog.filteredrevs
1269 ):
1268 ):
1270 # push everything,
1269 # push everything,
1271 # use the fast path, no race possible on push
1270 # use the fast path, no race possible on push
1272 cg = changegroup.makechangegroup(
1271 cg = changegroup.makechangegroup(
1273 pushop.repo,
1272 pushop.repo,
1274 outgoing,
1273 outgoing,
1275 b'01',
1274 b'01',
1276 b'push',
1275 b'push',
1277 fastpath=True,
1276 fastpath=True,
1278 bundlecaps=bundlecaps,
1277 bundlecaps=bundlecaps,
1279 )
1278 )
1280 else:
1279 else:
1281 cg = changegroup.makechangegroup(
1280 cg = changegroup.makechangegroup(
1282 pushop.repo, outgoing, b'01', b'push', bundlecaps=bundlecaps
1281 pushop.repo, outgoing, b'01', b'push', bundlecaps=bundlecaps
1283 )
1282 )
1284
1283
1285 # apply changegroup to remote
1284 # apply changegroup to remote
1286 # local repo finds heads on server, finds out what
1285 # local repo finds heads on server, finds out what
1287 # revs it must push. once revs transferred, if server
1286 # revs it must push. once revs transferred, if server
1288 # finds it has different heads (someone else won
1287 # finds it has different heads (someone else won
1289 # commit/push race), server aborts.
1288 # commit/push race), server aborts.
1290 if pushop.force:
1289 if pushop.force:
1291 remoteheads = [b'force']
1290 remoteheads = [b'force']
1292 else:
1291 else:
1293 remoteheads = pushop.remoteheads
1292 remoteheads = pushop.remoteheads
1294 # ssh: return remote's addchangegroup()
1293 # ssh: return remote's addchangegroup()
1295 # http: return remote's addchangegroup() or 0 for error
1294 # http: return remote's addchangegroup() or 0 for error
1296 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads, pushop.repo.url())
1295 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads, pushop.repo.url())
1297
1296
1298
1297
1299 def _pushsyncphase(pushop):
1298 def _pushsyncphase(pushop):
1300 """synchronise phase information locally and remotely"""
1299 """synchronise phase information locally and remotely"""
1301 cheads = pushop.commonheads
1300 cheads = pushop.commonheads
1302 # even when we don't push, exchanging phase data is useful
1301 # even when we don't push, exchanging phase data is useful
1303 remotephases = listkeys(pushop.remote, b'phases')
1302 remotephases = listkeys(pushop.remote, b'phases')
1304 if (
1303 if (
1305 pushop.ui.configbool(b'ui', b'_usedassubrepo')
1304 pushop.ui.configbool(b'ui', b'_usedassubrepo')
1306 and remotephases # server supports phases
1305 and remotephases # server supports phases
1307 and pushop.cgresult is None # nothing was pushed
1306 and pushop.cgresult is None # nothing was pushed
1308 and remotephases.get(b'publishing', False)
1307 and remotephases.get(b'publishing', False)
1309 ):
1308 ):
1310 # When:
1309 # When:
1311 # - this is a subrepo push
1310 # - this is a subrepo push
1312 # - and remote support phase
1311 # - and remote support phase
1313 # - and no changeset was pushed
1312 # - and no changeset was pushed
1314 # - and remote is publishing
1313 # - and remote is publishing
1315 # We may be in issue 3871 case!
1314 # We may be in issue 3871 case!
1316 # We drop the possible phase synchronisation done by
1315 # We drop the possible phase synchronisation done by
1317 # courtesy to publish changesets possibly locally draft
1316 # courtesy to publish changesets possibly locally draft
1318 # on the remote.
1317 # on the remote.
1319 remotephases = {b'publishing': b'True'}
1318 remotephases = {b'publishing': b'True'}
1320 if not remotephases: # old server or public only reply from non-publishing
1319 if not remotephases: # old server or public only reply from non-publishing
1321 _localphasemove(pushop, cheads)
1320 _localphasemove(pushop, cheads)
1322 # don't push any phase data as there is nothing to push
1321 # don't push any phase data as there is nothing to push
1323 else:
1322 else:
1324 unfi = pushop.repo.unfiltered()
1323 unfi = pushop.repo.unfiltered()
1325 to_rev = unfi.changelog.index.rev
1324 to_rev = unfi.changelog.index.rev
1326 to_node = unfi.changelog.node
1325 to_node = unfi.changelog.node
1327 cheads_revs = [to_rev(n) for n in cheads]
1326 cheads_revs = [to_rev(n) for n in cheads]
1328 pheads_revs, _dr = phases.analyze_remote_phases(
1327 pheads_revs, _dr = phases.analyze_remote_phases(
1329 pushop.repo,
1328 pushop.repo,
1330 cheads_revs,
1329 cheads_revs,
1331 remotephases,
1330 remotephases,
1332 )
1331 )
1333 pheads = [to_node(r) for r in pheads_revs]
1332 pheads = [to_node(r) for r in pheads_revs]
1334 ### Apply remote phase on local
1333 ### Apply remote phase on local
1335 if remotephases.get(b'publishing', False):
1334 if remotephases.get(b'publishing', False):
1336 _localphasemove(pushop, cheads)
1335 _localphasemove(pushop, cheads)
1337 else: # publish = False
1336 else: # publish = False
1338 _localphasemove(pushop, pheads)
1337 _localphasemove(pushop, pheads)
1339 _localphasemove(pushop, cheads, phases.draft)
1338 _localphasemove(pushop, cheads, phases.draft)
1340 ### Apply local phase on remote
1339 ### Apply local phase on remote
1341
1340
1342 if pushop.cgresult:
1341 if pushop.cgresult:
1343 if b'phases' in pushop.stepsdone:
1342 if b'phases' in pushop.stepsdone:
1344 # phases already pushed though bundle2
1343 # phases already pushed though bundle2
1345 return
1344 return
1346 outdated = pushop.outdatedphases
1345 outdated = pushop.outdatedphases
1347 else:
1346 else:
1348 outdated = pushop.fallbackoutdatedphases
1347 outdated = pushop.fallbackoutdatedphases
1349
1348
1350 pushop.stepsdone.add(b'phases')
1349 pushop.stepsdone.add(b'phases')
1351
1350
1352 # filter heads already turned public by the push
1351 # filter heads already turned public by the push
1353 outdated = [c for c in outdated if c.node() not in pheads]
1352 outdated = [c for c in outdated if c.node() not in pheads]
1354 # fallback to independent pushkey command
1353 # fallback to independent pushkey command
1355 for newremotehead in outdated:
1354 for newremotehead in outdated:
1356 with pushop.remote.commandexecutor() as e:
1355 with pushop.remote.commandexecutor() as e:
1357 r = e.callcommand(
1356 r = e.callcommand(
1358 b'pushkey',
1357 b'pushkey',
1359 {
1358 {
1360 b'namespace': b'phases',
1359 b'namespace': b'phases',
1361 b'key': newremotehead.hex(),
1360 b'key': newremotehead.hex(),
1362 b'old': b'%d' % phases.draft,
1361 b'old': b'%d' % phases.draft,
1363 b'new': b'%d' % phases.public,
1362 b'new': b'%d' % phases.public,
1364 },
1363 },
1365 ).result()
1364 ).result()
1366
1365
1367 if not r:
1366 if not r:
1368 pushop.ui.warn(
1367 pushop.ui.warn(
1369 _(b'updating %s to public failed!\n') % newremotehead
1368 _(b'updating %s to public failed!\n') % newremotehead
1370 )
1369 )
1371
1370
1372
1371
1373 def _localphasemove(pushop, nodes, phase=phases.public):
1372 def _localphasemove(pushop, nodes, phase=phases.public):
1374 """move <nodes> to <phase> in the local source repo"""
1373 """move <nodes> to <phase> in the local source repo"""
1375 if pushop.trmanager:
1374 if pushop.trmanager:
1376 phases.advanceboundary(
1375 phases.advanceboundary(
1377 pushop.repo, pushop.trmanager.transaction(), phase, nodes
1376 pushop.repo, pushop.trmanager.transaction(), phase, nodes
1378 )
1377 )
1379 else:
1378 else:
1380 # repo is not locked, do not change any phases!
1379 # repo is not locked, do not change any phases!
1381 # Informs the user that phases should have been moved when
1380 # Informs the user that phases should have been moved when
1382 # applicable.
1381 # applicable.
1383 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1382 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1384 phasestr = phases.phasenames[phase]
1383 phasestr = phases.phasenames[phase]
1385 if actualmoves:
1384 if actualmoves:
1386 pushop.ui.status(
1385 pushop.ui.status(
1387 _(
1386 _(
1388 b'cannot lock source repo, skipping '
1387 b'cannot lock source repo, skipping '
1389 b'local %s phase update\n'
1388 b'local %s phase update\n'
1390 )
1389 )
1391 % phasestr
1390 % phasestr
1392 )
1391 )
1393
1392
1394
1393
1395 def _pushobsolete(pushop):
1394 def _pushobsolete(pushop):
1396 """utility function to push obsolete markers to a remote"""
1395 """utility function to push obsolete markers to a remote"""
1397 if b'obsmarkers' in pushop.stepsdone:
1396 if b'obsmarkers' in pushop.stepsdone:
1398 return
1397 return
1399 repo = pushop.repo
1398 repo = pushop.repo
1400 remote = pushop.remote
1399 remote = pushop.remote
1401 pushop.stepsdone.add(b'obsmarkers')
1400 pushop.stepsdone.add(b'obsmarkers')
1402 if pushop.outobsmarkers:
1401 if pushop.outobsmarkers:
1403 pushop.ui.debug(b'try to push obsolete markers to remote\n')
1402 pushop.ui.debug(b'try to push obsolete markers to remote\n')
1404 rslts = []
1403 rslts = []
1405 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
1404 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
1406 remotedata = obsolete._pushkeyescape(markers)
1405 remotedata = obsolete._pushkeyescape(markers)
1407 for key in sorted(remotedata, reverse=True):
1406 for key in sorted(remotedata, reverse=True):
1408 # reverse sort to ensure we end with dump0
1407 # reverse sort to ensure we end with dump0
1409 data = remotedata[key]
1408 data = remotedata[key]
1410 rslts.append(remote.pushkey(b'obsolete', key, b'', data))
1409 rslts.append(remote.pushkey(b'obsolete', key, b'', data))
1411 if [r for r in rslts if not r]:
1410 if [r for r in rslts if not r]:
1412 msg = _(b'failed to push some obsolete markers!\n')
1411 msg = _(b'failed to push some obsolete markers!\n')
1413 repo.ui.warn(msg)
1412 repo.ui.warn(msg)
1414
1413
1415
1414
1416 def _pushbookmark(pushop):
1415 def _pushbookmark(pushop):
1417 """Update bookmark position on remote"""
1416 """Update bookmark position on remote"""
1418 if pushop.cgresult == 0 or b'bookmarks' in pushop.stepsdone:
1417 if pushop.cgresult == 0 or b'bookmarks' in pushop.stepsdone:
1419 return
1418 return
1420 pushop.stepsdone.add(b'bookmarks')
1419 pushop.stepsdone.add(b'bookmarks')
1421 ui = pushop.ui
1420 ui = pushop.ui
1422 remote = pushop.remote
1421 remote = pushop.remote
1423
1422
1424 for b, old, new in pushop.outbookmarks:
1423 for b, old, new in pushop.outbookmarks:
1425 action = b'update'
1424 action = b'update'
1426 if not old:
1425 if not old:
1427 action = b'export'
1426 action = b'export'
1428 elif not new:
1427 elif not new:
1429 action = b'delete'
1428 action = b'delete'
1430
1429
1431 with remote.commandexecutor() as e:
1430 with remote.commandexecutor() as e:
1432 r = e.callcommand(
1431 r = e.callcommand(
1433 b'pushkey',
1432 b'pushkey',
1434 {
1433 {
1435 b'namespace': b'bookmarks',
1434 b'namespace': b'bookmarks',
1436 b'key': b,
1435 b'key': b,
1437 b'old': hex(old),
1436 b'old': hex(old),
1438 b'new': hex(new),
1437 b'new': hex(new),
1439 },
1438 },
1440 ).result()
1439 ).result()
1441
1440
1442 if r:
1441 if r:
1443 ui.status(bookmsgmap[action][0] % b)
1442 ui.status(bookmsgmap[action][0] % b)
1444 else:
1443 else:
1445 ui.warn(bookmsgmap[action][1] % b)
1444 ui.warn(bookmsgmap[action][1] % b)
1446 # discovery can have set the value form invalid entry
1445 # discovery can have set the value form invalid entry
1447 if pushop.bkresult is not None:
1446 if pushop.bkresult is not None:
1448 pushop.bkresult = 1
1447 pushop.bkresult = 1
1449
1448
1450
1449
1451 class pulloperation:
1450 class pulloperation:
1452 """A object that represent a single pull operation
1451 """A object that represent a single pull operation
1453
1452
1454 It purpose is to carry pull related state and very common operation.
1453 It purpose is to carry pull related state and very common operation.
1455
1454
1456 A new should be created at the beginning of each pull and discarded
1455 A new should be created at the beginning of each pull and discarded
1457 afterward.
1456 afterward.
1458 """
1457 """
1459
1458
1460 def __init__(
1459 def __init__(
1461 self,
1460 self,
1462 repo,
1461 repo,
1463 remote,
1462 remote,
1464 heads=None,
1463 heads=None,
1465 force=False,
1464 force=False,
1466 bookmarks=(),
1465 bookmarks=(),
1467 remotebookmarks=None,
1466 remotebookmarks=None,
1468 streamclonerequested=None,
1467 streamclonerequested=None,
1469 includepats=None,
1468 includepats=None,
1470 excludepats=None,
1469 excludepats=None,
1471 depth=None,
1470 depth=None,
1472 path=None,
1471 path=None,
1473 ):
1472 ):
1474 # repo we pull into
1473 # repo we pull into
1475 self.repo = repo
1474 self.repo = repo
1476 # repo we pull from
1475 # repo we pull from
1477 self.remote = remote
1476 self.remote = remote
1478 # path object used to build this remote
1477 # path object used to build this remote
1479 #
1478 #
1480 # Ideally, the remote peer would carry that directly.
1479 # Ideally, the remote peer would carry that directly.
1481 self.remote_path = path
1480 self.remote_path = path
1482 # revision we try to pull (None is "all")
1481 # revision we try to pull (None is "all")
1483 self.heads = heads
1482 self.heads = heads
1484 # bookmark pulled explicitly
1483 # bookmark pulled explicitly
1485 self.explicitbookmarks = [
1484 self.explicitbookmarks = [
1486 repo._bookmarks.expandname(bookmark) for bookmark in bookmarks
1485 repo._bookmarks.expandname(bookmark) for bookmark in bookmarks
1487 ]
1486 ]
1488 # do we force pull?
1487 # do we force pull?
1489 self.force = force
1488 self.force = force
1490 # whether a streaming clone was requested
1489 # whether a streaming clone was requested
1491 self.streamclonerequested = streamclonerequested
1490 self.streamclonerequested = streamclonerequested
1492 # transaction manager
1491 # transaction manager
1493 self.trmanager = None
1492 self.trmanager = None
1494 # set of common changeset between local and remote before pull
1493 # set of common changeset between local and remote before pull
1495 self.common = None
1494 self.common = None
1496 # set of pulled head
1495 # set of pulled head
1497 self.rheads = None
1496 self.rheads = None
1498 # list of missing changeset to fetch remotely
1497 # list of missing changeset to fetch remotely
1499 self.fetch = None
1498 self.fetch = None
1500 # remote bookmarks data
1499 # remote bookmarks data
1501 self.remotebookmarks = remotebookmarks
1500 self.remotebookmarks = remotebookmarks
1502 # result of changegroup pulling (used as return code by pull)
1501 # result of changegroup pulling (used as return code by pull)
1503 self.cgresult = None
1502 self.cgresult = None
1504 # list of step already done
1503 # list of step already done
1505 self.stepsdone = set()
1504 self.stepsdone = set()
1506 # Whether we attempted a clone from pre-generated bundles.
1505 # Whether we attempted a clone from pre-generated bundles.
1507 self.clonebundleattempted = False
1506 self.clonebundleattempted = False
1508 # Set of file patterns to include.
1507 # Set of file patterns to include.
1509 self.includepats = includepats
1508 self.includepats = includepats
1510 # Set of file patterns to exclude.
1509 # Set of file patterns to exclude.
1511 self.excludepats = excludepats
1510 self.excludepats = excludepats
1512 # Number of ancestor changesets to pull from each pulled head.
1511 # Number of ancestor changesets to pull from each pulled head.
1513 self.depth = depth
1512 self.depth = depth
1514
1513
1515 @util.propertycache
1514 @util.propertycache
1516 def pulledsubset(self):
1515 def pulledsubset(self):
1517 """heads of the set of changeset target by the pull"""
1516 """heads of the set of changeset target by the pull"""
1518 # compute target subset
1517 # compute target subset
1519 if self.heads is None:
1518 if self.heads is None:
1520 # We pulled every thing possible
1519 # We pulled every thing possible
1521 # sync on everything common
1520 # sync on everything common
1522 c = set(self.common)
1521 c = set(self.common)
1523 ret = list(self.common)
1522 ret = list(self.common)
1524 for n in self.rheads:
1523 for n in self.rheads:
1525 if n not in c:
1524 if n not in c:
1526 ret.append(n)
1525 ret.append(n)
1527 return ret
1526 return ret
1528 else:
1527 else:
1529 # We pulled a specific subset
1528 # We pulled a specific subset
1530 # sync on this subset
1529 # sync on this subset
1531 return self.heads
1530 return self.heads
1532
1531
1533 @util.propertycache
1532 @util.propertycache
1534 def canusebundle2(self):
1533 def canusebundle2(self):
1535 return not _forcebundle1(self)
1534 return not _forcebundle1(self)
1536
1535
1537 @util.propertycache
1536 @util.propertycache
1538 def remotebundle2caps(self):
1537 def remotebundle2caps(self):
1539 return bundle2.bundle2caps(self.remote)
1538 return bundle2.bundle2caps(self.remote)
1540
1539
1541 def gettransaction(self):
1540 def gettransaction(self):
1542 # deprecated; talk to trmanager directly
1541 # deprecated; talk to trmanager directly
1543 return self.trmanager.transaction()
1542 return self.trmanager.transaction()
1544
1543
1545
1544
1546 class transactionmanager(util.transactional):
1545 class transactionmanager(util.transactional):
1547 """An object to manage the life cycle of a transaction
1546 """An object to manage the life cycle of a transaction
1548
1547
1549 It creates the transaction on demand and calls the appropriate hooks when
1548 It creates the transaction on demand and calls the appropriate hooks when
1550 closing the transaction."""
1549 closing the transaction."""
1551
1550
1552 def __init__(self, repo, source, url):
1551 def __init__(self, repo, source, url):
1553 self.repo = repo
1552 self.repo = repo
1554 self.source = source
1553 self.source = source
1555 self.url = url
1554 self.url = url
1556 self._tr = None
1555 self._tr = None
1557
1556
1558 def transaction(self):
1557 def transaction(self):
1559 """Return an open transaction object, constructing if necessary"""
1558 """Return an open transaction object, constructing if necessary"""
1560 if not self._tr:
1559 if not self._tr:
1561 trname = b'%s\n%s' % (self.source, urlutil.hidepassword(self.url))
1560 trname = b'%s\n%s' % (self.source, urlutil.hidepassword(self.url))
1562 self._tr = self.repo.transaction(trname)
1561 self._tr = self.repo.transaction(trname)
1563 self._tr.hookargs[b'source'] = self.source
1562 self._tr.hookargs[b'source'] = self.source
1564 self._tr.hookargs[b'url'] = self.url
1563 self._tr.hookargs[b'url'] = self.url
1565 return self._tr
1564 return self._tr
1566
1565
1567 def close(self):
1566 def close(self):
1568 """close transaction if created"""
1567 """close transaction if created"""
1569 if self._tr is not None:
1568 if self._tr is not None:
1570 self._tr.close()
1569 self._tr.close()
1571
1570
1572 def release(self):
1571 def release(self):
1573 """release transaction if created"""
1572 """release transaction if created"""
1574 if self._tr is not None:
1573 if self._tr is not None:
1575 self._tr.release()
1574 self._tr.release()
1576
1575
1577
1576
1578 def listkeys(remote, namespace):
1577 def listkeys(remote, namespace):
1579 with remote.commandexecutor() as e:
1578 with remote.commandexecutor() as e:
1580 return e.callcommand(b'listkeys', {b'namespace': namespace}).result()
1579 return e.callcommand(b'listkeys', {b'namespace': namespace}).result()
1581
1580
1582
1581
1583 def _fullpullbundle2(repo, pullop):
1582 def _fullpullbundle2(repo, pullop):
1584 # The server may send a partial reply, i.e. when inlining
1583 # The server may send a partial reply, i.e. when inlining
1585 # pre-computed bundles. In that case, update the common
1584 # pre-computed bundles. In that case, update the common
1586 # set based on the results and pull another bundle.
1585 # set based on the results and pull another bundle.
1587 #
1586 #
1588 # There are two indicators that the process is finished:
1587 # There are two indicators that the process is finished:
1589 # - no changeset has been added, or
1588 # - no changeset has been added, or
1590 # - all remote heads are known locally.
1589 # - all remote heads are known locally.
1591 # The head check must use the unfiltered view as obsoletion
1590 # The head check must use the unfiltered view as obsoletion
1592 # markers can hide heads.
1591 # markers can hide heads.
1593 unfi = repo.unfiltered()
1592 unfi = repo.unfiltered()
1594 unficl = unfi.changelog
1593 unficl = unfi.changelog
1595
1594
1596 def headsofdiff(h1, h2):
1595 def headsofdiff(h1, h2):
1597 """Returns heads(h1 % h2)"""
1596 """Returns heads(h1 % h2)"""
1598 res = unfi.set(b'heads(%ln %% %ln)', h1, h2)
1597 res = unfi.set(b'heads(%ln %% %ln)', h1, h2)
1599 return {ctx.node() for ctx in res}
1598 return {ctx.node() for ctx in res}
1600
1599
1601 def headsofunion(h1, h2):
1600 def headsofunion(h1, h2):
1602 """Returns heads((h1 + h2) - null)"""
1601 """Returns heads((h1 + h2) - null)"""
1603 res = unfi.set(b'heads((%ln + %ln - null))', h1, h2)
1602 res = unfi.set(b'heads((%ln + %ln - null))', h1, h2)
1604 return {ctx.node() for ctx in res}
1603 return {ctx.node() for ctx in res}
1605
1604
1606 while True:
1605 while True:
1607 old_heads = unficl.heads()
1606 old_heads = unficl.heads()
1608 clstart = len(unficl)
1607 clstart = len(unficl)
1609 _pullbundle2(pullop)
1608 _pullbundle2(pullop)
1610 if requirements.NARROW_REQUIREMENT in repo.requirements:
1609 if requirements.NARROW_REQUIREMENT in repo.requirements:
1611 # XXX narrow clones filter the heads on the server side during
1610 # XXX narrow clones filter the heads on the server side during
1612 # XXX getbundle and result in partial replies as well.
1611 # XXX getbundle and result in partial replies as well.
1613 # XXX Disable pull bundles in this case as band aid to avoid
1612 # XXX Disable pull bundles in this case as band aid to avoid
1614 # XXX extra round trips.
1613 # XXX extra round trips.
1615 break
1614 break
1616 if clstart == len(unficl):
1615 if clstart == len(unficl):
1617 break
1616 break
1618 if all(unficl.hasnode(n) for n in pullop.rheads):
1617 if all(unficl.hasnode(n) for n in pullop.rheads):
1619 break
1618 break
1620 new_heads = headsofdiff(unficl.heads(), old_heads)
1619 new_heads = headsofdiff(unficl.heads(), old_heads)
1621 pullop.common = headsofunion(new_heads, pullop.common)
1620 pullop.common = headsofunion(new_heads, pullop.common)
1622 pullop.rheads = set(pullop.rheads) - pullop.common
1621 pullop.rheads = set(pullop.rheads) - pullop.common
1623
1622
1624
1623
1625 def add_confirm_callback(repo, pullop):
1624 def add_confirm_callback(repo, pullop):
1626 """adds a finalize callback to transaction which can be used to show stats
1625 """adds a finalize callback to transaction which can be used to show stats
1627 to user and confirm the pull before committing transaction"""
1626 to user and confirm the pull before committing transaction"""
1628
1627
1629 tr = pullop.trmanager.transaction()
1628 tr = pullop.trmanager.transaction()
1630 scmutil.registersummarycallback(
1629 scmutil.registersummarycallback(
1631 repo, tr, txnname=b'pull', as_validator=True
1630 repo, tr, txnname=b'pull', as_validator=True
1632 )
1631 )
1633 reporef = weakref.ref(repo.unfiltered())
1632 reporef = weakref.ref(repo.unfiltered())
1634
1633
1635 def prompt(tr):
1634 def prompt(tr):
1636 repo = reporef()
1635 repo = reporef()
1637 cm = _(b'accept incoming changes (yn)?$$ &Yes $$ &No')
1636 cm = _(b'accept incoming changes (yn)?$$ &Yes $$ &No')
1638 if repo.ui.promptchoice(cm):
1637 if repo.ui.promptchoice(cm):
1639 raise error.Abort(b"user aborted")
1638 raise error.Abort(b"user aborted")
1640
1639
1641 tr.addvalidator(b'900-pull-prompt', prompt)
1640 tr.addvalidator(b'900-pull-prompt', prompt)
1642
1641
1643
1642
1644 def pull(
1643 def pull(
1645 repo,
1644 repo,
1646 remote,
1645 remote,
1647 path=None,
1646 path=None,
1648 heads=None,
1647 heads=None,
1649 force=False,
1648 force=False,
1650 bookmarks=(),
1649 bookmarks=(),
1651 opargs=None,
1650 opargs=None,
1652 streamclonerequested=None,
1651 streamclonerequested=None,
1653 includepats=None,
1652 includepats=None,
1654 excludepats=None,
1653 excludepats=None,
1655 depth=None,
1654 depth=None,
1656 confirm=None,
1655 confirm=None,
1657 ):
1656 ):
1658 """Fetch repository data from a remote.
1657 """Fetch repository data from a remote.
1659
1658
1660 This is the main function used to retrieve data from a remote repository.
1659 This is the main function used to retrieve data from a remote repository.
1661
1660
1662 ``repo`` is the local repository to clone into.
1661 ``repo`` is the local repository to clone into.
1663 ``remote`` is a peer instance.
1662 ``remote`` is a peer instance.
1664 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1663 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1665 default) means to pull everything from the remote.
1664 default) means to pull everything from the remote.
1666 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1665 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1667 default, all remote bookmarks are pulled.
1666 default, all remote bookmarks are pulled.
1668 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1667 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1669 initialization.
1668 initialization.
1670 ``streamclonerequested`` is a boolean indicating whether a "streaming
1669 ``streamclonerequested`` is a boolean indicating whether a "streaming
1671 clone" is requested. A "streaming clone" is essentially a raw file copy
1670 clone" is requested. A "streaming clone" is essentially a raw file copy
1672 of revlogs from the server. This only works when the local repository is
1671 of revlogs from the server. This only works when the local repository is
1673 empty. The default value of ``None`` means to respect the server
1672 empty. The default value of ``None`` means to respect the server
1674 configuration for preferring stream clones.
1673 configuration for preferring stream clones.
1675 ``includepats`` and ``excludepats`` define explicit file patterns to
1674 ``includepats`` and ``excludepats`` define explicit file patterns to
1676 include and exclude in storage, respectively. If not defined, narrow
1675 include and exclude in storage, respectively. If not defined, narrow
1677 patterns from the repo instance are used, if available.
1676 patterns from the repo instance are used, if available.
1678 ``depth`` is an integer indicating the DAG depth of history we're
1677 ``depth`` is an integer indicating the DAG depth of history we're
1679 interested in. If defined, for each revision specified in ``heads``, we
1678 interested in. If defined, for each revision specified in ``heads``, we
1680 will fetch up to this many of its ancestors and data associated with them.
1679 will fetch up to this many of its ancestors and data associated with them.
1681 ``confirm`` is a boolean indicating whether the pull should be confirmed
1680 ``confirm`` is a boolean indicating whether the pull should be confirmed
1682 before committing the transaction. This overrides HGPLAIN.
1681 before committing the transaction. This overrides HGPLAIN.
1683
1682
1684 Returns the ``pulloperation`` created for this pull.
1683 Returns the ``pulloperation`` created for this pull.
1685 """
1684 """
1686 if opargs is None:
1685 if opargs is None:
1687 opargs = {}
1686 opargs = {}
1688
1687
1689 # We allow the narrow patterns to be passed in explicitly to provide more
1688 # We allow the narrow patterns to be passed in explicitly to provide more
1690 # flexibility for API consumers.
1689 # flexibility for API consumers.
1691 if includepats is not None or excludepats is not None:
1690 if includepats is not None or excludepats is not None:
1692 includepats = includepats or set()
1691 includepats = includepats or set()
1693 excludepats = excludepats or set()
1692 excludepats = excludepats or set()
1694 else:
1693 else:
1695 includepats, excludepats = repo.narrowpats
1694 includepats, excludepats = repo.narrowpats
1696
1695
1697 narrowspec.validatepatterns(includepats)
1696 narrowspec.validatepatterns(includepats)
1698 narrowspec.validatepatterns(excludepats)
1697 narrowspec.validatepatterns(excludepats)
1699
1698
1700 pullop = pulloperation(
1699 pullop = pulloperation(
1701 repo,
1700 repo,
1702 remote,
1701 remote,
1703 path=path,
1702 path=path,
1704 heads=heads,
1703 heads=heads,
1705 force=force,
1704 force=force,
1706 bookmarks=bookmarks,
1705 bookmarks=bookmarks,
1707 streamclonerequested=streamclonerequested,
1706 streamclonerequested=streamclonerequested,
1708 includepats=includepats,
1707 includepats=includepats,
1709 excludepats=excludepats,
1708 excludepats=excludepats,
1710 depth=depth,
1709 depth=depth,
1711 **pycompat.strkwargs(opargs)
1710 **pycompat.strkwargs(opargs)
1712 )
1711 )
1713
1712
1714 peerlocal = pullop.remote.local()
1713 peerlocal = pullop.remote.local()
1715 if peerlocal:
1714 if peerlocal:
1716 missing = set(peerlocal.requirements) - pullop.repo.supported
1715 missing = set(peerlocal.requirements) - pullop.repo.supported
1717 if missing:
1716 if missing:
1718 msg = _(
1717 msg = _(
1719 b"required features are not"
1718 b"required features are not"
1720 b" supported in the destination:"
1719 b" supported in the destination:"
1721 b" %s"
1720 b" %s"
1722 ) % (b', '.join(sorted(missing)))
1721 ) % (b', '.join(sorted(missing)))
1723 raise error.Abort(msg)
1722 raise error.Abort(msg)
1724
1723
1725 for category in repo._wanted_sidedata:
1724 for category in repo._wanted_sidedata:
1726 # Check that a computer is registered for that category for at least
1725 # Check that a computer is registered for that category for at least
1727 # one revlog kind.
1726 # one revlog kind.
1728 for kind, computers in repo._sidedata_computers.items():
1727 for kind, computers in repo._sidedata_computers.items():
1729 if computers.get(category):
1728 if computers.get(category):
1730 break
1729 break
1731 else:
1730 else:
1732 # This should never happen since repos are supposed to be able to
1731 # This should never happen since repos are supposed to be able to
1733 # generate the sidedata they require.
1732 # generate the sidedata they require.
1734 raise error.ProgrammingError(
1733 raise error.ProgrammingError(
1735 _(
1734 _(
1736 b'sidedata category requested by local side without local'
1735 b'sidedata category requested by local side without local'
1737 b"support: '%s'"
1736 b"support: '%s'"
1738 )
1737 )
1739 % pycompat.bytestr(category)
1738 % pycompat.bytestr(category)
1740 )
1739 )
1741
1740
1742 pullop.trmanager = transactionmanager(repo, b'pull', remote.url())
1741 pullop.trmanager = transactionmanager(repo, b'pull', remote.url())
1743 wlock = util.nullcontextmanager()
1742 wlock = util.nullcontextmanager()
1744 if not bookmod.bookmarksinstore(repo):
1743 if not bookmod.bookmarksinstore(repo):
1745 wlock = repo.wlock()
1744 wlock = repo.wlock()
1746 with wlock, repo.lock(), pullop.trmanager:
1745 with wlock, repo.lock(), pullop.trmanager:
1747 if confirm or (
1746 if confirm or (
1748 repo.ui.configbool(b"pull", b"confirm") and not repo.ui.plain()
1747 repo.ui.configbool(b"pull", b"confirm") and not repo.ui.plain()
1749 ):
1748 ):
1750 add_confirm_callback(repo, pullop)
1749 add_confirm_callback(repo, pullop)
1751
1750
1752 # This should ideally be in _pullbundle2(). However, it needs to run
1751 # This should ideally be in _pullbundle2(). However, it needs to run
1753 # before discovery to avoid extra work.
1752 # before discovery to avoid extra work.
1754 _maybeapplyclonebundle(pullop)
1753 _maybeapplyclonebundle(pullop)
1755 streamclone.maybeperformlegacystreamclone(pullop)
1754 streamclone.maybeperformlegacystreamclone(pullop)
1756 _pulldiscovery(pullop)
1755 _pulldiscovery(pullop)
1757 if pullop.canusebundle2:
1756 if pullop.canusebundle2:
1758 _fullpullbundle2(repo, pullop)
1757 _fullpullbundle2(repo, pullop)
1759 _pullchangeset(pullop)
1758 _pullchangeset(pullop)
1760 _pullphase(pullop)
1759 _pullphase(pullop)
1761 _pullbookmarks(pullop)
1760 _pullbookmarks(pullop)
1762 _pullobsolete(pullop)
1761 _pullobsolete(pullop)
1763
1762
1764 # storing remotenames
1763 # storing remotenames
1765 if repo.ui.configbool(b'experimental', b'remotenames'):
1764 if repo.ui.configbool(b'experimental', b'remotenames'):
1766 logexchange.pullremotenames(repo, remote)
1765 logexchange.pullremotenames(repo, remote)
1767
1766
1768 return pullop
1767 return pullop
1769
1768
1770
1769
1771 # list of steps to perform discovery before pull
1770 # list of steps to perform discovery before pull
1772 pulldiscoveryorder = []
1771 pulldiscoveryorder = []
1773
1772
1774 # Mapping between step name and function
1773 # Mapping between step name and function
1775 #
1774 #
1776 # This exists to help extensions wrap steps if necessary
1775 # This exists to help extensions wrap steps if necessary
1777 pulldiscoverymapping = {}
1776 pulldiscoverymapping = {}
1778
1777
1779
1778
1780 def pulldiscovery(stepname):
1779 def pulldiscovery(stepname):
1781 """decorator for function performing discovery before pull
1780 """decorator for function performing discovery before pull
1782
1781
1783 The function is added to the step -> function mapping and appended to the
1782 The function is added to the step -> function mapping and appended to the
1784 list of steps. Beware that decorated function will be added in order (this
1783 list of steps. Beware that decorated function will be added in order (this
1785 may matter).
1784 may matter).
1786
1785
1787 You can only use this decorator for a new step, if you want to wrap a step
1786 You can only use this decorator for a new step, if you want to wrap a step
1788 from an extension, change the pulldiscovery dictionary directly."""
1787 from an extension, change the pulldiscovery dictionary directly."""
1789
1788
1790 def dec(func):
1789 def dec(func):
1791 assert stepname not in pulldiscoverymapping
1790 assert stepname not in pulldiscoverymapping
1792 pulldiscoverymapping[stepname] = func
1791 pulldiscoverymapping[stepname] = func
1793 pulldiscoveryorder.append(stepname)
1792 pulldiscoveryorder.append(stepname)
1794 return func
1793 return func
1795
1794
1796 return dec
1795 return dec
1797
1796
1798
1797
1799 def _pulldiscovery(pullop):
1798 def _pulldiscovery(pullop):
1800 """Run all discovery steps"""
1799 """Run all discovery steps"""
1801 for stepname in pulldiscoveryorder:
1800 for stepname in pulldiscoveryorder:
1802 step = pulldiscoverymapping[stepname]
1801 step = pulldiscoverymapping[stepname]
1803 step(pullop)
1802 step(pullop)
1804
1803
1805
1804
1806 @pulldiscovery(b'b1:bookmarks')
1805 @pulldiscovery(b'b1:bookmarks')
1807 def _pullbookmarkbundle1(pullop):
1806 def _pullbookmarkbundle1(pullop):
1808 """fetch bookmark data in bundle1 case
1807 """fetch bookmark data in bundle1 case
1809
1808
1810 If not using bundle2, we have to fetch bookmarks before changeset
1809 If not using bundle2, we have to fetch bookmarks before changeset
1811 discovery to reduce the chance and impact of race conditions."""
1810 discovery to reduce the chance and impact of race conditions."""
1812 if pullop.remotebookmarks is not None:
1811 if pullop.remotebookmarks is not None:
1813 return
1812 return
1814 if pullop.canusebundle2 and b'listkeys' in pullop.remotebundle2caps:
1813 if pullop.canusebundle2 and b'listkeys' in pullop.remotebundle2caps:
1815 # all known bundle2 servers now support listkeys, but lets be nice with
1814 # all known bundle2 servers now support listkeys, but lets be nice with
1816 # new implementation.
1815 # new implementation.
1817 return
1816 return
1818 books = listkeys(pullop.remote, b'bookmarks')
1817 books = listkeys(pullop.remote, b'bookmarks')
1819 pullop.remotebookmarks = bookmod.unhexlifybookmarks(books)
1818 pullop.remotebookmarks = bookmod.unhexlifybookmarks(books)
1820
1819
1821
1820
1822 @pulldiscovery(b'changegroup')
1821 @pulldiscovery(b'changegroup')
1823 def _pulldiscoverychangegroup(pullop):
1822 def _pulldiscoverychangegroup(pullop):
1824 """discovery phase for the pull
1823 """discovery phase for the pull
1825
1824
1826 Current handle changeset discovery only, will change handle all discovery
1825 Current handle changeset discovery only, will change handle all discovery
1827 at some point."""
1826 at some point."""
1828 tmp = discovery.findcommonincoming(
1827 tmp = discovery.findcommonincoming(
1829 pullop.repo, pullop.remote, heads=pullop.heads, force=pullop.force
1828 pullop.repo, pullop.remote, heads=pullop.heads, force=pullop.force
1830 )
1829 )
1831 common, fetch, rheads = tmp
1830 common, fetch, rheads = tmp
1832 has_node = pullop.repo.unfiltered().changelog.index.has_node
1831 has_node = pullop.repo.unfiltered().changelog.index.has_node
1833 if fetch and rheads:
1832 if fetch and rheads:
1834 # If a remote heads is filtered locally, put in back in common.
1833 # If a remote heads is filtered locally, put in back in common.
1835 #
1834 #
1836 # This is a hackish solution to catch most of "common but locally
1835 # This is a hackish solution to catch most of "common but locally
1837 # hidden situation". We do not performs discovery on unfiltered
1836 # hidden situation". We do not performs discovery on unfiltered
1838 # repository because it end up doing a pathological amount of round
1837 # repository because it end up doing a pathological amount of round
1839 # trip for w huge amount of changeset we do not care about.
1838 # trip for w huge amount of changeset we do not care about.
1840 #
1839 #
1841 # If a set of such "common but filtered" changeset exist on the server
1840 # If a set of such "common but filtered" changeset exist on the server
1842 # but are not including a remote heads, we'll not be able to detect it,
1841 # but are not including a remote heads, we'll not be able to detect it,
1843 scommon = set(common)
1842 scommon = set(common)
1844 for n in rheads:
1843 for n in rheads:
1845 if has_node(n):
1844 if has_node(n):
1846 if n not in scommon:
1845 if n not in scommon:
1847 common.append(n)
1846 common.append(n)
1848 if set(rheads).issubset(set(common)):
1847 if set(rheads).issubset(set(common)):
1849 fetch = []
1848 fetch = []
1850 pullop.common = common
1849 pullop.common = common
1851 pullop.fetch = fetch
1850 pullop.fetch = fetch
1852 pullop.rheads = rheads
1851 pullop.rheads = rheads
1853
1852
1854
1853
1855 def _pullbundle2(pullop):
1854 def _pullbundle2(pullop):
1856 """pull data using bundle2
1855 """pull data using bundle2
1857
1856
1858 For now, the only supported data are changegroup."""
1857 For now, the only supported data are changegroup."""
1859 kwargs = {b'bundlecaps': caps20to10(pullop.repo, role=b'client')}
1858 kwargs = {b'bundlecaps': caps20to10(pullop.repo, role=b'client')}
1860
1859
1861 # make ui easier to access
1860 # make ui easier to access
1862 ui = pullop.repo.ui
1861 ui = pullop.repo.ui
1863
1862
1864 # At the moment we don't do stream clones over bundle2. If that is
1863 # At the moment we don't do stream clones over bundle2. If that is
1865 # implemented then here's where the check for that will go.
1864 # implemented then here's where the check for that will go.
1866 streaming = streamclone.canperformstreamclone(pullop, bundle2=True)[0]
1865 streaming = streamclone.canperformstreamclone(pullop, bundle2=True)[0]
1867
1866
1868 # declare pull perimeters
1867 # declare pull perimeters
1869 kwargs[b'common'] = pullop.common
1868 kwargs[b'common'] = pullop.common
1870 kwargs[b'heads'] = pullop.heads or pullop.rheads
1869 kwargs[b'heads'] = pullop.heads or pullop.rheads
1871
1870
1872 # check server supports narrow and then adding includepats and excludepats
1871 # check server supports narrow and then adding includepats and excludepats
1873 servernarrow = pullop.remote.capable(wireprototypes.NARROWCAP)
1872 servernarrow = pullop.remote.capable(wireprototypes.NARROWCAP)
1874 if servernarrow and pullop.includepats:
1873 if servernarrow and pullop.includepats:
1875 kwargs[b'includepats'] = pullop.includepats
1874 kwargs[b'includepats'] = pullop.includepats
1876 if servernarrow and pullop.excludepats:
1875 if servernarrow and pullop.excludepats:
1877 kwargs[b'excludepats'] = pullop.excludepats
1876 kwargs[b'excludepats'] = pullop.excludepats
1878
1877
1879 if streaming:
1878 if streaming:
1880 kwargs[b'cg'] = False
1879 kwargs[b'cg'] = False
1881 kwargs[b'stream'] = True
1880 kwargs[b'stream'] = True
1882 pullop.stepsdone.add(b'changegroup')
1881 pullop.stepsdone.add(b'changegroup')
1883 pullop.stepsdone.add(b'phases')
1882 pullop.stepsdone.add(b'phases')
1884
1883
1885 else:
1884 else:
1886 # pulling changegroup
1885 # pulling changegroup
1887 pullop.stepsdone.add(b'changegroup')
1886 pullop.stepsdone.add(b'changegroup')
1888
1887
1889 kwargs[b'cg'] = pullop.fetch
1888 kwargs[b'cg'] = pullop.fetch
1890
1889
1891 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
1890 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
1892 hasbinaryphase = b'heads' in pullop.remotebundle2caps.get(b'phases', ())
1891 hasbinaryphase = b'heads' in pullop.remotebundle2caps.get(b'phases', ())
1893 if not legacyphase and hasbinaryphase:
1892 if not legacyphase and hasbinaryphase:
1894 kwargs[b'phases'] = True
1893 kwargs[b'phases'] = True
1895 pullop.stepsdone.add(b'phases')
1894 pullop.stepsdone.add(b'phases')
1896
1895
1897 if b'listkeys' in pullop.remotebundle2caps:
1896 if b'listkeys' in pullop.remotebundle2caps:
1898 if b'phases' not in pullop.stepsdone:
1897 if b'phases' not in pullop.stepsdone:
1899 kwargs[b'listkeys'] = [b'phases']
1898 kwargs[b'listkeys'] = [b'phases']
1900
1899
1901 bookmarksrequested = False
1900 bookmarksrequested = False
1902 legacybookmark = b'bookmarks' in ui.configlist(b'devel', b'legacy.exchange')
1901 legacybookmark = b'bookmarks' in ui.configlist(b'devel', b'legacy.exchange')
1903 hasbinarybook = b'bookmarks' in pullop.remotebundle2caps
1902 hasbinarybook = b'bookmarks' in pullop.remotebundle2caps
1904
1903
1905 if pullop.remotebookmarks is not None:
1904 if pullop.remotebookmarks is not None:
1906 pullop.stepsdone.add(b'request-bookmarks')
1905 pullop.stepsdone.add(b'request-bookmarks')
1907
1906
1908 if (
1907 if (
1909 b'request-bookmarks' not in pullop.stepsdone
1908 b'request-bookmarks' not in pullop.stepsdone
1910 and pullop.remotebookmarks is None
1909 and pullop.remotebookmarks is None
1911 and not legacybookmark
1910 and not legacybookmark
1912 and hasbinarybook
1911 and hasbinarybook
1913 ):
1912 ):
1914 kwargs[b'bookmarks'] = True
1913 kwargs[b'bookmarks'] = True
1915 bookmarksrequested = True
1914 bookmarksrequested = True
1916
1915
1917 if b'listkeys' in pullop.remotebundle2caps:
1916 if b'listkeys' in pullop.remotebundle2caps:
1918 if b'request-bookmarks' not in pullop.stepsdone:
1917 if b'request-bookmarks' not in pullop.stepsdone:
1919 # make sure to always includes bookmark data when migrating
1918 # make sure to always includes bookmark data when migrating
1920 # `hg incoming --bundle` to using this function.
1919 # `hg incoming --bundle` to using this function.
1921 pullop.stepsdone.add(b'request-bookmarks')
1920 pullop.stepsdone.add(b'request-bookmarks')
1922 kwargs.setdefault(b'listkeys', []).append(b'bookmarks')
1921 kwargs.setdefault(b'listkeys', []).append(b'bookmarks')
1923
1922
1924 # If this is a full pull / clone and the server supports the clone bundles
1923 # If this is a full pull / clone and the server supports the clone bundles
1925 # feature, tell the server whether we attempted a clone bundle. The
1924 # feature, tell the server whether we attempted a clone bundle. The
1926 # presence of this flag indicates the client supports clone bundles. This
1925 # presence of this flag indicates the client supports clone bundles. This
1927 # will enable the server to treat clients that support clone bundles
1926 # will enable the server to treat clients that support clone bundles
1928 # differently from those that don't.
1927 # differently from those that don't.
1929 if (
1928 if (
1930 pullop.remote.capable(b'clonebundles')
1929 pullop.remote.capable(b'clonebundles')
1931 and pullop.heads is None
1930 and pullop.heads is None
1932 and list(pullop.common) == [pullop.repo.nullid]
1931 and list(pullop.common) == [pullop.repo.nullid]
1933 ):
1932 ):
1934 kwargs[b'cbattempted'] = pullop.clonebundleattempted
1933 kwargs[b'cbattempted'] = pullop.clonebundleattempted
1935
1934
1936 if streaming:
1935 if streaming:
1937 pullop.repo.ui.status(_(b'streaming all changes\n'))
1936 pullop.repo.ui.status(_(b'streaming all changes\n'))
1938 elif not pullop.fetch:
1937 elif not pullop.fetch:
1939 pullop.repo.ui.status(_(b"no changes found\n"))
1938 pullop.repo.ui.status(_(b"no changes found\n"))
1940 pullop.cgresult = 0
1939 pullop.cgresult = 0
1941 else:
1940 else:
1942 if pullop.heads is None and list(pullop.common) == [pullop.repo.nullid]:
1941 if pullop.heads is None and list(pullop.common) == [pullop.repo.nullid]:
1943 pullop.repo.ui.status(_(b"requesting all changes\n"))
1942 pullop.repo.ui.status(_(b"requesting all changes\n"))
1944 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1943 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1945 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1944 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1946 if obsolete.commonversion(remoteversions) is not None:
1945 if obsolete.commonversion(remoteversions) is not None:
1947 kwargs[b'obsmarkers'] = True
1946 kwargs[b'obsmarkers'] = True
1948 pullop.stepsdone.add(b'obsmarkers')
1947 pullop.stepsdone.add(b'obsmarkers')
1949 _pullbundle2extraprepare(pullop, kwargs)
1948 _pullbundle2extraprepare(pullop, kwargs)
1950
1949
1951 remote_sidedata = bundle2.read_remote_wanted_sidedata(pullop.remote)
1950 remote_sidedata = bundle2.read_remote_wanted_sidedata(pullop.remote)
1952 if remote_sidedata:
1951 if remote_sidedata:
1953 kwargs[b'remote_sidedata'] = remote_sidedata
1952 kwargs[b'remote_sidedata'] = remote_sidedata
1954
1953
1955 with pullop.remote.commandexecutor() as e:
1954 with pullop.remote.commandexecutor() as e:
1956 args = dict(kwargs)
1955 args = dict(kwargs)
1957 args[b'source'] = b'pull'
1956 args[b'source'] = b'pull'
1958 bundle = e.callcommand(b'getbundle', args).result()
1957 bundle = e.callcommand(b'getbundle', args).result()
1959
1958
1960 try:
1959 try:
1961 op = bundle2.bundleoperation(
1960 op = bundle2.bundleoperation(
1962 pullop.repo,
1961 pullop.repo,
1963 pullop.gettransaction,
1962 pullop.gettransaction,
1964 source=b'pull',
1963 source=b'pull',
1965 remote=pullop.remote,
1964 remote=pullop.remote,
1966 )
1965 )
1967 op.modes[b'bookmarks'] = b'records'
1966 op.modes[b'bookmarks'] = b'records'
1968 bundle2.processbundle(
1967 bundle2.processbundle(
1969 pullop.repo,
1968 pullop.repo,
1970 bundle,
1969 bundle,
1971 op=op,
1970 op=op,
1972 remote=pullop.remote,
1971 remote=pullop.remote,
1973 )
1972 )
1974 except bundle2.AbortFromPart as exc:
1973 except bundle2.AbortFromPart as exc:
1975 pullop.repo.ui.error(_(b'remote: abort: %s\n') % exc)
1974 pullop.repo.ui.error(_(b'remote: abort: %s\n') % exc)
1976 raise error.RemoteError(_(b'pull failed on remote'), hint=exc.hint)
1975 raise error.RemoteError(_(b'pull failed on remote'), hint=exc.hint)
1977 except error.BundleValueError as exc:
1976 except error.BundleValueError as exc:
1978 raise error.RemoteError(_(b'missing support for %s') % exc)
1977 raise error.RemoteError(_(b'missing support for %s') % exc)
1979
1978
1980 if pullop.fetch:
1979 if pullop.fetch:
1981 pullop.cgresult = bundle2.combinechangegroupresults(op)
1980 pullop.cgresult = bundle2.combinechangegroupresults(op)
1982
1981
1983 # processing phases change
1982 # processing phases change
1984 for namespace, value in op.records[b'listkeys']:
1983 for namespace, value in op.records[b'listkeys']:
1985 if namespace == b'phases':
1984 if namespace == b'phases':
1986 _pullapplyphases(pullop, value)
1985 _pullapplyphases(pullop, value)
1987
1986
1988 # processing bookmark update
1987 # processing bookmark update
1989 if bookmarksrequested:
1988 if bookmarksrequested:
1990 books = {}
1989 books = {}
1991 for record in op.records[b'bookmarks']:
1990 for record in op.records[b'bookmarks']:
1992 books[record[b'bookmark']] = record[b"node"]
1991 books[record[b'bookmark']] = record[b"node"]
1993 pullop.remotebookmarks = books
1992 pullop.remotebookmarks = books
1994 else:
1993 else:
1995 for namespace, value in op.records[b'listkeys']:
1994 for namespace, value in op.records[b'listkeys']:
1996 if namespace == b'bookmarks':
1995 if namespace == b'bookmarks':
1997 pullop.remotebookmarks = bookmod.unhexlifybookmarks(value)
1996 pullop.remotebookmarks = bookmod.unhexlifybookmarks(value)
1998
1997
1999 # bookmark data were either already there or pulled in the bundle
1998 # bookmark data were either already there or pulled in the bundle
2000 if pullop.remotebookmarks is not None:
1999 if pullop.remotebookmarks is not None:
2001 _pullbookmarks(pullop)
2000 _pullbookmarks(pullop)
2002
2001
2003
2002
2004 def _pullbundle2extraprepare(pullop, kwargs):
2003 def _pullbundle2extraprepare(pullop, kwargs):
2005 """hook function so that extensions can extend the getbundle call"""
2004 """hook function so that extensions can extend the getbundle call"""
2006
2005
2007
2006
2008 def _pullchangeset(pullop):
2007 def _pullchangeset(pullop):
2009 """pull changeset from unbundle into the local repo"""
2008 """pull changeset from unbundle into the local repo"""
2010 # We delay the open of the transaction as late as possible so we
2009 # We delay the open of the transaction as late as possible so we
2011 # don't open transaction for nothing or you break future useful
2010 # don't open transaction for nothing or you break future useful
2012 # rollback call
2011 # rollback call
2013 if b'changegroup' in pullop.stepsdone:
2012 if b'changegroup' in pullop.stepsdone:
2014 return
2013 return
2015 pullop.stepsdone.add(b'changegroup')
2014 pullop.stepsdone.add(b'changegroup')
2016 if not pullop.fetch:
2015 if not pullop.fetch:
2017 pullop.repo.ui.status(_(b"no changes found\n"))
2016 pullop.repo.ui.status(_(b"no changes found\n"))
2018 pullop.cgresult = 0
2017 pullop.cgresult = 0
2019 return
2018 return
2020 tr = pullop.gettransaction()
2019 tr = pullop.gettransaction()
2021 if pullop.heads is None and list(pullop.common) == [pullop.repo.nullid]:
2020 if pullop.heads is None and list(pullop.common) == [pullop.repo.nullid]:
2022 pullop.repo.ui.status(_(b"requesting all changes\n"))
2021 pullop.repo.ui.status(_(b"requesting all changes\n"))
2023 elif pullop.heads is None and pullop.remote.capable(b'changegroupsubset'):
2022 elif pullop.heads is None and pullop.remote.capable(b'changegroupsubset'):
2024 # issue1320, avoid a race if remote changed after discovery
2023 # issue1320, avoid a race if remote changed after discovery
2025 pullop.heads = pullop.rheads
2024 pullop.heads = pullop.rheads
2026
2025
2027 if pullop.remote.capable(b'getbundle'):
2026 if pullop.remote.capable(b'getbundle'):
2028 # TODO: get bundlecaps from remote
2027 # TODO: get bundlecaps from remote
2029 cg = pullop.remote.getbundle(
2028 cg = pullop.remote.getbundle(
2030 b'pull', common=pullop.common, heads=pullop.heads or pullop.rheads
2029 b'pull', common=pullop.common, heads=pullop.heads or pullop.rheads
2031 )
2030 )
2032 elif pullop.heads is None:
2031 elif pullop.heads is None:
2033 with pullop.remote.commandexecutor() as e:
2032 with pullop.remote.commandexecutor() as e:
2034 cg = e.callcommand(
2033 cg = e.callcommand(
2035 b'changegroup',
2034 b'changegroup',
2036 {
2035 {
2037 b'nodes': pullop.fetch,
2036 b'nodes': pullop.fetch,
2038 b'source': b'pull',
2037 b'source': b'pull',
2039 },
2038 },
2040 ).result()
2039 ).result()
2041
2040
2042 elif not pullop.remote.capable(b'changegroupsubset'):
2041 elif not pullop.remote.capable(b'changegroupsubset'):
2043 raise error.Abort(
2042 raise error.Abort(
2044 _(
2043 _(
2045 b"partial pull cannot be done because "
2044 b"partial pull cannot be done because "
2046 b"other repository doesn't support "
2045 b"other repository doesn't support "
2047 b"changegroupsubset."
2046 b"changegroupsubset."
2048 )
2047 )
2049 )
2048 )
2050 else:
2049 else:
2051 with pullop.remote.commandexecutor() as e:
2050 with pullop.remote.commandexecutor() as e:
2052 cg = e.callcommand(
2051 cg = e.callcommand(
2053 b'changegroupsubset',
2052 b'changegroupsubset',
2054 {
2053 {
2055 b'bases': pullop.fetch,
2054 b'bases': pullop.fetch,
2056 b'heads': pullop.heads,
2055 b'heads': pullop.heads,
2057 b'source': b'pull',
2056 b'source': b'pull',
2058 },
2057 },
2059 ).result()
2058 ).result()
2060
2059
2061 bundleop = bundle2.applybundle(
2060 bundleop = bundle2.applybundle(
2062 pullop.repo,
2061 pullop.repo,
2063 cg,
2062 cg,
2064 tr,
2063 tr,
2065 b'pull',
2064 b'pull',
2066 pullop.remote.url(),
2065 pullop.remote.url(),
2067 remote=pullop.remote,
2066 remote=pullop.remote,
2068 )
2067 )
2069 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
2068 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
2070
2069
2071
2070
2072 def _pullphase(pullop):
2071 def _pullphase(pullop):
2073 # Get remote phases data from remote
2072 # Get remote phases data from remote
2074 if b'phases' in pullop.stepsdone:
2073 if b'phases' in pullop.stepsdone:
2075 return
2074 return
2076 remotephases = listkeys(pullop.remote, b'phases')
2075 remotephases = listkeys(pullop.remote, b'phases')
2077 _pullapplyphases(pullop, remotephases)
2076 _pullapplyphases(pullop, remotephases)
2078
2077
2079
2078
2080 def _pullapplyphases(pullop, remotephases):
2079 def _pullapplyphases(pullop, remotephases):
2081 """apply phase movement from observed remote state"""
2080 """apply phase movement from observed remote state"""
2082 if b'phases' in pullop.stepsdone:
2081 if b'phases' in pullop.stepsdone:
2083 return
2082 return
2084 pullop.stepsdone.add(b'phases')
2083 pullop.stepsdone.add(b'phases')
2085 publishing = bool(remotephases.get(b'publishing', False))
2084 publishing = bool(remotephases.get(b'publishing', False))
2086 if remotephases and not publishing:
2085 if remotephases and not publishing:
2087 unfi = pullop.repo.unfiltered()
2086 unfi = pullop.repo.unfiltered()
2088 to_rev = unfi.changelog.index.rev
2087 to_rev = unfi.changelog.index.rev
2089 to_node = unfi.changelog.node
2088 to_node = unfi.changelog.node
2090 pulledsubset_revs = [to_rev(n) for n in pullop.pulledsubset]
2089 pulledsubset_revs = [to_rev(n) for n in pullop.pulledsubset]
2091 # remote is new and non-publishing
2090 # remote is new and non-publishing
2092 pheads_revs, _dr = phases.analyze_remote_phases(
2091 pheads_revs, _dr = phases.analyze_remote_phases(
2093 pullop.repo,
2092 pullop.repo,
2094 pulledsubset_revs,
2093 pulledsubset_revs,
2095 remotephases,
2094 remotephases,
2096 )
2095 )
2097 pheads = [to_node(r) for r in pheads_revs]
2096 pheads = [to_node(r) for r in pheads_revs]
2098 dheads = pullop.pulledsubset
2097 dheads = pullop.pulledsubset
2099 else:
2098 else:
2100 # Remote is old or publishing all common changesets
2099 # Remote is old or publishing all common changesets
2101 # should be seen as public
2100 # should be seen as public
2102 pheads = pullop.pulledsubset
2101 pheads = pullop.pulledsubset
2103 dheads = []
2102 dheads = []
2104 unfi = pullop.repo.unfiltered()
2103 unfi = pullop.repo.unfiltered()
2105 phase = unfi._phasecache.phase
2104 phase = unfi._phasecache.phase
2106 rev = unfi.changelog.index.get_rev
2105 rev = unfi.changelog.index.get_rev
2107 public = phases.public
2106 public = phases.public
2108 draft = phases.draft
2107 draft = phases.draft
2109
2108
2110 # exclude changesets already public locally and update the others
2109 # exclude changesets already public locally and update the others
2111 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
2110 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
2112 if pheads:
2111 if pheads:
2113 tr = pullop.gettransaction()
2112 tr = pullop.gettransaction()
2114 phases.advanceboundary(pullop.repo, tr, public, pheads)
2113 phases.advanceboundary(pullop.repo, tr, public, pheads)
2115
2114
2116 # exclude changesets already draft locally and update the others
2115 # exclude changesets already draft locally and update the others
2117 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
2116 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
2118 if dheads:
2117 if dheads:
2119 tr = pullop.gettransaction()
2118 tr = pullop.gettransaction()
2120 phases.advanceboundary(pullop.repo, tr, draft, dheads)
2119 phases.advanceboundary(pullop.repo, tr, draft, dheads)
2121
2120
2122
2121
2123 def _pullbookmarks(pullop):
2122 def _pullbookmarks(pullop):
2124 """process the remote bookmark information to update the local one"""
2123 """process the remote bookmark information to update the local one"""
2125 if b'bookmarks' in pullop.stepsdone:
2124 if b'bookmarks' in pullop.stepsdone:
2126 return
2125 return
2127 pullop.stepsdone.add(b'bookmarks')
2126 pullop.stepsdone.add(b'bookmarks')
2128 repo = pullop.repo
2127 repo = pullop.repo
2129 remotebookmarks = pullop.remotebookmarks
2128 remotebookmarks = pullop.remotebookmarks
2130 bookmarks_mode = None
2129 bookmarks_mode = None
2131 if pullop.remote_path is not None:
2130 if pullop.remote_path is not None:
2132 bookmarks_mode = pullop.remote_path.bookmarks_mode
2131 bookmarks_mode = pullop.remote_path.bookmarks_mode
2133 bookmod.updatefromremote(
2132 bookmod.updatefromremote(
2134 repo.ui,
2133 repo.ui,
2135 repo,
2134 repo,
2136 remotebookmarks,
2135 remotebookmarks,
2137 pullop.remote.url(),
2136 pullop.remote.url(),
2138 pullop.gettransaction,
2137 pullop.gettransaction,
2139 explicit=pullop.explicitbookmarks,
2138 explicit=pullop.explicitbookmarks,
2140 mode=bookmarks_mode,
2139 mode=bookmarks_mode,
2141 )
2140 )
2142
2141
2143
2142
2144 def _pullobsolete(pullop):
2143 def _pullobsolete(pullop):
2145 """utility function to pull obsolete markers from a remote
2144 """utility function to pull obsolete markers from a remote
2146
2145
2147 The `gettransaction` is function that return the pull transaction, creating
2146 The `gettransaction` is function that return the pull transaction, creating
2148 one if necessary. We return the transaction to inform the calling code that
2147 one if necessary. We return the transaction to inform the calling code that
2149 a new transaction have been created (when applicable).
2148 a new transaction have been created (when applicable).
2150
2149
2151 Exists mostly to allow overriding for experimentation purpose"""
2150 Exists mostly to allow overriding for experimentation purpose"""
2152 if b'obsmarkers' in pullop.stepsdone:
2151 if b'obsmarkers' in pullop.stepsdone:
2153 return
2152 return
2154 pullop.stepsdone.add(b'obsmarkers')
2153 pullop.stepsdone.add(b'obsmarkers')
2155 tr = None
2154 tr = None
2156 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
2155 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
2157 pullop.repo.ui.debug(b'fetching remote obsolete markers\n')
2156 pullop.repo.ui.debug(b'fetching remote obsolete markers\n')
2158 remoteobs = listkeys(pullop.remote, b'obsolete')
2157 remoteobs = listkeys(pullop.remote, b'obsolete')
2159 if b'dump0' in remoteobs:
2158 if b'dump0' in remoteobs:
2160 tr = pullop.gettransaction()
2159 tr = pullop.gettransaction()
2161 markers = []
2160 markers = []
2162 for key in sorted(remoteobs, reverse=True):
2161 for key in sorted(remoteobs, reverse=True):
2163 if key.startswith(b'dump'):
2162 if key.startswith(b'dump'):
2164 data = util.b85decode(remoteobs[key])
2163 data = util.b85decode(remoteobs[key])
2165 version, newmarks = obsolete._readmarkers(data)
2164 version, newmarks = obsolete._readmarkers(data)
2166 markers += newmarks
2165 markers += newmarks
2167 if markers:
2166 if markers:
2168 pullop.repo.obsstore.add(tr, markers)
2167 pullop.repo.obsstore.add(tr, markers)
2169 pullop.repo.invalidatevolatilesets()
2168 pullop.repo.invalidatevolatilesets()
2170 return tr
2169 return tr
2171
2170
2172
2171
2173 def applynarrowacl(repo, kwargs):
2172 def applynarrowacl(repo, kwargs):
2174 """Apply narrow fetch access control.
2173 """Apply narrow fetch access control.
2175
2174
2176 This massages the named arguments for getbundle wire protocol commands
2175 This massages the named arguments for getbundle wire protocol commands
2177 so requested data is filtered through access control rules.
2176 so requested data is filtered through access control rules.
2178 """
2177 """
2179 ui = repo.ui
2178 ui = repo.ui
2180 # TODO this assumes existence of HTTP and is a layering violation.
2179 # TODO this assumes existence of HTTP and is a layering violation.
2181 username = ui.shortuser(ui.environ.get(b'REMOTE_USER') or ui.username())
2180 username = ui.shortuser(ui.environ.get(b'REMOTE_USER') or ui.username())
2182 user_includes = ui.configlist(
2181 user_includes = ui.configlist(
2183 _NARROWACL_SECTION,
2182 _NARROWACL_SECTION,
2184 username + b'.includes',
2183 username + b'.includes',
2185 ui.configlist(_NARROWACL_SECTION, b'default.includes'),
2184 ui.configlist(_NARROWACL_SECTION, b'default.includes'),
2186 )
2185 )
2187 user_excludes = ui.configlist(
2186 user_excludes = ui.configlist(
2188 _NARROWACL_SECTION,
2187 _NARROWACL_SECTION,
2189 username + b'.excludes',
2188 username + b'.excludes',
2190 ui.configlist(_NARROWACL_SECTION, b'default.excludes'),
2189 ui.configlist(_NARROWACL_SECTION, b'default.excludes'),
2191 )
2190 )
2192 if not user_includes:
2191 if not user_includes:
2193 raise error.Abort(
2192 raise error.Abort(
2194 _(b"%s configuration for user %s is empty")
2193 _(b"%s configuration for user %s is empty")
2195 % (_NARROWACL_SECTION, username)
2194 % (_NARROWACL_SECTION, username)
2196 )
2195 )
2197
2196
2198 user_includes = [
2197 user_includes = [
2199 b'path:.' if p == b'*' else b'path:' + p for p in user_includes
2198 b'path:.' if p == b'*' else b'path:' + p for p in user_includes
2200 ]
2199 ]
2201 user_excludes = [
2200 user_excludes = [
2202 b'path:.' if p == b'*' else b'path:' + p for p in user_excludes
2201 b'path:.' if p == b'*' else b'path:' + p for p in user_excludes
2203 ]
2202 ]
2204
2203
2205 req_includes = set(kwargs.get('includepats', []))
2204 req_includes = set(kwargs.get('includepats', []))
2206 req_excludes = set(kwargs.get('excludepats', []))
2205 req_excludes = set(kwargs.get('excludepats', []))
2207
2206
2208 req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns(
2207 req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns(
2209 req_includes, req_excludes, user_includes, user_excludes
2208 req_includes, req_excludes, user_includes, user_excludes
2210 )
2209 )
2211
2210
2212 if invalid_includes:
2211 if invalid_includes:
2213 raise error.Abort(
2212 raise error.Abort(
2214 _(b"The following includes are not accessible for %s: %s")
2213 _(b"The following includes are not accessible for %s: %s")
2215 % (username, stringutil.pprint(invalid_includes))
2214 % (username, stringutil.pprint(invalid_includes))
2216 )
2215 )
2217
2216
2218 new_args = {}
2217 new_args = {}
2219 new_args.update(kwargs)
2218 new_args.update(kwargs)
2220 new_args['narrow'] = True
2219 new_args['narrow'] = True
2221 new_args['narrow_acl'] = True
2220 new_args['narrow_acl'] = True
2222 new_args['includepats'] = req_includes
2221 new_args['includepats'] = req_includes
2223 if req_excludes:
2222 if req_excludes:
2224 new_args['excludepats'] = req_excludes
2223 new_args['excludepats'] = req_excludes
2225
2224
2226 return new_args
2225 return new_args
2227
2226
2228
2227
2229 def _computeellipsis(repo, common, heads, known, match, depth=None):
2228 def _computeellipsis(repo, common, heads, known, match, depth=None):
2230 """Compute the shape of a narrowed DAG.
2229 """Compute the shape of a narrowed DAG.
2231
2230
2232 Args:
2231 Args:
2233 repo: The repository we're transferring.
2232 repo: The repository we're transferring.
2234 common: The roots of the DAG range we're transferring.
2233 common: The roots of the DAG range we're transferring.
2235 May be just [nullid], which means all ancestors of heads.
2234 May be just [nullid], which means all ancestors of heads.
2236 heads: The heads of the DAG range we're transferring.
2235 heads: The heads of the DAG range we're transferring.
2237 match: The narrowmatcher that allows us to identify relevant changes.
2236 match: The narrowmatcher that allows us to identify relevant changes.
2238 depth: If not None, only consider nodes to be full nodes if they are at
2237 depth: If not None, only consider nodes to be full nodes if they are at
2239 most depth changesets away from one of heads.
2238 most depth changesets away from one of heads.
2240
2239
2241 Returns:
2240 Returns:
2242 A tuple of (visitnodes, relevant_nodes, ellipsisroots) where:
2241 A tuple of (visitnodes, relevant_nodes, ellipsisroots) where:
2243
2242
2244 visitnodes: The list of nodes (either full or ellipsis) which
2243 visitnodes: The list of nodes (either full or ellipsis) which
2245 need to be sent to the client.
2244 need to be sent to the client.
2246 relevant_nodes: The set of changelog nodes which change a file inside
2245 relevant_nodes: The set of changelog nodes which change a file inside
2247 the narrowspec. The client needs these as non-ellipsis nodes.
2246 the narrowspec. The client needs these as non-ellipsis nodes.
2248 ellipsisroots: A dict of {rev: parents} that is used in
2247 ellipsisroots: A dict of {rev: parents} that is used in
2249 narrowchangegroup to produce ellipsis nodes with the
2248 narrowchangegroup to produce ellipsis nodes with the
2250 correct parents.
2249 correct parents.
2251 """
2250 """
2252 cl = repo.changelog
2251 cl = repo.changelog
2253 mfl = repo.manifestlog
2252 mfl = repo.manifestlog
2254
2253
2255 clrev = cl.rev
2254 clrev = cl.rev
2256
2255
2257 commonrevs = {clrev(n) for n in common} | {nullrev}
2256 commonrevs = {clrev(n) for n in common} | {nullrev}
2258 headsrevs = {clrev(n) for n in heads}
2257 headsrevs = {clrev(n) for n in heads}
2259
2258
2260 if depth:
2259 if depth:
2261 revdepth = {h: 0 for h in headsrevs}
2260 revdepth = {h: 0 for h in headsrevs}
2262
2261
2263 ellipsisheads = collections.defaultdict(set)
2262 ellipsisheads = collections.defaultdict(set)
2264 ellipsisroots = collections.defaultdict(set)
2263 ellipsisroots = collections.defaultdict(set)
2265
2264
2266 def addroot(head, curchange):
2265 def addroot(head, curchange):
2267 """Add a root to an ellipsis head, splitting heads with 3 roots."""
2266 """Add a root to an ellipsis head, splitting heads with 3 roots."""
2268 ellipsisroots[head].add(curchange)
2267 ellipsisroots[head].add(curchange)
2269 # Recursively split ellipsis heads with 3 roots by finding the
2268 # Recursively split ellipsis heads with 3 roots by finding the
2270 # roots' youngest common descendant which is an elided merge commit.
2269 # roots' youngest common descendant which is an elided merge commit.
2271 # That descendant takes 2 of the 3 roots as its own, and becomes a
2270 # That descendant takes 2 of the 3 roots as its own, and becomes a
2272 # root of the head.
2271 # root of the head.
2273 while len(ellipsisroots[head]) > 2:
2272 while len(ellipsisroots[head]) > 2:
2274 child, roots = splithead(head)
2273 child, roots = splithead(head)
2275 splitroots(head, child, roots)
2274 splitroots(head, child, roots)
2276 head = child # Recurse in case we just added a 3rd root
2275 head = child # Recurse in case we just added a 3rd root
2277
2276
2278 def splitroots(head, child, roots):
2277 def splitroots(head, child, roots):
2279 ellipsisroots[head].difference_update(roots)
2278 ellipsisroots[head].difference_update(roots)
2280 ellipsisroots[head].add(child)
2279 ellipsisroots[head].add(child)
2281 ellipsisroots[child].update(roots)
2280 ellipsisroots[child].update(roots)
2282 ellipsisroots[child].discard(child)
2281 ellipsisroots[child].discard(child)
2283
2282
2284 def splithead(head):
2283 def splithead(head):
2285 r1, r2, r3 = sorted(ellipsisroots[head])
2284 r1, r2, r3 = sorted(ellipsisroots[head])
2286 for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)):
2285 for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)):
2287 mid = repo.revs(
2286 mid = repo.revs(
2288 b'sort(merge() & %d::%d & %d::%d, -rev)', nr1, head, nr2, head
2287 b'sort(merge() & %d::%d & %d::%d, -rev)', nr1, head, nr2, head
2289 )
2288 )
2290 for j in mid:
2289 for j in mid:
2291 if j == nr2:
2290 if j == nr2:
2292 return nr2, (nr1, nr2)
2291 return nr2, (nr1, nr2)
2293 if j not in ellipsisroots or len(ellipsisroots[j]) < 2:
2292 if j not in ellipsisroots or len(ellipsisroots[j]) < 2:
2294 return j, (nr1, nr2)
2293 return j, (nr1, nr2)
2295 raise error.Abort(
2294 raise error.Abort(
2296 _(
2295 _(
2297 b'Failed to split up ellipsis node! head: %d, '
2296 b'Failed to split up ellipsis node! head: %d, '
2298 b'roots: %d %d %d'
2297 b'roots: %d %d %d'
2299 )
2298 )
2300 % (head, r1, r2, r3)
2299 % (head, r1, r2, r3)
2301 )
2300 )
2302
2301
2303 missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs))
2302 missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs))
2304 visit = reversed(missing)
2303 visit = reversed(missing)
2305 relevant_nodes = set()
2304 relevant_nodes = set()
2306 visitnodes = [cl.node(m) for m in missing]
2305 visitnodes = [cl.node(m) for m in missing]
2307 required = set(headsrevs) | known
2306 required = set(headsrevs) | known
2308 for rev in visit:
2307 for rev in visit:
2309 clrev = cl.changelogrevision(rev)
2308 clrev = cl.changelogrevision(rev)
2310 ps = [prev for prev in cl.parentrevs(rev) if prev != nullrev]
2309 ps = [prev for prev in cl.parentrevs(rev) if prev != nullrev]
2311 if depth is not None:
2310 if depth is not None:
2312 curdepth = revdepth[rev]
2311 curdepth = revdepth[rev]
2313 for p in ps:
2312 for p in ps:
2314 revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1))
2313 revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1))
2315 needed = False
2314 needed = False
2316 shallow_enough = depth is None or revdepth[rev] <= depth
2315 shallow_enough = depth is None or revdepth[rev] <= depth
2317 if shallow_enough:
2316 if shallow_enough:
2318 curmf = mfl[clrev.manifest].read()
2317 curmf = mfl[clrev.manifest].read()
2319 if ps:
2318 if ps:
2320 # We choose to not trust the changed files list in
2319 # We choose to not trust the changed files list in
2321 # changesets because it's not always correct. TODO: could
2320 # changesets because it's not always correct. TODO: could
2322 # we trust it for the non-merge case?
2321 # we trust it for the non-merge case?
2323 p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read()
2322 p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read()
2324 needed = bool(curmf.diff(p1mf, match))
2323 needed = bool(curmf.diff(p1mf, match))
2325 if not needed and len(ps) > 1:
2324 if not needed and len(ps) > 1:
2326 # For merge changes, the list of changed files is not
2325 # For merge changes, the list of changed files is not
2327 # helpful, since we need to emit the merge if a file
2326 # helpful, since we need to emit the merge if a file
2328 # in the narrow spec has changed on either side of the
2327 # in the narrow spec has changed on either side of the
2329 # merge. As a result, we do a manifest diff to check.
2328 # merge. As a result, we do a manifest diff to check.
2330 p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read()
2329 p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read()
2331 needed = bool(curmf.diff(p2mf, match))
2330 needed = bool(curmf.diff(p2mf, match))
2332 else:
2331 else:
2333 # For a root node, we need to include the node if any
2332 # For a root node, we need to include the node if any
2334 # files in the node match the narrowspec.
2333 # files in the node match the narrowspec.
2335 needed = any(curmf.walk(match))
2334 needed = any(curmf.walk(match))
2336
2335
2337 if needed:
2336 if needed:
2338 for head in ellipsisheads[rev]:
2337 for head in ellipsisheads[rev]:
2339 addroot(head, rev)
2338 addroot(head, rev)
2340 for p in ps:
2339 for p in ps:
2341 required.add(p)
2340 required.add(p)
2342 relevant_nodes.add(cl.node(rev))
2341 relevant_nodes.add(cl.node(rev))
2343 else:
2342 else:
2344 if not ps:
2343 if not ps:
2345 ps = [nullrev]
2344 ps = [nullrev]
2346 if rev in required:
2345 if rev in required:
2347 for head in ellipsisheads[rev]:
2346 for head in ellipsisheads[rev]:
2348 addroot(head, rev)
2347 addroot(head, rev)
2349 for p in ps:
2348 for p in ps:
2350 ellipsisheads[p].add(rev)
2349 ellipsisheads[p].add(rev)
2351 else:
2350 else:
2352 for p in ps:
2351 for p in ps:
2353 ellipsisheads[p] |= ellipsisheads[rev]
2352 ellipsisheads[p] |= ellipsisheads[rev]
2354
2353
2355 # add common changesets as roots of their reachable ellipsis heads
2354 # add common changesets as roots of their reachable ellipsis heads
2356 for c in commonrevs:
2355 for c in commonrevs:
2357 for head in ellipsisheads[c]:
2356 for head in ellipsisheads[c]:
2358 addroot(head, c)
2357 addroot(head, c)
2359 return visitnodes, relevant_nodes, ellipsisroots
2358 return visitnodes, relevant_nodes, ellipsisroots
2360
2359
2361
2360
2362 def caps20to10(repo, role):
2361 def caps20to10(repo, role):
2363 """return a set with appropriate options to use bundle20 during getbundle"""
2362 """return a set with appropriate options to use bundle20 during getbundle"""
2364 caps = {b'HG20'}
2363 caps = {b'HG20'}
2365 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=role))
2364 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=role))
2366 caps.add(b'bundle2=' + urlreq.quote(capsblob))
2365 caps.add(b'bundle2=' + urlreq.quote(capsblob))
2367 return caps
2366 return caps
2368
2367
2369
2368
2370 # List of names of steps to perform for a bundle2 for getbundle, order matters.
2369 # List of names of steps to perform for a bundle2 for getbundle, order matters.
2371 getbundle2partsorder = []
2370 getbundle2partsorder = []
2372
2371
2373 # Mapping between step name and function
2372 # Mapping between step name and function
2374 #
2373 #
2375 # This exists to help extensions wrap steps if necessary
2374 # This exists to help extensions wrap steps if necessary
2376 getbundle2partsmapping = {}
2375 getbundle2partsmapping = {}
2377
2376
2378
2377
2379 def getbundle2partsgenerator(stepname, idx=None):
2378 def getbundle2partsgenerator(stepname, idx=None):
2380 """decorator for function generating bundle2 part for getbundle
2379 """decorator for function generating bundle2 part for getbundle
2381
2380
2382 The function is added to the step -> function mapping and appended to the
2381 The function is added to the step -> function mapping and appended to the
2383 list of steps. Beware that decorated functions will be added in order
2382 list of steps. Beware that decorated functions will be added in order
2384 (this may matter).
2383 (this may matter).
2385
2384
2386 You can only use this decorator for new steps, if you want to wrap a step
2385 You can only use this decorator for new steps, if you want to wrap a step
2387 from an extension, attack the getbundle2partsmapping dictionary directly."""
2386 from an extension, attack the getbundle2partsmapping dictionary directly."""
2388
2387
2389 def dec(func):
2388 def dec(func):
2390 assert stepname not in getbundle2partsmapping
2389 assert stepname not in getbundle2partsmapping
2391 getbundle2partsmapping[stepname] = func
2390 getbundle2partsmapping[stepname] = func
2392 if idx is None:
2391 if idx is None:
2393 getbundle2partsorder.append(stepname)
2392 getbundle2partsorder.append(stepname)
2394 else:
2393 else:
2395 getbundle2partsorder.insert(idx, stepname)
2394 getbundle2partsorder.insert(idx, stepname)
2396 return func
2395 return func
2397
2396
2398 return dec
2397 return dec
2399
2398
2400
2399
2401 def bundle2requested(bundlecaps):
2400 def bundle2requested(bundlecaps):
2402 if bundlecaps is not None:
2401 if bundlecaps is not None:
2403 return any(cap.startswith(b'HG2') for cap in bundlecaps)
2402 return any(cap.startswith(b'HG2') for cap in bundlecaps)
2404 return False
2403 return False
2405
2404
2406
2405
2407 def getbundlechunks(
2406 def getbundlechunks(
2408 repo,
2407 repo,
2409 source,
2408 source,
2410 heads=None,
2409 heads=None,
2411 common=None,
2410 common=None,
2412 bundlecaps=None,
2411 bundlecaps=None,
2413 remote_sidedata=None,
2412 remote_sidedata=None,
2414 **kwargs
2413 **kwargs
2415 ):
2414 ):
2416 """Return chunks constituting a bundle's raw data.
2415 """Return chunks constituting a bundle's raw data.
2417
2416
2418 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
2417 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
2419 passed.
2418 passed.
2420
2419
2421 Returns a 2-tuple of a dict with metadata about the generated bundle
2420 Returns a 2-tuple of a dict with metadata about the generated bundle
2422 and an iterator over raw chunks (of varying sizes).
2421 and an iterator over raw chunks (of varying sizes).
2423 """
2422 """
2424 kwargs = pycompat.byteskwargs(kwargs)
2423 kwargs = pycompat.byteskwargs(kwargs)
2425 info = {}
2424 info = {}
2426 usebundle2 = bundle2requested(bundlecaps)
2425 usebundle2 = bundle2requested(bundlecaps)
2427 # bundle10 case
2426 # bundle10 case
2428 if not usebundle2:
2427 if not usebundle2:
2429 if bundlecaps and not kwargs.get(b'cg', True):
2428 if bundlecaps and not kwargs.get(b'cg', True):
2430 raise ValueError(
2429 raise ValueError(
2431 _(b'request for bundle10 must include changegroup')
2430 _(b'request for bundle10 must include changegroup')
2432 )
2431 )
2433
2432
2434 if kwargs:
2433 if kwargs:
2435 raise ValueError(
2434 raise ValueError(
2436 _(b'unsupported getbundle arguments: %s')
2435 _(b'unsupported getbundle arguments: %s')
2437 % b', '.join(sorted(kwargs.keys()))
2436 % b', '.join(sorted(kwargs.keys()))
2438 )
2437 )
2439 outgoing = _computeoutgoing(repo, heads, common)
2438 outgoing = _computeoutgoing(repo, heads, common)
2440 info[b'bundleversion'] = 1
2439 info[b'bundleversion'] = 1
2441 return (
2440 return (
2442 info,
2441 info,
2443 changegroup.makestream(
2442 changegroup.makestream(
2444 repo,
2443 repo,
2445 outgoing,
2444 outgoing,
2446 b'01',
2445 b'01',
2447 source,
2446 source,
2448 bundlecaps=bundlecaps,
2447 bundlecaps=bundlecaps,
2449 remote_sidedata=remote_sidedata,
2448 remote_sidedata=remote_sidedata,
2450 ),
2449 ),
2451 )
2450 )
2452
2451
2453 # bundle20 case
2452 # bundle20 case
2454 info[b'bundleversion'] = 2
2453 info[b'bundleversion'] = 2
2455 b2caps = {}
2454 b2caps = {}
2456 for bcaps in bundlecaps:
2455 for bcaps in bundlecaps:
2457 if bcaps.startswith(b'bundle2='):
2456 if bcaps.startswith(b'bundle2='):
2458 blob = urlreq.unquote(bcaps[len(b'bundle2=') :])
2457 blob = urlreq.unquote(bcaps[len(b'bundle2=') :])
2459 b2caps.update(bundle2.decodecaps(blob))
2458 b2caps.update(bundle2.decodecaps(blob))
2460 bundler = bundle2.bundle20(repo.ui, b2caps)
2459 bundler = bundle2.bundle20(repo.ui, b2caps)
2461
2460
2462 kwargs[b'heads'] = heads
2461 kwargs[b'heads'] = heads
2463 kwargs[b'common'] = common
2462 kwargs[b'common'] = common
2464
2463
2465 for name in getbundle2partsorder:
2464 for name in getbundle2partsorder:
2466 func = getbundle2partsmapping[name]
2465 func = getbundle2partsmapping[name]
2467 func(
2466 func(
2468 bundler,
2467 bundler,
2469 repo,
2468 repo,
2470 source,
2469 source,
2471 bundlecaps=bundlecaps,
2470 bundlecaps=bundlecaps,
2472 b2caps=b2caps,
2471 b2caps=b2caps,
2473 remote_sidedata=remote_sidedata,
2472 remote_sidedata=remote_sidedata,
2474 **pycompat.strkwargs(kwargs)
2473 **pycompat.strkwargs(kwargs)
2475 )
2474 )
2476
2475
2477 info[b'prefercompressed'] = bundler.prefercompressed
2476 info[b'prefercompressed'] = bundler.prefercompressed
2478
2477
2479 return info, bundler.getchunks()
2478 return info, bundler.getchunks()
2480
2479
2481
2480
2482 @getbundle2partsgenerator(b'stream')
2481 @getbundle2partsgenerator(b'stream')
2483 def _getbundlestream2(bundler, repo, *args, **kwargs):
2482 def _getbundlestream2(bundler, repo, *args, **kwargs):
2484 return bundle2.addpartbundlestream2(bundler, repo, **kwargs)
2483 return bundle2.addpartbundlestream2(bundler, repo, **kwargs)
2485
2484
2486
2485
2487 @getbundle2partsgenerator(b'changegroup')
2486 @getbundle2partsgenerator(b'changegroup')
2488 def _getbundlechangegrouppart(
2487 def _getbundlechangegrouppart(
2489 bundler,
2488 bundler,
2490 repo,
2489 repo,
2491 source,
2490 source,
2492 bundlecaps=None,
2491 bundlecaps=None,
2493 b2caps=None,
2492 b2caps=None,
2494 heads=None,
2493 heads=None,
2495 common=None,
2494 common=None,
2496 remote_sidedata=None,
2495 remote_sidedata=None,
2497 **kwargs
2496 **kwargs
2498 ):
2497 ):
2499 """add a changegroup part to the requested bundle"""
2498 """add a changegroup part to the requested bundle"""
2500 if not kwargs.get('cg', True) or not b2caps:
2499 if not kwargs.get('cg', True) or not b2caps:
2501 return
2500 return
2502
2501
2503 version = b'01'
2502 version = b'01'
2504 cgversions = b2caps.get(b'changegroup')
2503 cgversions = b2caps.get(b'changegroup')
2505 if cgversions: # 3.1 and 3.2 ship with an empty value
2504 if cgversions: # 3.1 and 3.2 ship with an empty value
2506 cgversions = [
2505 cgversions = [
2507 v
2506 v
2508 for v in cgversions
2507 for v in cgversions
2509 if v in changegroup.supportedoutgoingversions(repo)
2508 if v in changegroup.supportedoutgoingversions(repo)
2510 ]
2509 ]
2511 if not cgversions:
2510 if not cgversions:
2512 raise error.Abort(_(b'no common changegroup version'))
2511 raise error.Abort(_(b'no common changegroup version'))
2513 version = max(cgversions)
2512 version = max(cgversions)
2514
2513
2515 outgoing = _computeoutgoing(repo, heads, common)
2514 outgoing = _computeoutgoing(repo, heads, common)
2516 if not outgoing.missing:
2515 if not outgoing.missing:
2517 return
2516 return
2518
2517
2519 if kwargs.get('narrow', False):
2518 if kwargs.get('narrow', False):
2520 include = sorted(filter(bool, kwargs.get('includepats', [])))
2519 include = sorted(filter(bool, kwargs.get('includepats', [])))
2521 exclude = sorted(filter(bool, kwargs.get('excludepats', [])))
2520 exclude = sorted(filter(bool, kwargs.get('excludepats', [])))
2522 matcher = narrowspec.match(repo.root, include=include, exclude=exclude)
2521 matcher = narrowspec.match(repo.root, include=include, exclude=exclude)
2523 else:
2522 else:
2524 matcher = None
2523 matcher = None
2525
2524
2526 cgstream = changegroup.makestream(
2525 cgstream = changegroup.makestream(
2527 repo,
2526 repo,
2528 outgoing,
2527 outgoing,
2529 version,
2528 version,
2530 source,
2529 source,
2531 bundlecaps=bundlecaps,
2530 bundlecaps=bundlecaps,
2532 matcher=matcher,
2531 matcher=matcher,
2533 remote_sidedata=remote_sidedata,
2532 remote_sidedata=remote_sidedata,
2534 )
2533 )
2535
2534
2536 part = bundler.newpart(b'changegroup', data=cgstream)
2535 part = bundler.newpart(b'changegroup', data=cgstream)
2537 if cgversions:
2536 if cgversions:
2538 part.addparam(b'version', version)
2537 part.addparam(b'version', version)
2539
2538
2540 part.addparam(b'nbchanges', b'%d' % len(outgoing.missing), mandatory=False)
2539 part.addparam(b'nbchanges', b'%d' % len(outgoing.missing), mandatory=False)
2541
2540
2542 if scmutil.istreemanifest(repo):
2541 if scmutil.istreemanifest(repo):
2543 part.addparam(b'treemanifest', b'1')
2542 part.addparam(b'treemanifest', b'1')
2544
2543
2545 if repository.REPO_FEATURE_SIDE_DATA in repo.features:
2544 if repository.REPO_FEATURE_SIDE_DATA in repo.features:
2546 part.addparam(b'exp-sidedata', b'1')
2545 part.addparam(b'exp-sidedata', b'1')
2547 sidedata = bundle2.format_remote_wanted_sidedata(repo)
2546 sidedata = bundle2.format_remote_wanted_sidedata(repo)
2548 part.addparam(b'exp-wanted-sidedata', sidedata)
2547 part.addparam(b'exp-wanted-sidedata', sidedata)
2549
2548
2550 if (
2549 if (
2551 kwargs.get('narrow', False)
2550 kwargs.get('narrow', False)
2552 and kwargs.get('narrow_acl', False)
2551 and kwargs.get('narrow_acl', False)
2553 and (include or exclude)
2552 and (include or exclude)
2554 ):
2553 ):
2555 # this is mandatory because otherwise ACL clients won't work
2554 # this is mandatory because otherwise ACL clients won't work
2556 narrowspecpart = bundler.newpart(b'Narrow:responsespec')
2555 narrowspecpart = bundler.newpart(b'Narrow:responsespec')
2557 narrowspecpart.data = b'%s\0%s' % (
2556 narrowspecpart.data = b'%s\0%s' % (
2558 b'\n'.join(include),
2557 b'\n'.join(include),
2559 b'\n'.join(exclude),
2558 b'\n'.join(exclude),
2560 )
2559 )
2561
2560
2562
2561
2563 @getbundle2partsgenerator(b'bookmarks')
2562 @getbundle2partsgenerator(b'bookmarks')
2564 def _getbundlebookmarkpart(
2563 def _getbundlebookmarkpart(
2565 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2564 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2566 ):
2565 ):
2567 """add a bookmark part to the requested bundle"""
2566 """add a bookmark part to the requested bundle"""
2568 if not kwargs.get('bookmarks', False):
2567 if not kwargs.get('bookmarks', False):
2569 return
2568 return
2570 if not b2caps or b'bookmarks' not in b2caps:
2569 if not b2caps or b'bookmarks' not in b2caps:
2571 raise error.Abort(_(b'no common bookmarks exchange method'))
2570 raise error.Abort(_(b'no common bookmarks exchange method'))
2572 books = bookmod.listbinbookmarks(repo)
2571 books = bookmod.listbinbookmarks(repo)
2573 data = bookmod.binaryencode(repo, books)
2572 data = bookmod.binaryencode(repo, books)
2574 if data:
2573 if data:
2575 bundler.newpart(b'bookmarks', data=data)
2574 bundler.newpart(b'bookmarks', data=data)
2576
2575
2577
2576
2578 @getbundle2partsgenerator(b'listkeys')
2577 @getbundle2partsgenerator(b'listkeys')
2579 def _getbundlelistkeysparts(
2578 def _getbundlelistkeysparts(
2580 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2579 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2581 ):
2580 ):
2582 """add parts containing listkeys namespaces to the requested bundle"""
2581 """add parts containing listkeys namespaces to the requested bundle"""
2583 listkeys = kwargs.get('listkeys', ())
2582 listkeys = kwargs.get('listkeys', ())
2584 for namespace in listkeys:
2583 for namespace in listkeys:
2585 part = bundler.newpart(b'listkeys')
2584 part = bundler.newpart(b'listkeys')
2586 part.addparam(b'namespace', namespace)
2585 part.addparam(b'namespace', namespace)
2587 keys = repo.listkeys(namespace).items()
2586 keys = repo.listkeys(namespace).items()
2588 part.data = pushkey.encodekeys(keys)
2587 part.data = pushkey.encodekeys(keys)
2589
2588
2590
2589
2591 @getbundle2partsgenerator(b'obsmarkers')
2590 @getbundle2partsgenerator(b'obsmarkers')
2592 def _getbundleobsmarkerpart(
2591 def _getbundleobsmarkerpart(
2593 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2592 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2594 ):
2593 ):
2595 """add an obsolescence markers part to the requested bundle"""
2594 """add an obsolescence markers part to the requested bundle"""
2596 if kwargs.get('obsmarkers', False):
2595 if kwargs.get('obsmarkers', False):
2597 if heads is None:
2596 if heads is None:
2598 heads = repo.heads()
2597 heads = repo.heads()
2599 subset = [c.node() for c in repo.set(b'::%ln', heads)]
2598 subset = [c.node() for c in repo.set(b'::%ln', heads)]
2600 markers = repo.obsstore.relevantmarkers(subset)
2599 markers = repo.obsstore.relevantmarkers(subset)
2601 markers = obsutil.sortedmarkers(markers)
2600 markers = obsutil.sortedmarkers(markers)
2602 bundle2.buildobsmarkerspart(bundler, markers)
2601 bundle2.buildobsmarkerspart(bundler, markers)
2603
2602
2604
2603
2605 @getbundle2partsgenerator(b'phases')
2604 @getbundle2partsgenerator(b'phases')
2606 def _getbundlephasespart(
2605 def _getbundlephasespart(
2607 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2606 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2608 ):
2607 ):
2609 """add phase heads part to the requested bundle"""
2608 """add phase heads part to the requested bundle"""
2610 if kwargs.get('phases', False):
2609 if kwargs.get('phases', False):
2611 if not b2caps or b'heads' not in b2caps.get(b'phases'):
2610 if not b2caps or b'heads' not in b2caps.get(b'phases'):
2612 raise error.Abort(_(b'no common phases exchange method'))
2611 raise error.Abort(_(b'no common phases exchange method'))
2613 if heads is None:
2612 if heads is None:
2614 heads = repo.heads()
2613 heads = repo.heads()
2615
2614
2616 headsbyphase = collections.defaultdict(set)
2615 headsbyphase = collections.defaultdict(set)
2617 if repo.publishing():
2616 if repo.publishing():
2618 headsbyphase[phases.public] = heads
2617 headsbyphase[phases.public] = heads
2619 else:
2618 else:
2620 # find the appropriate heads to move
2619 # find the appropriate heads to move
2621
2620
2622 phase = repo._phasecache.phase
2621 phase = repo._phasecache.phase
2623 node = repo.changelog.node
2622 node = repo.changelog.node
2624 rev = repo.changelog.rev
2623 rev = repo.changelog.rev
2625 for h in heads:
2624 for h in heads:
2626 headsbyphase[phase(repo, rev(h))].add(h)
2625 headsbyphase[phase(repo, rev(h))].add(h)
2627 seenphases = list(headsbyphase.keys())
2626 seenphases = list(headsbyphase.keys())
2628
2627
2629 # We do not handle anything but public and draft phase for now)
2628 # We do not handle anything but public and draft phase for now)
2630 if seenphases:
2629 if seenphases:
2631 assert max(seenphases) <= phases.draft
2630 assert max(seenphases) <= phases.draft
2632
2631
2633 # if client is pulling non-public changesets, we need to find
2632 # if client is pulling non-public changesets, we need to find
2634 # intermediate public heads.
2633 # intermediate public heads.
2635 draftheads = headsbyphase.get(phases.draft, set())
2634 draftheads = headsbyphase.get(phases.draft, set())
2636 if draftheads:
2635 if draftheads:
2637 publicheads = headsbyphase.get(phases.public, set())
2636 publicheads = headsbyphase.get(phases.public, set())
2638
2637
2639 revset = b'heads(only(%ln, %ln) and public())'
2638 revset = b'heads(only(%ln, %ln) and public())'
2640 extraheads = repo.revs(revset, draftheads, publicheads)
2639 extraheads = repo.revs(revset, draftheads, publicheads)
2641 for r in extraheads:
2640 for r in extraheads:
2642 headsbyphase[phases.public].add(node(r))
2641 headsbyphase[phases.public].add(node(r))
2643
2642
2644 # transform data in a format used by the encoding function
2643 # transform data in a format used by the encoding function
2645 phasemapping = {
2644 phasemapping = {
2646 phase: sorted(headsbyphase[phase]) for phase in phases.allphases
2645 phase: sorted(headsbyphase[phase]) for phase in phases.allphases
2647 }
2646 }
2648
2647
2649 # generate the actual part
2648 # generate the actual part
2650 phasedata = phases.binaryencode(phasemapping)
2649 phasedata = phases.binaryencode(phasemapping)
2651 bundler.newpart(b'phase-heads', data=phasedata)
2650 bundler.newpart(b'phase-heads', data=phasedata)
2652
2651
2653
2652
2654 @getbundle2partsgenerator(b'hgtagsfnodes')
2653 @getbundle2partsgenerator(b'hgtagsfnodes')
2655 def _getbundletagsfnodes(
2654 def _getbundletagsfnodes(
2656 bundler,
2655 bundler,
2657 repo,
2656 repo,
2658 source,
2657 source,
2659 bundlecaps=None,
2658 bundlecaps=None,
2660 b2caps=None,
2659 b2caps=None,
2661 heads=None,
2660 heads=None,
2662 common=None,
2661 common=None,
2663 **kwargs
2662 **kwargs
2664 ):
2663 ):
2665 """Transfer the .hgtags filenodes mapping.
2664 """Transfer the .hgtags filenodes mapping.
2666
2665
2667 Only values for heads in this bundle will be transferred.
2666 Only values for heads in this bundle will be transferred.
2668
2667
2669 The part data consists of pairs of 20 byte changeset node and .hgtags
2668 The part data consists of pairs of 20 byte changeset node and .hgtags
2670 filenodes raw values.
2669 filenodes raw values.
2671 """
2670 """
2672 # Don't send unless:
2671 # Don't send unless:
2673 # - changeset are being exchanged,
2672 # - changeset are being exchanged,
2674 # - the client supports it.
2673 # - the client supports it.
2675 if not b2caps or not (kwargs.get('cg', True) and b'hgtagsfnodes' in b2caps):
2674 if not b2caps or not (kwargs.get('cg', True) and b'hgtagsfnodes' in b2caps):
2676 return
2675 return
2677
2676
2678 outgoing = _computeoutgoing(repo, heads, common)
2677 outgoing = _computeoutgoing(repo, heads, common)
2679 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
2678 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
2680
2679
2681
2680
2682 @getbundle2partsgenerator(b'cache:rev-branch-cache')
2681 @getbundle2partsgenerator(b'cache:rev-branch-cache')
2683 def _getbundlerevbranchcache(
2682 def _getbundlerevbranchcache(
2684 bundler,
2683 bundler,
2685 repo,
2684 repo,
2686 source,
2685 source,
2687 bundlecaps=None,
2686 bundlecaps=None,
2688 b2caps=None,
2687 b2caps=None,
2689 heads=None,
2688 heads=None,
2690 common=None,
2689 common=None,
2691 **kwargs
2690 **kwargs
2692 ):
2691 ):
2693 """Transfer the rev-branch-cache mapping
2692 """Transfer the rev-branch-cache mapping
2694
2693
2695 The payload is a series of data related to each branch
2694 The payload is a series of data related to each branch
2696
2695
2697 1) branch name length
2696 1) branch name length
2698 2) number of open heads
2697 2) number of open heads
2699 3) number of closed heads
2698 3) number of closed heads
2700 4) open heads nodes
2699 4) open heads nodes
2701 5) closed heads nodes
2700 5) closed heads nodes
2702 """
2701 """
2703 # Don't send unless:
2702 # Don't send unless:
2704 # - changeset are being exchanged,
2703 # - changeset are being exchanged,
2705 # - the client supports it.
2704 # - the client supports it.
2706 # - narrow bundle isn't in play (not currently compatible).
2705 # - narrow bundle isn't in play (not currently compatible).
2707 if (
2706 if (
2708 not kwargs.get('cg', True)
2707 not kwargs.get('cg', True)
2709 or not b2caps
2708 or not b2caps
2710 or b'rev-branch-cache' not in b2caps
2709 or b'rev-branch-cache' not in b2caps
2711 or kwargs.get('narrow', False)
2710 or kwargs.get('narrow', False)
2712 or repo.ui.has_section(_NARROWACL_SECTION)
2711 or repo.ui.has_section(_NARROWACL_SECTION)
2713 ):
2712 ):
2714 return
2713 return
2715
2714
2716 outgoing = _computeoutgoing(repo, heads, common)
2715 outgoing = _computeoutgoing(repo, heads, common)
2717 bundle2.addpartrevbranchcache(repo, bundler, outgoing)
2716 bundle2.addpartrevbranchcache(repo, bundler, outgoing)
2718
2717
2719
2718
2720 def check_heads(repo, their_heads, context):
2719 def check_heads(repo, their_heads, context):
2721 """check if the heads of a repo have been modified
2720 """check if the heads of a repo have been modified
2722
2721
2723 Used by peer for unbundling.
2722 Used by peer for unbundling.
2724 """
2723 """
2725 heads = repo.heads()
2724 heads = repo.heads()
2726 heads_hash = hashutil.sha1(b''.join(sorted(heads))).digest()
2725 heads_hash = hashutil.sha1(b''.join(sorted(heads))).digest()
2727 if not (
2726 if not (
2728 their_heads == [b'force']
2727 their_heads == [b'force']
2729 or their_heads == heads
2728 or their_heads == heads
2730 or their_heads == [b'hashed', heads_hash]
2729 or their_heads == [b'hashed', heads_hash]
2731 ):
2730 ):
2732 # someone else committed/pushed/unbundled while we
2731 # someone else committed/pushed/unbundled while we
2733 # were transferring data
2732 # were transferring data
2734 raise error.PushRaced(
2733 raise error.PushRaced(
2735 b'repository changed while %s - please try again' % context
2734 b'repository changed while %s - please try again' % context
2736 )
2735 )
2737
2736
2738
2737
2739 def unbundle(repo, cg, heads, source, url):
2738 def unbundle(repo, cg, heads, source, url):
2740 """Apply a bundle to a repo.
2739 """Apply a bundle to a repo.
2741
2740
2742 this function makes sure the repo is locked during the application and have
2741 this function makes sure the repo is locked during the application and have
2743 mechanism to check that no push race occurred between the creation of the
2742 mechanism to check that no push race occurred between the creation of the
2744 bundle and its application.
2743 bundle and its application.
2745
2744
2746 If the push was raced as PushRaced exception is raised."""
2745 If the push was raced as PushRaced exception is raised."""
2747 r = 0
2746 r = 0
2748 # need a transaction when processing a bundle2 stream
2747 # need a transaction when processing a bundle2 stream
2749 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
2748 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
2750 lockandtr = [None, None, None]
2749 lockandtr = [None, None, None]
2751 recordout = None
2750 recordout = None
2752 # quick fix for output mismatch with bundle2 in 3.4
2751 # quick fix for output mismatch with bundle2 in 3.4
2753 captureoutput = repo.ui.configbool(
2752 captureoutput = repo.ui.configbool(
2754 b'experimental', b'bundle2-output-capture'
2753 b'experimental', b'bundle2-output-capture'
2755 )
2754 )
2756 if url.startswith(b'remote:http:') or url.startswith(b'remote:https:'):
2755 if url.startswith(b'remote:http:') or url.startswith(b'remote:https:'):
2757 captureoutput = True
2756 captureoutput = True
2758 try:
2757 try:
2759 # note: outside bundle1, 'heads' is expected to be empty and this
2758 # note: outside bundle1, 'heads' is expected to be empty and this
2760 # 'check_heads' call wil be a no-op
2759 # 'check_heads' call wil be a no-op
2761 check_heads(repo, heads, b'uploading changes')
2760 check_heads(repo, heads, b'uploading changes')
2762 # push can proceed
2761 # push can proceed
2763 if not isinstance(cg, bundle2.unbundle20):
2762 if not isinstance(cg, bundle2.unbundle20):
2764 # legacy case: bundle1 (changegroup 01)
2763 # legacy case: bundle1 (changegroup 01)
2765 txnname = b"\n".join([source, urlutil.hidepassword(url)])
2764 txnname = b"\n".join([source, urlutil.hidepassword(url)])
2766 with repo.lock(), repo.transaction(txnname) as tr:
2765 with repo.lock(), repo.transaction(txnname) as tr:
2767 op = bundle2.applybundle(repo, cg, tr, source, url)
2766 op = bundle2.applybundle(repo, cg, tr, source, url)
2768 r = bundle2.combinechangegroupresults(op)
2767 r = bundle2.combinechangegroupresults(op)
2769 else:
2768 else:
2770 r = None
2769 r = None
2771 try:
2770 try:
2772
2771
2773 def gettransaction():
2772 def gettransaction():
2774 if not lockandtr[2]:
2773 if not lockandtr[2]:
2775 if not bookmod.bookmarksinstore(repo):
2774 if not bookmod.bookmarksinstore(repo):
2776 lockandtr[0] = repo.wlock()
2775 lockandtr[0] = repo.wlock()
2777 lockandtr[1] = repo.lock()
2776 lockandtr[1] = repo.lock()
2778 lockandtr[2] = repo.transaction(source)
2777 lockandtr[2] = repo.transaction(source)
2779 lockandtr[2].hookargs[b'source'] = source
2778 lockandtr[2].hookargs[b'source'] = source
2780 lockandtr[2].hookargs[b'url'] = url
2779 lockandtr[2].hookargs[b'url'] = url
2781 lockandtr[2].hookargs[b'bundle2'] = b'1'
2780 lockandtr[2].hookargs[b'bundle2'] = b'1'
2782 return lockandtr[2]
2781 return lockandtr[2]
2783
2782
2784 # Do greedy locking by default until we're satisfied with lazy
2783 # Do greedy locking by default until we're satisfied with lazy
2785 # locking.
2784 # locking.
2786 if not repo.ui.configbool(
2785 if not repo.ui.configbool(
2787 b'experimental', b'bundle2lazylocking'
2786 b'experimental', b'bundle2lazylocking'
2788 ):
2787 ):
2789 gettransaction()
2788 gettransaction()
2790
2789
2791 op = bundle2.bundleoperation(
2790 op = bundle2.bundleoperation(
2792 repo,
2791 repo,
2793 gettransaction,
2792 gettransaction,
2794 captureoutput=captureoutput,
2793 captureoutput=captureoutput,
2795 source=b'push',
2794 source=b'push',
2796 )
2795 )
2797 try:
2796 try:
2798 op = bundle2.processbundle(repo, cg, op=op)
2797 op = bundle2.processbundle(repo, cg, op=op)
2799 finally:
2798 finally:
2800 r = op.reply
2799 r = op.reply
2801 if captureoutput and r is not None:
2800 if captureoutput and r is not None:
2802 repo.ui.pushbuffer(error=True, subproc=True)
2801 repo.ui.pushbuffer(error=True, subproc=True)
2803
2802
2804 def recordout(output):
2803 def recordout(output):
2805 r.newpart(b'output', data=output, mandatory=False)
2804 r.newpart(b'output', data=output, mandatory=False)
2806
2805
2807 if lockandtr[2] is not None:
2806 if lockandtr[2] is not None:
2808 lockandtr[2].close()
2807 lockandtr[2].close()
2809 except BaseException as exc:
2808 except BaseException as exc:
2810 exc.duringunbundle2 = True
2809 exc.duringunbundle2 = True
2811 if captureoutput and r is not None:
2810 if captureoutput and r is not None:
2812 parts = exc._bundle2salvagedoutput = r.salvageoutput()
2811 parts = exc._bundle2salvagedoutput = r.salvageoutput()
2813
2812
2814 def recordout(output):
2813 def recordout(output):
2815 part = bundle2.bundlepart(
2814 part = bundle2.bundlepart(
2816 b'output', data=output, mandatory=False
2815 b'output', data=output, mandatory=False
2817 )
2816 )
2818 parts.append(part)
2817 parts.append(part)
2819
2818
2820 raise
2819 raise
2821 finally:
2820 finally:
2822 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
2821 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
2823 if recordout is not None:
2822 if recordout is not None:
2824 recordout(repo.ui.popbuffer())
2823 recordout(repo.ui.popbuffer())
2825 return r
2824 return r
2826
2825
2827
2826
2828 def _maybeapplyclonebundle(pullop):
2827 def _maybeapplyclonebundle(pullop):
2829 """Apply a clone bundle from a remote, if possible."""
2828 """Apply a clone bundle from a remote, if possible."""
2830
2829
2831 repo = pullop.repo
2830 repo = pullop.repo
2832 remote = pullop.remote
2831 remote = pullop.remote
2833
2832
2834 if not repo.ui.configbool(b'ui', b'clonebundles'):
2833 if not repo.ui.configbool(b'ui', b'clonebundles'):
2835 return
2834 return
2836
2835
2837 # Only run if local repo is empty.
2836 # Only run if local repo is empty.
2838 if len(repo):
2837 if len(repo):
2839 return
2838 return
2840
2839
2841 if pullop.heads:
2840 if pullop.heads:
2842 return
2841 return
2843
2842
2844 if not remote.capable(b'clonebundles'):
2843 if not remote.capable(b'clonebundles'):
2845 return
2844 return
2846
2845
2847 with remote.commandexecutor() as e:
2846 with remote.commandexecutor() as e:
2848 res = e.callcommand(b'clonebundles', {}).result()
2847 res = e.callcommand(b'clonebundles', {}).result()
2849
2848
2850 # If we call the wire protocol command, that's good enough to record the
2849 # If we call the wire protocol command, that's good enough to record the
2851 # attempt.
2850 # attempt.
2852 pullop.clonebundleattempted = True
2851 pullop.clonebundleattempted = True
2853
2852
2854 entries = bundlecaches.parseclonebundlesmanifest(repo, res)
2853 entries = bundlecaches.parseclonebundlesmanifest(repo, res)
2855 if not entries:
2854 if not entries:
2856 repo.ui.note(
2855 repo.ui.note(
2857 _(
2856 _(
2858 b'no clone bundles available on remote; '
2857 b'no clone bundles available on remote; '
2859 b'falling back to regular clone\n'
2858 b'falling back to regular clone\n'
2860 )
2859 )
2861 )
2860 )
2862 return
2861 return
2863
2862
2864 entries = bundlecaches.filterclonebundleentries(
2863 entries = bundlecaches.filterclonebundleentries(
2865 repo, entries, streamclonerequested=pullop.streamclonerequested
2864 repo, entries, streamclonerequested=pullop.streamclonerequested
2866 )
2865 )
2867
2866
2868 if not entries:
2867 if not entries:
2869 # There is a thundering herd concern here. However, if a server
2868 # There is a thundering herd concern here. However, if a server
2870 # operator doesn't advertise bundles appropriate for its clients,
2869 # operator doesn't advertise bundles appropriate for its clients,
2871 # they deserve what's coming. Furthermore, from a client's
2870 # they deserve what's coming. Furthermore, from a client's
2872 # perspective, no automatic fallback would mean not being able to
2871 # perspective, no automatic fallback would mean not being able to
2873 # clone!
2872 # clone!
2874 repo.ui.warn(
2873 repo.ui.warn(
2875 _(
2874 _(
2876 b'no compatible clone bundles available on server; '
2875 b'no compatible clone bundles available on server; '
2877 b'falling back to regular clone\n'
2876 b'falling back to regular clone\n'
2878 )
2877 )
2879 )
2878 )
2880 repo.ui.warn(
2879 repo.ui.warn(
2881 _(b'(you may want to report this to the server operator)\n')
2880 _(b'(you may want to report this to the server operator)\n')
2882 )
2881 )
2883 return
2882 return
2884
2883
2885 entries = bundlecaches.sortclonebundleentries(repo.ui, entries)
2884 entries = bundlecaches.sortclonebundleentries(repo.ui, entries)
2886
2885
2887 url = entries[0][b'URL']
2886 url = entries[0][b'URL']
2888 repo.ui.status(_(b'applying clone bundle from %s\n') % url)
2887 repo.ui.status(_(b'applying clone bundle from %s\n') % url)
2889 if trypullbundlefromurl(repo.ui, repo, url, remote):
2888 if trypullbundlefromurl(repo.ui, repo, url, remote):
2890 repo.ui.status(_(b'finished applying clone bundle\n'))
2889 repo.ui.status(_(b'finished applying clone bundle\n'))
2891 # Bundle failed.
2890 # Bundle failed.
2892 #
2891 #
2893 # We abort by default to avoid the thundering herd of
2892 # We abort by default to avoid the thundering herd of
2894 # clients flooding a server that was expecting expensive
2893 # clients flooding a server that was expecting expensive
2895 # clone load to be offloaded.
2894 # clone load to be offloaded.
2896 elif repo.ui.configbool(b'ui', b'clonebundlefallback'):
2895 elif repo.ui.configbool(b'ui', b'clonebundlefallback'):
2897 repo.ui.warn(_(b'falling back to normal clone\n'))
2896 repo.ui.warn(_(b'falling back to normal clone\n'))
2898 else:
2897 else:
2899 raise error.Abort(
2898 raise error.Abort(
2900 _(b'error applying bundle'),
2899 _(b'error applying bundle'),
2901 hint=_(
2900 hint=_(
2902 b'if this error persists, consider contacting '
2901 b'if this error persists, consider contacting '
2903 b'the server operator or disable clone '
2902 b'the server operator or disable clone '
2904 b'bundles via '
2903 b'bundles via '
2905 b'"--config ui.clonebundles=false"'
2904 b'"--config ui.clonebundles=false"'
2906 ),
2905 ),
2907 )
2906 )
2908
2907
2909
2908
2910 def inline_clone_bundle_open(ui, url, peer):
2909 def inline_clone_bundle_open(ui, url, peer):
2911 if not peer:
2910 if not peer:
2912 raise error.Abort(_(b'no remote repository supplied for %s' % url))
2911 raise error.Abort(_(b'no remote repository supplied for %s' % url))
2913 clonebundleid = url[len(bundlecaches.CLONEBUNDLESCHEME) :]
2912 clonebundleid = url[len(bundlecaches.CLONEBUNDLESCHEME) :]
2914 peerclonebundle = peer.get_cached_bundle_inline(clonebundleid)
2913 peerclonebundle = peer.get_cached_bundle_inline(clonebundleid)
2915 return util.chunkbuffer(peerclonebundle)
2914 return util.chunkbuffer(peerclonebundle)
2916
2915
2917
2916
2918 def trypullbundlefromurl(ui, repo, url, peer):
2917 def trypullbundlefromurl(ui, repo, url, peer):
2919 """Attempt to apply a bundle from a URL."""
2918 """Attempt to apply a bundle from a URL."""
2920 with repo.lock(), repo.transaction(b'bundleurl') as tr:
2919 with repo.lock(), repo.transaction(b'bundleurl') as tr:
2921 try:
2920 try:
2922 if url.startswith(bundlecaches.CLONEBUNDLESCHEME):
2921 if url.startswith(bundlecaches.CLONEBUNDLESCHEME):
2923 fh = inline_clone_bundle_open(ui, url, peer)
2922 fh = inline_clone_bundle_open(ui, url, peer)
2924 else:
2923 else:
2925 fh = urlmod.open(ui, url)
2924 fh = urlmod.open(ui, url)
2926 cg = readbundle(ui, fh, b'stream')
2925 cg = readbundle(ui, fh, b'stream')
2927
2926
2928 if isinstance(cg, streamclone.streamcloneapplier):
2927 if isinstance(cg, streamclone.streamcloneapplier):
2929 cg.apply(repo)
2928 cg.apply(repo)
2930 else:
2929 else:
2931 bundle2.applybundle(repo, cg, tr, b'clonebundles', url)
2930 bundle2.applybundle(repo, cg, tr, b'clonebundles', url)
2932 return True
2931 return True
2933 except urlerr.httperror as e:
2932 except urlerr.httperror as e:
2934 ui.warn(
2933 ui.warn(
2935 _(b'HTTP error fetching bundle: %s\n')
2934 _(b'HTTP error fetching bundle: %s\n')
2936 % stringutil.forcebytestr(e)
2935 % stringutil.forcebytestr(e)
2937 )
2936 )
2938 except urlerr.urlerror as e:
2937 except urlerr.urlerror as e:
2939 ui.warn(
2938 ui.warn(
2940 _(b'error fetching bundle: %s\n')
2939 _(b'error fetching bundle: %s\n')
2941 % stringutil.forcebytestr(e.reason)
2940 % stringutil.forcebytestr(e.reason)
2942 )
2941 )
2943
2942
2944 return False
2943 return False
@@ -1,1703 +1,1706 b''
1 # match.py - filename matching
1 # match.py - filename matching
2 #
2 #
3 # Copyright 2008, 2009 Olivia Mackall <olivia@selenic.com> and others
3 # Copyright 2008, 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
8
9 import bisect
9 import bisect
10 import copy
10 import copy
11 import itertools
11 import itertools
12 import os
12 import os
13 import re
13 import re
14
14
15 from .i18n import _
15 from .i18n import _
16 from .pycompat import open
16 from .pycompat import open
17 from . import (
17 from . import (
18 encoding,
18 encoding,
19 error,
19 error,
20 pathutil,
20 pathutil,
21 policy,
21 policy,
22 pycompat,
22 pycompat,
23 util,
23 util,
24 )
24 )
25 from .utils import stringutil
25 from .utils import stringutil
26
26
27 rustmod = policy.importrust('dirstate')
27 rustmod = policy.importrust('dirstate')
28
28
29 allpatternkinds = (
29 allpatternkinds = (
30 b're',
30 b're',
31 b'glob',
31 b'glob',
32 b'path',
32 b'path',
33 b'filepath',
33 b'filepath',
34 b'relglob',
34 b'relglob',
35 b'relpath',
35 b'relpath',
36 b'relre',
36 b'relre',
37 b'rootglob',
37 b'rootglob',
38 b'listfile',
38 b'listfile',
39 b'listfile0',
39 b'listfile0',
40 b'set',
40 b'set',
41 b'include',
41 b'include',
42 b'subinclude',
42 b'subinclude',
43 b'rootfilesin',
43 b'rootfilesin',
44 )
44 )
45 cwdrelativepatternkinds = (b'relpath', b'glob')
45 cwdrelativepatternkinds = (b'relpath', b'glob')
46
46
47 propertycache = util.propertycache
47 propertycache = util.propertycache
48
48
49
49
50 def _rematcher(regex):
50 def _rematcher(regex):
51 """compile the regexp with the best available regexp engine and return a
51 """compile the regexp with the best available regexp engine and return a
52 matcher function"""
52 matcher function"""
53 m = util.re.compile(regex)
53 m = util.re.compile(regex)
54 try:
54 try:
55 # slightly faster, provided by facebook's re2 bindings
55 # slightly faster, provided by facebook's re2 bindings
56 return m.test_match
56 return m.test_match
57 except AttributeError:
57 except AttributeError:
58 return m.match
58 return m.match
59
59
60
60
61 def _expandsets(cwd, kindpats, ctx=None, listsubrepos=False, badfn=None):
61 def _expandsets(cwd, kindpats, ctx=None, listsubrepos=False, badfn=None):
62 '''Returns the kindpats list with the 'set' patterns expanded to matchers'''
62 '''Returns the kindpats list with the 'set' patterns expanded to matchers'''
63 matchers = []
63 matchers = []
64 other = []
64 other = []
65
65
66 for kind, pat, source in kindpats:
66 for kind, pat, source in kindpats:
67 if kind == b'set':
67 if kind == b'set':
68 if ctx is None:
68 if ctx is None:
69 raise error.ProgrammingError(
69 raise error.ProgrammingError(
70 b"fileset expression with no context"
70 b"fileset expression with no context"
71 )
71 )
72 matchers.append(ctx.matchfileset(cwd, pat, badfn=badfn))
72 matchers.append(ctx.matchfileset(cwd, pat, badfn=badfn))
73
73
74 if listsubrepos:
74 if listsubrepos:
75 for subpath in ctx.substate:
75 for subpath in ctx.substate:
76 sm = ctx.sub(subpath).matchfileset(cwd, pat, badfn=badfn)
76 sm = ctx.sub(subpath).matchfileset(cwd, pat, badfn=badfn)
77 pm = prefixdirmatcher(subpath, sm, badfn=badfn)
77 pm = prefixdirmatcher(subpath, sm, badfn=badfn)
78 matchers.append(pm)
78 matchers.append(pm)
79
79
80 continue
80 continue
81 other.append((kind, pat, source))
81 other.append((kind, pat, source))
82 return matchers, other
82 return matchers, other
83
83
84
84
85 def _expandsubinclude(kindpats, root):
85 def _expandsubinclude(kindpats, root):
86 """Returns the list of subinclude matcher args and the kindpats without the
86 """Returns the list of subinclude matcher args and the kindpats without the
87 subincludes in it."""
87 subincludes in it."""
88 relmatchers = []
88 relmatchers = []
89 other = []
89 other = []
90
90
91 for kind, pat, source in kindpats:
91 for kind, pat, source in kindpats:
92 if kind == b'subinclude':
92 if kind == b'subinclude':
93 sourceroot = pathutil.dirname(util.normpath(source))
93 sourceroot = pathutil.dirname(util.normpath(source))
94 pat = util.pconvert(pat)
94 pat = util.pconvert(pat)
95 path = pathutil.join(sourceroot, pat)
95 path = pathutil.join(sourceroot, pat)
96
96
97 newroot = pathutil.dirname(path)
97 newroot = pathutil.dirname(path)
98 matcherargs = (newroot, b'', [], [b'include:%s' % path])
98 matcherargs = (newroot, b'', [], [b'include:%s' % path])
99
99
100 prefix = pathutil.canonpath(root, root, newroot)
100 prefix = pathutil.canonpath(root, root, newroot)
101 if prefix:
101 if prefix:
102 prefix += b'/'
102 prefix += b'/'
103 relmatchers.append((prefix, matcherargs))
103 relmatchers.append((prefix, matcherargs))
104 else:
104 else:
105 other.append((kind, pat, source))
105 other.append((kind, pat, source))
106
106
107 return relmatchers, other
107 return relmatchers, other
108
108
109
109
110 def _kindpatsalwaysmatch(kindpats):
110 def _kindpatsalwaysmatch(kindpats):
111 """Checks whether the kindspats match everything, as e.g.
111 """Checks whether the kindspats match everything, as e.g.
112 'relpath:.' does.
112 'relpath:.' does.
113 """
113 """
114 for kind, pat, source in kindpats:
114 for kind, pat, source in kindpats:
115 if pat != b'' or kind not in [b'relpath', b'glob']:
115 if pat != b'' or kind not in [b'relpath', b'glob']:
116 return False
116 return False
117 return True
117 return True
118
118
119
119
120 def _buildkindpatsmatcher(
120 def _buildkindpatsmatcher(
121 matchercls,
121 matchercls,
122 root,
122 root,
123 cwd,
123 cwd,
124 kindpats,
124 kindpats,
125 ctx=None,
125 ctx=None,
126 listsubrepos=False,
126 listsubrepos=False,
127 badfn=None,
127 badfn=None,
128 ):
128 ):
129 matchers = []
129 matchers = []
130 fms, kindpats = _expandsets(
130 fms, kindpats = _expandsets(
131 cwd,
131 cwd,
132 kindpats,
132 kindpats,
133 ctx=ctx,
133 ctx=ctx,
134 listsubrepos=listsubrepos,
134 listsubrepos=listsubrepos,
135 badfn=badfn,
135 badfn=badfn,
136 )
136 )
137 if kindpats:
137 if kindpats:
138 m = matchercls(root, kindpats, badfn=badfn)
138 m = matchercls(root, kindpats, badfn=badfn)
139 matchers.append(m)
139 matchers.append(m)
140 if fms:
140 if fms:
141 matchers.extend(fms)
141 matchers.extend(fms)
142 if not matchers:
142 if not matchers:
143 return nevermatcher(badfn=badfn)
143 return nevermatcher(badfn=badfn)
144 if len(matchers) == 1:
144 if len(matchers) == 1:
145 return matchers[0]
145 return matchers[0]
146 return unionmatcher(matchers)
146 return unionmatcher(matchers)
147
147
148
148
149 def match(
149 def match(
150 root,
150 root,
151 cwd,
151 cwd,
152 patterns=None,
152 patterns=None,
153 include=None,
153 include=None,
154 exclude=None,
154 exclude=None,
155 default=b'glob',
155 default=b'glob',
156 auditor=None,
156 auditor=None,
157 ctx=None,
157 ctx=None,
158 listsubrepos=False,
158 listsubrepos=False,
159 warn=None,
159 warn=None,
160 badfn=None,
160 badfn=None,
161 icasefs=False,
161 icasefs=False,
162 ):
162 ):
163 r"""build an object to match a set of file patterns
163 r"""build an object to match a set of file patterns
164
164
165 arguments:
165 arguments:
166 root - the canonical root of the tree you're matching against
166 root - the canonical root of the tree you're matching against
167 cwd - the current working directory, if relevant
167 cwd - the current working directory, if relevant
168 patterns - patterns to find
168 patterns - patterns to find
169 include - patterns to include (unless they are excluded)
169 include - patterns to include (unless they are excluded)
170 exclude - patterns to exclude (even if they are included)
170 exclude - patterns to exclude (even if they are included)
171 default - if a pattern in patterns has no explicit type, assume this one
171 default - if a pattern in patterns has no explicit type, assume this one
172 auditor - optional path auditor
172 auditor - optional path auditor
173 ctx - optional changecontext
173 ctx - optional changecontext
174 listsubrepos - if True, recurse into subrepositories
174 listsubrepos - if True, recurse into subrepositories
175 warn - optional function used for printing warnings
175 warn - optional function used for printing warnings
176 badfn - optional bad() callback for this matcher instead of the default
176 badfn - optional bad() callback for this matcher instead of the default
177 icasefs - make a matcher for wdir on case insensitive filesystems, which
177 icasefs - make a matcher for wdir on case insensitive filesystems, which
178 normalizes the given patterns to the case in the filesystem
178 normalizes the given patterns to the case in the filesystem
179
179
180 a pattern is one of:
180 a pattern is one of:
181 'glob:<glob>' - a glob relative to cwd
181 'glob:<glob>' - a glob relative to cwd
182 're:<regexp>' - a regular expression
182 're:<regexp>' - a regular expression
183 'path:<path>' - a path relative to repository root, which is matched
183 'path:<path>' - a path relative to repository root, which is matched
184 recursively
184 recursively
185 'filepath:<path>' - an exact path to a single file, relative to the
185 'filepath:<path>' - an exact path to a single file, relative to the
186 repository root
186 repository root
187 'rootfilesin:<path>' - a path relative to repository root, which is
187 'rootfilesin:<path>' - a path relative to repository root, which is
188 matched non-recursively (will not match subdirectories)
188 matched non-recursively (will not match subdirectories)
189 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
189 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
190 'relpath:<path>' - a path relative to cwd
190 'relpath:<path>' - a path relative to cwd
191 'relre:<regexp>' - a regexp that needn't match the start of a name
191 'relre:<regexp>' - a regexp that needn't match the start of a name
192 'set:<fileset>' - a fileset expression
192 'set:<fileset>' - a fileset expression
193 'include:<path>' - a file of patterns to read and include
193 'include:<path>' - a file of patterns to read and include
194 'subinclude:<path>' - a file of patterns to match against files under
194 'subinclude:<path>' - a file of patterns to match against files under
195 the same directory
195 the same directory
196 '<something>' - a pattern of the specified default type
196 '<something>' - a pattern of the specified default type
197
197
198 >>> def _match(root, *args, **kwargs):
198 >>> def _match(root, *args, **kwargs):
199 ... return match(util.localpath(root), *args, **kwargs)
199 ... return match(util.localpath(root), *args, **kwargs)
200
200
201 Usually a patternmatcher is returned:
201 Usually a patternmatcher is returned:
202 >>> _match(b'/foo', b'.', [br're:.*\.c$', b'path:foo/a', b'*.py'])
202 >>> _match(b'/foo', b'.', [br're:.*\.c$', b'path:foo/a', b'*.py'])
203 <patternmatcher patterns='[^/]*\\.py$|foo/a(?:/|$)|.*\\.c$'>
203 <patternmatcher patterns='[^/]*\\.py$|foo/a(?:/|$)|.*\\.c$'>
204
204
205 Combining 'patterns' with 'include' (resp. 'exclude') gives an
205 Combining 'patterns' with 'include' (resp. 'exclude') gives an
206 intersectionmatcher (resp. a differencematcher):
206 intersectionmatcher (resp. a differencematcher):
207 >>> type(_match(b'/foo', b'.', [br're:.*\.c$'], include=[b'path:lib']))
207 >>> type(_match(b'/foo', b'.', [br're:.*\.c$'], include=[b'path:lib']))
208 <class 'mercurial.match.intersectionmatcher'>
208 <class 'mercurial.match.intersectionmatcher'>
209 >>> type(_match(b'/foo', b'.', [br're:.*\.c$'], exclude=[b'path:build']))
209 >>> type(_match(b'/foo', b'.', [br're:.*\.c$'], exclude=[b'path:build']))
210 <class 'mercurial.match.differencematcher'>
210 <class 'mercurial.match.differencematcher'>
211
211
212 Notice that, if 'patterns' is empty, an alwaysmatcher is returned:
212 Notice that, if 'patterns' is empty, an alwaysmatcher is returned:
213 >>> _match(b'/foo', b'.', [])
213 >>> _match(b'/foo', b'.', [])
214 <alwaysmatcher>
214 <alwaysmatcher>
215
215
216 The 'default' argument determines which kind of pattern is assumed if a
216 The 'default' argument determines which kind of pattern is assumed if a
217 pattern has no prefix:
217 pattern has no prefix:
218 >>> _match(b'/foo', b'.', [br'.*\.c$'], default=b're')
218 >>> _match(b'/foo', b'.', [br'.*\.c$'], default=b're')
219 <patternmatcher patterns='.*\\.c$'>
219 <patternmatcher patterns='.*\\.c$'>
220 >>> _match(b'/foo', b'.', [b'main.py'], default=b'relpath')
220 >>> _match(b'/foo', b'.', [b'main.py'], default=b'relpath')
221 <patternmatcher patterns='main\\.py(?:/|$)'>
221 <patternmatcher patterns='main\\.py(?:/|$)'>
222 >>> _match(b'/foo', b'.', [b'main.py'], default=b're')
222 >>> _match(b'/foo', b'.', [b'main.py'], default=b're')
223 <patternmatcher patterns='main.py'>
223 <patternmatcher patterns='main.py'>
224
224
225 The primary use of matchers is to check whether a value (usually a file
225 The primary use of matchers is to check whether a value (usually a file
226 name) matches againset one of the patterns given at initialization. There
226 name) matches againset one of the patterns given at initialization. There
227 are two ways of doing this check.
227 are two ways of doing this check.
228
228
229 >>> m = _match(b'/foo', b'', [br're:.*\.c$', b'relpath:a'])
229 >>> m = _match(b'/foo', b'', [br're:.*\.c$', b'relpath:a'])
230
230
231 1. Calling the matcher with a file name returns True if any pattern
231 1. Calling the matcher with a file name returns True if any pattern
232 matches that file name:
232 matches that file name:
233 >>> m(b'a')
233 >>> m(b'a')
234 True
234 True
235 >>> m(b'main.c')
235 >>> m(b'main.c')
236 True
236 True
237 >>> m(b'test.py')
237 >>> m(b'test.py')
238 False
238 False
239
239
240 2. Using the exact() method only returns True if the file name matches one
240 2. Using the exact() method only returns True if the file name matches one
241 of the exact patterns (i.e. not re: or glob: patterns):
241 of the exact patterns (i.e. not re: or glob: patterns):
242 >>> m.exact(b'a')
242 >>> m.exact(b'a')
243 True
243 True
244 >>> m.exact(b'main.c')
244 >>> m.exact(b'main.c')
245 False
245 False
246 """
246 """
247 assert os.path.isabs(root)
247 assert os.path.isabs(root)
248 cwd = os.path.join(root, util.localpath(cwd))
248 cwd = os.path.join(root, util.localpath(cwd))
249 normalize = _donormalize
249 normalize = _donormalize
250 if icasefs:
250 if icasefs:
251 dirstate = ctx.repo().dirstate
251 dirstate = ctx.repo().dirstate
252 dsnormalize = dirstate.normalize
252 dsnormalize = dirstate.normalize
253
253
254 def normalize(patterns, default, root, cwd, auditor, warn):
254 def normalize(patterns, default, root, cwd, auditor, warn):
255 kp = _donormalize(patterns, default, root, cwd, auditor, warn)
255 kp = _donormalize(patterns, default, root, cwd, auditor, warn)
256 kindpats = []
256 kindpats = []
257 for kind, pats, source in kp:
257 for kind, pats, source in kp:
258 if kind not in (b're', b'relre'): # regex can't be normalized
258 if kind not in (b're', b'relre'): # regex can't be normalized
259 p = pats
259 p = pats
260 pats = dsnormalize(pats)
260 pats = dsnormalize(pats)
261
261
262 # Preserve the original to handle a case only rename.
262 # Preserve the original to handle a case only rename.
263 if p != pats and p in dirstate:
263 if p != pats and p in dirstate:
264 kindpats.append((kind, p, source))
264 kindpats.append((kind, p, source))
265
265
266 kindpats.append((kind, pats, source))
266 kindpats.append((kind, pats, source))
267 return kindpats
267 return kindpats
268
268
269 if patterns:
269 if patterns:
270 kindpats = normalize(patterns, default, root, cwd, auditor, warn)
270 kindpats = normalize(patterns, default, root, cwd, auditor, warn)
271 if _kindpatsalwaysmatch(kindpats):
271 if _kindpatsalwaysmatch(kindpats):
272 m = alwaysmatcher(badfn)
272 m = alwaysmatcher(badfn)
273 else:
273 else:
274 m = _buildkindpatsmatcher(
274 m = _buildkindpatsmatcher(
275 patternmatcher,
275 patternmatcher,
276 root,
276 root,
277 cwd,
277 cwd,
278 kindpats,
278 kindpats,
279 ctx=ctx,
279 ctx=ctx,
280 listsubrepos=listsubrepos,
280 listsubrepos=listsubrepos,
281 badfn=badfn,
281 badfn=badfn,
282 )
282 )
283 else:
283 else:
284 # It's a little strange that no patterns means to match everything.
284 # It's a little strange that no patterns means to match everything.
285 # Consider changing this to match nothing (probably using nevermatcher).
285 # Consider changing this to match nothing (probably using nevermatcher).
286 m = alwaysmatcher(badfn)
286 m = alwaysmatcher(badfn)
287
287
288 if include:
288 if include:
289 kindpats = normalize(include, b'glob', root, cwd, auditor, warn)
289 kindpats = normalize(include, b'glob', root, cwd, auditor, warn)
290 im = _buildkindpatsmatcher(
290 im = _buildkindpatsmatcher(
291 includematcher,
291 includematcher,
292 root,
292 root,
293 cwd,
293 cwd,
294 kindpats,
294 kindpats,
295 ctx=ctx,
295 ctx=ctx,
296 listsubrepos=listsubrepos,
296 listsubrepos=listsubrepos,
297 badfn=None,
297 badfn=None,
298 )
298 )
299 m = intersectmatchers(m, im)
299 m = intersectmatchers(m, im)
300 if exclude:
300 if exclude:
301 kindpats = normalize(exclude, b'glob', root, cwd, auditor, warn)
301 kindpats = normalize(exclude, b'glob', root, cwd, auditor, warn)
302 em = _buildkindpatsmatcher(
302 em = _buildkindpatsmatcher(
303 includematcher,
303 includematcher,
304 root,
304 root,
305 cwd,
305 cwd,
306 kindpats,
306 kindpats,
307 ctx=ctx,
307 ctx=ctx,
308 listsubrepos=listsubrepos,
308 listsubrepos=listsubrepos,
309 badfn=None,
309 badfn=None,
310 )
310 )
311 m = differencematcher(m, em)
311 m = differencematcher(m, em)
312 return m
312 return m
313
313
314
314
315 def exact(files, badfn=None):
315 def exact(files, badfn=None):
316 return exactmatcher(files, badfn=badfn)
316 return exactmatcher(files, badfn=badfn)
317
317
318
318
319 def always(badfn=None):
319 def always(badfn=None):
320 return alwaysmatcher(badfn)
320 return alwaysmatcher(badfn)
321
321
322
322
323 def never(badfn=None):
323 def never(badfn=None):
324 return nevermatcher(badfn)
324 return nevermatcher(badfn)
325
325
326
326
327 def badmatch(match, badfn):
327 def badmatch(match, badfn):
328 """Make a copy of the given matcher, replacing its bad method with the given
328 """Make a copy of the given matcher, replacing its bad method with the given
329 one.
329 one.
330 """
330 """
331 m = copy.copy(match)
331 m = copy.copy(match)
332 m.bad = badfn
332 m.bad = badfn
333 return m
333 return m
334
334
335
335
336 def _donormalize(patterns, default, root, cwd, auditor=None, warn=None):
336 def _donormalize(patterns, default, root, cwd, auditor=None, warn=None):
337 """Convert 'kind:pat' from the patterns list to tuples with kind and
337 """Convert 'kind:pat' from the patterns list to tuples with kind and
338 normalized and rooted patterns and with listfiles expanded."""
338 normalized and rooted patterns and with listfiles expanded."""
339 kindpats = []
339 kindpats = []
340 kinds_to_normalize = (
340 kinds_to_normalize = (
341 b'relglob',
341 b'relglob',
342 b'path',
342 b'path',
343 b'filepath',
343 b'filepath',
344 b'rootfilesin',
344 b'rootfilesin',
345 b'rootglob',
345 b'rootglob',
346 )
346 )
347
347
348 for kind, pat in [_patsplit(p, default) for p in patterns]:
348 for kind, pat in [_patsplit(p, default) for p in patterns]:
349 if kind in cwdrelativepatternkinds:
349 if kind in cwdrelativepatternkinds:
350 pat = pathutil.canonpath(root, cwd, pat, auditor=auditor)
350 pat = pathutil.canonpath(root, cwd, pat, auditor=auditor)
351 elif kind in kinds_to_normalize:
351 elif kind in kinds_to_normalize:
352 pat = util.normpath(pat)
352 pat = util.normpath(pat)
353 elif kind in (b'listfile', b'listfile0'):
353 elif kind in (b'listfile', b'listfile0'):
354 try:
354 try:
355 files = util.readfile(pat)
355 files = util.readfile(pat)
356 if kind == b'listfile0':
356 if kind == b'listfile0':
357 files = files.split(b'\0')
357 files = files.split(b'\0')
358 else:
358 else:
359 files = files.splitlines()
359 files = files.splitlines()
360 files = [f for f in files if f]
360 files = [f for f in files if f]
361 except EnvironmentError:
361 except EnvironmentError:
362 raise error.Abort(_(b"unable to read file list (%s)") % pat)
362 raise error.Abort(_(b"unable to read file list (%s)") % pat)
363 for k, p, source in _donormalize(
363 for k, p, source in _donormalize(
364 files, default, root, cwd, auditor, warn
364 files, default, root, cwd, auditor, warn
365 ):
365 ):
366 kindpats.append((k, p, pat))
366 kindpats.append((k, p, pat))
367 continue
367 continue
368 elif kind == b'include':
368 elif kind == b'include':
369 try:
369 try:
370 fullpath = os.path.join(root, util.localpath(pat))
370 fullpath = os.path.join(root, util.localpath(pat))
371 includepats = readpatternfile(fullpath, warn)
371 includepats = readpatternfile(fullpath, warn)
372 for k, p, source in _donormalize(
372 for k, p, source in _donormalize(
373 includepats, default, root, cwd, auditor, warn
373 includepats, default, root, cwd, auditor, warn
374 ):
374 ):
375 kindpats.append((k, p, source or pat))
375 kindpats.append((k, p, source or pat))
376 except error.Abort as inst:
376 except error.Abort as inst:
377 raise error.Abort(
377 raise error.Abort(
378 b'%s: %s'
378 b'%s: %s'
379 % (
379 % (
380 pat,
380 pat,
381 inst.message,
381 inst.message,
382 )
382 )
383 )
383 )
384 except IOError as inst:
384 except IOError as inst:
385 if warn:
385 if warn:
386 warn(
386 warn(
387 _(b"skipping unreadable pattern file '%s': %s\n")
387 _(b"skipping unreadable pattern file '%s': %s\n")
388 % (pat, stringutil.forcebytestr(inst.strerror))
388 % (pat, stringutil.forcebytestr(inst.strerror))
389 )
389 )
390 continue
390 continue
391 # else: re or relre - which cannot be normalized
391 # else: re or relre - which cannot be normalized
392 kindpats.append((kind, pat, b''))
392 kindpats.append((kind, pat, b''))
393 return kindpats
393 return kindpats
394
394
395
395
396 class basematcher:
396 class basematcher:
397 def __init__(self, badfn=None):
397 def __init__(self, badfn=None):
398 if badfn is not None:
398 if badfn is not None:
399 self.bad = badfn
399 self.bad = badfn
400
400
401 def __call__(self, fn):
401 def __call__(self, fn):
402 return self.matchfn(fn)
402 return self.matchfn(fn)
403
403
404 # Callbacks related to how the matcher is used by dirstate.walk.
404 # Callbacks related to how the matcher is used by dirstate.walk.
405 # Subscribers to these events must monkeypatch the matcher object.
405 # Subscribers to these events must monkeypatch the matcher object.
406 def bad(self, f, msg):
406 def bad(self, f, msg):
407 """Callback from dirstate.walk for each explicit file that can't be
407 """Callback from dirstate.walk for each explicit file that can't be
408 found/accessed, with an error message."""
408 found/accessed, with an error message."""
409
409
410 # If an traversedir is set, it will be called when a directory discovered
410 # If an traversedir is set, it will be called when a directory discovered
411 # by recursive traversal is visited.
411 # by recursive traversal is visited.
412 traversedir = None
412 traversedir = None
413
413
414 @propertycache
414 @propertycache
415 def _files(self):
415 def _files(self):
416 return []
416 return []
417
417
418 def files(self):
418 def files(self):
419 """Explicitly listed files or patterns or roots:
419 """Explicitly listed files or patterns or roots:
420 if no patterns or .always(): empty list,
420 if no patterns or .always(): empty list,
421 if exact: list exact files,
421 if exact: list exact files,
422 if not .anypats(): list all files and dirs,
422 if not .anypats(): list all files and dirs,
423 else: optimal roots"""
423 else: optimal roots"""
424 return self._files
424 return self._files
425
425
426 @propertycache
426 @propertycache
427 def _fileset(self):
427 def _fileset(self):
428 return set(self._files)
428 return set(self._files)
429
429
430 def exact(self, f):
430 def exact(self, f):
431 '''Returns True if f is in .files().'''
431 '''Returns True if f is in .files().'''
432 return f in self._fileset
432 return f in self._fileset
433
433
434 def matchfn(self, f):
434 def matchfn(self, f):
435 return False
435 return False
436
436
437 def visitdir(self, dir):
437 def visitdir(self, dir):
438 """Decides whether a directory should be visited based on whether it
438 """Decides whether a directory should be visited based on whether it
439 has potential matches in it or one of its subdirectories. This is
439 has potential matches in it or one of its subdirectories. This is
440 based on the match's primary, included, and excluded patterns.
440 based on the match's primary, included, and excluded patterns.
441
441
442 Returns the string 'all' if the given directory and all subdirectories
442 Returns the string 'all' if the given directory and all subdirectories
443 should be visited. Otherwise returns True or False indicating whether
443 should be visited. Otherwise returns True or False indicating whether
444 the given directory should be visited.
444 the given directory should be visited.
445 """
445 """
446 return True
446 return True
447
447
448 def visitchildrenset(self, dir):
448 def visitchildrenset(self, dir):
449 """Decides whether a directory should be visited based on whether it
449 """Decides whether a directory should be visited based on whether it
450 has potential matches in it or one of its subdirectories, and
450 has potential matches in it or one of its subdirectories, and
451 potentially lists which subdirectories of that directory should be
451 potentially lists which subdirectories of that directory should be
452 visited. This is based on the match's primary, included, and excluded
452 visited. This is based on the match's primary, included, and excluded
453 patterns.
453 patterns.
454
454
455 This function is very similar to 'visitdir', and the following mapping
455 This function is very similar to 'visitdir', and the following mapping
456 can be applied:
456 can be applied:
457
457
458 visitdir | visitchildrenlist
458 visitdir | visitchildrenlist
459 ----------+-------------------
459 ----------+-------------------
460 False | set()
460 False | set()
461 'all' | 'all'
461 'all' | 'all'
462 True | 'this' OR non-empty set of subdirs -or files- to visit
462 True | 'this' OR non-empty set of subdirs -or files- to visit
463
463
464 Example:
464 Example:
465 Assume matchers ['path:foo/bar', 'rootfilesin:qux'], we would return
465 Assume matchers ['path:foo/bar', 'rootfilesin:qux'], we would return
466 the following values (assuming the implementation of visitchildrenset
466 the following values (assuming the implementation of visitchildrenset
467 is capable of recognizing this; some implementations are not).
467 is capable of recognizing this; some implementations are not).
468
468
469 '' -> {'foo', 'qux'}
469 '' -> {'foo', 'qux'}
470 'baz' -> set()
470 'baz' -> set()
471 'foo' -> {'bar'}
471 'foo' -> {'bar'}
472 # Ideally this would be 'all', but since the prefix nature of matchers
472 # Ideally this would be 'all', but since the prefix nature of matchers
473 # is applied to the entire matcher, we have to downgrade this to
473 # is applied to the entire matcher, we have to downgrade this to
474 # 'this' due to the non-prefix 'rootfilesin'-kind matcher being mixed
474 # 'this' due to the non-prefix 'rootfilesin'-kind matcher being mixed
475 # in.
475 # in.
476 'foo/bar' -> 'this'
476 'foo/bar' -> 'this'
477 'qux' -> 'this'
477 'qux' -> 'this'
478
478
479 Important:
479 Important:
480 Most matchers do not know if they're representing files or
480 Most matchers do not know if they're representing files or
481 directories. They see ['path:dir/f'] and don't know whether 'f' is a
481 directories. They see ['path:dir/f'] and don't know whether 'f' is a
482 file or a directory, so visitchildrenset('dir') for most matchers will
482 file or a directory, so visitchildrenset('dir') for most matchers will
483 return {'f'}, but if the matcher knows it's a file (like exactmatcher
483 return {'f'}, but if the matcher knows it's a file (like exactmatcher
484 does), it may return 'this'. Do not rely on the return being a set
484 does), it may return 'this'. Do not rely on the return being a set
485 indicating that there are no files in this dir to investigate (or
485 indicating that there are no files in this dir to investigate (or
486 equivalently that if there are files to investigate in 'dir' that it
486 equivalently that if there are files to investigate in 'dir' that it
487 will always return 'this').
487 will always return 'this').
488 """
488 """
489 return b'this'
489 return b'this'
490
490
491 def always(self):
491 def always(self):
492 """Matcher will match everything and .files() will be empty --
492 """Matcher will match everything and .files() will be empty --
493 optimization might be possible."""
493 optimization might be possible."""
494 return False
494 return False
495
495
496 def isexact(self):
496 def isexact(self):
497 """Matcher will match exactly the list of files in .files() --
497 """Matcher will match exactly the list of files in .files() --
498 optimization might be possible."""
498 optimization might be possible."""
499 return False
499 return False
500
500
501 def prefix(self):
501 def prefix(self):
502 """Matcher will match the paths in .files() recursively --
502 """Matcher will match the paths in .files() recursively --
503 optimization might be possible."""
503 optimization might be possible."""
504 return False
504 return False
505
505
506 def anypats(self):
506 def anypats(self):
507 """None of .always(), .isexact(), and .prefix() is true --
507 """None of .always(), .isexact(), and .prefix() is true --
508 optimizations will be difficult."""
508 optimizations will be difficult."""
509 return not self.always() and not self.isexact() and not self.prefix()
509 return not self.always() and not self.isexact() and not self.prefix()
510
510
511
511
512 class alwaysmatcher(basematcher):
512 class alwaysmatcher(basematcher):
513 '''Matches everything.'''
513 '''Matches everything.'''
514
514
515 def __init__(self, badfn=None):
515 def __init__(self, badfn=None):
516 super(alwaysmatcher, self).__init__(badfn)
516 super(alwaysmatcher, self).__init__(badfn)
517
517
518 def always(self):
518 def always(self):
519 return True
519 return True
520
520
521 def matchfn(self, f):
521 def matchfn(self, f):
522 return True
522 return True
523
523
524 def visitdir(self, dir):
524 def visitdir(self, dir):
525 return b'all'
525 return b'all'
526
526
527 def visitchildrenset(self, dir):
527 def visitchildrenset(self, dir):
528 return b'all'
528 return b'all'
529
529
530 def __repr__(self):
530 def __repr__(self):
531 return r'<alwaysmatcher>'
531 return r'<alwaysmatcher>'
532
532
533
533
534 class nevermatcher(basematcher):
534 class nevermatcher(basematcher):
535 '''Matches nothing.'''
535 '''Matches nothing.'''
536
536
537 def __init__(self, badfn=None):
537 def __init__(self, badfn=None):
538 super(nevermatcher, self).__init__(badfn)
538 super(nevermatcher, self).__init__(badfn)
539
539
540 # It's a little weird to say that the nevermatcher is an exact matcher
540 # It's a little weird to say that the nevermatcher is an exact matcher
541 # or a prefix matcher, but it seems to make sense to let callers take
541 # or a prefix matcher, but it seems to make sense to let callers take
542 # fast paths based on either. There will be no exact matches, nor any
542 # fast paths based on either. There will be no exact matches, nor any
543 # prefixes (files() returns []), so fast paths iterating over them should
543 # prefixes (files() returns []), so fast paths iterating over them should
544 # be efficient (and correct).
544 # be efficient (and correct).
545 def isexact(self):
545 def isexact(self):
546 return True
546 return True
547
547
548 def prefix(self):
548 def prefix(self):
549 return True
549 return True
550
550
551 def visitdir(self, dir):
551 def visitdir(self, dir):
552 return False
552 return False
553
553
554 def visitchildrenset(self, dir):
554 def visitchildrenset(self, dir):
555 return set()
555 return set()
556
556
557 def __repr__(self):
557 def __repr__(self):
558 return r'<nevermatcher>'
558 return r'<nevermatcher>'
559
559
560
560
561 class predicatematcher(basematcher):
561 class predicatematcher(basematcher):
562 """A matcher adapter for a simple boolean function"""
562 """A matcher adapter for a simple boolean function"""
563
563
564 def __init__(self, predfn, predrepr=None, badfn=None):
564 def __init__(self, predfn, predrepr=None, badfn=None):
565 super(predicatematcher, self).__init__(badfn)
565 super(predicatematcher, self).__init__(badfn)
566 self.matchfn = predfn
566 self.matchfn = predfn
567 self._predrepr = predrepr
567 self._predrepr = predrepr
568
568
569 @encoding.strmethod
569 @encoding.strmethod
570 def __repr__(self):
570 def __repr__(self):
571 s = stringutil.buildrepr(self._predrepr) or pycompat.byterepr(
571 s = stringutil.buildrepr(self._predrepr) or pycompat.byterepr(
572 self.matchfn
572 self.matchfn
573 )
573 )
574 return b'<predicatenmatcher pred=%s>' % s
574 return b'<predicatenmatcher pred=%s>' % s
575
575
576
576
577 def path_or_parents_in_set(path, prefix_set):
577 def path_or_parents_in_set(path, prefix_set):
578 """Returns True if `path` (or any parent of `path`) is in `prefix_set`."""
578 """Returns True if `path` (or any parent of `path`) is in `prefix_set`."""
579 l = len(prefix_set)
579 l = len(prefix_set)
580 if l == 0:
580 if l == 0:
581 return False
581 return False
582 if path in prefix_set:
582 if path in prefix_set:
583 return True
583 return True
584 # If there's more than 5 paths in prefix_set, it's *probably* quicker to
584 # If there's more than 5 paths in prefix_set, it's *probably* quicker to
585 # "walk up" the directory hierarchy instead, with the assumption that most
585 # "walk up" the directory hierarchy instead, with the assumption that most
586 # directory hierarchies are relatively shallow and hash lookup is cheap.
586 # directory hierarchies are relatively shallow and hash lookup is cheap.
587 if l > 5:
587 if l > 5:
588 return any(
588 return any(
589 parentdir in prefix_set for parentdir in pathutil.finddirs(path)
589 parentdir in prefix_set for parentdir in pathutil.finddirs(path)
590 )
590 )
591
591
592 # FIXME: Ideally we'd never get to this point if this is the case - we'd
592 # FIXME: Ideally we'd never get to this point if this is the case - we'd
593 # recognize ourselves as an 'always' matcher and skip this.
593 # recognize ourselves as an 'always' matcher and skip this.
594 if b'' in prefix_set:
594 if b'' in prefix_set:
595 return True
595 return True
596
596
597 sl = ord(b'/')
597 sl = ord(b'/')
598
598
599 # We already checked that path isn't in prefix_set exactly, so
599 # We already checked that path isn't in prefix_set exactly, so
600 # `path[len(pf)] should never raise IndexError.
600 # `path[len(pf)] should never raise IndexError.
601 return any(path.startswith(pf) and path[len(pf)] == sl for pf in prefix_set)
601 return any(path.startswith(pf) and path[len(pf)] == sl for pf in prefix_set)
602
602
603
603
604 class patternmatcher(basematcher):
604 class patternmatcher(basematcher):
605 r"""Matches a set of (kind, pat, source) against a 'root' directory.
605 r"""Matches a set of (kind, pat, source) against a 'root' directory.
606
606
607 >>> kindpats = [
607 >>> kindpats = [
608 ... (b're', br'.*\.c$', b''),
608 ... (b're', br'.*\.c$', b''),
609 ... (b'path', b'foo/a', b''),
609 ... (b'path', b'foo/a', b''),
610 ... (b'relpath', b'b', b''),
610 ... (b'relpath', b'b', b''),
611 ... (b'glob', b'*.h', b''),
611 ... (b'glob', b'*.h', b''),
612 ... ]
612 ... ]
613 >>> m = patternmatcher(b'foo', kindpats)
613 >>> m = patternmatcher(b'foo', kindpats)
614 >>> m(b'main.c') # matches re:.*\.c$
614 >>> m(b'main.c') # matches re:.*\.c$
615 True
615 True
616 >>> m(b'b.txt')
616 >>> m(b'b.txt')
617 False
617 False
618 >>> m(b'foo/a') # matches path:foo/a
618 >>> m(b'foo/a') # matches path:foo/a
619 True
619 True
620 >>> m(b'a') # does not match path:b, since 'root' is 'foo'
620 >>> m(b'a') # does not match path:b, since 'root' is 'foo'
621 False
621 False
622 >>> m(b'b') # matches relpath:b, since 'root' is 'foo'
622 >>> m(b'b') # matches relpath:b, since 'root' is 'foo'
623 True
623 True
624 >>> m(b'lib.h') # matches glob:*.h
624 >>> m(b'lib.h') # matches glob:*.h
625 True
625 True
626
626
627 >>> m.files()
627 >>> m.files()
628 [b'', b'foo/a', b'', b'b']
628 [b'', b'foo/a', b'', b'b']
629 >>> m.exact(b'foo/a')
629 >>> m.exact(b'foo/a')
630 True
630 True
631 >>> m.exact(b'b')
631 >>> m.exact(b'b')
632 True
632 True
633 >>> m.exact(b'lib.h') # exact matches are for (rel)path kinds
633 >>> m.exact(b'lib.h') # exact matches are for (rel)path kinds
634 False
634 False
635 """
635 """
636
636
637 def __init__(self, root, kindpats, badfn=None):
637 def __init__(self, root, kindpats, badfn=None):
638 super(patternmatcher, self).__init__(badfn)
638 super(patternmatcher, self).__init__(badfn)
639 kindpats.sort()
639 kindpats.sort()
640
640
641 roots, dirs, parents = _rootsdirsandparents(kindpats)
641 self._files = _explicitfiles(kindpats)
642 self._files = _explicitfiles(kindpats)
643 self._dirs_explicit = set(dirs)
644 self._dirs = parents
642 self._prefix = _prefix(kindpats)
645 self._prefix = _prefix(kindpats)
643 self._pats, self._matchfn = _buildmatch(kindpats, b'$', root)
646 self._pats, self._matchfn = _buildmatch(kindpats, b'$', root)
644
647
645 def matchfn(self, fn):
648 def matchfn(self, fn):
646 if fn in self._fileset:
649 if fn in self._fileset:
647 return True
650 return True
648 return self._matchfn(fn)
651 return self._matchfn(fn)
649
652
650 @propertycache
651 def _dirs(self):
652 return set(pathutil.dirs(self._fileset))
653
654 def visitdir(self, dir):
653 def visitdir(self, dir):
655 if self._prefix and dir in self._fileset:
654 if self._prefix and dir in self._fileset:
656 return b'all'
655 return b'all'
657 return dir in self._dirs or path_or_parents_in_set(dir, self._fileset)
656 return (
657 dir in self._dirs
658 or path_or_parents_in_set(dir, self._fileset)
659 or path_or_parents_in_set(dir, self._dirs_explicit)
660 )
658
661
659 def visitchildrenset(self, dir):
662 def visitchildrenset(self, dir):
660 ret = self.visitdir(dir)
663 ret = self.visitdir(dir)
661 if ret is True:
664 if ret is True:
662 return b'this'
665 return b'this'
663 elif not ret:
666 elif not ret:
664 return set()
667 return set()
665 assert ret == b'all'
668 assert ret == b'all'
666 return b'all'
669 return b'all'
667
670
668 def prefix(self):
671 def prefix(self):
669 return self._prefix
672 return self._prefix
670
673
671 @encoding.strmethod
674 @encoding.strmethod
672 def __repr__(self):
675 def __repr__(self):
673 return b'<patternmatcher patterns=%r>' % pycompat.bytestr(self._pats)
676 return b'<patternmatcher patterns=%r>' % pycompat.bytestr(self._pats)
674
677
675
678
676 # This is basically a reimplementation of pathutil.dirs that stores the
679 # This is basically a reimplementation of pathutil.dirs that stores the
677 # children instead of just a count of them, plus a small optional optimization
680 # children instead of just a count of them, plus a small optional optimization
678 # to avoid some directories we don't need.
681 # to avoid some directories we don't need.
679 class _dirchildren:
682 class _dirchildren:
680 def __init__(self, paths, onlyinclude=None):
683 def __init__(self, paths, onlyinclude=None):
681 self._dirs = {}
684 self._dirs = {}
682 self._onlyinclude = onlyinclude or []
685 self._onlyinclude = onlyinclude or []
683 addpath = self.addpath
686 addpath = self.addpath
684 for f in paths:
687 for f in paths:
685 addpath(f)
688 addpath(f)
686
689
687 def addpath(self, path):
690 def addpath(self, path):
688 if path == b'':
691 if path == b'':
689 return
692 return
690 dirs = self._dirs
693 dirs = self._dirs
691 findsplitdirs = _dirchildren._findsplitdirs
694 findsplitdirs = _dirchildren._findsplitdirs
692 for d, b in findsplitdirs(path):
695 for d, b in findsplitdirs(path):
693 if d not in self._onlyinclude:
696 if d not in self._onlyinclude:
694 continue
697 continue
695 dirs.setdefault(d, set()).add(b)
698 dirs.setdefault(d, set()).add(b)
696
699
697 @staticmethod
700 @staticmethod
698 def _findsplitdirs(path):
701 def _findsplitdirs(path):
699 # yields (dirname, basename) tuples, walking back to the root. This is
702 # yields (dirname, basename) tuples, walking back to the root. This is
700 # very similar to pathutil.finddirs, except:
703 # very similar to pathutil.finddirs, except:
701 # - produces a (dirname, basename) tuple, not just 'dirname'
704 # - produces a (dirname, basename) tuple, not just 'dirname'
702 # Unlike manifest._splittopdir, this does not suffix `dirname` with a
705 # Unlike manifest._splittopdir, this does not suffix `dirname` with a
703 # slash.
706 # slash.
704 oldpos = len(path)
707 oldpos = len(path)
705 pos = path.rfind(b'/')
708 pos = path.rfind(b'/')
706 while pos != -1:
709 while pos != -1:
707 yield path[:pos], path[pos + 1 : oldpos]
710 yield path[:pos], path[pos + 1 : oldpos]
708 oldpos = pos
711 oldpos = pos
709 pos = path.rfind(b'/', 0, pos)
712 pos = path.rfind(b'/', 0, pos)
710 yield b'', path[:oldpos]
713 yield b'', path[:oldpos]
711
714
712 def get(self, path):
715 def get(self, path):
713 return self._dirs.get(path, set())
716 return self._dirs.get(path, set())
714
717
715
718
716 class includematcher(basematcher):
719 class includematcher(basematcher):
717 def __init__(self, root, kindpats, badfn=None):
720 def __init__(self, root, kindpats, badfn=None):
718 super(includematcher, self).__init__(badfn)
721 super(includematcher, self).__init__(badfn)
719 if rustmod is not None:
722 if rustmod is not None:
720 # We need to pass the patterns to Rust because they can contain
723 # We need to pass the patterns to Rust because they can contain
721 # patterns from the user interface
724 # patterns from the user interface
722 self._kindpats = kindpats
725 self._kindpats = kindpats
723 self._pats, self.matchfn = _buildmatch(kindpats, b'(?:/|$)', root)
726 self._pats, self.matchfn = _buildmatch(kindpats, b'(?:/|$)', root)
724 self._prefix = _prefix(kindpats)
727 self._prefix = _prefix(kindpats)
725 roots, dirs, parents = _rootsdirsandparents(kindpats)
728 roots, dirs, parents = _rootsdirsandparents(kindpats)
726 # roots are directories which are recursively included.
729 # roots are directories which are recursively included.
727 self._roots = set(roots)
730 self._roots = set(roots)
728 # dirs are directories which are non-recursively included.
731 # dirs are directories which are non-recursively included.
729 self._dirs = set(dirs)
732 self._dirs = set(dirs)
730 # parents are directories which are non-recursively included because
733 # parents are directories which are non-recursively included because
731 # they are needed to get to items in _dirs or _roots.
734 # they are needed to get to items in _dirs or _roots.
732 self._parents = parents
735 self._parents = parents
733
736
734 def visitdir(self, dir):
737 def visitdir(self, dir):
735 if self._prefix and dir in self._roots:
738 if self._prefix and dir in self._roots:
736 return b'all'
739 return b'all'
737 return (
740 return (
738 dir in self._dirs
741 dir in self._dirs
739 or dir in self._parents
742 or dir in self._parents
740 or path_or_parents_in_set(dir, self._roots)
743 or path_or_parents_in_set(dir, self._roots)
741 )
744 )
742
745
743 @propertycache
746 @propertycache
744 def _allparentschildren(self):
747 def _allparentschildren(self):
745 # It may seem odd that we add dirs, roots, and parents, and then
748 # It may seem odd that we add dirs, roots, and parents, and then
746 # restrict to only parents. This is to catch the case of:
749 # restrict to only parents. This is to catch the case of:
747 # dirs = ['foo/bar']
750 # dirs = ['foo/bar']
748 # parents = ['foo']
751 # parents = ['foo']
749 # if we asked for the children of 'foo', but had only added
752 # if we asked for the children of 'foo', but had only added
750 # self._parents, we wouldn't be able to respond ['bar'].
753 # self._parents, we wouldn't be able to respond ['bar'].
751 return _dirchildren(
754 return _dirchildren(
752 itertools.chain(self._dirs, self._roots, self._parents),
755 itertools.chain(self._dirs, self._roots, self._parents),
753 onlyinclude=self._parents,
756 onlyinclude=self._parents,
754 )
757 )
755
758
756 def visitchildrenset(self, dir):
759 def visitchildrenset(self, dir):
757 if self._prefix and dir in self._roots:
760 if self._prefix and dir in self._roots:
758 return b'all'
761 return b'all'
759 # Note: this does *not* include the 'dir in self._parents' case from
762 # Note: this does *not* include the 'dir in self._parents' case from
760 # visitdir, that's handled below.
763 # visitdir, that's handled below.
761 if (
764 if (
762 b'' in self._roots
765 b'' in self._roots
763 or dir in self._dirs
766 or dir in self._dirs
764 or path_or_parents_in_set(dir, self._roots)
767 or path_or_parents_in_set(dir, self._roots)
765 ):
768 ):
766 return b'this'
769 return b'this'
767
770
768 if dir in self._parents:
771 if dir in self._parents:
769 return self._allparentschildren.get(dir) or set()
772 return self._allparentschildren.get(dir) or set()
770 return set()
773 return set()
771
774
772 @encoding.strmethod
775 @encoding.strmethod
773 def __repr__(self):
776 def __repr__(self):
774 return b'<includematcher includes=%r>' % pycompat.bytestr(self._pats)
777 return b'<includematcher includes=%r>' % pycompat.bytestr(self._pats)
775
778
776
779
777 class exactmatcher(basematcher):
780 class exactmatcher(basematcher):
778 r"""Matches the input files exactly. They are interpreted as paths, not
781 r"""Matches the input files exactly. They are interpreted as paths, not
779 patterns (so no kind-prefixes).
782 patterns (so no kind-prefixes).
780
783
781 >>> m = exactmatcher([b'a.txt', br're:.*\.c$'])
784 >>> m = exactmatcher([b'a.txt', br're:.*\.c$'])
782 >>> m(b'a.txt')
785 >>> m(b'a.txt')
783 True
786 True
784 >>> m(b'b.txt')
787 >>> m(b'b.txt')
785 False
788 False
786
789
787 Input files that would be matched are exactly those returned by .files()
790 Input files that would be matched are exactly those returned by .files()
788 >>> m.files()
791 >>> m.files()
789 ['a.txt', 're:.*\\.c$']
792 ['a.txt', 're:.*\\.c$']
790
793
791 So pattern 're:.*\.c$' is not considered as a regex, but as a file name
794 So pattern 're:.*\.c$' is not considered as a regex, but as a file name
792 >>> m(b'main.c')
795 >>> m(b'main.c')
793 False
796 False
794 >>> m(br're:.*\.c$')
797 >>> m(br're:.*\.c$')
795 True
798 True
796 """
799 """
797
800
798 def __init__(self, files, badfn=None):
801 def __init__(self, files, badfn=None):
799 super(exactmatcher, self).__init__(badfn)
802 super(exactmatcher, self).__init__(badfn)
800
803
801 if isinstance(files, list):
804 if isinstance(files, list):
802 self._files = files
805 self._files = files
803 else:
806 else:
804 self._files = list(files)
807 self._files = list(files)
805
808
806 matchfn = basematcher.exact
809 matchfn = basematcher.exact
807
810
808 @propertycache
811 @propertycache
809 def _dirs(self):
812 def _dirs(self):
810 return set(pathutil.dirs(self._fileset))
813 return set(pathutil.dirs(self._fileset))
811
814
812 def visitdir(self, dir):
815 def visitdir(self, dir):
813 return dir in self._dirs
816 return dir in self._dirs
814
817
815 @propertycache
818 @propertycache
816 def _visitchildrenset_candidates(self):
819 def _visitchildrenset_candidates(self):
817 """A memoized set of candidates for visitchildrenset."""
820 """A memoized set of candidates for visitchildrenset."""
818 return self._fileset | self._dirs - {b''}
821 return self._fileset | self._dirs - {b''}
819
822
820 @propertycache
823 @propertycache
821 def _sorted_visitchildrenset_candidates(self):
824 def _sorted_visitchildrenset_candidates(self):
822 """A memoized sorted list of candidates for visitchildrenset."""
825 """A memoized sorted list of candidates for visitchildrenset."""
823 return sorted(self._visitchildrenset_candidates)
826 return sorted(self._visitchildrenset_candidates)
824
827
825 def visitchildrenset(self, dir):
828 def visitchildrenset(self, dir):
826 if not self._fileset or dir not in self._dirs:
829 if not self._fileset or dir not in self._dirs:
827 return set()
830 return set()
828
831
829 if dir == b'':
832 if dir == b'':
830 candidates = self._visitchildrenset_candidates
833 candidates = self._visitchildrenset_candidates
831 else:
834 else:
832 candidates = self._sorted_visitchildrenset_candidates
835 candidates = self._sorted_visitchildrenset_candidates
833 d = dir + b'/'
836 d = dir + b'/'
834 # Use bisect to find the first element potentially starting with d
837 # Use bisect to find the first element potentially starting with d
835 # (i.e. >= d). This should always find at least one element (we'll
838 # (i.e. >= d). This should always find at least one element (we'll
836 # assert later if this is not the case).
839 # assert later if this is not the case).
837 first = bisect.bisect_left(candidates, d)
840 first = bisect.bisect_left(candidates, d)
838 # We need a representation of the first element that is > d that
841 # We need a representation of the first element that is > d that
839 # does not start with d, so since we added a `/` on the end of dir,
842 # does not start with d, so since we added a `/` on the end of dir,
840 # we'll add whatever comes after slash (we could probably assume
843 # we'll add whatever comes after slash (we could probably assume
841 # that `0` is after `/`, but let's not) to the end of dir instead.
844 # that `0` is after `/`, but let's not) to the end of dir instead.
842 dnext = dir + encoding.strtolocal(chr(ord(b'/') + 1))
845 dnext = dir + encoding.strtolocal(chr(ord(b'/') + 1))
843 # Use bisect to find the first element >= d_next
846 # Use bisect to find the first element >= d_next
844 last = bisect.bisect_left(candidates, dnext, lo=first)
847 last = bisect.bisect_left(candidates, dnext, lo=first)
845 dlen = len(d)
848 dlen = len(d)
846 candidates = {c[dlen:] for c in candidates[first:last]}
849 candidates = {c[dlen:] for c in candidates[first:last]}
847 # self._dirs includes all of the directories, recursively, so if
850 # self._dirs includes all of the directories, recursively, so if
848 # we're attempting to match foo/bar/baz.txt, it'll have '', 'foo',
851 # we're attempting to match foo/bar/baz.txt, it'll have '', 'foo',
849 # 'foo/bar' in it. Thus we can safely ignore a candidate that has a
852 # 'foo/bar' in it. Thus we can safely ignore a candidate that has a
850 # '/' in it, indicating a it's for a subdir-of-a-subdir; the
853 # '/' in it, indicating a it's for a subdir-of-a-subdir; the
851 # immediate subdir will be in there without a slash.
854 # immediate subdir will be in there without a slash.
852 ret = {c for c in candidates if b'/' not in c}
855 ret = {c for c in candidates if b'/' not in c}
853 # We really do not expect ret to be empty, since that would imply that
856 # We really do not expect ret to be empty, since that would imply that
854 # there's something in _dirs that didn't have a file in _fileset.
857 # there's something in _dirs that didn't have a file in _fileset.
855 assert ret
858 assert ret
856 return ret
859 return ret
857
860
858 def isexact(self):
861 def isexact(self):
859 return True
862 return True
860
863
861 @encoding.strmethod
864 @encoding.strmethod
862 def __repr__(self):
865 def __repr__(self):
863 return b'<exactmatcher files=%r>' % self._files
866 return b'<exactmatcher files=%r>' % self._files
864
867
865
868
866 class differencematcher(basematcher):
869 class differencematcher(basematcher):
867 """Composes two matchers by matching if the first matches and the second
870 """Composes two matchers by matching if the first matches and the second
868 does not.
871 does not.
869
872
870 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
873 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
871 """
874 """
872
875
873 def __init__(self, m1, m2):
876 def __init__(self, m1, m2):
874 super(differencematcher, self).__init__()
877 super(differencematcher, self).__init__()
875 self._m1 = m1
878 self._m1 = m1
876 self._m2 = m2
879 self._m2 = m2
877 self.bad = m1.bad
880 self.bad = m1.bad
878 self.traversedir = m1.traversedir
881 self.traversedir = m1.traversedir
879
882
880 def matchfn(self, f):
883 def matchfn(self, f):
881 return self._m1(f) and not self._m2(f)
884 return self._m1(f) and not self._m2(f)
882
885
883 @propertycache
886 @propertycache
884 def _files(self):
887 def _files(self):
885 if self.isexact():
888 if self.isexact():
886 return [f for f in self._m1.files() if self(f)]
889 return [f for f in self._m1.files() if self(f)]
887 # If m1 is not an exact matcher, we can't easily figure out the set of
890 # If m1 is not an exact matcher, we can't easily figure out the set of
888 # files, because its files() are not always files. For example, if
891 # files, because its files() are not always files. For example, if
889 # m1 is "path:dir" and m2 is "rootfileins:.", we don't
892 # m1 is "path:dir" and m2 is "rootfileins:.", we don't
890 # want to remove "dir" from the set even though it would match m2,
893 # want to remove "dir" from the set even though it would match m2,
891 # because the "dir" in m1 may not be a file.
894 # because the "dir" in m1 may not be a file.
892 return self._m1.files()
895 return self._m1.files()
893
896
894 def visitdir(self, dir):
897 def visitdir(self, dir):
895 if self._m2.visitdir(dir) == b'all':
898 if self._m2.visitdir(dir) == b'all':
896 return False
899 return False
897 elif not self._m2.visitdir(dir):
900 elif not self._m2.visitdir(dir):
898 # m2 does not match dir, we can return 'all' here if possible
901 # m2 does not match dir, we can return 'all' here if possible
899 return self._m1.visitdir(dir)
902 return self._m1.visitdir(dir)
900 return bool(self._m1.visitdir(dir))
903 return bool(self._m1.visitdir(dir))
901
904
902 def visitchildrenset(self, dir):
905 def visitchildrenset(self, dir):
903 m2_set = self._m2.visitchildrenset(dir)
906 m2_set = self._m2.visitchildrenset(dir)
904 if m2_set == b'all':
907 if m2_set == b'all':
905 return set()
908 return set()
906 m1_set = self._m1.visitchildrenset(dir)
909 m1_set = self._m1.visitchildrenset(dir)
907 # Possible values for m1: 'all', 'this', set(...), set()
910 # Possible values for m1: 'all', 'this', set(...), set()
908 # Possible values for m2: 'this', set(...), set()
911 # Possible values for m2: 'this', set(...), set()
909 # If m2 has nothing under here that we care about, return m1, even if
912 # If m2 has nothing under here that we care about, return m1, even if
910 # it's 'all'. This is a change in behavior from visitdir, which would
913 # it's 'all'. This is a change in behavior from visitdir, which would
911 # return True, not 'all', for some reason.
914 # return True, not 'all', for some reason.
912 if not m2_set:
915 if not m2_set:
913 return m1_set
916 return m1_set
914 if m1_set in [b'all', b'this']:
917 if m1_set in [b'all', b'this']:
915 # Never return 'all' here if m2_set is any kind of non-empty (either
918 # Never return 'all' here if m2_set is any kind of non-empty (either
916 # 'this' or set(foo)), since m2 might return set() for a
919 # 'this' or set(foo)), since m2 might return set() for a
917 # subdirectory.
920 # subdirectory.
918 return b'this'
921 return b'this'
919 # Possible values for m1: set(...), set()
922 # Possible values for m1: set(...), set()
920 # Possible values for m2: 'this', set(...)
923 # Possible values for m2: 'this', set(...)
921 # We ignore m2's set results. They're possibly incorrect:
924 # We ignore m2's set results. They're possibly incorrect:
922 # m1 = path:dir/subdir, m2=rootfilesin:dir, visitchildrenset(''):
925 # m1 = path:dir/subdir, m2=rootfilesin:dir, visitchildrenset(''):
923 # m1 returns {'dir'}, m2 returns {'dir'}, if we subtracted we'd
926 # m1 returns {'dir'}, m2 returns {'dir'}, if we subtracted we'd
924 # return set(), which is *not* correct, we still need to visit 'dir'!
927 # return set(), which is *not* correct, we still need to visit 'dir'!
925 return m1_set
928 return m1_set
926
929
927 def isexact(self):
930 def isexact(self):
928 return self._m1.isexact()
931 return self._m1.isexact()
929
932
930 @encoding.strmethod
933 @encoding.strmethod
931 def __repr__(self):
934 def __repr__(self):
932 return b'<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2)
935 return b'<differencematcher m1=%r, m2=%r>' % (self._m1, self._m2)
933
936
934
937
935 def intersectmatchers(m1, m2):
938 def intersectmatchers(m1, m2):
936 """Composes two matchers by matching if both of them match.
939 """Composes two matchers by matching if both of them match.
937
940
938 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
941 The second matcher's non-matching-attributes (bad, traversedir) are ignored.
939 """
942 """
940 if m1 is None or m2 is None:
943 if m1 is None or m2 is None:
941 return m1 or m2
944 return m1 or m2
942 if m1.always():
945 if m1.always():
943 m = copy.copy(m2)
946 m = copy.copy(m2)
944 # TODO: Consider encapsulating these things in a class so there's only
947 # TODO: Consider encapsulating these things in a class so there's only
945 # one thing to copy from m1.
948 # one thing to copy from m1.
946 m.bad = m1.bad
949 m.bad = m1.bad
947 m.traversedir = m1.traversedir
950 m.traversedir = m1.traversedir
948 return m
951 return m
949 if m2.always():
952 if m2.always():
950 m = copy.copy(m1)
953 m = copy.copy(m1)
951 return m
954 return m
952 return intersectionmatcher(m1, m2)
955 return intersectionmatcher(m1, m2)
953
956
954
957
955 class intersectionmatcher(basematcher):
958 class intersectionmatcher(basematcher):
956 def __init__(self, m1, m2):
959 def __init__(self, m1, m2):
957 super(intersectionmatcher, self).__init__()
960 super(intersectionmatcher, self).__init__()
958 self._m1 = m1
961 self._m1 = m1
959 self._m2 = m2
962 self._m2 = m2
960 self.bad = m1.bad
963 self.bad = m1.bad
961 self.traversedir = m1.traversedir
964 self.traversedir = m1.traversedir
962
965
963 @propertycache
966 @propertycache
964 def _files(self):
967 def _files(self):
965 if self.isexact():
968 if self.isexact():
966 m1, m2 = self._m1, self._m2
969 m1, m2 = self._m1, self._m2
967 if not m1.isexact():
970 if not m1.isexact():
968 m1, m2 = m2, m1
971 m1, m2 = m2, m1
969 return [f for f in m1.files() if m2(f)]
972 return [f for f in m1.files() if m2(f)]
970 # It neither m1 nor m2 is an exact matcher, we can't easily intersect
973 # It neither m1 nor m2 is an exact matcher, we can't easily intersect
971 # the set of files, because their files() are not always files. For
974 # the set of files, because their files() are not always files. For
972 # example, if intersecting a matcher "-I glob:foo.txt" with matcher of
975 # example, if intersecting a matcher "-I glob:foo.txt" with matcher of
973 # "path:dir2", we don't want to remove "dir2" from the set.
976 # "path:dir2", we don't want to remove "dir2" from the set.
974 return self._m1.files() + self._m2.files()
977 return self._m1.files() + self._m2.files()
975
978
976 def matchfn(self, f):
979 def matchfn(self, f):
977 return self._m1(f) and self._m2(f)
980 return self._m1(f) and self._m2(f)
978
981
979 def visitdir(self, dir):
982 def visitdir(self, dir):
980 visit1 = self._m1.visitdir(dir)
983 visit1 = self._m1.visitdir(dir)
981 if visit1 == b'all':
984 if visit1 == b'all':
982 return self._m2.visitdir(dir)
985 return self._m2.visitdir(dir)
983 # bool() because visit1=True + visit2='all' should not be 'all'
986 # bool() because visit1=True + visit2='all' should not be 'all'
984 return bool(visit1 and self._m2.visitdir(dir))
987 return bool(visit1 and self._m2.visitdir(dir))
985
988
986 def visitchildrenset(self, dir):
989 def visitchildrenset(self, dir):
987 m1_set = self._m1.visitchildrenset(dir)
990 m1_set = self._m1.visitchildrenset(dir)
988 if not m1_set:
991 if not m1_set:
989 return set()
992 return set()
990 m2_set = self._m2.visitchildrenset(dir)
993 m2_set = self._m2.visitchildrenset(dir)
991 if not m2_set:
994 if not m2_set:
992 return set()
995 return set()
993
996
994 if m1_set == b'all':
997 if m1_set == b'all':
995 return m2_set
998 return m2_set
996 elif m2_set == b'all':
999 elif m2_set == b'all':
997 return m1_set
1000 return m1_set
998
1001
999 if m1_set == b'this' or m2_set == b'this':
1002 if m1_set == b'this' or m2_set == b'this':
1000 return b'this'
1003 return b'this'
1001
1004
1002 assert isinstance(m1_set, set) and isinstance(m2_set, set)
1005 assert isinstance(m1_set, set) and isinstance(m2_set, set)
1003 return m1_set.intersection(m2_set)
1006 return m1_set.intersection(m2_set)
1004
1007
1005 def always(self):
1008 def always(self):
1006 return self._m1.always() and self._m2.always()
1009 return self._m1.always() and self._m2.always()
1007
1010
1008 def isexact(self):
1011 def isexact(self):
1009 return self._m1.isexact() or self._m2.isexact()
1012 return self._m1.isexact() or self._m2.isexact()
1010
1013
1011 @encoding.strmethod
1014 @encoding.strmethod
1012 def __repr__(self):
1015 def __repr__(self):
1013 return b'<intersectionmatcher m1=%r, m2=%r>' % (self._m1, self._m2)
1016 return b'<intersectionmatcher m1=%r, m2=%r>' % (self._m1, self._m2)
1014
1017
1015
1018
1016 class subdirmatcher(basematcher):
1019 class subdirmatcher(basematcher):
1017 """Adapt a matcher to work on a subdirectory only.
1020 """Adapt a matcher to work on a subdirectory only.
1018
1021
1019 The paths are remapped to remove/insert the path as needed:
1022 The paths are remapped to remove/insert the path as needed:
1020
1023
1021 >>> from . import pycompat
1024 >>> from . import pycompat
1022 >>> m1 = match(util.localpath(b'/root'), b'', [b'a.txt', b'sub/b.txt'], auditor=lambda name: None)
1025 >>> m1 = match(util.localpath(b'/root'), b'', [b'a.txt', b'sub/b.txt'], auditor=lambda name: None)
1023 >>> m2 = subdirmatcher(b'sub', m1)
1026 >>> m2 = subdirmatcher(b'sub', m1)
1024 >>> m2(b'a.txt')
1027 >>> m2(b'a.txt')
1025 False
1028 False
1026 >>> m2(b'b.txt')
1029 >>> m2(b'b.txt')
1027 True
1030 True
1028 >>> m2.matchfn(b'a.txt')
1031 >>> m2.matchfn(b'a.txt')
1029 False
1032 False
1030 >>> m2.matchfn(b'b.txt')
1033 >>> m2.matchfn(b'b.txt')
1031 True
1034 True
1032 >>> m2.files()
1035 >>> m2.files()
1033 ['b.txt']
1036 ['b.txt']
1034 >>> m2.exact(b'b.txt')
1037 >>> m2.exact(b'b.txt')
1035 True
1038 True
1036 >>> def bad(f, msg):
1039 >>> def bad(f, msg):
1037 ... print(pycompat.sysstr(b"%s: %s" % (f, msg)))
1040 ... print(pycompat.sysstr(b"%s: %s" % (f, msg)))
1038 >>> m1.bad = bad
1041 >>> m1.bad = bad
1039 >>> m2.bad(b'x.txt', b'No such file')
1042 >>> m2.bad(b'x.txt', b'No such file')
1040 sub/x.txt: No such file
1043 sub/x.txt: No such file
1041 """
1044 """
1042
1045
1043 def __init__(self, path, matcher):
1046 def __init__(self, path, matcher):
1044 super(subdirmatcher, self).__init__()
1047 super(subdirmatcher, self).__init__()
1045 self._path = path
1048 self._path = path
1046 self._matcher = matcher
1049 self._matcher = matcher
1047 self._always = matcher.always()
1050 self._always = matcher.always()
1048
1051
1049 self._files = [
1052 self._files = [
1050 f[len(path) + 1 :]
1053 f[len(path) + 1 :]
1051 for f in matcher._files
1054 for f in matcher._files
1052 if f.startswith(path + b"/")
1055 if f.startswith(path + b"/")
1053 ]
1056 ]
1054
1057
1055 # If the parent repo had a path to this subrepo and the matcher is
1058 # If the parent repo had a path to this subrepo and the matcher is
1056 # a prefix matcher, this submatcher always matches.
1059 # a prefix matcher, this submatcher always matches.
1057 if matcher.prefix():
1060 if matcher.prefix():
1058 self._always = any(f == path for f in matcher._files)
1061 self._always = any(f == path for f in matcher._files)
1059
1062
1060 def bad(self, f, msg):
1063 def bad(self, f, msg):
1061 self._matcher.bad(self._path + b"/" + f, msg)
1064 self._matcher.bad(self._path + b"/" + f, msg)
1062
1065
1063 def matchfn(self, f):
1066 def matchfn(self, f):
1064 # Some information is lost in the superclass's constructor, so we
1067 # Some information is lost in the superclass's constructor, so we
1065 # can not accurately create the matching function for the subdirectory
1068 # can not accurately create the matching function for the subdirectory
1066 # from the inputs. Instead, we override matchfn() and visitdir() to
1069 # from the inputs. Instead, we override matchfn() and visitdir() to
1067 # call the original matcher with the subdirectory path prepended.
1070 # call the original matcher with the subdirectory path prepended.
1068 return self._matcher.matchfn(self._path + b"/" + f)
1071 return self._matcher.matchfn(self._path + b"/" + f)
1069
1072
1070 def visitdir(self, dir):
1073 def visitdir(self, dir):
1071 if dir == b'':
1074 if dir == b'':
1072 dir = self._path
1075 dir = self._path
1073 else:
1076 else:
1074 dir = self._path + b"/" + dir
1077 dir = self._path + b"/" + dir
1075 return self._matcher.visitdir(dir)
1078 return self._matcher.visitdir(dir)
1076
1079
1077 def visitchildrenset(self, dir):
1080 def visitchildrenset(self, dir):
1078 if dir == b'':
1081 if dir == b'':
1079 dir = self._path
1082 dir = self._path
1080 else:
1083 else:
1081 dir = self._path + b"/" + dir
1084 dir = self._path + b"/" + dir
1082 return self._matcher.visitchildrenset(dir)
1085 return self._matcher.visitchildrenset(dir)
1083
1086
1084 def always(self):
1087 def always(self):
1085 return self._always
1088 return self._always
1086
1089
1087 def prefix(self):
1090 def prefix(self):
1088 return self._matcher.prefix() and not self._always
1091 return self._matcher.prefix() and not self._always
1089
1092
1090 @encoding.strmethod
1093 @encoding.strmethod
1091 def __repr__(self):
1094 def __repr__(self):
1092 return b'<subdirmatcher path=%r, matcher=%r>' % (
1095 return b'<subdirmatcher path=%r, matcher=%r>' % (
1093 self._path,
1096 self._path,
1094 self._matcher,
1097 self._matcher,
1095 )
1098 )
1096
1099
1097
1100
1098 class prefixdirmatcher(basematcher):
1101 class prefixdirmatcher(basematcher):
1099 """Adapt a matcher to work on a parent directory.
1102 """Adapt a matcher to work on a parent directory.
1100
1103
1101 The matcher's non-matching-attributes (bad, traversedir) are ignored.
1104 The matcher's non-matching-attributes (bad, traversedir) are ignored.
1102
1105
1103 The prefix path should usually be the relative path from the root of
1106 The prefix path should usually be the relative path from the root of
1104 this matcher to the root of the wrapped matcher.
1107 this matcher to the root of the wrapped matcher.
1105
1108
1106 >>> m1 = match(util.localpath(b'/root/d/e'), b'f', [b'../a.txt', b'b.txt'], auditor=lambda name: None)
1109 >>> m1 = match(util.localpath(b'/root/d/e'), b'f', [b'../a.txt', b'b.txt'], auditor=lambda name: None)
1107 >>> m2 = prefixdirmatcher(b'd/e', m1)
1110 >>> m2 = prefixdirmatcher(b'd/e', m1)
1108 >>> m2(b'a.txt')
1111 >>> m2(b'a.txt')
1109 False
1112 False
1110 >>> m2(b'd/e/a.txt')
1113 >>> m2(b'd/e/a.txt')
1111 True
1114 True
1112 >>> m2(b'd/e/b.txt')
1115 >>> m2(b'd/e/b.txt')
1113 False
1116 False
1114 >>> m2.files()
1117 >>> m2.files()
1115 ['d/e/a.txt', 'd/e/f/b.txt']
1118 ['d/e/a.txt', 'd/e/f/b.txt']
1116 >>> m2.exact(b'd/e/a.txt')
1119 >>> m2.exact(b'd/e/a.txt')
1117 True
1120 True
1118 >>> m2.visitdir(b'd')
1121 >>> m2.visitdir(b'd')
1119 True
1122 True
1120 >>> m2.visitdir(b'd/e')
1123 >>> m2.visitdir(b'd/e')
1121 True
1124 True
1122 >>> m2.visitdir(b'd/e/f')
1125 >>> m2.visitdir(b'd/e/f')
1123 True
1126 True
1124 >>> m2.visitdir(b'd/e/g')
1127 >>> m2.visitdir(b'd/e/g')
1125 False
1128 False
1126 >>> m2.visitdir(b'd/ef')
1129 >>> m2.visitdir(b'd/ef')
1127 False
1130 False
1128 """
1131 """
1129
1132
1130 def __init__(self, path, matcher, badfn=None):
1133 def __init__(self, path, matcher, badfn=None):
1131 super(prefixdirmatcher, self).__init__(badfn)
1134 super(prefixdirmatcher, self).__init__(badfn)
1132 if not path:
1135 if not path:
1133 raise error.ProgrammingError(b'prefix path must not be empty')
1136 raise error.ProgrammingError(b'prefix path must not be empty')
1134 self._path = path
1137 self._path = path
1135 self._pathprefix = path + b'/'
1138 self._pathprefix = path + b'/'
1136 self._matcher = matcher
1139 self._matcher = matcher
1137
1140
1138 @propertycache
1141 @propertycache
1139 def _files(self):
1142 def _files(self):
1140 return [self._pathprefix + f for f in self._matcher._files]
1143 return [self._pathprefix + f for f in self._matcher._files]
1141
1144
1142 def matchfn(self, f):
1145 def matchfn(self, f):
1143 if not f.startswith(self._pathprefix):
1146 if not f.startswith(self._pathprefix):
1144 return False
1147 return False
1145 return self._matcher.matchfn(f[len(self._pathprefix) :])
1148 return self._matcher.matchfn(f[len(self._pathprefix) :])
1146
1149
1147 @propertycache
1150 @propertycache
1148 def _pathdirs(self):
1151 def _pathdirs(self):
1149 return set(pathutil.finddirs(self._path))
1152 return set(pathutil.finddirs(self._path))
1150
1153
1151 def visitdir(self, dir):
1154 def visitdir(self, dir):
1152 if dir == self._path:
1155 if dir == self._path:
1153 return self._matcher.visitdir(b'')
1156 return self._matcher.visitdir(b'')
1154 if dir.startswith(self._pathprefix):
1157 if dir.startswith(self._pathprefix):
1155 return self._matcher.visitdir(dir[len(self._pathprefix) :])
1158 return self._matcher.visitdir(dir[len(self._pathprefix) :])
1156 return dir in self._pathdirs
1159 return dir in self._pathdirs
1157
1160
1158 def visitchildrenset(self, dir):
1161 def visitchildrenset(self, dir):
1159 if dir == self._path:
1162 if dir == self._path:
1160 return self._matcher.visitchildrenset(b'')
1163 return self._matcher.visitchildrenset(b'')
1161 if dir.startswith(self._pathprefix):
1164 if dir.startswith(self._pathprefix):
1162 return self._matcher.visitchildrenset(dir[len(self._pathprefix) :])
1165 return self._matcher.visitchildrenset(dir[len(self._pathprefix) :])
1163 if dir in self._pathdirs:
1166 if dir in self._pathdirs:
1164 return b'this'
1167 return b'this'
1165 return set()
1168 return set()
1166
1169
1167 def isexact(self):
1170 def isexact(self):
1168 return self._matcher.isexact()
1171 return self._matcher.isexact()
1169
1172
1170 def prefix(self):
1173 def prefix(self):
1171 return self._matcher.prefix()
1174 return self._matcher.prefix()
1172
1175
1173 @encoding.strmethod
1176 @encoding.strmethod
1174 def __repr__(self):
1177 def __repr__(self):
1175 return b'<prefixdirmatcher path=%r, matcher=%r>' % (
1178 return b'<prefixdirmatcher path=%r, matcher=%r>' % (
1176 pycompat.bytestr(self._path),
1179 pycompat.bytestr(self._path),
1177 self._matcher,
1180 self._matcher,
1178 )
1181 )
1179
1182
1180
1183
1181 class unionmatcher(basematcher):
1184 class unionmatcher(basematcher):
1182 """A matcher that is the union of several matchers.
1185 """A matcher that is the union of several matchers.
1183
1186
1184 The non-matching-attributes (bad, traversedir) are taken from the first
1187 The non-matching-attributes (bad, traversedir) are taken from the first
1185 matcher.
1188 matcher.
1186 """
1189 """
1187
1190
1188 def __init__(self, matchers):
1191 def __init__(self, matchers):
1189 m1 = matchers[0]
1192 m1 = matchers[0]
1190 super(unionmatcher, self).__init__()
1193 super(unionmatcher, self).__init__()
1191 self.traversedir = m1.traversedir
1194 self.traversedir = m1.traversedir
1192 self._matchers = matchers
1195 self._matchers = matchers
1193
1196
1194 def matchfn(self, f):
1197 def matchfn(self, f):
1195 for match in self._matchers:
1198 for match in self._matchers:
1196 if match(f):
1199 if match(f):
1197 return True
1200 return True
1198 return False
1201 return False
1199
1202
1200 def visitdir(self, dir):
1203 def visitdir(self, dir):
1201 r = False
1204 r = False
1202 for m in self._matchers:
1205 for m in self._matchers:
1203 v = m.visitdir(dir)
1206 v = m.visitdir(dir)
1204 if v == b'all':
1207 if v == b'all':
1205 return v
1208 return v
1206 r |= v
1209 r |= v
1207 return r
1210 return r
1208
1211
1209 def visitchildrenset(self, dir):
1212 def visitchildrenset(self, dir):
1210 r = set()
1213 r = set()
1211 this = False
1214 this = False
1212 for m in self._matchers:
1215 for m in self._matchers:
1213 v = m.visitchildrenset(dir)
1216 v = m.visitchildrenset(dir)
1214 if not v:
1217 if not v:
1215 continue
1218 continue
1216 if v == b'all':
1219 if v == b'all':
1217 return v
1220 return v
1218 if this or v == b'this':
1221 if this or v == b'this':
1219 this = True
1222 this = True
1220 # don't break, we might have an 'all' in here.
1223 # don't break, we might have an 'all' in here.
1221 continue
1224 continue
1222 assert isinstance(v, set)
1225 assert isinstance(v, set)
1223 r = r.union(v)
1226 r = r.union(v)
1224 if this:
1227 if this:
1225 return b'this'
1228 return b'this'
1226 return r
1229 return r
1227
1230
1228 @encoding.strmethod
1231 @encoding.strmethod
1229 def __repr__(self):
1232 def __repr__(self):
1230 return b'<unionmatcher matchers=%r>' % self._matchers
1233 return b'<unionmatcher matchers=%r>' % self._matchers
1231
1234
1232
1235
1233 def patkind(pattern, default=None):
1236 def patkind(pattern, default=None):
1234 r"""If pattern is 'kind:pat' with a known kind, return kind.
1237 r"""If pattern is 'kind:pat' with a known kind, return kind.
1235
1238
1236 >>> patkind(br're:.*\.c$')
1239 >>> patkind(br're:.*\.c$')
1237 're'
1240 're'
1238 >>> patkind(b'glob:*.c')
1241 >>> patkind(b'glob:*.c')
1239 'glob'
1242 'glob'
1240 >>> patkind(b'relpath:test.py')
1243 >>> patkind(b'relpath:test.py')
1241 'relpath'
1244 'relpath'
1242 >>> patkind(b'main.py')
1245 >>> patkind(b'main.py')
1243 >>> patkind(b'main.py', default=b're')
1246 >>> patkind(b'main.py', default=b're')
1244 're'
1247 're'
1245 """
1248 """
1246 return _patsplit(pattern, default)[0]
1249 return _patsplit(pattern, default)[0]
1247
1250
1248
1251
1249 def _patsplit(pattern, default):
1252 def _patsplit(pattern, default):
1250 """Split a string into the optional pattern kind prefix and the actual
1253 """Split a string into the optional pattern kind prefix and the actual
1251 pattern."""
1254 pattern."""
1252 if b':' in pattern:
1255 if b':' in pattern:
1253 kind, pat = pattern.split(b':', 1)
1256 kind, pat = pattern.split(b':', 1)
1254 if kind in allpatternkinds:
1257 if kind in allpatternkinds:
1255 return kind, pat
1258 return kind, pat
1256 return default, pattern
1259 return default, pattern
1257
1260
1258
1261
1259 def _globre(pat):
1262 def _globre(pat):
1260 r"""Convert an extended glob string to a regexp string.
1263 r"""Convert an extended glob string to a regexp string.
1261
1264
1262 >>> from . import pycompat
1265 >>> from . import pycompat
1263 >>> def bprint(s):
1266 >>> def bprint(s):
1264 ... print(pycompat.sysstr(s))
1267 ... print(pycompat.sysstr(s))
1265 >>> bprint(_globre(br'?'))
1268 >>> bprint(_globre(br'?'))
1266 .
1269 .
1267 >>> bprint(_globre(br'*'))
1270 >>> bprint(_globre(br'*'))
1268 [^/]*
1271 [^/]*
1269 >>> bprint(_globre(br'**'))
1272 >>> bprint(_globre(br'**'))
1270 .*
1273 .*
1271 >>> bprint(_globre(br'**/a'))
1274 >>> bprint(_globre(br'**/a'))
1272 (?:.*/)?a
1275 (?:.*/)?a
1273 >>> bprint(_globre(br'a/**/b'))
1276 >>> bprint(_globre(br'a/**/b'))
1274 a/(?:.*/)?b
1277 a/(?:.*/)?b
1275 >>> bprint(_globre(br'[a*?!^][^b][!c]'))
1278 >>> bprint(_globre(br'[a*?!^][^b][!c]'))
1276 [a*?!^][\^b][^c]
1279 [a*?!^][\^b][^c]
1277 >>> bprint(_globre(br'{a,b}'))
1280 >>> bprint(_globre(br'{a,b}'))
1278 (?:a|b)
1281 (?:a|b)
1279 >>> bprint(_globre(br'.\*\?'))
1282 >>> bprint(_globre(br'.\*\?'))
1280 \.\*\?
1283 \.\*\?
1281 """
1284 """
1282 i, n = 0, len(pat)
1285 i, n = 0, len(pat)
1283 res = b''
1286 res = b''
1284 group = 0
1287 group = 0
1285 escape = util.stringutil.regexbytesescapemap.get
1288 escape = util.stringutil.regexbytesescapemap.get
1286
1289
1287 def peek():
1290 def peek():
1288 return i < n and pat[i : i + 1]
1291 return i < n and pat[i : i + 1]
1289
1292
1290 while i < n:
1293 while i < n:
1291 c = pat[i : i + 1]
1294 c = pat[i : i + 1]
1292 i += 1
1295 i += 1
1293 if c not in b'*?[{},\\':
1296 if c not in b'*?[{},\\':
1294 res += escape(c, c)
1297 res += escape(c, c)
1295 elif c == b'*':
1298 elif c == b'*':
1296 if peek() == b'*':
1299 if peek() == b'*':
1297 i += 1
1300 i += 1
1298 if peek() == b'/':
1301 if peek() == b'/':
1299 i += 1
1302 i += 1
1300 res += b'(?:.*/)?'
1303 res += b'(?:.*/)?'
1301 else:
1304 else:
1302 res += b'.*'
1305 res += b'.*'
1303 else:
1306 else:
1304 res += b'[^/]*'
1307 res += b'[^/]*'
1305 elif c == b'?':
1308 elif c == b'?':
1306 res += b'.'
1309 res += b'.'
1307 elif c == b'[':
1310 elif c == b'[':
1308 j = i
1311 j = i
1309 if j < n and pat[j : j + 1] in b'!]':
1312 if j < n and pat[j : j + 1] in b'!]':
1310 j += 1
1313 j += 1
1311 while j < n and pat[j : j + 1] != b']':
1314 while j < n and pat[j : j + 1] != b']':
1312 j += 1
1315 j += 1
1313 if j >= n:
1316 if j >= n:
1314 res += b'\\['
1317 res += b'\\['
1315 else:
1318 else:
1316 stuff = pat[i:j].replace(b'\\', b'\\\\')
1319 stuff = pat[i:j].replace(b'\\', b'\\\\')
1317 i = j + 1
1320 i = j + 1
1318 if stuff[0:1] == b'!':
1321 if stuff[0:1] == b'!':
1319 stuff = b'^' + stuff[1:]
1322 stuff = b'^' + stuff[1:]
1320 elif stuff[0:1] == b'^':
1323 elif stuff[0:1] == b'^':
1321 stuff = b'\\' + stuff
1324 stuff = b'\\' + stuff
1322 res = b'%s[%s]' % (res, stuff)
1325 res = b'%s[%s]' % (res, stuff)
1323 elif c == b'{':
1326 elif c == b'{':
1324 group += 1
1327 group += 1
1325 res += b'(?:'
1328 res += b'(?:'
1326 elif c == b'}' and group:
1329 elif c == b'}' and group:
1327 res += b')'
1330 res += b')'
1328 group -= 1
1331 group -= 1
1329 elif c == b',' and group:
1332 elif c == b',' and group:
1330 res += b'|'
1333 res += b'|'
1331 elif c == b'\\':
1334 elif c == b'\\':
1332 p = peek()
1335 p = peek()
1333 if p:
1336 if p:
1334 i += 1
1337 i += 1
1335 res += escape(p, p)
1338 res += escape(p, p)
1336 else:
1339 else:
1337 res += escape(c, c)
1340 res += escape(c, c)
1338 else:
1341 else:
1339 res += escape(c, c)
1342 res += escape(c, c)
1340 return res
1343 return res
1341
1344
1342
1345
1343 FLAG_RE = util.re.compile(br'^\(\?([aiLmsux]+)\)(.*)')
1346 FLAG_RE = util.re.compile(br'^\(\?([aiLmsux]+)\)(.*)')
1344
1347
1345
1348
1346 def _regex(kind, pat, globsuffix):
1349 def _regex(kind, pat, globsuffix):
1347 """Convert a (normalized) pattern of any kind into a
1350 """Convert a (normalized) pattern of any kind into a
1348 regular expression.
1351 regular expression.
1349 globsuffix is appended to the regexp of globs."""
1352 globsuffix is appended to the regexp of globs."""
1350 if not pat and kind in (b'glob', b'relpath'):
1353 if not pat and kind in (b'glob', b'relpath'):
1351 return b''
1354 return b''
1352 if kind == b're':
1355 if kind == b're':
1353 return pat
1356 return pat
1354 if kind == b'filepath':
1357 if kind == b'filepath':
1355 raise error.ProgrammingError(
1358 raise error.ProgrammingError(
1356 "'filepath:' patterns should not be converted to a regex"
1359 "'filepath:' patterns should not be converted to a regex"
1357 )
1360 )
1358 if kind in (b'path', b'relpath'):
1361 if kind in (b'path', b'relpath'):
1359 if pat == b'.':
1362 if pat == b'.':
1360 return b''
1363 return b''
1361 return util.stringutil.reescape(pat) + b'(?:/|$)'
1364 return util.stringutil.reescape(pat) + b'(?:/|$)'
1362 if kind == b'rootfilesin':
1365 if kind == b'rootfilesin':
1363 if pat == b'.':
1366 if pat == b'.':
1364 escaped = b''
1367 escaped = b''
1365 else:
1368 else:
1366 # Pattern is a directory name.
1369 # Pattern is a directory name.
1367 escaped = util.stringutil.reescape(pat) + b'/'
1370 escaped = util.stringutil.reescape(pat) + b'/'
1368 # Anything after the pattern must be a non-directory.
1371 # Anything after the pattern must be a non-directory.
1369 return escaped + b'[^/]+$'
1372 return escaped + b'[^/]+$'
1370 if kind == b'relglob':
1373 if kind == b'relglob':
1371 globre = _globre(pat)
1374 globre = _globre(pat)
1372 if globre.startswith(b'[^/]*'):
1375 if globre.startswith(b'[^/]*'):
1373 # When pat has the form *XYZ (common), make the returned regex more
1376 # When pat has the form *XYZ (common), make the returned regex more
1374 # legible by returning the regex for **XYZ instead of **/*XYZ.
1377 # legible by returning the regex for **XYZ instead of **/*XYZ.
1375 return b'.*' + globre[len(b'[^/]*') :] + globsuffix
1378 return b'.*' + globre[len(b'[^/]*') :] + globsuffix
1376 return b'(?:|.*/)' + globre + globsuffix
1379 return b'(?:|.*/)' + globre + globsuffix
1377 if kind == b'relre':
1380 if kind == b'relre':
1378 flag = None
1381 flag = None
1379 m = FLAG_RE.match(pat)
1382 m = FLAG_RE.match(pat)
1380 if m:
1383 if m:
1381 flag, pat = m.groups()
1384 flag, pat = m.groups()
1382 if not pat.startswith(b'^'):
1385 if not pat.startswith(b'^'):
1383 pat = b'.*' + pat
1386 pat = b'.*' + pat
1384 if flag is not None:
1387 if flag is not None:
1385 pat = br'(?%s:%s)' % (flag, pat)
1388 pat = br'(?%s:%s)' % (flag, pat)
1386 return pat
1389 return pat
1387 if kind in (b'glob', b'rootglob'):
1390 if kind in (b'glob', b'rootglob'):
1388 return _globre(pat) + globsuffix
1391 return _globre(pat) + globsuffix
1389 raise error.ProgrammingError(b'not a regex pattern: %s:%s' % (kind, pat))
1392 raise error.ProgrammingError(b'not a regex pattern: %s:%s' % (kind, pat))
1390
1393
1391
1394
1392 def _buildmatch(kindpats, globsuffix, root):
1395 def _buildmatch(kindpats, globsuffix, root):
1393 """Return regexp string and a matcher function for kindpats.
1396 """Return regexp string and a matcher function for kindpats.
1394 globsuffix is appended to the regexp of globs."""
1397 globsuffix is appended to the regexp of globs."""
1395 matchfuncs = []
1398 matchfuncs = []
1396
1399
1397 subincludes, kindpats = _expandsubinclude(kindpats, root)
1400 subincludes, kindpats = _expandsubinclude(kindpats, root)
1398 if subincludes:
1401 if subincludes:
1399 submatchers = {}
1402 submatchers = {}
1400
1403
1401 def matchsubinclude(f):
1404 def matchsubinclude(f):
1402 for prefix, matcherargs in subincludes:
1405 for prefix, matcherargs in subincludes:
1403 if f.startswith(prefix):
1406 if f.startswith(prefix):
1404 mf = submatchers.get(prefix)
1407 mf = submatchers.get(prefix)
1405 if mf is None:
1408 if mf is None:
1406 mf = match(*matcherargs)
1409 mf = match(*matcherargs)
1407 submatchers[prefix] = mf
1410 submatchers[prefix] = mf
1408
1411
1409 if mf(f[len(prefix) :]):
1412 if mf(f[len(prefix) :]):
1410 return True
1413 return True
1411 return False
1414 return False
1412
1415
1413 matchfuncs.append(matchsubinclude)
1416 matchfuncs.append(matchsubinclude)
1414
1417
1415 regex = b''
1418 regex = b''
1416 if kindpats:
1419 if kindpats:
1417 if all(k == b'rootfilesin' for k, p, s in kindpats):
1420 if all(k == b'rootfilesin' for k, p, s in kindpats):
1418 dirs = {p for k, p, s in kindpats}
1421 dirs = {p for k, p, s in kindpats}
1419
1422
1420 def mf(f):
1423 def mf(f):
1421 i = f.rfind(b'/')
1424 i = f.rfind(b'/')
1422 if i >= 0:
1425 if i >= 0:
1423 dir = f[:i]
1426 dir = f[:i]
1424 else:
1427 else:
1425 dir = b'.'
1428 dir = b'.'
1426 return dir in dirs
1429 return dir in dirs
1427
1430
1428 regex = b'rootfilesin: %s' % stringutil.pprint(list(sorted(dirs)))
1431 regex = b'rootfilesin: %s' % stringutil.pprint(list(sorted(dirs)))
1429 matchfuncs.append(mf)
1432 matchfuncs.append(mf)
1430 else:
1433 else:
1431 regex, mf = _buildregexmatch(kindpats, globsuffix)
1434 regex, mf = _buildregexmatch(kindpats, globsuffix)
1432 matchfuncs.append(mf)
1435 matchfuncs.append(mf)
1433
1436
1434 if len(matchfuncs) == 1:
1437 if len(matchfuncs) == 1:
1435 return regex, matchfuncs[0]
1438 return regex, matchfuncs[0]
1436 else:
1439 else:
1437 return regex, lambda f: any(mf(f) for mf in matchfuncs)
1440 return regex, lambda f: any(mf(f) for mf in matchfuncs)
1438
1441
1439
1442
1440 MAX_RE_SIZE = 20000
1443 MAX_RE_SIZE = 20000
1441
1444
1442
1445
1443 def _joinregexes(regexps):
1446 def _joinregexes(regexps):
1444 """gather multiple regular expressions into a single one"""
1447 """gather multiple regular expressions into a single one"""
1445 return b'|'.join(regexps)
1448 return b'|'.join(regexps)
1446
1449
1447
1450
1448 def _buildregexmatch(kindpats, globsuffix):
1451 def _buildregexmatch(kindpats, globsuffix):
1449 """Build a match function from a list of kinds and kindpats,
1452 """Build a match function from a list of kinds and kindpats,
1450 return regexp string and a matcher function.
1453 return regexp string and a matcher function.
1451
1454
1452 Test too large input
1455 Test too large input
1453 >>> _buildregexmatch([
1456 >>> _buildregexmatch([
1454 ... (b'relglob', b'?' * MAX_RE_SIZE, b'')
1457 ... (b'relglob', b'?' * MAX_RE_SIZE, b'')
1455 ... ], b'$')
1458 ... ], b'$')
1456 Traceback (most recent call last):
1459 Traceback (most recent call last):
1457 ...
1460 ...
1458 Abort: matcher pattern is too long (20009 bytes)
1461 Abort: matcher pattern is too long (20009 bytes)
1459 """
1462 """
1460 try:
1463 try:
1461 allgroups = []
1464 allgroups = []
1462 regexps = []
1465 regexps = []
1463 exact = set()
1466 exact = set()
1464 for (kind, pattern, _source) in kindpats:
1467 for kind, pattern, _source in kindpats:
1465 if kind == b'filepath':
1468 if kind == b'filepath':
1466 exact.add(pattern)
1469 exact.add(pattern)
1467 continue
1470 continue
1468 regexps.append(_regex(kind, pattern, globsuffix))
1471 regexps.append(_regex(kind, pattern, globsuffix))
1469
1472
1470 fullregexp = _joinregexes(regexps)
1473 fullregexp = _joinregexes(regexps)
1471
1474
1472 startidx = 0
1475 startidx = 0
1473 groupsize = 0
1476 groupsize = 0
1474 for idx, r in enumerate(regexps):
1477 for idx, r in enumerate(regexps):
1475 piecesize = len(r)
1478 piecesize = len(r)
1476 if piecesize > MAX_RE_SIZE:
1479 if piecesize > MAX_RE_SIZE:
1477 msg = _(b"matcher pattern is too long (%d bytes)") % piecesize
1480 msg = _(b"matcher pattern is too long (%d bytes)") % piecesize
1478 raise error.Abort(msg)
1481 raise error.Abort(msg)
1479 elif (groupsize + piecesize) > MAX_RE_SIZE:
1482 elif (groupsize + piecesize) > MAX_RE_SIZE:
1480 group = regexps[startidx:idx]
1483 group = regexps[startidx:idx]
1481 allgroups.append(_joinregexes(group))
1484 allgroups.append(_joinregexes(group))
1482 startidx = idx
1485 startidx = idx
1483 groupsize = 0
1486 groupsize = 0
1484 groupsize += piecesize + 1
1487 groupsize += piecesize + 1
1485
1488
1486 if startidx == 0:
1489 if startidx == 0:
1487 matcher = _rematcher(fullregexp)
1490 matcher = _rematcher(fullregexp)
1488 func = lambda s: bool(matcher(s))
1491 func = lambda s: bool(matcher(s))
1489 else:
1492 else:
1490 group = regexps[startidx:]
1493 group = regexps[startidx:]
1491 allgroups.append(_joinregexes(group))
1494 allgroups.append(_joinregexes(group))
1492 allmatchers = [_rematcher(g) for g in allgroups]
1495 allmatchers = [_rematcher(g) for g in allgroups]
1493 func = lambda s: any(m(s) for m in allmatchers)
1496 func = lambda s: any(m(s) for m in allmatchers)
1494
1497
1495 actualfunc = func
1498 actualfunc = func
1496 if exact:
1499 if exact:
1497 # An empty regex will always match, so only call the regex if
1500 # An empty regex will always match, so only call the regex if
1498 # there were any actual patterns to match.
1501 # there were any actual patterns to match.
1499 if not regexps:
1502 if not regexps:
1500 actualfunc = lambda s: s in exact
1503 actualfunc = lambda s: s in exact
1501 else:
1504 else:
1502 actualfunc = lambda s: s in exact or func(s)
1505 actualfunc = lambda s: s in exact or func(s)
1503 return fullregexp, actualfunc
1506 return fullregexp, actualfunc
1504 except re.error:
1507 except re.error:
1505 for k, p, s in kindpats:
1508 for k, p, s in kindpats:
1506 if k == b'filepath':
1509 if k == b'filepath':
1507 continue
1510 continue
1508 try:
1511 try:
1509 _rematcher(_regex(k, p, globsuffix))
1512 _rematcher(_regex(k, p, globsuffix))
1510 except re.error:
1513 except re.error:
1511 if s:
1514 if s:
1512 raise error.Abort(
1515 raise error.Abort(
1513 _(b"%s: invalid pattern (%s): %s") % (s, k, p)
1516 _(b"%s: invalid pattern (%s): %s") % (s, k, p)
1514 )
1517 )
1515 else:
1518 else:
1516 raise error.Abort(_(b"invalid pattern (%s): %s") % (k, p))
1519 raise error.Abort(_(b"invalid pattern (%s): %s") % (k, p))
1517 raise error.Abort(_(b"invalid pattern"))
1520 raise error.Abort(_(b"invalid pattern"))
1518
1521
1519
1522
1520 def _patternrootsanddirs(kindpats):
1523 def _patternrootsanddirs(kindpats):
1521 """Returns roots and directories corresponding to each pattern.
1524 """Returns roots and directories corresponding to each pattern.
1522
1525
1523 This calculates the roots and directories exactly matching the patterns and
1526 This calculates the roots and directories exactly matching the patterns and
1524 returns a tuple of (roots, dirs) for each. It does not return other
1527 returns a tuple of (roots, dirs) for each. It does not return other
1525 directories which may also need to be considered, like the parent
1528 directories which may also need to be considered, like the parent
1526 directories.
1529 directories.
1527 """
1530 """
1528 r = []
1531 r = []
1529 d = []
1532 d = []
1530 for kind, pat, source in kindpats:
1533 for kind, pat, source in kindpats:
1531 if kind in (b'glob', b'rootglob'): # find the non-glob prefix
1534 if kind in (b'glob', b'rootglob'): # find the non-glob prefix
1532 root = []
1535 root = []
1533 for p in pat.split(b'/'):
1536 for p in pat.split(b'/'):
1534 if b'[' in p or b'{' in p or b'*' in p or b'?' in p:
1537 if b'[' in p or b'{' in p or b'*' in p or b'?' in p:
1535 break
1538 break
1536 root.append(p)
1539 root.append(p)
1537 r.append(b'/'.join(root))
1540 r.append(b'/'.join(root))
1538 elif kind in (b'relpath', b'path', b'filepath'):
1541 elif kind in (b'relpath', b'path', b'filepath'):
1539 if pat == b'.':
1542 if pat == b'.':
1540 pat = b''
1543 pat = b''
1541 r.append(pat)
1544 r.append(pat)
1542 elif kind in (b'rootfilesin',):
1545 elif kind in (b'rootfilesin',):
1543 if pat == b'.':
1546 if pat == b'.':
1544 pat = b''
1547 pat = b''
1545 d.append(pat)
1548 d.append(pat)
1546 else: # relglob, re, relre
1549 else: # relglob, re, relre
1547 r.append(b'')
1550 r.append(b'')
1548 return r, d
1551 return r, d
1549
1552
1550
1553
1551 def _roots(kindpats):
1554 def _roots(kindpats):
1552 '''Returns root directories to match recursively from the given patterns.'''
1555 '''Returns root directories to match recursively from the given patterns.'''
1553 roots, dirs = _patternrootsanddirs(kindpats)
1556 roots, dirs = _patternrootsanddirs(kindpats)
1554 return roots
1557 return roots
1555
1558
1556
1559
1557 def _rootsdirsandparents(kindpats):
1560 def _rootsdirsandparents(kindpats):
1558 """Returns roots and exact directories from patterns.
1561 """Returns roots and exact directories from patterns.
1559
1562
1560 `roots` are directories to match recursively, `dirs` should
1563 `roots` are directories to match recursively, `dirs` should
1561 be matched non-recursively, and `parents` are the implicitly required
1564 be matched non-recursively, and `parents` are the implicitly required
1562 directories to walk to items in either roots or dirs.
1565 directories to walk to items in either roots or dirs.
1563
1566
1564 Returns a tuple of (roots, dirs, parents).
1567 Returns a tuple of (roots, dirs, parents).
1565
1568
1566 >>> r = _rootsdirsandparents(
1569 >>> r = _rootsdirsandparents(
1567 ... [(b'glob', b'g/h/*', b''), (b'glob', b'g/h', b''),
1570 ... [(b'glob', b'g/h/*', b''), (b'glob', b'g/h', b''),
1568 ... (b'glob', b'g*', b'')])
1571 ... (b'glob', b'g*', b'')])
1569 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1572 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1570 (['g/h', 'g/h', ''], []) ['', 'g']
1573 (['g/h', 'g/h', ''], []) ['', 'g']
1571 >>> r = _rootsdirsandparents(
1574 >>> r = _rootsdirsandparents(
1572 ... [(b'rootfilesin', b'g/h', b''), (b'rootfilesin', b'', b'')])
1575 ... [(b'rootfilesin', b'g/h', b''), (b'rootfilesin', b'', b'')])
1573 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1576 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1574 ([], ['g/h', '']) ['', 'g']
1577 ([], ['g/h', '']) ['', 'g']
1575 >>> r = _rootsdirsandparents(
1578 >>> r = _rootsdirsandparents(
1576 ... [(b'relpath', b'r', b''), (b'path', b'p/p', b''),
1579 ... [(b'relpath', b'r', b''), (b'path', b'p/p', b''),
1577 ... (b'path', b'', b'')])
1580 ... (b'path', b'', b'')])
1578 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1581 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1579 (['r', 'p/p', ''], []) ['', 'p']
1582 (['r', 'p/p', ''], []) ['', 'p']
1580 >>> r = _rootsdirsandparents(
1583 >>> r = _rootsdirsandparents(
1581 ... [(b'relglob', b'rg*', b''), (b're', b're/', b''),
1584 ... [(b'relglob', b'rg*', b''), (b're', b're/', b''),
1582 ... (b'relre', b'rr', b'')])
1585 ... (b'relre', b'rr', b'')])
1583 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1586 >>> print(r[0:2], sorted(r[2])) # the set has an unstable output
1584 (['', '', ''], []) ['']
1587 (['', '', ''], []) ['']
1585 """
1588 """
1586 r, d = _patternrootsanddirs(kindpats)
1589 r, d = _patternrootsanddirs(kindpats)
1587
1590
1588 p = set()
1591 p = set()
1589 # Add the parents as non-recursive/exact directories, since they must be
1592 # Add the parents as non-recursive/exact directories, since they must be
1590 # scanned to get to either the roots or the other exact directories.
1593 # scanned to get to either the roots or the other exact directories.
1591 p.update(pathutil.dirs(d))
1594 p.update(pathutil.dirs(d))
1592 p.update(pathutil.dirs(r))
1595 p.update(pathutil.dirs(r))
1593
1596
1594 # FIXME: all uses of this function convert these to sets, do so before
1597 # FIXME: all uses of this function convert these to sets, do so before
1595 # returning.
1598 # returning.
1596 # FIXME: all uses of this function do not need anything in 'roots' and
1599 # FIXME: all uses of this function do not need anything in 'roots' and
1597 # 'dirs' to also be in 'parents', consider removing them before returning.
1600 # 'dirs' to also be in 'parents', consider removing them before returning.
1598 return r, d, p
1601 return r, d, p
1599
1602
1600
1603
1601 def _explicitfiles(kindpats):
1604 def _explicitfiles(kindpats):
1602 """Returns the potential explicit filenames from the patterns.
1605 """Returns the potential explicit filenames from the patterns.
1603
1606
1604 >>> _explicitfiles([(b'path', b'foo/bar', b'')])
1607 >>> _explicitfiles([(b'path', b'foo/bar', b'')])
1605 ['foo/bar']
1608 ['foo/bar']
1606 >>> _explicitfiles([(b'rootfilesin', b'foo/bar', b'')])
1609 >>> _explicitfiles([(b'rootfilesin', b'foo/bar', b'')])
1607 []
1610 []
1608 """
1611 """
1609 # Keep only the pattern kinds where one can specify filenames (vs only
1612 # Keep only the pattern kinds where one can specify filenames (vs only
1610 # directory names).
1613 # directory names).
1611 filable = [kp for kp in kindpats if kp[0] not in (b'rootfilesin',)]
1614 filable = [kp for kp in kindpats if kp[0] not in (b'rootfilesin',)]
1612 return _roots(filable)
1615 return _roots(filable)
1613
1616
1614
1617
1615 def _prefix(kindpats):
1618 def _prefix(kindpats):
1616 '''Whether all the patterns match a prefix (i.e. recursively)'''
1619 '''Whether all the patterns match a prefix (i.e. recursively)'''
1617 for kind, pat, source in kindpats:
1620 for kind, pat, source in kindpats:
1618 if kind not in (b'path', b'relpath'):
1621 if kind not in (b'path', b'relpath'):
1619 return False
1622 return False
1620 return True
1623 return True
1621
1624
1622
1625
1623 _commentre = None
1626 _commentre = None
1624
1627
1625
1628
1626 def readpatternfile(filepath, warn, sourceinfo=False):
1629 def readpatternfile(filepath, warn, sourceinfo=False):
1627 """parse a pattern file, returning a list of
1630 """parse a pattern file, returning a list of
1628 patterns. These patterns should be given to compile()
1631 patterns. These patterns should be given to compile()
1629 to be validated and converted into a match function.
1632 to be validated and converted into a match function.
1630
1633
1631 trailing white space is dropped.
1634 trailing white space is dropped.
1632 the escape character is backslash.
1635 the escape character is backslash.
1633 comments start with #.
1636 comments start with #.
1634 empty lines are skipped.
1637 empty lines are skipped.
1635
1638
1636 lines can be of the following formats:
1639 lines can be of the following formats:
1637
1640
1638 syntax: regexp # defaults following lines to non-rooted regexps
1641 syntax: regexp # defaults following lines to non-rooted regexps
1639 syntax: glob # defaults following lines to non-rooted globs
1642 syntax: glob # defaults following lines to non-rooted globs
1640 re:pattern # non-rooted regular expression
1643 re:pattern # non-rooted regular expression
1641 glob:pattern # non-rooted glob
1644 glob:pattern # non-rooted glob
1642 rootglob:pat # rooted glob (same root as ^ in regexps)
1645 rootglob:pat # rooted glob (same root as ^ in regexps)
1643 pattern # pattern of the current default type
1646 pattern # pattern of the current default type
1644
1647
1645 if sourceinfo is set, returns a list of tuples:
1648 if sourceinfo is set, returns a list of tuples:
1646 (pattern, lineno, originalline).
1649 (pattern, lineno, originalline).
1647 This is useful to debug ignore patterns.
1650 This is useful to debug ignore patterns.
1648 """
1651 """
1649
1652
1650 syntaxes = {
1653 syntaxes = {
1651 b're': b'relre:',
1654 b're': b'relre:',
1652 b'regexp': b'relre:',
1655 b'regexp': b'relre:',
1653 b'glob': b'relglob:',
1656 b'glob': b'relglob:',
1654 b'rootglob': b'rootglob:',
1657 b'rootglob': b'rootglob:',
1655 b'include': b'include',
1658 b'include': b'include',
1656 b'subinclude': b'subinclude',
1659 b'subinclude': b'subinclude',
1657 }
1660 }
1658 syntax = b'relre:'
1661 syntax = b'relre:'
1659 patterns = []
1662 patterns = []
1660
1663
1661 fp = open(filepath, b'rb')
1664 fp = open(filepath, b'rb')
1662 for lineno, line in enumerate(fp, start=1):
1665 for lineno, line in enumerate(fp, start=1):
1663 if b"#" in line:
1666 if b"#" in line:
1664 global _commentre
1667 global _commentre
1665 if not _commentre:
1668 if not _commentre:
1666 _commentre = util.re.compile(br'((?:^|[^\\])(?:\\\\)*)#.*')
1669 _commentre = util.re.compile(br'((?:^|[^\\])(?:\\\\)*)#.*')
1667 # remove comments prefixed by an even number of escapes
1670 # remove comments prefixed by an even number of escapes
1668 m = _commentre.search(line)
1671 m = _commentre.search(line)
1669 if m:
1672 if m:
1670 line = line[: m.end(1)]
1673 line = line[: m.end(1)]
1671 # fixup properly escaped comments that survived the above
1674 # fixup properly escaped comments that survived the above
1672 line = line.replace(b"\\#", b"#")
1675 line = line.replace(b"\\#", b"#")
1673 line = line.rstrip()
1676 line = line.rstrip()
1674 if not line:
1677 if not line:
1675 continue
1678 continue
1676
1679
1677 if line.startswith(b'syntax:'):
1680 if line.startswith(b'syntax:'):
1678 s = line[7:].strip()
1681 s = line[7:].strip()
1679 try:
1682 try:
1680 syntax = syntaxes[s]
1683 syntax = syntaxes[s]
1681 except KeyError:
1684 except KeyError:
1682 if warn:
1685 if warn:
1683 warn(
1686 warn(
1684 _(b"%s: ignoring invalid syntax '%s'\n") % (filepath, s)
1687 _(b"%s: ignoring invalid syntax '%s'\n") % (filepath, s)
1685 )
1688 )
1686 continue
1689 continue
1687
1690
1688 linesyntax = syntax
1691 linesyntax = syntax
1689 for s, rels in syntaxes.items():
1692 for s, rels in syntaxes.items():
1690 if line.startswith(rels):
1693 if line.startswith(rels):
1691 linesyntax = rels
1694 linesyntax = rels
1692 line = line[len(rels) :]
1695 line = line[len(rels) :]
1693 break
1696 break
1694 elif line.startswith(s + b':'):
1697 elif line.startswith(s + b':'):
1695 linesyntax = rels
1698 linesyntax = rels
1696 line = line[len(s) + 1 :]
1699 line = line[len(s) + 1 :]
1697 break
1700 break
1698 if sourceinfo:
1701 if sourceinfo:
1699 patterns.append((linesyntax + line, lineno, line))
1702 patterns.append((linesyntax + line, lineno, line))
1700 else:
1703 else:
1701 patterns.append(linesyntax + line)
1704 patterns.append(linesyntax + line)
1702 fp.close()
1705 fp.close()
1703 return patterns
1706 return patterns
@@ -1,550 +1,560 b''
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
2 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
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 contextlib
8 import contextlib
9 import struct
9 import struct
10 import threading
10 import threading
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 encoding,
14 encoding,
15 error,
15 error,
16 pycompat,
16 pycompat,
17 util,
17 util,
18 wireprototypes,
18 wireprototypes,
19 wireprotov1server,
19 wireprotov1server,
20 )
20 )
21 from .interfaces import util as interfaceutil
21 from .interfaces import util as interfaceutil
22 from .utils import (
22 from .utils import (
23 compression,
23 compression,
24 stringutil,
24 stringutil,
25 )
25 )
26
26
27 stringio = util.stringio
27 stringio = util.stringio
28
28
29 urlerr = util.urlerr
29 urlerr = util.urlerr
30 urlreq = util.urlreq
30 urlreq = util.urlreq
31
31
32 HTTP_OK = 200
32 HTTP_OK = 200
33
33
34 HGTYPE = b'application/mercurial-0.1'
34 HGTYPE = b'application/mercurial-0.1'
35 HGTYPE2 = b'application/mercurial-0.2'
35 HGTYPE2 = b'application/mercurial-0.2'
36 HGERRTYPE = b'application/hg-error'
36 HGERRTYPE = b'application/hg-error'
37
37
38 SSHV1 = wireprototypes.SSHV1
38 SSHV1 = wireprototypes.SSHV1
39
39
40
40
41 def decodevaluefromheaders(req, headerprefix):
41 def decodevaluefromheaders(req, headerprefix):
42 """Decode a long value from multiple HTTP request headers.
42 """Decode a long value from multiple HTTP request headers.
43
43
44 Returns the value as a bytes, not a str.
44 Returns the value as a bytes, not a str.
45 """
45 """
46 chunks = []
46 chunks = []
47 i = 1
47 i = 1
48 while True:
48 while True:
49 v = req.headers.get(b'%s-%d' % (headerprefix, i))
49 v = req.headers.get(b'%s-%d' % (headerprefix, i))
50 if v is None:
50 if v is None:
51 break
51 break
52 chunks.append(pycompat.bytesurl(v))
52 chunks.append(pycompat.bytesurl(v))
53 i += 1
53 i += 1
54
54
55 return b''.join(chunks)
55 return b''.join(chunks)
56
56
57
57
58 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
58 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
59 class httpv1protocolhandler:
59 class httpv1protocolhandler:
60 def __init__(self, req, ui, checkperm):
60 def __init__(self, req, ui, checkperm):
61 self._req = req
61 self._req = req
62 self._ui = ui
62 self._ui = ui
63 self._checkperm = checkperm
63 self._checkperm = checkperm
64 self._protocaps = None
64 self._protocaps = None
65
65
66 @property
66 @property
67 def name(self):
67 def name(self):
68 return b'http-v1'
68 return b'http-v1'
69
69
70 def getargs(self, args):
70 def getargs(self, args):
71 knownargs = self._args()
71 knownargs = self._args()
72 data = {}
72 data = {}
73 keys = args.split()
73 keys = args.split()
74 for k in keys:
74 for k in keys:
75 if k == b'*':
75 if k == b'*':
76 star = {}
76 star = {}
77 for key in knownargs.keys():
77 for key in knownargs.keys():
78 if key != b'cmd' and key not in keys:
78 if key != b'cmd' and key not in keys:
79 star[key] = knownargs[key][0]
79 star[key] = knownargs[key][0]
80 data[b'*'] = star
80 data[b'*'] = star
81 else:
81 else:
82 data[k] = knownargs[k][0]
82 data[k] = knownargs[k][0]
83 return [data[k] for k in keys]
83 return [data[k] for k in keys]
84
84
85 def _args(self):
85 def _args(self):
86 args = self._req.qsparams.asdictoflists()
86 args = self._req.qsparams.asdictoflists()
87 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
87 postlen = int(self._req.headers.get(b'X-HgArgs-Post', 0))
88 if postlen:
88 if postlen:
89 args.update(
89 args.update(
90 urlreq.parseqs(
90 urlreq.parseqs(
91 self._req.bodyfh.read(postlen), keep_blank_values=True
91 self._req.bodyfh.read(postlen), keep_blank_values=True
92 )
92 )
93 )
93 )
94 return args
94 return args
95
95
96 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
96 argvalue = decodevaluefromheaders(self._req, b'X-HgArg')
97 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
97 args.update(urlreq.parseqs(argvalue, keep_blank_values=True))
98 return args
98 return args
99
99
100 def getprotocaps(self):
100 def getprotocaps(self):
101 if self._protocaps is None:
101 if self._protocaps is None:
102 value = decodevaluefromheaders(self._req, b'X-HgProto')
102 value = decodevaluefromheaders(self._req, b'X-HgProto')
103 self._protocaps = set(value.split(b' '))
103 self._protocaps = set(value.split(b' '))
104 return self._protocaps
104 return self._protocaps
105
105
106 def getpayload(self):
106 def getpayload(self):
107 # Existing clients *always* send Content-Length.
107 # Existing clients *always* send Content-Length.
108 length = int(self._req.headers[b'Content-Length'])
108 length = int(self._req.headers[b'Content-Length'])
109
109
110 # If httppostargs is used, we need to read Content-Length
110 # If httppostargs is used, we need to read Content-Length
111 # minus the amount that was consumed by args.
111 # minus the amount that was consumed by args.
112 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
112 length -= int(self._req.headers.get(b'X-HgArgs-Post', 0))
113 return util.filechunkiter(self._req.bodyfh, limit=length)
113 return util.filechunkiter(self._req.bodyfh, limit=length)
114
114
115 @contextlib.contextmanager
115 @contextlib.contextmanager
116 def mayberedirectstdio(self):
116 def mayberedirectstdio(self):
117 oldout = self._ui.fout
117 oldout = self._ui.fout
118 olderr = self._ui.ferr
118 olderr = self._ui.ferr
119
119
120 out = util.stringio()
120 out = util.stringio()
121
121
122 try:
122 try:
123 self._ui.fout = out
123 self._ui.fout = out
124 self._ui.ferr = out
124 self._ui.ferr = out
125 yield out
125 yield out
126 finally:
126 finally:
127 self._ui.fout = oldout
127 self._ui.fout = oldout
128 self._ui.ferr = olderr
128 self._ui.ferr = olderr
129
129
130 def client(self):
130 def client(self):
131 return b'remote:%s:%s:%s' % (
131 return b'remote:%s:%s:%s' % (
132 self._req.urlscheme,
132 self._req.urlscheme,
133 urlreq.quote(self._req.remotehost or b''),
133 urlreq.quote(self._req.remotehost or b''),
134 urlreq.quote(self._req.remoteuser or b''),
134 urlreq.quote(self._req.remoteuser or b''),
135 )
135 )
136
136
137 def addcapabilities(self, repo, caps):
137 def addcapabilities(self, repo, caps):
138 caps.append(b'batch')
138 caps.append(b'batch')
139
139
140 caps.append(
140 caps.append(
141 b'httpheader=%d' % repo.ui.configint(b'server', b'maxhttpheaderlen')
141 b'httpheader=%d' % repo.ui.configint(b'server', b'maxhttpheaderlen')
142 )
142 )
143 if repo.ui.configbool(b'experimental', b'httppostargs'):
143 if repo.ui.configbool(b'experimental', b'httppostargs'):
144 caps.append(b'httppostargs')
144 caps.append(b'httppostargs')
145
145
146 # FUTURE advertise 0.2rx once support is implemented
146 # FUTURE advertise 0.2rx once support is implemented
147 # FUTURE advertise minrx and mintx after consulting config option
147 # FUTURE advertise minrx and mintx after consulting config option
148 caps.append(b'httpmediatype=0.1rx,0.1tx,0.2tx')
148 caps.append(b'httpmediatype=0.1rx,0.1tx,0.2tx')
149
149
150 compengines = wireprototypes.supportedcompengines(
150 compengines = wireprototypes.supportedcompengines(
151 repo.ui, compression.SERVERROLE
151 repo.ui, compression.SERVERROLE
152 )
152 )
153 if compengines:
153 if compengines:
154 comptypes = b','.join(
154 comptypes = b','.join(
155 urlreq.quote(e.wireprotosupport().name) for e in compengines
155 urlreq.quote(e.wireprotosupport().name) for e in compengines
156 )
156 )
157 caps.append(b'compression=%s' % comptypes)
157 caps.append(b'compression=%s' % comptypes)
158
158
159 return caps
159 return caps
160
160
161 def checkperm(self, perm):
161 def checkperm(self, perm):
162 return self._checkperm(perm)
162 return self._checkperm(perm)
163
163
164
164
165 # This method exists mostly so that extensions like remotefilelog can
165 # This method exists mostly so that extensions like remotefilelog can
166 # disable a kludgey legacy method only over http. As of early 2018,
166 # disable a kludgey legacy method only over http. As of early 2018,
167 # there are no other known users, so with any luck we can discard this
167 # there are no other known users, so with any luck we can discard this
168 # hook if remotefilelog becomes a first-party extension.
168 # hook if remotefilelog becomes a first-party extension.
169 def iscmd(cmd):
169 def iscmd(cmd):
170 return cmd in wireprotov1server.commands
170 return cmd in wireprotov1server.commands
171
171
172
172
173 def handlewsgirequest(rctx, req, res, checkperm):
173 def handlewsgirequest(rctx, req, res, checkperm):
174 """Possibly process a wire protocol request.
174 """Possibly process a wire protocol request.
175
175
176 If the current request is a wire protocol request, the request is
176 If the current request is a wire protocol request, the request is
177 processed by this function.
177 processed by this function.
178
178
179 ``req`` is a ``parsedrequest`` instance.
179 ``req`` is a ``parsedrequest`` instance.
180 ``res`` is a ``wsgiresponse`` instance.
180 ``res`` is a ``wsgiresponse`` instance.
181
181
182 Returns a bool indicating if the request was serviced. If set, the caller
182 Returns a bool indicating if the request was serviced. If set, the caller
183 should stop processing the request, as a response has already been issued.
183 should stop processing the request, as a response has already been issued.
184 """
184 """
185 # Avoid cycle involving hg module.
185 # Avoid cycle involving hg module.
186 from .hgweb import common as hgwebcommon
186 from .hgweb import common as hgwebcommon
187
187
188 repo = rctx.repo
188 repo = rctx.repo
189
189
190 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
190 # HTTP version 1 wire protocol requests are denoted by a "cmd" query
191 # string parameter. If it isn't present, this isn't a wire protocol
191 # string parameter. If it isn't present, this isn't a wire protocol
192 # request.
192 # request.
193 if b'cmd' not in req.qsparams:
193 if b'cmd' not in req.qsparams:
194 return False
194 return False
195
195
196 cmd = req.qsparams[b'cmd']
196 cmd = req.qsparams[b'cmd']
197
197
198 # The "cmd" request parameter is used by both the wire protocol and hgweb.
198 # The "cmd" request parameter is used by both the wire protocol and hgweb.
199 # While not all wire protocol commands are available for all transports,
199 # While not all wire protocol commands are available for all transports,
200 # if we see a "cmd" value that resembles a known wire protocol command, we
200 # if we see a "cmd" value that resembles a known wire protocol command, we
201 # route it to a protocol handler. This is better than routing possible
201 # route it to a protocol handler. This is better than routing possible
202 # wire protocol requests to hgweb because it prevents hgweb from using
202 # wire protocol requests to hgweb because it prevents hgweb from using
203 # known wire protocol commands and it is less confusing for machine
203 # known wire protocol commands and it is less confusing for machine
204 # clients.
204 # clients.
205 if not iscmd(cmd):
205 if not iscmd(cmd):
206 return False
206 return False
207
207
208 # The "cmd" query string argument is only valid on the root path of the
208 # The "cmd" query string argument is only valid on the root path of the
209 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
209 # repo. e.g. ``/?cmd=foo``, ``/repo?cmd=foo``. URL paths within the repo
210 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
210 # like ``/blah?cmd=foo`` are not allowed. So don't recognize the request
211 # in this case. We send an HTTP 404 for backwards compatibility reasons.
211 # in this case. We send an HTTP 404 for backwards compatibility reasons.
212 if req.dispatchpath:
212 if req.dispatchpath:
213 res.status = hgwebcommon.statusmessage(404)
213 res.status = hgwebcommon.statusmessage(404)
214 res.headers[b'Content-Type'] = HGTYPE
214 res.headers[b'Content-Type'] = HGTYPE
215 # TODO This is not a good response to issue for this request. This
215 # TODO This is not a good response to issue for this request. This
216 # is mostly for BC for now.
216 # is mostly for BC for now.
217 res.setbodybytes(b'0\n%s\n' % b'Not Found')
217 res.setbodybytes(b'0\n%s\n' % b'Not Found')
218 return True
218 return True
219
219
220 proto = httpv1protocolhandler(
220 proto = httpv1protocolhandler(
221 req, repo.ui, lambda perm: checkperm(rctx, req, perm)
221 req, repo.ui, lambda perm: checkperm(rctx, req, perm)
222 )
222 )
223
223
224 # The permissions checker should be the only thing that can raise an
224 # The permissions checker should be the only thing that can raise an
225 # ErrorResponse. It is kind of a layer violation to catch an hgweb
225 # ErrorResponse. It is kind of a layer violation to catch an hgweb
226 # exception here. So consider refactoring into a exception type that
226 # exception here. So consider refactoring into a exception type that
227 # is associated with the wire protocol.
227 # is associated with the wire protocol.
228 try:
228 try:
229 _callhttp(repo, req, res, proto, cmd)
229 _callhttp(repo, req, res, proto, cmd)
230 except hgwebcommon.ErrorResponse as e:
230 except hgwebcommon.ErrorResponse as e:
231 for k, v in e.headers:
231 for k, v in e.headers:
232 res.headers[k] = v
232 res.headers[k] = v
233 res.status = hgwebcommon.statusmessage(
233 res.status = hgwebcommon.statusmessage(
234 e.code, stringutil.forcebytestr(e)
234 e.code, stringutil.forcebytestr(e)
235 )
235 )
236 # TODO This response body assumes the failed command was
236 # TODO This response body assumes the failed command was
237 # "unbundle." That assumption is not always valid.
237 # "unbundle." That assumption is not always valid.
238 res.setbodybytes(b'0\n%s\n' % stringutil.forcebytestr(e))
238 res.setbodybytes(b'0\n%s\n' % stringutil.forcebytestr(e))
239
239
240 return True
240 return True
241
241
242
242
243 def _httpresponsetype(ui, proto, prefer_uncompressed):
243 def _httpresponsetype(ui, proto, prefer_uncompressed):
244 """Determine the appropriate response type and compression settings.
244 """Determine the appropriate response type and compression settings.
245
245
246 Returns a tuple of (mediatype, compengine, engineopts).
246 Returns a tuple of (mediatype, compengine, engineopts).
247 """
247 """
248 # Determine the response media type and compression engine based
248 # Determine the response media type and compression engine based
249 # on the request parameters.
249 # on the request parameters.
250
250
251 if b'0.2' in proto.getprotocaps():
251 if b'0.2' in proto.getprotocaps():
252 # All clients are expected to support uncompressed data.
252 # All clients are expected to support uncompressed data.
253 if prefer_uncompressed:
253 if prefer_uncompressed:
254 return HGTYPE2, compression._noopengine(), {}
254 return HGTYPE2, compression._noopengine(), {}
255
255
256 # Now find an agreed upon compression format.
256 # Now find an agreed upon compression format.
257 compformats = wireprotov1server.clientcompressionsupport(proto)
257 compformats = wireprotov1server.clientcompressionsupport(proto)
258 for engine in wireprototypes.supportedcompengines(
258 for engine in wireprototypes.supportedcompengines(
259 ui, compression.SERVERROLE
259 ui, compression.SERVERROLE
260 ):
260 ):
261 if engine.wireprotosupport().name in compformats:
261 if engine.wireprotosupport().name in compformats:
262 opts = {}
262 opts = {}
263 level = ui.configint(b'server', b'%slevel' % engine.name())
263 level = ui.configint(b'server', b'%slevel' % engine.name())
264 if level is not None:
264 if level is not None:
265 opts[b'level'] = level
265 opts[b'level'] = level
266
266
267 return HGTYPE2, engine, opts
267 return HGTYPE2, engine, opts
268
268
269 # No mutually supported compression format. Fall back to the
269 # No mutually supported compression format. Fall back to the
270 # legacy protocol.
270 # legacy protocol.
271
271
272 # Don't allow untrusted settings because disabling compression or
272 # Don't allow untrusted settings because disabling compression or
273 # setting a very high compression level could lead to flooding
273 # setting a very high compression level could lead to flooding
274 # the server's network or CPU.
274 # the server's network or CPU.
275 opts = {b'level': ui.configint(b'server', b'zliblevel')}
275 opts = {b'level': ui.configint(b'server', b'zliblevel')}
276 return HGTYPE, util.compengines[b'zlib'], opts
276 return HGTYPE, util.compengines[b'zlib'], opts
277
277
278
278
279 def _callhttp(repo, req, res, proto, cmd):
279 def _callhttp(repo, req, res, proto, cmd):
280 # Avoid cycle involving hg module.
280 # Avoid cycle involving hg module.
281 from .hgweb import common as hgwebcommon
281 from .hgweb import common as hgwebcommon
282
282
283 def genversion2(gen, engine, engineopts):
283 def genversion2(gen, engine, engineopts):
284 # application/mercurial-0.2 always sends a payload header
284 # application/mercurial-0.2 always sends a payload header
285 # identifying the compression engine.
285 # identifying the compression engine.
286 name = engine.wireprotosupport().name
286 name = engine.wireprotosupport().name
287 assert 0 < len(name) < 256
287 assert 0 < len(name) < 256
288 yield struct.pack(b'B', len(name))
288 yield struct.pack(b'B', len(name))
289 yield name
289 yield name
290
290
291 for chunk in gen:
291 for chunk in gen:
292 yield chunk
292 yield chunk
293
293
294 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
294 def setresponse(code, contenttype, bodybytes=None, bodygen=None):
295 if code == HTTP_OK:
295 if code == HTTP_OK:
296 res.status = b'200 Script output follows'
296 res.status = b'200 Script output follows'
297 else:
297 else:
298 res.status = hgwebcommon.statusmessage(code)
298 res.status = hgwebcommon.statusmessage(code)
299
299
300 res.headers[b'Content-Type'] = contenttype
300 res.headers[b'Content-Type'] = contenttype
301
301
302 if bodybytes is not None:
302 if bodybytes is not None:
303 res.setbodybytes(bodybytes)
303 res.setbodybytes(bodybytes)
304 if bodygen is not None:
304 if bodygen is not None:
305 res.setbodygen(bodygen)
305 res.setbodygen(bodygen)
306
306
307 if not wireprotov1server.commands.commandavailable(cmd, proto):
307 if not wireprotov1server.commands.commandavailable(cmd, proto):
308 setresponse(
308 setresponse(
309 HTTP_OK,
309 HTTP_OK,
310 HGERRTYPE,
310 HGERRTYPE,
311 _(
311 _(
312 b'requested wire protocol command is not available over '
312 b'requested wire protocol command is not available over '
313 b'HTTP'
313 b'HTTP'
314 ),
314 ),
315 )
315 )
316 return
316 return
317
317
318 proto.checkperm(wireprotov1server.commands[cmd].permission)
318 proto.checkperm(wireprotov1server.commands[cmd].permission)
319
319
320 accesshidden = hgwebcommon.hashiddenaccess(repo, req)
320 accesshidden = hgwebcommon.hashiddenaccess(repo, req)
321 rsp = wireprotov1server.dispatch(repo, proto, cmd, accesshidden)
321 rsp = wireprotov1server.dispatch(repo, proto, cmd, accesshidden)
322
322
323 if isinstance(rsp, bytes):
323 if isinstance(rsp, bytes):
324 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
324 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
325 elif isinstance(rsp, wireprototypes.bytesresponse):
325 elif isinstance(rsp, wireprototypes.bytesresponse):
326 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
326 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp.data)
327 elif isinstance(rsp, wireprototypes.streamreslegacy):
327 elif isinstance(rsp, wireprototypes.streamreslegacy):
328 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
328 setresponse(HTTP_OK, HGTYPE, bodygen=rsp.gen)
329 elif isinstance(rsp, wireprototypes.streamres):
329 elif isinstance(rsp, wireprototypes.streamres):
330 gen = rsp.gen
330 gen = rsp.gen
331
331
332 # This code for compression should not be streamres specific. It
332 # This code for compression should not be streamres specific. It
333 # is here because we only compress streamres at the moment.
333 # is here because we only compress streamres at the moment.
334 mediatype, engine, engineopts = _httpresponsetype(
334 mediatype, engine, engineopts = _httpresponsetype(
335 repo.ui, proto, rsp.prefer_uncompressed
335 repo.ui, proto, rsp.prefer_uncompressed
336 )
336 )
337 gen = engine.compressstream(gen, engineopts)
337 gen = engine.compressstream(gen, engineopts)
338
338
339 if mediatype == HGTYPE2:
339 if mediatype == HGTYPE2:
340 gen = genversion2(gen, engine, engineopts)
340 gen = genversion2(gen, engine, engineopts)
341
341
342 setresponse(HTTP_OK, mediatype, bodygen=gen)
342 setresponse(HTTP_OK, mediatype, bodygen=gen)
343 elif isinstance(rsp, wireprototypes.pushres):
343 elif isinstance(rsp, wireprototypes.pushres):
344 rsp = b'%d\n%s' % (rsp.res, rsp.output)
344 rsp = b'%d\n%s' % (rsp.res, rsp.output)
345 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
345 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
346 elif isinstance(rsp, wireprototypes.pusherr):
346 elif isinstance(rsp, wireprototypes.pusherr):
347 rsp = b'0\n%s\n' % rsp.res
347 rsp = b'0\n%s\n' % rsp.res
348 res.drain = True
348 res.drain = True
349 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
349 setresponse(HTTP_OK, HGTYPE, bodybytes=rsp)
350 elif isinstance(rsp, wireprototypes.ooberror):
350 elif isinstance(rsp, wireprototypes.ooberror):
351 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
351 setresponse(HTTP_OK, HGERRTYPE, bodybytes=rsp.message)
352 else:
352 else:
353 raise error.ProgrammingError(b'hgweb.protocol internal failure', rsp)
353 raise error.ProgrammingError(b'hgweb.protocol internal failure', rsp)
354
354
355
355
356 def _sshv1respondbytes(fout, value):
356 def _sshv1respondbytes(fout, value):
357 """Send a bytes response for protocol version 1."""
357 """Send a bytes response for protocol version 1."""
358 fout.write(b'%d\n' % len(value))
358 fout.write(b'%d\n' % len(value))
359 fout.write(value)
359 fout.write(value)
360 fout.flush()
360 fout.flush()
361
361
362
362
363 def _sshv1respondstream(fout, source):
363 def _sshv1respondstream(fout, source):
364 write = fout.write
364 write = fout.write
365 for chunk in source.gen:
365 for chunk in source.gen:
366 write(chunk)
366 write(chunk)
367 fout.flush()
367 fout.flush()
368
368
369
369
370 def _sshv1respondooberror(fout, ferr, rsp):
370 def _sshv1respondooberror(fout, ferr, rsp):
371 ferr.write(b'%s\n-\n' % rsp)
371 ferr.write(b'%s\n-\n' % rsp)
372 ferr.flush()
372 ferr.flush()
373 fout.write(b'\n')
373 fout.write(b'\n')
374 fout.flush()
374 fout.flush()
375
375
376
376
377 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
377 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
378 class sshv1protocolhandler:
378 class sshv1protocolhandler:
379 """Handler for requests services via version 1 of SSH protocol."""
379 """Handler for requests services via version 1 of SSH protocol."""
380
380
381 def __init__(self, ui, fin, fout):
381 def __init__(self, ui, fin, fout):
382 self._ui = ui
382 self._ui = ui
383 self._fin = fin
383 self._fin = fin
384 self._fout = fout
384 self._fout = fout
385 self._protocaps = set()
385 self._protocaps = set()
386
386
387 @property
387 @property
388 def name(self):
388 def name(self):
389 return wireprototypes.SSHV1
389 return wireprototypes.SSHV1
390
390
391 def getargs(self, args):
391 def getargs(self, args):
392 data = {}
392 data = {}
393 keys = args.split()
393 keys = args.split()
394 for n in range(len(keys)):
394 for n in range(len(keys)):
395 argline = self._fin.readline()[:-1]
395 argline = self._fin.readline()[:-1]
396 arg, l = argline.split()
396 arg, l = argline.split()
397 if arg not in keys:
397 if arg not in keys:
398 raise error.Abort(_(b"unexpected parameter %r") % arg)
398 raise error.Abort(_(b"unexpected parameter %r") % arg)
399 if arg == b'*':
399 if arg == b'*':
400 star = {}
400 star = {}
401 for k in range(int(l)):
401 for k in range(int(l)):
402 argline = self._fin.readline()[:-1]
402 argline = self._fin.readline()[:-1]
403 arg, l = argline.split()
403 arg, l = argline.split()
404 val = self._fin.read(int(l))
404 val = self._fin.read(int(l))
405 star[arg] = val
405 star[arg] = val
406 data[b'*'] = star
406 data[b'*'] = star
407 else:
407 else:
408 val = self._fin.read(int(l))
408 val = self._fin.read(int(l))
409 data[arg] = val
409 data[arg] = val
410 return [data[k] for k in keys]
410 return [data[k] for k in keys]
411
411
412 def getprotocaps(self):
412 def getprotocaps(self):
413 return self._protocaps
413 return self._protocaps
414
414
415 def getpayload(self):
415 def getpayload(self):
416 # We initially send an empty response. This tells the client it is
416 # We initially send an empty response. This tells the client it is
417 # OK to start sending data. If a client sees any other response, it
417 # OK to start sending data. If a client sees any other response, it
418 # interprets it as an error.
418 # interprets it as an error.
419 _sshv1respondbytes(self._fout, b'')
419 _sshv1respondbytes(self._fout, b'')
420
420
421 # The file is in the form:
421 # The file is in the form:
422 #
422 #
423 # <chunk size>\n<chunk>
423 # <chunk size>\n<chunk>
424 # ...
424 # ...
425 # 0\n
425 # 0\n
426 count = int(self._fin.readline())
426 count = int(self._fin.readline())
427 while count:
427 while count:
428 yield self._fin.read(count)
428 yield self._fin.read(count)
429 count = int(self._fin.readline())
429 count = int(self._fin.readline())
430
430
431 @contextlib.contextmanager
431 @contextlib.contextmanager
432 def mayberedirectstdio(self):
432 def mayberedirectstdio(self):
433 yield None
433 yield None
434
434
435 def client(self):
435 def client(self):
436 client = encoding.environ.get(b'SSH_CLIENT', b'').split(b' ', 1)[0]
436 client = encoding.environ.get(b'SSH_CLIENT', b'').split(b' ', 1)[0]
437 return b'remote:ssh:' + client
437 return b'remote:ssh:' + client
438
438
439 def addcapabilities(self, repo, caps):
439 def addcapabilities(self, repo, caps):
440 if self.name == wireprototypes.SSHV1:
440 if self.name == wireprototypes.SSHV1:
441 caps.append(b'protocaps')
441 caps.append(b'protocaps')
442 caps.append(b'batch')
442 caps.append(b'batch')
443 return caps
443 return caps
444
444
445 def checkperm(self, perm):
445 def checkperm(self, perm):
446 pass
446 pass
447
447
448
448
449 def _runsshserver(ui, repo, fin, fout, ev, accesshidden=False):
449 def _runsshserver(ui, repo, fin, fout, ev, accesshidden=False):
450 # This function operates like a state machine of sorts. The following
450 # This function operates like a state machine of sorts. The following
451 # states are defined:
451 # states are defined:
452 #
452 #
453 # protov1-serving
453 # protov1-serving
454 # Server is in protocol version 1 serving mode. Commands arrive on
454 # Server is in protocol version 1 serving mode. Commands arrive on
455 # new lines. These commands are processed in this state, one command
455 # new lines. These commands are processed in this state, one command
456 # after the other.
456 # after the other.
457 #
457 #
458 # shutdown
458 # shutdown
459 # The server is shutting down, possibly in reaction to a client event.
459 # The server is shutting down, possibly in reaction to a client event.
460 #
460 #
461 # And here are their transitions:
461 # And here are their transitions:
462 #
462 #
463 # protov1-serving -> shutdown
463 # protov1-serving -> shutdown
464 # When server receives an empty request or encounters another
464 # When server receives an empty request or encounters another
465 # error.
465 # error.
466
466
467 state = b'protov1-serving'
467 state = b'protov1-serving'
468 proto = sshv1protocolhandler(ui, fin, fout)
468 proto = sshv1protocolhandler(ui, fin, fout)
469
469
470 while not ev.is_set():
470 while not ev.is_set():
471 if state == b'protov1-serving':
471 if state == b'protov1-serving':
472 # Commands are issued on new lines.
472 # Commands are issued on new lines.
473 request = fin.readline()[:-1]
473 request = fin.readline()[:-1]
474
474
475 # Empty lines signal to terminate the connection.
475 # Empty lines signal to terminate the connection.
476 if not request:
476 if not request:
477 state = b'shutdown'
477 state = b'shutdown'
478 continue
478 continue
479
479
480 available = wireprotov1server.commands.commandavailable(
480 available = wireprotov1server.commands.commandavailable(
481 request, proto
481 request, proto
482 )
482 )
483
483
484 # This command isn't available. Send an empty response and go
484 # This command isn't available. Send an empty response and go
485 # back to waiting for a new command.
485 # back to waiting for a new command.
486 if not available:
486 if not available:
487 _sshv1respondbytes(fout, b'')
487 _sshv1respondbytes(fout, b'')
488 continue
488 continue
489
489
490 rsp = wireprotov1server.dispatch(
490 rsp = wireprotov1server.dispatch(
491 repo, proto, request, accesshidden=accesshidden
491 repo, proto, request, accesshidden=accesshidden
492 )
492 )
493 repo.ui.fout.flush()
493 repo.ui.fout.flush()
494 repo.ui.ferr.flush()
494 repo.ui.ferr.flush()
495
495
496 if isinstance(rsp, bytes):
496 if isinstance(rsp, bytes):
497 _sshv1respondbytes(fout, rsp)
497 _sshv1respondbytes(fout, rsp)
498 elif isinstance(rsp, wireprototypes.bytesresponse):
498 elif isinstance(rsp, wireprototypes.bytesresponse):
499 _sshv1respondbytes(fout, rsp.data)
499 _sshv1respondbytes(fout, rsp.data)
500 elif isinstance(rsp, wireprototypes.streamres):
500 elif isinstance(rsp, wireprototypes.streamres):
501 _sshv1respondstream(fout, rsp)
501 _sshv1respondstream(fout, rsp)
502 elif isinstance(rsp, wireprototypes.streamreslegacy):
502 elif isinstance(rsp, wireprototypes.streamreslegacy):
503 _sshv1respondstream(fout, rsp)
503 _sshv1respondstream(fout, rsp)
504 elif isinstance(rsp, wireprototypes.pushres):
504 elif isinstance(rsp, wireprototypes.pushres):
505 _sshv1respondbytes(fout, b'')
505 _sshv1respondbytes(fout, b'')
506 _sshv1respondbytes(fout, b'%d' % rsp.res)
506 _sshv1respondbytes(fout, b'%d' % rsp.res)
507 elif isinstance(rsp, wireprototypes.pusherr):
507 elif isinstance(rsp, wireprototypes.pusherr):
508 _sshv1respondbytes(fout, rsp.res)
508 _sshv1respondbytes(fout, rsp.res)
509 elif isinstance(rsp, wireprototypes.ooberror):
509 elif isinstance(rsp, wireprototypes.ooberror):
510 _sshv1respondooberror(fout, ui.ferr, rsp.message)
510 _sshv1respondooberror(fout, ui.ferr, rsp.message)
511 else:
511 else:
512 raise error.ProgrammingError(
512 raise error.ProgrammingError(
513 b'unhandled response type from '
513 b'unhandled response type from '
514 b'wire protocol command: %s' % rsp
514 b'wire protocol command: %s' % rsp
515 )
515 )
516
516
517 elif state == b'shutdown':
517 elif state == b'shutdown':
518 break
518 break
519
519
520 else:
520 else:
521 raise error.ProgrammingError(
521 raise error.ProgrammingError(
522 b'unhandled ssh server state: %s' % state
522 b'unhandled ssh server state: %s' % state
523 )
523 )
524
524
525
525
526 class sshserver:
526 class sshserver:
527 def __init__(self, ui, repo, logfh=None, accesshidden=False):
527 def __init__(self, ui, repo, logfh=None, accesshidden=False):
528 self._ui = ui
528 self._ui = ui
529 self._repo = repo
529 self._repo = repo
530 self._fin, self._fout = ui.protectfinout()
531 self._accesshidden = accesshidden
530 self._accesshidden = accesshidden
532
531 self._logfh = logfh
533 # Log write I/O to stdout and stderr if configured.
534 if logfh:
535 self._fout = util.makeloggingfileobject(
536 logfh, self._fout, b'o', logdata=True
537 )
538 ui.ferr = util.makeloggingfileobject(
539 logfh, ui.ferr, b'e', logdata=True
540 )
541
532
542 def serve_forever(self):
533 def serve_forever(self):
543 self.serveuntil(threading.Event())
534 self.serveuntil(threading.Event())
544 self._ui.restorefinout(self._fin, self._fout)
545
535
546 def serveuntil(self, ev):
536 def serveuntil(self, ev):
547 """Serve until a threading.Event is set."""
537 """Serve until a threading.Event is set."""
548 _runsshserver(
538 with self._ui.protectedfinout() as (fin, fout):
549 self._ui, self._repo, self._fin, self._fout, ev, self._accesshidden
539 if self._logfh:
550 )
540 # Log write I/O to stdout and stderr if configured.
541 fout = util.makeloggingfileobject(
542 self._logfh,
543 fout,
544 b'o',
545 logdata=True,
546 )
547 self._ui.ferr = util.makeloggingfileobject(
548 self._logfh,
549 self._ui.ferr,
550 b'e',
551 logdata=True,
552 )
553 _runsshserver(
554 self._ui,
555 self._repo,
556 fin,
557 fout,
558 ev,
559 self._accesshidden,
560 )
@@ -1,416 +1,415 b''
1 // dirs_multiset.rs
1 // dirs_multiset.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
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 //! A multiset of directory names.
8 //! A multiset of directory names.
9 //!
9 //!
10 //! Used to counts the references to directories in a manifest or dirstate.
10 //! Used to counts the references to directories in a manifest or dirstate.
11 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
11 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
12 use crate::{
12 use crate::{
13 utils::{
13 utils::{
14 files,
14 files,
15 hg_path::{HgPath, HgPathBuf, HgPathError},
15 hg_path::{HgPath, HgPathBuf, HgPathError},
16 },
16 },
17 DirstateEntry, DirstateError, DirstateMapError, FastHashMap,
17 DirstateEntry, DirstateError, DirstateMapError, FastHashMap,
18 };
18 };
19 use std::collections::{hash_map, hash_map::Entry, HashMap, HashSet};
19 use std::collections::{hash_map, hash_map::Entry, HashMap, HashSet};
20
20
21 // could be encapsulated if we care API stability more seriously
21 // could be encapsulated if we care API stability more seriously
22 pub type DirsMultisetIter<'a> = hash_map::Keys<'a, HgPathBuf, u32>;
22 pub type DirsMultisetIter<'a> = hash_map::Keys<'a, HgPathBuf, u32>;
23
23
24 #[derive(PartialEq, Debug)]
24 #[derive(PartialEq, Debug)]
25 pub struct DirsMultiset {
25 pub struct DirsMultiset {
26 inner: FastHashMap<HgPathBuf, u32>,
26 inner: FastHashMap<HgPathBuf, u32>,
27 }
27 }
28
28
29 impl DirsMultiset {
29 impl DirsMultiset {
30 /// Initializes the multiset from a dirstate.
30 /// Initializes the multiset from a dirstate.
31 ///
31 ///
32 /// If `skip_state` is provided, skips dirstate entries with equal state.
32 /// If `skip_state` is provided, skips dirstate entries with equal state.
33 pub fn from_dirstate<I, P>(
33 pub fn from_dirstate<I, P>(
34 dirstate: I,
34 dirstate: I,
35 only_tracked: bool,
35 only_tracked: bool,
36 ) -> Result<Self, DirstateError>
36 ) -> Result<Self, DirstateError>
37 where
37 where
38 I: IntoIterator<
38 I: IntoIterator<
39 Item = Result<(P, DirstateEntry), DirstateV2ParseError>,
39 Item = Result<(P, DirstateEntry), DirstateV2ParseError>,
40 >,
40 >,
41 P: AsRef<HgPath>,
41 P: AsRef<HgPath>,
42 {
42 {
43 let mut multiset = DirsMultiset {
43 let mut multiset = DirsMultiset {
44 inner: FastHashMap::default(),
44 inner: FastHashMap::default(),
45 };
45 };
46 for item in dirstate {
46 for item in dirstate {
47 let (filename, entry) = item?;
47 let (filename, entry) = item?;
48 let filename = filename.as_ref();
48 let filename = filename.as_ref();
49 // This `if` is optimized out of the loop
49 // This `if` is optimized out of the loop
50 if only_tracked {
50 if only_tracked {
51 if !entry.removed() {
51 if !entry.removed() {
52 multiset.add_path(filename)?;
52 multiset.add_path(filename)?;
53 }
53 }
54 } else {
54 } else {
55 multiset.add_path(filename)?;
55 multiset.add_path(filename)?;
56 }
56 }
57 }
57 }
58
58
59 Ok(multiset)
59 Ok(multiset)
60 }
60 }
61
61
62 /// Initializes the multiset from a manifest.
62 /// Initializes the multiset from a manifest.
63 pub fn from_manifest(
63 pub fn from_manifest(
64 manifest: &[impl AsRef<HgPath>],
64 manifest: &[impl AsRef<HgPath>],
65 ) -> Result<Self, HgPathError> {
65 ) -> Result<Self, HgPathError> {
66 let mut multiset = DirsMultiset {
66 let mut multiset = DirsMultiset {
67 inner: FastHashMap::default(),
67 inner: FastHashMap::default(),
68 };
68 };
69
69
70 for filename in manifest {
70 for filename in manifest {
71 multiset.add_path(filename.as_ref())?;
71 multiset.add_path(filename.as_ref())?;
72 }
72 }
73
73
74 Ok(multiset)
74 Ok(multiset)
75 }
75 }
76
76
77 /// Increases the count of deepest directory contained in the path.
77 /// Increases the count of deepest directory contained in the path.
78 ///
78 ///
79 /// If the directory is not yet in the map, adds its parents.
79 /// If the directory is not yet in the map, adds its parents.
80 pub fn add_path(
80 pub fn add_path(
81 &mut self,
81 &mut self,
82 path: impl AsRef<HgPath>,
82 path: impl AsRef<HgPath>,
83 ) -> Result<(), HgPathError> {
83 ) -> Result<(), HgPathError> {
84 for subpath in files::find_dirs(path.as_ref()) {
84 for subpath in files::find_dirs(path.as_ref()) {
85 if subpath.as_bytes().last() == Some(&b'/') {
85 if subpath.as_bytes().last() == Some(&b'/') {
86 // TODO Remove this once PathAuditor is certified
86 // TODO Remove this once PathAuditor is certified
87 // as the only entrypoint for path data
87 // as the only entrypoint for path data
88 let second_slash_index = subpath.len() - 1;
88 let second_slash_index = subpath.len() - 1;
89
89
90 return Err(HgPathError::ConsecutiveSlashes {
90 return Err(HgPathError::ConsecutiveSlashes {
91 bytes: path.as_ref().as_bytes().to_owned(),
91 bytes: path.as_ref().as_bytes().to_owned(),
92 second_slash_index,
92 second_slash_index,
93 });
93 });
94 }
94 }
95 if let Some(val) = self.inner.get_mut(subpath) {
95 if let Some(val) = self.inner.get_mut(subpath) {
96 *val += 1;
96 *val += 1;
97 break;
97 break;
98 }
98 }
99 self.inner.insert(subpath.to_owned(), 1);
99 self.inner.insert(subpath.to_owned(), 1);
100 }
100 }
101 Ok(())
101 Ok(())
102 }
102 }
103
103
104 /// Decreases the count of deepest directory contained in the path.
104 /// Decreases the count of deepest directory contained in the path.
105 ///
105 ///
106 /// If it is the only reference, decreases all parents until one is
106 /// If it is the only reference, decreases all parents until one is
107 /// removed.
107 /// removed.
108 /// If the directory is not in the map, something horrible has happened.
108 /// If the directory is not in the map, something horrible has happened.
109 pub fn delete_path(
109 pub fn delete_path(
110 &mut self,
110 &mut self,
111 path: impl AsRef<HgPath>,
111 path: impl AsRef<HgPath>,
112 ) -> Result<(), DirstateMapError> {
112 ) -> Result<(), DirstateMapError> {
113 for subpath in files::find_dirs(path.as_ref()) {
113 for subpath in files::find_dirs(path.as_ref()) {
114 match self.inner.entry(subpath.to_owned()) {
114 match self.inner.entry(subpath.to_owned()) {
115 Entry::Occupied(mut entry) => {
115 Entry::Occupied(mut entry) => {
116 let val = *entry.get();
116 let val = *entry.get();
117 if val > 1 {
117 if val > 1 {
118 entry.insert(val - 1);
118 entry.insert(val - 1);
119 break;
119 break;
120 }
120 }
121 entry.remove();
121 entry.remove();
122 }
122 }
123 Entry::Vacant(_) => {
123 Entry::Vacant(_) => {
124 return Err(DirstateMapError::PathNotFound(
124 return Err(DirstateMapError::PathNotFound(
125 path.as_ref().to_owned(),
125 path.as_ref().to_owned(),
126 ))
126 ))
127 }
127 }
128 };
128 };
129 }
129 }
130
130
131 Ok(())
131 Ok(())
132 }
132 }
133
133
134 pub fn contains(&self, key: impl AsRef<HgPath>) -> bool {
134 pub fn contains(&self, key: impl AsRef<HgPath>) -> bool {
135 self.inner.contains_key(key.as_ref())
135 self.inner.contains_key(key.as_ref())
136 }
136 }
137
137
138 pub fn iter(&self) -> DirsMultisetIter {
138 pub fn iter(&self) -> DirsMultisetIter {
139 self.inner.keys()
139 self.inner.keys()
140 }
140 }
141
141
142 pub fn len(&self) -> usize {
142 pub fn len(&self) -> usize {
143 self.inner.len()
143 self.inner.len()
144 }
144 }
145
145
146 pub fn is_empty(&self) -> bool {
146 pub fn is_empty(&self) -> bool {
147 self.len() == 0
147 self.len() == 0
148 }
148 }
149 }
149 }
150
150
151 /// This is basically a reimplementation of `DirsMultiset` that stores the
151 /// This is basically a reimplementation of `DirsMultiset` that stores the
152 /// children instead of just a count of them, plus a small optional
152 /// children instead of just a count of them, plus a small optional
153 /// optimization to avoid some directories we don't need.
153 /// optimization to avoid some directories we don't need.
154 #[derive(PartialEq, Debug)]
154 #[derive(PartialEq, Debug)]
155 pub struct DirsChildrenMultiset<'a> {
155 pub struct DirsChildrenMultiset<'a> {
156 inner: FastHashMap<&'a HgPath, HashSet<&'a HgPath>>,
156 inner: FastHashMap<&'a HgPath, HashSet<&'a HgPath>>,
157 only_include: Option<HashSet<&'a HgPath>>,
157 only_include: Option<HashSet<&'a HgPath>>,
158 }
158 }
159
159
160 impl<'a> DirsChildrenMultiset<'a> {
160 impl<'a> DirsChildrenMultiset<'a> {
161 pub fn new(
161 pub fn new<I: Iterator<Item = &'a HgPathBuf>>(
162 paths: impl Iterator<Item = &'a HgPathBuf>,
162 paths: impl Iterator<Item = &'a HgPathBuf>,
163 only_include: Option<&'a HashSet<impl AsRef<HgPath> + 'a>>,
163 only_include: Option<I>,
164 ) -> Self {
164 ) -> Self {
165 let mut new = Self {
165 let mut new = Self {
166 inner: HashMap::default(),
166 inner: HashMap::default(),
167 only_include: only_include
167 only_include: only_include.map(|s| s.map(AsRef::as_ref).collect()),
168 .map(|s| s.iter().map(AsRef::as_ref).collect()),
169 };
168 };
170
169
171 for path in paths {
170 for path in paths {
172 new.add_path(path)
171 new.add_path(path)
173 }
172 }
174
173
175 new
174 new
176 }
175 }
177 fn add_path(&mut self, path: &'a (impl AsRef<HgPath> + 'a)) {
176 fn add_path(&mut self, path: &'a (impl AsRef<HgPath> + 'a)) {
178 if path.as_ref().is_empty() {
177 if path.as_ref().is_empty() {
179 return;
178 return;
180 }
179 }
181 for (directory, basename) in files::find_dirs_with_base(path.as_ref())
180 for (directory, basename) in files::find_dirs_with_base(path.as_ref())
182 {
181 {
183 if !self.is_dir_included(directory) {
182 if !self.is_dir_included(directory) {
184 continue;
183 continue;
185 }
184 }
186 self.inner
185 self.inner
187 .entry(directory)
186 .entry(directory)
188 .and_modify(|e| {
187 .and_modify(|e| {
189 e.insert(basename);
188 e.insert(basename);
190 })
189 })
191 .or_insert_with(|| {
190 .or_insert_with(|| {
192 let mut set = HashSet::new();
191 let mut set = HashSet::new();
193 set.insert(basename);
192 set.insert(basename);
194 set
193 set
195 });
194 });
196 }
195 }
197 }
196 }
198 fn is_dir_included(&self, dir: impl AsRef<HgPath>) -> bool {
197 fn is_dir_included(&self, dir: impl AsRef<HgPath>) -> bool {
199 match &self.only_include {
198 match &self.only_include {
200 None => false,
199 None => false,
201 Some(i) => i.contains(dir.as_ref()),
200 Some(i) => i.contains(dir.as_ref()),
202 }
201 }
203 }
202 }
204
203
205 pub fn get(
204 pub fn get(
206 &self,
205 &self,
207 path: impl AsRef<HgPath>,
206 path: impl AsRef<HgPath>,
208 ) -> Option<&HashSet<&'a HgPath>> {
207 ) -> Option<&HashSet<&'a HgPath>> {
209 self.inner.get(path.as_ref())
208 self.inner.get(path.as_ref())
210 }
209 }
211 }
210 }
212
211
213 #[cfg(test)]
212 #[cfg(test)]
214 mod tests {
213 mod tests {
215 use crate::EntryState;
214 use crate::EntryState;
216
215
217 use super::*;
216 use super::*;
218
217
219 #[test]
218 #[test]
220 fn test_delete_path_path_not_found() {
219 fn test_delete_path_path_not_found() {
221 let manifest: Vec<HgPathBuf> = vec![];
220 let manifest: Vec<HgPathBuf> = vec![];
222 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
221 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
223 let path = HgPathBuf::from_bytes(b"doesnotexist/");
222 let path = HgPathBuf::from_bytes(b"doesnotexist/");
224 assert_eq!(
223 assert_eq!(
225 Err(DirstateMapError::PathNotFound(path.to_owned())),
224 Err(DirstateMapError::PathNotFound(path.to_owned())),
226 map.delete_path(&path)
225 map.delete_path(&path)
227 );
226 );
228 }
227 }
229
228
230 #[test]
229 #[test]
231 fn test_delete_path_empty_path() {
230 fn test_delete_path_empty_path() {
232 let mut map =
231 let mut map =
233 DirsMultiset::from_manifest(&[HgPathBuf::new()]).unwrap();
232 DirsMultiset::from_manifest(&[HgPathBuf::new()]).unwrap();
234 let path = HgPath::new(b"");
233 let path = HgPath::new(b"");
235 assert_eq!(Ok(()), map.delete_path(path));
234 assert_eq!(Ok(()), map.delete_path(path));
236 assert_eq!(
235 assert_eq!(
237 Err(DirstateMapError::PathNotFound(path.to_owned())),
236 Err(DirstateMapError::PathNotFound(path.to_owned())),
238 map.delete_path(path)
237 map.delete_path(path)
239 );
238 );
240 }
239 }
241
240
242 #[test]
241 #[test]
243 fn test_delete_path_successful() {
242 fn test_delete_path_successful() {
244 let mut map = DirsMultiset {
243 let mut map = DirsMultiset {
245 inner: [("", 5), ("a", 3), ("a/b", 2), ("a/c", 1)]
244 inner: [("", 5), ("a", 3), ("a/b", 2), ("a/c", 1)]
246 .iter()
245 .iter()
247 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
246 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
248 .collect(),
247 .collect(),
249 };
248 };
250
249
251 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/b/")));
250 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/b/")));
252 eprintln!("{:?}", map);
251 eprintln!("{:?}", map);
253 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/b/")));
252 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/b/")));
254 eprintln!("{:?}", map);
253 eprintln!("{:?}", map);
255 assert_eq!(
254 assert_eq!(
256 Err(DirstateMapError::PathNotFound(HgPathBuf::from_bytes(
255 Err(DirstateMapError::PathNotFound(HgPathBuf::from_bytes(
257 b"a/b/"
256 b"a/b/"
258 ))),
257 ))),
259 map.delete_path(HgPath::new(b"a/b/"))
258 map.delete_path(HgPath::new(b"a/b/"))
260 );
259 );
261
260
262 assert_eq!(2, *map.inner.get(HgPath::new(b"a")).unwrap());
261 assert_eq!(2, *map.inner.get(HgPath::new(b"a")).unwrap());
263 assert_eq!(1, *map.inner.get(HgPath::new(b"a/c")).unwrap());
262 assert_eq!(1, *map.inner.get(HgPath::new(b"a/c")).unwrap());
264 eprintln!("{:?}", map);
263 eprintln!("{:?}", map);
265 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/")));
264 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/")));
266 eprintln!("{:?}", map);
265 eprintln!("{:?}", map);
267
266
268 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/c/")));
267 assert_eq!(Ok(()), map.delete_path(HgPath::new(b"a/c/")));
269 assert_eq!(
268 assert_eq!(
270 Err(DirstateMapError::PathNotFound(HgPathBuf::from_bytes(
269 Err(DirstateMapError::PathNotFound(HgPathBuf::from_bytes(
271 b"a/c/"
270 b"a/c/"
272 ))),
271 ))),
273 map.delete_path(HgPath::new(b"a/c/"))
272 map.delete_path(HgPath::new(b"a/c/"))
274 );
273 );
275 }
274 }
276
275
277 #[test]
276 #[test]
278 fn test_add_path_empty_path() {
277 fn test_add_path_empty_path() {
279 let manifest: Vec<HgPathBuf> = vec![];
278 let manifest: Vec<HgPathBuf> = vec![];
280 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
279 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
281 let path = HgPath::new(b"");
280 let path = HgPath::new(b"");
282 map.add_path(path).unwrap();
281 map.add_path(path).unwrap();
283
282
284 assert_eq!(1, map.len());
283 assert_eq!(1, map.len());
285 }
284 }
286
285
287 #[test]
286 #[test]
288 fn test_add_path_successful() {
287 fn test_add_path_successful() {
289 let manifest: Vec<HgPathBuf> = vec![];
288 let manifest: Vec<HgPathBuf> = vec![];
290 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
289 let mut map = DirsMultiset::from_manifest(&manifest).unwrap();
291
290
292 map.add_path(HgPath::new(b"a/")).unwrap();
291 map.add_path(HgPath::new(b"a/")).unwrap();
293 assert_eq!(1, *map.inner.get(HgPath::new(b"a")).unwrap());
292 assert_eq!(1, *map.inner.get(HgPath::new(b"a")).unwrap());
294 assert_eq!(1, *map.inner.get(HgPath::new(b"")).unwrap());
293 assert_eq!(1, *map.inner.get(HgPath::new(b"")).unwrap());
295 assert_eq!(2, map.len());
294 assert_eq!(2, map.len());
296
295
297 // Non directory should be ignored
296 // Non directory should be ignored
298 map.add_path(HgPath::new(b"a")).unwrap();
297 map.add_path(HgPath::new(b"a")).unwrap();
299 assert_eq!(1, *map.inner.get(HgPath::new(b"a")).unwrap());
298 assert_eq!(1, *map.inner.get(HgPath::new(b"a")).unwrap());
300 assert_eq!(2, map.len());
299 assert_eq!(2, map.len());
301
300
302 // Non directory will still add its base
301 // Non directory will still add its base
303 map.add_path(HgPath::new(b"a/b")).unwrap();
302 map.add_path(HgPath::new(b"a/b")).unwrap();
304 assert_eq!(2, *map.inner.get(HgPath::new(b"a")).unwrap());
303 assert_eq!(2, *map.inner.get(HgPath::new(b"a")).unwrap());
305 assert_eq!(2, map.len());
304 assert_eq!(2, map.len());
306
305
307 // Duplicate path works
306 // Duplicate path works
308 map.add_path(HgPath::new(b"a/")).unwrap();
307 map.add_path(HgPath::new(b"a/")).unwrap();
309 assert_eq!(3, *map.inner.get(HgPath::new(b"a")).unwrap());
308 assert_eq!(3, *map.inner.get(HgPath::new(b"a")).unwrap());
310
309
311 // Nested dir adds to its base
310 // Nested dir adds to its base
312 map.add_path(HgPath::new(b"a/b/")).unwrap();
311 map.add_path(HgPath::new(b"a/b/")).unwrap();
313 assert_eq!(4, *map.inner.get(HgPath::new(b"a")).unwrap());
312 assert_eq!(4, *map.inner.get(HgPath::new(b"a")).unwrap());
314 assert_eq!(1, *map.inner.get(HgPath::new(b"a/b")).unwrap());
313 assert_eq!(1, *map.inner.get(HgPath::new(b"a/b")).unwrap());
315
314
316 // but not its base's base, because it already existed
315 // but not its base's base, because it already existed
317 map.add_path(HgPath::new(b"a/b/c/")).unwrap();
316 map.add_path(HgPath::new(b"a/b/c/")).unwrap();
318 assert_eq!(4, *map.inner.get(HgPath::new(b"a")).unwrap());
317 assert_eq!(4, *map.inner.get(HgPath::new(b"a")).unwrap());
319 assert_eq!(2, *map.inner.get(HgPath::new(b"a/b")).unwrap());
318 assert_eq!(2, *map.inner.get(HgPath::new(b"a/b")).unwrap());
320
319
321 map.add_path(HgPath::new(b"a/c/")).unwrap();
320 map.add_path(HgPath::new(b"a/c/")).unwrap();
322 assert_eq!(1, *map.inner.get(HgPath::new(b"a/c")).unwrap());
321 assert_eq!(1, *map.inner.get(HgPath::new(b"a/c")).unwrap());
323
322
324 let expected = DirsMultiset {
323 let expected = DirsMultiset {
325 inner: [("", 2), ("a", 5), ("a/b", 2), ("a/b/c", 1), ("a/c", 1)]
324 inner: [("", 2), ("a", 5), ("a/b", 2), ("a/b/c", 1), ("a/c", 1)]
326 .iter()
325 .iter()
327 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
326 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
328 .collect(),
327 .collect(),
329 };
328 };
330 assert_eq!(map, expected);
329 assert_eq!(map, expected);
331 }
330 }
332
331
333 #[test]
332 #[test]
334 fn test_dirsmultiset_new_empty() {
333 fn test_dirsmultiset_new_empty() {
335 let manifest: Vec<HgPathBuf> = vec![];
334 let manifest: Vec<HgPathBuf> = vec![];
336 let new = DirsMultiset::from_manifest(&manifest).unwrap();
335 let new = DirsMultiset::from_manifest(&manifest).unwrap();
337 let expected = DirsMultiset {
336 let expected = DirsMultiset {
338 inner: FastHashMap::default(),
337 inner: FastHashMap::default(),
339 };
338 };
340 assert_eq!(expected, new);
339 assert_eq!(expected, new);
341
340
342 let new = DirsMultiset::from_dirstate::<_, HgPathBuf>(
341 let new = DirsMultiset::from_dirstate::<_, HgPathBuf>(
343 std::iter::empty(),
342 std::iter::empty(),
344 false,
343 false,
345 )
344 )
346 .unwrap();
345 .unwrap();
347 let expected = DirsMultiset {
346 let expected = DirsMultiset {
348 inner: FastHashMap::default(),
347 inner: FastHashMap::default(),
349 };
348 };
350 assert_eq!(expected, new);
349 assert_eq!(expected, new);
351 }
350 }
352
351
353 #[test]
352 #[test]
354 fn test_dirsmultiset_new_no_skip() {
353 fn test_dirsmultiset_new_no_skip() {
355 let input_vec: Vec<HgPathBuf> = ["a/", "b/", "a/c", "a/d/"]
354 let input_vec: Vec<HgPathBuf> = ["a/", "b/", "a/c", "a/d/"]
356 .iter()
355 .iter()
357 .map(|e| HgPathBuf::from_bytes(e.as_bytes()))
356 .map(|e| HgPathBuf::from_bytes(e.as_bytes()))
358 .collect();
357 .collect();
359 let expected_inner = [("", 2), ("a", 3), ("b", 1), ("a/d", 1)]
358 let expected_inner = [("", 2), ("a", 3), ("b", 1), ("a/d", 1)]
360 .iter()
359 .iter()
361 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
360 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
362 .collect();
361 .collect();
363
362
364 let new = DirsMultiset::from_manifest(&input_vec).unwrap();
363 let new = DirsMultiset::from_manifest(&input_vec).unwrap();
365 let expected = DirsMultiset {
364 let expected = DirsMultiset {
366 inner: expected_inner,
365 inner: expected_inner,
367 };
366 };
368 assert_eq!(expected, new);
367 assert_eq!(expected, new);
369
368
370 let input_map = ["b/x", "a/c", "a/d/x"].iter().map(|f| {
369 let input_map = ["b/x", "a/c", "a/d/x"].iter().map(|f| {
371 Ok((
370 Ok((
372 HgPathBuf::from_bytes(f.as_bytes()),
371 HgPathBuf::from_bytes(f.as_bytes()),
373 DirstateEntry::from_v1_data(EntryState::Normal, 0, 0, 0),
372 DirstateEntry::from_v1_data(EntryState::Normal, 0, 0, 0),
374 ))
373 ))
375 });
374 });
376 let expected_inner = [("", 2), ("a", 2), ("b", 1), ("a/d", 1)]
375 let expected_inner = [("", 2), ("a", 2), ("b", 1), ("a/d", 1)]
377 .iter()
376 .iter()
378 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
377 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
379 .collect();
378 .collect();
380
379
381 let new = DirsMultiset::from_dirstate(input_map, false).unwrap();
380 let new = DirsMultiset::from_dirstate(input_map, false).unwrap();
382 let expected = DirsMultiset {
381 let expected = DirsMultiset {
383 inner: expected_inner,
382 inner: expected_inner,
384 };
383 };
385 assert_eq!(expected, new);
384 assert_eq!(expected, new);
386 }
385 }
387
386
388 #[test]
387 #[test]
389 fn test_dirsmultiset_new_skip() {
388 fn test_dirsmultiset_new_skip() {
390 let input_map = [
389 let input_map = [
391 ("a/", EntryState::Normal),
390 ("a/", EntryState::Normal),
392 ("a/b", EntryState::Normal),
391 ("a/b", EntryState::Normal),
393 ("a/c", EntryState::Removed),
392 ("a/c", EntryState::Removed),
394 ("a/d", EntryState::Merged),
393 ("a/d", EntryState::Merged),
395 ]
394 ]
396 .iter()
395 .iter()
397 .map(|(f, state)| {
396 .map(|(f, state)| {
398 Ok((
397 Ok((
399 HgPathBuf::from_bytes(f.as_bytes()),
398 HgPathBuf::from_bytes(f.as_bytes()),
400 DirstateEntry::from_v1_data(*state, 0, 0, 0),
399 DirstateEntry::from_v1_data(*state, 0, 0, 0),
401 ))
400 ))
402 });
401 });
403
402
404 // "a" incremented with "a/c" and "a/d/"
403 // "a" incremented with "a/c" and "a/d/"
405 let expected_inner = [("", 1), ("a", 3)]
404 let expected_inner = [("", 1), ("a", 3)]
406 .iter()
405 .iter()
407 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
406 .map(|(k, v)| (HgPathBuf::from_bytes(k.as_bytes()), *v))
408 .collect();
407 .collect();
409
408
410 let new = DirsMultiset::from_dirstate(input_map, true).unwrap();
409 let new = DirsMultiset::from_dirstate(input_map, true).unwrap();
411 let expected = DirsMultiset {
410 let expected = DirsMultiset {
412 inner: expected_inner,
411 inner: expected_inner,
413 };
412 };
414 assert_eq!(expected, new);
413 assert_eq!(expected, new);
415 }
414 }
416 }
415 }
@@ -1,874 +1,874 b''
1 // filepatterns.rs
1 // filepatterns.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
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 //! Handling of Mercurial-specific patterns.
8 //! Handling of Mercurial-specific patterns.
9
9
10 use crate::{
10 use crate::{
11 utils::{
11 utils::{
12 files::{canonical_path, get_bytes_from_path, get_path_from_bytes},
12 files::{canonical_path, get_bytes_from_path, get_path_from_bytes},
13 hg_path::{path_to_hg_path_buf, HgPathBuf, HgPathError},
13 hg_path::{path_to_hg_path_buf, HgPathBuf, HgPathError},
14 SliceExt,
14 SliceExt,
15 },
15 },
16 FastHashMap, PatternError,
16 FastHashMap, PatternError,
17 };
17 };
18 use lazy_static::lazy_static;
18 use lazy_static::lazy_static;
19 use regex::bytes::{NoExpand, Regex};
19 use regex::bytes::{NoExpand, Regex};
20 use std::ops::Deref;
20 use std::ops::Deref;
21 use std::path::{Path, PathBuf};
21 use std::path::{Path, PathBuf};
22 use std::vec::Vec;
22 use std::vec::Vec;
23
23
24 lazy_static! {
24 lazy_static! {
25 static ref RE_ESCAPE: Vec<Vec<u8>> = {
25 static ref RE_ESCAPE: Vec<Vec<u8>> = {
26 let mut v: Vec<Vec<u8>> = (0..=255).map(|byte| vec![byte]).collect();
26 let mut v: Vec<Vec<u8>> = (0..=255).map(|byte| vec![byte]).collect();
27 let to_escape = b"()[]{}?*+-|^$\\.&~#\t\n\r\x0b\x0c";
27 let to_escape = b"()[]{}?*+-|^$\\.&~#\t\n\r\x0b\x0c";
28 for byte in to_escape {
28 for byte in to_escape {
29 v[*byte as usize].insert(0, b'\\');
29 v[*byte as usize].insert(0, b'\\');
30 }
30 }
31 v
31 v
32 };
32 };
33 }
33 }
34
34
35 /// These are matched in order
35 /// These are matched in order
36 const GLOB_REPLACEMENTS: &[(&[u8], &[u8])] =
36 const GLOB_REPLACEMENTS: &[(&[u8], &[u8])] =
37 &[(b"*/", b"(?:.*/)?"), (b"*", b".*"), (b"", b"[^/]*")];
37 &[(b"*/", b"(?:.*/)?"), (b"*", b".*"), (b"", b"[^/]*")];
38
38
39 #[derive(Debug, Clone, PartialEq, Eq)]
39 #[derive(Debug, Clone, PartialEq, Eq)]
40 pub enum PatternSyntax {
40 pub enum PatternSyntax {
41 /// A regular expression
41 /// A regular expression
42 Regexp,
42 Regexp,
43 /// Glob that matches at the front of the path
43 /// Glob that matches at the front of the path
44 RootGlob,
44 RootGlob,
45 /// Glob that matches at any suffix of the path (still anchored at
45 /// Glob that matches at any suffix of the path (still anchored at
46 /// slashes)
46 /// slashes)
47 Glob,
47 Glob,
48 /// a path relative to repository root, which is matched recursively
48 /// a path relative to repository root, which is matched recursively
49 Path,
49 Path,
50 /// a single exact path relative to repository root
50 /// a single exact path relative to repository root
51 FilePath,
51 FilePath,
52 /// A path relative to cwd
52 /// A path relative to cwd
53 RelPath,
53 RelPath,
54 /// an unrooted glob (*.rs matches Rust files in all dirs)
54 /// an unrooted glob (*.rs matches Rust files in all dirs)
55 RelGlob,
55 RelGlob,
56 /// A regexp that needn't match the start of a name
56 /// A regexp that needn't match the start of a name
57 RelRegexp,
57 RelRegexp,
58 /// A path relative to repository root, which is matched non-recursively
58 /// A path relative to repository root, which is matched non-recursively
59 /// (will not match subdirectories)
59 /// (will not match subdirectories)
60 RootFiles,
60 RootFilesIn,
61 /// A file of patterns to read and include
61 /// A file of patterns to read and include
62 Include,
62 Include,
63 /// A file of patterns to match against files under the same directory
63 /// A file of patterns to match against files under the same directory
64 SubInclude,
64 SubInclude,
65 /// SubInclude with the result of parsing the included file
65 /// SubInclude with the result of parsing the included file
66 ///
66 ///
67 /// Note: there is no ExpandedInclude because that expansion can be done
67 /// Note: there is no ExpandedInclude because that expansion can be done
68 /// in place by replacing the Include pattern by the included patterns.
68 /// in place by replacing the Include pattern by the included patterns.
69 /// SubInclude requires more handling.
69 /// SubInclude requires more handling.
70 ///
70 ///
71 /// Note: `Box` is used to minimize size impact on other enum variants
71 /// Note: `Box` is used to minimize size impact on other enum variants
72 ExpandedSubInclude(Box<SubInclude>),
72 ExpandedSubInclude(Box<SubInclude>),
73 }
73 }
74
74
75 /// Transforms a glob pattern into a regex
75 /// Transforms a glob pattern into a regex
76 pub fn glob_to_re(pat: &[u8]) -> Vec<u8> {
76 pub fn glob_to_re(pat: &[u8]) -> Vec<u8> {
77 let mut input = pat;
77 let mut input = pat;
78 let mut res: Vec<u8> = vec![];
78 let mut res: Vec<u8> = vec![];
79 let mut group_depth = 0;
79 let mut group_depth = 0;
80
80
81 while let Some((c, rest)) = input.split_first() {
81 while let Some((c, rest)) = input.split_first() {
82 input = rest;
82 input = rest;
83
83
84 match c {
84 match c {
85 b'*' => {
85 b'*' => {
86 for (source, repl) in GLOB_REPLACEMENTS {
86 for (source, repl) in GLOB_REPLACEMENTS {
87 if let Some(rest) = input.drop_prefix(source) {
87 if let Some(rest) = input.drop_prefix(source) {
88 input = rest;
88 input = rest;
89 res.extend(*repl);
89 res.extend(*repl);
90 break;
90 break;
91 }
91 }
92 }
92 }
93 }
93 }
94 b'?' => res.extend(b"."),
94 b'?' => res.extend(b"."),
95 b'[' => {
95 b'[' => {
96 match input.iter().skip(1).position(|b| *b == b']') {
96 match input.iter().skip(1).position(|b| *b == b']') {
97 None => res.extend(b"\\["),
97 None => res.extend(b"\\["),
98 Some(end) => {
98 Some(end) => {
99 // Account for the one we skipped
99 // Account for the one we skipped
100 let end = end + 1;
100 let end = end + 1;
101
101
102 res.extend(b"[");
102 res.extend(b"[");
103
103
104 for (i, b) in input[..end].iter().enumerate() {
104 for (i, b) in input[..end].iter().enumerate() {
105 if *b == b'!' && i == 0 {
105 if *b == b'!' && i == 0 {
106 res.extend(b"^")
106 res.extend(b"^")
107 } else if *b == b'^' && i == 0 {
107 } else if *b == b'^' && i == 0 {
108 res.extend(b"\\^")
108 res.extend(b"\\^")
109 } else if *b == b'\\' {
109 } else if *b == b'\\' {
110 res.extend(b"\\\\")
110 res.extend(b"\\\\")
111 } else {
111 } else {
112 res.push(*b)
112 res.push(*b)
113 }
113 }
114 }
114 }
115 res.extend(b"]");
115 res.extend(b"]");
116 input = &input[end + 1..];
116 input = &input[end + 1..];
117 }
117 }
118 }
118 }
119 }
119 }
120 b'{' => {
120 b'{' => {
121 group_depth += 1;
121 group_depth += 1;
122 res.extend(b"(?:")
122 res.extend(b"(?:")
123 }
123 }
124 b'}' if group_depth > 0 => {
124 b'}' if group_depth > 0 => {
125 group_depth -= 1;
125 group_depth -= 1;
126 res.extend(b")");
126 res.extend(b")");
127 }
127 }
128 b',' if group_depth > 0 => res.extend(b"|"),
128 b',' if group_depth > 0 => res.extend(b"|"),
129 b'\\' => {
129 b'\\' => {
130 let c = {
130 let c = {
131 if let Some((c, rest)) = input.split_first() {
131 if let Some((c, rest)) = input.split_first() {
132 input = rest;
132 input = rest;
133 c
133 c
134 } else {
134 } else {
135 c
135 c
136 }
136 }
137 };
137 };
138 res.extend(&RE_ESCAPE[*c as usize])
138 res.extend(&RE_ESCAPE[*c as usize])
139 }
139 }
140 _ => res.extend(&RE_ESCAPE[*c as usize]),
140 _ => res.extend(&RE_ESCAPE[*c as usize]),
141 }
141 }
142 }
142 }
143 res
143 res
144 }
144 }
145
145
146 fn escape_pattern(pattern: &[u8]) -> Vec<u8> {
146 fn escape_pattern(pattern: &[u8]) -> Vec<u8> {
147 pattern
147 pattern
148 .iter()
148 .iter()
149 .flat_map(|c| RE_ESCAPE[*c as usize].clone())
149 .flat_map(|c| RE_ESCAPE[*c as usize].clone())
150 .collect()
150 .collect()
151 }
151 }
152
152
153 pub fn parse_pattern_syntax(
153 pub fn parse_pattern_syntax(
154 kind: &[u8],
154 kind: &[u8],
155 ) -> Result<PatternSyntax, PatternError> {
155 ) -> Result<PatternSyntax, PatternError> {
156 match kind {
156 match kind {
157 b"re:" => Ok(PatternSyntax::Regexp),
157 b"re:" => Ok(PatternSyntax::Regexp),
158 b"path:" => Ok(PatternSyntax::Path),
158 b"path:" => Ok(PatternSyntax::Path),
159 b"filepath:" => Ok(PatternSyntax::FilePath),
159 b"filepath:" => Ok(PatternSyntax::FilePath),
160 b"relpath:" => Ok(PatternSyntax::RelPath),
160 b"relpath:" => Ok(PatternSyntax::RelPath),
161 b"rootfilesin:" => Ok(PatternSyntax::RootFiles),
161 b"rootfilesin:" => Ok(PatternSyntax::RootFilesIn),
162 b"relglob:" => Ok(PatternSyntax::RelGlob),
162 b"relglob:" => Ok(PatternSyntax::RelGlob),
163 b"relre:" => Ok(PatternSyntax::RelRegexp),
163 b"relre:" => Ok(PatternSyntax::RelRegexp),
164 b"glob:" => Ok(PatternSyntax::Glob),
164 b"glob:" => Ok(PatternSyntax::Glob),
165 b"rootglob:" => Ok(PatternSyntax::RootGlob),
165 b"rootglob:" => Ok(PatternSyntax::RootGlob),
166 b"include:" => Ok(PatternSyntax::Include),
166 b"include:" => Ok(PatternSyntax::Include),
167 b"subinclude:" => Ok(PatternSyntax::SubInclude),
167 b"subinclude:" => Ok(PatternSyntax::SubInclude),
168 _ => Err(PatternError::UnsupportedSyntax(
168 _ => Err(PatternError::UnsupportedSyntax(
169 String::from_utf8_lossy(kind).to_string(),
169 String::from_utf8_lossy(kind).to_string(),
170 )),
170 )),
171 }
171 }
172 }
172 }
173
173
174 lazy_static! {
174 lazy_static! {
175 static ref FLAG_RE: Regex = Regex::new(r"^\(\?[aiLmsux]+\)").unwrap();
175 static ref FLAG_RE: Regex = Regex::new(r"^\(\?[aiLmsux]+\)").unwrap();
176 }
176 }
177
177
178 /// Builds the regex that corresponds to the given pattern.
178 /// Builds the regex that corresponds to the given pattern.
179 /// If within a `syntax: regexp` context, returns the pattern,
179 /// If within a `syntax: regexp` context, returns the pattern,
180 /// otherwise, returns the corresponding regex.
180 /// otherwise, returns the corresponding regex.
181 fn _build_single_regex(entry: &IgnorePattern, glob_suffix: &[u8]) -> Vec<u8> {
181 fn _build_single_regex(entry: &IgnorePattern, glob_suffix: &[u8]) -> Vec<u8> {
182 let IgnorePattern {
182 let IgnorePattern {
183 syntax, pattern, ..
183 syntax, pattern, ..
184 } = entry;
184 } = entry;
185 if pattern.is_empty() {
185 if pattern.is_empty() {
186 return vec![];
186 return vec![];
187 }
187 }
188 match syntax {
188 match syntax {
189 PatternSyntax::Regexp => pattern.to_owned(),
189 PatternSyntax::Regexp => pattern.to_owned(),
190 PatternSyntax::RelRegexp => {
190 PatternSyntax::RelRegexp => {
191 // The `regex` crate accepts `**` while `re2` and Python's `re`
191 // The `regex` crate accepts `**` while `re2` and Python's `re`
192 // do not. Checking for `*` correctly triggers the same error all
192 // do not. Checking for `*` correctly triggers the same error all
193 // engines.
193 // engines.
194 if pattern[0] == b'^'
194 if pattern[0] == b'^'
195 || pattern[0] == b'*'
195 || pattern[0] == b'*'
196 || pattern.starts_with(b".*")
196 || pattern.starts_with(b".*")
197 {
197 {
198 return pattern.to_owned();
198 return pattern.to_owned();
199 }
199 }
200 match FLAG_RE.find(pattern) {
200 match FLAG_RE.find(pattern) {
201 Some(mat) => {
201 Some(mat) => {
202 let s = mat.start();
202 let s = mat.start();
203 let e = mat.end();
203 let e = mat.end();
204 [
204 [
205 &b"(?"[..],
205 &b"(?"[..],
206 &pattern[s + 2..e - 1],
206 &pattern[s + 2..e - 1],
207 &b":"[..],
207 &b":"[..],
208 if pattern[e] == b'^'
208 if pattern[e] == b'^'
209 || pattern[e] == b'*'
209 || pattern[e] == b'*'
210 || pattern[e..].starts_with(b".*")
210 || pattern[e..].starts_with(b".*")
211 {
211 {
212 &b""[..]
212 &b""[..]
213 } else {
213 } else {
214 &b".*"[..]
214 &b".*"[..]
215 },
215 },
216 &pattern[e..],
216 &pattern[e..],
217 &b")"[..],
217 &b")"[..],
218 ]
218 ]
219 .concat()
219 .concat()
220 }
220 }
221 None => [&b".*"[..], pattern].concat(),
221 None => [&b".*"[..], pattern].concat(),
222 }
222 }
223 }
223 }
224 PatternSyntax::Path | PatternSyntax::RelPath => {
224 PatternSyntax::Path | PatternSyntax::RelPath => {
225 if pattern == b"." {
225 if pattern == b"." {
226 return vec![];
226 return vec![];
227 }
227 }
228 [escape_pattern(pattern).as_slice(), b"(?:/|$)"].concat()
228 [escape_pattern(pattern).as_slice(), b"(?:/|$)"].concat()
229 }
229 }
230 PatternSyntax::RootFiles => {
230 PatternSyntax::RootFilesIn => {
231 let mut res = if pattern == b"." {
231 let mut res = if pattern == b"." {
232 vec![]
232 vec![]
233 } else {
233 } else {
234 // Pattern is a directory name.
234 // Pattern is a directory name.
235 [escape_pattern(pattern).as_slice(), b"/"].concat()
235 [escape_pattern(pattern).as_slice(), b"/"].concat()
236 };
236 };
237
237
238 // Anything after the pattern must be a non-directory.
238 // Anything after the pattern must be a non-directory.
239 res.extend(b"[^/]+$");
239 res.extend(b"[^/]+$");
240 res
240 res
241 }
241 }
242 PatternSyntax::RelGlob => {
242 PatternSyntax::RelGlob => {
243 let glob_re = glob_to_re(pattern);
243 let glob_re = glob_to_re(pattern);
244 if let Some(rest) = glob_re.drop_prefix(b"[^/]*") {
244 if let Some(rest) = glob_re.drop_prefix(b"[^/]*") {
245 [b".*", rest, glob_suffix].concat()
245 [b".*", rest, glob_suffix].concat()
246 } else {
246 } else {
247 [b"(?:.*/)?", glob_re.as_slice(), glob_suffix].concat()
247 [b"(?:.*/)?", glob_re.as_slice(), glob_suffix].concat()
248 }
248 }
249 }
249 }
250 PatternSyntax::Glob | PatternSyntax::RootGlob => {
250 PatternSyntax::Glob | PatternSyntax::RootGlob => {
251 [glob_to_re(pattern).as_slice(), glob_suffix].concat()
251 [glob_to_re(pattern).as_slice(), glob_suffix].concat()
252 }
252 }
253 PatternSyntax::Include
253 PatternSyntax::Include
254 | PatternSyntax::SubInclude
254 | PatternSyntax::SubInclude
255 | PatternSyntax::ExpandedSubInclude(_)
255 | PatternSyntax::ExpandedSubInclude(_)
256 | PatternSyntax::FilePath => unreachable!(),
256 | PatternSyntax::FilePath => unreachable!(),
257 }
257 }
258 }
258 }
259
259
260 const GLOB_SPECIAL_CHARACTERS: [u8; 7] =
260 const GLOB_SPECIAL_CHARACTERS: [u8; 7] =
261 [b'*', b'?', b'[', b']', b'{', b'}', b'\\'];
261 [b'*', b'?', b'[', b']', b'{', b'}', b'\\'];
262
262
263 /// TODO support other platforms
263 /// TODO support other platforms
264 #[cfg(unix)]
264 #[cfg(unix)]
265 pub fn normalize_path_bytes(bytes: &[u8]) -> Vec<u8> {
265 pub fn normalize_path_bytes(bytes: &[u8]) -> Vec<u8> {
266 if bytes.is_empty() {
266 if bytes.is_empty() {
267 return b".".to_vec();
267 return b".".to_vec();
268 }
268 }
269 let sep = b'/';
269 let sep = b'/';
270
270
271 let mut initial_slashes = bytes.iter().take_while(|b| **b == sep).count();
271 let mut initial_slashes = bytes.iter().take_while(|b| **b == sep).count();
272 if initial_slashes > 2 {
272 if initial_slashes > 2 {
273 // POSIX allows one or two initial slashes, but treats three or more
273 // POSIX allows one or two initial slashes, but treats three or more
274 // as single slash.
274 // as single slash.
275 initial_slashes = 1;
275 initial_slashes = 1;
276 }
276 }
277 let components = bytes
277 let components = bytes
278 .split(|b| *b == sep)
278 .split(|b| *b == sep)
279 .filter(|c| !(c.is_empty() || c == b"."))
279 .filter(|c| !(c.is_empty() || c == b"."))
280 .fold(vec![], |mut acc, component| {
280 .fold(vec![], |mut acc, component| {
281 if component != b".."
281 if component != b".."
282 || (initial_slashes == 0 && acc.is_empty())
282 || (initial_slashes == 0 && acc.is_empty())
283 || (!acc.is_empty() && acc[acc.len() - 1] == b"..")
283 || (!acc.is_empty() && acc[acc.len() - 1] == b"..")
284 {
284 {
285 acc.push(component)
285 acc.push(component)
286 } else if !acc.is_empty() {
286 } else if !acc.is_empty() {
287 acc.pop();
287 acc.pop();
288 }
288 }
289 acc
289 acc
290 });
290 });
291 let mut new_bytes = components.join(&sep);
291 let mut new_bytes = components.join(&sep);
292
292
293 if initial_slashes > 0 {
293 if initial_slashes > 0 {
294 let mut buf: Vec<_> = (0..initial_slashes).map(|_| sep).collect();
294 let mut buf: Vec<_> = (0..initial_slashes).map(|_| sep).collect();
295 buf.extend(new_bytes);
295 buf.extend(new_bytes);
296 new_bytes = buf;
296 new_bytes = buf;
297 }
297 }
298 if new_bytes.is_empty() {
298 if new_bytes.is_empty() {
299 b".".to_vec()
299 b".".to_vec()
300 } else {
300 } else {
301 new_bytes
301 new_bytes
302 }
302 }
303 }
303 }
304
304
305 /// Wrapper function to `_build_single_regex` that short-circuits 'exact' globs
305 /// Wrapper function to `_build_single_regex` that short-circuits 'exact' globs
306 /// that don't need to be transformed into a regex.
306 /// that don't need to be transformed into a regex.
307 pub fn build_single_regex(
307 pub fn build_single_regex(
308 entry: &IgnorePattern,
308 entry: &IgnorePattern,
309 glob_suffix: &[u8],
309 glob_suffix: &[u8],
310 ) -> Result<Option<Vec<u8>>, PatternError> {
310 ) -> Result<Option<Vec<u8>>, PatternError> {
311 let IgnorePattern {
311 let IgnorePattern {
312 pattern, syntax, ..
312 pattern, syntax, ..
313 } = entry;
313 } = entry;
314 let pattern = match syntax {
314 let pattern = match syntax {
315 PatternSyntax::RootGlob
315 PatternSyntax::RootGlob
316 | PatternSyntax::Path
316 | PatternSyntax::Path
317 | PatternSyntax::RelGlob
317 | PatternSyntax::RelGlob
318 | PatternSyntax::RelPath
318 | PatternSyntax::RelPath
319 | PatternSyntax::RootFiles => normalize_path_bytes(pattern),
319 | PatternSyntax::RootFilesIn => normalize_path_bytes(pattern),
320 PatternSyntax::Include | PatternSyntax::SubInclude => {
320 PatternSyntax::Include | PatternSyntax::SubInclude => {
321 return Err(PatternError::NonRegexPattern(entry.clone()))
321 return Err(PatternError::NonRegexPattern(entry.clone()))
322 }
322 }
323 _ => pattern.to_owned(),
323 _ => pattern.to_owned(),
324 };
324 };
325 let is_simple_rootglob = *syntax == PatternSyntax::RootGlob
325 let is_simple_rootglob = *syntax == PatternSyntax::RootGlob
326 && !pattern.iter().any(|b| GLOB_SPECIAL_CHARACTERS.contains(b));
326 && !pattern.iter().any(|b| GLOB_SPECIAL_CHARACTERS.contains(b));
327 if is_simple_rootglob || syntax == &PatternSyntax::FilePath {
327 if is_simple_rootglob || syntax == &PatternSyntax::FilePath {
328 Ok(None)
328 Ok(None)
329 } else {
329 } else {
330 let mut entry = entry.clone();
330 let mut entry = entry.clone();
331 entry.pattern = pattern;
331 entry.pattern = pattern;
332 Ok(Some(_build_single_regex(&entry, glob_suffix)))
332 Ok(Some(_build_single_regex(&entry, glob_suffix)))
333 }
333 }
334 }
334 }
335
335
336 lazy_static! {
336 lazy_static! {
337 static ref SYNTAXES: FastHashMap<&'static [u8], PatternSyntax> = {
337 static ref SYNTAXES: FastHashMap<&'static [u8], PatternSyntax> = {
338 let mut m = FastHashMap::default();
338 let mut m = FastHashMap::default();
339
339
340 m.insert(b"re:".as_ref(), PatternSyntax::Regexp);
340 m.insert(b"re:".as_ref(), PatternSyntax::Regexp);
341 m.insert(b"regexp:".as_ref(), PatternSyntax::Regexp);
341 m.insert(b"regexp:".as_ref(), PatternSyntax::Regexp);
342 m.insert(b"path:".as_ref(), PatternSyntax::Path);
342 m.insert(b"path:".as_ref(), PatternSyntax::Path);
343 m.insert(b"filepath:".as_ref(), PatternSyntax::FilePath);
343 m.insert(b"filepath:".as_ref(), PatternSyntax::FilePath);
344 m.insert(b"relpath:".as_ref(), PatternSyntax::RelPath);
344 m.insert(b"relpath:".as_ref(), PatternSyntax::RelPath);
345 m.insert(b"rootfilesin:".as_ref(), PatternSyntax::RootFiles);
345 m.insert(b"rootfilesin:".as_ref(), PatternSyntax::RootFilesIn);
346 m.insert(b"relglob:".as_ref(), PatternSyntax::RelGlob);
346 m.insert(b"relglob:".as_ref(), PatternSyntax::RelGlob);
347 m.insert(b"relre:".as_ref(), PatternSyntax::RelRegexp);
347 m.insert(b"relre:".as_ref(), PatternSyntax::RelRegexp);
348 m.insert(b"glob:".as_ref(), PatternSyntax::Glob);
348 m.insert(b"glob:".as_ref(), PatternSyntax::Glob);
349 m.insert(b"rootglob:".as_ref(), PatternSyntax::RootGlob);
349 m.insert(b"rootglob:".as_ref(), PatternSyntax::RootGlob);
350 m.insert(b"include:".as_ref(), PatternSyntax::Include);
350 m.insert(b"include:".as_ref(), PatternSyntax::Include);
351 m.insert(b"subinclude:".as_ref(), PatternSyntax::SubInclude);
351 m.insert(b"subinclude:".as_ref(), PatternSyntax::SubInclude);
352
352
353 m
353 m
354 };
354 };
355 }
355 }
356
356
357 #[derive(Debug)]
357 #[derive(Debug)]
358 pub enum PatternFileWarning {
358 pub enum PatternFileWarning {
359 /// (file path, syntax bytes)
359 /// (file path, syntax bytes)
360 InvalidSyntax(PathBuf, Vec<u8>),
360 InvalidSyntax(PathBuf, Vec<u8>),
361 /// File path
361 /// File path
362 NoSuchFile(PathBuf),
362 NoSuchFile(PathBuf),
363 }
363 }
364
364
365 pub fn parse_one_pattern(
365 pub fn parse_one_pattern(
366 pattern: &[u8],
366 pattern: &[u8],
367 source: &Path,
367 source: &Path,
368 default: PatternSyntax,
368 default: PatternSyntax,
369 normalize: bool,
369 normalize: bool,
370 ) -> IgnorePattern {
370 ) -> IgnorePattern {
371 let mut pattern_bytes: &[u8] = pattern;
371 let mut pattern_bytes: &[u8] = pattern;
372 let mut syntax = default;
372 let mut syntax = default;
373
373
374 for (s, val) in SYNTAXES.iter() {
374 for (s, val) in SYNTAXES.iter() {
375 if let Some(rest) = pattern_bytes.drop_prefix(s) {
375 if let Some(rest) = pattern_bytes.drop_prefix(s) {
376 syntax = val.clone();
376 syntax = val.clone();
377 pattern_bytes = rest;
377 pattern_bytes = rest;
378 break;
378 break;
379 }
379 }
380 }
380 }
381
381
382 let pattern = match syntax {
382 let pattern = match syntax {
383 PatternSyntax::RootGlob
383 PatternSyntax::RootGlob
384 | PatternSyntax::Path
384 | PatternSyntax::Path
385 | PatternSyntax::Glob
385 | PatternSyntax::Glob
386 | PatternSyntax::RelGlob
386 | PatternSyntax::RelGlob
387 | PatternSyntax::RelPath
387 | PatternSyntax::RelPath
388 | PatternSyntax::RootFiles
388 | PatternSyntax::RootFilesIn
389 if normalize =>
389 if normalize =>
390 {
390 {
391 normalize_path_bytes(pattern_bytes)
391 normalize_path_bytes(pattern_bytes)
392 }
392 }
393 _ => pattern_bytes.to_vec(),
393 _ => pattern_bytes.to_vec(),
394 };
394 };
395
395
396 IgnorePattern {
396 IgnorePattern {
397 syntax,
397 syntax,
398 pattern,
398 pattern,
399 source: source.to_owned(),
399 source: source.to_owned(),
400 }
400 }
401 }
401 }
402
402
403 pub fn parse_pattern_file_contents(
403 pub fn parse_pattern_file_contents(
404 lines: &[u8],
404 lines: &[u8],
405 file_path: &Path,
405 file_path: &Path,
406 default_syntax_override: Option<PatternSyntax>,
406 default_syntax_override: Option<PatternSyntax>,
407 warn: bool,
407 warn: bool,
408 relativize: bool,
408 relativize: bool,
409 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
409 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
410 let comment_regex = Regex::new(r"((?:^|[^\\])(?:\\\\)*)#.*").unwrap();
410 let comment_regex = Regex::new(r"((?:^|[^\\])(?:\\\\)*)#.*").unwrap();
411
411
412 #[allow(clippy::trivial_regex)]
412 #[allow(clippy::trivial_regex)]
413 let comment_escape_regex = Regex::new(r"\\#").unwrap();
413 let comment_escape_regex = Regex::new(r"\\#").unwrap();
414 let mut inputs: Vec<IgnorePattern> = vec![];
414 let mut inputs: Vec<IgnorePattern> = vec![];
415 let mut warnings: Vec<PatternFileWarning> = vec![];
415 let mut warnings: Vec<PatternFileWarning> = vec![];
416
416
417 let mut current_syntax =
417 let mut current_syntax =
418 default_syntax_override.unwrap_or(PatternSyntax::RelRegexp);
418 default_syntax_override.unwrap_or(PatternSyntax::RelRegexp);
419
419
420 for mut line in lines.split(|c| *c == b'\n') {
420 for mut line in lines.split(|c| *c == b'\n') {
421 let line_buf;
421 let line_buf;
422 if line.contains(&b'#') {
422 if line.contains(&b'#') {
423 if let Some(cap) = comment_regex.captures(line) {
423 if let Some(cap) = comment_regex.captures(line) {
424 line = &line[..cap.get(1).unwrap().end()]
424 line = &line[..cap.get(1).unwrap().end()]
425 }
425 }
426 line_buf = comment_escape_regex.replace_all(line, NoExpand(b"#"));
426 line_buf = comment_escape_regex.replace_all(line, NoExpand(b"#"));
427 line = &line_buf;
427 line = &line_buf;
428 }
428 }
429
429
430 let line = line.trim_end();
430 let line = line.trim_end();
431
431
432 if line.is_empty() {
432 if line.is_empty() {
433 continue;
433 continue;
434 }
434 }
435
435
436 if let Some(syntax) = line.drop_prefix(b"syntax:") {
436 if let Some(syntax) = line.drop_prefix(b"syntax:") {
437 let syntax = syntax.trim();
437 let syntax = syntax.trim();
438
438
439 if let Some(parsed) =
439 if let Some(parsed) =
440 SYNTAXES.get([syntax, &b":"[..]].concat().as_slice())
440 SYNTAXES.get([syntax, &b":"[..]].concat().as_slice())
441 {
441 {
442 current_syntax = parsed.clone();
442 current_syntax = parsed.clone();
443 } else if warn {
443 } else if warn {
444 warnings.push(PatternFileWarning::InvalidSyntax(
444 warnings.push(PatternFileWarning::InvalidSyntax(
445 file_path.to_owned(),
445 file_path.to_owned(),
446 syntax.to_owned(),
446 syntax.to_owned(),
447 ));
447 ));
448 }
448 }
449 } else {
449 } else {
450 let pattern = parse_one_pattern(
450 let pattern = parse_one_pattern(
451 line,
451 line,
452 file_path,
452 file_path,
453 current_syntax.clone(),
453 current_syntax.clone(),
454 false,
454 false,
455 );
455 );
456 inputs.push(if relativize {
456 inputs.push(if relativize {
457 pattern.to_relative()
457 pattern.to_relative()
458 } else {
458 } else {
459 pattern
459 pattern
460 })
460 })
461 }
461 }
462 }
462 }
463 Ok((inputs, warnings))
463 Ok((inputs, warnings))
464 }
464 }
465
465
466 pub fn parse_pattern_args(
466 pub fn parse_pattern_args(
467 patterns: Vec<Vec<u8>>,
467 patterns: Vec<Vec<u8>>,
468 cwd: &Path,
468 cwd: &Path,
469 root: &Path,
469 root: &Path,
470 ) -> Result<Vec<IgnorePattern>, HgPathError> {
470 ) -> Result<Vec<IgnorePattern>, HgPathError> {
471 let mut ignore_patterns: Vec<IgnorePattern> = Vec::new();
471 let mut ignore_patterns: Vec<IgnorePattern> = Vec::new();
472 for pattern in patterns {
472 for pattern in patterns {
473 let pattern = parse_one_pattern(
473 let pattern = parse_one_pattern(
474 &pattern,
474 &pattern,
475 Path::new("<args>"),
475 Path::new("<args>"),
476 PatternSyntax::RelPath,
476 PatternSyntax::RelPath,
477 true,
477 true,
478 );
478 );
479 match pattern.syntax {
479 match pattern.syntax {
480 PatternSyntax::RelGlob | PatternSyntax::RelPath => {
480 PatternSyntax::RelGlob | PatternSyntax::RelPath => {
481 let name = get_path_from_bytes(&pattern.pattern);
481 let name = get_path_from_bytes(&pattern.pattern);
482 let canon = canonical_path(root, cwd, name)?;
482 let canon = canonical_path(root, cwd, name)?;
483 ignore_patterns.push(IgnorePattern {
483 ignore_patterns.push(IgnorePattern {
484 syntax: pattern.syntax,
484 syntax: pattern.syntax,
485 pattern: get_bytes_from_path(canon),
485 pattern: get_bytes_from_path(canon),
486 source: pattern.source,
486 source: pattern.source,
487 })
487 })
488 }
488 }
489 _ => ignore_patterns.push(pattern.to_owned()),
489 _ => ignore_patterns.push(pattern.to_owned()),
490 };
490 };
491 }
491 }
492 Ok(ignore_patterns)
492 Ok(ignore_patterns)
493 }
493 }
494
494
495 pub fn read_pattern_file(
495 pub fn read_pattern_file(
496 file_path: &Path,
496 file_path: &Path,
497 warn: bool,
497 warn: bool,
498 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
498 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
499 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
499 ) -> Result<(Vec<IgnorePattern>, Vec<PatternFileWarning>), PatternError> {
500 match std::fs::read(file_path) {
500 match std::fs::read(file_path) {
501 Ok(contents) => {
501 Ok(contents) => {
502 inspect_pattern_bytes(file_path, &contents);
502 inspect_pattern_bytes(file_path, &contents);
503 parse_pattern_file_contents(&contents, file_path, None, warn, true)
503 parse_pattern_file_contents(&contents, file_path, None, warn, true)
504 }
504 }
505 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok((
505 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok((
506 vec![],
506 vec![],
507 vec![PatternFileWarning::NoSuchFile(file_path.to_owned())],
507 vec![PatternFileWarning::NoSuchFile(file_path.to_owned())],
508 )),
508 )),
509 Err(e) => Err(e.into()),
509 Err(e) => Err(e.into()),
510 }
510 }
511 }
511 }
512
512
513 /// Represents an entry in an "ignore" file.
513 /// Represents an entry in an "ignore" file.
514 #[derive(Debug, Eq, PartialEq, Clone)]
514 #[derive(Debug, Eq, PartialEq, Clone)]
515 pub struct IgnorePattern {
515 pub struct IgnorePattern {
516 pub syntax: PatternSyntax,
516 pub syntax: PatternSyntax,
517 pub pattern: Vec<u8>,
517 pub pattern: Vec<u8>,
518 pub source: PathBuf,
518 pub source: PathBuf,
519 }
519 }
520
520
521 impl IgnorePattern {
521 impl IgnorePattern {
522 pub fn new(syntax: PatternSyntax, pattern: &[u8], source: &Path) -> Self {
522 pub fn new(syntax: PatternSyntax, pattern: &[u8], source: &Path) -> Self {
523 Self {
523 Self {
524 syntax,
524 syntax,
525 pattern: pattern.to_owned(),
525 pattern: pattern.to_owned(),
526 source: source.to_owned(),
526 source: source.to_owned(),
527 }
527 }
528 }
528 }
529
529
530 pub fn to_relative(self) -> Self {
530 pub fn to_relative(self) -> Self {
531 let Self {
531 let Self {
532 syntax,
532 syntax,
533 pattern,
533 pattern,
534 source,
534 source,
535 } = self;
535 } = self;
536 Self {
536 Self {
537 syntax: match syntax {
537 syntax: match syntax {
538 PatternSyntax::Regexp => PatternSyntax::RelRegexp,
538 PatternSyntax::Regexp => PatternSyntax::RelRegexp,
539 PatternSyntax::Glob => PatternSyntax::RelGlob,
539 PatternSyntax::Glob => PatternSyntax::RelGlob,
540 x => x,
540 x => x,
541 },
541 },
542 pattern,
542 pattern,
543 source,
543 source,
544 }
544 }
545 }
545 }
546 }
546 }
547
547
548 pub type PatternResult<T> = Result<T, PatternError>;
548 pub type PatternResult<T> = Result<T, PatternError>;
549
549
550 /// Wrapper for `read_pattern_file` that also recursively expands `include:`
550 /// Wrapper for `read_pattern_file` that also recursively expands `include:`
551 /// and `subinclude:` patterns.
551 /// and `subinclude:` patterns.
552 ///
552 ///
553 /// The former are expanded in place, while `PatternSyntax::ExpandedSubInclude`
553 /// The former are expanded in place, while `PatternSyntax::ExpandedSubInclude`
554 /// is used for the latter to form a tree of patterns.
554 /// is used for the latter to form a tree of patterns.
555 pub fn get_patterns_from_file(
555 pub fn get_patterns_from_file(
556 pattern_file: &Path,
556 pattern_file: &Path,
557 root_dir: &Path,
557 root_dir: &Path,
558 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
558 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
559 ) -> PatternResult<(Vec<IgnorePattern>, Vec<PatternFileWarning>)> {
559 ) -> PatternResult<(Vec<IgnorePattern>, Vec<PatternFileWarning>)> {
560 let (patterns, mut warnings) =
560 let (patterns, mut warnings) =
561 read_pattern_file(pattern_file, true, inspect_pattern_bytes)?;
561 read_pattern_file(pattern_file, true, inspect_pattern_bytes)?;
562 let patterns = patterns
562 let patterns = patterns
563 .into_iter()
563 .into_iter()
564 .flat_map(|entry| -> PatternResult<_> {
564 .flat_map(|entry| -> PatternResult<_> {
565 Ok(match &entry.syntax {
565 Ok(match &entry.syntax {
566 PatternSyntax::Include => {
566 PatternSyntax::Include => {
567 let inner_include =
567 let inner_include =
568 root_dir.join(get_path_from_bytes(&entry.pattern));
568 root_dir.join(get_path_from_bytes(&entry.pattern));
569 let (inner_pats, inner_warnings) = get_patterns_from_file(
569 let (inner_pats, inner_warnings) = get_patterns_from_file(
570 &inner_include,
570 &inner_include,
571 root_dir,
571 root_dir,
572 inspect_pattern_bytes,
572 inspect_pattern_bytes,
573 )?;
573 )?;
574 warnings.extend(inner_warnings);
574 warnings.extend(inner_warnings);
575 inner_pats
575 inner_pats
576 }
576 }
577 PatternSyntax::SubInclude => {
577 PatternSyntax::SubInclude => {
578 let mut sub_include = SubInclude::new(
578 let mut sub_include = SubInclude::new(
579 root_dir,
579 root_dir,
580 &entry.pattern,
580 &entry.pattern,
581 &entry.source,
581 &entry.source,
582 )?;
582 )?;
583 let (inner_patterns, inner_warnings) =
583 let (inner_patterns, inner_warnings) =
584 get_patterns_from_file(
584 get_patterns_from_file(
585 &sub_include.path,
585 &sub_include.path,
586 &sub_include.root,
586 &sub_include.root,
587 inspect_pattern_bytes,
587 inspect_pattern_bytes,
588 )?;
588 )?;
589 sub_include.included_patterns = inner_patterns;
589 sub_include.included_patterns = inner_patterns;
590 warnings.extend(inner_warnings);
590 warnings.extend(inner_warnings);
591 vec![IgnorePattern {
591 vec![IgnorePattern {
592 syntax: PatternSyntax::ExpandedSubInclude(Box::new(
592 syntax: PatternSyntax::ExpandedSubInclude(Box::new(
593 sub_include,
593 sub_include,
594 )),
594 )),
595 ..entry
595 ..entry
596 }]
596 }]
597 }
597 }
598 _ => vec![entry],
598 _ => vec![entry],
599 })
599 })
600 })
600 })
601 .flatten()
601 .flatten()
602 .collect();
602 .collect();
603
603
604 Ok((patterns, warnings))
604 Ok((patterns, warnings))
605 }
605 }
606
606
607 /// Holds all the information needed to handle a `subinclude:` pattern.
607 /// Holds all the information needed to handle a `subinclude:` pattern.
608 #[derive(Debug, PartialEq, Eq, Clone)]
608 #[derive(Debug, PartialEq, Eq, Clone)]
609 pub struct SubInclude {
609 pub struct SubInclude {
610 /// Will be used for repository (hg) paths that start with this prefix.
610 /// Will be used for repository (hg) paths that start with this prefix.
611 /// It is relative to the current working directory, so comparing against
611 /// It is relative to the current working directory, so comparing against
612 /// repository paths is painless.
612 /// repository paths is painless.
613 pub prefix: HgPathBuf,
613 pub prefix: HgPathBuf,
614 /// The file itself, containing the patterns
614 /// The file itself, containing the patterns
615 pub path: PathBuf,
615 pub path: PathBuf,
616 /// Folder in the filesystem where this it applies
616 /// Folder in the filesystem where this it applies
617 pub root: PathBuf,
617 pub root: PathBuf,
618
618
619 pub included_patterns: Vec<IgnorePattern>,
619 pub included_patterns: Vec<IgnorePattern>,
620 }
620 }
621
621
622 impl SubInclude {
622 impl SubInclude {
623 pub fn new(
623 pub fn new(
624 root_dir: &Path,
624 root_dir: &Path,
625 pattern: &[u8],
625 pattern: &[u8],
626 source: &Path,
626 source: &Path,
627 ) -> Result<SubInclude, HgPathError> {
627 ) -> Result<SubInclude, HgPathError> {
628 let normalized_source =
628 let normalized_source =
629 normalize_path_bytes(&get_bytes_from_path(source));
629 normalize_path_bytes(&get_bytes_from_path(source));
630
630
631 let source_root = get_path_from_bytes(&normalized_source);
631 let source_root = get_path_from_bytes(&normalized_source);
632 let source_root = source_root.parent().unwrap_or(source_root);
632 let source_root = source_root.parent().unwrap_or(source_root);
633
633
634 let path = source_root.join(get_path_from_bytes(pattern));
634 let path = source_root.join(get_path_from_bytes(pattern));
635 let new_root = path.parent().unwrap_or_else(|| path.deref());
635 let new_root = path.parent().unwrap_or_else(|| path.deref());
636
636
637 let prefix = canonical_path(root_dir, root_dir, new_root)?;
637 let prefix = canonical_path(root_dir, root_dir, new_root)?;
638
638
639 Ok(Self {
639 Ok(Self {
640 prefix: path_to_hg_path_buf(prefix).map(|mut p| {
640 prefix: path_to_hg_path_buf(prefix).map(|mut p| {
641 if !p.is_empty() {
641 if !p.is_empty() {
642 p.push_byte(b'/');
642 p.push_byte(b'/');
643 }
643 }
644 p
644 p
645 })?,
645 })?,
646 path: path.to_owned(),
646 path: path.to_owned(),
647 root: new_root.to_owned(),
647 root: new_root.to_owned(),
648 included_patterns: Vec::new(),
648 included_patterns: Vec::new(),
649 })
649 })
650 }
650 }
651 }
651 }
652
652
653 /// Separate and pre-process subincludes from other patterns for the "ignore"
653 /// Separate and pre-process subincludes from other patterns for the "ignore"
654 /// phase.
654 /// phase.
655 pub fn filter_subincludes(
655 pub fn filter_subincludes(
656 ignore_patterns: Vec<IgnorePattern>,
656 ignore_patterns: Vec<IgnorePattern>,
657 ) -> Result<(Vec<SubInclude>, Vec<IgnorePattern>), HgPathError> {
657 ) -> Result<(Vec<SubInclude>, Vec<IgnorePattern>), HgPathError> {
658 let mut subincludes = vec![];
658 let mut subincludes = vec![];
659 let mut others = vec![];
659 let mut others = vec![];
660
660
661 for pattern in ignore_patterns {
661 for pattern in ignore_patterns {
662 if let PatternSyntax::ExpandedSubInclude(sub_include) = pattern.syntax
662 if let PatternSyntax::ExpandedSubInclude(sub_include) = pattern.syntax
663 {
663 {
664 subincludes.push(*sub_include);
664 subincludes.push(*sub_include);
665 } else {
665 } else {
666 others.push(pattern)
666 others.push(pattern)
667 }
667 }
668 }
668 }
669 Ok((subincludes, others))
669 Ok((subincludes, others))
670 }
670 }
671
671
672 #[cfg(test)]
672 #[cfg(test)]
673 mod tests {
673 mod tests {
674 use super::*;
674 use super::*;
675 use pretty_assertions::assert_eq;
675 use pretty_assertions::assert_eq;
676
676
677 #[test]
677 #[test]
678 fn escape_pattern_test() {
678 fn escape_pattern_test() {
679 let untouched =
679 let untouched =
680 br#"!"%',/0123456789:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz"#;
680 br#"!"%',/0123456789:;<=>@ABCDEFGHIJKLMNOPQRSTUVWXYZ_`abcdefghijklmnopqrstuvwxyz"#;
681 assert_eq!(escape_pattern(untouched), untouched.to_vec());
681 assert_eq!(escape_pattern(untouched), untouched.to_vec());
682 // All escape codes
682 // All escape codes
683 assert_eq!(
683 assert_eq!(
684 escape_pattern(br"()[]{}?*+-|^$\\.&~#\t\n\r\v\f"),
684 escape_pattern(br"()[]{}?*+-|^$\\.&~#\t\n\r\v\f"),
685 br"\(\)\[\]\{\}\?\*\+\-\|\^\$\\\\\.\&\~\#\\t\\n\\r\\v\\f".to_vec()
685 br"\(\)\[\]\{\}\?\*\+\-\|\^\$\\\\\.\&\~\#\\t\\n\\r\\v\\f".to_vec()
686 );
686 );
687 }
687 }
688
688
689 #[test]
689 #[test]
690 fn glob_test() {
690 fn glob_test() {
691 assert_eq!(glob_to_re(br"?"), br".");
691 assert_eq!(glob_to_re(br"?"), br".");
692 assert_eq!(glob_to_re(br"*"), br"[^/]*");
692 assert_eq!(glob_to_re(br"*"), br"[^/]*");
693 assert_eq!(glob_to_re(br"**"), br".*");
693 assert_eq!(glob_to_re(br"**"), br".*");
694 assert_eq!(glob_to_re(br"**/a"), br"(?:.*/)?a");
694 assert_eq!(glob_to_re(br"**/a"), br"(?:.*/)?a");
695 assert_eq!(glob_to_re(br"a/**/b"), br"a/(?:.*/)?b");
695 assert_eq!(glob_to_re(br"a/**/b"), br"a/(?:.*/)?b");
696 assert_eq!(glob_to_re(br"[a*?!^][^b][!c]"), br"[a*?!^][\^b][^c]");
696 assert_eq!(glob_to_re(br"[a*?!^][^b][!c]"), br"[a*?!^][\^b][^c]");
697 assert_eq!(glob_to_re(br"{a,b}"), br"(?:a|b)");
697 assert_eq!(glob_to_re(br"{a,b}"), br"(?:a|b)");
698 assert_eq!(glob_to_re(br".\*\?"), br"\.\*\?");
698 assert_eq!(glob_to_re(br".\*\?"), br"\.\*\?");
699 }
699 }
700
700
701 #[test]
701 #[test]
702 fn test_parse_pattern_file_contents() {
702 fn test_parse_pattern_file_contents() {
703 let lines = b"syntax: glob\n*.elc";
703 let lines = b"syntax: glob\n*.elc";
704
704
705 assert_eq!(
705 assert_eq!(
706 parse_pattern_file_contents(
706 parse_pattern_file_contents(
707 lines,
707 lines,
708 Path::new("file_path"),
708 Path::new("file_path"),
709 None,
709 None,
710 false,
710 false,
711 true,
711 true,
712 )
712 )
713 .unwrap()
713 .unwrap()
714 .0,
714 .0,
715 vec![IgnorePattern::new(
715 vec![IgnorePattern::new(
716 PatternSyntax::RelGlob,
716 PatternSyntax::RelGlob,
717 b"*.elc",
717 b"*.elc",
718 Path::new("file_path")
718 Path::new("file_path")
719 )],
719 )],
720 );
720 );
721
721
722 let lines = b"syntax: include\nsyntax: glob";
722 let lines = b"syntax: include\nsyntax: glob";
723
723
724 assert_eq!(
724 assert_eq!(
725 parse_pattern_file_contents(
725 parse_pattern_file_contents(
726 lines,
726 lines,
727 Path::new("file_path"),
727 Path::new("file_path"),
728 None,
728 None,
729 false,
729 false,
730 true,
730 true,
731 )
731 )
732 .unwrap()
732 .unwrap()
733 .0,
733 .0,
734 vec![]
734 vec![]
735 );
735 );
736 let lines = b"glob:**.o";
736 let lines = b"glob:**.o";
737 assert_eq!(
737 assert_eq!(
738 parse_pattern_file_contents(
738 parse_pattern_file_contents(
739 lines,
739 lines,
740 Path::new("file_path"),
740 Path::new("file_path"),
741 None,
741 None,
742 false,
742 false,
743 true,
743 true,
744 )
744 )
745 .unwrap()
745 .unwrap()
746 .0,
746 .0,
747 vec![IgnorePattern::new(
747 vec![IgnorePattern::new(
748 PatternSyntax::RelGlob,
748 PatternSyntax::RelGlob,
749 b"**.o",
749 b"**.o",
750 Path::new("file_path")
750 Path::new("file_path")
751 )]
751 )]
752 );
752 );
753 }
753 }
754
754
755 #[test]
755 #[test]
756 fn test_build_single_regex() {
756 fn test_build_single_regex() {
757 assert_eq!(
757 assert_eq!(
758 build_single_regex(
758 build_single_regex(
759 &IgnorePattern::new(
759 &IgnorePattern::new(
760 PatternSyntax::RelGlob,
760 PatternSyntax::RelGlob,
761 b"rust/target/",
761 b"rust/target/",
762 Path::new("")
762 Path::new("")
763 ),
763 ),
764 b"(?:/|$)"
764 b"(?:/|$)"
765 )
765 )
766 .unwrap(),
766 .unwrap(),
767 Some(br"(?:.*/)?rust/target(?:/|$)".to_vec()),
767 Some(br"(?:.*/)?rust/target(?:/|$)".to_vec()),
768 );
768 );
769 assert_eq!(
769 assert_eq!(
770 build_single_regex(
770 build_single_regex(
771 &IgnorePattern::new(
771 &IgnorePattern::new(
772 PatternSyntax::Regexp,
772 PatternSyntax::Regexp,
773 br"rust/target/\d+",
773 br"rust/target/\d+",
774 Path::new("")
774 Path::new("")
775 ),
775 ),
776 b"(?:/|$)"
776 b"(?:/|$)"
777 )
777 )
778 .unwrap(),
778 .unwrap(),
779 Some(br"rust/target/\d+".to_vec()),
779 Some(br"rust/target/\d+".to_vec()),
780 );
780 );
781 }
781 }
782
782
783 #[test]
783 #[test]
784 fn test_build_single_regex_shortcut() {
784 fn test_build_single_regex_shortcut() {
785 assert_eq!(
785 assert_eq!(
786 build_single_regex(
786 build_single_regex(
787 &IgnorePattern::new(
787 &IgnorePattern::new(
788 PatternSyntax::RootGlob,
788 PatternSyntax::RootGlob,
789 b"",
789 b"",
790 Path::new("")
790 Path::new("")
791 ),
791 ),
792 b"(?:/|$)"
792 b"(?:/|$)"
793 )
793 )
794 .unwrap(),
794 .unwrap(),
795 None,
795 None,
796 );
796 );
797 assert_eq!(
797 assert_eq!(
798 build_single_regex(
798 build_single_regex(
799 &IgnorePattern::new(
799 &IgnorePattern::new(
800 PatternSyntax::RootGlob,
800 PatternSyntax::RootGlob,
801 b"whatever",
801 b"whatever",
802 Path::new("")
802 Path::new("")
803 ),
803 ),
804 b"(?:/|$)"
804 b"(?:/|$)"
805 )
805 )
806 .unwrap(),
806 .unwrap(),
807 None,
807 None,
808 );
808 );
809 assert_eq!(
809 assert_eq!(
810 build_single_regex(
810 build_single_regex(
811 &IgnorePattern::new(
811 &IgnorePattern::new(
812 PatternSyntax::RootGlob,
812 PatternSyntax::RootGlob,
813 b"*.o",
813 b"*.o",
814 Path::new("")
814 Path::new("")
815 ),
815 ),
816 b"(?:/|$)"
816 b"(?:/|$)"
817 )
817 )
818 .unwrap(),
818 .unwrap(),
819 Some(br"[^/]*\.o(?:/|$)".to_vec()),
819 Some(br"[^/]*\.o(?:/|$)".to_vec()),
820 );
820 );
821 }
821 }
822
822
823 #[test]
823 #[test]
824 fn test_build_single_relregex() {
824 fn test_build_single_relregex() {
825 assert_eq!(
825 assert_eq!(
826 build_single_regex(
826 build_single_regex(
827 &IgnorePattern::new(
827 &IgnorePattern::new(
828 PatternSyntax::RelRegexp,
828 PatternSyntax::RelRegexp,
829 b"^ba{2}r",
829 b"^ba{2}r",
830 Path::new("")
830 Path::new("")
831 ),
831 ),
832 b"(?:/|$)"
832 b"(?:/|$)"
833 )
833 )
834 .unwrap(),
834 .unwrap(),
835 Some(b"^ba{2}r".to_vec()),
835 Some(b"^ba{2}r".to_vec()),
836 );
836 );
837 assert_eq!(
837 assert_eq!(
838 build_single_regex(
838 build_single_regex(
839 &IgnorePattern::new(
839 &IgnorePattern::new(
840 PatternSyntax::RelRegexp,
840 PatternSyntax::RelRegexp,
841 b"ba{2}r",
841 b"ba{2}r",
842 Path::new("")
842 Path::new("")
843 ),
843 ),
844 b"(?:/|$)"
844 b"(?:/|$)"
845 )
845 )
846 .unwrap(),
846 .unwrap(),
847 Some(b".*ba{2}r".to_vec()),
847 Some(b".*ba{2}r".to_vec()),
848 );
848 );
849 assert_eq!(
849 assert_eq!(
850 build_single_regex(
850 build_single_regex(
851 &IgnorePattern::new(
851 &IgnorePattern::new(
852 PatternSyntax::RelRegexp,
852 PatternSyntax::RelRegexp,
853 b"(?ia)ba{2}r",
853 b"(?ia)ba{2}r",
854 Path::new("")
854 Path::new("")
855 ),
855 ),
856 b"(?:/|$)"
856 b"(?:/|$)"
857 )
857 )
858 .unwrap(),
858 .unwrap(),
859 Some(b"(?ia:.*ba{2}r)".to_vec()),
859 Some(b"(?ia:.*ba{2}r)".to_vec()),
860 );
860 );
861 assert_eq!(
861 assert_eq!(
862 build_single_regex(
862 build_single_regex(
863 &IgnorePattern::new(
863 &IgnorePattern::new(
864 PatternSyntax::RelRegexp,
864 PatternSyntax::RelRegexp,
865 b"(?ia)^ba{2}r",
865 b"(?ia)^ba{2}r",
866 Path::new("")
866 Path::new("")
867 ),
867 ),
868 b"(?:/|$)"
868 b"(?:/|$)"
869 )
869 )
870 .unwrap(),
870 .unwrap(),
871 Some(b"(?ia:^ba{2}r)".to_vec()),
871 Some(b"(?ia:^ba{2}r)".to_vec()),
872 );
872 );
873 }
873 }
874 }
874 }
@@ -1,2122 +1,2450 b''
1 // matchers.rs
1 // matchers.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
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 //! Structs and types for matching files and directories.
8 //! Structs and types for matching files and directories.
9
9
10 use format_bytes::format_bytes;
10 use format_bytes::format_bytes;
11 use once_cell::sync::OnceCell;
11 use once_cell::sync::OnceCell;
12
12
13 use crate::{
13 use crate::{
14 dirstate::dirs_multiset::DirsChildrenMultiset,
14 dirstate::dirs_multiset::DirsChildrenMultiset,
15 filepatterns::{
15 filepatterns::{
16 build_single_regex, filter_subincludes, get_patterns_from_file,
16 build_single_regex, filter_subincludes, get_patterns_from_file,
17 PatternFileWarning, PatternResult,
17 PatternFileWarning, PatternResult,
18 },
18 },
19 utils::{
19 utils::{
20 files::find_dirs,
20 files::{dir_ancestors, find_dirs},
21 hg_path::{HgPath, HgPathBuf, HgPathError},
21 hg_path::{HgPath, HgPathBuf, HgPathError},
22 Escaped,
22 Escaped,
23 },
23 },
24 DirsMultiset, FastHashMap, IgnorePattern, PatternError, PatternSyntax,
24 DirsMultiset, FastHashMap, IgnorePattern, PatternError, PatternSyntax,
25 };
25 };
26
26
27 use crate::dirstate::status::IgnoreFnType;
27 use crate::dirstate::status::IgnoreFnType;
28 use crate::filepatterns::normalize_path_bytes;
28 use crate::filepatterns::normalize_path_bytes;
29 use std::collections::HashSet;
29 use std::collections::HashSet;
30 use std::fmt::{Display, Error, Formatter};
30 use std::fmt::{Display, Error, Formatter};
31 use std::path::{Path, PathBuf};
31 use std::path::{Path, PathBuf};
32 use std::{borrow::ToOwned, collections::BTreeSet};
32 use std::{borrow::ToOwned, collections::BTreeSet};
33
33
34 #[derive(Debug, PartialEq)]
34 #[derive(Debug, PartialEq)]
35 pub enum VisitChildrenSet {
35 pub enum VisitChildrenSet {
36 /// Don't visit anything
36 /// Don't visit anything
37 Empty,
37 Empty,
38 /// Only visit this directory
38 /// Visit this directory and probably its children
39 This,
39 This,
40 /// Visit this directory and these subdirectories
40 /// Only visit the children (both files and directories) if they
41 /// are mentioned in this set. (empty set corresponds to [Empty])
41 /// TODO Should we implement a `NonEmptyHashSet`?
42 /// TODO Should we implement a `NonEmptyHashSet`?
42 Set(HashSet<HgPathBuf>),
43 Set(HashSet<HgPathBuf>),
43 /// Visit this directory and all subdirectories
44 /// Visit this directory and all subdirectories
45 /// (you can stop asking about the children set)
44 Recursive,
46 Recursive,
45 }
47 }
46
48
47 pub trait Matcher: core::fmt::Debug {
49 pub trait Matcher: core::fmt::Debug {
48 /// Explicitly listed files
50 /// Explicitly listed files
49 fn file_set(&self) -> Option<&HashSet<HgPathBuf>>;
51 fn file_set(&self) -> Option<&HashSet<HgPathBuf>>;
50 /// Returns whether `filename` is in `file_set`
52 /// Returns whether `filename` is in `file_set`
51 fn exact_match(&self, filename: &HgPath) -> bool;
53 fn exact_match(&self, filename: &HgPath) -> bool;
52 /// Returns whether `filename` is matched by this matcher
54 /// Returns whether `filename` is matched by this matcher
53 fn matches(&self, filename: &HgPath) -> bool;
55 fn matches(&self, filename: &HgPath) -> bool;
54 /// Decides whether a directory should be visited based on whether it
56 /// Decides whether a directory should be visited based on whether it
55 /// has potential matches in it or one of its subdirectories, and
57 /// has potential matches in it or one of its subdirectories, and
56 /// potentially lists which subdirectories of that directory should be
58 /// potentially lists which subdirectories of that directory should be
57 /// visited. This is based on the match's primary, included, and excluded
59 /// visited. This is based on the match's primary, included, and excluded
58 /// patterns.
60 /// patterns.
59 ///
61 ///
60 /// # Example
62 /// # Example
61 ///
63 ///
62 /// Assume matchers `['path:foo/bar', 'rootfilesin:qux']`, we would
64 /// Assume matchers `['path:foo/bar', 'rootfilesin:qux']`, we would
63 /// return the following values (assuming the implementation of
65 /// return the following values (assuming the implementation of
64 /// visit_children_set is capable of recognizing this; some implementations
66 /// visit_children_set is capable of recognizing this; some implementations
65 /// are not).
67 /// are not).
66 ///
68 ///
67 /// ```text
69 /// ```text
68 /// ```ignore
70 /// ```ignore
69 /// '' -> {'foo', 'qux'}
71 /// '' -> {'foo', 'qux'}
70 /// 'baz' -> set()
72 /// 'baz' -> set()
71 /// 'foo' -> {'bar'}
73 /// 'foo' -> {'bar'}
72 /// // Ideally this would be `Recursive`, but since the prefix nature of
74 /// // Ideally this would be `Recursive`, but since the prefix nature of
73 /// // matchers is applied to the entire matcher, we have to downgrade this
75 /// // matchers is applied to the entire matcher, we have to downgrade this
74 /// // to `This` due to the (yet to be implemented in Rust) non-prefix
76 /// // to `This` due to the (yet to be implemented in Rust) non-prefix
75 /// // `RootFilesIn'-kind matcher being mixed in.
77 /// // `RootFilesIn'-kind matcher being mixed in.
76 /// 'foo/bar' -> 'this'
78 /// 'foo/bar' -> 'this'
77 /// 'qux' -> 'this'
79 /// 'qux' -> 'this'
78 /// ```
80 /// ```
79 /// # Important
81 /// # Important
80 ///
82 ///
81 /// Most matchers do not know if they're representing files or
83 /// Most matchers do not know if they're representing files or
82 /// directories. They see `['path:dir/f']` and don't know whether `f` is a
84 /// directories. They see `['path:dir/f']` and don't know whether `f` is a
83 /// file or a directory, so `visit_children_set('dir')` for most matchers
85 /// file or a directory, so `visit_children_set('dir')` for most matchers
84 /// will return `HashSet{ HgPath { "f" } }`, but if the matcher knows it's
86 /// will return `HashSet{ HgPath { "f" } }`, but if the matcher knows it's
85 /// a file (like the yet to be implemented in Rust `ExactMatcher` does),
87 /// a file (like the yet to be implemented in Rust `ExactMatcher` does),
86 /// it may return `VisitChildrenSet::This`.
88 /// it may return `VisitChildrenSet::This`.
87 /// Do not rely on the return being a `HashSet` indicating that there are
89 /// Do not rely on the return being a `HashSet` indicating that there are
88 /// no files in this dir to investigate (or equivalently that if there are
90 /// no files in this dir to investigate (or equivalently that if there are
89 /// files to investigate in 'dir' that it will always return
91 /// files to investigate in 'dir' that it will always return
90 /// `VisitChildrenSet::This`).
92 /// `VisitChildrenSet::This`).
91 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet;
93 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet;
92 /// Matcher will match everything and `files_set()` will be empty:
94 /// Matcher will match everything and `files_set()` will be empty:
93 /// optimization might be possible.
95 /// optimization might be possible.
94 fn matches_everything(&self) -> bool;
96 fn matches_everything(&self) -> bool;
95 /// Matcher will match exactly the files in `files_set()`: optimization
97 /// Matcher will match exactly the files in `files_set()`: optimization
96 /// might be possible.
98 /// might be possible.
97 fn is_exact(&self) -> bool;
99 fn is_exact(&self) -> bool;
98 }
100 }
99
101
100 /// Matches everything.
102 /// Matches everything.
101 ///```
103 ///```
102 /// use hg::{ matchers::{Matcher, AlwaysMatcher}, utils::hg_path::HgPath };
104 /// use hg::{ matchers::{Matcher, AlwaysMatcher}, utils::hg_path::HgPath };
103 ///
105 ///
104 /// let matcher = AlwaysMatcher;
106 /// let matcher = AlwaysMatcher;
105 ///
107 ///
106 /// assert_eq!(matcher.matches(HgPath::new(b"whatever")), true);
108 /// assert_eq!(matcher.matches(HgPath::new(b"whatever")), true);
107 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), true);
109 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), true);
108 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), true);
110 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), true);
109 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
111 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
110 /// ```
112 /// ```
111 #[derive(Debug)]
113 #[derive(Debug)]
112 pub struct AlwaysMatcher;
114 pub struct AlwaysMatcher;
113
115
114 impl Matcher for AlwaysMatcher {
116 impl Matcher for AlwaysMatcher {
115 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
117 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
116 None
118 None
117 }
119 }
118 fn exact_match(&self, _filename: &HgPath) -> bool {
120 fn exact_match(&self, _filename: &HgPath) -> bool {
119 false
121 false
120 }
122 }
121 fn matches(&self, _filename: &HgPath) -> bool {
123 fn matches(&self, _filename: &HgPath) -> bool {
122 true
124 true
123 }
125 }
124 fn visit_children_set(&self, _directory: &HgPath) -> VisitChildrenSet {
126 fn visit_children_set(&self, _directory: &HgPath) -> VisitChildrenSet {
125 VisitChildrenSet::Recursive
127 VisitChildrenSet::Recursive
126 }
128 }
127 fn matches_everything(&self) -> bool {
129 fn matches_everything(&self) -> bool {
128 true
130 true
129 }
131 }
130 fn is_exact(&self) -> bool {
132 fn is_exact(&self) -> bool {
131 false
133 false
132 }
134 }
133 }
135 }
134
136
135 /// Matches nothing.
137 /// Matches nothing.
136 #[derive(Debug)]
138 #[derive(Debug)]
137 pub struct NeverMatcher;
139 pub struct NeverMatcher;
138
140
139 impl Matcher for NeverMatcher {
141 impl Matcher for NeverMatcher {
140 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
142 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
141 None
143 None
142 }
144 }
143 fn exact_match(&self, _filename: &HgPath) -> bool {
145 fn exact_match(&self, _filename: &HgPath) -> bool {
144 false
146 false
145 }
147 }
146 fn matches(&self, _filename: &HgPath) -> bool {
148 fn matches(&self, _filename: &HgPath) -> bool {
147 false
149 false
148 }
150 }
149 fn visit_children_set(&self, _directory: &HgPath) -> VisitChildrenSet {
151 fn visit_children_set(&self, _directory: &HgPath) -> VisitChildrenSet {
150 VisitChildrenSet::Empty
152 VisitChildrenSet::Empty
151 }
153 }
152 fn matches_everything(&self) -> bool {
154 fn matches_everything(&self) -> bool {
153 false
155 false
154 }
156 }
155 fn is_exact(&self) -> bool {
157 fn is_exact(&self) -> bool {
156 true
158 true
157 }
159 }
158 }
160 }
159
161
160 /// Matches the input files exactly. They are interpreted as paths, not
162 /// Matches the input files exactly. They are interpreted as paths, not
161 /// patterns.
163 /// patterns.
162 ///
164 ///
163 ///```
165 ///```
164 /// use hg::{ matchers::{Matcher, FileMatcher}, utils::hg_path::{HgPath, HgPathBuf} };
166 /// use hg::{ matchers::{Matcher, FileMatcher}, utils::hg_path::{HgPath, HgPathBuf} };
165 ///
167 ///
166 /// let files = vec![HgPathBuf::from_bytes(b"a.txt"), HgPathBuf::from_bytes(br"re:.*\.c$")];
168 /// let files = vec![HgPathBuf::from_bytes(b"a.txt"), HgPathBuf::from_bytes(br"re:.*\.c$")];
167 /// let matcher = FileMatcher::new(files).unwrap();
169 /// let matcher = FileMatcher::new(files).unwrap();
168 ///
170 ///
169 /// assert_eq!(matcher.matches(HgPath::new(b"a.txt")), true);
171 /// assert_eq!(matcher.matches(HgPath::new(b"a.txt")), true);
170 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), false);
172 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), false);
171 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), false);
173 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), false);
172 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
174 /// assert_eq!(matcher.matches(HgPath::new(br"re:.*\.c$")), true);
173 /// ```
175 /// ```
174 #[derive(Debug)]
176 #[derive(Debug)]
175 pub struct FileMatcher {
177 pub struct FileMatcher {
176 files: HashSet<HgPathBuf>,
178 files: HashSet<HgPathBuf>,
177 dirs: DirsMultiset,
179 dirs: DirsMultiset,
178 sorted_visitchildrenset_candidates: OnceCell<BTreeSet<HgPathBuf>>,
180 sorted_visitchildrenset_candidates: OnceCell<BTreeSet<HgPathBuf>>,
179 }
181 }
180
182
181 impl FileMatcher {
183 impl FileMatcher {
182 pub fn new(files: Vec<HgPathBuf>) -> Result<Self, HgPathError> {
184 pub fn new(files: Vec<HgPathBuf>) -> Result<Self, HgPathError> {
183 let dirs = DirsMultiset::from_manifest(&files)?;
185 let dirs = DirsMultiset::from_manifest(&files)?;
184 Ok(Self {
186 Ok(Self {
185 files: HashSet::from_iter(files),
187 files: HashSet::from_iter(files),
186 dirs,
188 dirs,
187 sorted_visitchildrenset_candidates: OnceCell::new(),
189 sorted_visitchildrenset_candidates: OnceCell::new(),
188 })
190 })
189 }
191 }
190 fn inner_matches(&self, filename: &HgPath) -> bool {
192 fn inner_matches(&self, filename: &HgPath) -> bool {
191 self.files.contains(filename.as_ref())
193 self.files.contains(filename.as_ref())
192 }
194 }
193 }
195 }
194
196
195 impl Matcher for FileMatcher {
197 impl Matcher for FileMatcher {
196 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
198 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
197 Some(&self.files)
199 Some(&self.files)
198 }
200 }
199 fn exact_match(&self, filename: &HgPath) -> bool {
201 fn exact_match(&self, filename: &HgPath) -> bool {
200 self.inner_matches(filename)
202 self.inner_matches(filename)
201 }
203 }
202 fn matches(&self, filename: &HgPath) -> bool {
204 fn matches(&self, filename: &HgPath) -> bool {
203 self.inner_matches(filename)
205 self.inner_matches(filename)
204 }
206 }
205 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
207 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
206 if self.files.is_empty() || !self.dirs.contains(directory) {
208 if self.files.is_empty() || !self.dirs.contains(directory) {
207 return VisitChildrenSet::Empty;
209 return VisitChildrenSet::Empty;
208 }
210 }
209
211
210 let compute_candidates = || -> BTreeSet<HgPathBuf> {
212 let compute_candidates = || -> BTreeSet<HgPathBuf> {
211 let mut candidates: BTreeSet<HgPathBuf> =
213 let mut candidates: BTreeSet<HgPathBuf> =
212 self.dirs.iter().cloned().collect();
214 self.dirs.iter().cloned().collect();
213 candidates.extend(self.files.iter().cloned());
215 candidates.extend(self.files.iter().cloned());
214 candidates.remove(HgPath::new(b""));
216 candidates.remove(HgPath::new(b""));
215 candidates
217 candidates
216 };
218 };
217 let candidates =
219 let candidates =
218 if directory.as_ref().is_empty() {
220 if directory.as_ref().is_empty() {
219 compute_candidates()
221 compute_candidates()
220 } else {
222 } else {
221 let sorted_candidates = self
223 let sorted_candidates = self
222 .sorted_visitchildrenset_candidates
224 .sorted_visitchildrenset_candidates
223 .get_or_init(compute_candidates);
225 .get_or_init(compute_candidates);
224 let directory_bytes = directory.as_ref().as_bytes();
226 let directory_bytes = directory.as_ref().as_bytes();
225 let start: HgPathBuf =
227 let start: HgPathBuf =
226 format_bytes!(b"{}/", directory_bytes).into();
228 format_bytes!(b"{}/", directory_bytes).into();
227 let start_len = start.len();
229 let start_len = start.len();
228 // `0` sorts after `/`
230 // `0` sorts after `/`
229 let end = format_bytes!(b"{}0", directory_bytes).into();
231 let end = format_bytes!(b"{}0", directory_bytes).into();
230 BTreeSet::from_iter(sorted_candidates.range(start..end).map(
232 BTreeSet::from_iter(sorted_candidates.range(start..end).map(
231 |c| HgPathBuf::from_bytes(&c.as_bytes()[start_len..]),
233 |c| HgPathBuf::from_bytes(&c.as_bytes()[start_len..]),
232 ))
234 ))
233 };
235 };
234
236
235 // `self.dirs` includes all of the directories, recursively, so if
237 // `self.dirs` includes all of the directories, recursively, so if
236 // we're attempting to match 'foo/bar/baz.txt', it'll have '', 'foo',
238 // we're attempting to match 'foo/bar/baz.txt', it'll have '', 'foo',
237 // 'foo/bar' in it. Thus we can safely ignore a candidate that has a
239 // 'foo/bar' in it. Thus we can safely ignore a candidate that has a
238 // '/' in it, indicating it's for a subdir-of-a-subdir; the immediate
240 // '/' in it, indicating it's for a subdir-of-a-subdir; the immediate
239 // subdir will be in there without a slash.
241 // subdir will be in there without a slash.
240 VisitChildrenSet::Set(
242 VisitChildrenSet::Set(
241 candidates
243 candidates
242 .into_iter()
244 .into_iter()
243 .filter_map(|c| {
245 .filter_map(|c| {
244 if c.bytes().all(|b| *b != b'/') {
246 if c.bytes().all(|b| *b != b'/') {
245 Some(c)
247 Some(c)
246 } else {
248 } else {
247 None
249 None
248 }
250 }
249 })
251 })
250 .collect(),
252 .collect(),
251 )
253 )
252 }
254 }
253 fn matches_everything(&self) -> bool {
255 fn matches_everything(&self) -> bool {
254 false
256 false
255 }
257 }
256 fn is_exact(&self) -> bool {
258 fn is_exact(&self) -> bool {
257 true
259 true
258 }
260 }
259 }
261 }
260
262
261 /// Matches a set of (kind, pat, source) against a 'root' directory.
263 /// Matches a set of (kind, pat, source) against a 'root' directory.
262 /// (Currently the 'root' directory is effectively always empty)
264 /// (Currently the 'root' directory is effectively always empty)
263 /// ```
265 /// ```
264 /// use hg::{
266 /// use hg::{
265 /// matchers::{PatternMatcher, Matcher},
267 /// matchers::{PatternMatcher, Matcher},
266 /// IgnorePattern,
268 /// IgnorePattern,
267 /// PatternSyntax,
269 /// PatternSyntax,
268 /// utils::hg_path::{HgPath, HgPathBuf}
270 /// utils::hg_path::{HgPath, HgPathBuf}
269 /// };
271 /// };
270 /// use std::collections::HashSet;
272 /// use std::collections::HashSet;
271 /// use std::path::Path;
273 /// use std::path::Path;
272 /// ///
274 /// ///
273 /// let ignore_patterns : Vec<IgnorePattern> =
275 /// let ignore_patterns : Vec<IgnorePattern> =
274 /// vec![IgnorePattern::new(PatternSyntax::Regexp, br".*\.c$", Path::new("")),
276 /// vec![IgnorePattern::new(PatternSyntax::Regexp, br".*\.c$", Path::new("")),
275 /// IgnorePattern::new(PatternSyntax::Path, b"foo/a", Path::new("")),
277 /// IgnorePattern::new(PatternSyntax::Path, b"foo/a", Path::new("")),
276 /// IgnorePattern::new(PatternSyntax::RelPath, b"b", Path::new("")),
278 /// IgnorePattern::new(PatternSyntax::RelPath, b"b", Path::new("")),
277 /// IgnorePattern::new(PatternSyntax::Glob, b"*.h", Path::new("")),
279 /// IgnorePattern::new(PatternSyntax::Glob, b"*.h", Path::new("")),
278 /// ];
280 /// ];
279 /// let matcher = PatternMatcher::new(ignore_patterns).unwrap();
281 /// let matcher = PatternMatcher::new(ignore_patterns).unwrap();
280 /// ///
282 /// ///
281 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), true); // matches re:.*\.c$
283 /// assert_eq!(matcher.matches(HgPath::new(b"main.c")), true); // matches re:.*\.c$
282 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), false);
284 /// assert_eq!(matcher.matches(HgPath::new(b"b.txt")), false);
283 /// assert_eq!(matcher.matches(HgPath::new(b"foo/a")), true); // matches path:foo/a
285 /// assert_eq!(matcher.matches(HgPath::new(b"foo/a")), true); // matches path:foo/a
284 /// assert_eq!(matcher.matches(HgPath::new(b"a")), false); // does not match path:b, since 'root' is 'foo'
286 /// assert_eq!(matcher.matches(HgPath::new(b"a")), false); // does not match path:b, since 'root' is 'foo'
285 /// assert_eq!(matcher.matches(HgPath::new(b"b")), true); // matches relpath:b, since 'root' is 'foo'
287 /// assert_eq!(matcher.matches(HgPath::new(b"b")), true); // matches relpath:b, since 'root' is 'foo'
286 /// assert_eq!(matcher.matches(HgPath::new(b"lib.h")), true); // matches glob:*.h
288 /// assert_eq!(matcher.matches(HgPath::new(b"lib.h")), true); // matches glob:*.h
287 /// assert_eq!(matcher.file_set().unwrap(),
289 /// assert_eq!(matcher.file_set().unwrap(),
288 /// &HashSet::from([HgPathBuf::from_bytes(b""), HgPathBuf::from_bytes(b"foo/a"),
290 /// &HashSet::from([HgPathBuf::from_bytes(b""), HgPathBuf::from_bytes(b"foo/a"),
289 /// HgPathBuf::from_bytes(b""), HgPathBuf::from_bytes(b"b")]));
291 /// HgPathBuf::from_bytes(b""), HgPathBuf::from_bytes(b"b")]));
290 /// assert_eq!(matcher.exact_match(HgPath::new(b"foo/a")), true);
292 /// assert_eq!(matcher.exact_match(HgPath::new(b"foo/a")), true);
291 /// assert_eq!(matcher.exact_match(HgPath::new(b"b")), true);
293 /// assert_eq!(matcher.exact_match(HgPath::new(b"b")), true);
292 /// assert_eq!(matcher.exact_match(HgPath::new(b"lib.h")), false); // exact matches are for (rel)path kinds
294 /// assert_eq!(matcher.exact_match(HgPath::new(b"lib.h")), false); // exact matches are for (rel)path kinds
293 /// ```
295 /// ```
294 pub struct PatternMatcher<'a> {
296 pub struct PatternMatcher<'a> {
295 patterns: Vec<u8>,
297 patterns: Vec<u8>,
296 match_fn: IgnoreFnType<'a>,
298 match_fn: IgnoreFnType<'a>,
297 /// Whether all the patterns match a prefix (i.e. recursively)
299 /// Whether all the patterns match a prefix (i.e. recursively)
298 prefix: bool,
300 prefix: bool,
299 files: HashSet<HgPathBuf>,
301 files: HashSet<HgPathBuf>,
302 dirs_explicit: HashSet<HgPathBuf>,
300 dirs: DirsMultiset,
303 dirs: DirsMultiset,
301 }
304 }
302
305
303 impl core::fmt::Debug for PatternMatcher<'_> {
306 impl core::fmt::Debug for PatternMatcher<'_> {
304 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
307 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
305 f.debug_struct("PatternMatcher")
308 f.debug_struct("PatternMatcher")
306 .field("patterns", &String::from_utf8_lossy(&self.patterns))
309 .field("patterns", &String::from_utf8_lossy(&self.patterns))
307 .field("prefix", &self.prefix)
310 .field("prefix", &self.prefix)
308 .field("files", &self.files)
311 .field("files", &self.files)
309 .field("dirs", &self.dirs)
312 .field("dirs", &self.dirs)
310 .finish()
313 .finish()
311 }
314 }
312 }
315 }
313
316
314 impl<'a> PatternMatcher<'a> {
317 impl<'a> PatternMatcher<'a> {
315 pub fn new(ignore_patterns: Vec<IgnorePattern>) -> PatternResult<Self> {
318 pub fn new(ignore_patterns: Vec<IgnorePattern>) -> PatternResult<Self> {
316 let (files, _) = roots_and_dirs(&ignore_patterns);
319 let RootsDirsAndParents {
317 let dirs = DirsMultiset::from_manifest(&files)?;
320 roots,
321 dirs: dirs_explicit,
322 parents,
323 } = roots_dirs_and_parents(&ignore_patterns)?;
324 let files = roots;
325 let dirs = parents;
318 let files: HashSet<HgPathBuf> = HashSet::from_iter(files);
326 let files: HashSet<HgPathBuf> = HashSet::from_iter(files);
319
327
320 let prefix = ignore_patterns.iter().all(|k| {
328 let prefix = ignore_patterns.iter().all(|k| {
321 matches!(k.syntax, PatternSyntax::Path | PatternSyntax::RelPath)
329 matches!(k.syntax, PatternSyntax::Path | PatternSyntax::RelPath)
322 });
330 });
323 let (patterns, match_fn) = build_match(ignore_patterns, b"$")?;
331 let (patterns, match_fn) = build_match(ignore_patterns, b"$")?;
324
332
325 Ok(Self {
333 Ok(Self {
326 patterns,
334 patterns,
327 match_fn,
335 match_fn,
328 prefix,
336 prefix,
329 files,
337 files,
330 dirs,
338 dirs,
339 dirs_explicit,
331 })
340 })
332 }
341 }
333 }
342 }
334
343
335 impl<'a> Matcher for PatternMatcher<'a> {
344 impl<'a> Matcher for PatternMatcher<'a> {
336 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
345 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
337 Some(&self.files)
346 Some(&self.files)
338 }
347 }
339
348
340 fn exact_match(&self, filename: &HgPath) -> bool {
349 fn exact_match(&self, filename: &HgPath) -> bool {
341 self.files.contains(filename)
350 self.files.contains(filename)
342 }
351 }
343
352
344 fn matches(&self, filename: &HgPath) -> bool {
353 fn matches(&self, filename: &HgPath) -> bool {
345 if self.files.contains(filename) {
354 if self.files.contains(filename) {
346 return true;
355 return true;
347 }
356 }
348 (self.match_fn)(filename)
357 (self.match_fn)(filename)
349 }
358 }
350
359
351 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
360 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
352 if self.prefix && self.files.contains(directory) {
361 if self.prefix && self.files.contains(directory) {
353 return VisitChildrenSet::Recursive;
362 return VisitChildrenSet::Recursive;
354 }
363 }
355 let path_or_parents_in_set = find_dirs(directory)
364 if self.dirs.contains(directory) {
356 .any(|parent_dir| self.files.contains(parent_dir));
365 return VisitChildrenSet::This;
357 if self.dirs.contains(directory) || path_or_parents_in_set {
366 }
367 if dir_ancestors(directory).any(|parent_dir| {
368 self.files.contains(parent_dir)
369 || self.dirs_explicit.contains(parent_dir)
370 }) {
358 VisitChildrenSet::This
371 VisitChildrenSet::This
359 } else {
372 } else {
360 VisitChildrenSet::Empty
373 VisitChildrenSet::Empty
361 }
374 }
362 }
375 }
363
376
364 fn matches_everything(&self) -> bool {
377 fn matches_everything(&self) -> bool {
365 false
378 false
366 }
379 }
367
380
368 fn is_exact(&self) -> bool {
381 fn is_exact(&self) -> bool {
369 false
382 false
370 }
383 }
371 }
384 }
372
385
373 /// Matches files that are included in the ignore rules.
386 /// Matches files that are included in the ignore rules.
374 /// ```
387 /// ```
375 /// use hg::{
388 /// use hg::{
376 /// matchers::{IncludeMatcher, Matcher},
389 /// matchers::{IncludeMatcher, Matcher},
377 /// IgnorePattern,
390 /// IgnorePattern,
378 /// PatternSyntax,
391 /// PatternSyntax,
379 /// utils::hg_path::HgPath
392 /// utils::hg_path::HgPath
380 /// };
393 /// };
381 /// use std::path::Path;
394 /// use std::path::Path;
382 /// ///
395 /// ///
383 /// let ignore_patterns =
396 /// let ignore_patterns =
384 /// vec![IgnorePattern::new(PatternSyntax::RootGlob, b"this*", Path::new(""))];
397 /// vec![IgnorePattern::new(PatternSyntax::RootGlob, b"this*", Path::new(""))];
385 /// let matcher = IncludeMatcher::new(ignore_patterns).unwrap();
398 /// let matcher = IncludeMatcher::new(ignore_patterns).unwrap();
386 /// ///
399 /// ///
387 /// assert_eq!(matcher.matches(HgPath::new(b"testing")), false);
400 /// assert_eq!(matcher.matches(HgPath::new(b"testing")), false);
388 /// assert_eq!(matcher.matches(HgPath::new(b"this should work")), true);
401 /// assert_eq!(matcher.matches(HgPath::new(b"this should work")), true);
389 /// assert_eq!(matcher.matches(HgPath::new(b"this also")), true);
402 /// assert_eq!(matcher.matches(HgPath::new(b"this also")), true);
390 /// assert_eq!(matcher.matches(HgPath::new(b"but not this")), false);
403 /// assert_eq!(matcher.matches(HgPath::new(b"but not this")), false);
391 /// ///
404 /// ///
392 /// let ignore_patterns =
405 /// let ignore_patterns =
393 /// vec![IgnorePattern::new(PatternSyntax::RootFiles, b"dir/subdir", Path::new(""))];
406 /// vec![IgnorePattern::new(PatternSyntax::RootFilesIn, b"dir/subdir", Path::new(""))];
394 /// let matcher = IncludeMatcher::new(ignore_patterns).unwrap();
407 /// let matcher = IncludeMatcher::new(ignore_patterns).unwrap();
395 /// ///
408 /// ///
396 /// assert!(!matcher.matches(HgPath::new(b"file")));
409 /// assert!(!matcher.matches(HgPath::new(b"file")));
397 /// assert!(!matcher.matches(HgPath::new(b"dir/file")));
410 /// assert!(!matcher.matches(HgPath::new(b"dir/file")));
398 /// assert!(matcher.matches(HgPath::new(b"dir/subdir/file")));
411 /// assert!(matcher.matches(HgPath::new(b"dir/subdir/file")));
399 /// assert!(!matcher.matches(HgPath::new(b"dir/subdir/subsubdir/file")));
412 /// assert!(!matcher.matches(HgPath::new(b"dir/subdir/subsubdir/file")));
400 /// ```
413 /// ```
401 pub struct IncludeMatcher<'a> {
414 pub struct IncludeMatcher<'a> {
402 patterns: Vec<u8>,
415 patterns: Vec<u8>,
403 match_fn: IgnoreFnType<'a>,
416 match_fn: IgnoreFnType<'a>,
404 /// Whether all the patterns match a prefix (i.e. recursively)
417 /// Whether all the patterns match a prefix (i.e. recursively)
405 prefix: bool,
418 prefix: bool,
406 roots: HashSet<HgPathBuf>,
419 roots: HashSet<HgPathBuf>,
407 dirs: HashSet<HgPathBuf>,
420 dirs: HashSet<HgPathBuf>,
408 parents: HashSet<HgPathBuf>,
421 parents: DirsMultiset,
409 }
422 }
410
423
411 impl core::fmt::Debug for IncludeMatcher<'_> {
424 impl core::fmt::Debug for IncludeMatcher<'_> {
412 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
425 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
413 f.debug_struct("IncludeMatcher")
426 f.debug_struct("IncludeMatcher")
414 .field("patterns", &String::from_utf8_lossy(&self.patterns))
427 .field("patterns", &String::from_utf8_lossy(&self.patterns))
415 .field("prefix", &self.prefix)
428 .field("prefix", &self.prefix)
416 .field("roots", &self.roots)
429 .field("roots", &self.roots)
417 .field("dirs", &self.dirs)
430 .field("dirs", &self.dirs)
418 .field("parents", &self.parents)
431 .field("parents", &self.parents)
419 .finish()
432 .finish()
420 }
433 }
421 }
434 }
422
435
423 impl<'a> Matcher for IncludeMatcher<'a> {
436 impl<'a> Matcher for IncludeMatcher<'a> {
424 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
437 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
425 None
438 None
426 }
439 }
427
440
428 fn exact_match(&self, _filename: &HgPath) -> bool {
441 fn exact_match(&self, _filename: &HgPath) -> bool {
429 false
442 false
430 }
443 }
431
444
432 fn matches(&self, filename: &HgPath) -> bool {
445 fn matches(&self, filename: &HgPath) -> bool {
433 (self.match_fn)(filename)
446 (self.match_fn)(filename)
434 }
447 }
435
448
436 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
449 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
437 let dir = directory;
450 let dir = directory;
438 if self.prefix && self.roots.contains(dir) {
451 if self.prefix && self.roots.contains(dir) {
439 return VisitChildrenSet::Recursive;
452 return VisitChildrenSet::Recursive;
440 }
453 }
441 if self.roots.contains(HgPath::new(b""))
454 if self.roots.contains(HgPath::new(b""))
442 || self.roots.contains(dir)
455 || self.roots.contains(dir)
443 || self.dirs.contains(dir)
456 || self.dirs.contains(dir)
444 || find_dirs(dir).any(|parent_dir| self.roots.contains(parent_dir))
457 || find_dirs(dir).any(|parent_dir| self.roots.contains(parent_dir))
445 {
458 {
446 return VisitChildrenSet::This;
459 return VisitChildrenSet::This;
447 }
460 }
448
461
449 if self.parents.contains(dir.as_ref()) {
462 if self.parents.contains(dir.as_ref()) {
450 let multiset = self.get_all_parents_children();
463 let multiset = self.get_all_parents_children();
451 if let Some(children) = multiset.get(dir) {
464 if let Some(children) = multiset.get(dir) {
452 return VisitChildrenSet::Set(
465 return VisitChildrenSet::Set(
453 children.iter().map(HgPathBuf::from).collect(),
466 children.iter().map(HgPathBuf::from).collect(),
454 );
467 );
455 }
468 }
456 }
469 }
457 VisitChildrenSet::Empty
470 VisitChildrenSet::Empty
458 }
471 }
459
472
460 fn matches_everything(&self) -> bool {
473 fn matches_everything(&self) -> bool {
461 false
474 false
462 }
475 }
463
476
464 fn is_exact(&self) -> bool {
477 fn is_exact(&self) -> bool {
465 false
478 false
466 }
479 }
467 }
480 }
468
481
469 /// The union of multiple matchers. Will match if any of the matchers match.
482 /// The union of multiple matchers. Will match if any of the matchers match.
470 #[derive(Debug)]
483 #[derive(Debug)]
471 pub struct UnionMatcher {
484 pub struct UnionMatcher {
472 matchers: Vec<Box<dyn Matcher + Sync>>,
485 matchers: Vec<Box<dyn Matcher + Sync>>,
473 }
486 }
474
487
475 impl Matcher for UnionMatcher {
488 impl Matcher for UnionMatcher {
476 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
489 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
477 None
490 None
478 }
491 }
479
492
480 fn exact_match(&self, _filename: &HgPath) -> bool {
493 fn exact_match(&self, _filename: &HgPath) -> bool {
481 false
494 false
482 }
495 }
483
496
484 fn matches(&self, filename: &HgPath) -> bool {
497 fn matches(&self, filename: &HgPath) -> bool {
485 self.matchers.iter().any(|m| m.matches(filename))
498 self.matchers.iter().any(|m| m.matches(filename))
486 }
499 }
487
500
488 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
501 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
489 let mut result = HashSet::new();
502 let mut result = HashSet::new();
490 let mut this = false;
503 let mut this = false;
491 for matcher in self.matchers.iter() {
504 for matcher in self.matchers.iter() {
492 let visit = matcher.visit_children_set(directory);
505 let visit = matcher.visit_children_set(directory);
493 match visit {
506 match visit {
494 VisitChildrenSet::Empty => continue,
507 VisitChildrenSet::Empty => continue,
495 VisitChildrenSet::This => {
508 VisitChildrenSet::This => {
496 this = true;
509 this = true;
497 // Don't break, we might have an 'all' in here.
510 // Don't break, we might have an 'all' in here.
498 continue;
511 continue;
499 }
512 }
500 VisitChildrenSet::Set(set) => {
513 VisitChildrenSet::Set(set) => {
501 result.extend(set);
514 result.extend(set);
502 }
515 }
503 VisitChildrenSet::Recursive => {
516 VisitChildrenSet::Recursive => {
504 return visit;
517 return visit;
505 }
518 }
506 }
519 }
507 }
520 }
508 if this {
521 if this {
509 return VisitChildrenSet::This;
522 return VisitChildrenSet::This;
510 }
523 }
511 if result.is_empty() {
524 if result.is_empty() {
512 VisitChildrenSet::Empty
525 VisitChildrenSet::Empty
513 } else {
526 } else {
514 VisitChildrenSet::Set(result)
527 VisitChildrenSet::Set(result)
515 }
528 }
516 }
529 }
517
530
518 fn matches_everything(&self) -> bool {
531 fn matches_everything(&self) -> bool {
519 // TODO Maybe if all are AlwaysMatcher?
532 // TODO Maybe if all are AlwaysMatcher?
520 false
533 false
521 }
534 }
522
535
523 fn is_exact(&self) -> bool {
536 fn is_exact(&self) -> bool {
524 false
537 false
525 }
538 }
526 }
539 }
527
540
528 impl UnionMatcher {
541 impl UnionMatcher {
529 pub fn new(matchers: Vec<Box<dyn Matcher + Sync>>) -> Self {
542 pub fn new(matchers: Vec<Box<dyn Matcher + Sync>>) -> Self {
530 Self { matchers }
543 Self { matchers }
531 }
544 }
532 }
545 }
533
546
534 #[derive(Debug)]
547 #[derive(Debug)]
535 pub struct IntersectionMatcher {
548 pub struct IntersectionMatcher {
536 m1: Box<dyn Matcher + Sync>,
549 m1: Box<dyn Matcher + Sync>,
537 m2: Box<dyn Matcher + Sync>,
550 m2: Box<dyn Matcher + Sync>,
538 files: Option<HashSet<HgPathBuf>>,
551 files: Option<HashSet<HgPathBuf>>,
539 }
552 }
540
553
541 impl Matcher for IntersectionMatcher {
554 impl Matcher for IntersectionMatcher {
542 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
555 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
543 self.files.as_ref()
556 self.files.as_ref()
544 }
557 }
545
558
546 fn exact_match(&self, filename: &HgPath) -> bool {
559 fn exact_match(&self, filename: &HgPath) -> bool {
547 self.files.as_ref().map_or(false, |f| f.contains(filename))
560 self.files.as_ref().map_or(false, |f| f.contains(filename))
548 }
561 }
549
562
550 fn matches(&self, filename: &HgPath) -> bool {
563 fn matches(&self, filename: &HgPath) -> bool {
551 self.m1.matches(filename) && self.m2.matches(filename)
564 self.m1.matches(filename) && self.m2.matches(filename)
552 }
565 }
553
566
554 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
567 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
555 let m1_set = self.m1.visit_children_set(directory);
568 let m1_set = self.m1.visit_children_set(directory);
556 if m1_set == VisitChildrenSet::Empty {
569 if m1_set == VisitChildrenSet::Empty {
557 return VisitChildrenSet::Empty;
570 return VisitChildrenSet::Empty;
558 }
571 }
559 let m2_set = self.m2.visit_children_set(directory);
572 let m2_set = self.m2.visit_children_set(directory);
560 if m2_set == VisitChildrenSet::Empty {
573 if m2_set == VisitChildrenSet::Empty {
561 return VisitChildrenSet::Empty;
574 return VisitChildrenSet::Empty;
562 }
575 }
563
576
564 if m1_set == VisitChildrenSet::Recursive {
577 if m1_set == VisitChildrenSet::Recursive {
565 return m2_set;
578 return m2_set;
566 } else if m2_set == VisitChildrenSet::Recursive {
579 } else if m2_set == VisitChildrenSet::Recursive {
567 return m1_set;
580 return m1_set;
568 }
581 }
569
582
570 match (&m1_set, &m2_set) {
583 match (&m1_set, &m2_set) {
571 (VisitChildrenSet::Recursive, _) => m2_set,
584 (VisitChildrenSet::Recursive, _) => m2_set,
572 (_, VisitChildrenSet::Recursive) => m1_set,
585 (_, VisitChildrenSet::Recursive) => m1_set,
573 (VisitChildrenSet::This, _) | (_, VisitChildrenSet::This) => {
586 (VisitChildrenSet::This, _) | (_, VisitChildrenSet::This) => {
574 VisitChildrenSet::This
587 VisitChildrenSet::This
575 }
588 }
576 (VisitChildrenSet::Set(m1), VisitChildrenSet::Set(m2)) => {
589 (VisitChildrenSet::Set(m1), VisitChildrenSet::Set(m2)) => {
577 let set: HashSet<_> = m1.intersection(m2).cloned().collect();
590 let set: HashSet<_> = m1.intersection(m2).cloned().collect();
578 if set.is_empty() {
591 if set.is_empty() {
579 VisitChildrenSet::Empty
592 VisitChildrenSet::Empty
580 } else {
593 } else {
581 VisitChildrenSet::Set(set)
594 VisitChildrenSet::Set(set)
582 }
595 }
583 }
596 }
584 _ => unreachable!(),
597 _ => unreachable!(),
585 }
598 }
586 }
599 }
587
600
588 fn matches_everything(&self) -> bool {
601 fn matches_everything(&self) -> bool {
589 self.m1.matches_everything() && self.m2.matches_everything()
602 self.m1.matches_everything() && self.m2.matches_everything()
590 }
603 }
591
604
592 fn is_exact(&self) -> bool {
605 fn is_exact(&self) -> bool {
593 self.m1.is_exact() || self.m2.is_exact()
606 self.m1.is_exact() || self.m2.is_exact()
594 }
607 }
595 }
608 }
596
609
597 impl IntersectionMatcher {
610 impl IntersectionMatcher {
598 pub fn new(
611 pub fn new(
599 mut m1: Box<dyn Matcher + Sync>,
612 mut m1: Box<dyn Matcher + Sync>,
600 mut m2: Box<dyn Matcher + Sync>,
613 mut m2: Box<dyn Matcher + Sync>,
601 ) -> Self {
614 ) -> Self {
602 let files = if m1.is_exact() || m2.is_exact() {
615 let files = if m1.is_exact() || m2.is_exact() {
603 if !m1.is_exact() {
616 if !m1.is_exact() {
604 std::mem::swap(&mut m1, &mut m2);
617 std::mem::swap(&mut m1, &mut m2);
605 }
618 }
606 m1.file_set().map(|m1_files| {
619 m1.file_set().map(|m1_files| {
607 m1_files.iter().cloned().filter(|f| m2.matches(f)).collect()
620 m1_files.iter().cloned().filter(|f| m2.matches(f)).collect()
608 })
621 })
609 } else {
622 } else {
610 // without exact input file sets, we can't do an exact
623 // without exact input file sets, we can't do an exact
611 // intersection, so we must over-approximate by
624 // intersection, so we must over-approximate by
612 // unioning instead
625 // unioning instead
613 m1.file_set().map(|m1_files| match m2.file_set() {
626 m1.file_set().map(|m1_files| match m2.file_set() {
614 Some(m2_files) => m1_files.union(m2_files).cloned().collect(),
627 Some(m2_files) => m1_files.union(m2_files).cloned().collect(),
615 None => m1_files.iter().cloned().collect(),
628 None => m1_files.iter().cloned().collect(),
616 })
629 })
617 };
630 };
618 Self { m1, m2, files }
631 Self { m1, m2, files }
619 }
632 }
620 }
633 }
621
634
622 #[derive(Debug)]
635 #[derive(Debug)]
623 pub struct DifferenceMatcher {
636 pub struct DifferenceMatcher {
624 base: Box<dyn Matcher + Sync>,
637 base: Box<dyn Matcher + Sync>,
625 excluded: Box<dyn Matcher + Sync>,
638 excluded: Box<dyn Matcher + Sync>,
626 files: Option<HashSet<HgPathBuf>>,
639 files: Option<HashSet<HgPathBuf>>,
627 }
640 }
628
641
629 impl Matcher for DifferenceMatcher {
642 impl Matcher for DifferenceMatcher {
630 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
643 fn file_set(&self) -> Option<&HashSet<HgPathBuf>> {
631 self.files.as_ref()
644 self.files.as_ref()
632 }
645 }
633
646
634 fn exact_match(&self, filename: &HgPath) -> bool {
647 fn exact_match(&self, filename: &HgPath) -> bool {
635 self.files.as_ref().map_or(false, |f| f.contains(filename))
648 self.files.as_ref().map_or(false, |f| f.contains(filename))
636 }
649 }
637
650
638 fn matches(&self, filename: &HgPath) -> bool {
651 fn matches(&self, filename: &HgPath) -> bool {
639 self.base.matches(filename) && !self.excluded.matches(filename)
652 self.base.matches(filename) && !self.excluded.matches(filename)
640 }
653 }
641
654
642 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
655 fn visit_children_set(&self, directory: &HgPath) -> VisitChildrenSet {
643 let excluded_set = self.excluded.visit_children_set(directory);
656 let excluded_set = self.excluded.visit_children_set(directory);
644 if excluded_set == VisitChildrenSet::Recursive {
657 if excluded_set == VisitChildrenSet::Recursive {
645 return VisitChildrenSet::Empty;
658 return VisitChildrenSet::Empty;
646 }
659 }
647 let base_set = self.base.visit_children_set(directory);
660 let base_set = self.base.visit_children_set(directory);
648 // Possible values for base: 'recursive', 'this', set(...), set()
661 // Possible values for base: 'recursive', 'this', set(...), set()
649 // Possible values for excluded: 'this', set(...), set()
662 // Possible values for excluded: 'this', set(...), set()
650 // If excluded has nothing under here that we care about, return base,
663 // If excluded has nothing under here that we care about, return base,
651 // even if it's 'recursive'.
664 // even if it's 'recursive'.
652 if excluded_set == VisitChildrenSet::Empty {
665 if excluded_set == VisitChildrenSet::Empty {
653 return base_set;
666 return base_set;
654 }
667 }
655 match base_set {
668 match base_set {
656 VisitChildrenSet::This | VisitChildrenSet::Recursive => {
669 VisitChildrenSet::This | VisitChildrenSet::Recursive => {
657 // Never return 'recursive' here if excluded_set is any kind of
670 // Never return 'recursive' here if excluded_set is any kind of
658 // non-empty (either 'this' or set(foo)), since excluded might
671 // non-empty (either 'this' or set(foo)), since excluded might
659 // return set() for a subdirectory.
672 // return set() for a subdirectory.
660 VisitChildrenSet::This
673 VisitChildrenSet::This
661 }
674 }
662 set => {
675 set => {
663 // Possible values for base: set(...), set()
676 // Possible values for base: set(...), set()
664 // Possible values for excluded: 'this', set(...)
677 // Possible values for excluded: 'this', set(...)
665 // We ignore excluded set results. They're possibly incorrect:
678 // We ignore excluded set results. They're possibly incorrect:
666 // base = path:dir/subdir
679 // base = path:dir/subdir
667 // excluded=rootfilesin:dir,
680 // excluded=rootfilesin:dir,
668 // visit_children_set(''):
681 // visit_children_set(''):
669 // base returns {'dir'}, excluded returns {'dir'}, if we
682 // base returns {'dir'}, excluded returns {'dir'}, if we
670 // subtracted we'd return set(), which is *not* correct, we
683 // subtracted we'd return set(), which is *not* correct, we
671 // still need to visit 'dir'!
684 // still need to visit 'dir'!
672 set
685 set
673 }
686 }
674 }
687 }
675 }
688 }
676
689
677 fn matches_everything(&self) -> bool {
690 fn matches_everything(&self) -> bool {
678 false
691 false
679 }
692 }
680
693
681 fn is_exact(&self) -> bool {
694 fn is_exact(&self) -> bool {
682 self.base.is_exact()
695 self.base.is_exact()
683 }
696 }
684 }
697 }
685
698
686 impl DifferenceMatcher {
699 impl DifferenceMatcher {
687 pub fn new(
700 pub fn new(
688 base: Box<dyn Matcher + Sync>,
701 base: Box<dyn Matcher + Sync>,
689 excluded: Box<dyn Matcher + Sync>,
702 excluded: Box<dyn Matcher + Sync>,
690 ) -> Self {
703 ) -> Self {
691 let base_is_exact = base.is_exact();
704 let base_is_exact = base.is_exact();
692 let base_files = base.file_set().map(ToOwned::to_owned);
705 let base_files = base.file_set().map(ToOwned::to_owned);
693 let mut new = Self {
706 let mut new = Self {
694 base,
707 base,
695 excluded,
708 excluded,
696 files: None,
709 files: None,
697 };
710 };
698 if base_is_exact {
711 if base_is_exact {
699 new.files = base_files.map(|files| {
712 new.files = base_files.map(|files| {
700 files.iter().cloned().filter(|f| new.matches(f)).collect()
713 files.iter().cloned().filter(|f| new.matches(f)).collect()
701 });
714 });
702 }
715 }
703 new
716 new
704 }
717 }
705 }
718 }
706
719
707 /// Wraps [`regex::bytes::Regex`] to improve performance in multithreaded
720 /// Wraps [`regex::bytes::Regex`] to improve performance in multithreaded
708 /// contexts.
721 /// contexts.
709 ///
722 ///
710 /// The `status` algorithm makes heavy use of threads, and calling `is_match`
723 /// The `status` algorithm makes heavy use of threads, and calling `is_match`
711 /// from many threads at once is prone to contention, probably within the
724 /// from many threads at once is prone to contention, probably within the
712 /// scratch space needed as the regex DFA is built lazily.
725 /// scratch space needed as the regex DFA is built lazily.
713 ///
726 ///
714 /// We are in the process of raising the issue upstream, but for now
727 /// We are in the process of raising the issue upstream, but for now
715 /// the workaround used here is to store the `Regex` in a lazily populated
728 /// the workaround used here is to store the `Regex` in a lazily populated
716 /// thread-local variable, sharing the initial read-only compilation, but
729 /// thread-local variable, sharing the initial read-only compilation, but
717 /// not the lazy dfa scratch space mentioned above.
730 /// not the lazy dfa scratch space mentioned above.
718 ///
731 ///
719 /// This reduces the contention observed with 16+ threads, but does not
732 /// This reduces the contention observed with 16+ threads, but does not
720 /// completely remove it. Hopefully this can be addressed upstream.
733 /// completely remove it. Hopefully this can be addressed upstream.
721 struct RegexMatcher {
734 struct RegexMatcher {
722 /// Compiled at the start of the status algorithm, used as a base for
735 /// Compiled at the start of the status algorithm, used as a base for
723 /// cloning in each thread-local `self.local`, thus sharing the expensive
736 /// cloning in each thread-local `self.local`, thus sharing the expensive
724 /// first compilation.
737 /// first compilation.
725 base: regex::bytes::Regex,
738 base: regex::bytes::Regex,
726 /// Thread-local variable that holds the `Regex` that is actually queried
739 /// Thread-local variable that holds the `Regex` that is actually queried
727 /// from each thread.
740 /// from each thread.
728 local: thread_local::ThreadLocal<regex::bytes::Regex>,
741 local: thread_local::ThreadLocal<regex::bytes::Regex>,
729 }
742 }
730
743
731 impl RegexMatcher {
744 impl RegexMatcher {
732 /// Returns whether the path matches the stored `Regex`.
745 /// Returns whether the path matches the stored `Regex`.
733 pub fn is_match(&self, path: &HgPath) -> bool {
746 pub fn is_match(&self, path: &HgPath) -> bool {
734 self.local
747 self.local
735 .get_or(|| self.base.clone())
748 .get_or(|| self.base.clone())
736 .is_match(path.as_bytes())
749 .is_match(path.as_bytes())
737 }
750 }
738 }
751 }
739
752
740 /// Return a `RegexBuilder` from a bytes pattern
753 /// Return a `RegexBuilder` from a bytes pattern
741 ///
754 ///
742 /// This works around the fact that even if it works on byte haysacks,
755 /// This works around the fact that even if it works on byte haysacks,
743 /// [`regex::bytes::Regex`] still uses UTF-8 patterns.
756 /// [`regex::bytes::Regex`] still uses UTF-8 patterns.
744 pub fn re_bytes_builder(pattern: &[u8]) -> regex::bytes::RegexBuilder {
757 pub fn re_bytes_builder(pattern: &[u8]) -> regex::bytes::RegexBuilder {
745 use std::io::Write;
758 use std::io::Write;
746
759
747 // The `regex` crate adds `.*` to the start and end of expressions if there
760 // The `regex` crate adds `.*` to the start and end of expressions if there
748 // are no anchors, so add the start anchor.
761 // are no anchors, so add the start anchor.
749 let mut escaped_bytes = vec![b'^', b'(', b'?', b':'];
762 let mut escaped_bytes = vec![b'^', b'(', b'?', b':'];
750 for byte in pattern {
763 for byte in pattern {
751 if *byte > 127 {
764 if *byte > 127 {
752 write!(escaped_bytes, "\\x{:x}", *byte).unwrap();
765 write!(escaped_bytes, "\\x{:x}", *byte).unwrap();
753 } else {
766 } else {
754 escaped_bytes.push(*byte);
767 escaped_bytes.push(*byte);
755 }
768 }
756 }
769 }
757 escaped_bytes.push(b')');
770 escaped_bytes.push(b')');
758
771
759 // Avoid the cost of UTF8 checking
772 // Avoid the cost of UTF8 checking
760 //
773 //
761 // # Safety
774 // # Safety
762 // This is safe because we escaped all non-ASCII bytes.
775 // This is safe because we escaped all non-ASCII bytes.
763 let pattern_string = unsafe { String::from_utf8_unchecked(escaped_bytes) };
776 let pattern_string = unsafe { String::from_utf8_unchecked(escaped_bytes) };
764 regex::bytes::RegexBuilder::new(&pattern_string)
777 regex::bytes::RegexBuilder::new(&pattern_string)
765 }
778 }
766
779
767 /// Returns a function that matches an `HgPath` against the given regex
780 /// Returns a function that matches an `HgPath` against the given regex
768 /// pattern.
781 /// pattern.
769 ///
782 ///
770 /// This can fail when the pattern is invalid or not supported by the
783 /// This can fail when the pattern is invalid or not supported by the
771 /// underlying engine (the `regex` crate), for instance anything with
784 /// underlying engine (the `regex` crate), for instance anything with
772 /// back-references.
785 /// back-references.
773 #[logging_timer::time("trace")]
786 #[logging_timer::time("trace")]
774 fn re_matcher(pattern: &[u8]) -> PatternResult<RegexMatcher> {
787 fn re_matcher(pattern: &[u8]) -> PatternResult<RegexMatcher> {
775 let re = re_bytes_builder(pattern)
788 let re = re_bytes_builder(pattern)
776 .unicode(false)
789 .unicode(false)
777 // Big repos with big `.hgignore` will hit the default limit and
790 // Big repos with big `.hgignore` will hit the default limit and
778 // incur a significant performance hit. One repo's `hg status` hit
791 // incur a significant performance hit. One repo's `hg status` hit
779 // multiple *minutes*.
792 // multiple *minutes*.
780 .dfa_size_limit(50 * (1 << 20))
793 .dfa_size_limit(50 * (1 << 20))
781 .build()
794 .build()
782 .map_err(|e| PatternError::UnsupportedSyntax(e.to_string()))?;
795 .map_err(|e| PatternError::UnsupportedSyntax(e.to_string()))?;
783
796
784 Ok(RegexMatcher {
797 Ok(RegexMatcher {
785 base: re,
798 base: re,
786 local: Default::default(),
799 local: Default::default(),
787 })
800 })
788 }
801 }
789
802
790 /// Returns the regex pattern and a function that matches an `HgPath` against
803 /// Returns the regex pattern and a function that matches an `HgPath` against
791 /// said regex formed by the given ignore patterns.
804 /// said regex formed by the given ignore patterns.
792 fn build_regex_match<'a>(
805 fn build_regex_match<'a>(
793 ignore_patterns: &[IgnorePattern],
806 ignore_patterns: &[IgnorePattern],
794 glob_suffix: &[u8],
807 glob_suffix: &[u8],
795 ) -> PatternResult<(Vec<u8>, IgnoreFnType<'a>)> {
808 ) -> PatternResult<(Vec<u8>, IgnoreFnType<'a>)> {
796 let mut regexps = vec![];
809 let mut regexps = vec![];
797 let mut exact_set = HashSet::new();
810 let mut exact_set = HashSet::new();
798
811
799 for pattern in ignore_patterns {
812 for pattern in ignore_patterns {
800 if let Some(re) = build_single_regex(pattern, glob_suffix)? {
813 if let Some(re) = build_single_regex(pattern, glob_suffix)? {
801 regexps.push(re);
814 regexps.push(re);
802 } else {
815 } else {
803 let exact = normalize_path_bytes(&pattern.pattern);
816 let exact = normalize_path_bytes(&pattern.pattern);
804 exact_set.insert(HgPathBuf::from_bytes(&exact));
817 exact_set.insert(HgPathBuf::from_bytes(&exact));
805 }
818 }
806 }
819 }
807
820
808 let full_regex = regexps.join(&b'|');
821 let full_regex = regexps.join(&b'|');
809
822
810 // An empty pattern would cause the regex engine to incorrectly match the
823 // An empty pattern would cause the regex engine to incorrectly match the
811 // (empty) root directory
824 // (empty) root directory
812 let func = if !(regexps.is_empty()) {
825 let func = if !(regexps.is_empty()) {
813 let matcher = re_matcher(&full_regex)?;
826 let matcher = re_matcher(&full_regex)?;
814 let func = move |filename: &HgPath| {
827 let func = move |filename: &HgPath| {
815 exact_set.contains(filename) || matcher.is_match(filename)
828 exact_set.contains(filename) || matcher.is_match(filename)
816 };
829 };
817 Box::new(func) as IgnoreFnType
830 Box::new(func) as IgnoreFnType
818 } else {
831 } else {
819 let func = move |filename: &HgPath| exact_set.contains(filename);
832 let func = move |filename: &HgPath| exact_set.contains(filename);
820 Box::new(func) as IgnoreFnType
833 Box::new(func) as IgnoreFnType
821 };
834 };
822
835
823 Ok((full_regex, func))
836 Ok((full_regex, func))
824 }
837 }
825
838
826 /// Returns roots and directories corresponding to each pattern.
839 /// Returns roots and directories corresponding to each pattern.
827 ///
840 ///
828 /// This calculates the roots and directories exactly matching the patterns and
841 /// This calculates the roots and directories exactly matching the patterns and
829 /// returns a tuple of (roots, dirs). It does not return other directories
842 /// returns a tuple of (roots, dirs). It does not return other directories
830 /// which may also need to be considered, like the parent directories.
843 /// which may also need to be considered, like the parent directories.
831 fn roots_and_dirs(
844 fn roots_and_dirs(
832 ignore_patterns: &[IgnorePattern],
845 ignore_patterns: &[IgnorePattern],
833 ) -> (Vec<HgPathBuf>, Vec<HgPathBuf>) {
846 ) -> (Vec<HgPathBuf>, Vec<HgPathBuf>) {
834 let mut roots = Vec::new();
847 let mut roots = Vec::new();
835 let mut dirs = Vec::new();
848 let mut dirs = Vec::new();
836
849
837 for ignore_pattern in ignore_patterns {
850 for ignore_pattern in ignore_patterns {
838 let IgnorePattern {
851 let IgnorePattern {
839 syntax, pattern, ..
852 syntax, pattern, ..
840 } = ignore_pattern;
853 } = ignore_pattern;
841 match syntax {
854 match syntax {
842 PatternSyntax::RootGlob | PatternSyntax::Glob => {
855 PatternSyntax::RootGlob | PatternSyntax::Glob => {
843 let mut root = HgPathBuf::new();
856 let mut root = HgPathBuf::new();
844 for p in pattern.split(|c| *c == b'/') {
857 for p in pattern.split(|c| *c == b'/') {
845 if p.iter()
858 if p.iter()
846 .any(|c| matches!(*c, b'[' | b'{' | b'*' | b'?'))
859 .any(|c| matches!(*c, b'[' | b'{' | b'*' | b'?'))
847 {
860 {
848 break;
861 break;
849 }
862 }
850 root.push(HgPathBuf::from_bytes(p).as_ref());
863 root.push(HgPathBuf::from_bytes(p).as_ref());
851 }
864 }
852 roots.push(root);
865 roots.push(root);
853 }
866 }
854 PatternSyntax::Path
867 PatternSyntax::Path
855 | PatternSyntax::RelPath
868 | PatternSyntax::RelPath
856 | PatternSyntax::FilePath => {
869 | PatternSyntax::FilePath => {
857 let pat = HgPath::new(if pattern == b"." {
870 let pat = HgPath::new(if pattern == b"." {
858 &[] as &[u8]
871 &[] as &[u8]
859 } else {
872 } else {
860 pattern
873 pattern
861 });
874 });
862 roots.push(pat.to_owned());
875 roots.push(pat.to_owned());
863 }
876 }
864 PatternSyntax::RootFiles => {
877 PatternSyntax::RootFilesIn => {
865 let pat = if pattern == b"." {
878 let pat = if pattern == b"." {
866 &[] as &[u8]
879 &[] as &[u8]
867 } else {
880 } else {
868 pattern
881 pattern
869 };
882 };
870 dirs.push(HgPathBuf::from_bytes(pat));
883 dirs.push(HgPathBuf::from_bytes(pat));
871 }
884 }
872 _ => {
885 _ => {
873 roots.push(HgPathBuf::new());
886 roots.push(HgPathBuf::new());
874 }
887 }
875 }
888 }
876 }
889 }
877 (roots, dirs)
890 (roots, dirs)
878 }
891 }
879
892
880 /// Paths extracted from patterns
893 /// Paths extracted from patterns
881 #[derive(Debug, PartialEq)]
894 #[derive(Debug, PartialEq)]
882 struct RootsDirsAndParents {
895 struct RootsDirsAndParents {
883 /// Directories to match recursively
896 /// Directories to match recursively
884 pub roots: HashSet<HgPathBuf>,
897 pub roots: HashSet<HgPathBuf>,
885 /// Directories to match non-recursively
898 /// Directories to match non-recursively
886 pub dirs: HashSet<HgPathBuf>,
899 pub dirs: HashSet<HgPathBuf>,
887 /// Implicitly required directories to go to items in either roots or dirs
900 /// Implicitly required directories to go to items in either roots or dirs
888 pub parents: HashSet<HgPathBuf>,
901 pub parents: DirsMultiset,
889 }
902 }
890
903
891 /// Extract roots, dirs and parents from patterns.
904 /// Extract roots, dirs and parents from patterns.
892 fn roots_dirs_and_parents(
905 fn roots_dirs_and_parents(
893 ignore_patterns: &[IgnorePattern],
906 ignore_patterns: &[IgnorePattern],
894 ) -> PatternResult<RootsDirsAndParents> {
907 ) -> PatternResult<RootsDirsAndParents> {
895 let (roots, dirs) = roots_and_dirs(ignore_patterns);
908 let (roots, dirs) = roots_and_dirs(ignore_patterns);
896
909
897 let mut parents = HashSet::new();
910 let mut parents = DirsMultiset::from_manifest(&dirs)?;
898
911
899 parents.extend(
912 for path in &roots {
900 DirsMultiset::from_manifest(&dirs)?
913 parents.add_path(path)?
901 .iter()
914 }
902 .map(ToOwned::to_owned),
903 );
904 parents.extend(
905 DirsMultiset::from_manifest(&roots)?
906 .iter()
907 .map(ToOwned::to_owned),
908 );
909
915
910 Ok(RootsDirsAndParents {
916 Ok(RootsDirsAndParents {
911 roots: HashSet::from_iter(roots),
917 roots: HashSet::from_iter(roots),
912 dirs: HashSet::from_iter(dirs),
918 dirs: HashSet::from_iter(dirs),
913 parents,
919 parents,
914 })
920 })
915 }
921 }
916
922
917 /// Returns a function that checks whether a given file (in the general sense)
923 /// Returns a function that checks whether a given file (in the general sense)
918 /// should be matched.
924 /// should be matched.
919 fn build_match<'a>(
925 fn build_match<'a>(
920 ignore_patterns: Vec<IgnorePattern>,
926 ignore_patterns: Vec<IgnorePattern>,
921 glob_suffix: &[u8],
927 glob_suffix: &[u8],
922 ) -> PatternResult<(Vec<u8>, IgnoreFnType<'a>)> {
928 ) -> PatternResult<(Vec<u8>, IgnoreFnType<'a>)> {
923 let mut match_funcs: Vec<IgnoreFnType<'a>> = vec![];
929 let mut match_funcs: Vec<IgnoreFnType<'a>> = vec![];
924 // For debugging and printing
930 // For debugging and printing
925 let mut patterns = vec![];
931 let mut patterns = vec![];
926
932
927 let (subincludes, ignore_patterns) = filter_subincludes(ignore_patterns)?;
933 let (subincludes, ignore_patterns) = filter_subincludes(ignore_patterns)?;
928
934
929 if !subincludes.is_empty() {
935 if !subincludes.is_empty() {
930 // Build prefix-based matcher functions for subincludes
936 // Build prefix-based matcher functions for subincludes
931 let mut submatchers = FastHashMap::default();
937 let mut submatchers = FastHashMap::default();
932 let mut prefixes = vec![];
938 let mut prefixes = vec![];
933
939
934 for sub_include in subincludes {
940 for sub_include in subincludes {
935 let matcher = IncludeMatcher::new(sub_include.included_patterns)?;
941 let matcher = IncludeMatcher::new(sub_include.included_patterns)?;
936 let match_fn =
942 let match_fn =
937 Box::new(move |path: &HgPath| matcher.matches(path));
943 Box::new(move |path: &HgPath| matcher.matches(path));
938 prefixes.push(sub_include.prefix.clone());
944 prefixes.push(sub_include.prefix.clone());
939 submatchers.insert(sub_include.prefix.clone(), match_fn);
945 submatchers.insert(sub_include.prefix.clone(), match_fn);
940 }
946 }
941
947
942 let match_subinclude = move |filename: &HgPath| {
948 let match_subinclude = move |filename: &HgPath| {
943 for prefix in prefixes.iter() {
949 for prefix in prefixes.iter() {
944 if let Some(rel) = filename.relative_to(prefix) {
950 if let Some(rel) = filename.relative_to(prefix) {
945 if (submatchers[prefix])(rel) {
951 if (submatchers[prefix])(rel) {
946 return true;
952 return true;
947 }
953 }
948 }
954 }
949 }
955 }
950 false
956 false
951 };
957 };
952
958
953 match_funcs.push(Box::new(match_subinclude));
959 match_funcs.push(Box::new(match_subinclude));
954 }
960 }
955
961
956 if !ignore_patterns.is_empty() {
962 if !ignore_patterns.is_empty() {
957 // Either do dumb matching if all patterns are rootfiles, or match
963 // Either do dumb matching if all patterns are rootfiles, or match
958 // with a regex.
964 // with a regex.
959 if ignore_patterns
965 if ignore_patterns
960 .iter()
966 .iter()
961 .all(|k| k.syntax == PatternSyntax::RootFiles)
967 .all(|k| k.syntax == PatternSyntax::RootFilesIn)
962 {
968 {
963 let dirs: HashSet<_> = ignore_patterns
969 let dirs: HashSet<_> = ignore_patterns
964 .iter()
970 .iter()
965 .map(|k| k.pattern.to_owned())
971 .map(|k| k.pattern.to_owned())
966 .collect();
972 .collect();
967 let mut dirs_vec: Vec<_> = dirs.iter().cloned().collect();
973 let mut dirs_vec: Vec<_> = dirs.iter().cloned().collect();
968
974
969 let match_func = move |path: &HgPath| -> bool {
975 let match_func = move |path: &HgPath| -> bool {
970 let path = path.as_bytes();
976 let path = path.as_bytes();
971 let i = path.iter().rposition(|a| *a == b'/');
977 let i = path.iter().rposition(|a| *a == b'/');
972 let dir = if let Some(i) = i { &path[..i] } else { b"." };
978 let dir = if let Some(i) = i { &path[..i] } else { b"." };
973 dirs.contains(dir)
979 dirs.contains(dir)
974 };
980 };
975 match_funcs.push(Box::new(match_func));
981 match_funcs.push(Box::new(match_func));
976
982
977 patterns.extend(b"rootfilesin: ");
983 patterns.extend(b"rootfilesin: ");
978 dirs_vec.sort();
984 dirs_vec.sort();
979 patterns.extend(dirs_vec.escaped_bytes());
985 patterns.extend(dirs_vec.escaped_bytes());
980 } else {
986 } else {
981 let (new_re, match_func) =
987 let (new_re, match_func) =
982 build_regex_match(&ignore_patterns, glob_suffix)?;
988 build_regex_match(&ignore_patterns, glob_suffix)?;
983 patterns = new_re;
989 patterns = new_re;
984 match_funcs.push(match_func)
990 match_funcs.push(match_func)
985 }
991 }
986 }
992 }
987
993
988 Ok(if match_funcs.len() == 1 {
994 Ok(if match_funcs.len() == 1 {
989 (patterns, match_funcs.remove(0))
995 (patterns, match_funcs.remove(0))
990 } else {
996 } else {
991 (
997 (
992 patterns,
998 patterns,
993 Box::new(move |f: &HgPath| -> bool {
999 Box::new(move |f: &HgPath| -> bool {
994 match_funcs.iter().any(|match_func| match_func(f))
1000 match_funcs.iter().any(|match_func| match_func(f))
995 }),
1001 }),
996 )
1002 )
997 })
1003 })
998 }
1004 }
999
1005
1000 /// Parses all "ignore" files with their recursive includes and returns a
1006 /// Parses all "ignore" files with their recursive includes and returns a
1001 /// function that checks whether a given file (in the general sense) should be
1007 /// function that checks whether a given file (in the general sense) should be
1002 /// ignored.
1008 /// ignored.
1003 pub fn get_ignore_matcher<'a>(
1009 pub fn get_ignore_matcher<'a>(
1004 mut all_pattern_files: Vec<PathBuf>,
1010 mut all_pattern_files: Vec<PathBuf>,
1005 root_dir: &Path,
1011 root_dir: &Path,
1006 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
1012 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
1007 ) -> PatternResult<(IncludeMatcher<'a>, Vec<PatternFileWarning>)> {
1013 ) -> PatternResult<(IncludeMatcher<'a>, Vec<PatternFileWarning>)> {
1008 let mut all_patterns = vec![];
1014 let mut all_patterns = vec![];
1009 let mut all_warnings = vec![];
1015 let mut all_warnings = vec![];
1010
1016
1011 // Sort to make the ordering of calls to `inspect_pattern_bytes`
1017 // Sort to make the ordering of calls to `inspect_pattern_bytes`
1012 // deterministic even if the ordering of `all_pattern_files` is not (such
1018 // deterministic even if the ordering of `all_pattern_files` is not (such
1013 // as when a iteration order of a Python dict or Rust HashMap is involved).
1019 // as when a iteration order of a Python dict or Rust HashMap is involved).
1014 // Sort by "string" representation instead of the default by component
1020 // Sort by "string" representation instead of the default by component
1015 // (with a Rust-specific definition of a component)
1021 // (with a Rust-specific definition of a component)
1016 all_pattern_files
1022 all_pattern_files
1017 .sort_unstable_by(|a, b| a.as_os_str().cmp(b.as_os_str()));
1023 .sort_unstable_by(|a, b| a.as_os_str().cmp(b.as_os_str()));
1018
1024
1019 for pattern_file in &all_pattern_files {
1025 for pattern_file in &all_pattern_files {
1020 let (patterns, warnings) = get_patterns_from_file(
1026 let (patterns, warnings) = get_patterns_from_file(
1021 pattern_file,
1027 pattern_file,
1022 root_dir,
1028 root_dir,
1023 inspect_pattern_bytes,
1029 inspect_pattern_bytes,
1024 )?;
1030 )?;
1025
1031
1026 all_patterns.extend(patterns.to_owned());
1032 all_patterns.extend(patterns.to_owned());
1027 all_warnings.extend(warnings);
1033 all_warnings.extend(warnings);
1028 }
1034 }
1029 let matcher = IncludeMatcher::new(all_patterns)?;
1035 let matcher = IncludeMatcher::new(all_patterns)?;
1030 Ok((matcher, all_warnings))
1036 Ok((matcher, all_warnings))
1031 }
1037 }
1032
1038
1033 /// Parses all "ignore" files with their recursive includes and returns a
1039 /// Parses all "ignore" files with their recursive includes and returns a
1034 /// function that checks whether a given file (in the general sense) should be
1040 /// function that checks whether a given file (in the general sense) should be
1035 /// ignored.
1041 /// ignored.
1036 pub fn get_ignore_function<'a>(
1042 pub fn get_ignore_function<'a>(
1037 all_pattern_files: Vec<PathBuf>,
1043 all_pattern_files: Vec<PathBuf>,
1038 root_dir: &Path,
1044 root_dir: &Path,
1039 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
1045 inspect_pattern_bytes: &mut impl FnMut(&Path, &[u8]),
1040 ) -> PatternResult<(IgnoreFnType<'a>, Vec<PatternFileWarning>)> {
1046 ) -> PatternResult<(IgnoreFnType<'a>, Vec<PatternFileWarning>)> {
1041 let res =
1047 let res =
1042 get_ignore_matcher(all_pattern_files, root_dir, inspect_pattern_bytes);
1048 get_ignore_matcher(all_pattern_files, root_dir, inspect_pattern_bytes);
1043 res.map(|(matcher, all_warnings)| {
1049 res.map(|(matcher, all_warnings)| {
1044 let res: IgnoreFnType<'a> =
1050 let res: IgnoreFnType<'a> =
1045 Box::new(move |path: &HgPath| matcher.matches(path));
1051 Box::new(move |path: &HgPath| matcher.matches(path));
1046
1052
1047 (res, all_warnings)
1053 (res, all_warnings)
1048 })
1054 })
1049 }
1055 }
1050
1056
1051 impl<'a> IncludeMatcher<'a> {
1057 impl<'a> IncludeMatcher<'a> {
1052 pub fn new(ignore_patterns: Vec<IgnorePattern>) -> PatternResult<Self> {
1058 pub fn new(ignore_patterns: Vec<IgnorePattern>) -> PatternResult<Self> {
1053 let RootsDirsAndParents {
1059 let RootsDirsAndParents {
1054 roots,
1060 roots,
1055 dirs,
1061 dirs,
1056 parents,
1062 parents,
1057 } = roots_dirs_and_parents(&ignore_patterns)?;
1063 } = roots_dirs_and_parents(&ignore_patterns)?;
1058 let prefix = ignore_patterns.iter().all(|k| {
1064 let prefix = ignore_patterns.iter().all(|k| {
1059 matches!(k.syntax, PatternSyntax::Path | PatternSyntax::RelPath)
1065 matches!(k.syntax, PatternSyntax::Path | PatternSyntax::RelPath)
1060 });
1066 });
1061 let (patterns, match_fn) = build_match(ignore_patterns, b"(?:/|$)")?;
1067 let (patterns, match_fn) = build_match(ignore_patterns, b"(?:/|$)")?;
1062
1068
1063 Ok(Self {
1069 Ok(Self {
1064 patterns,
1070 patterns,
1065 match_fn,
1071 match_fn,
1066 prefix,
1072 prefix,
1067 roots,
1073 roots,
1068 dirs,
1074 dirs,
1069 parents,
1075 parents,
1070 })
1076 })
1071 }
1077 }
1072
1078
1073 fn get_all_parents_children(&self) -> DirsChildrenMultiset {
1079 fn get_all_parents_children(&self) -> DirsChildrenMultiset {
1074 // TODO cache
1080 // TODO cache
1075 let thing = self
1081 let thing = self
1076 .dirs
1082 .dirs
1077 .iter()
1083 .iter()
1078 .chain(self.roots.iter())
1084 .chain(self.roots.iter())
1079 .chain(self.parents.iter());
1085 .chain(self.parents.iter());
1080 DirsChildrenMultiset::new(thing, Some(&self.parents))
1086 DirsChildrenMultiset::new(thing, Some(self.parents.iter()))
1081 }
1087 }
1082
1088
1083 pub fn debug_get_patterns(&self) -> &[u8] {
1089 pub fn debug_get_patterns(&self) -> &[u8] {
1084 self.patterns.as_ref()
1090 self.patterns.as_ref()
1085 }
1091 }
1086 }
1092 }
1087
1093
1088 impl<'a> Display for IncludeMatcher<'a> {
1094 impl<'a> Display for IncludeMatcher<'a> {
1089 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
1095 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> {
1090 // XXX What about exact matches?
1096 // XXX What about exact matches?
1091 // I'm not sure it's worth it to clone the HashSet and keep it
1097 // I'm not sure it's worth it to clone the HashSet and keep it
1092 // around just in case someone wants to display the matcher, plus
1098 // around just in case someone wants to display the matcher, plus
1093 // it's going to be unreadable after a few entries, but we need to
1099 // it's going to be unreadable after a few entries, but we need to
1094 // inform in this display that exact matches are being used and are
1100 // inform in this display that exact matches are being used and are
1095 // (on purpose) missing from the `includes`.
1101 // (on purpose) missing from the `includes`.
1096 write!(
1102 write!(
1097 f,
1103 f,
1098 "IncludeMatcher(includes='{}')",
1104 "IncludeMatcher(includes='{}')",
1099 String::from_utf8_lossy(&self.patterns.escaped_bytes())
1105 String::from_utf8_lossy(&self.patterns.escaped_bytes())
1100 )
1106 )
1101 }
1107 }
1102 }
1108 }
1103
1109
1104 #[cfg(test)]
1110 #[cfg(test)]
1105 mod tests {
1111 mod tests {
1106 use super::*;
1112 use super::*;
1107 use pretty_assertions::assert_eq;
1113 use pretty_assertions::assert_eq;
1114 use std::collections::BTreeMap;
1115 use std::collections::BTreeSet;
1116 use std::fmt::Debug;
1108 use std::path::Path;
1117 use std::path::Path;
1109
1118
1110 #[test]
1119 #[test]
1111 fn test_roots_and_dirs() {
1120 fn test_roots_and_dirs() {
1112 let pats = vec![
1121 let pats = vec![
1113 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
1122 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
1114 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
1123 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
1115 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
1124 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
1116 ];
1125 ];
1117 let (roots, dirs) = roots_and_dirs(&pats);
1126 let (roots, dirs) = roots_and_dirs(&pats);
1118
1127
1119 assert_eq!(
1128 assert_eq!(
1120 roots,
1129 roots,
1121 vec!(
1130 vec!(
1122 HgPathBuf::from_bytes(b"g/h"),
1131 HgPathBuf::from_bytes(b"g/h"),
1123 HgPathBuf::from_bytes(b"g/h"),
1132 HgPathBuf::from_bytes(b"g/h"),
1124 HgPathBuf::new()
1133 HgPathBuf::new()
1125 ),
1134 ),
1126 );
1135 );
1127 assert_eq!(dirs, vec!());
1136 assert_eq!(dirs, vec!());
1128 }
1137 }
1129
1138
1130 #[test]
1139 #[test]
1131 fn test_roots_dirs_and_parents() {
1140 fn test_roots_dirs_and_parents() {
1132 let pats = vec![
1141 let pats = vec![
1133 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
1142 IgnorePattern::new(PatternSyntax::Glob, b"g/h/*", Path::new("")),
1134 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
1143 IgnorePattern::new(PatternSyntax::Glob, b"g/h", Path::new("")),
1135 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
1144 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
1136 ];
1145 ];
1137
1146
1138 let mut roots = HashSet::new();
1147 let mut roots = HashSet::new();
1139 roots.insert(HgPathBuf::from_bytes(b"g/h"));
1148 roots.insert(HgPathBuf::from_bytes(b"g/h"));
1140 roots.insert(HgPathBuf::new());
1149 roots.insert(HgPathBuf::new());
1141
1150
1142 let dirs = HashSet::new();
1151 let dirs = HashSet::new();
1143
1152
1144 let mut parents = HashSet::new();
1153 let parents = DirsMultiset::from_manifest(&[
1145 parents.insert(HgPathBuf::new());
1154 HgPathBuf::from_bytes(b"x"),
1146 parents.insert(HgPathBuf::from_bytes(b"g"));
1155 HgPathBuf::from_bytes(b"g/x"),
1156 HgPathBuf::from_bytes(b"g/y"),
1157 ])
1158 .unwrap();
1147
1159
1148 assert_eq!(
1160 assert_eq!(
1149 roots_dirs_and_parents(&pats).unwrap(),
1161 roots_dirs_and_parents(&pats).unwrap(),
1150 RootsDirsAndParents {
1162 RootsDirsAndParents {
1151 roots,
1163 roots,
1152 dirs,
1164 dirs,
1153 parents
1165 parents
1154 }
1166 }
1155 );
1167 );
1156 }
1168 }
1157
1169
1158 #[test]
1170 #[test]
1159 fn test_filematcher_visit_children_set() {
1171 fn test_filematcher_visit_children_set() {
1160 // Visitchildrenset
1172 // Visitchildrenset
1161 let files = vec![HgPathBuf::from_bytes(b"dir/subdir/foo.txt")];
1173 let files = vec![HgPathBuf::from_bytes(b"dir/subdir/foo.txt")];
1162 let matcher = FileMatcher::new(files).unwrap();
1174 let matcher = FileMatcher::new(files).unwrap();
1163
1175
1164 let mut set = HashSet::new();
1176 let mut set = HashSet::new();
1165 set.insert(HgPathBuf::from_bytes(b"dir"));
1177 set.insert(HgPathBuf::from_bytes(b"dir"));
1166 assert_eq!(
1178 assert_eq!(
1167 matcher.visit_children_set(HgPath::new(b"")),
1179 matcher.visit_children_set(HgPath::new(b"")),
1168 VisitChildrenSet::Set(set)
1180 VisitChildrenSet::Set(set)
1169 );
1181 );
1170
1182
1171 let mut set = HashSet::new();
1183 let mut set = HashSet::new();
1172 set.insert(HgPathBuf::from_bytes(b"subdir"));
1184 set.insert(HgPathBuf::from_bytes(b"subdir"));
1173 assert_eq!(
1185 assert_eq!(
1174 matcher.visit_children_set(HgPath::new(b"dir")),
1186 matcher.visit_children_set(HgPath::new(b"dir")),
1175 VisitChildrenSet::Set(set)
1187 VisitChildrenSet::Set(set)
1176 );
1188 );
1177
1189
1178 let mut set = HashSet::new();
1190 let mut set = HashSet::new();
1179 set.insert(HgPathBuf::from_bytes(b"foo.txt"));
1191 set.insert(HgPathBuf::from_bytes(b"foo.txt"));
1180 assert_eq!(
1192 assert_eq!(
1181 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1193 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1182 VisitChildrenSet::Set(set)
1194 VisitChildrenSet::Set(set)
1183 );
1195 );
1184
1196
1185 assert_eq!(
1197 assert_eq!(
1186 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1198 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1187 VisitChildrenSet::Empty
1199 VisitChildrenSet::Empty
1188 );
1200 );
1189 assert_eq!(
1201 assert_eq!(
1190 matcher.visit_children_set(HgPath::new(b"dir/subdir/foo.txt")),
1202 matcher.visit_children_set(HgPath::new(b"dir/subdir/foo.txt")),
1191 VisitChildrenSet::Empty
1203 VisitChildrenSet::Empty
1192 );
1204 );
1193 assert_eq!(
1205 assert_eq!(
1194 matcher.visit_children_set(HgPath::new(b"folder")),
1206 matcher.visit_children_set(HgPath::new(b"folder")),
1195 VisitChildrenSet::Empty
1207 VisitChildrenSet::Empty
1196 );
1208 );
1197 }
1209 }
1198
1210
1199 #[test]
1211 #[test]
1200 fn test_filematcher_visit_children_set_files_and_dirs() {
1212 fn test_filematcher_visit_children_set_files_and_dirs() {
1201 let files = vec![
1213 let files = vec![
1202 HgPathBuf::from_bytes(b"rootfile.txt"),
1214 HgPathBuf::from_bytes(b"rootfile.txt"),
1203 HgPathBuf::from_bytes(b"a/file1.txt"),
1215 HgPathBuf::from_bytes(b"a/file1.txt"),
1204 HgPathBuf::from_bytes(b"a/b/file2.txt"),
1216 HgPathBuf::from_bytes(b"a/b/file2.txt"),
1205 // No file in a/b/c
1217 // No file in a/b/c
1206 HgPathBuf::from_bytes(b"a/b/c/d/file4.txt"),
1218 HgPathBuf::from_bytes(b"a/b/c/d/file4.txt"),
1207 ];
1219 ];
1208 let matcher = FileMatcher::new(files).unwrap();
1220 let matcher = FileMatcher::new(files).unwrap();
1209
1221
1210 let mut set = HashSet::new();
1222 let mut set = HashSet::new();
1211 set.insert(HgPathBuf::from_bytes(b"a"));
1223 set.insert(HgPathBuf::from_bytes(b"a"));
1212 set.insert(HgPathBuf::from_bytes(b"rootfile.txt"));
1224 set.insert(HgPathBuf::from_bytes(b"rootfile.txt"));
1213 assert_eq!(
1225 assert_eq!(
1214 matcher.visit_children_set(HgPath::new(b"")),
1226 matcher.visit_children_set(HgPath::new(b"")),
1215 VisitChildrenSet::Set(set)
1227 VisitChildrenSet::Set(set)
1216 );
1228 );
1217
1229
1218 let mut set = HashSet::new();
1230 let mut set = HashSet::new();
1219 set.insert(HgPathBuf::from_bytes(b"b"));
1231 set.insert(HgPathBuf::from_bytes(b"b"));
1220 set.insert(HgPathBuf::from_bytes(b"file1.txt"));
1232 set.insert(HgPathBuf::from_bytes(b"file1.txt"));
1221 assert_eq!(
1233 assert_eq!(
1222 matcher.visit_children_set(HgPath::new(b"a")),
1234 matcher.visit_children_set(HgPath::new(b"a")),
1223 VisitChildrenSet::Set(set)
1235 VisitChildrenSet::Set(set)
1224 );
1236 );
1225
1237
1226 let mut set = HashSet::new();
1238 let mut set = HashSet::new();
1227 set.insert(HgPathBuf::from_bytes(b"c"));
1239 set.insert(HgPathBuf::from_bytes(b"c"));
1228 set.insert(HgPathBuf::from_bytes(b"file2.txt"));
1240 set.insert(HgPathBuf::from_bytes(b"file2.txt"));
1229 assert_eq!(
1241 assert_eq!(
1230 matcher.visit_children_set(HgPath::new(b"a/b")),
1242 matcher.visit_children_set(HgPath::new(b"a/b")),
1231 VisitChildrenSet::Set(set)
1243 VisitChildrenSet::Set(set)
1232 );
1244 );
1233
1245
1234 let mut set = HashSet::new();
1246 let mut set = HashSet::new();
1235 set.insert(HgPathBuf::from_bytes(b"d"));
1247 set.insert(HgPathBuf::from_bytes(b"d"));
1236 assert_eq!(
1248 assert_eq!(
1237 matcher.visit_children_set(HgPath::new(b"a/b/c")),
1249 matcher.visit_children_set(HgPath::new(b"a/b/c")),
1238 VisitChildrenSet::Set(set)
1250 VisitChildrenSet::Set(set)
1239 );
1251 );
1240 let mut set = HashSet::new();
1252 let mut set = HashSet::new();
1241 set.insert(HgPathBuf::from_bytes(b"file4.txt"));
1253 set.insert(HgPathBuf::from_bytes(b"file4.txt"));
1242 assert_eq!(
1254 assert_eq!(
1243 matcher.visit_children_set(HgPath::new(b"a/b/c/d")),
1255 matcher.visit_children_set(HgPath::new(b"a/b/c/d")),
1244 VisitChildrenSet::Set(set)
1256 VisitChildrenSet::Set(set)
1245 );
1257 );
1246
1258
1247 assert_eq!(
1259 assert_eq!(
1248 matcher.visit_children_set(HgPath::new(b"a/b/c/d/e")),
1260 matcher.visit_children_set(HgPath::new(b"a/b/c/d/e")),
1249 VisitChildrenSet::Empty
1261 VisitChildrenSet::Empty
1250 );
1262 );
1251 assert_eq!(
1263 assert_eq!(
1252 matcher.visit_children_set(HgPath::new(b"folder")),
1264 matcher.visit_children_set(HgPath::new(b"folder")),
1253 VisitChildrenSet::Empty
1265 VisitChildrenSet::Empty
1254 );
1266 );
1255 }
1267 }
1256
1268
1257 #[test]
1269 #[test]
1258 fn test_patternmatcher() {
1270 fn test_patternmatcher() {
1259 // VisitdirPrefix
1271 // VisitdirPrefix
1260 let m = PatternMatcher::new(vec![IgnorePattern::new(
1272 let m = PatternMatcher::new(vec![IgnorePattern::new(
1261 PatternSyntax::Path,
1273 PatternSyntax::Path,
1262 b"dir/subdir",
1274 b"dir/subdir",
1263 Path::new(""),
1275 Path::new(""),
1264 )])
1276 )])
1265 .unwrap();
1277 .unwrap();
1266 assert_eq!(
1278 assert_eq!(
1267 m.visit_children_set(HgPath::new(b"")),
1279 m.visit_children_set(HgPath::new(b"")),
1268 VisitChildrenSet::This
1280 VisitChildrenSet::This
1269 );
1281 );
1270 assert_eq!(
1282 assert_eq!(
1271 m.visit_children_set(HgPath::new(b"dir")),
1283 m.visit_children_set(HgPath::new(b"dir")),
1272 VisitChildrenSet::This
1284 VisitChildrenSet::This
1273 );
1285 );
1274 assert_eq!(
1286 assert_eq!(
1275 m.visit_children_set(HgPath::new(b"dir/subdir")),
1287 m.visit_children_set(HgPath::new(b"dir/subdir")),
1276 VisitChildrenSet::Recursive
1288 VisitChildrenSet::Recursive
1277 );
1289 );
1278 // OPT: This should probably be Recursive if its parent is?
1290 // OPT: This should probably be Recursive if its parent is?
1279 assert_eq!(
1291 assert_eq!(
1280 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1292 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1281 VisitChildrenSet::This
1293 VisitChildrenSet::This
1282 );
1294 );
1283 assert_eq!(
1295 assert_eq!(
1284 m.visit_children_set(HgPath::new(b"folder")),
1296 m.visit_children_set(HgPath::new(b"folder")),
1285 VisitChildrenSet::Empty
1297 VisitChildrenSet::Empty
1286 );
1298 );
1287
1299
1288 // VisitchildrensetPrefix
1300 // VisitchildrensetPrefix
1289 let m = PatternMatcher::new(vec![IgnorePattern::new(
1301 let m = PatternMatcher::new(vec![IgnorePattern::new(
1290 PatternSyntax::Path,
1302 PatternSyntax::Path,
1291 b"dir/subdir",
1303 b"dir/subdir",
1292 Path::new(""),
1304 Path::new(""),
1293 )])
1305 )])
1294 .unwrap();
1306 .unwrap();
1295 assert_eq!(
1307 assert_eq!(
1296 m.visit_children_set(HgPath::new(b"")),
1308 m.visit_children_set(HgPath::new(b"")),
1297 VisitChildrenSet::This
1309 VisitChildrenSet::This
1298 );
1310 );
1299 assert_eq!(
1311 assert_eq!(
1300 m.visit_children_set(HgPath::new(b"dir")),
1312 m.visit_children_set(HgPath::new(b"dir")),
1301 VisitChildrenSet::This
1313 VisitChildrenSet::This
1302 );
1314 );
1303 assert_eq!(
1315 assert_eq!(
1304 m.visit_children_set(HgPath::new(b"dir/subdir")),
1316 m.visit_children_set(HgPath::new(b"dir/subdir")),
1305 VisitChildrenSet::Recursive
1317 VisitChildrenSet::Recursive
1306 );
1318 );
1307 // OPT: This should probably be Recursive if its parent is?
1319 // OPT: This should probably be Recursive if its parent is?
1308 assert_eq!(
1320 assert_eq!(
1309 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1321 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1310 VisitChildrenSet::This
1322 VisitChildrenSet::This
1311 );
1323 );
1312 assert_eq!(
1324 assert_eq!(
1313 m.visit_children_set(HgPath::new(b"folder")),
1325 m.visit_children_set(HgPath::new(b"folder")),
1314 VisitChildrenSet::Empty
1326 VisitChildrenSet::Empty
1315 );
1327 );
1316
1328
1317 // VisitdirRootfilesin
1329 // VisitdirRootfilesin
1318 let m = PatternMatcher::new(vec![IgnorePattern::new(
1330 let m = PatternMatcher::new(vec![IgnorePattern::new(
1319 PatternSyntax::RootFiles,
1331 PatternSyntax::RootFilesIn,
1320 b"dir/subdir",
1332 b"dir/subdir",
1321 Path::new(""),
1333 Path::new(""),
1322 )])
1334 )])
1323 .unwrap();
1335 .unwrap();
1324 assert_eq!(
1336 assert_eq!(
1325 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1337 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1326 VisitChildrenSet::Empty
1338 VisitChildrenSet::This
1327 );
1339 );
1328 assert_eq!(
1340 assert_eq!(
1329 m.visit_children_set(HgPath::new(b"folder")),
1341 m.visit_children_set(HgPath::new(b"folder")),
1330 VisitChildrenSet::Empty
1342 VisitChildrenSet::Empty
1331 );
1343 );
1332 // FIXME: These should probably be This.
1333 assert_eq!(
1344 assert_eq!(
1334 m.visit_children_set(HgPath::new(b"")),
1345 m.visit_children_set(HgPath::new(b"")),
1335 VisitChildrenSet::Empty
1346 VisitChildrenSet::This
1336 );
1347 );
1337 assert_eq!(
1348 assert_eq!(
1338 m.visit_children_set(HgPath::new(b"dir")),
1349 m.visit_children_set(HgPath::new(b"dir")),
1339 VisitChildrenSet::Empty
1350 VisitChildrenSet::This
1340 );
1351 );
1341 assert_eq!(
1352 assert_eq!(
1342 m.visit_children_set(HgPath::new(b"dir/subdir")),
1353 m.visit_children_set(HgPath::new(b"dir/subdir")),
1343 VisitChildrenSet::Empty
1354 VisitChildrenSet::This
1344 );
1355 );
1345
1356
1346 // VisitchildrensetRootfilesin
1357 // VisitchildrensetRootfilesin
1347 let m = PatternMatcher::new(vec![IgnorePattern::new(
1358 let m = PatternMatcher::new(vec![IgnorePattern::new(
1348 PatternSyntax::RootFiles,
1359 PatternSyntax::RootFilesIn,
1349 b"dir/subdir",
1360 b"dir/subdir",
1350 Path::new(""),
1361 Path::new(""),
1351 )])
1362 )])
1352 .unwrap();
1363 .unwrap();
1353 assert_eq!(
1364 assert_eq!(
1354 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1365 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1355 VisitChildrenSet::Empty
1366 VisitChildrenSet::This
1356 );
1367 );
1357 assert_eq!(
1368 assert_eq!(
1358 m.visit_children_set(HgPath::new(b"folder")),
1369 m.visit_children_set(HgPath::new(b"folder")),
1359 VisitChildrenSet::Empty
1370 VisitChildrenSet::Empty
1360 );
1371 );
1361 // FIXME: These should probably be {'dir'}, {'subdir'} and This,
1372 // FIXME: These should probably be {'dir'}, {'subdir'} and This,
1362 // respectively, or at least This for all three.
1373 // respectively
1363 assert_eq!(
1374 assert_eq!(
1364 m.visit_children_set(HgPath::new(b"")),
1375 m.visit_children_set(HgPath::new(b"")),
1365 VisitChildrenSet::Empty
1376 VisitChildrenSet::This
1366 );
1377 );
1367 assert_eq!(
1378 assert_eq!(
1368 m.visit_children_set(HgPath::new(b"dir")),
1379 m.visit_children_set(HgPath::new(b"dir")),
1369 VisitChildrenSet::Empty
1380 VisitChildrenSet::This
1370 );
1381 );
1371 assert_eq!(
1382 assert_eq!(
1372 m.visit_children_set(HgPath::new(b"dir/subdir")),
1383 m.visit_children_set(HgPath::new(b"dir/subdir")),
1373 VisitChildrenSet::Empty
1384 VisitChildrenSet::This
1374 );
1385 );
1375
1386
1376 // VisitdirGlob
1387 // VisitdirGlob
1377 let m = PatternMatcher::new(vec![IgnorePattern::new(
1388 let m = PatternMatcher::new(vec![IgnorePattern::new(
1378 PatternSyntax::Glob,
1389 PatternSyntax::Glob,
1379 b"dir/z*",
1390 b"dir/z*",
1380 Path::new(""),
1391 Path::new(""),
1381 )])
1392 )])
1382 .unwrap();
1393 .unwrap();
1383 assert_eq!(
1394 assert_eq!(
1384 m.visit_children_set(HgPath::new(b"")),
1395 m.visit_children_set(HgPath::new(b"")),
1385 VisitChildrenSet::This
1396 VisitChildrenSet::This
1386 );
1397 );
1387 // FIXME: This probably should be This
1388 assert_eq!(
1398 assert_eq!(
1389 m.visit_children_set(HgPath::new(b"dir")),
1399 m.visit_children_set(HgPath::new(b"dir")),
1390 VisitChildrenSet::Empty
1400 VisitChildrenSet::This
1391 );
1401 );
1392 assert_eq!(
1402 assert_eq!(
1393 m.visit_children_set(HgPath::new(b"folder")),
1403 m.visit_children_set(HgPath::new(b"folder")),
1394 VisitChildrenSet::Empty
1404 VisitChildrenSet::Empty
1395 );
1405 );
1396 // OPT: these should probably be False.
1406 // OPT: these should probably be False.
1397 assert_eq!(
1407 assert_eq!(
1398 m.visit_children_set(HgPath::new(b"dir/subdir")),
1408 m.visit_children_set(HgPath::new(b"dir/subdir")),
1399 VisitChildrenSet::This
1409 VisitChildrenSet::This
1400 );
1410 );
1401 assert_eq!(
1411 assert_eq!(
1402 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1412 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1403 VisitChildrenSet::This
1413 VisitChildrenSet::This
1404 );
1414 );
1405
1415
1406 // VisitchildrensetGlob
1416 // VisitchildrensetGlob
1407 let m = PatternMatcher::new(vec![IgnorePattern::new(
1417 let m = PatternMatcher::new(vec![IgnorePattern::new(
1408 PatternSyntax::Glob,
1418 PatternSyntax::Glob,
1409 b"dir/z*",
1419 b"dir/z*",
1410 Path::new(""),
1420 Path::new(""),
1411 )])
1421 )])
1412 .unwrap();
1422 .unwrap();
1413 assert_eq!(
1423 assert_eq!(
1414 m.visit_children_set(HgPath::new(b"")),
1424 m.visit_children_set(HgPath::new(b"")),
1415 VisitChildrenSet::This
1425 VisitChildrenSet::This
1416 );
1426 );
1417 assert_eq!(
1427 assert_eq!(
1418 m.visit_children_set(HgPath::new(b"folder")),
1428 m.visit_children_set(HgPath::new(b"folder")),
1419 VisitChildrenSet::Empty
1429 VisitChildrenSet::Empty
1420 );
1430 );
1421 // FIXME: This probably should be This
1422 assert_eq!(
1431 assert_eq!(
1423 m.visit_children_set(HgPath::new(b"dir")),
1432 m.visit_children_set(HgPath::new(b"dir")),
1424 VisitChildrenSet::Empty
1433 VisitChildrenSet::This
1425 );
1434 );
1426 // OPT: these should probably be Empty
1435 // OPT: these should probably be Empty
1427 assert_eq!(
1436 assert_eq!(
1428 m.visit_children_set(HgPath::new(b"dir/subdir")),
1437 m.visit_children_set(HgPath::new(b"dir/subdir")),
1429 VisitChildrenSet::This
1438 VisitChildrenSet::This
1430 );
1439 );
1431 assert_eq!(
1440 assert_eq!(
1432 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1441 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1433 VisitChildrenSet::This
1442 VisitChildrenSet::This
1434 );
1443 );
1435
1444
1436 // VisitdirFilepath
1445 // VisitdirFilepath
1437 let m = PatternMatcher::new(vec![IgnorePattern::new(
1446 let m = PatternMatcher::new(vec![IgnorePattern::new(
1438 PatternSyntax::FilePath,
1447 PatternSyntax::FilePath,
1439 b"dir/z",
1448 b"dir/z",
1440 Path::new(""),
1449 Path::new(""),
1441 )])
1450 )])
1442 .unwrap();
1451 .unwrap();
1443 assert_eq!(
1452 assert_eq!(
1444 m.visit_children_set(HgPath::new(b"")),
1453 m.visit_children_set(HgPath::new(b"")),
1445 VisitChildrenSet::This
1454 VisitChildrenSet::This
1446 );
1455 );
1447 assert_eq!(
1456 assert_eq!(
1448 m.visit_children_set(HgPath::new(b"dir")),
1457 m.visit_children_set(HgPath::new(b"dir")),
1449 VisitChildrenSet::This
1458 VisitChildrenSet::This
1450 );
1459 );
1451 assert_eq!(
1460 assert_eq!(
1452 m.visit_children_set(HgPath::new(b"folder")),
1461 m.visit_children_set(HgPath::new(b"folder")),
1453 VisitChildrenSet::Empty
1462 VisitChildrenSet::Empty
1454 );
1463 );
1455 assert_eq!(
1464 assert_eq!(
1456 m.visit_children_set(HgPath::new(b"dir/subdir")),
1465 m.visit_children_set(HgPath::new(b"dir/subdir")),
1457 VisitChildrenSet::Empty
1466 VisitChildrenSet::Empty
1458 );
1467 );
1459 assert_eq!(
1468 assert_eq!(
1460 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1469 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1461 VisitChildrenSet::Empty
1470 VisitChildrenSet::Empty
1462 );
1471 );
1463
1472
1464 // VisitchildrensetFilepath
1473 // VisitchildrensetFilepath
1465 let m = PatternMatcher::new(vec![IgnorePattern::new(
1474 let m = PatternMatcher::new(vec![IgnorePattern::new(
1466 PatternSyntax::FilePath,
1475 PatternSyntax::FilePath,
1467 b"dir/z",
1476 b"dir/z",
1468 Path::new(""),
1477 Path::new(""),
1469 )])
1478 )])
1470 .unwrap();
1479 .unwrap();
1471 assert_eq!(
1480 assert_eq!(
1472 m.visit_children_set(HgPath::new(b"")),
1481 m.visit_children_set(HgPath::new(b"")),
1473 VisitChildrenSet::This
1482 VisitChildrenSet::This
1474 );
1483 );
1475 assert_eq!(
1484 assert_eq!(
1476 m.visit_children_set(HgPath::new(b"folder")),
1485 m.visit_children_set(HgPath::new(b"folder")),
1477 VisitChildrenSet::Empty
1486 VisitChildrenSet::Empty
1478 );
1487 );
1479 assert_eq!(
1488 assert_eq!(
1480 m.visit_children_set(HgPath::new(b"dir")),
1489 m.visit_children_set(HgPath::new(b"dir")),
1481 VisitChildrenSet::This
1490 VisitChildrenSet::This
1482 );
1491 );
1483 assert_eq!(
1492 assert_eq!(
1484 m.visit_children_set(HgPath::new(b"dir/subdir")),
1493 m.visit_children_set(HgPath::new(b"dir/subdir")),
1485 VisitChildrenSet::Empty
1494 VisitChildrenSet::Empty
1486 );
1495 );
1487 assert_eq!(
1496 assert_eq!(
1488 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1497 m.visit_children_set(HgPath::new(b"dir/subdir/x")),
1489 VisitChildrenSet::Empty
1498 VisitChildrenSet::Empty
1490 );
1499 );
1491 }
1500 }
1492
1501
1493 #[test]
1502 #[test]
1494 fn test_includematcher() {
1503 fn test_includematcher() {
1495 // VisitchildrensetPrefix
1504 // VisitchildrensetPrefix
1496 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1505 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1497 PatternSyntax::RelPath,
1506 PatternSyntax::RelPath,
1498 b"dir/subdir",
1507 b"dir/subdir",
1499 Path::new(""),
1508 Path::new(""),
1500 )])
1509 )])
1501 .unwrap();
1510 .unwrap();
1502
1511
1503 let mut set = HashSet::new();
1512 let mut set = HashSet::new();
1504 set.insert(HgPathBuf::from_bytes(b"dir"));
1513 set.insert(HgPathBuf::from_bytes(b"dir"));
1505 assert_eq!(
1514 assert_eq!(
1506 matcher.visit_children_set(HgPath::new(b"")),
1515 matcher.visit_children_set(HgPath::new(b"")),
1507 VisitChildrenSet::Set(set)
1516 VisitChildrenSet::Set(set)
1508 );
1517 );
1509
1518
1510 let mut set = HashSet::new();
1519 let mut set = HashSet::new();
1511 set.insert(HgPathBuf::from_bytes(b"subdir"));
1520 set.insert(HgPathBuf::from_bytes(b"subdir"));
1512 assert_eq!(
1521 assert_eq!(
1513 matcher.visit_children_set(HgPath::new(b"dir")),
1522 matcher.visit_children_set(HgPath::new(b"dir")),
1514 VisitChildrenSet::Set(set)
1523 VisitChildrenSet::Set(set)
1515 );
1524 );
1516 assert_eq!(
1525 assert_eq!(
1517 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1526 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1518 VisitChildrenSet::Recursive
1527 VisitChildrenSet::Recursive
1519 );
1528 );
1520 // OPT: This should probably be 'all' if its parent is?
1529 // OPT: This should probably be 'all' if its parent is?
1521 assert_eq!(
1530 assert_eq!(
1522 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1531 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1523 VisitChildrenSet::This
1532 VisitChildrenSet::This
1524 );
1533 );
1525 assert_eq!(
1534 assert_eq!(
1526 matcher.visit_children_set(HgPath::new(b"folder")),
1535 matcher.visit_children_set(HgPath::new(b"folder")),
1527 VisitChildrenSet::Empty
1536 VisitChildrenSet::Empty
1528 );
1537 );
1529
1538
1530 // VisitchildrensetRootfilesin
1539 // VisitchildrensetRootfilesin
1531 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1540 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1532 PatternSyntax::RootFiles,
1541 PatternSyntax::RootFilesIn,
1533 b"dir/subdir",
1542 b"dir/subdir",
1534 Path::new(""),
1543 Path::new(""),
1535 )])
1544 )])
1536 .unwrap();
1545 .unwrap();
1537
1546
1538 let mut set = HashSet::new();
1547 let mut set = HashSet::new();
1539 set.insert(HgPathBuf::from_bytes(b"dir"));
1548 set.insert(HgPathBuf::from_bytes(b"dir"));
1540 assert_eq!(
1549 assert_eq!(
1541 matcher.visit_children_set(HgPath::new(b"")),
1550 matcher.visit_children_set(HgPath::new(b"")),
1542 VisitChildrenSet::Set(set)
1551 VisitChildrenSet::Set(set)
1543 );
1552 );
1544
1553
1545 let mut set = HashSet::new();
1554 let mut set = HashSet::new();
1546 set.insert(HgPathBuf::from_bytes(b"subdir"));
1555 set.insert(HgPathBuf::from_bytes(b"subdir"));
1547 assert_eq!(
1556 assert_eq!(
1548 matcher.visit_children_set(HgPath::new(b"dir")),
1557 matcher.visit_children_set(HgPath::new(b"dir")),
1549 VisitChildrenSet::Set(set)
1558 VisitChildrenSet::Set(set)
1550 );
1559 );
1551
1560
1552 assert_eq!(
1561 assert_eq!(
1553 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1562 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1554 VisitChildrenSet::This
1563 VisitChildrenSet::This
1555 );
1564 );
1556 assert_eq!(
1565 assert_eq!(
1557 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1566 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1558 VisitChildrenSet::Empty
1567 VisitChildrenSet::Empty
1559 );
1568 );
1560 assert_eq!(
1569 assert_eq!(
1561 matcher.visit_children_set(HgPath::new(b"folder")),
1570 matcher.visit_children_set(HgPath::new(b"folder")),
1562 VisitChildrenSet::Empty
1571 VisitChildrenSet::Empty
1563 );
1572 );
1564
1573
1565 // VisitchildrensetGlob
1574 // VisitchildrensetGlob
1566 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1575 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1567 PatternSyntax::Glob,
1576 PatternSyntax::Glob,
1568 b"dir/z*",
1577 b"dir/z*",
1569 Path::new(""),
1578 Path::new(""),
1570 )])
1579 )])
1571 .unwrap();
1580 .unwrap();
1572
1581
1573 let mut set = HashSet::new();
1582 let mut set = HashSet::new();
1574 set.insert(HgPathBuf::from_bytes(b"dir"));
1583 set.insert(HgPathBuf::from_bytes(b"dir"));
1575 assert_eq!(
1584 assert_eq!(
1576 matcher.visit_children_set(HgPath::new(b"")),
1585 matcher.visit_children_set(HgPath::new(b"")),
1577 VisitChildrenSet::Set(set)
1586 VisitChildrenSet::Set(set)
1578 );
1587 );
1579 assert_eq!(
1588 assert_eq!(
1580 matcher.visit_children_set(HgPath::new(b"folder")),
1589 matcher.visit_children_set(HgPath::new(b"folder")),
1581 VisitChildrenSet::Empty
1590 VisitChildrenSet::Empty
1582 );
1591 );
1583 assert_eq!(
1592 assert_eq!(
1584 matcher.visit_children_set(HgPath::new(b"dir")),
1593 matcher.visit_children_set(HgPath::new(b"dir")),
1585 VisitChildrenSet::This
1594 VisitChildrenSet::This
1586 );
1595 );
1587 // OPT: these should probably be set().
1596 // OPT: these should probably be set().
1588 assert_eq!(
1597 assert_eq!(
1589 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1598 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1590 VisitChildrenSet::This
1599 VisitChildrenSet::This
1591 );
1600 );
1592 assert_eq!(
1601 assert_eq!(
1593 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1602 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1594 VisitChildrenSet::This
1603 VisitChildrenSet::This
1595 );
1604 );
1596
1605
1597 // VisitchildrensetFilePath
1606 // VisitchildrensetFilePath
1598 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1607 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1599 PatternSyntax::FilePath,
1608 PatternSyntax::FilePath,
1600 b"dir/z",
1609 b"dir/z",
1601 Path::new(""),
1610 Path::new(""),
1602 )])
1611 )])
1603 .unwrap();
1612 .unwrap();
1604
1613
1605 let mut set = HashSet::new();
1614 let mut set = HashSet::new();
1606 set.insert(HgPathBuf::from_bytes(b"dir"));
1615 set.insert(HgPathBuf::from_bytes(b"dir"));
1607 assert_eq!(
1616 assert_eq!(
1608 matcher.visit_children_set(HgPath::new(b"")),
1617 matcher.visit_children_set(HgPath::new(b"")),
1609 VisitChildrenSet::Set(set)
1618 VisitChildrenSet::Set(set)
1610 );
1619 );
1611 assert_eq!(
1620 assert_eq!(
1612 matcher.visit_children_set(HgPath::new(b"folder")),
1621 matcher.visit_children_set(HgPath::new(b"folder")),
1613 VisitChildrenSet::Empty
1622 VisitChildrenSet::Empty
1614 );
1623 );
1615 let mut set = HashSet::new();
1624 let mut set = HashSet::new();
1616 set.insert(HgPathBuf::from_bytes(b"z"));
1625 set.insert(HgPathBuf::from_bytes(b"z"));
1617 assert_eq!(
1626 assert_eq!(
1618 matcher.visit_children_set(HgPath::new(b"dir")),
1627 matcher.visit_children_set(HgPath::new(b"dir")),
1619 VisitChildrenSet::Set(set)
1628 VisitChildrenSet::Set(set)
1620 );
1629 );
1621 // OPT: these should probably be set().
1630 // OPT: these should probably be set().
1622 assert_eq!(
1631 assert_eq!(
1623 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1632 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1624 VisitChildrenSet::Empty
1633 VisitChildrenSet::Empty
1625 );
1634 );
1626 assert_eq!(
1635 assert_eq!(
1627 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1636 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1628 VisitChildrenSet::Empty
1637 VisitChildrenSet::Empty
1629 );
1638 );
1630
1639
1631 // Test multiple patterns
1640 // Test multiple patterns
1632 let matcher = IncludeMatcher::new(vec![
1641 let matcher = IncludeMatcher::new(vec![
1633 IgnorePattern::new(PatternSyntax::RelPath, b"foo", Path::new("")),
1642 IgnorePattern::new(PatternSyntax::RelPath, b"foo", Path::new("")),
1634 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
1643 IgnorePattern::new(PatternSyntax::Glob, b"g*", Path::new("")),
1635 ])
1644 ])
1636 .unwrap();
1645 .unwrap();
1637
1646
1638 assert_eq!(
1647 assert_eq!(
1639 matcher.visit_children_set(HgPath::new(b"")),
1648 matcher.visit_children_set(HgPath::new(b"")),
1640 VisitChildrenSet::This
1649 VisitChildrenSet::This
1641 );
1650 );
1642
1651
1643 // Test multiple patterns
1652 // Test multiple patterns
1644 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1653 let matcher = IncludeMatcher::new(vec![IgnorePattern::new(
1645 PatternSyntax::Glob,
1654 PatternSyntax::Glob,
1646 b"**/*.exe",
1655 b"**/*.exe",
1647 Path::new(""),
1656 Path::new(""),
1648 )])
1657 )])
1649 .unwrap();
1658 .unwrap();
1650
1659
1651 assert_eq!(
1660 assert_eq!(
1652 matcher.visit_children_set(HgPath::new(b"")),
1661 matcher.visit_children_set(HgPath::new(b"")),
1653 VisitChildrenSet::This
1662 VisitChildrenSet::This
1654 );
1663 );
1655 }
1664 }
1656
1665
1657 #[test]
1666 #[test]
1658 fn test_unionmatcher() {
1667 fn test_unionmatcher() {
1659 // Path + Rootfiles
1668 // Path + Rootfiles
1660 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1669 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1661 PatternSyntax::RelPath,
1670 PatternSyntax::RelPath,
1662 b"dir/subdir",
1671 b"dir/subdir",
1663 Path::new(""),
1672 Path::new(""),
1664 )])
1673 )])
1665 .unwrap();
1674 .unwrap();
1666 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1675 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1667 PatternSyntax::RootFiles,
1676 PatternSyntax::RootFilesIn,
1668 b"dir",
1677 b"dir",
1669 Path::new(""),
1678 Path::new(""),
1670 )])
1679 )])
1671 .unwrap();
1680 .unwrap();
1672 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1681 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1673
1682
1674 let mut set = HashSet::new();
1683 let mut set = HashSet::new();
1675 set.insert(HgPathBuf::from_bytes(b"dir"));
1684 set.insert(HgPathBuf::from_bytes(b"dir"));
1676 assert_eq!(
1685 assert_eq!(
1677 matcher.visit_children_set(HgPath::new(b"")),
1686 matcher.visit_children_set(HgPath::new(b"")),
1678 VisitChildrenSet::Set(set)
1687 VisitChildrenSet::Set(set)
1679 );
1688 );
1680 assert_eq!(
1689 assert_eq!(
1681 matcher.visit_children_set(HgPath::new(b"dir")),
1690 matcher.visit_children_set(HgPath::new(b"dir")),
1682 VisitChildrenSet::This
1691 VisitChildrenSet::This
1683 );
1692 );
1684 assert_eq!(
1693 assert_eq!(
1685 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1694 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1686 VisitChildrenSet::Recursive
1695 VisitChildrenSet::Recursive
1687 );
1696 );
1688 assert_eq!(
1697 assert_eq!(
1689 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1698 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1690 VisitChildrenSet::Empty
1699 VisitChildrenSet::Empty
1691 );
1700 );
1692 assert_eq!(
1701 assert_eq!(
1693 matcher.visit_children_set(HgPath::new(b"folder")),
1702 matcher.visit_children_set(HgPath::new(b"folder")),
1694 VisitChildrenSet::Empty
1703 VisitChildrenSet::Empty
1695 );
1704 );
1696 assert_eq!(
1705 assert_eq!(
1697 matcher.visit_children_set(HgPath::new(b"folder")),
1706 matcher.visit_children_set(HgPath::new(b"folder")),
1698 VisitChildrenSet::Empty
1707 VisitChildrenSet::Empty
1699 );
1708 );
1700
1709
1701 // OPT: These next two could be 'all' instead of 'this'.
1710 // OPT: These next two could be 'all' instead of 'this'.
1702 assert_eq!(
1711 assert_eq!(
1703 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1712 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1704 VisitChildrenSet::This
1713 VisitChildrenSet::This
1705 );
1714 );
1706 assert_eq!(
1715 assert_eq!(
1707 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1716 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1708 VisitChildrenSet::This
1717 VisitChildrenSet::This
1709 );
1718 );
1710
1719
1711 // Path + unrelated Path
1720 // Path + unrelated Path
1712 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1721 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1713 PatternSyntax::RelPath,
1722 PatternSyntax::RelPath,
1714 b"dir/subdir",
1723 b"dir/subdir",
1715 Path::new(""),
1724 Path::new(""),
1716 )])
1725 )])
1717 .unwrap();
1726 .unwrap();
1718 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1727 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1719 PatternSyntax::RelPath,
1728 PatternSyntax::RelPath,
1720 b"folder",
1729 b"folder",
1721 Path::new(""),
1730 Path::new(""),
1722 )])
1731 )])
1723 .unwrap();
1732 .unwrap();
1724 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1733 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1725
1734
1726 let mut set = HashSet::new();
1735 let mut set = HashSet::new();
1727 set.insert(HgPathBuf::from_bytes(b"folder"));
1736 set.insert(HgPathBuf::from_bytes(b"folder"));
1728 set.insert(HgPathBuf::from_bytes(b"dir"));
1737 set.insert(HgPathBuf::from_bytes(b"dir"));
1729 assert_eq!(
1738 assert_eq!(
1730 matcher.visit_children_set(HgPath::new(b"")),
1739 matcher.visit_children_set(HgPath::new(b"")),
1731 VisitChildrenSet::Set(set)
1740 VisitChildrenSet::Set(set)
1732 );
1741 );
1733 let mut set = HashSet::new();
1742 let mut set = HashSet::new();
1734 set.insert(HgPathBuf::from_bytes(b"subdir"));
1743 set.insert(HgPathBuf::from_bytes(b"subdir"));
1735 assert_eq!(
1744 assert_eq!(
1736 matcher.visit_children_set(HgPath::new(b"dir")),
1745 matcher.visit_children_set(HgPath::new(b"dir")),
1737 VisitChildrenSet::Set(set)
1746 VisitChildrenSet::Set(set)
1738 );
1747 );
1739
1748
1740 assert_eq!(
1749 assert_eq!(
1741 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1750 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1742 VisitChildrenSet::Recursive
1751 VisitChildrenSet::Recursive
1743 );
1752 );
1744 assert_eq!(
1753 assert_eq!(
1745 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1754 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1746 VisitChildrenSet::Empty
1755 VisitChildrenSet::Empty
1747 );
1756 );
1748
1757
1749 assert_eq!(
1758 assert_eq!(
1750 matcher.visit_children_set(HgPath::new(b"folder")),
1759 matcher.visit_children_set(HgPath::new(b"folder")),
1751 VisitChildrenSet::Recursive
1760 VisitChildrenSet::Recursive
1752 );
1761 );
1753 // OPT: These next two could be 'all' instead of 'this'.
1762 // OPT: These next two could be 'all' instead of 'this'.
1754 assert_eq!(
1763 assert_eq!(
1755 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1764 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1756 VisitChildrenSet::This
1765 VisitChildrenSet::This
1757 );
1766 );
1758 assert_eq!(
1767 assert_eq!(
1759 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1768 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1760 VisitChildrenSet::This
1769 VisitChildrenSet::This
1761 );
1770 );
1762
1771
1763 // Path + subpath
1772 // Path + subpath
1764 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1773 let m1 = IncludeMatcher::new(vec![IgnorePattern::new(
1765 PatternSyntax::RelPath,
1774 PatternSyntax::RelPath,
1766 b"dir/subdir/x",
1775 b"dir/subdir/x",
1767 Path::new(""),
1776 Path::new(""),
1768 )])
1777 )])
1769 .unwrap();
1778 .unwrap();
1770 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1779 let m2 = IncludeMatcher::new(vec![IgnorePattern::new(
1771 PatternSyntax::RelPath,
1780 PatternSyntax::RelPath,
1772 b"dir/subdir",
1781 b"dir/subdir",
1773 Path::new(""),
1782 Path::new(""),
1774 )])
1783 )])
1775 .unwrap();
1784 .unwrap();
1776 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1785 let matcher = UnionMatcher::new(vec![Box::new(m1), Box::new(m2)]);
1777
1786
1778 let mut set = HashSet::new();
1787 let mut set = HashSet::new();
1779 set.insert(HgPathBuf::from_bytes(b"dir"));
1788 set.insert(HgPathBuf::from_bytes(b"dir"));
1780 assert_eq!(
1789 assert_eq!(
1781 matcher.visit_children_set(HgPath::new(b"")),
1790 matcher.visit_children_set(HgPath::new(b"")),
1782 VisitChildrenSet::Set(set)
1791 VisitChildrenSet::Set(set)
1783 );
1792 );
1784 let mut set = HashSet::new();
1793 let mut set = HashSet::new();
1785 set.insert(HgPathBuf::from_bytes(b"subdir"));
1794 set.insert(HgPathBuf::from_bytes(b"subdir"));
1786 assert_eq!(
1795 assert_eq!(
1787 matcher.visit_children_set(HgPath::new(b"dir")),
1796 matcher.visit_children_set(HgPath::new(b"dir")),
1788 VisitChildrenSet::Set(set)
1797 VisitChildrenSet::Set(set)
1789 );
1798 );
1790
1799
1791 assert_eq!(
1800 assert_eq!(
1792 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1801 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1793 VisitChildrenSet::Recursive
1802 VisitChildrenSet::Recursive
1794 );
1803 );
1795 assert_eq!(
1804 assert_eq!(
1796 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1805 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1797 VisitChildrenSet::Empty
1806 VisitChildrenSet::Empty
1798 );
1807 );
1799
1808
1800 assert_eq!(
1809 assert_eq!(
1801 matcher.visit_children_set(HgPath::new(b"folder")),
1810 matcher.visit_children_set(HgPath::new(b"folder")),
1802 VisitChildrenSet::Empty
1811 VisitChildrenSet::Empty
1803 );
1812 );
1804 assert_eq!(
1813 assert_eq!(
1805 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1814 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1806 VisitChildrenSet::Recursive
1815 VisitChildrenSet::Recursive
1807 );
1816 );
1808 // OPT: this should probably be 'all' not 'this'.
1817 // OPT: this should probably be 'all' not 'this'.
1809 assert_eq!(
1818 assert_eq!(
1810 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1819 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1811 VisitChildrenSet::This
1820 VisitChildrenSet::This
1812 );
1821 );
1813 }
1822 }
1814
1823
1815 #[test]
1824 #[test]
1816 fn test_intersectionmatcher() {
1825 fn test_intersectionmatcher() {
1817 // Include path + Include rootfiles
1826 // Include path + Include rootfiles
1818 let m1 = Box::new(
1827 let m1 = Box::new(
1819 IncludeMatcher::new(vec![IgnorePattern::new(
1828 IncludeMatcher::new(vec![IgnorePattern::new(
1820 PatternSyntax::RelPath,
1829 PatternSyntax::RelPath,
1821 b"dir/subdir",
1830 b"dir/subdir",
1822 Path::new(""),
1831 Path::new(""),
1823 )])
1832 )])
1824 .unwrap(),
1833 .unwrap(),
1825 );
1834 );
1826 let m2 = Box::new(
1835 let m2 = Box::new(
1827 IncludeMatcher::new(vec![IgnorePattern::new(
1836 IncludeMatcher::new(vec![IgnorePattern::new(
1828 PatternSyntax::RootFiles,
1837 PatternSyntax::RootFilesIn,
1829 b"dir",
1838 b"dir",
1830 Path::new(""),
1839 Path::new(""),
1831 )])
1840 )])
1832 .unwrap(),
1841 .unwrap(),
1833 );
1842 );
1834 let matcher = IntersectionMatcher::new(m1, m2);
1843 let matcher = IntersectionMatcher::new(m1, m2);
1835
1844
1836 let mut set = HashSet::new();
1845 let mut set = HashSet::new();
1837 set.insert(HgPathBuf::from_bytes(b"dir"));
1846 set.insert(HgPathBuf::from_bytes(b"dir"));
1838 assert_eq!(
1847 assert_eq!(
1839 matcher.visit_children_set(HgPath::new(b"")),
1848 matcher.visit_children_set(HgPath::new(b"")),
1840 VisitChildrenSet::Set(set)
1849 VisitChildrenSet::Set(set)
1841 );
1850 );
1842 assert_eq!(
1851 assert_eq!(
1843 matcher.visit_children_set(HgPath::new(b"dir")),
1852 matcher.visit_children_set(HgPath::new(b"dir")),
1844 VisitChildrenSet::This
1853 VisitChildrenSet::This
1845 );
1854 );
1846 assert_eq!(
1855 assert_eq!(
1847 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1856 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1848 VisitChildrenSet::Empty
1857 VisitChildrenSet::Empty
1849 );
1858 );
1850 assert_eq!(
1859 assert_eq!(
1851 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1860 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1852 VisitChildrenSet::Empty
1861 VisitChildrenSet::Empty
1853 );
1862 );
1854 assert_eq!(
1863 assert_eq!(
1855 matcher.visit_children_set(HgPath::new(b"folder")),
1864 matcher.visit_children_set(HgPath::new(b"folder")),
1856 VisitChildrenSet::Empty
1865 VisitChildrenSet::Empty
1857 );
1866 );
1858 assert_eq!(
1867 assert_eq!(
1859 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1868 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1860 VisitChildrenSet::Empty
1869 VisitChildrenSet::Empty
1861 );
1870 );
1862 assert_eq!(
1871 assert_eq!(
1863 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1872 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1864 VisitChildrenSet::Empty
1873 VisitChildrenSet::Empty
1865 );
1874 );
1866
1875
1867 // Non intersecting paths
1876 // Non intersecting paths
1868 let m1 = Box::new(
1877 let m1 = Box::new(
1869 IncludeMatcher::new(vec![IgnorePattern::new(
1878 IncludeMatcher::new(vec![IgnorePattern::new(
1870 PatternSyntax::RelPath,
1879 PatternSyntax::RelPath,
1871 b"dir/subdir",
1880 b"dir/subdir",
1872 Path::new(""),
1881 Path::new(""),
1873 )])
1882 )])
1874 .unwrap(),
1883 .unwrap(),
1875 );
1884 );
1876 let m2 = Box::new(
1885 let m2 = Box::new(
1877 IncludeMatcher::new(vec![IgnorePattern::new(
1886 IncludeMatcher::new(vec![IgnorePattern::new(
1878 PatternSyntax::RelPath,
1887 PatternSyntax::RelPath,
1879 b"folder",
1888 b"folder",
1880 Path::new(""),
1889 Path::new(""),
1881 )])
1890 )])
1882 .unwrap(),
1891 .unwrap(),
1883 );
1892 );
1884 let matcher = IntersectionMatcher::new(m1, m2);
1893 let matcher = IntersectionMatcher::new(m1, m2);
1885
1894
1886 assert_eq!(
1895 assert_eq!(
1887 matcher.visit_children_set(HgPath::new(b"")),
1896 matcher.visit_children_set(HgPath::new(b"")),
1888 VisitChildrenSet::Empty
1897 VisitChildrenSet::Empty
1889 );
1898 );
1890 assert_eq!(
1899 assert_eq!(
1891 matcher.visit_children_set(HgPath::new(b"dir")),
1900 matcher.visit_children_set(HgPath::new(b"dir")),
1892 VisitChildrenSet::Empty
1901 VisitChildrenSet::Empty
1893 );
1902 );
1894 assert_eq!(
1903 assert_eq!(
1895 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1904 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1896 VisitChildrenSet::Empty
1905 VisitChildrenSet::Empty
1897 );
1906 );
1898 assert_eq!(
1907 assert_eq!(
1899 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1908 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1900 VisitChildrenSet::Empty
1909 VisitChildrenSet::Empty
1901 );
1910 );
1902 assert_eq!(
1911 assert_eq!(
1903 matcher.visit_children_set(HgPath::new(b"folder")),
1912 matcher.visit_children_set(HgPath::new(b"folder")),
1904 VisitChildrenSet::Empty
1913 VisitChildrenSet::Empty
1905 );
1914 );
1906 assert_eq!(
1915 assert_eq!(
1907 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1916 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1908 VisitChildrenSet::Empty
1917 VisitChildrenSet::Empty
1909 );
1918 );
1910 assert_eq!(
1919 assert_eq!(
1911 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1920 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1912 VisitChildrenSet::Empty
1921 VisitChildrenSet::Empty
1913 );
1922 );
1914
1923
1915 // Nested paths
1924 // Nested paths
1916 let m1 = Box::new(
1925 let m1 = Box::new(
1917 IncludeMatcher::new(vec![IgnorePattern::new(
1926 IncludeMatcher::new(vec![IgnorePattern::new(
1918 PatternSyntax::RelPath,
1927 PatternSyntax::RelPath,
1919 b"dir/subdir/x",
1928 b"dir/subdir/x",
1920 Path::new(""),
1929 Path::new(""),
1921 )])
1930 )])
1922 .unwrap(),
1931 .unwrap(),
1923 );
1932 );
1924 let m2 = Box::new(
1933 let m2 = Box::new(
1925 IncludeMatcher::new(vec![IgnorePattern::new(
1934 IncludeMatcher::new(vec![IgnorePattern::new(
1926 PatternSyntax::RelPath,
1935 PatternSyntax::RelPath,
1927 b"dir/subdir",
1936 b"dir/subdir",
1928 Path::new(""),
1937 Path::new(""),
1929 )])
1938 )])
1930 .unwrap(),
1939 .unwrap(),
1931 );
1940 );
1932 let matcher = IntersectionMatcher::new(m1, m2);
1941 let matcher = IntersectionMatcher::new(m1, m2);
1933
1942
1934 let mut set = HashSet::new();
1943 let mut set = HashSet::new();
1935 set.insert(HgPathBuf::from_bytes(b"dir"));
1944 set.insert(HgPathBuf::from_bytes(b"dir"));
1936 assert_eq!(
1945 assert_eq!(
1937 matcher.visit_children_set(HgPath::new(b"")),
1946 matcher.visit_children_set(HgPath::new(b"")),
1938 VisitChildrenSet::Set(set)
1947 VisitChildrenSet::Set(set)
1939 );
1948 );
1940
1949
1941 let mut set = HashSet::new();
1950 let mut set = HashSet::new();
1942 set.insert(HgPathBuf::from_bytes(b"subdir"));
1951 set.insert(HgPathBuf::from_bytes(b"subdir"));
1943 assert_eq!(
1952 assert_eq!(
1944 matcher.visit_children_set(HgPath::new(b"dir")),
1953 matcher.visit_children_set(HgPath::new(b"dir")),
1945 VisitChildrenSet::Set(set)
1954 VisitChildrenSet::Set(set)
1946 );
1955 );
1947 let mut set = HashSet::new();
1956 let mut set = HashSet::new();
1948 set.insert(HgPathBuf::from_bytes(b"x"));
1957 set.insert(HgPathBuf::from_bytes(b"x"));
1949 assert_eq!(
1958 assert_eq!(
1950 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1959 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
1951 VisitChildrenSet::Set(set)
1960 VisitChildrenSet::Set(set)
1952 );
1961 );
1953 assert_eq!(
1962 assert_eq!(
1954 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1963 matcher.visit_children_set(HgPath::new(b"dir/foo")),
1955 VisitChildrenSet::Empty
1964 VisitChildrenSet::Empty
1956 );
1965 );
1957 assert_eq!(
1966 assert_eq!(
1958 matcher.visit_children_set(HgPath::new(b"folder")),
1967 matcher.visit_children_set(HgPath::new(b"folder")),
1959 VisitChildrenSet::Empty
1968 VisitChildrenSet::Empty
1960 );
1969 );
1961 assert_eq!(
1970 assert_eq!(
1962 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1971 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
1963 VisitChildrenSet::Empty
1972 VisitChildrenSet::Empty
1964 );
1973 );
1965 // OPT: this should probably be 'all' not 'this'.
1974 // OPT: this should probably be 'all' not 'this'.
1966 assert_eq!(
1975 assert_eq!(
1967 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1976 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
1968 VisitChildrenSet::This
1977 VisitChildrenSet::This
1969 );
1978 );
1970
1979
1971 // Diverging paths
1980 // Diverging paths
1972 let m1 = Box::new(
1981 let m1 = Box::new(
1973 IncludeMatcher::new(vec![IgnorePattern::new(
1982 IncludeMatcher::new(vec![IgnorePattern::new(
1974 PatternSyntax::RelPath,
1983 PatternSyntax::RelPath,
1975 b"dir/subdir/x",
1984 b"dir/subdir/x",
1976 Path::new(""),
1985 Path::new(""),
1977 )])
1986 )])
1978 .unwrap(),
1987 .unwrap(),
1979 );
1988 );
1980 let m2 = Box::new(
1989 let m2 = Box::new(
1981 IncludeMatcher::new(vec![IgnorePattern::new(
1990 IncludeMatcher::new(vec![IgnorePattern::new(
1982 PatternSyntax::RelPath,
1991 PatternSyntax::RelPath,
1983 b"dir/subdir/z",
1992 b"dir/subdir/z",
1984 Path::new(""),
1993 Path::new(""),
1985 )])
1994 )])
1986 .unwrap(),
1995 .unwrap(),
1987 );
1996 );
1988 let matcher = IntersectionMatcher::new(m1, m2);
1997 let matcher = IntersectionMatcher::new(m1, m2);
1989
1998
1990 // OPT: these next two could probably be Empty as well.
1999 // OPT: these next two could probably be Empty as well.
1991 let mut set = HashSet::new();
2000 let mut set = HashSet::new();
1992 set.insert(HgPathBuf::from_bytes(b"dir"));
2001 set.insert(HgPathBuf::from_bytes(b"dir"));
1993 assert_eq!(
2002 assert_eq!(
1994 matcher.visit_children_set(HgPath::new(b"")),
2003 matcher.visit_children_set(HgPath::new(b"")),
1995 VisitChildrenSet::Set(set)
2004 VisitChildrenSet::Set(set)
1996 );
2005 );
1997 // OPT: these next two could probably be Empty as well.
2006 // OPT: these next two could probably be Empty as well.
1998 let mut set = HashSet::new();
2007 let mut set = HashSet::new();
1999 set.insert(HgPathBuf::from_bytes(b"subdir"));
2008 set.insert(HgPathBuf::from_bytes(b"subdir"));
2000 assert_eq!(
2009 assert_eq!(
2001 matcher.visit_children_set(HgPath::new(b"dir")),
2010 matcher.visit_children_set(HgPath::new(b"dir")),
2002 VisitChildrenSet::Set(set)
2011 VisitChildrenSet::Set(set)
2003 );
2012 );
2004 assert_eq!(
2013 assert_eq!(
2005 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
2014 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
2006 VisitChildrenSet::Empty
2015 VisitChildrenSet::Empty
2007 );
2016 );
2008 assert_eq!(
2017 assert_eq!(
2009 matcher.visit_children_set(HgPath::new(b"dir/foo")),
2018 matcher.visit_children_set(HgPath::new(b"dir/foo")),
2010 VisitChildrenSet::Empty
2019 VisitChildrenSet::Empty
2011 );
2020 );
2012 assert_eq!(
2021 assert_eq!(
2013 matcher.visit_children_set(HgPath::new(b"folder")),
2022 matcher.visit_children_set(HgPath::new(b"folder")),
2014 VisitChildrenSet::Empty
2023 VisitChildrenSet::Empty
2015 );
2024 );
2016 assert_eq!(
2025 assert_eq!(
2017 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
2026 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
2018 VisitChildrenSet::Empty
2027 VisitChildrenSet::Empty
2019 );
2028 );
2020 assert_eq!(
2029 assert_eq!(
2021 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
2030 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
2022 VisitChildrenSet::Empty
2031 VisitChildrenSet::Empty
2023 );
2032 );
2024 }
2033 }
2025
2034
2026 #[test]
2035 #[test]
2027 fn test_differencematcher() {
2036 fn test_differencematcher() {
2028 // Two alwaysmatchers should function like a nevermatcher
2037 // Two alwaysmatchers should function like a nevermatcher
2029 let m1 = AlwaysMatcher;
2038 let m1 = AlwaysMatcher;
2030 let m2 = AlwaysMatcher;
2039 let m2 = AlwaysMatcher;
2031 let matcher = DifferenceMatcher::new(Box::new(m1), Box::new(m2));
2040 let matcher = DifferenceMatcher::new(Box::new(m1), Box::new(m2));
2032
2041
2033 for case in &[
2042 for case in &[
2034 &b""[..],
2043 &b""[..],
2035 b"dir",
2044 b"dir",
2036 b"dir/subdir",
2045 b"dir/subdir",
2037 b"dir/subdir/z",
2046 b"dir/subdir/z",
2038 b"dir/foo",
2047 b"dir/foo",
2039 b"dir/subdir/x",
2048 b"dir/subdir/x",
2040 b"folder",
2049 b"folder",
2041 ] {
2050 ] {
2042 assert_eq!(
2051 assert_eq!(
2043 matcher.visit_children_set(HgPath::new(case)),
2052 matcher.visit_children_set(HgPath::new(case)),
2044 VisitChildrenSet::Empty
2053 VisitChildrenSet::Empty
2045 );
2054 );
2046 }
2055 }
2047
2056
2048 // One always and one never should behave the same as an always
2057 // One always and one never should behave the same as an always
2049 let m1 = AlwaysMatcher;
2058 let m1 = AlwaysMatcher;
2050 let m2 = NeverMatcher;
2059 let m2 = NeverMatcher;
2051 let matcher = DifferenceMatcher::new(Box::new(m1), Box::new(m2));
2060 let matcher = DifferenceMatcher::new(Box::new(m1), Box::new(m2));
2052
2061
2053 for case in &[
2062 for case in &[
2054 &b""[..],
2063 &b""[..],
2055 b"dir",
2064 b"dir",
2056 b"dir/subdir",
2065 b"dir/subdir",
2057 b"dir/subdir/z",
2066 b"dir/subdir/z",
2058 b"dir/foo",
2067 b"dir/foo",
2059 b"dir/subdir/x",
2068 b"dir/subdir/x",
2060 b"folder",
2069 b"folder",
2061 ] {
2070 ] {
2062 assert_eq!(
2071 assert_eq!(
2063 matcher.visit_children_set(HgPath::new(case)),
2072 matcher.visit_children_set(HgPath::new(case)),
2064 VisitChildrenSet::Recursive
2073 VisitChildrenSet::Recursive
2065 );
2074 );
2066 }
2075 }
2067
2076
2068 // Two include matchers
2077 // Two include matchers
2069 let m1 = Box::new(
2078 let m1 = Box::new(
2070 IncludeMatcher::new(vec![IgnorePattern::new(
2079 IncludeMatcher::new(vec![IgnorePattern::new(
2071 PatternSyntax::RelPath,
2080 PatternSyntax::RelPath,
2072 b"dir/subdir",
2081 b"dir/subdir",
2073 Path::new("/repo"),
2082 Path::new("/repo"),
2074 )])
2083 )])
2075 .unwrap(),
2084 .unwrap(),
2076 );
2085 );
2077 let m2 = Box::new(
2086 let m2 = Box::new(
2078 IncludeMatcher::new(vec![IgnorePattern::new(
2087 IncludeMatcher::new(vec![IgnorePattern::new(
2079 PatternSyntax::RootFiles,
2088 PatternSyntax::RootFilesIn,
2080 b"dir",
2089 b"dir",
2081 Path::new("/repo"),
2090 Path::new("/repo"),
2082 )])
2091 )])
2083 .unwrap(),
2092 .unwrap(),
2084 );
2093 );
2085
2094
2086 let matcher = DifferenceMatcher::new(m1, m2);
2095 let matcher = DifferenceMatcher::new(m1, m2);
2087
2096
2088 let mut set = HashSet::new();
2097 let mut set = HashSet::new();
2089 set.insert(HgPathBuf::from_bytes(b"dir"));
2098 set.insert(HgPathBuf::from_bytes(b"dir"));
2090 assert_eq!(
2099 assert_eq!(
2091 matcher.visit_children_set(HgPath::new(b"")),
2100 matcher.visit_children_set(HgPath::new(b"")),
2092 VisitChildrenSet::Set(set)
2101 VisitChildrenSet::Set(set)
2093 );
2102 );
2094
2103
2095 let mut set = HashSet::new();
2104 let mut set = HashSet::new();
2096 set.insert(HgPathBuf::from_bytes(b"subdir"));
2105 set.insert(HgPathBuf::from_bytes(b"subdir"));
2097 assert_eq!(
2106 assert_eq!(
2098 matcher.visit_children_set(HgPath::new(b"dir")),
2107 matcher.visit_children_set(HgPath::new(b"dir")),
2099 VisitChildrenSet::Set(set)
2108 VisitChildrenSet::Set(set)
2100 );
2109 );
2101 assert_eq!(
2110 assert_eq!(
2102 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
2111 matcher.visit_children_set(HgPath::new(b"dir/subdir")),
2103 VisitChildrenSet::Recursive
2112 VisitChildrenSet::Recursive
2104 );
2113 );
2105 assert_eq!(
2114 assert_eq!(
2106 matcher.visit_children_set(HgPath::new(b"dir/foo")),
2115 matcher.visit_children_set(HgPath::new(b"dir/foo")),
2107 VisitChildrenSet::Empty
2116 VisitChildrenSet::Empty
2108 );
2117 );
2109 assert_eq!(
2118 assert_eq!(
2110 matcher.visit_children_set(HgPath::new(b"folder")),
2119 matcher.visit_children_set(HgPath::new(b"folder")),
2111 VisitChildrenSet::Empty
2120 VisitChildrenSet::Empty
2112 );
2121 );
2113 assert_eq!(
2122 assert_eq!(
2114 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
2123 matcher.visit_children_set(HgPath::new(b"dir/subdir/z")),
2115 VisitChildrenSet::This
2124 VisitChildrenSet::This
2116 );
2125 );
2117 assert_eq!(
2126 assert_eq!(
2118 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
2127 matcher.visit_children_set(HgPath::new(b"dir/subdir/x")),
2119 VisitChildrenSet::This
2128 VisitChildrenSet::This
2120 );
2129 );
2121 }
2130 }
2131
2132 mod invariants {
2133 pub mod visit_children_set {
2134
2135 use crate::{
2136 matchers::{tests::Tree, Matcher, VisitChildrenSet},
2137 utils::hg_path::HgPath,
2138 };
2139
2140 #[allow(dead_code)]
2141 #[derive(Debug)]
2142 struct Error<'a, M> {
2143 matcher: &'a M,
2144 path: &'a HgPath,
2145 matching: &'a Tree,
2146 visit_children_set: &'a VisitChildrenSet,
2147 }
2148
2149 fn holds(
2150 matching: &Tree,
2151 not_matching: &Tree,
2152 vcs: &VisitChildrenSet,
2153 ) -> bool {
2154 match vcs {
2155 VisitChildrenSet::Empty => matching.is_empty(),
2156 VisitChildrenSet::This => {
2157 // `This` does not come with any obligations.
2158 true
2159 }
2160 VisitChildrenSet::Recursive => {
2161 // `Recursive` requires that *everything* in the
2162 // subtree matches. This
2163 // requirement is relied on for example in
2164 // DifferenceMatcher implementation.
2165 not_matching.is_empty()
2166 }
2167 VisitChildrenSet::Set(allowed_children) => {
2168 // `allowed_children` does not distinguish between
2169 // files and directories: if it's not included, it
2170 // must not be matched.
2171 for k in matching.dirs.keys() {
2172 if !(allowed_children.contains(k)) {
2173 return false;
2174 }
2175 }
2176 for k in matching.files.iter() {
2177 if !(allowed_children.contains(k)) {
2178 return false;
2179 }
2180 }
2181 true
2182 }
2183 }
2184 }
2185
2186 pub fn check<M: Matcher + std::fmt::Debug>(
2187 matcher: &M,
2188 path: &HgPath,
2189 matching: &Tree,
2190 not_matching: &Tree,
2191 visit_children_set: &VisitChildrenSet,
2192 ) {
2193 if !holds(matching, not_matching, visit_children_set) {
2194 panic!(
2195 "{:#?}",
2196 Error {
2197 matcher,
2198 path,
2199 visit_children_set,
2200 matching
2201 }
2202 )
2203 }
2204 }
2205 }
2206 }
2207
2208 #[derive(Debug, Clone)]
2209 pub struct Tree {
2210 files: BTreeSet<HgPathBuf>,
2211 dirs: BTreeMap<HgPathBuf, Tree>,
2212 }
2213
2214 impl Tree {
2215 fn len(&self) -> usize {
2216 let mut n = 0;
2217 n += self.files.len();
2218 for d in self.dirs.values() {
2219 n += d.len();
2220 }
2221 n
2222 }
2223
2224 fn is_empty(&self) -> bool {
2225 self.files.is_empty() && self.dirs.is_empty()
2226 }
2227
2228 fn make(
2229 files: BTreeSet<HgPathBuf>,
2230 dirs: BTreeMap<HgPathBuf, Tree>,
2231 ) -> Self {
2232 Self {
2233 files,
2234 dirs: dirs
2235 .into_iter()
2236 .filter(|(_k, v)| (!(v.is_empty())))
2237 .collect(),
2238 }
2239 }
2240
2241 fn filter_and_check<M: Matcher + Debug>(
2242 &self,
2243 m: &M,
2244 path: &HgPath,
2245 ) -> (Self, Self) {
2246 let (files1, files2): (BTreeSet<HgPathBuf>, BTreeSet<HgPathBuf>) =
2247 self.files
2248 .iter()
2249 .map(|v| v.to_owned())
2250 .partition(|v| m.matches(&path.join(v)));
2251 let (dirs1, dirs2): (
2252 BTreeMap<HgPathBuf, Tree>,
2253 BTreeMap<HgPathBuf, Tree>,
2254 ) = self
2255 .dirs
2256 .iter()
2257 .map(|(k, v)| {
2258 let path = path.join(k);
2259 let (t1, t2) = v.filter_and_check(m, &path);
2260 ((k.clone(), t1), (k.clone(), t2))
2261 })
2262 .unzip();
2263 let matching = Self::make(files1, dirs1);
2264 let not_matching = Self::make(files2, dirs2);
2265 let vcs = m.visit_children_set(path);
2266 invariants::visit_children_set::check(
2267 m,
2268 path,
2269 &matching,
2270 &not_matching,
2271 &vcs,
2272 );
2273 (matching, not_matching)
2274 }
2275
2276 fn check_matcher<M: Matcher + Debug>(
2277 &self,
2278 m: &M,
2279 expect_count: usize,
2280 ) {
2281 let res = self.filter_and_check(m, &HgPathBuf::new());
2282 if expect_count != res.0.len() {
2283 eprintln!(
2284 "warning: expected {} matches, got {} for {:#?}",
2285 expect_count,
2286 res.0.len(),
2287 m
2288 );
2289 }
2290 }
2291 }
2292
2293 fn mkdir(children: &[(&[u8], &Tree)]) -> Tree {
2294 let p = HgPathBuf::from_bytes;
2295 let names = [
2296 p(b"a"),
2297 p(b"b.txt"),
2298 p(b"file.txt"),
2299 p(b"c.c"),
2300 p(b"c.h"),
2301 p(b"dir1"),
2302 p(b"dir2"),
2303 p(b"subdir"),
2304 ];
2305 let files: BTreeSet<HgPathBuf> = BTreeSet::from(names);
2306 let dirs = children
2307 .iter()
2308 .map(|(name, t)| (p(name), (*t).clone()))
2309 .collect();
2310 Tree { files, dirs }
2311 }
2312
2313 fn make_example_tree() -> Tree {
2314 let leaf = mkdir(&[]);
2315 let abc = mkdir(&[(b"d", &leaf)]);
2316 let ab = mkdir(&[(b"c", &abc)]);
2317 let a = mkdir(&[(b"b", &ab)]);
2318 let dir = mkdir(&[(b"subdir", &leaf), (b"subdir.c", &leaf)]);
2319 mkdir(&[(b"dir", &dir), (b"dir1", &dir), (b"dir2", &dir), (b"a", &a)])
2320 }
2321
2322 #[test]
2323 fn test_pattern_matcher_visit_children_set() {
2324 let tree = make_example_tree();
2325 let pattern_dir1_glob_c =
2326 PatternMatcher::new(vec![IgnorePattern::new(
2327 PatternSyntax::Glob,
2328 b"dir1/*.c",
2329 Path::new(""),
2330 )])
2331 .unwrap();
2332 let pattern_dir1 = || {
2333 PatternMatcher::new(vec![IgnorePattern::new(
2334 PatternSyntax::Path,
2335 b"dir1",
2336 Path::new(""),
2337 )])
2338 .unwrap()
2339 };
2340 let pattern_dir1_a = PatternMatcher::new(vec![IgnorePattern::new(
2341 PatternSyntax::Glob,
2342 b"dir1/a",
2343 Path::new(""),
2344 )])
2345 .unwrap();
2346 let pattern_relglob_c = || {
2347 PatternMatcher::new(vec![IgnorePattern::new(
2348 PatternSyntax::RelGlob,
2349 b"*.c",
2350 Path::new(""),
2351 )])
2352 .unwrap()
2353 };
2354 let files = vec![HgPathBuf::from_bytes(b"dir/subdir/b.txt")];
2355 let file_dir_subdir_b = FileMatcher::new(files).unwrap();
2356
2357 let files = vec![
2358 HgPathBuf::from_bytes(b"file.txt"),
2359 HgPathBuf::from_bytes(b"a/file.txt"),
2360 HgPathBuf::from_bytes(b"a/b/file.txt"),
2361 // No file in a/b/c
2362 HgPathBuf::from_bytes(b"a/b/c/d/file.txt"),
2363 ];
2364 let file_abcdfile = FileMatcher::new(files).unwrap();
2365 let rootfilesin_dir = PatternMatcher::new(vec![IgnorePattern::new(
2366 PatternSyntax::RootFilesIn,
2367 b"dir",
2368 Path::new(""),
2369 )])
2370 .unwrap();
2371
2372 let pattern_filepath_dir_subdir =
2373 PatternMatcher::new(vec![IgnorePattern::new(
2374 PatternSyntax::FilePath,
2375 b"dir/subdir",
2376 Path::new(""),
2377 )])
2378 .unwrap();
2379
2380 let include_dir_subdir =
2381 IncludeMatcher::new(vec![IgnorePattern::new(
2382 PatternSyntax::RelPath,
2383 b"dir/subdir",
2384 Path::new(""),
2385 )])
2386 .unwrap();
2387
2388 let more_includematchers = [
2389 IncludeMatcher::new(vec![IgnorePattern::new(
2390 PatternSyntax::Glob,
2391 b"dir/s*",
2392 Path::new(""),
2393 )])
2394 .unwrap(),
2395 // Test multiple patterns
2396 IncludeMatcher::new(vec![
2397 IgnorePattern::new(
2398 PatternSyntax::RelPath,
2399 b"dir",
2400 Path::new(""),
2401 ),
2402 IgnorePattern::new(PatternSyntax::Glob, b"s*", Path::new("")),
2403 ])
2404 .unwrap(),
2405 // Test multiple patterns
2406 IncludeMatcher::new(vec![IgnorePattern::new(
2407 PatternSyntax::Glob,
2408 b"**/*.c",
2409 Path::new(""),
2410 )])
2411 .unwrap(),
2412 ];
2413
2414 tree.check_matcher(&pattern_dir1(), 25);
2415 tree.check_matcher(&pattern_dir1_a, 1);
2416 tree.check_matcher(&pattern_dir1_glob_c, 2);
2417 tree.check_matcher(&pattern_relglob_c(), 14);
2418 tree.check_matcher(&AlwaysMatcher, 112);
2419 tree.check_matcher(&NeverMatcher, 0);
2420 tree.check_matcher(
2421 &IntersectionMatcher::new(
2422 Box::new(pattern_relglob_c()),
2423 Box::new(pattern_dir1()),
2424 ),
2425 3,
2426 );
2427 tree.check_matcher(
2428 &UnionMatcher::new(vec![
2429 Box::new(pattern_relglob_c()),
2430 Box::new(pattern_dir1()),
2431 ]),
2432 36,
2433 );
2434 tree.check_matcher(
2435 &DifferenceMatcher::new(
2436 Box::new(pattern_relglob_c()),
2437 Box::new(pattern_dir1()),
2438 ),
2439 11,
2440 );
2441 tree.check_matcher(&file_dir_subdir_b, 1);
2442 tree.check_matcher(&file_abcdfile, 4);
2443 tree.check_matcher(&rootfilesin_dir, 8);
2444 tree.check_matcher(&pattern_filepath_dir_subdir, 1);
2445 tree.check_matcher(&include_dir_subdir, 9);
2446 tree.check_matcher(&more_includematchers[0], 17);
2447 tree.check_matcher(&more_includematchers[1], 25);
2448 tree.check_matcher(&more_includematchers[2], 35);
2449 }
2122 }
2450 }
@@ -1,436 +1,440 b''
1 // files.rs
1 // files.rs
2 //
2 //
3 // Copyright 2019
3 // Copyright 2019
4 // Raphaël Gomès <rgomes@octobus.net>,
4 // Raphaël Gomès <rgomes@octobus.net>,
5 // Yuya Nishihara <yuya@tcha.org>
5 // Yuya Nishihara <yuya@tcha.org>
6 //
6 //
7 // This software may be used and distributed according to the terms of the
7 // This software may be used and distributed according to the terms of the
8 // GNU General Public License version 2 or any later version.
8 // GNU General Public License version 2 or any later version.
9
9
10 //! Functions for fiddling with files.
10 //! Functions for fiddling with files.
11
11
12 use crate::utils::{
12 use crate::utils::{
13 hg_path::{path_to_hg_path_buf, HgPath, HgPathBuf, HgPathError},
13 hg_path::{path_to_hg_path_buf, HgPath, HgPathBuf, HgPathError},
14 path_auditor::PathAuditor,
14 path_auditor::PathAuditor,
15 replace_slice,
15 replace_slice,
16 };
16 };
17 use lazy_static::lazy_static;
17 use lazy_static::lazy_static;
18 use same_file::is_same_file;
18 use same_file::is_same_file;
19 use std::borrow::{Cow, ToOwned};
19 use std::borrow::{Cow, ToOwned};
20 use std::ffi::{OsStr, OsString};
20 use std::ffi::{OsStr, OsString};
21 use std::iter::FusedIterator;
21 use std::iter::FusedIterator;
22 use std::ops::Deref;
22 use std::ops::Deref;
23 use std::path::{Path, PathBuf};
23 use std::path::{Path, PathBuf};
24
24
25 pub fn get_os_str_from_bytes(bytes: &[u8]) -> &OsStr {
25 pub fn get_os_str_from_bytes(bytes: &[u8]) -> &OsStr {
26 let os_str;
26 let os_str;
27 #[cfg(unix)]
27 #[cfg(unix)]
28 {
28 {
29 use std::os::unix::ffi::OsStrExt;
29 use std::os::unix::ffi::OsStrExt;
30 os_str = std::ffi::OsStr::from_bytes(bytes);
30 os_str = std::ffi::OsStr::from_bytes(bytes);
31 }
31 }
32 // TODO Handle other platforms
32 // TODO Handle other platforms
33 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
33 // TODO: convert from WTF8 to Windows MBCS (ANSI encoding).
34 // Perhaps, the return type would have to be Result<PathBuf>.
34 // Perhaps, the return type would have to be Result<PathBuf>.
35 os_str
35 os_str
36 }
36 }
37
37
38 pub fn get_path_from_bytes(bytes: &[u8]) -> &Path {
38 pub fn get_path_from_bytes(bytes: &[u8]) -> &Path {
39 Path::new(get_os_str_from_bytes(bytes))
39 Path::new(get_os_str_from_bytes(bytes))
40 }
40 }
41
41
42 // TODO: need to convert from WTF8 to MBCS bytes on Windows.
42 // TODO: need to convert from WTF8 to MBCS bytes on Windows.
43 // that's why Vec<u8> is returned.
43 // that's why Vec<u8> is returned.
44 #[cfg(unix)]
44 #[cfg(unix)]
45 pub fn get_bytes_from_path(path: impl AsRef<Path>) -> Vec<u8> {
45 pub fn get_bytes_from_path(path: impl AsRef<Path>) -> Vec<u8> {
46 get_bytes_from_os_str(path.as_ref())
46 get_bytes_from_os_str(path.as_ref())
47 }
47 }
48
48
49 #[cfg(unix)]
49 #[cfg(unix)]
50 pub fn get_bytes_from_os_str(str: impl AsRef<OsStr>) -> Vec<u8> {
50 pub fn get_bytes_from_os_str(str: impl AsRef<OsStr>) -> Vec<u8> {
51 use std::os::unix::ffi::OsStrExt;
51 use std::os::unix::ffi::OsStrExt;
52 str.as_ref().as_bytes().to_vec()
52 str.as_ref().as_bytes().to_vec()
53 }
53 }
54
54
55 #[cfg(unix)]
55 #[cfg(unix)]
56 pub fn get_bytes_from_os_string(str: OsString) -> Vec<u8> {
56 pub fn get_bytes_from_os_string(str: OsString) -> Vec<u8> {
57 use std::os::unix::ffi::OsStringExt;
57 use std::os::unix::ffi::OsStringExt;
58 str.into_vec()
58 str.into_vec()
59 }
59 }
60
60
61 /// An iterator over repository path yielding itself and its ancestors.
61 /// An iterator over repository path yielding itself and its ancestors.
62 #[derive(Copy, Clone, Debug)]
62 #[derive(Copy, Clone, Debug)]
63 pub struct Ancestors<'a> {
63 pub struct Ancestors<'a> {
64 next: Option<&'a HgPath>,
64 next: Option<&'a HgPath>,
65 }
65 }
66
66
67 impl<'a> Iterator for Ancestors<'a> {
67 impl<'a> Iterator for Ancestors<'a> {
68 type Item = &'a HgPath;
68 type Item = &'a HgPath;
69
69
70 fn next(&mut self) -> Option<Self::Item> {
70 fn next(&mut self) -> Option<Self::Item> {
71 let next = self.next;
71 let next = self.next;
72 self.next = match self.next {
72 self.next = match self.next {
73 Some(s) if s.is_empty() => None,
73 Some(s) if s.is_empty() => None,
74 Some(s) => {
74 Some(s) => {
75 let p = s.bytes().rposition(|c| *c == b'/').unwrap_or(0);
75 let p = s.bytes().rposition(|c| *c == b'/').unwrap_or(0);
76 Some(HgPath::new(&s.as_bytes()[..p]))
76 Some(HgPath::new(&s.as_bytes()[..p]))
77 }
77 }
78 None => None,
78 None => None,
79 };
79 };
80 next
80 next
81 }
81 }
82 }
82 }
83
83
84 impl<'a> FusedIterator for Ancestors<'a> {}
84 impl<'a> FusedIterator for Ancestors<'a> {}
85
85
86 /// An iterator over repository path yielding itself and its ancestors.
86 /// An iterator over repository path yielding itself and its ancestors.
87 #[derive(Copy, Clone, Debug)]
87 #[derive(Copy, Clone, Debug)]
88 pub(crate) struct AncestorsWithBase<'a> {
88 pub(crate) struct AncestorsWithBase<'a> {
89 next: Option<(&'a HgPath, &'a HgPath)>,
89 next: Option<(&'a HgPath, &'a HgPath)>,
90 }
90 }
91
91
92 impl<'a> Iterator for AncestorsWithBase<'a> {
92 impl<'a> Iterator for AncestorsWithBase<'a> {
93 type Item = (&'a HgPath, &'a HgPath);
93 type Item = (&'a HgPath, &'a HgPath);
94
94
95 fn next(&mut self) -> Option<Self::Item> {
95 fn next(&mut self) -> Option<Self::Item> {
96 let next = self.next;
96 let next = self.next;
97 self.next = match self.next {
97 self.next = match self.next {
98 Some((s, _)) if s.is_empty() => None,
98 Some((s, _)) if s.is_empty() => None,
99 Some((s, _)) => Some(s.split_filename()),
99 Some((s, _)) => Some(s.split_filename()),
100 None => None,
100 None => None,
101 };
101 };
102 next
102 next
103 }
103 }
104 }
104 }
105
105
106 impl<'a> FusedIterator for AncestorsWithBase<'a> {}
106 impl<'a> FusedIterator for AncestorsWithBase<'a> {}
107
107
108 /// Returns an iterator yielding ancestor directories of the given repository
108 /// Returns an iterator yielding ancestor directories of the given repository
109 /// path.
109 /// path.
110 ///
110 ///
111 /// The path is separated by '/', and must not start with '/'.
111 /// The path is separated by '/', and must not start with '/'.
112 ///
112 ///
113 /// The path itself isn't included unless it is b"" (meaning the root
113 /// The path itself isn't included unless it is b"" (meaning the root
114 /// directory.)
114 /// directory.)
115 pub fn find_dirs(path: &HgPath) -> Ancestors {
115 pub fn find_dirs(path: &HgPath) -> Ancestors {
116 let mut dirs = Ancestors { next: Some(path) };
116 let mut dirs = Ancestors { next: Some(path) };
117 if !path.is_empty() {
117 if !path.is_empty() {
118 dirs.next(); // skip itself
118 dirs.next(); // skip itself
119 }
119 }
120 dirs
120 dirs
121 }
121 }
122
122
123 pub fn dir_ancestors(path: &HgPath) -> Ancestors {
124 Ancestors { next: Some(path) }
125 }
126
123 /// Returns an iterator yielding ancestor directories of the given repository
127 /// Returns an iterator yielding ancestor directories of the given repository
124 /// path.
128 /// path.
125 ///
129 ///
126 /// The path is separated by '/', and must not start with '/'.
130 /// The path is separated by '/', and must not start with '/'.
127 ///
131 ///
128 /// The path itself isn't included unless it is b"" (meaning the root
132 /// The path itself isn't included unless it is b"" (meaning the root
129 /// directory.)
133 /// directory.)
130 pub(crate) fn find_dirs_with_base(path: &HgPath) -> AncestorsWithBase {
134 pub(crate) fn find_dirs_with_base(path: &HgPath) -> AncestorsWithBase {
131 let mut dirs = AncestorsWithBase {
135 let mut dirs = AncestorsWithBase {
132 next: Some((path, HgPath::new(b""))),
136 next: Some((path, HgPath::new(b""))),
133 };
137 };
134 if !path.is_empty() {
138 if !path.is_empty() {
135 dirs.next(); // skip itself
139 dirs.next(); // skip itself
136 }
140 }
137 dirs
141 dirs
138 }
142 }
139
143
140 /// TODO more than ASCII?
144 /// TODO more than ASCII?
141 pub fn normalize_case(path: &HgPath) -> HgPathBuf {
145 pub fn normalize_case(path: &HgPath) -> HgPathBuf {
142 #[cfg(windows)] // NTFS compares via upper()
146 #[cfg(windows)] // NTFS compares via upper()
143 return path.to_ascii_uppercase();
147 return path.to_ascii_uppercase();
144 #[cfg(unix)]
148 #[cfg(unix)]
145 path.to_ascii_lowercase()
149 path.to_ascii_lowercase()
146 }
150 }
147
151
148 lazy_static! {
152 lazy_static! {
149 static ref IGNORED_CHARS: Vec<Vec<u8>> = {
153 static ref IGNORED_CHARS: Vec<Vec<u8>> = {
150 [
154 [
151 0x200c, 0x200d, 0x200e, 0x200f, 0x202a, 0x202b, 0x202c, 0x202d,
155 0x200c, 0x200d, 0x200e, 0x200f, 0x202a, 0x202b, 0x202c, 0x202d,
152 0x202e, 0x206a, 0x206b, 0x206c, 0x206d, 0x206e, 0x206f, 0xfeff,
156 0x202e, 0x206a, 0x206b, 0x206c, 0x206d, 0x206e, 0x206f, 0xfeff,
153 ]
157 ]
154 .iter()
158 .iter()
155 .map(|code| {
159 .map(|code| {
156 std::char::from_u32(*code)
160 std::char::from_u32(*code)
157 .unwrap()
161 .unwrap()
158 .encode_utf8(&mut [0; 3])
162 .encode_utf8(&mut [0; 3])
159 .bytes()
163 .bytes()
160 .collect()
164 .collect()
161 })
165 })
162 .collect()
166 .collect()
163 };
167 };
164 }
168 }
165
169
166 fn hfs_ignore_clean(bytes: &[u8]) -> Vec<u8> {
170 fn hfs_ignore_clean(bytes: &[u8]) -> Vec<u8> {
167 let mut buf = bytes.to_owned();
171 let mut buf = bytes.to_owned();
168 let needs_escaping = bytes.iter().any(|b| *b == b'\xe2' || *b == b'\xef');
172 let needs_escaping = bytes.iter().any(|b| *b == b'\xe2' || *b == b'\xef');
169 if needs_escaping {
173 if needs_escaping {
170 for forbidden in IGNORED_CHARS.iter() {
174 for forbidden in IGNORED_CHARS.iter() {
171 replace_slice(&mut buf, forbidden, &[])
175 replace_slice(&mut buf, forbidden, &[])
172 }
176 }
173 buf
177 buf
174 } else {
178 } else {
175 buf
179 buf
176 }
180 }
177 }
181 }
178
182
179 pub fn lower_clean(bytes: &[u8]) -> Vec<u8> {
183 pub fn lower_clean(bytes: &[u8]) -> Vec<u8> {
180 hfs_ignore_clean(&bytes.to_ascii_lowercase())
184 hfs_ignore_clean(&bytes.to_ascii_lowercase())
181 }
185 }
182
186
183 /// Returns the canonical path of `name`, given `cwd` and `root`
187 /// Returns the canonical path of `name`, given `cwd` and `root`
184 pub fn canonical_path(
188 pub fn canonical_path(
185 root: impl AsRef<Path>,
189 root: impl AsRef<Path>,
186 cwd: impl AsRef<Path>,
190 cwd: impl AsRef<Path>,
187 name: impl AsRef<Path>,
191 name: impl AsRef<Path>,
188 ) -> Result<PathBuf, HgPathError> {
192 ) -> Result<PathBuf, HgPathError> {
189 // TODO add missing normalization for other platforms
193 // TODO add missing normalization for other platforms
190 let root = root.as_ref();
194 let root = root.as_ref();
191 let cwd = cwd.as_ref();
195 let cwd = cwd.as_ref();
192 let name = name.as_ref();
196 let name = name.as_ref();
193
197
194 let name = if !name.is_absolute() {
198 let name = if !name.is_absolute() {
195 root.join(cwd).join(name)
199 root.join(cwd).join(name)
196 } else {
200 } else {
197 name.to_owned()
201 name.to_owned()
198 };
202 };
199 let auditor = PathAuditor::new(root);
203 let auditor = PathAuditor::new(root);
200 if name != root && name.starts_with(root) {
204 if name != root && name.starts_with(root) {
201 let name = name.strip_prefix(root).unwrap();
205 let name = name.strip_prefix(root).unwrap();
202 auditor.audit_path(path_to_hg_path_buf(name)?)?;
206 auditor.audit_path(path_to_hg_path_buf(name)?)?;
203 Ok(name.to_owned())
207 Ok(name.to_owned())
204 } else if name == root {
208 } else if name == root {
205 Ok("".into())
209 Ok("".into())
206 } else {
210 } else {
207 // Determine whether `name' is in the hierarchy at or beneath `root',
211 // Determine whether `name' is in the hierarchy at or beneath `root',
208 // by iterating name=name.parent() until it returns `None` (can't
212 // by iterating name=name.parent() until it returns `None` (can't
209 // check name == '/', because that doesn't work on windows).
213 // check name == '/', because that doesn't work on windows).
210 let mut name = name.deref();
214 let mut name = name.deref();
211 let original_name = name.to_owned();
215 let original_name = name.to_owned();
212 loop {
216 loop {
213 let same = is_same_file(name, root).unwrap_or(false);
217 let same = is_same_file(name, root).unwrap_or(false);
214 if same {
218 if same {
215 if name == original_name {
219 if name == original_name {
216 // `name` was actually the same as root (maybe a symlink)
220 // `name` was actually the same as root (maybe a symlink)
217 return Ok("".into());
221 return Ok("".into());
218 }
222 }
219 // `name` is a symlink to root, so `original_name` is under
223 // `name` is a symlink to root, so `original_name` is under
220 // root
224 // root
221 let rel_path = original_name.strip_prefix(name).unwrap();
225 let rel_path = original_name.strip_prefix(name).unwrap();
222 auditor.audit_path(path_to_hg_path_buf(rel_path)?)?;
226 auditor.audit_path(path_to_hg_path_buf(rel_path)?)?;
223 return Ok(rel_path.to_owned());
227 return Ok(rel_path.to_owned());
224 }
228 }
225 name = match name.parent() {
229 name = match name.parent() {
226 None => break,
230 None => break,
227 Some(p) => p,
231 Some(p) => p,
228 };
232 };
229 }
233 }
230 // TODO hint to the user about using --cwd
234 // TODO hint to the user about using --cwd
231 // Bubble up the responsibility to Python for now
235 // Bubble up the responsibility to Python for now
232 Err(HgPathError::NotUnderRoot {
236 Err(HgPathError::NotUnderRoot {
233 path: original_name,
237 path: original_name,
234 root: root.to_owned(),
238 root: root.to_owned(),
235 })
239 })
236 }
240 }
237 }
241 }
238
242
239 /// Returns the representation of the path relative to the current working
243 /// Returns the representation of the path relative to the current working
240 /// directory for display purposes.
244 /// directory for display purposes.
241 ///
245 ///
242 /// `cwd` is a `HgPath`, so it is considered relative to the root directory
246 /// `cwd` is a `HgPath`, so it is considered relative to the root directory
243 /// of the repository.
247 /// of the repository.
244 ///
248 ///
245 /// # Examples
249 /// # Examples
246 ///
250 ///
247 /// ```
251 /// ```
248 /// use hg::utils::hg_path::HgPath;
252 /// use hg::utils::hg_path::HgPath;
249 /// use hg::utils::files::relativize_path;
253 /// use hg::utils::files::relativize_path;
250 /// use std::borrow::Cow;
254 /// use std::borrow::Cow;
251 ///
255 ///
252 /// let file = HgPath::new(b"nested/file");
256 /// let file = HgPath::new(b"nested/file");
253 /// let cwd = HgPath::new(b"");
257 /// let cwd = HgPath::new(b"");
254 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"nested/file"));
258 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"nested/file"));
255 ///
259 ///
256 /// let cwd = HgPath::new(b"nested");
260 /// let cwd = HgPath::new(b"nested");
257 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"file"));
261 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"file"));
258 ///
262 ///
259 /// let cwd = HgPath::new(b"other");
263 /// let cwd = HgPath::new(b"other");
260 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"../nested/file"));
264 /// assert_eq!(relativize_path(file, cwd), Cow::Borrowed(b"../nested/file"));
261 /// ```
265 /// ```
262 pub fn relativize_path(path: &HgPath, cwd: impl AsRef<HgPath>) -> Cow<[u8]> {
266 pub fn relativize_path(path: &HgPath, cwd: impl AsRef<HgPath>) -> Cow<[u8]> {
263 if cwd.as_ref().is_empty() {
267 if cwd.as_ref().is_empty() {
264 Cow::Borrowed(path.as_bytes())
268 Cow::Borrowed(path.as_bytes())
265 } else {
269 } else {
266 // This is not all accurate as to how large `res` will actually be, but
270 // This is not all accurate as to how large `res` will actually be, but
267 // profiling `rhg files` on a large-ish repo shows it’s better than
271 // profiling `rhg files` on a large-ish repo shows it’s better than
268 // starting from a zero-capacity `Vec` and letting `extend` reallocate
272 // starting from a zero-capacity `Vec` and letting `extend` reallocate
269 // repeatedly.
273 // repeatedly.
270 let guesstimate = path.as_bytes().len();
274 let guesstimate = path.as_bytes().len();
271
275
272 let mut res: Vec<u8> = Vec::with_capacity(guesstimate);
276 let mut res: Vec<u8> = Vec::with_capacity(guesstimate);
273 let mut path_iter = path.as_bytes().split(|b| *b == b'/').peekable();
277 let mut path_iter = path.as_bytes().split(|b| *b == b'/').peekable();
274 let mut cwd_iter =
278 let mut cwd_iter =
275 cwd.as_ref().as_bytes().split(|b| *b == b'/').peekable();
279 cwd.as_ref().as_bytes().split(|b| *b == b'/').peekable();
276 loop {
280 loop {
277 match (path_iter.peek(), cwd_iter.peek()) {
281 match (path_iter.peek(), cwd_iter.peek()) {
278 (Some(a), Some(b)) if a == b => (),
282 (Some(a), Some(b)) if a == b => (),
279 _ => break,
283 _ => break,
280 }
284 }
281 path_iter.next();
285 path_iter.next();
282 cwd_iter.next();
286 cwd_iter.next();
283 }
287 }
284 let mut need_sep = false;
288 let mut need_sep = false;
285 for _ in cwd_iter {
289 for _ in cwd_iter {
286 if need_sep {
290 if need_sep {
287 res.extend(b"/")
291 res.extend(b"/")
288 } else {
292 } else {
289 need_sep = true
293 need_sep = true
290 };
294 };
291 res.extend(b"..");
295 res.extend(b"..");
292 }
296 }
293 for c in path_iter {
297 for c in path_iter {
294 if need_sep {
298 if need_sep {
295 res.extend(b"/")
299 res.extend(b"/")
296 } else {
300 } else {
297 need_sep = true
301 need_sep = true
298 };
302 };
299 res.extend(c);
303 res.extend(c);
300 }
304 }
301 Cow::Owned(res)
305 Cow::Owned(res)
302 }
306 }
303 }
307 }
304
308
305 #[cfg(test)]
309 #[cfg(test)]
306 mod tests {
310 mod tests {
307 use super::*;
311 use super::*;
308 use pretty_assertions::assert_eq;
312 use pretty_assertions::assert_eq;
309
313
310 #[test]
314 #[test]
311 fn find_dirs_some() {
315 fn find_dirs_some() {
312 let mut dirs = super::find_dirs(HgPath::new(b"foo/bar/baz"));
316 let mut dirs = super::find_dirs(HgPath::new(b"foo/bar/baz"));
313 assert_eq!(dirs.next(), Some(HgPath::new(b"foo/bar")));
317 assert_eq!(dirs.next(), Some(HgPath::new(b"foo/bar")));
314 assert_eq!(dirs.next(), Some(HgPath::new(b"foo")));
318 assert_eq!(dirs.next(), Some(HgPath::new(b"foo")));
315 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
319 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
316 assert_eq!(dirs.next(), None);
320 assert_eq!(dirs.next(), None);
317 assert_eq!(dirs.next(), None);
321 assert_eq!(dirs.next(), None);
318 }
322 }
319
323
320 #[test]
324 #[test]
321 fn find_dirs_empty() {
325 fn find_dirs_empty() {
322 // looks weird, but mercurial.pathutil.finddirs(b"") yields b""
326 // looks weird, but mercurial.pathutil.finddirs(b"") yields b""
323 let mut dirs = super::find_dirs(HgPath::new(b""));
327 let mut dirs = super::find_dirs(HgPath::new(b""));
324 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
328 assert_eq!(dirs.next(), Some(HgPath::new(b"")));
325 assert_eq!(dirs.next(), None);
329 assert_eq!(dirs.next(), None);
326 assert_eq!(dirs.next(), None);
330 assert_eq!(dirs.next(), None);
327 }
331 }
328
332
329 #[test]
333 #[test]
330 fn test_find_dirs_with_base_some() {
334 fn test_find_dirs_with_base_some() {
331 let mut dirs = super::find_dirs_with_base(HgPath::new(b"foo/bar/baz"));
335 let mut dirs = super::find_dirs_with_base(HgPath::new(b"foo/bar/baz"));
332 assert_eq!(
336 assert_eq!(
333 dirs.next(),
337 dirs.next(),
334 Some((HgPath::new(b"foo/bar"), HgPath::new(b"baz")))
338 Some((HgPath::new(b"foo/bar"), HgPath::new(b"baz")))
335 );
339 );
336 assert_eq!(
340 assert_eq!(
337 dirs.next(),
341 dirs.next(),
338 Some((HgPath::new(b"foo"), HgPath::new(b"bar")))
342 Some((HgPath::new(b"foo"), HgPath::new(b"bar")))
339 );
343 );
340 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b"foo"))));
344 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b"foo"))));
341 assert_eq!(dirs.next(), None);
345 assert_eq!(dirs.next(), None);
342 assert_eq!(dirs.next(), None);
346 assert_eq!(dirs.next(), None);
343 }
347 }
344
348
345 #[test]
349 #[test]
346 fn test_find_dirs_with_base_empty() {
350 fn test_find_dirs_with_base_empty() {
347 let mut dirs = super::find_dirs_with_base(HgPath::new(b""));
351 let mut dirs = super::find_dirs_with_base(HgPath::new(b""));
348 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b""))));
352 assert_eq!(dirs.next(), Some((HgPath::new(b""), HgPath::new(b""))));
349 assert_eq!(dirs.next(), None);
353 assert_eq!(dirs.next(), None);
350 assert_eq!(dirs.next(), None);
354 assert_eq!(dirs.next(), None);
351 }
355 }
352
356
353 #[test]
357 #[test]
354 fn test_canonical_path() {
358 fn test_canonical_path() {
355 let root = Path::new("/repo");
359 let root = Path::new("/repo");
356 let cwd = Path::new("/dir");
360 let cwd = Path::new("/dir");
357 let name = Path::new("filename");
361 let name = Path::new("filename");
358 assert_eq!(
362 assert_eq!(
359 canonical_path(root, cwd, name),
363 canonical_path(root, cwd, name),
360 Err(HgPathError::NotUnderRoot {
364 Err(HgPathError::NotUnderRoot {
361 path: PathBuf::from("/dir/filename"),
365 path: PathBuf::from("/dir/filename"),
362 root: root.to_path_buf()
366 root: root.to_path_buf()
363 })
367 })
364 );
368 );
365
369
366 let root = Path::new("/repo");
370 let root = Path::new("/repo");
367 let cwd = Path::new("/");
371 let cwd = Path::new("/");
368 let name = Path::new("filename");
372 let name = Path::new("filename");
369 assert_eq!(
373 assert_eq!(
370 canonical_path(root, cwd, name),
374 canonical_path(root, cwd, name),
371 Err(HgPathError::NotUnderRoot {
375 Err(HgPathError::NotUnderRoot {
372 path: PathBuf::from("/filename"),
376 path: PathBuf::from("/filename"),
373 root: root.to_path_buf()
377 root: root.to_path_buf()
374 })
378 })
375 );
379 );
376
380
377 let root = Path::new("/repo");
381 let root = Path::new("/repo");
378 let cwd = Path::new("/");
382 let cwd = Path::new("/");
379 let name = Path::new("repo/filename");
383 let name = Path::new("repo/filename");
380 assert_eq!(
384 assert_eq!(
381 canonical_path(root, cwd, name),
385 canonical_path(root, cwd, name),
382 Ok(PathBuf::from("filename"))
386 Ok(PathBuf::from("filename"))
383 );
387 );
384
388
385 let root = Path::new("/repo");
389 let root = Path::new("/repo");
386 let cwd = Path::new("/repo");
390 let cwd = Path::new("/repo");
387 let name = Path::new("filename");
391 let name = Path::new("filename");
388 assert_eq!(
392 assert_eq!(
389 canonical_path(root, cwd, name),
393 canonical_path(root, cwd, name),
390 Ok(PathBuf::from("filename"))
394 Ok(PathBuf::from("filename"))
391 );
395 );
392
396
393 let root = Path::new("/repo");
397 let root = Path::new("/repo");
394 let cwd = Path::new("/repo/subdir");
398 let cwd = Path::new("/repo/subdir");
395 let name = Path::new("filename");
399 let name = Path::new("filename");
396 assert_eq!(
400 assert_eq!(
397 canonical_path(root, cwd, name),
401 canonical_path(root, cwd, name),
398 Ok(PathBuf::from("subdir/filename"))
402 Ok(PathBuf::from("subdir/filename"))
399 );
403 );
400 }
404 }
401
405
402 #[test]
406 #[test]
403 fn test_canonical_path_not_rooted() {
407 fn test_canonical_path_not_rooted() {
404 use std::fs::create_dir;
408 use std::fs::create_dir;
405 use tempfile::tempdir;
409 use tempfile::tempdir;
406
410
407 let base_dir = tempdir().unwrap();
411 let base_dir = tempdir().unwrap();
408 let base_dir_path = base_dir.path();
412 let base_dir_path = base_dir.path();
409 let beneath_repo = base_dir_path.join("a");
413 let beneath_repo = base_dir_path.join("a");
410 let root = base_dir_path.join("a/b");
414 let root = base_dir_path.join("a/b");
411 let out_of_repo = base_dir_path.join("c");
415 let out_of_repo = base_dir_path.join("c");
412 let under_repo_symlink = out_of_repo.join("d");
416 let under_repo_symlink = out_of_repo.join("d");
413
417
414 create_dir(&beneath_repo).unwrap();
418 create_dir(&beneath_repo).unwrap();
415 create_dir(&root).unwrap();
419 create_dir(&root).unwrap();
416
420
417 // TODO make portable
421 // TODO make portable
418 std::os::unix::fs::symlink(&root, &out_of_repo).unwrap();
422 std::os::unix::fs::symlink(&root, &out_of_repo).unwrap();
419
423
420 assert_eq!(
424 assert_eq!(
421 canonical_path(&root, Path::new(""), out_of_repo),
425 canonical_path(&root, Path::new(""), out_of_repo),
422 Ok(PathBuf::from(""))
426 Ok(PathBuf::from(""))
423 );
427 );
424 assert_eq!(
428 assert_eq!(
425 canonical_path(&root, Path::new(""), &beneath_repo),
429 canonical_path(&root, Path::new(""), &beneath_repo),
426 Err(HgPathError::NotUnderRoot {
430 Err(HgPathError::NotUnderRoot {
427 path: beneath_repo,
431 path: beneath_repo,
428 root: root.to_owned()
432 root: root.to_owned()
429 })
433 })
430 );
434 );
431 assert_eq!(
435 assert_eq!(
432 canonical_path(&root, Path::new(""), under_repo_symlink),
436 canonical_path(&root, Path::new(""), under_repo_symlink),
433 Ok(PathBuf::from("d"))
437 Ok(PathBuf::from("d"))
434 );
438 );
435 }
439 }
436 }
440 }
@@ -1,1835 +1,1839 b''
1 #
1 #
2 # This is the mercurial setup script.
2 # This is the mercurial setup script.
3 #
3 #
4 # 'python setup.py install', or
4 # 'python setup.py install', or
5 # 'python setup.py --help' for more options
5 # 'python setup.py --help' for more options
6 import os
6 import os
7
7
8 # Mercurial can't work on 3.6.0 or 3.6.1 due to a bug in % formatting
8 # Mercurial can't work on 3.6.0 or 3.6.1 due to a bug in % formatting
9 # in bytestrings.
9 # in bytestrings.
10 supportedpy = ','.join(
10 supportedpy = ','.join(
11 [
11 [
12 '>=3.6.2',
12 '>=3.6.2',
13 ]
13 ]
14 )
14 )
15
15
16 import sys, platform
16 import sys, platform
17 import sysconfig
17 import sysconfig
18
18
19
19
20 def sysstr(s):
20 def sysstr(s):
21 return s.decode('latin-1')
21 return s.decode('latin-1')
22
22
23
23
24 def eprint(*args, **kwargs):
24 def eprint(*args, **kwargs):
25 kwargs['file'] = sys.stderr
25 kwargs['file'] = sys.stderr
26 print(*args, **kwargs)
26 print(*args, **kwargs)
27
27
28
28
29 import ssl
29 import ssl
30
30
31 # ssl.HAS_TLSv1* are preferred to check support but they were added in Python
31 # ssl.HAS_TLSv1* are preferred to check support but they were added in Python
32 # 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
32 # 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
33 # (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
33 # (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
34 # were defined only if compiled against a OpenSSL version with TLS 1.1 / 1.2
34 # were defined only if compiled against a OpenSSL version with TLS 1.1 / 1.2
35 # support. At the mentioned commit, they were unconditionally defined.
35 # support. At the mentioned commit, they were unconditionally defined.
36 _notset = object()
36 _notset = object()
37 has_tlsv1_1 = getattr(ssl, 'HAS_TLSv1_1', _notset)
37 has_tlsv1_1 = getattr(ssl, 'HAS_TLSv1_1', _notset)
38 if has_tlsv1_1 is _notset:
38 if has_tlsv1_1 is _notset:
39 has_tlsv1_1 = getattr(ssl, 'PROTOCOL_TLSv1_1', _notset) is not _notset
39 has_tlsv1_1 = getattr(ssl, 'PROTOCOL_TLSv1_1', _notset) is not _notset
40 has_tlsv1_2 = getattr(ssl, 'HAS_TLSv1_2', _notset)
40 has_tlsv1_2 = getattr(ssl, 'HAS_TLSv1_2', _notset)
41 if has_tlsv1_2 is _notset:
41 if has_tlsv1_2 is _notset:
42 has_tlsv1_2 = getattr(ssl, 'PROTOCOL_TLSv1_2', _notset) is not _notset
42 has_tlsv1_2 = getattr(ssl, 'PROTOCOL_TLSv1_2', _notset) is not _notset
43 if not (has_tlsv1_1 or has_tlsv1_2):
43 if not (has_tlsv1_1 or has_tlsv1_2):
44 error = """
44 error = """
45 The `ssl` module does not advertise support for TLS 1.1 or TLS 1.2.
45 The `ssl` module does not advertise support for TLS 1.1 or TLS 1.2.
46 Please make sure that your Python installation was compiled against an OpenSSL
46 Please make sure that your Python installation was compiled against an OpenSSL
47 version enabling these features (likely this requires the OpenSSL version to
47 version enabling these features (likely this requires the OpenSSL version to
48 be at least 1.0.1).
48 be at least 1.0.1).
49 """
49 """
50 print(error, file=sys.stderr)
50 print(error, file=sys.stderr)
51 sys.exit(1)
51 sys.exit(1)
52
52
53 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
53 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
54
54
55 # Solaris Python packaging brain damage
55 # Solaris Python packaging brain damage
56 try:
56 try:
57 import hashlib
57 import hashlib
58
58
59 sha = hashlib.sha1()
59 sha = hashlib.sha1()
60 except ImportError:
60 except ImportError:
61 try:
61 try:
62 import sha
62 import sha
63
63
64 sha.sha # silence unused import warning
64 sha.sha # silence unused import warning
65 except ImportError:
65 except ImportError:
66 raise SystemExit(
66 raise SystemExit(
67 "Couldn't import standard hashlib (incomplete Python install)."
67 "Couldn't import standard hashlib (incomplete Python install)."
68 )
68 )
69
69
70 try:
70 try:
71 import zlib
71 import zlib
72
72
73 zlib.compressobj # silence unused import warning
73 zlib.compressobj # silence unused import warning
74 except ImportError:
74 except ImportError:
75 raise SystemExit(
75 raise SystemExit(
76 "Couldn't import standard zlib (incomplete Python install)."
76 "Couldn't import standard zlib (incomplete Python install)."
77 )
77 )
78
78
79 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
79 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
80 isironpython = False
80 isironpython = False
81 try:
81 try:
82 isironpython = (
82 isironpython = (
83 platform.python_implementation().lower().find("ironpython") != -1
83 platform.python_implementation().lower().find("ironpython") != -1
84 )
84 )
85 except AttributeError:
85 except AttributeError:
86 pass
86 pass
87
87
88 if isironpython:
88 if isironpython:
89 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
89 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
90 else:
90 else:
91 try:
91 try:
92 import bz2
92 import bz2
93
93
94 bz2.BZ2Compressor # silence unused import warning
94 bz2.BZ2Compressor # silence unused import warning
95 except ImportError:
95 except ImportError:
96 raise SystemExit(
96 raise SystemExit(
97 "Couldn't import standard bz2 (incomplete Python install)."
97 "Couldn't import standard bz2 (incomplete Python install)."
98 )
98 )
99
99
100 ispypy = "PyPy" in sys.version
100 ispypy = "PyPy" in sys.version
101
101
102 import ctypes
102 import ctypes
103 import stat, subprocess, time
103 import stat, subprocess, time
104 import re
104 import re
105 import shutil
105 import shutil
106 import tempfile
106 import tempfile
107
107
108 # We have issues with setuptools on some platforms and builders. Until
108 # We have issues with setuptools on some platforms and builders. Until
109 # those are resolved, setuptools is opt-in except for platforms where
109 # those are resolved, setuptools is opt-in except for platforms where
110 # we don't have issues.
110 # we don't have issues.
111 issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
111 issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
112 if issetuptools:
112 if issetuptools:
113 from setuptools import setup
113 from setuptools import setup
114 else:
114 else:
115 try:
115 try:
116 from distutils.core import setup
116 from distutils.core import setup
117 except ModuleNotFoundError:
117 except ModuleNotFoundError:
118 from setuptools import setup
118 from setuptools import setup
119 from distutils.ccompiler import new_compiler
119 from distutils.ccompiler import new_compiler
120 from distutils.core import Command, Extension
120 from distutils.core import Command, Extension
121 from distutils.dist import Distribution
121 from distutils.dist import Distribution
122 from distutils.command.build import build
122 from distutils.command.build import build
123 from distutils.command.build_ext import build_ext
123 from distutils.command.build_ext import build_ext
124 from distutils.command.build_py import build_py
124 from distutils.command.build_py import build_py
125 from distutils.command.build_scripts import build_scripts
125 from distutils.command.build_scripts import build_scripts
126 from distutils.command.install import install
126 from distutils.command.install import install
127 from distutils.command.install_lib import install_lib
127 from distutils.command.install_lib import install_lib
128 from distutils.command.install_scripts import install_scripts
128 from distutils.command.install_scripts import install_scripts
129 from distutils import log
129 from distutils import log
130 from distutils.spawn import spawn, find_executable
130 from distutils.spawn import spawn, find_executable
131 from distutils import file_util
131 from distutils import file_util
132 from distutils.errors import (
132 from distutils.errors import (
133 CCompilerError,
133 CCompilerError,
134 DistutilsError,
134 DistutilsError,
135 DistutilsExecError,
135 DistutilsExecError,
136 )
136 )
137 from distutils.sysconfig import get_python_inc
137 from distutils.sysconfig import get_python_inc
138
138
139
139
140 def write_if_changed(path, content):
140 def write_if_changed(path, content):
141 """Write content to a file iff the content hasn't changed."""
141 """Write content to a file iff the content hasn't changed."""
142 if os.path.exists(path):
142 if os.path.exists(path):
143 with open(path, 'rb') as fh:
143 with open(path, 'rb') as fh:
144 current = fh.read()
144 current = fh.read()
145 else:
145 else:
146 current = b''
146 current = b''
147
147
148 if current != content:
148 if current != content:
149 with open(path, 'wb') as fh:
149 with open(path, 'wb') as fh:
150 fh.write(content)
150 fh.write(content)
151
151
152
152
153 scripts = ['hg']
153 scripts = ['hg']
154 if os.name == 'nt':
154 if os.name == 'nt':
155 # We remove hg.bat if we are able to build hg.exe.
155 # We remove hg.bat if we are able to build hg.exe.
156 scripts.append('contrib/win32/hg.bat')
156 scripts.append('contrib/win32/hg.bat')
157
157
158
158
159 def cancompile(cc, code):
159 def cancompile(cc, code):
160 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
160 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
161 devnull = oldstderr = None
161 devnull = oldstderr = None
162 try:
162 try:
163 fname = os.path.join(tmpdir, 'testcomp.c')
163 fname = os.path.join(tmpdir, 'testcomp.c')
164 f = open(fname, 'w')
164 f = open(fname, 'w')
165 f.write(code)
165 f.write(code)
166 f.close()
166 f.close()
167 # Redirect stderr to /dev/null to hide any error messages
167 # Redirect stderr to /dev/null to hide any error messages
168 # from the compiler.
168 # from the compiler.
169 # This will have to be changed if we ever have to check
169 # This will have to be changed if we ever have to check
170 # for a function on Windows.
170 # for a function on Windows.
171 devnull = open('/dev/null', 'w')
171 devnull = open('/dev/null', 'w')
172 oldstderr = os.dup(sys.stderr.fileno())
172 oldstderr = os.dup(sys.stderr.fileno())
173 os.dup2(devnull.fileno(), sys.stderr.fileno())
173 os.dup2(devnull.fileno(), sys.stderr.fileno())
174 objects = cc.compile([fname], output_dir=tmpdir)
174 objects = cc.compile([fname], output_dir=tmpdir)
175 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
175 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
176 return True
176 return True
177 except Exception:
177 except Exception:
178 return False
178 return False
179 finally:
179 finally:
180 if oldstderr is not None:
180 if oldstderr is not None:
181 os.dup2(oldstderr, sys.stderr.fileno())
181 os.dup2(oldstderr, sys.stderr.fileno())
182 if devnull is not None:
182 if devnull is not None:
183 devnull.close()
183 devnull.close()
184 shutil.rmtree(tmpdir)
184 shutil.rmtree(tmpdir)
185
185
186
186
187 # simplified version of distutils.ccompiler.CCompiler.has_function
187 # simplified version of distutils.ccompiler.CCompiler.has_function
188 # that actually removes its temporary files.
188 # that actually removes its temporary files.
189 def hasfunction(cc, funcname):
189 def hasfunction(cc, funcname):
190 code = 'int main(void) { %s(); }\n' % funcname
190 code = 'int main(void) { %s(); }\n' % funcname
191 return cancompile(cc, code)
191 return cancompile(cc, code)
192
192
193
193
194 def hasheader(cc, headername):
194 def hasheader(cc, headername):
195 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
195 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
196 return cancompile(cc, code)
196 return cancompile(cc, code)
197
197
198
198
199 # py2exe needs to be installed to work
199 # py2exe needs to be installed to work
200 try:
200 try:
201 import py2exe
201 import py2exe
202
202
203 py2exe.patch_distutils()
203 py2exe.patch_distutils()
204 py2exeloaded = True
204 py2exeloaded = True
205 # import py2exe's patched Distribution class
205 # import py2exe's patched Distribution class
206 from distutils.core import Distribution
206 from distutils.core import Distribution
207 except ImportError:
207 except ImportError:
208 py2exeloaded = False
208 py2exeloaded = False
209
209
210
210
211 def runcmd(cmd, env, cwd=None):
211 def runcmd(cmd, env, cwd=None):
212 p = subprocess.Popen(
212 p = subprocess.Popen(
213 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
213 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
214 )
214 )
215 out, err = p.communicate()
215 out, err = p.communicate()
216 return p.returncode, out, err
216 return p.returncode, out, err
217
217
218
218
219 class hgcommand:
219 class hgcommand:
220 def __init__(self, cmd, env):
220 def __init__(self, cmd, env):
221 self.cmd = cmd
221 self.cmd = cmd
222 self.env = env
222 self.env = env
223
223
224 def __repr__(self):
224 def __repr__(self):
225 return f"<hgcommand cmd={self.cmd} env={self.env}>"
225 return f"<hgcommand cmd={self.cmd} env={self.env}>"
226
226
227 def run(self, args):
227 def run(self, args):
228 cmd = self.cmd + args
228 cmd = self.cmd + args
229 returncode, out, err = runcmd(cmd, self.env)
229 returncode, out, err = runcmd(cmd, self.env)
230 err = filterhgerr(err)
230 err = filterhgerr(err)
231 if err:
231 if err:
232 print("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
232 print("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
233 print(err, file=sys.stderr)
233 print(err, file=sys.stderr)
234 if returncode != 0:
234 if returncode != 0:
235 print(
236 "non zero-return '%s': %d" % (' '.join(cmd), returncode),
237 file=sys.stderr,
238 )
235 return b''
239 return b''
236 return out
240 return out
237
241
238
242
239 def filterhgerr(err):
243 def filterhgerr(err):
240 # If root is executing setup.py, but the repository is owned by
244 # If root is executing setup.py, but the repository is owned by
241 # another user (as in "sudo python setup.py install") we will get
245 # another user (as in "sudo python setup.py install") we will get
242 # trust warnings since the .hg/hgrc file is untrusted. That is
246 # trust warnings since the .hg/hgrc file is untrusted. That is
243 # fine, we don't want to load it anyway. Python may warn about
247 # fine, we don't want to load it anyway. Python may warn about
244 # a missing __init__.py in mercurial/locale, we also ignore that.
248 # a missing __init__.py in mercurial/locale, we also ignore that.
245 err = [
249 err = [
246 e
250 e
247 for e in err.splitlines()
251 for e in err.splitlines()
248 if (
252 if (
249 not e.startswith(b'not trusting file')
253 not e.startswith(b'not trusting file')
250 and not e.startswith(b'warning: Not importing')
254 and not e.startswith(b'warning: Not importing')
251 and not e.startswith(b'obsolete feature not enabled')
255 and not e.startswith(b'obsolete feature not enabled')
252 and not e.startswith(b'*** failed to import extension')
256 and not e.startswith(b'*** failed to import extension')
253 and not e.startswith(b'devel-warn:')
257 and not e.startswith(b'devel-warn:')
254 and not (
258 and not (
255 e.startswith(b'(third party extension')
259 e.startswith(b'(third party extension')
256 and e.endswith(b'or newer of Mercurial; disabling)')
260 and e.endswith(b'or newer of Mercurial; disabling)')
257 )
261 )
258 )
262 )
259 ]
263 ]
260 return b'\n'.join(b' ' + e for e in err)
264 return b'\n'.join(b' ' + e for e in err)
261
265
262
266
263 def findhg():
267 def findhg():
264 """Try to figure out how we should invoke hg for examining the local
268 """Try to figure out how we should invoke hg for examining the local
265 repository contents.
269 repository contents.
266
270
267 Returns an hgcommand object."""
271 Returns an hgcommand object."""
268 # By default, prefer the "hg" command in the user's path. This was
272 # By default, prefer the "hg" command in the user's path. This was
269 # presumably the hg command that the user used to create this repository.
273 # presumably the hg command that the user used to create this repository.
270 #
274 #
271 # This repository may require extensions or other settings that would not
275 # This repository may require extensions or other settings that would not
272 # be enabled by running the hg script directly from this local repository.
276 # be enabled by running the hg script directly from this local repository.
273 hgenv = os.environ.copy()
277 hgenv = os.environ.copy()
274 # Use HGPLAIN to disable hgrc settings that would change output formatting,
278 # Use HGPLAIN to disable hgrc settings that would change output formatting,
275 # and disable localization for the same reasons.
279 # and disable localization for the same reasons.
276 hgenv['HGPLAIN'] = '1'
280 hgenv['HGPLAIN'] = '1'
277 hgenv['LANGUAGE'] = 'C'
281 hgenv['LANGUAGE'] = 'C'
278 hgcmd = ['hg']
282 hgcmd = ['hg']
279 # Run a simple "hg log" command just to see if using hg from the user's
283 # Run a simple "hg log" command just to see if using hg from the user's
280 # path works and can successfully interact with this repository. Windows
284 # path works and can successfully interact with this repository. Windows
281 # gives precedence to hg.exe in the current directory, so fall back to the
285 # gives precedence to hg.exe in the current directory, so fall back to the
282 # python invocation of local hg, where pythonXY.dll can always be found.
286 # python invocation of local hg, where pythonXY.dll can always be found.
283 check_cmd = ['log', '-r.', '-Ttest']
287 check_cmd = ['log', '-r.', '-Ttest']
284 attempts = []
288 attempts = []
285
289
286 def attempt(cmd, env):
290 def attempt(cmd, env):
287 try:
291 try:
288 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
292 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
289 res = (True, retcode, out, err)
293 res = (True, retcode, out, err)
290 if retcode == 0 and not filterhgerr(err):
294 if retcode == 0 and not filterhgerr(err):
291 return True
295 return True
292 except EnvironmentError as e:
296 except EnvironmentError as e:
293 res = (False, e)
297 res = (False, e)
294 attempts.append((cmd, res))
298 attempts.append((cmd, res))
295 return False
299 return False
296
300
297 if os.name != 'nt' or not os.path.exists("hg.exe"):
301 if os.name != 'nt' or not os.path.exists("hg.exe"):
298 if attempt(hgcmd + check_cmd, hgenv):
302 if attempt(hgcmd + check_cmd, hgenv):
299 return hgcommand(hgcmd, hgenv)
303 return hgcommand(hgcmd, hgenv)
300
304
301 # Fall back to trying the local hg installation (pure python)
305 # Fall back to trying the local hg installation (pure python)
302 repo_hg = os.path.join(os.path.dirname(__file__), 'hg')
306 repo_hg = os.path.join(os.path.dirname(__file__), 'hg')
303 hgenv = localhgenv()
307 hgenv = localhgenv()
304 hgcmd = [sys.executable, repo_hg]
308 hgcmd = [sys.executable, repo_hg]
305 if attempt(hgcmd + check_cmd, hgenv):
309 if attempt(hgcmd + check_cmd, hgenv):
306 return hgcommand(hgcmd, hgenv)
310 return hgcommand(hgcmd, hgenv)
307 # Fall back to trying the local hg installation (whatever we can)
311 # Fall back to trying the local hg installation (whatever we can)
308 hgenv = localhgenv(pure_python=False)
312 hgenv = localhgenv(pure_python=False)
309 hgcmd = [sys.executable, repo_hg]
313 hgcmd = [sys.executable, repo_hg]
310 if attempt(hgcmd + check_cmd, hgenv):
314 if attempt(hgcmd + check_cmd, hgenv):
311 return hgcommand(hgcmd, hgenv)
315 return hgcommand(hgcmd, hgenv)
312
316
313 eprint("/!\\")
317 eprint("/!\\")
314 eprint(r"/!\ Unable to find a working hg binary")
318 eprint(r"/!\ Unable to find a working hg binary")
315 eprint(r"/!\ Version cannot be extracted from the repository")
319 eprint(r"/!\ Version cannot be extracted from the repository")
316 eprint(r"/!\ Re-run the setup once a first version is built")
320 eprint(r"/!\ Re-run the setup once a first version is built")
317 eprint(r"/!\ Attempts:")
321 eprint(r"/!\ Attempts:")
318 for i, e in enumerate(attempts):
322 for i, e in enumerate(attempts):
319 eprint(r"/!\ attempt #%d:" % (i))
323 eprint(r"/!\ attempt #%d:" % (i))
320 eprint(r"/!\ cmd: ", e[0])
324 eprint(r"/!\ cmd: ", e[0])
321 res = e[1]
325 res = e[1]
322 if res[0]:
326 if res[0]:
323 eprint(r"/!\ return code:", res[1])
327 eprint(r"/!\ return code:", res[1])
324 eprint("/!\\ std output:\n%s" % (res[2].decode()), end="")
328 eprint("/!\\ std output:\n%s" % (res[2].decode()), end="")
325 eprint("/!\\ std error:\n%s" % (res[3].decode()), end="")
329 eprint("/!\\ std error:\n%s" % (res[3].decode()), end="")
326 else:
330 else:
327 eprint(r"/!\ exception: ", res[1])
331 eprint(r"/!\ exception: ", res[1])
328 return None
332 return None
329
333
330
334
331 def localhgenv(pure_python=True):
335 def localhgenv(pure_python=True):
332 """Get an environment dictionary to use for invoking or importing
336 """Get an environment dictionary to use for invoking or importing
333 mercurial from the local repository."""
337 mercurial from the local repository."""
334 # Execute hg out of this directory with a custom environment which takes
338 # Execute hg out of this directory with a custom environment which takes
335 # care to not use any hgrc files and do no localization.
339 # care to not use any hgrc files and do no localization.
336 env = {
340 env = {
337 'HGRCPATH': '',
341 'HGRCPATH': '',
338 'LANGUAGE': 'C',
342 'LANGUAGE': 'C',
339 'PATH': '',
343 'PATH': '',
340 } # make pypi modules that use os.environ['PATH'] happy
344 } # make pypi modules that use os.environ['PATH'] happy
341 if pure_python:
345 if pure_python:
342 env['HGMODULEPOLICY'] = 'py'
346 env['HGMODULEPOLICY'] = 'py'
343 if 'LD_LIBRARY_PATH' in os.environ:
347 if 'LD_LIBRARY_PATH' in os.environ:
344 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
348 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
345 if 'SystemRoot' in os.environ:
349 if 'SystemRoot' in os.environ:
346 # SystemRoot is required by Windows to load various DLLs. See:
350 # SystemRoot is required by Windows to load various DLLs. See:
347 # https://bugs.python.org/issue13524#msg148850
351 # https://bugs.python.org/issue13524#msg148850
348 env['SystemRoot'] = os.environ['SystemRoot']
352 env['SystemRoot'] = os.environ['SystemRoot']
349 return env
353 return env
350
354
351
355
352 version = ''
356 version = ''
353
357
354
358
355 def _try_get_version():
359 def _try_get_version():
356 hg = findhg()
360 hg = findhg()
357 if hg is None:
361 if hg is None:
358 return ''
362 return ''
359 hgid = None
363 hgid = None
360 numerictags = []
364 numerictags = []
361 cmd = ['log', '-r', '.', '--template', '{tags}\n']
365 cmd = ['log', '-r', '.', '--template', '{tags}\n']
362 pieces = sysstr(hg.run(cmd)).split()
366 pieces = sysstr(hg.run(cmd)).split()
363 numerictags = [t for t in pieces if t[0:1].isdigit()]
367 numerictags = [t for t in pieces if t[0:1].isdigit()]
364 hgid = sysstr(hg.run(['id', '-i'])).strip()
368 hgid = sysstr(hg.run(['id', '-i'])).strip()
365 if hgid.count('+') == 2:
369 if hgid.count('+') == 2:
366 hgid = hgid.replace("+", ".", 1)
370 hgid = hgid.replace("+", ".", 1)
367 if not hgid:
371 if not hgid:
368 eprint("/!\\")
372 eprint("/!\\")
369 eprint(r"/!\ Unable to determine hg version from local repository")
373 eprint(r"/!\ Unable to determine hg version from local repository")
370 eprint(r"/!\ Failed to retrieve current revision tags")
374 eprint(r"/!\ Failed to retrieve current revision tags")
371 return ''
375 return ''
372 if numerictags: # tag(s) found
376 if numerictags: # tag(s) found
373 version = numerictags[-1]
377 version = numerictags[-1]
374 if hgid.endswith('+'): # propagate the dirty status to the tag
378 if hgid.endswith('+'): # propagate the dirty status to the tag
375 version += '+'
379 version += '+'
376 else: # no tag found on the checked out revision
380 else: # no tag found on the checked out revision
377 ltagcmd = ['log', '--rev', 'wdir()', '--template', '{latesttag}']
381 ltagcmd = ['log', '--rev', 'wdir()', '--template', '{latesttag}']
378 ltag = sysstr(hg.run(ltagcmd))
382 ltag = sysstr(hg.run(ltagcmd))
379 if not ltag:
383 if not ltag:
380 eprint("/!\\")
384 eprint("/!\\")
381 eprint(r"/!\ Unable to determine hg version from local repository")
385 eprint(r"/!\ Unable to determine hg version from local repository")
382 eprint(
386 eprint(
383 r"/!\ Failed to retrieve current revision distance to lated tag"
387 r"/!\ Failed to retrieve current revision distance to lated tag"
384 )
388 )
385 return ''
389 return ''
386 changessincecmd = [
390 changessincecmd = [
387 'log',
391 'log',
388 '-T',
392 '-T',
389 'x\n',
393 'x\n',
390 '-r',
394 '-r',
391 "only(parents(),'%s')" % ltag,
395 "only(parents(),'%s')" % ltag,
392 ]
396 ]
393 changessince = len(hg.run(changessincecmd).splitlines())
397 changessince = len(hg.run(changessincecmd).splitlines())
394 version = '%s+hg%s.%s' % (ltag, changessince, hgid)
398 version = '%s+hg%s.%s' % (ltag, changessince, hgid)
395 if version.endswith('+'):
399 if version.endswith('+'):
396 version = version[:-1] + 'local' + time.strftime('%Y%m%d')
400 version = version[:-1] + 'local' + time.strftime('%Y%m%d')
397 return version
401 return version
398
402
399
403
400 if os.path.isdir('.hg'):
404 if os.path.isdir('.hg'):
401 version = _try_get_version()
405 version = _try_get_version()
402 elif os.path.exists('.hg_archival.txt'):
406 elif os.path.exists('.hg_archival.txt'):
403 kw = dict(
407 kw = dict(
404 [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
408 [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
405 )
409 )
406 if 'tag' in kw:
410 if 'tag' in kw:
407 version = kw['tag']
411 version = kw['tag']
408 elif 'latesttag' in kw:
412 elif 'latesttag' in kw:
409 if 'changessincelatesttag' in kw:
413 if 'changessincelatesttag' in kw:
410 version = (
414 version = (
411 '%(latesttag)s+hg%(changessincelatesttag)s.%(node).12s' % kw
415 '%(latesttag)s+hg%(changessincelatesttag)s.%(node).12s' % kw
412 )
416 )
413 else:
417 else:
414 version = '%(latesttag)s+hg%(latesttagdistance)s.%(node).12s' % kw
418 version = '%(latesttag)s+hg%(latesttagdistance)s.%(node).12s' % kw
415 else:
419 else:
416 version = '0+hg' + kw.get('node', '')[:12]
420 version = '0+hg' + kw.get('node', '')[:12]
417 elif os.path.exists('mercurial/__version__.py'):
421 elif os.path.exists('mercurial/__version__.py'):
418 with open('mercurial/__version__.py') as f:
422 with open('mercurial/__version__.py') as f:
419 data = f.read()
423 data = f.read()
420 version = re.search('version = b"(.*)"', data).group(1)
424 version = re.search('version = b"(.*)"', data).group(1)
421 if not version:
425 if not version:
422 if os.environ.get("MERCURIAL_SETUP_MAKE_LOCAL") == "1":
426 if os.environ.get("MERCURIAL_SETUP_MAKE_LOCAL") == "1":
423 version = "0.0+0"
427 version = "0.0+0"
424 eprint("/!\\")
428 eprint("/!\\")
425 eprint(r"/!\ Using '0.0+0' as the default version")
429 eprint(r"/!\ Using '0.0+0' as the default version")
426 eprint(r"/!\ Re-run make local once that first version is built")
430 eprint(r"/!\ Re-run make local once that first version is built")
427 eprint("/!\\")
431 eprint("/!\\")
428 else:
432 else:
429 eprint("/!\\")
433 eprint("/!\\")
430 eprint(r"/!\ Could not determine the Mercurial version")
434 eprint(r"/!\ Could not determine the Mercurial version")
431 eprint(r"/!\ You need to build a local version first")
435 eprint(r"/!\ You need to build a local version first")
432 eprint(r"/!\ Run `make local` and try again")
436 eprint(r"/!\ Run `make local` and try again")
433 eprint("/!\\")
437 eprint("/!\\")
434 msg = "Run `make local` first to get a working local version"
438 msg = "Run `make local` first to get a working local version"
435 raise SystemExit(msg)
439 raise SystemExit(msg)
436
440
437 versionb = version
441 versionb = version
438 if not isinstance(versionb, bytes):
442 if not isinstance(versionb, bytes):
439 versionb = versionb.encode('ascii')
443 versionb = versionb.encode('ascii')
440
444
441 write_if_changed(
445 write_if_changed(
442 'mercurial/__version__.py',
446 'mercurial/__version__.py',
443 b''.join(
447 b''.join(
444 [
448 [
445 b'# this file is autogenerated by setup.py\n'
449 b'# this file is autogenerated by setup.py\n'
446 b'version = b"%s"\n' % versionb,
450 b'version = b"%s"\n' % versionb,
447 ]
451 ]
448 ),
452 ),
449 )
453 )
450
454
451
455
452 class hgbuild(build):
456 class hgbuild(build):
453 # Insert hgbuildmo first so that files in mercurial/locale/ are found
457 # Insert hgbuildmo first so that files in mercurial/locale/ are found
454 # when build_py is run next.
458 # when build_py is run next.
455 sub_commands = [('build_mo', None)] + build.sub_commands
459 sub_commands = [('build_mo', None)] + build.sub_commands
456
460
457
461
458 class hgbuildmo(build):
462 class hgbuildmo(build):
459
463
460 description = "build translations (.mo files)"
464 description = "build translations (.mo files)"
461
465
462 def run(self):
466 def run(self):
463 if not find_executable('msgfmt'):
467 if not find_executable('msgfmt'):
464 self.warn(
468 self.warn(
465 "could not find msgfmt executable, no translations "
469 "could not find msgfmt executable, no translations "
466 "will be built"
470 "will be built"
467 )
471 )
468 return
472 return
469
473
470 podir = 'i18n'
474 podir = 'i18n'
471 if not os.path.isdir(podir):
475 if not os.path.isdir(podir):
472 self.warn("could not find %s/ directory" % podir)
476 self.warn("could not find %s/ directory" % podir)
473 return
477 return
474
478
475 join = os.path.join
479 join = os.path.join
476 for po in os.listdir(podir):
480 for po in os.listdir(podir):
477 if not po.endswith('.po'):
481 if not po.endswith('.po'):
478 continue
482 continue
479 pofile = join(podir, po)
483 pofile = join(podir, po)
480 modir = join('locale', po[:-3], 'LC_MESSAGES')
484 modir = join('locale', po[:-3], 'LC_MESSAGES')
481 mofile = join(modir, 'hg.mo')
485 mofile = join(modir, 'hg.mo')
482 mobuildfile = join('mercurial', mofile)
486 mobuildfile = join('mercurial', mofile)
483 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
487 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
484 if sys.platform != 'sunos5':
488 if sys.platform != 'sunos5':
485 # msgfmt on Solaris does not know about -c
489 # msgfmt on Solaris does not know about -c
486 cmd.append('-c')
490 cmd.append('-c')
487 self.mkpath(join('mercurial', modir))
491 self.mkpath(join('mercurial', modir))
488 self.make_file([pofile], mobuildfile, spawn, (cmd,))
492 self.make_file([pofile], mobuildfile, spawn, (cmd,))
489
493
490
494
491 class hgdist(Distribution):
495 class hgdist(Distribution):
492 pure = False
496 pure = False
493 rust = False
497 rust = False
494 no_rust = False
498 no_rust = False
495 cffi = ispypy
499 cffi = ispypy
496
500
497 global_options = Distribution.global_options + [
501 global_options = Distribution.global_options + [
498 ('pure', None, "use pure (slow) Python code instead of C extensions"),
502 ('pure', None, "use pure (slow) Python code instead of C extensions"),
499 ('rust', None, "use Rust extensions additionally to C extensions"),
503 ('rust', None, "use Rust extensions additionally to C extensions"),
500 (
504 (
501 'no-rust',
505 'no-rust',
502 None,
506 None,
503 "do not use Rust extensions additionally to C extensions",
507 "do not use Rust extensions additionally to C extensions",
504 ),
508 ),
505 ]
509 ]
506
510
507 negative_opt = Distribution.negative_opt.copy()
511 negative_opt = Distribution.negative_opt.copy()
508 boolean_options = ['pure', 'rust', 'no-rust']
512 boolean_options = ['pure', 'rust', 'no-rust']
509 negative_opt['no-rust'] = 'rust'
513 negative_opt['no-rust'] = 'rust'
510
514
511 def _set_command_options(self, command_obj, option_dict=None):
515 def _set_command_options(self, command_obj, option_dict=None):
512 # Not all distutils versions in the wild have boolean_options.
516 # Not all distutils versions in the wild have boolean_options.
513 # This should be cleaned up when we're Python 3 only.
517 # This should be cleaned up when we're Python 3 only.
514 command_obj.boolean_options = (
518 command_obj.boolean_options = (
515 getattr(command_obj, 'boolean_options', []) + self.boolean_options
519 getattr(command_obj, 'boolean_options', []) + self.boolean_options
516 )
520 )
517 return Distribution._set_command_options(
521 return Distribution._set_command_options(
518 self, command_obj, option_dict=option_dict
522 self, command_obj, option_dict=option_dict
519 )
523 )
520
524
521 def parse_command_line(self):
525 def parse_command_line(self):
522 ret = Distribution.parse_command_line(self)
526 ret = Distribution.parse_command_line(self)
523 if not (self.rust or self.no_rust):
527 if not (self.rust or self.no_rust):
524 hgrustext = os.environ.get('HGWITHRUSTEXT')
528 hgrustext = os.environ.get('HGWITHRUSTEXT')
525 # TODO record it for proper rebuild upon changes
529 # TODO record it for proper rebuild upon changes
526 # (see mercurial/__modulepolicy__.py)
530 # (see mercurial/__modulepolicy__.py)
527 if hgrustext != 'cpython' and hgrustext is not None:
531 if hgrustext != 'cpython' and hgrustext is not None:
528 if hgrustext:
532 if hgrustext:
529 msg = 'unknown HGWITHRUSTEXT value: %s' % hgrustext
533 msg = 'unknown HGWITHRUSTEXT value: %s' % hgrustext
530 print(msg, file=sys.stderr)
534 print(msg, file=sys.stderr)
531 hgrustext = None
535 hgrustext = None
532 self.rust = hgrustext is not None
536 self.rust = hgrustext is not None
533 self.no_rust = not self.rust
537 self.no_rust = not self.rust
534 return ret
538 return ret
535
539
536 def has_ext_modules(self):
540 def has_ext_modules(self):
537 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
541 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
538 # too late for some cases
542 # too late for some cases
539 return not self.pure and Distribution.has_ext_modules(self)
543 return not self.pure and Distribution.has_ext_modules(self)
540
544
541
545
542 # This is ugly as a one-liner. So use a variable.
546 # This is ugly as a one-liner. So use a variable.
543 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
547 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
544 buildextnegops['no-zstd'] = 'zstd'
548 buildextnegops['no-zstd'] = 'zstd'
545 buildextnegops['no-rust'] = 'rust'
549 buildextnegops['no-rust'] = 'rust'
546
550
547
551
548 class hgbuildext(build_ext):
552 class hgbuildext(build_ext):
549 user_options = build_ext.user_options + [
553 user_options = build_ext.user_options + [
550 ('zstd', None, 'compile zstd bindings [default]'),
554 ('zstd', None, 'compile zstd bindings [default]'),
551 ('no-zstd', None, 'do not compile zstd bindings'),
555 ('no-zstd', None, 'do not compile zstd bindings'),
552 (
556 (
553 'rust',
557 'rust',
554 None,
558 None,
555 'compile Rust extensions if they are in use '
559 'compile Rust extensions if they are in use '
556 '(requires Cargo) [default]',
560 '(requires Cargo) [default]',
557 ),
561 ),
558 ('no-rust', None, 'do not compile Rust extensions'),
562 ('no-rust', None, 'do not compile Rust extensions'),
559 ]
563 ]
560
564
561 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
565 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
562 negative_opt = buildextnegops
566 negative_opt = buildextnegops
563
567
564 def initialize_options(self):
568 def initialize_options(self):
565 self.zstd = True
569 self.zstd = True
566 self.rust = True
570 self.rust = True
567
571
568 return build_ext.initialize_options(self)
572 return build_ext.initialize_options(self)
569
573
570 def finalize_options(self):
574 def finalize_options(self):
571 # Unless overridden by the end user, build extensions in parallel.
575 # Unless overridden by the end user, build extensions in parallel.
572 # Only influences behavior on Python 3.5+.
576 # Only influences behavior on Python 3.5+.
573 if getattr(self, 'parallel', None) is None:
577 if getattr(self, 'parallel', None) is None:
574 self.parallel = True
578 self.parallel = True
575
579
576 return build_ext.finalize_options(self)
580 return build_ext.finalize_options(self)
577
581
578 def build_extensions(self):
582 def build_extensions(self):
579 ruststandalones = [
583 ruststandalones = [
580 e for e in self.extensions if isinstance(e, RustStandaloneExtension)
584 e for e in self.extensions if isinstance(e, RustStandaloneExtension)
581 ]
585 ]
582 self.extensions = [
586 self.extensions = [
583 e for e in self.extensions if e not in ruststandalones
587 e for e in self.extensions if e not in ruststandalones
584 ]
588 ]
585 # Filter out zstd if disabled via argument.
589 # Filter out zstd if disabled via argument.
586 if not self.zstd:
590 if not self.zstd:
587 self.extensions = [
591 self.extensions = [
588 e for e in self.extensions if e.name != 'mercurial.zstd'
592 e for e in self.extensions if e.name != 'mercurial.zstd'
589 ]
593 ]
590
594
591 # Build Rust standalone extensions if it'll be used
595 # Build Rust standalone extensions if it'll be used
592 # and its build is not explicitly disabled (for external build
596 # and its build is not explicitly disabled (for external build
593 # as Linux distributions would do)
597 # as Linux distributions would do)
594 if self.distribution.rust and self.rust:
598 if self.distribution.rust and self.rust:
595 if not sys.platform.startswith('linux'):
599 if not sys.platform.startswith('linux'):
596 self.warn(
600 self.warn(
597 "rust extensions have only been tested on Linux "
601 "rust extensions have only been tested on Linux "
598 "and may not behave correctly on other platforms"
602 "and may not behave correctly on other platforms"
599 )
603 )
600
604
601 for rustext in ruststandalones:
605 for rustext in ruststandalones:
602 rustext.build('' if self.inplace else self.build_lib)
606 rustext.build('' if self.inplace else self.build_lib)
603
607
604 return build_ext.build_extensions(self)
608 return build_ext.build_extensions(self)
605
609
606 def build_extension(self, ext):
610 def build_extension(self, ext):
607 if (
611 if (
608 self.distribution.rust
612 self.distribution.rust
609 and self.rust
613 and self.rust
610 and isinstance(ext, RustExtension)
614 and isinstance(ext, RustExtension)
611 ):
615 ):
612 ext.rustbuild()
616 ext.rustbuild()
613 try:
617 try:
614 build_ext.build_extension(self, ext)
618 build_ext.build_extension(self, ext)
615 except CCompilerError:
619 except CCompilerError:
616 if not getattr(ext, 'optional', False):
620 if not getattr(ext, 'optional', False):
617 raise
621 raise
618 log.warn(
622 log.warn(
619 "Failed to build optional extension '%s' (skipping)", ext.name
623 "Failed to build optional extension '%s' (skipping)", ext.name
620 )
624 )
621
625
622
626
623 class hgbuildscripts(build_scripts):
627 class hgbuildscripts(build_scripts):
624 def run(self):
628 def run(self):
625 if os.name != 'nt' or self.distribution.pure:
629 if os.name != 'nt' or self.distribution.pure:
626 return build_scripts.run(self)
630 return build_scripts.run(self)
627
631
628 exebuilt = False
632 exebuilt = False
629 try:
633 try:
630 self.run_command('build_hgexe')
634 self.run_command('build_hgexe')
631 exebuilt = True
635 exebuilt = True
632 except (DistutilsError, CCompilerError):
636 except (DistutilsError, CCompilerError):
633 log.warn('failed to build optional hg.exe')
637 log.warn('failed to build optional hg.exe')
634
638
635 if exebuilt:
639 if exebuilt:
636 # Copying hg.exe to the scripts build directory ensures it is
640 # Copying hg.exe to the scripts build directory ensures it is
637 # installed by the install_scripts command.
641 # installed by the install_scripts command.
638 hgexecommand = self.get_finalized_command('build_hgexe')
642 hgexecommand = self.get_finalized_command('build_hgexe')
639 dest = os.path.join(self.build_dir, 'hg.exe')
643 dest = os.path.join(self.build_dir, 'hg.exe')
640 self.mkpath(self.build_dir)
644 self.mkpath(self.build_dir)
641 self.copy_file(hgexecommand.hgexepath, dest)
645 self.copy_file(hgexecommand.hgexepath, dest)
642
646
643 # Remove hg.bat because it is redundant with hg.exe.
647 # Remove hg.bat because it is redundant with hg.exe.
644 self.scripts.remove('contrib/win32/hg.bat')
648 self.scripts.remove('contrib/win32/hg.bat')
645
649
646 return build_scripts.run(self)
650 return build_scripts.run(self)
647
651
648
652
649 class hgbuildpy(build_py):
653 class hgbuildpy(build_py):
650 def finalize_options(self):
654 def finalize_options(self):
651 build_py.finalize_options(self)
655 build_py.finalize_options(self)
652
656
653 if self.distribution.pure:
657 if self.distribution.pure:
654 self.distribution.ext_modules = []
658 self.distribution.ext_modules = []
655 elif self.distribution.cffi:
659 elif self.distribution.cffi:
656 from mercurial.cffi import (
660 from mercurial.cffi import (
657 bdiffbuild,
661 bdiffbuild,
658 mpatchbuild,
662 mpatchbuild,
659 )
663 )
660
664
661 exts = [
665 exts = [
662 mpatchbuild.ffi.distutils_extension(),
666 mpatchbuild.ffi.distutils_extension(),
663 bdiffbuild.ffi.distutils_extension(),
667 bdiffbuild.ffi.distutils_extension(),
664 ]
668 ]
665 # cffi modules go here
669 # cffi modules go here
666 if sys.platform == 'darwin':
670 if sys.platform == 'darwin':
667 from mercurial.cffi import osutilbuild
671 from mercurial.cffi import osutilbuild
668
672
669 exts.append(osutilbuild.ffi.distutils_extension())
673 exts.append(osutilbuild.ffi.distutils_extension())
670 self.distribution.ext_modules = exts
674 self.distribution.ext_modules = exts
671 else:
675 else:
672 h = os.path.join(get_python_inc(), 'Python.h')
676 h = os.path.join(get_python_inc(), 'Python.h')
673 if not os.path.exists(h):
677 if not os.path.exists(h):
674 raise SystemExit(
678 raise SystemExit(
675 'Python headers are required to build '
679 'Python headers are required to build '
676 'Mercurial but weren\'t found in %s' % h
680 'Mercurial but weren\'t found in %s' % h
677 )
681 )
678
682
679 def run(self):
683 def run(self):
680 basepath = os.path.join(self.build_lib, 'mercurial')
684 basepath = os.path.join(self.build_lib, 'mercurial')
681 self.mkpath(basepath)
685 self.mkpath(basepath)
682
686
683 rust = self.distribution.rust
687 rust = self.distribution.rust
684 if self.distribution.pure:
688 if self.distribution.pure:
685 modulepolicy = 'py'
689 modulepolicy = 'py'
686 elif self.build_lib == '.':
690 elif self.build_lib == '.':
687 # in-place build should run without rebuilding and Rust extensions
691 # in-place build should run without rebuilding and Rust extensions
688 modulepolicy = 'rust+c-allow' if rust else 'allow'
692 modulepolicy = 'rust+c-allow' if rust else 'allow'
689 else:
693 else:
690 modulepolicy = 'rust+c' if rust else 'c'
694 modulepolicy = 'rust+c' if rust else 'c'
691
695
692 content = b''.join(
696 content = b''.join(
693 [
697 [
694 b'# this file is autogenerated by setup.py\n',
698 b'# this file is autogenerated by setup.py\n',
695 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
699 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
696 ]
700 ]
697 )
701 )
698 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'), content)
702 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'), content)
699
703
700 build_py.run(self)
704 build_py.run(self)
701
705
702
706
703 class buildhgextindex(Command):
707 class buildhgextindex(Command):
704 description = 'generate prebuilt index of hgext (for frozen package)'
708 description = 'generate prebuilt index of hgext (for frozen package)'
705 user_options = []
709 user_options = []
706 _indexfilename = 'hgext/__index__.py'
710 _indexfilename = 'hgext/__index__.py'
707
711
708 def initialize_options(self):
712 def initialize_options(self):
709 pass
713 pass
710
714
711 def finalize_options(self):
715 def finalize_options(self):
712 pass
716 pass
713
717
714 def run(self):
718 def run(self):
715 if os.path.exists(self._indexfilename):
719 if os.path.exists(self._indexfilename):
716 with open(self._indexfilename, 'w') as f:
720 with open(self._indexfilename, 'w') as f:
717 f.write('# empty\n')
721 f.write('# empty\n')
718
722
719 # here no extension enabled, disabled() lists up everything
723 # here no extension enabled, disabled() lists up everything
720 code = (
724 code = (
721 'import pprint; from mercurial import extensions; '
725 'import pprint; from mercurial import extensions; '
722 'ext = extensions.disabled();'
726 'ext = extensions.disabled();'
723 'ext.pop("__index__", None);'
727 'ext.pop("__index__", None);'
724 'pprint.pprint(ext)'
728 'pprint.pprint(ext)'
725 )
729 )
726 returncode, out, err = runcmd(
730 returncode, out, err = runcmd(
727 [sys.executable, '-c', code], localhgenv()
731 [sys.executable, '-c', code], localhgenv()
728 )
732 )
729 if err or returncode != 0:
733 if err or returncode != 0:
730 raise DistutilsExecError(err)
734 raise DistutilsExecError(err)
731
735
732 with open(self._indexfilename, 'wb') as f:
736 with open(self._indexfilename, 'wb') as f:
733 f.write(b'# this file is autogenerated by setup.py\n')
737 f.write(b'# this file is autogenerated by setup.py\n')
734 f.write(b'docs = ')
738 f.write(b'docs = ')
735 f.write(out)
739 f.write(out)
736
740
737
741
738 class buildhgexe(build_ext):
742 class buildhgexe(build_ext):
739 description = 'compile hg.exe from mercurial/exewrapper.c'
743 description = 'compile hg.exe from mercurial/exewrapper.c'
740
744
741 LONG_PATHS_MANIFEST = """\
745 LONG_PATHS_MANIFEST = """\
742 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
746 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
743 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
747 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
744 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
748 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
745 <security>
749 <security>
746 <requestedPrivileges>
750 <requestedPrivileges>
747 <requestedExecutionLevel
751 <requestedExecutionLevel
748 level="asInvoker"
752 level="asInvoker"
749 uiAccess="false"
753 uiAccess="false"
750 />
754 />
751 </requestedPrivileges>
755 </requestedPrivileges>
752 </security>
756 </security>
753 </trustInfo>
757 </trustInfo>
754 <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
758 <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
755 <application>
759 <application>
756 <!-- Windows Vista -->
760 <!-- Windows Vista -->
757 <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
761 <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
758 <!-- Windows 7 -->
762 <!-- Windows 7 -->
759 <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
763 <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
760 <!-- Windows 8 -->
764 <!-- Windows 8 -->
761 <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
765 <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
762 <!-- Windows 8.1 -->
766 <!-- Windows 8.1 -->
763 <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
767 <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
764 <!-- Windows 10 and Windows 11 -->
768 <!-- Windows 10 and Windows 11 -->
765 <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
769 <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
766 </application>
770 </application>
767 </compatibility>
771 </compatibility>
768 <application xmlns="urn:schemas-microsoft-com:asm.v3">
772 <application xmlns="urn:schemas-microsoft-com:asm.v3">
769 <windowsSettings
773 <windowsSettings
770 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
774 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
771 <ws2:longPathAware>true</ws2:longPathAware>
775 <ws2:longPathAware>true</ws2:longPathAware>
772 </windowsSettings>
776 </windowsSettings>
773 </application>
777 </application>
774 <dependency>
778 <dependency>
775 <dependentAssembly>
779 <dependentAssembly>
776 <assemblyIdentity type="win32"
780 <assemblyIdentity type="win32"
777 name="Microsoft.Windows.Common-Controls"
781 name="Microsoft.Windows.Common-Controls"
778 version="6.0.0.0"
782 version="6.0.0.0"
779 processorArchitecture="*"
783 processorArchitecture="*"
780 publicKeyToken="6595b64144ccf1df"
784 publicKeyToken="6595b64144ccf1df"
781 language="*" />
785 language="*" />
782 </dependentAssembly>
786 </dependentAssembly>
783 </dependency>
787 </dependency>
784 </assembly>
788 </assembly>
785 """
789 """
786
790
787 def initialize_options(self):
791 def initialize_options(self):
788 build_ext.initialize_options(self)
792 build_ext.initialize_options(self)
789
793
790 def build_extensions(self):
794 def build_extensions(self):
791 if os.name != 'nt':
795 if os.name != 'nt':
792 return
796 return
793 if isinstance(self.compiler, HackedMingw32CCompiler):
797 if isinstance(self.compiler, HackedMingw32CCompiler):
794 self.compiler.compiler_so = self.compiler.compiler # no -mdll
798 self.compiler.compiler_so = self.compiler.compiler # no -mdll
795 self.compiler.dll_libraries = [] # no -lmsrvc90
799 self.compiler.dll_libraries = [] # no -lmsrvc90
796
800
797 pythonlib = None
801 pythonlib = None
798
802
799 dirname = os.path.dirname(self.get_ext_fullpath('dummy'))
803 dirname = os.path.dirname(self.get_ext_fullpath('dummy'))
800 self.hgtarget = os.path.join(dirname, 'hg')
804 self.hgtarget = os.path.join(dirname, 'hg')
801
805
802 if getattr(sys, 'dllhandle', None):
806 if getattr(sys, 'dllhandle', None):
803 # Different Python installs can have different Python library
807 # Different Python installs can have different Python library
804 # names. e.g. the official CPython distribution uses pythonXY.dll
808 # names. e.g. the official CPython distribution uses pythonXY.dll
805 # and MinGW uses libpythonX.Y.dll.
809 # and MinGW uses libpythonX.Y.dll.
806 _kernel32 = ctypes.windll.kernel32
810 _kernel32 = ctypes.windll.kernel32
807 _kernel32.GetModuleFileNameA.argtypes = [
811 _kernel32.GetModuleFileNameA.argtypes = [
808 ctypes.c_void_p,
812 ctypes.c_void_p,
809 ctypes.c_void_p,
813 ctypes.c_void_p,
810 ctypes.c_ulong,
814 ctypes.c_ulong,
811 ]
815 ]
812 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
816 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
813 size = 1000
817 size = 1000
814 buf = ctypes.create_string_buffer(size + 1)
818 buf = ctypes.create_string_buffer(size + 1)
815 filelen = _kernel32.GetModuleFileNameA(
819 filelen = _kernel32.GetModuleFileNameA(
816 sys.dllhandle, ctypes.byref(buf), size
820 sys.dllhandle, ctypes.byref(buf), size
817 )
821 )
818
822
819 if filelen > 0 and filelen != size:
823 if filelen > 0 and filelen != size:
820 dllbasename = os.path.basename(buf.value)
824 dllbasename = os.path.basename(buf.value)
821 if not dllbasename.lower().endswith(b'.dll'):
825 if not dllbasename.lower().endswith(b'.dll'):
822 raise SystemExit(
826 raise SystemExit(
823 'Python DLL does not end with .dll: %s' % dllbasename
827 'Python DLL does not end with .dll: %s' % dllbasename
824 )
828 )
825 pythonlib = dllbasename[:-4]
829 pythonlib = dllbasename[:-4]
826
830
827 # Copy the pythonXY.dll next to the binary so that it runs
831 # Copy the pythonXY.dll next to the binary so that it runs
828 # without tampering with PATH.
832 # without tampering with PATH.
829 dest = os.path.join(
833 dest = os.path.join(
830 os.path.dirname(self.hgtarget),
834 os.path.dirname(self.hgtarget),
831 os.fsdecode(dllbasename),
835 os.fsdecode(dllbasename),
832 )
836 )
833
837
834 if not os.path.exists(dest):
838 if not os.path.exists(dest):
835 shutil.copy(buf.value, dest)
839 shutil.copy(buf.value, dest)
836
840
837 # Also overwrite python3.dll so that hgext.git is usable.
841 # Also overwrite python3.dll so that hgext.git is usable.
838 # TODO: also handle the MSYS flavor
842 # TODO: also handle the MSYS flavor
839 python_x = os.path.join(
843 python_x = os.path.join(
840 os.path.dirname(os.fsdecode(buf.value)),
844 os.path.dirname(os.fsdecode(buf.value)),
841 "python3.dll",
845 "python3.dll",
842 )
846 )
843
847
844 if os.path.exists(python_x):
848 if os.path.exists(python_x):
845 dest = os.path.join(
849 dest = os.path.join(
846 os.path.dirname(self.hgtarget),
850 os.path.dirname(self.hgtarget),
847 os.path.basename(python_x),
851 os.path.basename(python_x),
848 )
852 )
849
853
850 shutil.copy(python_x, dest)
854 shutil.copy(python_x, dest)
851
855
852 if not pythonlib:
856 if not pythonlib:
853 log.warn(
857 log.warn(
854 'could not determine Python DLL filename; assuming pythonXY'
858 'could not determine Python DLL filename; assuming pythonXY'
855 )
859 )
856
860
857 hv = sys.hexversion
861 hv = sys.hexversion
858 pythonlib = b'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)
862 pythonlib = b'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)
859
863
860 log.info('using %s as Python library name' % pythonlib)
864 log.info('using %s as Python library name' % pythonlib)
861 with open('mercurial/hgpythonlib.h', 'wb') as f:
865 with open('mercurial/hgpythonlib.h', 'wb') as f:
862 f.write(b'/* this file is autogenerated by setup.py */\n')
866 f.write(b'/* this file is autogenerated by setup.py */\n')
863 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
867 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
864
868
865 objects = self.compiler.compile(
869 objects = self.compiler.compile(
866 ['mercurial/exewrapper.c'],
870 ['mercurial/exewrapper.c'],
867 output_dir=self.build_temp,
871 output_dir=self.build_temp,
868 macros=[('_UNICODE', None), ('UNICODE', None)],
872 macros=[('_UNICODE', None), ('UNICODE', None)],
869 )
873 )
870 self.compiler.link_executable(
874 self.compiler.link_executable(
871 objects, self.hgtarget, libraries=[], output_dir=self.build_temp
875 objects, self.hgtarget, libraries=[], output_dir=self.build_temp
872 )
876 )
873
877
874 self.addlongpathsmanifest()
878 self.addlongpathsmanifest()
875
879
876 def addlongpathsmanifest(self):
880 def addlongpathsmanifest(self):
877 """Add manifest pieces so that hg.exe understands long paths
881 """Add manifest pieces so that hg.exe understands long paths
878
882
879 Why resource #1 should be used for .exe manifests? I don't know and
883 Why resource #1 should be used for .exe manifests? I don't know and
880 wasn't able to find an explanation for mortals. But it seems to work.
884 wasn't able to find an explanation for mortals. But it seems to work.
881 """
885 """
882 exefname = self.compiler.executable_filename(self.hgtarget)
886 exefname = self.compiler.executable_filename(self.hgtarget)
883 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
887 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
884 os.close(fdauto)
888 os.close(fdauto)
885 with open(manfname, 'w', encoding="UTF-8") as f:
889 with open(manfname, 'w', encoding="UTF-8") as f:
886 f.write(self.LONG_PATHS_MANIFEST)
890 f.write(self.LONG_PATHS_MANIFEST)
887 log.info("long paths manifest is written to '%s'" % manfname)
891 log.info("long paths manifest is written to '%s'" % manfname)
888 outputresource = '-outputresource:%s;#1' % exefname
892 outputresource = '-outputresource:%s;#1' % exefname
889 log.info("running mt.exe to update hg.exe's manifest in-place")
893 log.info("running mt.exe to update hg.exe's manifest in-place")
890
894
891 self.spawn(
895 self.spawn(
892 [
896 [
893 self.compiler.mt,
897 self.compiler.mt,
894 '-nologo',
898 '-nologo',
895 '-manifest',
899 '-manifest',
896 manfname,
900 manfname,
897 outputresource,
901 outputresource,
898 ]
902 ]
899 )
903 )
900 log.info("done updating hg.exe's manifest")
904 log.info("done updating hg.exe's manifest")
901 os.remove(manfname)
905 os.remove(manfname)
902
906
903 @property
907 @property
904 def hgexepath(self):
908 def hgexepath(self):
905 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
909 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
906 return os.path.join(self.build_temp, dir, 'hg.exe')
910 return os.path.join(self.build_temp, dir, 'hg.exe')
907
911
908
912
909 class hgbuilddoc(Command):
913 class hgbuilddoc(Command):
910 description = 'build documentation'
914 description = 'build documentation'
911 user_options = [
915 user_options = [
912 ('man', None, 'generate man pages'),
916 ('man', None, 'generate man pages'),
913 ('html', None, 'generate html pages'),
917 ('html', None, 'generate html pages'),
914 ]
918 ]
915
919
916 def initialize_options(self):
920 def initialize_options(self):
917 self.man = None
921 self.man = None
918 self.html = None
922 self.html = None
919
923
920 def finalize_options(self):
924 def finalize_options(self):
921 # If --man or --html are set, only generate what we're told to.
925 # If --man or --html are set, only generate what we're told to.
922 # Otherwise generate everything.
926 # Otherwise generate everything.
923 have_subset = self.man is not None or self.html is not None
927 have_subset = self.man is not None or self.html is not None
924
928
925 if have_subset:
929 if have_subset:
926 self.man = True if self.man else False
930 self.man = True if self.man else False
927 self.html = True if self.html else False
931 self.html = True if self.html else False
928 else:
932 else:
929 self.man = True
933 self.man = True
930 self.html = True
934 self.html = True
931
935
932 def run(self):
936 def run(self):
933 def normalizecrlf(p):
937 def normalizecrlf(p):
934 with open(p, 'rb') as fh:
938 with open(p, 'rb') as fh:
935 orig = fh.read()
939 orig = fh.read()
936
940
937 if b'\r\n' not in orig:
941 if b'\r\n' not in orig:
938 return
942 return
939
943
940 log.info('normalizing %s to LF line endings' % p)
944 log.info('normalizing %s to LF line endings' % p)
941 with open(p, 'wb') as fh:
945 with open(p, 'wb') as fh:
942 fh.write(orig.replace(b'\r\n', b'\n'))
946 fh.write(orig.replace(b'\r\n', b'\n'))
943
947
944 def gentxt(root):
948 def gentxt(root):
945 txt = 'doc/%s.txt' % root
949 txt = 'doc/%s.txt' % root
946 log.info('generating %s' % txt)
950 log.info('generating %s' % txt)
947 res, out, err = runcmd(
951 res, out, err = runcmd(
948 [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
952 [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
949 )
953 )
950 if res:
954 if res:
951 raise SystemExit(
955 raise SystemExit(
952 'error running gendoc.py: %s'
956 'error running gendoc.py: %s'
953 % '\n'.join([sysstr(out), sysstr(err)])
957 % '\n'.join([sysstr(out), sysstr(err)])
954 )
958 )
955
959
956 with open(txt, 'wb') as fh:
960 with open(txt, 'wb') as fh:
957 fh.write(out)
961 fh.write(out)
958
962
959 def gengendoc(root):
963 def gengendoc(root):
960 gendoc = 'doc/%s.gendoc.txt' % root
964 gendoc = 'doc/%s.gendoc.txt' % root
961
965
962 log.info('generating %s' % gendoc)
966 log.info('generating %s' % gendoc)
963 res, out, err = runcmd(
967 res, out, err = runcmd(
964 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
968 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
965 os.environ,
969 os.environ,
966 cwd='doc',
970 cwd='doc',
967 )
971 )
968 if res:
972 if res:
969 raise SystemExit(
973 raise SystemExit(
970 'error running gendoc: %s'
974 'error running gendoc: %s'
971 % '\n'.join([sysstr(out), sysstr(err)])
975 % '\n'.join([sysstr(out), sysstr(err)])
972 )
976 )
973
977
974 with open(gendoc, 'wb') as fh:
978 with open(gendoc, 'wb') as fh:
975 fh.write(out)
979 fh.write(out)
976
980
977 def genman(root):
981 def genman(root):
978 log.info('generating doc/%s' % root)
982 log.info('generating doc/%s' % root)
979 res, out, err = runcmd(
983 res, out, err = runcmd(
980 [
984 [
981 sys.executable,
985 sys.executable,
982 'runrst',
986 'runrst',
983 'hgmanpage',
987 'hgmanpage',
984 '--halt',
988 '--halt',
985 'warning',
989 'warning',
986 '--strip-elements-with-class',
990 '--strip-elements-with-class',
987 'htmlonly',
991 'htmlonly',
988 '%s.txt' % root,
992 '%s.txt' % root,
989 root,
993 root,
990 ],
994 ],
991 os.environ,
995 os.environ,
992 cwd='doc',
996 cwd='doc',
993 )
997 )
994 if res:
998 if res:
995 raise SystemExit(
999 raise SystemExit(
996 'error running runrst: %s'
1000 'error running runrst: %s'
997 % '\n'.join([sysstr(out), sysstr(err)])
1001 % '\n'.join([sysstr(out), sysstr(err)])
998 )
1002 )
999
1003
1000 normalizecrlf('doc/%s' % root)
1004 normalizecrlf('doc/%s' % root)
1001
1005
1002 def genhtml(root):
1006 def genhtml(root):
1003 log.info('generating doc/%s.html' % root)
1007 log.info('generating doc/%s.html' % root)
1004 res, out, err = runcmd(
1008 res, out, err = runcmd(
1005 [
1009 [
1006 sys.executable,
1010 sys.executable,
1007 'runrst',
1011 'runrst',
1008 'html',
1012 'html',
1009 '--halt',
1013 '--halt',
1010 'warning',
1014 'warning',
1011 '--link-stylesheet',
1015 '--link-stylesheet',
1012 '--stylesheet-path',
1016 '--stylesheet-path',
1013 'style.css',
1017 'style.css',
1014 '%s.txt' % root,
1018 '%s.txt' % root,
1015 '%s.html' % root,
1019 '%s.html' % root,
1016 ],
1020 ],
1017 os.environ,
1021 os.environ,
1018 cwd='doc',
1022 cwd='doc',
1019 )
1023 )
1020 if res:
1024 if res:
1021 raise SystemExit(
1025 raise SystemExit(
1022 'error running runrst: %s'
1026 'error running runrst: %s'
1023 % '\n'.join([sysstr(out), sysstr(err)])
1027 % '\n'.join([sysstr(out), sysstr(err)])
1024 )
1028 )
1025
1029
1026 normalizecrlf('doc/%s.html' % root)
1030 normalizecrlf('doc/%s.html' % root)
1027
1031
1028 # This logic is duplicated in doc/Makefile.
1032 # This logic is duplicated in doc/Makefile.
1029 sources = {
1033 sources = {
1030 f
1034 f
1031 for f in os.listdir('mercurial/helptext')
1035 for f in os.listdir('mercurial/helptext')
1032 if re.search(r'[0-9]\.txt$', f)
1036 if re.search(r'[0-9]\.txt$', f)
1033 }
1037 }
1034
1038
1035 # common.txt is a one-off.
1039 # common.txt is a one-off.
1036 gentxt('common')
1040 gentxt('common')
1037
1041
1038 for source in sorted(sources):
1042 for source in sorted(sources):
1039 assert source[-4:] == '.txt'
1043 assert source[-4:] == '.txt'
1040 root = source[:-4]
1044 root = source[:-4]
1041
1045
1042 gentxt(root)
1046 gentxt(root)
1043 gengendoc(root)
1047 gengendoc(root)
1044
1048
1045 if self.man:
1049 if self.man:
1046 genman(root)
1050 genman(root)
1047 if self.html:
1051 if self.html:
1048 genhtml(root)
1052 genhtml(root)
1049
1053
1050
1054
1051 class hginstall(install):
1055 class hginstall(install):
1052
1056
1053 user_options = install.user_options + [
1057 user_options = install.user_options + [
1054 (
1058 (
1055 'old-and-unmanageable',
1059 'old-and-unmanageable',
1056 None,
1060 None,
1057 'noop, present for eggless setuptools compat',
1061 'noop, present for eggless setuptools compat',
1058 ),
1062 ),
1059 (
1063 (
1060 'single-version-externally-managed',
1064 'single-version-externally-managed',
1061 None,
1065 None,
1062 'noop, present for eggless setuptools compat',
1066 'noop, present for eggless setuptools compat',
1063 ),
1067 ),
1064 ]
1068 ]
1065
1069
1066 sub_commands = install.sub_commands + [
1070 sub_commands = install.sub_commands + [
1067 ('install_completion', lambda self: True)
1071 ('install_completion', lambda self: True)
1068 ]
1072 ]
1069
1073
1070 # Also helps setuptools not be sad while we refuse to create eggs.
1074 # Also helps setuptools not be sad while we refuse to create eggs.
1071 single_version_externally_managed = True
1075 single_version_externally_managed = True
1072
1076
1073 def get_sub_commands(self):
1077 def get_sub_commands(self):
1074 # Screen out egg related commands to prevent egg generation. But allow
1078 # Screen out egg related commands to prevent egg generation. But allow
1075 # mercurial.egg-info generation, since that is part of modern
1079 # mercurial.egg-info generation, since that is part of modern
1076 # packaging.
1080 # packaging.
1077 excl = {'bdist_egg'}
1081 excl = {'bdist_egg'}
1078 return filter(lambda x: x not in excl, install.get_sub_commands(self))
1082 return filter(lambda x: x not in excl, install.get_sub_commands(self))
1079
1083
1080
1084
1081 class hginstalllib(install_lib):
1085 class hginstalllib(install_lib):
1082 """
1086 """
1083 This is a specialization of install_lib that replaces the copy_file used
1087 This is a specialization of install_lib that replaces the copy_file used
1084 there so that it supports setting the mode of files after copying them,
1088 there so that it supports setting the mode of files after copying them,
1085 instead of just preserving the mode that the files originally had. If your
1089 instead of just preserving the mode that the files originally had. If your
1086 system has a umask of something like 027, preserving the permissions when
1090 system has a umask of something like 027, preserving the permissions when
1087 copying will lead to a broken install.
1091 copying will lead to a broken install.
1088
1092
1089 Note that just passing keep_permissions=False to copy_file would be
1093 Note that just passing keep_permissions=False to copy_file would be
1090 insufficient, as it might still be applying a umask.
1094 insufficient, as it might still be applying a umask.
1091 """
1095 """
1092
1096
1093 def run(self):
1097 def run(self):
1094 realcopyfile = file_util.copy_file
1098 realcopyfile = file_util.copy_file
1095
1099
1096 def copyfileandsetmode(*args, **kwargs):
1100 def copyfileandsetmode(*args, **kwargs):
1097 src, dst = args[0], args[1]
1101 src, dst = args[0], args[1]
1098 dst, copied = realcopyfile(*args, **kwargs)
1102 dst, copied = realcopyfile(*args, **kwargs)
1099 if copied:
1103 if copied:
1100 st = os.stat(src)
1104 st = os.stat(src)
1101 # Persist executable bit (apply it to group and other if user
1105 # Persist executable bit (apply it to group and other if user
1102 # has it)
1106 # has it)
1103 if st[stat.ST_MODE] & stat.S_IXUSR:
1107 if st[stat.ST_MODE] & stat.S_IXUSR:
1104 setmode = int('0755', 8)
1108 setmode = int('0755', 8)
1105 else:
1109 else:
1106 setmode = int('0644', 8)
1110 setmode = int('0644', 8)
1107 m = stat.S_IMODE(st[stat.ST_MODE])
1111 m = stat.S_IMODE(st[stat.ST_MODE])
1108 m = (m & ~int('0777', 8)) | setmode
1112 m = (m & ~int('0777', 8)) | setmode
1109 os.chmod(dst, m)
1113 os.chmod(dst, m)
1110
1114
1111 file_util.copy_file = copyfileandsetmode
1115 file_util.copy_file = copyfileandsetmode
1112 try:
1116 try:
1113 install_lib.run(self)
1117 install_lib.run(self)
1114 finally:
1118 finally:
1115 file_util.copy_file = realcopyfile
1119 file_util.copy_file = realcopyfile
1116
1120
1117
1121
1118 class hginstallscripts(install_scripts):
1122 class hginstallscripts(install_scripts):
1119 """
1123 """
1120 This is a specialization of install_scripts that replaces the @LIBDIR@ with
1124 This is a specialization of install_scripts that replaces the @LIBDIR@ with
1121 the configured directory for modules. If possible, the path is made relative
1125 the configured directory for modules. If possible, the path is made relative
1122 to the directory for scripts.
1126 to the directory for scripts.
1123 """
1127 """
1124
1128
1125 def initialize_options(self):
1129 def initialize_options(self):
1126 install_scripts.initialize_options(self)
1130 install_scripts.initialize_options(self)
1127
1131
1128 self.install_lib = None
1132 self.install_lib = None
1129
1133
1130 def finalize_options(self):
1134 def finalize_options(self):
1131 install_scripts.finalize_options(self)
1135 install_scripts.finalize_options(self)
1132 self.set_undefined_options('install', ('install_lib', 'install_lib'))
1136 self.set_undefined_options('install', ('install_lib', 'install_lib'))
1133
1137
1134 def run(self):
1138 def run(self):
1135 install_scripts.run(self)
1139 install_scripts.run(self)
1136
1140
1137 # It only makes sense to replace @LIBDIR@ with the install path if
1141 # It only makes sense to replace @LIBDIR@ with the install path if
1138 # the install path is known. For wheels, the logic below calculates
1142 # the install path is known. For wheels, the logic below calculates
1139 # the libdir to be "../..". This is because the internal layout of a
1143 # the libdir to be "../..". This is because the internal layout of a
1140 # wheel archive looks like:
1144 # wheel archive looks like:
1141 #
1145 #
1142 # mercurial-3.6.1.data/scripts/hg
1146 # mercurial-3.6.1.data/scripts/hg
1143 # mercurial/__init__.py
1147 # mercurial/__init__.py
1144 #
1148 #
1145 # When installing wheels, the subdirectories of the "<pkg>.data"
1149 # When installing wheels, the subdirectories of the "<pkg>.data"
1146 # directory are translated to system local paths and files therein
1150 # directory are translated to system local paths and files therein
1147 # are copied in place. The mercurial/* files are installed into the
1151 # are copied in place. The mercurial/* files are installed into the
1148 # site-packages directory. However, the site-packages directory
1152 # site-packages directory. However, the site-packages directory
1149 # isn't known until wheel install time. This means we have no clue
1153 # isn't known until wheel install time. This means we have no clue
1150 # at wheel generation time what the installed site-packages directory
1154 # at wheel generation time what the installed site-packages directory
1151 # will be. And, wheels don't appear to provide the ability to register
1155 # will be. And, wheels don't appear to provide the ability to register
1152 # custom code to run during wheel installation. This all means that
1156 # custom code to run during wheel installation. This all means that
1153 # we can't reliably set the libdir in wheels: the default behavior
1157 # we can't reliably set the libdir in wheels: the default behavior
1154 # of looking in sys.path must do.
1158 # of looking in sys.path must do.
1155
1159
1156 if (
1160 if (
1157 os.path.splitdrive(self.install_dir)[0]
1161 os.path.splitdrive(self.install_dir)[0]
1158 != os.path.splitdrive(self.install_lib)[0]
1162 != os.path.splitdrive(self.install_lib)[0]
1159 ):
1163 ):
1160 # can't make relative paths from one drive to another, so use an
1164 # can't make relative paths from one drive to another, so use an
1161 # absolute path instead
1165 # absolute path instead
1162 libdir = self.install_lib
1166 libdir = self.install_lib
1163 else:
1167 else:
1164 libdir = os.path.relpath(self.install_lib, self.install_dir)
1168 libdir = os.path.relpath(self.install_lib, self.install_dir)
1165
1169
1166 for outfile in self.outfiles:
1170 for outfile in self.outfiles:
1167 with open(outfile, 'rb') as fp:
1171 with open(outfile, 'rb') as fp:
1168 data = fp.read()
1172 data = fp.read()
1169
1173
1170 # skip binary files
1174 # skip binary files
1171 if b'\0' in data:
1175 if b'\0' in data:
1172 continue
1176 continue
1173
1177
1174 # During local installs, the shebang will be rewritten to the final
1178 # During local installs, the shebang will be rewritten to the final
1175 # install path. During wheel packaging, the shebang has a special
1179 # install path. During wheel packaging, the shebang has a special
1176 # value.
1180 # value.
1177 if data.startswith(b'#!python'):
1181 if data.startswith(b'#!python'):
1178 log.info(
1182 log.info(
1179 'not rewriting @LIBDIR@ in %s because install path '
1183 'not rewriting @LIBDIR@ in %s because install path '
1180 'not known' % outfile
1184 'not known' % outfile
1181 )
1185 )
1182 continue
1186 continue
1183
1187
1184 data = data.replace(b'@LIBDIR@', libdir.encode('unicode_escape'))
1188 data = data.replace(b'@LIBDIR@', libdir.encode('unicode_escape'))
1185 with open(outfile, 'wb') as fp:
1189 with open(outfile, 'wb') as fp:
1186 fp.write(data)
1190 fp.write(data)
1187
1191
1188
1192
1189 class hginstallcompletion(Command):
1193 class hginstallcompletion(Command):
1190 description = 'Install shell completion'
1194 description = 'Install shell completion'
1191
1195
1192 def initialize_options(self):
1196 def initialize_options(self):
1193 self.install_dir = None
1197 self.install_dir = None
1194 self.outputs = []
1198 self.outputs = []
1195
1199
1196 def finalize_options(self):
1200 def finalize_options(self):
1197 self.set_undefined_options(
1201 self.set_undefined_options(
1198 'install_data', ('install_dir', 'install_dir')
1202 'install_data', ('install_dir', 'install_dir')
1199 )
1203 )
1200
1204
1201 def get_outputs(self):
1205 def get_outputs(self):
1202 return self.outputs
1206 return self.outputs
1203
1207
1204 def run(self):
1208 def run(self):
1205 for src, dir_path, dest in (
1209 for src, dir_path, dest in (
1206 (
1210 (
1207 'bash_completion',
1211 'bash_completion',
1208 ('share', 'bash-completion', 'completions'),
1212 ('share', 'bash-completion', 'completions'),
1209 'hg',
1213 'hg',
1210 ),
1214 ),
1211 ('zsh_completion', ('share', 'zsh', 'site-functions'), '_hg'),
1215 ('zsh_completion', ('share', 'zsh', 'site-functions'), '_hg'),
1212 ):
1216 ):
1213 dir = os.path.join(self.install_dir, *dir_path)
1217 dir = os.path.join(self.install_dir, *dir_path)
1214 self.mkpath(dir)
1218 self.mkpath(dir)
1215
1219
1216 dest = os.path.join(dir, dest)
1220 dest = os.path.join(dir, dest)
1217 self.outputs.append(dest)
1221 self.outputs.append(dest)
1218 self.copy_file(os.path.join('contrib', src), dest)
1222 self.copy_file(os.path.join('contrib', src), dest)
1219
1223
1220
1224
1221 # virtualenv installs custom distutils/__init__.py and
1225 # virtualenv installs custom distutils/__init__.py and
1222 # distutils/distutils.cfg files which essentially proxy back to the
1226 # distutils/distutils.cfg files which essentially proxy back to the
1223 # "real" distutils in the main Python install. The presence of this
1227 # "real" distutils in the main Python install. The presence of this
1224 # directory causes py2exe to pick up the "hacked" distutils package
1228 # directory causes py2exe to pick up the "hacked" distutils package
1225 # from the virtualenv and "import distutils" will fail from the py2exe
1229 # from the virtualenv and "import distutils" will fail from the py2exe
1226 # build because the "real" distutils files can't be located.
1230 # build because the "real" distutils files can't be located.
1227 #
1231 #
1228 # We work around this by monkeypatching the py2exe code finding Python
1232 # We work around this by monkeypatching the py2exe code finding Python
1229 # modules to replace the found virtualenv distutils modules with the
1233 # modules to replace the found virtualenv distutils modules with the
1230 # original versions via filesystem scanning. This is a bit hacky. But
1234 # original versions via filesystem scanning. This is a bit hacky. But
1231 # it allows us to use virtualenvs for py2exe packaging, which is more
1235 # it allows us to use virtualenvs for py2exe packaging, which is more
1232 # deterministic and reproducible.
1236 # deterministic and reproducible.
1233 #
1237 #
1234 # It's worth noting that the common StackOverflow suggestions for this
1238 # It's worth noting that the common StackOverflow suggestions for this
1235 # problem involve copying the original distutils files into the
1239 # problem involve copying the original distutils files into the
1236 # virtualenv or into the staging directory after setup() is invoked.
1240 # virtualenv or into the staging directory after setup() is invoked.
1237 # The former is very brittle and can easily break setup(). Our hacking
1241 # The former is very brittle and can easily break setup(). Our hacking
1238 # of the found modules routine has a similar result as copying the files
1242 # of the found modules routine has a similar result as copying the files
1239 # manually. But it makes fewer assumptions about how py2exe works and
1243 # manually. But it makes fewer assumptions about how py2exe works and
1240 # is less brittle.
1244 # is less brittle.
1241
1245
1242 # This only catches virtualenvs made with virtualenv (as opposed to
1246 # This only catches virtualenvs made with virtualenv (as opposed to
1243 # venv, which is likely what Python 3 uses).
1247 # venv, which is likely what Python 3 uses).
1244 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
1248 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
1245
1249
1246 if py2exehacked:
1250 if py2exehacked:
1247 from distutils.command.py2exe import py2exe as buildpy2exe
1251 from distutils.command.py2exe import py2exe as buildpy2exe
1248 from py2exe.mf import Module as py2exemodule
1252 from py2exe.mf import Module as py2exemodule
1249
1253
1250 class hgbuildpy2exe(buildpy2exe):
1254 class hgbuildpy2exe(buildpy2exe):
1251 def find_needed_modules(self, mf, files, modules):
1255 def find_needed_modules(self, mf, files, modules):
1252 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1256 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1253
1257
1254 # Replace virtualenv's distutils modules with the real ones.
1258 # Replace virtualenv's distutils modules with the real ones.
1255 modules = {}
1259 modules = {}
1256 for k, v in res.modules.items():
1260 for k, v in res.modules.items():
1257 if k != 'distutils' and not k.startswith('distutils.'):
1261 if k != 'distutils' and not k.startswith('distutils.'):
1258 modules[k] = v
1262 modules[k] = v
1259
1263
1260 res.modules = modules
1264 res.modules = modules
1261
1265
1262 import opcode
1266 import opcode
1263
1267
1264 distutilsreal = os.path.join(
1268 distutilsreal = os.path.join(
1265 os.path.dirname(opcode.__file__), 'distutils'
1269 os.path.dirname(opcode.__file__), 'distutils'
1266 )
1270 )
1267
1271
1268 for root, dirs, files in os.walk(distutilsreal):
1272 for root, dirs, files in os.walk(distutilsreal):
1269 for f in sorted(files):
1273 for f in sorted(files):
1270 if not f.endswith('.py'):
1274 if not f.endswith('.py'):
1271 continue
1275 continue
1272
1276
1273 full = os.path.join(root, f)
1277 full = os.path.join(root, f)
1274
1278
1275 parents = ['distutils']
1279 parents = ['distutils']
1276
1280
1277 if root != distutilsreal:
1281 if root != distutilsreal:
1278 rel = os.path.relpath(root, distutilsreal)
1282 rel = os.path.relpath(root, distutilsreal)
1279 parents.extend(p for p in rel.split(os.sep))
1283 parents.extend(p for p in rel.split(os.sep))
1280
1284
1281 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1285 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1282
1286
1283 if modname.startswith('distutils.tests.'):
1287 if modname.startswith('distutils.tests.'):
1284 continue
1288 continue
1285
1289
1286 if modname.endswith('.__init__'):
1290 if modname.endswith('.__init__'):
1287 modname = modname[: -len('.__init__')]
1291 modname = modname[: -len('.__init__')]
1288 path = os.path.dirname(full)
1292 path = os.path.dirname(full)
1289 else:
1293 else:
1290 path = None
1294 path = None
1291
1295
1292 res.modules[modname] = py2exemodule(
1296 res.modules[modname] = py2exemodule(
1293 modname, full, path=path
1297 modname, full, path=path
1294 )
1298 )
1295
1299
1296 if 'distutils' not in res.modules:
1300 if 'distutils' not in res.modules:
1297 raise SystemExit('could not find distutils modules')
1301 raise SystemExit('could not find distutils modules')
1298
1302
1299 return res
1303 return res
1300
1304
1301
1305
1302 cmdclass = {
1306 cmdclass = {
1303 'build': hgbuild,
1307 'build': hgbuild,
1304 'build_doc': hgbuilddoc,
1308 'build_doc': hgbuilddoc,
1305 'build_mo': hgbuildmo,
1309 'build_mo': hgbuildmo,
1306 'build_ext': hgbuildext,
1310 'build_ext': hgbuildext,
1307 'build_py': hgbuildpy,
1311 'build_py': hgbuildpy,
1308 'build_scripts': hgbuildscripts,
1312 'build_scripts': hgbuildscripts,
1309 'build_hgextindex': buildhgextindex,
1313 'build_hgextindex': buildhgextindex,
1310 'install': hginstall,
1314 'install': hginstall,
1311 'install_completion': hginstallcompletion,
1315 'install_completion': hginstallcompletion,
1312 'install_lib': hginstalllib,
1316 'install_lib': hginstalllib,
1313 'install_scripts': hginstallscripts,
1317 'install_scripts': hginstallscripts,
1314 'build_hgexe': buildhgexe,
1318 'build_hgexe': buildhgexe,
1315 }
1319 }
1316
1320
1317 if py2exehacked:
1321 if py2exehacked:
1318 cmdclass['py2exe'] = hgbuildpy2exe
1322 cmdclass['py2exe'] = hgbuildpy2exe
1319
1323
1320 packages = [
1324 packages = [
1321 'mercurial',
1325 'mercurial',
1322 'mercurial.admin',
1326 'mercurial.admin',
1323 'mercurial.cext',
1327 'mercurial.cext',
1324 'mercurial.cffi',
1328 'mercurial.cffi',
1325 'mercurial.defaultrc',
1329 'mercurial.defaultrc',
1326 'mercurial.dirstateutils',
1330 'mercurial.dirstateutils',
1327 'mercurial.helptext',
1331 'mercurial.helptext',
1328 'mercurial.helptext.internals',
1332 'mercurial.helptext.internals',
1329 'mercurial.hgweb',
1333 'mercurial.hgweb',
1330 'mercurial.interfaces',
1334 'mercurial.interfaces',
1331 'mercurial.pure',
1335 'mercurial.pure',
1332 'mercurial.stabletailgraph',
1336 'mercurial.stabletailgraph',
1333 'mercurial.templates',
1337 'mercurial.templates',
1334 'mercurial.thirdparty',
1338 'mercurial.thirdparty',
1335 'mercurial.thirdparty.attr',
1339 'mercurial.thirdparty.attr',
1336 'mercurial.thirdparty.tomli',
1340 'mercurial.thirdparty.tomli',
1337 'mercurial.thirdparty.zope',
1341 'mercurial.thirdparty.zope',
1338 'mercurial.thirdparty.zope.interface',
1342 'mercurial.thirdparty.zope.interface',
1339 'mercurial.upgrade_utils',
1343 'mercurial.upgrade_utils',
1340 'mercurial.utils',
1344 'mercurial.utils',
1341 'mercurial.revlogutils',
1345 'mercurial.revlogutils',
1342 'mercurial.testing',
1346 'mercurial.testing',
1343 'hgext',
1347 'hgext',
1344 'hgext.convert',
1348 'hgext.convert',
1345 'hgext.fsmonitor',
1349 'hgext.fsmonitor',
1346 'hgext.fastannotate',
1350 'hgext.fastannotate',
1347 'hgext.fsmonitor.pywatchman',
1351 'hgext.fsmonitor.pywatchman',
1348 'hgext.git',
1352 'hgext.git',
1349 'hgext.highlight',
1353 'hgext.highlight',
1350 'hgext.hooklib',
1354 'hgext.hooklib',
1351 'hgext.largefiles',
1355 'hgext.largefiles',
1352 'hgext.lfs',
1356 'hgext.lfs',
1353 'hgext.narrow',
1357 'hgext.narrow',
1354 'hgext.remotefilelog',
1358 'hgext.remotefilelog',
1355 'hgext.zeroconf',
1359 'hgext.zeroconf',
1356 'hgext3rd',
1360 'hgext3rd',
1357 'hgdemandimport',
1361 'hgdemandimport',
1358 ]
1362 ]
1359
1363
1360 for name in os.listdir(os.path.join('mercurial', 'templates')):
1364 for name in os.listdir(os.path.join('mercurial', 'templates')):
1361 if name != '__pycache__' and os.path.isdir(
1365 if name != '__pycache__' and os.path.isdir(
1362 os.path.join('mercurial', 'templates', name)
1366 os.path.join('mercurial', 'templates', name)
1363 ):
1367 ):
1364 packages.append('mercurial.templates.%s' % name)
1368 packages.append('mercurial.templates.%s' % name)
1365
1369
1366 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1370 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1367 # py2exe can't cope with namespace packages very well, so we have to
1371 # py2exe can't cope with namespace packages very well, so we have to
1368 # install any hgext3rd.* extensions that we want in the final py2exe
1372 # install any hgext3rd.* extensions that we want in the final py2exe
1369 # image here. This is gross, but you gotta do what you gotta do.
1373 # image here. This is gross, but you gotta do what you gotta do.
1370 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1374 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1371
1375
1372 common_depends = [
1376 common_depends = [
1373 'mercurial/bitmanipulation.h',
1377 'mercurial/bitmanipulation.h',
1374 'mercurial/compat.h',
1378 'mercurial/compat.h',
1375 'mercurial/cext/util.h',
1379 'mercurial/cext/util.h',
1376 ]
1380 ]
1377 common_include_dirs = ['mercurial']
1381 common_include_dirs = ['mercurial']
1378
1382
1379 common_cflags = []
1383 common_cflags = []
1380
1384
1381 # MSVC 2008 still needs declarations at the top of the scope, but Python 3.9
1385 # MSVC 2008 still needs declarations at the top of the scope, but Python 3.9
1382 # makes declarations not at the top of a scope in the headers.
1386 # makes declarations not at the top of a scope in the headers.
1383 if os.name != 'nt' and sys.version_info[1] < 9:
1387 if os.name != 'nt' and sys.version_info[1] < 9:
1384 common_cflags = ['-Werror=declaration-after-statement']
1388 common_cflags = ['-Werror=declaration-after-statement']
1385
1389
1386 osutil_cflags = []
1390 osutil_cflags = []
1387 osutil_ldflags = []
1391 osutil_ldflags = []
1388
1392
1389 # platform specific macros
1393 # platform specific macros
1390 for plat, func in [('bsd', 'setproctitle')]:
1394 for plat, func in [('bsd', 'setproctitle')]:
1391 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1395 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1392 osutil_cflags.append('-DHAVE_%s' % func.upper())
1396 osutil_cflags.append('-DHAVE_%s' % func.upper())
1393
1397
1394 for plat, macro, code in [
1398 for plat, macro, code in [
1395 (
1399 (
1396 'bsd|darwin',
1400 'bsd|darwin',
1397 'BSD_STATFS',
1401 'BSD_STATFS',
1398 '''
1402 '''
1399 #include <sys/param.h>
1403 #include <sys/param.h>
1400 #include <sys/mount.h>
1404 #include <sys/mount.h>
1401 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1405 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1402 ''',
1406 ''',
1403 ),
1407 ),
1404 (
1408 (
1405 'linux',
1409 'linux',
1406 'LINUX_STATFS',
1410 'LINUX_STATFS',
1407 '''
1411 '''
1408 #include <linux/magic.h>
1412 #include <linux/magic.h>
1409 #include <sys/vfs.h>
1413 #include <sys/vfs.h>
1410 int main() { struct statfs s; return sizeof(s.f_type); }
1414 int main() { struct statfs s; return sizeof(s.f_type); }
1411 ''',
1415 ''',
1412 ),
1416 ),
1413 ]:
1417 ]:
1414 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1418 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1415 osutil_cflags.append('-DHAVE_%s' % macro)
1419 osutil_cflags.append('-DHAVE_%s' % macro)
1416
1420
1417 if sys.platform == 'darwin':
1421 if sys.platform == 'darwin':
1418 osutil_ldflags += ['-framework', 'ApplicationServices']
1422 osutil_ldflags += ['-framework', 'ApplicationServices']
1419
1423
1420 if sys.platform == 'sunos5':
1424 if sys.platform == 'sunos5':
1421 osutil_ldflags += ['-lsocket']
1425 osutil_ldflags += ['-lsocket']
1422
1426
1423 xdiff_srcs = [
1427 xdiff_srcs = [
1424 'mercurial/thirdparty/xdiff/xdiffi.c',
1428 'mercurial/thirdparty/xdiff/xdiffi.c',
1425 'mercurial/thirdparty/xdiff/xprepare.c',
1429 'mercurial/thirdparty/xdiff/xprepare.c',
1426 'mercurial/thirdparty/xdiff/xutils.c',
1430 'mercurial/thirdparty/xdiff/xutils.c',
1427 ]
1431 ]
1428
1432
1429 xdiff_headers = [
1433 xdiff_headers = [
1430 'mercurial/thirdparty/xdiff/xdiff.h',
1434 'mercurial/thirdparty/xdiff/xdiff.h',
1431 'mercurial/thirdparty/xdiff/xdiffi.h',
1435 'mercurial/thirdparty/xdiff/xdiffi.h',
1432 'mercurial/thirdparty/xdiff/xinclude.h',
1436 'mercurial/thirdparty/xdiff/xinclude.h',
1433 'mercurial/thirdparty/xdiff/xmacros.h',
1437 'mercurial/thirdparty/xdiff/xmacros.h',
1434 'mercurial/thirdparty/xdiff/xprepare.h',
1438 'mercurial/thirdparty/xdiff/xprepare.h',
1435 'mercurial/thirdparty/xdiff/xtypes.h',
1439 'mercurial/thirdparty/xdiff/xtypes.h',
1436 'mercurial/thirdparty/xdiff/xutils.h',
1440 'mercurial/thirdparty/xdiff/xutils.h',
1437 ]
1441 ]
1438
1442
1439
1443
1440 class RustCompilationError(CCompilerError):
1444 class RustCompilationError(CCompilerError):
1441 """Exception class for Rust compilation errors."""
1445 """Exception class for Rust compilation errors."""
1442
1446
1443
1447
1444 class RustExtension(Extension):
1448 class RustExtension(Extension):
1445 """Base classes for concrete Rust Extension classes."""
1449 """Base classes for concrete Rust Extension classes."""
1446
1450
1447 rusttargetdir = os.path.join('rust', 'target', 'release')
1451 rusttargetdir = os.path.join('rust', 'target', 'release')
1448
1452
1449 def __init__(self, mpath, sources, rustlibname, subcrate, **kw):
1453 def __init__(self, mpath, sources, rustlibname, subcrate, **kw):
1450 Extension.__init__(self, mpath, sources, **kw)
1454 Extension.__init__(self, mpath, sources, **kw)
1451 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1455 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1452
1456
1453 # adding Rust source and control files to depends so that the extension
1457 # adding Rust source and control files to depends so that the extension
1454 # gets rebuilt if they've changed
1458 # gets rebuilt if they've changed
1455 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1459 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1456 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1460 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1457 if os.path.exists(cargo_lock):
1461 if os.path.exists(cargo_lock):
1458 self.depends.append(cargo_lock)
1462 self.depends.append(cargo_lock)
1459 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1463 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1460 self.depends.extend(
1464 self.depends.extend(
1461 os.path.join(dirpath, fname)
1465 os.path.join(dirpath, fname)
1462 for fname in fnames
1466 for fname in fnames
1463 if os.path.splitext(fname)[1] == '.rs'
1467 if os.path.splitext(fname)[1] == '.rs'
1464 )
1468 )
1465
1469
1466 @staticmethod
1470 @staticmethod
1467 def rustdylibsuffix():
1471 def rustdylibsuffix():
1468 """Return the suffix for shared libraries produced by rustc.
1472 """Return the suffix for shared libraries produced by rustc.
1469
1473
1470 See also: https://doc.rust-lang.org/reference/linkage.html
1474 See also: https://doc.rust-lang.org/reference/linkage.html
1471 """
1475 """
1472 if sys.platform == 'darwin':
1476 if sys.platform == 'darwin':
1473 return '.dylib'
1477 return '.dylib'
1474 elif os.name == 'nt':
1478 elif os.name == 'nt':
1475 return '.dll'
1479 return '.dll'
1476 else:
1480 else:
1477 return '.so'
1481 return '.so'
1478
1482
1479 def rustbuild(self):
1483 def rustbuild(self):
1480 env = os.environ.copy()
1484 env = os.environ.copy()
1481 if 'HGTEST_RESTOREENV' in env:
1485 if 'HGTEST_RESTOREENV' in env:
1482 # Mercurial tests change HOME to a temporary directory,
1486 # Mercurial tests change HOME to a temporary directory,
1483 # but, if installed with rustup, the Rust toolchain needs
1487 # but, if installed with rustup, the Rust toolchain needs
1484 # HOME to be correct (otherwise the 'no default toolchain'
1488 # HOME to be correct (otherwise the 'no default toolchain'
1485 # error message is issued and the build fails).
1489 # error message is issued and the build fails).
1486 # This happens currently with test-hghave.t, which does
1490 # This happens currently with test-hghave.t, which does
1487 # invoke this build.
1491 # invoke this build.
1488
1492
1489 # Unix only fix (os.path.expanduser not really reliable if
1493 # Unix only fix (os.path.expanduser not really reliable if
1490 # HOME is shadowed like this)
1494 # HOME is shadowed like this)
1491 import pwd
1495 import pwd
1492
1496
1493 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1497 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1494
1498
1495 cargocmd = ['cargo', 'rustc', '--release']
1499 cargocmd = ['cargo', 'rustc', '--release']
1496
1500
1497 rust_features = env.get("HG_RUST_FEATURES")
1501 rust_features = env.get("HG_RUST_FEATURES")
1498 if rust_features:
1502 if rust_features:
1499 cargocmd.extend(('--features', rust_features))
1503 cargocmd.extend(('--features', rust_features))
1500
1504
1501 cargocmd.append('--')
1505 cargocmd.append('--')
1502 if sys.platform == 'darwin':
1506 if sys.platform == 'darwin':
1503 cargocmd.extend(
1507 cargocmd.extend(
1504 ("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup")
1508 ("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup")
1505 )
1509 )
1506 try:
1510 try:
1507 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1511 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1508 except FileNotFoundError:
1512 except FileNotFoundError:
1509 raise RustCompilationError("Cargo not found")
1513 raise RustCompilationError("Cargo not found")
1510 except PermissionError:
1514 except PermissionError:
1511 raise RustCompilationError(
1515 raise RustCompilationError(
1512 "Cargo found, but permission to execute it is denied"
1516 "Cargo found, but permission to execute it is denied"
1513 )
1517 )
1514 except subprocess.CalledProcessError:
1518 except subprocess.CalledProcessError:
1515 raise RustCompilationError(
1519 raise RustCompilationError(
1516 "Cargo failed. Working directory: %r, "
1520 "Cargo failed. Working directory: %r, "
1517 "command: %r, environment: %r"
1521 "command: %r, environment: %r"
1518 % (self.rustsrcdir, cargocmd, env)
1522 % (self.rustsrcdir, cargocmd, env)
1519 )
1523 )
1520
1524
1521
1525
1522 class RustStandaloneExtension(RustExtension):
1526 class RustStandaloneExtension(RustExtension):
1523 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1527 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1524 RustExtension.__init__(
1528 RustExtension.__init__(
1525 self, pydottedname, [], dylibname, rustcrate, **kw
1529 self, pydottedname, [], dylibname, rustcrate, **kw
1526 )
1530 )
1527 self.dylibname = dylibname
1531 self.dylibname = dylibname
1528
1532
1529 def build(self, target_dir):
1533 def build(self, target_dir):
1530 self.rustbuild()
1534 self.rustbuild()
1531 target = [target_dir]
1535 target = [target_dir]
1532 target.extend(self.name.split('.'))
1536 target.extend(self.name.split('.'))
1533 target[-1] += DYLIB_SUFFIX
1537 target[-1] += DYLIB_SUFFIX
1534 target = os.path.join(*target)
1538 target = os.path.join(*target)
1535 os.makedirs(os.path.dirname(target), exist_ok=True)
1539 os.makedirs(os.path.dirname(target), exist_ok=True)
1536 shutil.copy2(
1540 shutil.copy2(
1537 os.path.join(
1541 os.path.join(
1538 self.rusttargetdir, self.dylibname + self.rustdylibsuffix()
1542 self.rusttargetdir, self.dylibname + self.rustdylibsuffix()
1539 ),
1543 ),
1540 target,
1544 target,
1541 )
1545 )
1542
1546
1543
1547
1544 extmodules = [
1548 extmodules = [
1545 Extension(
1549 Extension(
1546 'mercurial.cext.base85',
1550 'mercurial.cext.base85',
1547 ['mercurial/cext/base85.c'],
1551 ['mercurial/cext/base85.c'],
1548 include_dirs=common_include_dirs,
1552 include_dirs=common_include_dirs,
1549 extra_compile_args=common_cflags,
1553 extra_compile_args=common_cflags,
1550 depends=common_depends,
1554 depends=common_depends,
1551 ),
1555 ),
1552 Extension(
1556 Extension(
1553 'mercurial.cext.bdiff',
1557 'mercurial.cext.bdiff',
1554 ['mercurial/bdiff.c', 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1558 ['mercurial/bdiff.c', 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1555 include_dirs=common_include_dirs,
1559 include_dirs=common_include_dirs,
1556 extra_compile_args=common_cflags,
1560 extra_compile_args=common_cflags,
1557 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers,
1561 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers,
1558 ),
1562 ),
1559 Extension(
1563 Extension(
1560 'mercurial.cext.mpatch',
1564 'mercurial.cext.mpatch',
1561 ['mercurial/mpatch.c', 'mercurial/cext/mpatch.c'],
1565 ['mercurial/mpatch.c', 'mercurial/cext/mpatch.c'],
1562 include_dirs=common_include_dirs,
1566 include_dirs=common_include_dirs,
1563 extra_compile_args=common_cflags,
1567 extra_compile_args=common_cflags,
1564 depends=common_depends,
1568 depends=common_depends,
1565 ),
1569 ),
1566 Extension(
1570 Extension(
1567 'mercurial.cext.parsers',
1571 'mercurial.cext.parsers',
1568 [
1572 [
1569 'mercurial/cext/charencode.c',
1573 'mercurial/cext/charencode.c',
1570 'mercurial/cext/dirs.c',
1574 'mercurial/cext/dirs.c',
1571 'mercurial/cext/manifest.c',
1575 'mercurial/cext/manifest.c',
1572 'mercurial/cext/parsers.c',
1576 'mercurial/cext/parsers.c',
1573 'mercurial/cext/pathencode.c',
1577 'mercurial/cext/pathencode.c',
1574 'mercurial/cext/revlog.c',
1578 'mercurial/cext/revlog.c',
1575 ],
1579 ],
1576 include_dirs=common_include_dirs,
1580 include_dirs=common_include_dirs,
1577 extra_compile_args=common_cflags,
1581 extra_compile_args=common_cflags,
1578 depends=common_depends
1582 depends=common_depends
1579 + [
1583 + [
1580 'mercurial/cext/charencode.h',
1584 'mercurial/cext/charencode.h',
1581 'mercurial/cext/revlog.h',
1585 'mercurial/cext/revlog.h',
1582 ],
1586 ],
1583 ),
1587 ),
1584 Extension(
1588 Extension(
1585 'mercurial.cext.osutil',
1589 'mercurial.cext.osutil',
1586 ['mercurial/cext/osutil.c'],
1590 ['mercurial/cext/osutil.c'],
1587 include_dirs=common_include_dirs,
1591 include_dirs=common_include_dirs,
1588 extra_compile_args=common_cflags + osutil_cflags,
1592 extra_compile_args=common_cflags + osutil_cflags,
1589 extra_link_args=osutil_ldflags,
1593 extra_link_args=osutil_ldflags,
1590 depends=common_depends,
1594 depends=common_depends,
1591 ),
1595 ),
1592 Extension(
1596 Extension(
1593 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations',
1597 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations',
1594 [
1598 [
1595 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1599 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1596 ],
1600 ],
1597 extra_compile_args=common_cflags,
1601 extra_compile_args=common_cflags,
1598 ),
1602 ),
1599 Extension(
1603 Extension(
1600 'mercurial.thirdparty.sha1dc',
1604 'mercurial.thirdparty.sha1dc',
1601 [
1605 [
1602 'mercurial/thirdparty/sha1dc/cext.c',
1606 'mercurial/thirdparty/sha1dc/cext.c',
1603 'mercurial/thirdparty/sha1dc/lib/sha1.c',
1607 'mercurial/thirdparty/sha1dc/lib/sha1.c',
1604 'mercurial/thirdparty/sha1dc/lib/ubc_check.c',
1608 'mercurial/thirdparty/sha1dc/lib/ubc_check.c',
1605 ],
1609 ],
1606 extra_compile_args=common_cflags,
1610 extra_compile_args=common_cflags,
1607 ),
1611 ),
1608 Extension(
1612 Extension(
1609 'hgext.fsmonitor.pywatchman.bser',
1613 'hgext.fsmonitor.pywatchman.bser',
1610 ['hgext/fsmonitor/pywatchman/bser.c'],
1614 ['hgext/fsmonitor/pywatchman/bser.c'],
1611 extra_compile_args=common_cflags,
1615 extra_compile_args=common_cflags,
1612 ),
1616 ),
1613 RustStandaloneExtension(
1617 RustStandaloneExtension(
1614 'mercurial.rustext',
1618 'mercurial.rustext',
1615 'hg-cpython',
1619 'hg-cpython',
1616 'librusthg',
1620 'librusthg',
1617 ),
1621 ),
1618 ]
1622 ]
1619
1623
1620
1624
1621 sys.path.insert(0, 'contrib/python-zstandard')
1625 sys.path.insert(0, 'contrib/python-zstandard')
1622 import setup_zstd
1626 import setup_zstd
1623
1627
1624 zstd = setup_zstd.get_c_extension(
1628 zstd = setup_zstd.get_c_extension(
1625 name='mercurial.zstd', root=os.path.abspath(os.path.dirname(__file__))
1629 name='mercurial.zstd', root=os.path.abspath(os.path.dirname(__file__))
1626 )
1630 )
1627 zstd.extra_compile_args += common_cflags
1631 zstd.extra_compile_args += common_cflags
1628 extmodules.append(zstd)
1632 extmodules.append(zstd)
1629
1633
1630 try:
1634 try:
1631 from distutils import cygwinccompiler
1635 from distutils import cygwinccompiler
1632
1636
1633 # the -mno-cygwin option has been deprecated for years
1637 # the -mno-cygwin option has been deprecated for years
1634 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1638 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1635
1639
1636 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1640 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1637 def __init__(self, *args, **kwargs):
1641 def __init__(self, *args, **kwargs):
1638 mingw32compilerclass.__init__(self, *args, **kwargs)
1642 mingw32compilerclass.__init__(self, *args, **kwargs)
1639 for i in 'compiler compiler_so linker_exe linker_so'.split():
1643 for i in 'compiler compiler_so linker_exe linker_so'.split():
1640 try:
1644 try:
1641 getattr(self, i).remove('-mno-cygwin')
1645 getattr(self, i).remove('-mno-cygwin')
1642 except ValueError:
1646 except ValueError:
1643 pass
1647 pass
1644
1648
1645 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1649 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1646 except ImportError:
1650 except ImportError:
1647 # the cygwinccompiler package is not available on some Python
1651 # the cygwinccompiler package is not available on some Python
1648 # distributions like the ones from the optware project for Synology
1652 # distributions like the ones from the optware project for Synology
1649 # DiskStation boxes
1653 # DiskStation boxes
1650 class HackedMingw32CCompiler:
1654 class HackedMingw32CCompiler:
1651 pass
1655 pass
1652
1656
1653
1657
1654 if os.name == 'nt':
1658 if os.name == 'nt':
1655 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1659 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1656 # extra_link_args to distutils.extensions.Extension() doesn't have any
1660 # extra_link_args to distutils.extensions.Extension() doesn't have any
1657 # effect.
1661 # effect.
1658 from distutils import msvccompiler
1662 from distutils import msvccompiler
1659
1663
1660 msvccompilerclass = msvccompiler.MSVCCompiler
1664 msvccompilerclass = msvccompiler.MSVCCompiler
1661
1665
1662 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1666 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1663 def initialize(self):
1667 def initialize(self):
1664 msvccompilerclass.initialize(self)
1668 msvccompilerclass.initialize(self)
1665 # "warning LNK4197: export 'func' specified multiple times"
1669 # "warning LNK4197: export 'func' specified multiple times"
1666 self.ldflags_shared.append('/ignore:4197')
1670 self.ldflags_shared.append('/ignore:4197')
1667 self.ldflags_shared_debug.append('/ignore:4197')
1671 self.ldflags_shared_debug.append('/ignore:4197')
1668
1672
1669 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1673 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1670
1674
1671 packagedata = {
1675 packagedata = {
1672 'mercurial': [
1676 'mercurial': [
1673 'configitems.toml',
1677 'configitems.toml',
1674 'locale/*/LC_MESSAGES/hg.mo',
1678 'locale/*/LC_MESSAGES/hg.mo',
1675 'dummycert.pem',
1679 'dummycert.pem',
1676 ],
1680 ],
1677 'mercurial.defaultrc': [
1681 'mercurial.defaultrc': [
1678 '*.rc',
1682 '*.rc',
1679 ],
1683 ],
1680 'mercurial.helptext': [
1684 'mercurial.helptext': [
1681 '*.txt',
1685 '*.txt',
1682 ],
1686 ],
1683 'mercurial.helptext.internals': [
1687 'mercurial.helptext.internals': [
1684 '*.txt',
1688 '*.txt',
1685 ],
1689 ],
1686 'mercurial.thirdparty.attr': [
1690 'mercurial.thirdparty.attr': [
1687 '*.pyi',
1691 '*.pyi',
1688 'py.typed',
1692 'py.typed',
1689 ],
1693 ],
1690 }
1694 }
1691
1695
1692
1696
1693 def ordinarypath(p):
1697 def ordinarypath(p):
1694 return p and p[0] != '.' and p[-1] != '~'
1698 return p and p[0] != '.' and p[-1] != '~'
1695
1699
1696
1700
1697 for root in ('templates',):
1701 for root in ('templates',):
1698 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1702 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1699 packagename = curdir.replace(os.sep, '.')
1703 packagename = curdir.replace(os.sep, '.')
1700 packagedata[packagename] = list(filter(ordinarypath, files))
1704 packagedata[packagename] = list(filter(ordinarypath, files))
1701
1705
1702 datafiles = []
1706 datafiles = []
1703
1707
1704 # distutils expects version to be str/unicode. Converting it to
1708 # distutils expects version to be str/unicode. Converting it to
1705 # unicode on Python 2 still works because it won't contain any
1709 # unicode on Python 2 still works because it won't contain any
1706 # non-ascii bytes and will be implicitly converted back to bytes
1710 # non-ascii bytes and will be implicitly converted back to bytes
1707 # when operated on.
1711 # when operated on.
1708 assert isinstance(version, str)
1712 assert isinstance(version, str)
1709 setupversion = version
1713 setupversion = version
1710
1714
1711 extra = {}
1715 extra = {}
1712
1716
1713 py2exepackages = [
1717 py2exepackages = [
1714 'hgdemandimport',
1718 'hgdemandimport',
1715 'hgext3rd',
1719 'hgext3rd',
1716 'hgext',
1720 'hgext',
1717 'email',
1721 'email',
1718 # implicitly imported per module policy
1722 # implicitly imported per module policy
1719 # (cffi wouldn't be used as a frozen exe)
1723 # (cffi wouldn't be used as a frozen exe)
1720 'mercurial.cext',
1724 'mercurial.cext',
1721 #'mercurial.cffi',
1725 #'mercurial.cffi',
1722 'mercurial.pure',
1726 'mercurial.pure',
1723 ]
1727 ]
1724
1728
1725 py2exe_includes = []
1729 py2exe_includes = []
1726
1730
1727 py2exeexcludes = []
1731 py2exeexcludes = []
1728 py2exedllexcludes = ['crypt32.dll']
1732 py2exedllexcludes = ['crypt32.dll']
1729
1733
1730 if issetuptools:
1734 if issetuptools:
1731 extra['python_requires'] = supportedpy
1735 extra['python_requires'] = supportedpy
1732
1736
1733 if py2exeloaded:
1737 if py2exeloaded:
1734 extra['console'] = [
1738 extra['console'] = [
1735 {
1739 {
1736 'script': 'hg',
1740 'script': 'hg',
1737 'copyright': 'Copyright (C) 2005-2023 Olivia Mackall and others',
1741 'copyright': 'Copyright (C) 2005-2023 Olivia Mackall and others',
1738 'product_version': version,
1742 'product_version': version,
1739 }
1743 }
1740 ]
1744 ]
1741 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1745 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1742 # Need to override hgbuild because it has a private copy of
1746 # Need to override hgbuild because it has a private copy of
1743 # build.sub_commands.
1747 # build.sub_commands.
1744 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1748 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1745 # put dlls in sub directory so that they won't pollute PATH
1749 # put dlls in sub directory so that they won't pollute PATH
1746 extra['zipfile'] = 'lib/library.zip'
1750 extra['zipfile'] = 'lib/library.zip'
1747
1751
1748 # We allow some configuration to be supplemented via environment
1752 # We allow some configuration to be supplemented via environment
1749 # variables. This is better than setup.cfg files because it allows
1753 # variables. This is better than setup.cfg files because it allows
1750 # supplementing configs instead of replacing them.
1754 # supplementing configs instead of replacing them.
1751 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1755 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1752 if extrapackages:
1756 if extrapackages:
1753 py2exepackages.extend(extrapackages.split(' '))
1757 py2exepackages.extend(extrapackages.split(' '))
1754
1758
1755 extra_includes = os.environ.get('HG_PY2EXE_EXTRA_INCLUDES')
1759 extra_includes = os.environ.get('HG_PY2EXE_EXTRA_INCLUDES')
1756 if extra_includes:
1760 if extra_includes:
1757 py2exe_includes.extend(extra_includes.split(' '))
1761 py2exe_includes.extend(extra_includes.split(' '))
1758
1762
1759 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1763 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1760 if excludes:
1764 if excludes:
1761 py2exeexcludes.extend(excludes.split(' '))
1765 py2exeexcludes.extend(excludes.split(' '))
1762
1766
1763 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1767 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1764 if dllexcludes:
1768 if dllexcludes:
1765 py2exedllexcludes.extend(dllexcludes.split(' '))
1769 py2exedllexcludes.extend(dllexcludes.split(' '))
1766
1770
1767 if os.environ.get('PYOXIDIZER'):
1771 if os.environ.get('PYOXIDIZER'):
1768 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1772 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1769
1773
1770 if os.name == 'nt':
1774 if os.name == 'nt':
1771 # Windows binary file versions for exe/dll files must have the
1775 # Windows binary file versions for exe/dll files must have the
1772 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1776 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1773 setupversion = setupversion.split(r'+', 1)[0]
1777 setupversion = setupversion.split(r'+', 1)[0]
1774
1778
1775 setup(
1779 setup(
1776 name='mercurial',
1780 name='mercurial',
1777 version=setupversion,
1781 version=setupversion,
1778 author='Olivia Mackall and many others',
1782 author='Olivia Mackall and many others',
1779 author_email='mercurial@mercurial-scm.org',
1783 author_email='mercurial@mercurial-scm.org',
1780 url='https://mercurial-scm.org/',
1784 url='https://mercurial-scm.org/',
1781 download_url='https://mercurial-scm.org/release/',
1785 download_url='https://mercurial-scm.org/release/',
1782 description=(
1786 description=(
1783 'Fast scalable distributed SCM (revision control, version '
1787 'Fast scalable distributed SCM (revision control, version '
1784 'control) system'
1788 'control) system'
1785 ),
1789 ),
1786 long_description=(
1790 long_description=(
1787 'Mercurial is a distributed SCM tool written in Python.'
1791 'Mercurial is a distributed SCM tool written in Python.'
1788 ' It is used by a number of large projects that require'
1792 ' It is used by a number of large projects that require'
1789 ' fast, reliable distributed revision control, such as '
1793 ' fast, reliable distributed revision control, such as '
1790 'Mozilla.'
1794 'Mozilla.'
1791 ),
1795 ),
1792 license='GNU GPLv2 or any later version',
1796 license='GNU GPLv2 or any later version',
1793 classifiers=[
1797 classifiers=[
1794 'Development Status :: 6 - Mature',
1798 'Development Status :: 6 - Mature',
1795 'Environment :: Console',
1799 'Environment :: Console',
1796 'Intended Audience :: Developers',
1800 'Intended Audience :: Developers',
1797 'Intended Audience :: System Administrators',
1801 'Intended Audience :: System Administrators',
1798 'License :: OSI Approved :: GNU General Public License (GPL)',
1802 'License :: OSI Approved :: GNU General Public License (GPL)',
1799 'Natural Language :: Danish',
1803 'Natural Language :: Danish',
1800 'Natural Language :: English',
1804 'Natural Language :: English',
1801 'Natural Language :: German',
1805 'Natural Language :: German',
1802 'Natural Language :: Italian',
1806 'Natural Language :: Italian',
1803 'Natural Language :: Japanese',
1807 'Natural Language :: Japanese',
1804 'Natural Language :: Portuguese (Brazilian)',
1808 'Natural Language :: Portuguese (Brazilian)',
1805 'Operating System :: Microsoft :: Windows',
1809 'Operating System :: Microsoft :: Windows',
1806 'Operating System :: OS Independent',
1810 'Operating System :: OS Independent',
1807 'Operating System :: POSIX',
1811 'Operating System :: POSIX',
1808 'Programming Language :: C',
1812 'Programming Language :: C',
1809 'Programming Language :: Python',
1813 'Programming Language :: Python',
1810 'Topic :: Software Development :: Version Control',
1814 'Topic :: Software Development :: Version Control',
1811 ],
1815 ],
1812 scripts=scripts,
1816 scripts=scripts,
1813 packages=packages,
1817 packages=packages,
1814 ext_modules=extmodules,
1818 ext_modules=extmodules,
1815 data_files=datafiles,
1819 data_files=datafiles,
1816 package_data=packagedata,
1820 package_data=packagedata,
1817 cmdclass=cmdclass,
1821 cmdclass=cmdclass,
1818 distclass=hgdist,
1822 distclass=hgdist,
1819 options={
1823 options={
1820 'py2exe': {
1824 'py2exe': {
1821 'bundle_files': 3,
1825 'bundle_files': 3,
1822 'dll_excludes': py2exedllexcludes,
1826 'dll_excludes': py2exedllexcludes,
1823 'includes': py2exe_includes,
1827 'includes': py2exe_includes,
1824 'excludes': py2exeexcludes,
1828 'excludes': py2exeexcludes,
1825 'packages': py2exepackages,
1829 'packages': py2exepackages,
1826 },
1830 },
1827 'bdist_mpkg': {
1831 'bdist_mpkg': {
1828 'zipdist': False,
1832 'zipdist': False,
1829 'license': 'COPYING',
1833 'license': 'COPYING',
1830 'readme': 'contrib/packaging/macosx/Readme.html',
1834 'readme': 'contrib/packaging/macosx/Readme.html',
1831 'welcome': 'contrib/packaging/macosx/Welcome.html',
1835 'welcome': 'contrib/packaging/macosx/Welcome.html',
1832 },
1836 },
1833 },
1837 },
1834 **extra,
1838 **extra,
1835 )
1839 )
@@ -1,103 +1,102 b''
1 # sshprotoext.py - Extension to test behavior of SSH protocol
1 # sshprotoext.py - Extension to test behavior of SSH protocol
2 #
2 #
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # This extension replaces the SSH server started via `hg serve --stdio`.
8 # This extension replaces the SSH server started via `hg serve --stdio`.
9 # The server behaves differently depending on environment variables.
9 # The server behaves differently depending on environment variables.
10
10
11
11
12 from mercurial import (
12 from mercurial import (
13 error,
13 error,
14 extensions,
14 extensions,
15 registrar,
15 registrar,
16 sshpeer,
16 sshpeer,
17 wireprotoserver,
17 wireprotoserver,
18 wireprotov1server,
18 wireprotov1server,
19 )
19 )
20
20
21 configtable = {}
21 configtable = {}
22 configitem = registrar.configitem(configtable)
22 configitem = registrar.configitem(configtable)
23
23
24 configitem(b'sshpeer', b'mode', default=None)
24 configitem(b'sshpeer', b'mode', default=None)
25 configitem(b'sshpeer', b'handshake-mode', default=None)
25 configitem(b'sshpeer', b'handshake-mode', default=None)
26
26
27
27
28 class bannerserver(wireprotoserver.sshserver):
28 class bannerserver(wireprotoserver.sshserver):
29 """Server that sends a banner to stdout."""
29 """Server that sends a banner to stdout."""
30
30
31 def serve_forever(self):
31 def serve_forever(self):
32 for i in range(10):
32 for i in range(10):
33 self._fout.write(b'banner: line %d\n' % i)
33 self._ui.fout.write(b'banner: line %d\n' % i)
34
34
35 super(bannerserver, self).serve_forever()
35 super(bannerserver, self).serve_forever()
36
36
37
37
38 class prehelloserver(wireprotoserver.sshserver):
38 class prehelloserver(wireprotoserver.sshserver):
39 """Tests behavior when connecting to <0.9.1 servers.
39 """Tests behavior when connecting to <0.9.1 servers.
40
40
41 The ``hello`` wire protocol command was introduced in Mercurial
41 The ``hello`` wire protocol command was introduced in Mercurial
42 0.9.1. Modern clients send the ``hello`` command when connecting
42 0.9.1. Modern clients send the ``hello`` command when connecting
43 to SSH servers. This mock server tests behavior of the handshake
43 to SSH servers. This mock server tests behavior of the handshake
44 when ``hello`` is not supported.
44 when ``hello`` is not supported.
45 """
45 """
46
46
47 def serve_forever(self):
47 def serve_forever(self):
48 l = self._fin.readline()
48 ui = self._ui
49 l = ui.fin.readline()
49 assert l == b'hello\n'
50 assert l == b'hello\n'
50 # Respond to unknown commands with an empty reply.
51 # Respond to unknown commands with an empty reply.
51 wireprotoserver._sshv1respondbytes(self._fout, b'')
52 wireprotoserver._sshv1respondbytes(ui.fout, b'')
52 l = self._fin.readline()
53 l = ui.fin.readline()
53 assert l == b'between\n'
54 assert l == b'between\n'
54 proto = wireprotoserver.sshv1protocolhandler(
55 proto = wireprotoserver.sshv1protocolhandler(ui, ui.fin, ui.fout)
55 self._ui, self._fin, self._fout
56 )
57 rsp = wireprotov1server.dispatch(self._repo, proto, b'between')
56 rsp = wireprotov1server.dispatch(self._repo, proto, b'between')
58 wireprotoserver._sshv1respondbytes(self._fout, rsp.data)
57 wireprotoserver._sshv1respondbytes(ui.fout, rsp.data)
59
58
60 super(prehelloserver, self).serve_forever()
59 super(prehelloserver, self).serve_forever()
61
60
62
61
63 def performhandshake(orig, ui, stdin, stdout, stderr):
62 def performhandshake(orig, ui, stdin, stdout, stderr):
64 """Wrapped version of sshpeer._performhandshake to send extra commands."""
63 """Wrapped version of sshpeer._performhandshake to send extra commands."""
65 mode = ui.config(b'sshpeer', b'handshake-mode')
64 mode = ui.config(b'sshpeer', b'handshake-mode')
66 if mode == b'pre-no-args':
65 if mode == b'pre-no-args':
67 ui.debug(b'sending no-args command\n')
66 ui.debug(b'sending no-args command\n')
68 stdin.write(b'no-args\n')
67 stdin.write(b'no-args\n')
69 stdin.flush()
68 stdin.flush()
70 return orig(ui, stdin, stdout, stderr)
69 return orig(ui, stdin, stdout, stderr)
71 elif mode == b'pre-multiple-no-args':
70 elif mode == b'pre-multiple-no-args':
72 ui.debug(b'sending unknown1 command\n')
71 ui.debug(b'sending unknown1 command\n')
73 stdin.write(b'unknown1\n')
72 stdin.write(b'unknown1\n')
74 ui.debug(b'sending unknown2 command\n')
73 ui.debug(b'sending unknown2 command\n')
75 stdin.write(b'unknown2\n')
74 stdin.write(b'unknown2\n')
76 ui.debug(b'sending unknown3 command\n')
75 ui.debug(b'sending unknown3 command\n')
77 stdin.write(b'unknown3\n')
76 stdin.write(b'unknown3\n')
78 stdin.flush()
77 stdin.flush()
79 return orig(ui, stdin, stdout, stderr)
78 return orig(ui, stdin, stdout, stderr)
80 else:
79 else:
81 raise error.ProgrammingError(b'unknown HANDSHAKECOMMANDMODE: %s' % mode)
80 raise error.ProgrammingError(b'unknown HANDSHAKECOMMANDMODE: %s' % mode)
82
81
83
82
84 def extsetup(ui):
83 def extsetup(ui):
85 # It's easier for tests to define the server behavior via environment
84 # It's easier for tests to define the server behavior via environment
86 # variables than config options. This is because `hg serve --stdio`
85 # variables than config options. This is because `hg serve --stdio`
87 # has to be invoked with a certain form for security reasons and
86 # has to be invoked with a certain form for security reasons and
88 # `dummyssh` can't just add `--config` flags to the command line.
87 # `dummyssh` can't just add `--config` flags to the command line.
89 servermode = ui.environ.get(b'SSHSERVERMODE')
88 servermode = ui.environ.get(b'SSHSERVERMODE')
90
89
91 if servermode == b'banner':
90 if servermode == b'banner':
92 wireprotoserver.sshserver = bannerserver
91 wireprotoserver.sshserver = bannerserver
93 elif servermode == b'no-hello':
92 elif servermode == b'no-hello':
94 wireprotoserver.sshserver = prehelloserver
93 wireprotoserver.sshserver = prehelloserver
95 elif servermode:
94 elif servermode:
96 raise error.ProgrammingError(b'unknown server mode: %s' % servermode)
95 raise error.ProgrammingError(b'unknown server mode: %s' % servermode)
97
96
98 peermode = ui.config(b'sshpeer', b'mode')
97 peermode = ui.config(b'sshpeer', b'mode')
99
98
100 if peermode == b'extra-handshake-commands':
99 if peermode == b'extra-handshake-commands':
101 extensions.wrapfunction(sshpeer, '_performhandshake', performhandshake)
100 extensions.wrapfunction(sshpeer, '_performhandshake', performhandshake)
102 elif peermode:
101 elif peermode:
103 raise error.ProgrammingError(b'unknown peer mode: %s' % peermode)
102 raise error.ProgrammingError(b'unknown peer mode: %s' % peermode)
@@ -1,622 +1,712 b''
1 bundle w/o type option
1 bundle w/o type option
2
2
3 $ hg init t1
3 $ hg init t1
4 $ hg init t2
4 $ hg init t2
5 $ cd t1
5 $ cd t1
6 $ echo blablablablabla > file.txt
6 $ echo blablablablabla > file.txt
7 $ hg ci -A -m commit_root
7 $ hg ci -A -m commit_root
8 adding file.txt
8 adding file.txt
9 $ echo kapoue > file.txt
9 $ echo kapoue > file.txt
10 $ hg ci -m commit_1
10 $ hg ci -m commit_1
11 $ echo scrabageul > file.txt
11 $ echo scrabageul > file.txt
12 $ hg ci -m commit_2
12 $ hg ci -m commit_2
13 $ hg up 'desc("commit_root")'
13 $ hg up 'desc("commit_root")'
14 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
14 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
15 $ echo flagabalagla > file.txt
15 $ echo flagabalagla > file.txt
16 $ hg ci -m commit_3
16 $ hg ci -m commit_3
17 created new head
17 created new head
18 $ echo aliofia > file.txt
18 $ echo aliofia > file.txt
19 $ hg ci -m commit_4
19 $ hg ci -m commit_4
20 $ echo alklqo > file.txt
20 $ echo alklqo > file.txt
21 $ hg ci -m commit_5
21 $ hg ci -m commit_5
22 $ echo peakfeo > file.txt
22 $ echo peakfeo > file.txt
23 $ hg ci -m commit_6 --secret
23 $ hg ci -m commit_6 --secret
24 $ hg phase --public --rev 'desc(commit_3)'
24 $ hg phase --public --rev 'desc(commit_3)'
25 $ hg log -GT '[{phase}] {desc|firstline}\n'
25 $ hg log -GT '[{phase}] {desc|firstline}\n'
26 @ [secret] commit_6
26 @ [secret] commit_6
27 |
27 |
28 o [draft] commit_5
28 o [draft] commit_5
29 |
29 |
30 o [draft] commit_4
30 o [draft] commit_4
31 |
31 |
32 o [public] commit_3
32 o [public] commit_3
33 |
33 |
34 | o [draft] commit_2
34 | o [draft] commit_2
35 | |
35 | |
36 | o [draft] commit_1
36 | o [draft] commit_1
37 |/
37 |/
38 o [public] commit_root
38 o [public] commit_root
39
39
40
40
41 XXX the bundle generation is defined by a discovery round here. So the secret
41 XXX the bundle generation is defined by a discovery round here. So the secret
42 changeset should be excluded.
42 changeset should be excluded.
43
43
44 $ hg bundle ../b1.hg ../t2
44 $ hg bundle ../b1.hg ../t2
45 searching for changes
45 searching for changes
46 7 changesets found (known-bad-output !)
46 7 changesets found (known-bad-output !)
47 6 changesets found (missing-correct-output !)
47 6 changesets found (missing-correct-output !)
48 $ cd ..
48 $ cd ..
49
49
50 $ hg -R t2 unbundle ./b1.hg
50 $ hg -R t2 unbundle ./b1.hg
51 adding changesets
51 adding changesets
52 adding manifests
52 adding manifests
53 adding file changes
53 adding file changes
54 added 7 changesets with 7 changes to 1 files (+1 heads) (known-bad-output !)
54 added 7 changesets with 7 changes to 1 files (+1 heads) (known-bad-output !)
55 added 6 changesets with 6 changes to 1 files (+1 heads) (missing-correct-output !)
55 added 6 changesets with 6 changes to 1 files (+1 heads) (missing-correct-output !)
56 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
56 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
57 (run 'hg heads' to see heads, 'hg merge' to merge)
57 (run 'hg heads' to see heads, 'hg merge' to merge)
58 $ hg -R t2 up
58 $ hg -R t2 up
59 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
59 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
60 updated to "b9f5f740a8cd: commit_6"
60 updated to "b9f5f740a8cd: commit_6"
61 1 other heads for branch "default"
61 1 other heads for branch "default"
62 $ hg -R t2 log -GT '[{phase}] {desc|firstline}\n'
62 $ hg -R t2 log -GT '[{phase}] {desc|firstline}\n'
63 @ [draft] commit_6 (known-bad-output !)
63 @ [draft] commit_6 (known-bad-output !)
64 | (known-bad-output !)
64 | (known-bad-output !)
65 o [draft] commit_5
65 o [draft] commit_5
66 |
66 |
67 o [draft] commit_4
67 o [draft] commit_4
68 |
68 |
69 o [draft] commit_3
69 o [draft] commit_3
70 |
70 |
71 | o [draft] commit_2
71 | o [draft] commit_2
72 | |
72 | |
73 | o [draft] commit_1
73 | o [draft] commit_1
74 |/
74 |/
75 o [draft] commit_root
75 o [draft] commit_root
76
76
77
77
78 Unknown compression type is rejected
78 Unknown compression type is rejected
79
79
80 $ hg init t3
80 $ hg init t3
81 $ hg -R t3 -q unbundle ./b1.hg
81 $ hg -R t3 -q unbundle ./b1.hg
82 $ hg -R t3 bundle -a -t unknown out.hg
82 $ hg -R t3 bundle -a -t unknown out.hg
83 abort: unknown is not a recognized bundle specification
83 abort: unknown is not a recognized bundle specification
84 (see 'hg help bundlespec' for supported values for --type)
84 (see 'hg help bundlespec' for supported values for --type)
85 [10]
85 [10]
86
86
87 $ hg -R t3 bundle -a -t unknown-v2 out.hg
87 $ hg -R t3 bundle -a -t unknown-v2 out.hg
88 abort: unknown compression is not supported
88 abort: unknown compression is not supported
89 (see 'hg help bundlespec' for supported values for --type)
89 (see 'hg help bundlespec' for supported values for --type)
90 [10]
90 [10]
91
91
92 test bundle types
92 test bundle types
93 =================
93 =================
94
94
95 since we use --all, it is okay to include the secret changeset here. It is
95 since we use --all, it is okay to include the secret changeset here. It is
96 unfortunate that the phase information for the secret one is lost.
96 unfortunate that the phase information for the secret one is lost.
97
97
98 $ testbundle() {
98 $ testbundle() {
99 > echo % test bundle type $1
99 > echo % test bundle type $1
100 > echo '==================================='
100 > echo '==================================='
101 > hg -R t1 bundle --all --type $1 ./b-$1.hg
101 > hg -R t1 bundle --all --type $1 ./b-$1.hg
102 > f -q -B6 -D ./b-$1.hg; echo
102 > f -q -B6 -D ./b-$1.hg; echo
103 > hg debugbundle ./b-$1.hg
103 > hg debugbundle ./b-$1.hg
104 > hg debugbundle --spec ./b-$1.hg
104 > hg debugbundle --spec ./b-$1.hg
105 > echo
105 > echo
106 > hg init repo-from-type-$1
106 > hg init repo-from-type-$1
107 > hg unbundle -R repo-from-type-$1 ./b-$1.hg
107 > hg unbundle -R repo-from-type-$1 ./b-$1.hg
108 > hg -R repo-from-type-$1 log -GT '[{phase}] {desc|firstline}\n'
108 > hg -R repo-from-type-$1 log -GT '[{phase}] {desc|firstline}\n'
109 > echo
109 > echo
110 > }
110 > }
111
111
112 $ for t in "None" "bzip2" "gzip" "none-v2" "v2" "v1" "gzip-v1" "v3"; do
112 $ for t in "None" "bzip2" "gzip" "none-v2" "v2" "v1" "gzip-v1" "v3"; do
113 > testbundle $t
113 > testbundle $t
114 > done
114 > done
115 % test bundle type None
115 % test bundle type None
116 ===================================
116 ===================================
117 7 changesets found
117 7 changesets found
118 HG20\x00\x00 (esc)
118 HG20\x00\x00 (esc)
119 Stream params: {}
119 Stream params: {}
120 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
120 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
121 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
121 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
122 901e97fadc587978ec52f2fa76af4aefc2d191e8
122 901e97fadc587978ec52f2fa76af4aefc2d191e8
123 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
123 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
124 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
124 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
125 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
125 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
126 2ea90778052ba7558fab36e3fd5d149512ff986b
126 2ea90778052ba7558fab36e3fd5d149512ff986b
127 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
127 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
128 cache:rev-branch-cache -- {} (mandatory: False)
128 cache:rev-branch-cache -- {} (mandatory: False)
129 none-v2
129 none-v2
130
130
131 adding changesets
131 adding changesets
132 adding manifests
132 adding manifests
133 adding file changes
133 adding file changes
134 added 7 changesets with 7 changes to 1 files (+1 heads)
134 added 7 changesets with 7 changes to 1 files (+1 heads)
135 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
135 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
136 (run 'hg heads' to see heads, 'hg merge' to merge)
136 (run 'hg heads' to see heads, 'hg merge' to merge)
137 o [draft] commit_6
137 o [draft] commit_6
138 |
138 |
139 o [draft] commit_5
139 o [draft] commit_5
140 |
140 |
141 o [draft] commit_4
141 o [draft] commit_4
142 |
142 |
143 o [draft] commit_3
143 o [draft] commit_3
144 |
144 |
145 | o [draft] commit_2
145 | o [draft] commit_2
146 | |
146 | |
147 | o [draft] commit_1
147 | o [draft] commit_1
148 |/
148 |/
149 o [draft] commit_root
149 o [draft] commit_root
150
150
151
151
152 % test bundle type bzip2
152 % test bundle type bzip2
153 ===================================
153 ===================================
154 7 changesets found
154 7 changesets found
155 HG20\x00\x00 (esc)
155 HG20\x00\x00 (esc)
156 Stream params: {Compression: BZ}
156 Stream params: {Compression: BZ}
157 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
157 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
158 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
158 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
159 901e97fadc587978ec52f2fa76af4aefc2d191e8
159 901e97fadc587978ec52f2fa76af4aefc2d191e8
160 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
160 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
161 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
161 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
162 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
162 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
163 2ea90778052ba7558fab36e3fd5d149512ff986b
163 2ea90778052ba7558fab36e3fd5d149512ff986b
164 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
164 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
165 cache:rev-branch-cache -- {} (mandatory: False)
165 cache:rev-branch-cache -- {} (mandatory: False)
166 bzip2-v2
166 bzip2-v2
167
167
168 adding changesets
168 adding changesets
169 adding manifests
169 adding manifests
170 adding file changes
170 adding file changes
171 added 7 changesets with 7 changes to 1 files (+1 heads)
171 added 7 changesets with 7 changes to 1 files (+1 heads)
172 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
172 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
173 (run 'hg heads' to see heads, 'hg merge' to merge)
173 (run 'hg heads' to see heads, 'hg merge' to merge)
174 o [draft] commit_6
174 o [draft] commit_6
175 |
175 |
176 o [draft] commit_5
176 o [draft] commit_5
177 |
177 |
178 o [draft] commit_4
178 o [draft] commit_4
179 |
179 |
180 o [draft] commit_3
180 o [draft] commit_3
181 |
181 |
182 | o [draft] commit_2
182 | o [draft] commit_2
183 | |
183 | |
184 | o [draft] commit_1
184 | o [draft] commit_1
185 |/
185 |/
186 o [draft] commit_root
186 o [draft] commit_root
187
187
188
188
189 % test bundle type gzip
189 % test bundle type gzip
190 ===================================
190 ===================================
191 7 changesets found
191 7 changesets found
192 HG20\x00\x00 (esc)
192 HG20\x00\x00 (esc)
193 Stream params: {Compression: GZ}
193 Stream params: {Compression: GZ}
194 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
194 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
195 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
195 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
196 901e97fadc587978ec52f2fa76af4aefc2d191e8
196 901e97fadc587978ec52f2fa76af4aefc2d191e8
197 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
197 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
198 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
198 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
199 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
199 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
200 2ea90778052ba7558fab36e3fd5d149512ff986b
200 2ea90778052ba7558fab36e3fd5d149512ff986b
201 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
201 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
202 cache:rev-branch-cache -- {} (mandatory: False)
202 cache:rev-branch-cache -- {} (mandatory: False)
203 gzip-v2
203 gzip-v2
204
204
205 adding changesets
205 adding changesets
206 adding manifests
206 adding manifests
207 adding file changes
207 adding file changes
208 added 7 changesets with 7 changes to 1 files (+1 heads)
208 added 7 changesets with 7 changes to 1 files (+1 heads)
209 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
209 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
210 (run 'hg heads' to see heads, 'hg merge' to merge)
210 (run 'hg heads' to see heads, 'hg merge' to merge)
211 o [draft] commit_6
211 o [draft] commit_6
212 |
212 |
213 o [draft] commit_5
213 o [draft] commit_5
214 |
214 |
215 o [draft] commit_4
215 o [draft] commit_4
216 |
216 |
217 o [draft] commit_3
217 o [draft] commit_3
218 |
218 |
219 | o [draft] commit_2
219 | o [draft] commit_2
220 | |
220 | |
221 | o [draft] commit_1
221 | o [draft] commit_1
222 |/
222 |/
223 o [draft] commit_root
223 o [draft] commit_root
224
224
225
225
226 % test bundle type none-v2
226 % test bundle type none-v2
227 ===================================
227 ===================================
228 7 changesets found
228 7 changesets found
229 HG20\x00\x00 (esc)
229 HG20\x00\x00 (esc)
230 Stream params: {}
230 Stream params: {}
231 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
231 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
232 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
232 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
233 901e97fadc587978ec52f2fa76af4aefc2d191e8
233 901e97fadc587978ec52f2fa76af4aefc2d191e8
234 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
234 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
235 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
235 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
236 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
236 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
237 2ea90778052ba7558fab36e3fd5d149512ff986b
237 2ea90778052ba7558fab36e3fd5d149512ff986b
238 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
238 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
239 cache:rev-branch-cache -- {} (mandatory: False)
239 cache:rev-branch-cache -- {} (mandatory: False)
240 none-v2
240 none-v2
241
241
242 adding changesets
242 adding changesets
243 adding manifests
243 adding manifests
244 adding file changes
244 adding file changes
245 added 7 changesets with 7 changes to 1 files (+1 heads)
245 added 7 changesets with 7 changes to 1 files (+1 heads)
246 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
246 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
247 (run 'hg heads' to see heads, 'hg merge' to merge)
247 (run 'hg heads' to see heads, 'hg merge' to merge)
248 o [draft] commit_6
248 o [draft] commit_6
249 |
249 |
250 o [draft] commit_5
250 o [draft] commit_5
251 |
251 |
252 o [draft] commit_4
252 o [draft] commit_4
253 |
253 |
254 o [draft] commit_3
254 o [draft] commit_3
255 |
255 |
256 | o [draft] commit_2
256 | o [draft] commit_2
257 | |
257 | |
258 | o [draft] commit_1
258 | o [draft] commit_1
259 |/
259 |/
260 o [draft] commit_root
260 o [draft] commit_root
261
261
262
262
263 % test bundle type v2
263 % test bundle type v2
264 ===================================
264 ===================================
265 7 changesets found
265 7 changesets found
266 HG20\x00\x00 (esc)
266 HG20\x00\x00 (esc)
267 Stream params: {Compression: BZ}
267 Stream params: {Compression: BZ}
268 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
268 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
269 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
269 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
270 901e97fadc587978ec52f2fa76af4aefc2d191e8
270 901e97fadc587978ec52f2fa76af4aefc2d191e8
271 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
271 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
272 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
272 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
273 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
273 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
274 2ea90778052ba7558fab36e3fd5d149512ff986b
274 2ea90778052ba7558fab36e3fd5d149512ff986b
275 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
275 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
276 cache:rev-branch-cache -- {} (mandatory: False)
276 cache:rev-branch-cache -- {} (mandatory: False)
277 bzip2-v2
277 bzip2-v2
278
278
279 adding changesets
279 adding changesets
280 adding manifests
280 adding manifests
281 adding file changes
281 adding file changes
282 added 7 changesets with 7 changes to 1 files (+1 heads)
282 added 7 changesets with 7 changes to 1 files (+1 heads)
283 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
283 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
284 (run 'hg heads' to see heads, 'hg merge' to merge)
284 (run 'hg heads' to see heads, 'hg merge' to merge)
285 o [draft] commit_6
285 o [draft] commit_6
286 |
286 |
287 o [draft] commit_5
287 o [draft] commit_5
288 |
288 |
289 o [draft] commit_4
289 o [draft] commit_4
290 |
290 |
291 o [draft] commit_3
291 o [draft] commit_3
292 |
292 |
293 | o [draft] commit_2
293 | o [draft] commit_2
294 | |
294 | |
295 | o [draft] commit_1
295 | o [draft] commit_1
296 |/
296 |/
297 o [draft] commit_root
297 o [draft] commit_root
298
298
299
299
300 % test bundle type v1
300 % test bundle type v1
301 ===================================
301 ===================================
302 7 changesets found
302 7 changesets found
303 HG10BZ
303 HG10BZ
304 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
304 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
305 901e97fadc587978ec52f2fa76af4aefc2d191e8
305 901e97fadc587978ec52f2fa76af4aefc2d191e8
306 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
306 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
307 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
307 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
308 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
308 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
309 2ea90778052ba7558fab36e3fd5d149512ff986b
309 2ea90778052ba7558fab36e3fd5d149512ff986b
310 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
310 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
311 bzip2-v1
311 bzip2-v1
312
312
313 adding changesets
313 adding changesets
314 adding manifests
314 adding manifests
315 adding file changes
315 adding file changes
316 added 7 changesets with 7 changes to 1 files (+1 heads)
316 added 7 changesets with 7 changes to 1 files (+1 heads)
317 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
317 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
318 (run 'hg heads' to see heads, 'hg merge' to merge)
318 (run 'hg heads' to see heads, 'hg merge' to merge)
319 o [draft] commit_6
319 o [draft] commit_6
320 |
320 |
321 o [draft] commit_5
321 o [draft] commit_5
322 |
322 |
323 o [draft] commit_4
323 o [draft] commit_4
324 |
324 |
325 o [draft] commit_3
325 o [draft] commit_3
326 |
326 |
327 | o [draft] commit_2
327 | o [draft] commit_2
328 | |
328 | |
329 | o [draft] commit_1
329 | o [draft] commit_1
330 |/
330 |/
331 o [draft] commit_root
331 o [draft] commit_root
332
332
333
333
334 % test bundle type gzip-v1
334 % test bundle type gzip-v1
335 ===================================
335 ===================================
336 7 changesets found
336 7 changesets found
337 HG10GZ
337 HG10GZ
338 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
338 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
339 901e97fadc587978ec52f2fa76af4aefc2d191e8
339 901e97fadc587978ec52f2fa76af4aefc2d191e8
340 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
340 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
341 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
341 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
342 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
342 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
343 2ea90778052ba7558fab36e3fd5d149512ff986b
343 2ea90778052ba7558fab36e3fd5d149512ff986b
344 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
344 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
345 gzip-v1
345 gzip-v1
346
346
347 adding changesets
347 adding changesets
348 adding manifests
348 adding manifests
349 adding file changes
349 adding file changes
350 added 7 changesets with 7 changes to 1 files (+1 heads)
350 added 7 changesets with 7 changes to 1 files (+1 heads)
351 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
351 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
352 (run 'hg heads' to see heads, 'hg merge' to merge)
352 (run 'hg heads' to see heads, 'hg merge' to merge)
353 o [draft] commit_6
353 o [draft] commit_6
354 |
354 |
355 o [draft] commit_5
355 o [draft] commit_5
356 |
356 |
357 o [draft] commit_4
357 o [draft] commit_4
358 |
358 |
359 o [draft] commit_3
359 o [draft] commit_3
360 |
360 |
361 | o [draft] commit_2
361 | o [draft] commit_2
362 | |
362 | |
363 | o [draft] commit_1
363 | o [draft] commit_1
364 |/
364 |/
365 o [draft] commit_root
365 o [draft] commit_root
366
366
367
367
368 % test bundle type v3
368 % test bundle type v3
369 ===================================
369 ===================================
370 7 changesets found
370 7 changesets found
371 HG20\x00\x00 (esc)
371 HG20\x00\x00 (esc)
372 Stream params: {Compression: BZ}
372 Stream params: {Compression: BZ}
373 changegroup -- {nbchanges: 7, targetphase: 2, version: 03} (mandatory: True)
373 changegroup -- {nbchanges: 7, targetphase: 2, version: 03} (mandatory: True)
374 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
374 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
375 901e97fadc587978ec52f2fa76af4aefc2d191e8
375 901e97fadc587978ec52f2fa76af4aefc2d191e8
376 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
376 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
377 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
377 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
378 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
378 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
379 2ea90778052ba7558fab36e3fd5d149512ff986b
379 2ea90778052ba7558fab36e3fd5d149512ff986b
380 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
380 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
381 cache:rev-branch-cache -- {} (mandatory: False)
381 cache:rev-branch-cache -- {} (mandatory: False)
382 phase-heads -- {} (mandatory: True)
382 phase-heads -- {} (mandatory: True)
383 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d public
383 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d public
384 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55 draft
384 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55 draft
385 2ea90778052ba7558fab36e3fd5d149512ff986b draft
385 2ea90778052ba7558fab36e3fd5d149512ff986b draft
386 b9f5f740a8cd76700020e3903ee55ecff78bd3e5 secret
386 b9f5f740a8cd76700020e3903ee55ecff78bd3e5 secret
387 bzip2-v2;cg.version=03
387 bzip2-v2;cg.version=03
388
388
389 adding changesets
389 adding changesets
390 adding manifests
390 adding manifests
391 adding file changes
391 adding file changes
392 added 7 changesets with 7 changes to 1 files (+1 heads)
392 added 7 changesets with 7 changes to 1 files (+1 heads)
393 new changesets ac39af4a9f7d:b9f5f740a8cd (4 drafts, 1 secrets)
393 new changesets ac39af4a9f7d:b9f5f740a8cd (4 drafts, 1 secrets)
394 (run 'hg heads' to see heads, 'hg merge' to merge)
394 (run 'hg heads' to see heads, 'hg merge' to merge)
395 o [secret] commit_6
395 o [secret] commit_6
396 |
396 |
397 o [draft] commit_5
397 o [draft] commit_5
398 |
398 |
399 o [draft] commit_4
399 o [draft] commit_4
400 |
400 |
401 o [public] commit_3
401 o [public] commit_3
402 |
402 |
403 | o [draft] commit_2
403 | o [draft] commit_2
404 | |
404 | |
405 | o [draft] commit_1
405 | o [draft] commit_1
406 |/
406 |/
407 o [public] commit_root
407 o [public] commit_root
408
408
409
409
410
410
411 Compression level can be adjusted for bundle2 bundles
411 Compression level can be adjusted for bundle2 bundles
412
412
413 $ hg init test-complevel
413 $ hg init test-complevel
414 $ cd test-complevel
414 $ cd test-complevel
415
415
416 $ cat > file0 << EOF
416 $ cat > file0 << EOF
417 > this is a file
417 > this is a file
418 > with some text
418 > with some text
419 > and some more text
419 > and some more text
420 > and other content
420 > and other content
421 > EOF
421 > EOF
422 $ cat > file1 << EOF
422 $ cat > file1 << EOF
423 > this is another file
423 > this is another file
424 > with some other content
424 > with some other content
425 > and repeated, repeated, repeated, repeated content
425 > and repeated, repeated, repeated, repeated content
426 > EOF
426 > EOF
427 $ hg -q commit -A -m initial
427 $ hg -q commit -A -m initial
428
428
429 $ hg bundle -a -t gzip-v2 gzip-v2.hg
429 $ hg bundle -a -t gzip-v2 gzip-v2.hg
430 1 changesets found
430 1 changesets found
431 $ f --size gzip-v2.hg
431 $ f --size gzip-v2.hg
432 gzip-v2.hg: size=468
432 gzip-v2.hg: size=468
433
433
434 $ hg --config experimental.bundlecomplevel=1 bundle -a -t gzip-v2 gzip-v2-level1.hg
434 $ hg --config experimental.bundlecomplevel=1 bundle -a -t gzip-v2 gzip-v2-level1.hg
435 1 changesets found
435 1 changesets found
436 $ f --size gzip-v2-level1.hg
436 $ f --size gzip-v2-level1.hg
437 gzip-v2-level1.hg: size=475
437 gzip-v2-level1.hg: size=475
438
438
439 $ hg --config experimental.bundlecomplevel.gzip=1 --config experimental.bundlelevel=9 bundle -a -t gzip-v2 gzip-v2-level1.hg
439 $ hg --config experimental.bundlecomplevel.gzip=1 --config experimental.bundlelevel=9 bundle -a -t gzip-v2 gzip-v2-level1.hg
440 1 changesets found
440 1 changesets found
441 $ f --size gzip-v2-level1.hg
441 $ f --size gzip-v2-level1.hg
442 gzip-v2-level1.hg: size=475
442 gzip-v2-level1.hg: size=475
443
443
444 $ cd ..
444 $ cd ..
445
445
446 #if zstd
446 #if zstd
447
447
448 $ for t in "zstd" "zstd-v2"; do
448 $ for t in "zstd" "zstd-v2"; do
449 > testbundle $t
449 > testbundle $t
450 > done
450 > done
451 % test bundle type zstd
451 % test bundle type zstd
452 ===================================
452 ===================================
453 7 changesets found
453 7 changesets found
454 HG20\x00\x00 (esc)
454 HG20\x00\x00 (esc)
455 Stream params: {Compression: ZS}
455 Stream params: {Compression: ZS}
456 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
456 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
457 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
457 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
458 901e97fadc587978ec52f2fa76af4aefc2d191e8
458 901e97fadc587978ec52f2fa76af4aefc2d191e8
459 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
459 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
460 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
460 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
461 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
461 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
462 2ea90778052ba7558fab36e3fd5d149512ff986b
462 2ea90778052ba7558fab36e3fd5d149512ff986b
463 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
463 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
464 cache:rev-branch-cache -- {} (mandatory: False)
464 cache:rev-branch-cache -- {} (mandatory: False)
465 zstd-v2
465 zstd-v2
466
466
467 adding changesets
467 adding changesets
468 adding manifests
468 adding manifests
469 adding file changes
469 adding file changes
470 added 7 changesets with 7 changes to 1 files (+1 heads)
470 added 7 changesets with 7 changes to 1 files (+1 heads)
471 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
471 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
472 (run 'hg heads' to see heads, 'hg merge' to merge)
472 (run 'hg heads' to see heads, 'hg merge' to merge)
473 o [draft] commit_6
473 o [draft] commit_6
474 |
474 |
475 o [draft] commit_5
475 o [draft] commit_5
476 |
476 |
477 o [draft] commit_4
477 o [draft] commit_4
478 |
478 |
479 o [draft] commit_3
479 o [draft] commit_3
480 |
480 |
481 | o [draft] commit_2
481 | o [draft] commit_2
482 | |
482 | |
483 | o [draft] commit_1
483 | o [draft] commit_1
484 |/
484 |/
485 o [draft] commit_root
485 o [draft] commit_root
486
486
487
487
488 % test bundle type zstd-v2
488 % test bundle type zstd-v2
489 ===================================
489 ===================================
490 7 changesets found
490 7 changesets found
491 HG20\x00\x00 (esc)
491 HG20\x00\x00 (esc)
492 Stream params: {Compression: ZS}
492 Stream params: {Compression: ZS}
493 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
493 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
494 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
494 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
495 901e97fadc587978ec52f2fa76af4aefc2d191e8
495 901e97fadc587978ec52f2fa76af4aefc2d191e8
496 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
496 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
497 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
497 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
498 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
498 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
499 2ea90778052ba7558fab36e3fd5d149512ff986b
499 2ea90778052ba7558fab36e3fd5d149512ff986b
500 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
500 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
501 cache:rev-branch-cache -- {} (mandatory: False)
501 cache:rev-branch-cache -- {} (mandatory: False)
502 zstd-v2
502 zstd-v2
503
503
504 adding changesets
504 adding changesets
505 adding manifests
505 adding manifests
506 adding file changes
506 adding file changes
507 added 7 changesets with 7 changes to 1 files (+1 heads)
507 added 7 changesets with 7 changes to 1 files (+1 heads)
508 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
508 new changesets ac39af4a9f7d:b9f5f740a8cd (7 drafts)
509 (run 'hg heads' to see heads, 'hg merge' to merge)
509 (run 'hg heads' to see heads, 'hg merge' to merge)
510 o [draft] commit_6
510 o [draft] commit_6
511 |
511 |
512 o [draft] commit_5
512 o [draft] commit_5
513 |
513 |
514 o [draft] commit_4
514 o [draft] commit_4
515 |
515 |
516 o [draft] commit_3
516 o [draft] commit_3
517 |
517 |
518 | o [draft] commit_2
518 | o [draft] commit_2
519 | |
519 | |
520 | o [draft] commit_1
520 | o [draft] commit_1
521 |/
521 |/
522 o [draft] commit_root
522 o [draft] commit_root
523
523
524
524
525
525
526 Explicit request for zstd on non-generaldelta repos
526 Explicit request for zstd on non-generaldelta repos
527
527
528 $ hg --config format.usegeneraldelta=false init nogd
528 $ hg --config format.usegeneraldelta=false init nogd
529 $ hg -q -R nogd pull t1
529 $ hg -q -R nogd pull t1
530 $ hg -R nogd bundle -a -t zstd nogd-zstd
530 $ hg -R nogd bundle -a -t zstd nogd-zstd
531 6 changesets found
531 6 changesets found
532
532
533 zstd-v1 always fails
533 zstd-v1 always fails
534
534
535 $ hg -R t1 bundle -a -t zstd-v1 zstd-v1
535 $ hg -R t1 bundle -a -t zstd-v1 zstd-v1
536 abort: compression engine zstd is not supported on v1 bundles
536 abort: compression engine zstd is not supported on v1 bundles
537 (see 'hg help bundlespec' for supported values for --type)
537 (see 'hg help bundlespec' for supported values for --type)
538 [10]
538 [10]
539
539
540 zstd supports threading
540 zstd supports threading
541
541
542 $ hg init test-compthreads
542 $ hg init test-compthreads
543 $ cd test-compthreads
543 $ cd test-compthreads
544 $ hg debugbuilddag +3
544 $ hg debugbuilddag +3
545 $ hg --config experimental.bundlecompthreads=1 bundle -a -t zstd-v2 zstd-v2-threaded.hg
545 $ hg --config experimental.bundlecompthreads=1 bundle -a -t zstd-v2 zstd-v2-threaded.hg
546 3 changesets found
546 3 changesets found
547 $ cd ..
547 $ cd ..
548
548
549 #else
549 #else
550
550
551 zstd is a valid engine but isn't available
551 zstd is a valid engine but isn't available
552
552
553 $ hg -R t1 bundle -a -t zstd irrelevant.hg
553 $ hg -R t1 bundle -a -t zstd irrelevant.hg
554 abort: compression engine zstd could not be loaded
554 abort: compression engine zstd could not be loaded
555 [255]
555 [255]
556
556
557 #endif
557 #endif
558
558
559 test garbage file
559 test garbage file
560
560
561 $ echo garbage > bgarbage
561 $ echo garbage > bgarbage
562 $ hg init tgarbage
562 $ hg init tgarbage
563 $ cd tgarbage
563 $ cd tgarbage
564 $ hg pull ../bgarbage
564 $ hg pull ../bgarbage
565 pulling from ../bgarbage
565 pulling from ../bgarbage
566 abort: ../bgarbage: not a Mercurial bundle
566 abort: ../bgarbage: not a Mercurial bundle
567 [255]
567 [255]
568 $ cd ..
568 $ cd ..
569
569
570 test invalid bundle type
570 test invalid bundle type
571
571
572 $ cd t1
572 $ cd t1
573 $ hg bundle -a -t garbage ../bgarbage
573 $ hg bundle -a -t garbage ../bgarbage
574 abort: garbage is not a recognized bundle specification
574 abort: garbage is not a recognized bundle specification
575 (see 'hg help bundlespec' for supported values for --type)
575 (see 'hg help bundlespec' for supported values for --type)
576 [10]
576 [10]
577 $ cd ..
577 $ cd ..
578
578
579 Test controlling the changegroup version
579 Test controlling the changegroup version
580
580
581 $ hg -R t1 bundle --config experimental.changegroup3=yes -a -t v2 ./v2-cg-default.hg
581 $ hg -R t1 bundle --config experimental.changegroup3=yes -a -t v2 ./v2-cg-default.hg
582 7 changesets found
582 7 changesets found
583 $ hg debugbundle ./v2-cg-default.hg --part-type changegroup
583 $ hg debugbundle ./v2-cg-default.hg --part-type changegroup
584 Stream params: {Compression: BZ}
584 Stream params: {Compression: BZ}
585 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
585 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
586 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
586 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
587 901e97fadc587978ec52f2fa76af4aefc2d191e8
587 901e97fadc587978ec52f2fa76af4aefc2d191e8
588 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
588 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
589 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
589 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
590 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
590 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
591 2ea90778052ba7558fab36e3fd5d149512ff986b
591 2ea90778052ba7558fab36e3fd5d149512ff986b
592 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
592 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
593 $ hg debugbundle ./v2-cg-default.hg --spec
593 $ hg debugbundle ./v2-cg-default.hg --spec
594 bzip2-v2
594 bzip2-v2
595 $ hg -R t1 bundle --config experimental.changegroup3=yes -a -t 'v2;cg.version=02' ./v2-cg-02.hg
595 $ hg -R t1 bundle --config experimental.changegroup3=yes -a -t 'v2;cg.version=02' ./v2-cg-02.hg
596 7 changesets found
596 7 changesets found
597 $ hg debugbundle ./v2-cg-02.hg --part-type changegroup
597 $ hg debugbundle ./v2-cg-02.hg --part-type changegroup
598 Stream params: {Compression: BZ}
598 Stream params: {Compression: BZ}
599 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
599 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
600 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
600 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
601 901e97fadc587978ec52f2fa76af4aefc2d191e8
601 901e97fadc587978ec52f2fa76af4aefc2d191e8
602 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
602 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
603 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
603 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
604 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
604 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
605 2ea90778052ba7558fab36e3fd5d149512ff986b
605 2ea90778052ba7558fab36e3fd5d149512ff986b
606 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
606 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
607 $ hg debugbundle ./v2-cg-02.hg --spec
607 $ hg debugbundle ./v2-cg-02.hg --spec
608 bzip2-v2
608 bzip2-v2
609 $ hg -R t1 bundle --config experimental.changegroup3=yes -a -t 'v2;cg.version=03' ./v2-cg-03.hg
609 $ hg -R t1 bundle --config experimental.changegroup3=yes -a -t 'v2;cg.version=03' ./v2-cg-03.hg
610 7 changesets found
610 7 changesets found
611 $ hg debugbundle ./v2-cg-03.hg --part-type changegroup
611 $ hg debugbundle ./v2-cg-03.hg --part-type changegroup
612 Stream params: {Compression: BZ}
612 Stream params: {Compression: BZ}
613 changegroup -- {nbchanges: 7, version: 03} (mandatory: True)
613 changegroup -- {nbchanges: 7, version: 03} (mandatory: True)
614 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
614 ac39af4a9f7d2aaa7d244720e57838be9bf63b03
615 901e97fadc587978ec52f2fa76af4aefc2d191e8
615 901e97fadc587978ec52f2fa76af4aefc2d191e8
616 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
616 a8c3a1ed30eb71f03f476c5fa7ead831ef991a55
617 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
617 66e2c4b43e0cf8f0bdff0733a0b97ce57874e35d
618 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
618 624e609639853fe22c88d42a8fd1f53a0e9b7ebe
619 2ea90778052ba7558fab36e3fd5d149512ff986b
619 2ea90778052ba7558fab36e3fd5d149512ff986b
620 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
620 b9f5f740a8cd76700020e3903ee55ecff78bd3e5
621 $ hg debugbundle ./v2-cg-03.hg --spec
621 $ hg debugbundle ./v2-cg-03.hg --spec
622 bzip2-v2;cg.version=03
622 bzip2-v2;cg.version=03
623
624 tests controlling bundle contents
625 =================================
626
627 $ hg debugupdatecache -R t1
628
629 default content
630 ---------------
631
632 $ hg -R t1 bundle --all --quiet --type 'v2' ./v2.hg
633 $ hg debugbundle ./v2.hg --spec
634 bzip2-v2
635 $ hg debugbundle ./v2.hg --quiet
636 Stream params: {Compression: BZ}
637 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
638 hgtagsfnodes -- {} (mandatory: False)
639 cache:rev-branch-cache -- {} (mandatory: False)
640
641 $ hg -R t1 bundle --all --quiet --type 'v3' ./v3.hg
642 $ hg debugbundle ./v3.hg --spec
643 bzip2-v2;cg.version=03
644 $ hg debugbundle ./v3.hg --quiet
645 Stream params: {Compression: BZ}
646 changegroup -- {nbchanges: 7, targetphase: 2, version: 03} (mandatory: True)
647 hgtagsfnodes -- {} (mandatory: False)
648 cache:rev-branch-cache -- {} (mandatory: False)
649 phase-heads -- {} (mandatory: True)
650
651 adding extra parts
652 ------------------
653
654 We should have a "phase-heads" part here that we did not had in the default content
655
656 $ hg -R t1 bundle --all --quiet --type 'v2;phases=1' ./v2-phases.hg
657 $ hg debugbundle ./v2-phases.hg --spec
658 bzip2-v2
659 $ hg debugbundle ./v2-phases.hg --quiet
660 Stream params: {Compression: BZ}
661 changegroup -- {nbchanges: 7, targetphase: 2, version: 02} (mandatory: True)
662 hgtagsfnodes -- {} (mandatory: False)
663 cache:rev-branch-cache -- {} (mandatory: False)
664 phase-heads -- {} (mandatory: True)
665
666 skipping default inclusion
667 --------------------------
668
669 $ hg -R t1 bundle --all --quiet --type 'v2;tagsfnodescache=false' ./v2-no-tfc.hg
670 $ hg debugbundle ./v2-no-tfc.hg --spec
671 bzip2-v2
672 $ hg debugbundle ./v2-no-tfc.hg --quiet
673 Stream params: {Compression: BZ}
674 changegroup -- {nbchanges: 7, version: 02} (mandatory: True)
675 cache:rev-branch-cache -- {} (mandatory: False)
676
677 $ hg -R t1 bundle --all --quiet --type 'v3;phases=0' ./v3-no-phases.hg
678 $ hg debugbundle ./v3-no-phases.hg --spec
679 bzip2-v2;cg.version=03
680 $ hg debugbundle ./v3-no-phases.hg --quiet
681 Stream params: {Compression: BZ}
682 changegroup -- {nbchanges: 7, version: 03} (mandatory: True)
683 hgtagsfnodes -- {} (mandatory: False)
684 cache:rev-branch-cache -- {} (mandatory: False)
685
686 $ hg -R t1 bundle --all --quiet --type 'v3;phases=no;tagsfnodescache=0' ./v3-multi-no.hg
687 $ hg debugbundle ./v3-multi-no.hg --spec
688 bzip2-v2;cg.version=03
689 $ hg debugbundle ./v3-multi-no.hg --quiet
690 Stream params: {Compression: BZ}
691 changegroup -- {nbchanges: 7, version: 03} (mandatory: True)
692 cache:rev-branch-cache -- {} (mandatory: False)
693
694 skipping changegroup
695 --------------------
696
697 $ hg -R t1 bundle --all --quiet --type 'v2;changegroup=no' ./v2-no-cg.hg
698 $ hg debugbundle ./v2-no-cg.hg --spec
699 bzip2-v2;changegroup=no
700 $ hg debugbundle ./v2-no-cg.hg --quiet
701 Stream params: {Compression: BZ}
702 hgtagsfnodes -- {} (mandatory: False)
703 cache:rev-branch-cache -- {} (mandatory: False)
704
705 $ hg -R t1 bundle --all --quiet --type 'v3;changegroup=0' ./v3-no-cg.hg
706 $ hg debugbundle ./v3-no-cg.hg --spec
707 bzip2-v2;changegroup=no
708 $ hg debugbundle ./v3-no-cg.hg --quiet
709 Stream params: {Compression: BZ}
710 hgtagsfnodes -- {} (mandatory: False)
711 cache:rev-branch-cache -- {} (mandatory: False)
712 phase-heads -- {} (mandatory: True)
@@ -1,1007 +1,1009 b''
1 import unittest
1 import unittest
2
2
3 import silenttestrunner
3 import silenttestrunner
4
4
5 from mercurial import (
5 from mercurial import (
6 match as matchmod,
6 match as matchmod,
7 util,
7 util,
8 )
8 )
9
9
10
10
11 noop_auditor = lambda name: None
11 noop_auditor = lambda name: None
12
12
13
13
14 class BaseMatcherTests(unittest.TestCase):
14 class BaseMatcherTests(unittest.TestCase):
15 def testVisitdir(self):
15 def testVisitdir(self):
16 m = matchmod.basematcher()
16 m = matchmod.basematcher()
17 self.assertTrue(m.visitdir(b''))
17 self.assertTrue(m.visitdir(b''))
18 self.assertTrue(m.visitdir(b'dir'))
18 self.assertTrue(m.visitdir(b'dir'))
19
19
20 def testVisitchildrenset(self):
20 def testVisitchildrenset(self):
21 m = matchmod.basematcher()
21 m = matchmod.basematcher()
22 self.assertEqual(m.visitchildrenset(b''), b'this')
22 self.assertEqual(m.visitchildrenset(b''), b'this')
23 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
23 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
24
24
25
25
26 class AlwaysMatcherTests(unittest.TestCase):
26 class AlwaysMatcherTests(unittest.TestCase):
27 def testVisitdir(self):
27 def testVisitdir(self):
28 m = matchmod.alwaysmatcher()
28 m = matchmod.alwaysmatcher()
29 self.assertEqual(m.visitdir(b''), b'all')
29 self.assertEqual(m.visitdir(b''), b'all')
30 self.assertEqual(m.visitdir(b'dir'), b'all')
30 self.assertEqual(m.visitdir(b'dir'), b'all')
31
31
32 def testVisitchildrenset(self):
32 def testVisitchildrenset(self):
33 m = matchmod.alwaysmatcher()
33 m = matchmod.alwaysmatcher()
34 self.assertEqual(m.visitchildrenset(b''), b'all')
34 self.assertEqual(m.visitchildrenset(b''), b'all')
35 self.assertEqual(m.visitchildrenset(b'dir'), b'all')
35 self.assertEqual(m.visitchildrenset(b'dir'), b'all')
36
36
37
37
38 class NeverMatcherTests(unittest.TestCase):
38 class NeverMatcherTests(unittest.TestCase):
39 def testVisitdir(self):
39 def testVisitdir(self):
40 m = matchmod.nevermatcher()
40 m = matchmod.nevermatcher()
41 self.assertFalse(m.visitdir(b''))
41 self.assertFalse(m.visitdir(b''))
42 self.assertFalse(m.visitdir(b'dir'))
42 self.assertFalse(m.visitdir(b'dir'))
43
43
44 def testVisitchildrenset(self):
44 def testVisitchildrenset(self):
45 m = matchmod.nevermatcher()
45 m = matchmod.nevermatcher()
46 self.assertEqual(m.visitchildrenset(b''), set())
46 self.assertEqual(m.visitchildrenset(b''), set())
47 self.assertEqual(m.visitchildrenset(b'dir'), set())
47 self.assertEqual(m.visitchildrenset(b'dir'), set())
48
48
49
49
50 class PredicateMatcherTests(unittest.TestCase):
50 class PredicateMatcherTests(unittest.TestCase):
51 # predicatematcher does not currently define either of these methods, so
51 # predicatematcher does not currently define either of these methods, so
52 # this is equivalent to BaseMatcherTests.
52 # this is equivalent to BaseMatcherTests.
53
53
54 def testVisitdir(self):
54 def testVisitdir(self):
55 m = matchmod.predicatematcher(lambda *a: False)
55 m = matchmod.predicatematcher(lambda *a: False)
56 self.assertTrue(m.visitdir(b''))
56 self.assertTrue(m.visitdir(b''))
57 self.assertTrue(m.visitdir(b'dir'))
57 self.assertTrue(m.visitdir(b'dir'))
58
58
59 def testVisitchildrenset(self):
59 def testVisitchildrenset(self):
60 m = matchmod.predicatematcher(lambda *a: False)
60 m = matchmod.predicatematcher(lambda *a: False)
61 self.assertEqual(m.visitchildrenset(b''), b'this')
61 self.assertEqual(m.visitchildrenset(b''), b'this')
62 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
62 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
63
63
64
64
65 class PatternMatcherTests(unittest.TestCase):
65 class PatternMatcherTests(unittest.TestCase):
66 def testVisitdirPrefix(self):
66 def testVisitdirPrefix(self):
67 m = matchmod.match(
67 m = matchmod.match(
68 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
68 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
69 )
69 )
70 assert isinstance(m, matchmod.patternmatcher)
70 assert isinstance(m, matchmod.patternmatcher)
71 self.assertTrue(m.visitdir(b''))
71 self.assertTrue(m.visitdir(b''))
72 self.assertTrue(m.visitdir(b'dir'))
72 self.assertTrue(m.visitdir(b'dir'))
73 self.assertEqual(m.visitdir(b'dir/subdir'), b'all')
73 self.assertEqual(m.visitdir(b'dir/subdir'), b'all')
74 # OPT: This should probably be 'all' if its parent is?
74 # OPT: This should probably be 'all' if its parent is?
75 self.assertTrue(m.visitdir(b'dir/subdir/x'))
75 self.assertTrue(m.visitdir(b'dir/subdir/x'))
76 self.assertFalse(m.visitdir(b'folder'))
76 self.assertFalse(m.visitdir(b'folder'))
77
77
78 def testVisitchildrensetPrefix(self):
78 def testVisitchildrensetPrefix(self):
79 m = matchmod.match(
79 m = matchmod.match(
80 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
80 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
81 )
81 )
82 assert isinstance(m, matchmod.patternmatcher)
82 assert isinstance(m, matchmod.patternmatcher)
83 self.assertEqual(m.visitchildrenset(b''), b'this')
83 self.assertEqual(m.visitchildrenset(b''), b'this')
84 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
84 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
85 self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'all')
85 self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'all')
86 # OPT: This should probably be 'all' if its parent is?
86 # OPT: This should probably be 'all' if its parent is?
87 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this')
87 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this')
88 self.assertEqual(m.visitchildrenset(b'folder'), set())
88 self.assertEqual(m.visitchildrenset(b'folder'), set())
89
89
90 def testVisitdirRootfilesin(self):
90 def testVisitdirRootfilesin(self):
91 m = matchmod.match(
91 m = matchmod.match(
92 util.localpath(b'/repo'),
92 util.localpath(b'/repo'),
93 b'',
93 b'',
94 patterns=[b'rootfilesin:dir/subdir'],
94 patterns=[b'rootfilesin:dir/subdir'],
95 )
95 )
96 assert isinstance(m, matchmod.patternmatcher)
96 assert isinstance(m, matchmod.patternmatcher)
97 self.assertFalse(m.visitdir(b'dir/subdir/x'))
97 # OPT: we shouldn't visit [x] as a directory,
98 # but we should still visit it as a file.
99 # Unfortunately, `visitdir` is used for both.
100 self.assertTrue(m.visitdir(b'dir/subdir/x'))
98 self.assertFalse(m.visitdir(b'folder'))
101 self.assertFalse(m.visitdir(b'folder'))
99 # FIXME: These should probably be True.
102 self.assertTrue(m.visitdir(b''))
100 self.assertFalse(m.visitdir(b''))
103 self.assertTrue(m.visitdir(b'dir'))
101 self.assertFalse(m.visitdir(b'dir'))
104 self.assertTrue(m.visitdir(b'dir/subdir'))
102 self.assertFalse(m.visitdir(b'dir/subdir'))
103
105
104 def testVisitchildrensetRootfilesin(self):
106 def testVisitchildrensetRootfilesin(self):
105 m = matchmod.match(
107 m = matchmod.match(
106 util.localpath(b'/repo'),
108 util.localpath(b'/repo'),
107 b'',
109 b'',
108 patterns=[b'rootfilesin:dir/subdir'],
110 patterns=[b'rootfilesin:dir/subdir'],
109 )
111 )
110 assert isinstance(m, matchmod.patternmatcher)
112 assert isinstance(m, matchmod.patternmatcher)
111 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set())
113 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this')
112 self.assertEqual(m.visitchildrenset(b'folder'), set())
114 self.assertEqual(m.visitchildrenset(b'folder'), set())
113 # FIXME: These should probably be {'dir'}, {'subdir'} and 'this',
115 # OPT: These should probably be {'dir'}, {'subdir'} and 'this',
114 # respectively, or at least 'this' for all three.
116 # respectively
115 self.assertEqual(m.visitchildrenset(b''), set())
117 self.assertEqual(m.visitchildrenset(b''), b'this')
116 self.assertEqual(m.visitchildrenset(b'dir'), set())
118 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
117 self.assertEqual(m.visitchildrenset(b'dir/subdir'), set())
119 self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'this')
118
120
119 def testVisitdirGlob(self):
121 def testVisitdirGlob(self):
120 m = matchmod.match(
122 m = matchmod.match(
121 util.localpath(b'/repo'), b'', patterns=[b'glob:dir/z*']
123 util.localpath(b'/repo'), b'', patterns=[b'glob:dir/z*']
122 )
124 )
123 assert isinstance(m, matchmod.patternmatcher)
125 assert isinstance(m, matchmod.patternmatcher)
124 self.assertTrue(m.visitdir(b''))
126 self.assertTrue(m.visitdir(b''))
125 self.assertTrue(m.visitdir(b'dir'))
127 self.assertTrue(m.visitdir(b'dir'))
126 self.assertFalse(m.visitdir(b'folder'))
128 self.assertFalse(m.visitdir(b'folder'))
127 # OPT: these should probably be False.
129 # OPT: these should probably be False.
128 self.assertTrue(m.visitdir(b'dir/subdir'))
130 self.assertTrue(m.visitdir(b'dir/subdir'))
129 self.assertTrue(m.visitdir(b'dir/subdir/x'))
131 self.assertTrue(m.visitdir(b'dir/subdir/x'))
130
132
131 def testVisitchildrensetGlob(self):
133 def testVisitchildrensetGlob(self):
132 m = matchmod.match(
134 m = matchmod.match(
133 util.localpath(b'/repo'), b'', patterns=[b'glob:dir/z*']
135 util.localpath(b'/repo'), b'', patterns=[b'glob:dir/z*']
134 )
136 )
135 assert isinstance(m, matchmod.patternmatcher)
137 assert isinstance(m, matchmod.patternmatcher)
136 self.assertEqual(m.visitchildrenset(b''), b'this')
138 self.assertEqual(m.visitchildrenset(b''), b'this')
137 self.assertEqual(m.visitchildrenset(b'folder'), set())
139 self.assertEqual(m.visitchildrenset(b'folder'), set())
138 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
140 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
139 # OPT: these should probably be set().
141 # OPT: these should probably be set().
140 self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'this')
142 self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'this')
141 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this')
143 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this')
142
144
143 def testVisitdirFilepath(self):
145 def testVisitdirFilepath(self):
144 m = matchmod.match(
146 m = matchmod.match(
145 util.localpath(b'/repo'), b'', patterns=[b'filepath:dir/z']
147 util.localpath(b'/repo'), b'', patterns=[b'filepath:dir/z']
146 )
148 )
147 assert isinstance(m, matchmod.patternmatcher)
149 assert isinstance(m, matchmod.patternmatcher)
148 self.assertTrue(m.visitdir(b''))
150 self.assertTrue(m.visitdir(b''))
149 self.assertTrue(m.visitdir(b'dir'))
151 self.assertTrue(m.visitdir(b'dir'))
150 self.assertFalse(m.visitdir(b'folder'))
152 self.assertFalse(m.visitdir(b'folder'))
151 self.assertFalse(m.visitdir(b'dir/subdir'))
153 self.assertFalse(m.visitdir(b'dir/subdir'))
152 self.assertFalse(m.visitdir(b'dir/subdir/x'))
154 self.assertFalse(m.visitdir(b'dir/subdir/x'))
153
155
154 def testVisitchildrensetFilepath(self):
156 def testVisitchildrensetFilepath(self):
155 m = matchmod.match(
157 m = matchmod.match(
156 util.localpath(b'/repo'), b'', patterns=[b'filepath:dir/z']
158 util.localpath(b'/repo'), b'', patterns=[b'filepath:dir/z']
157 )
159 )
158 assert isinstance(m, matchmod.patternmatcher)
160 assert isinstance(m, matchmod.patternmatcher)
159 self.assertEqual(m.visitchildrenset(b''), b'this')
161 self.assertEqual(m.visitchildrenset(b''), b'this')
160 self.assertEqual(m.visitchildrenset(b'folder'), set())
162 self.assertEqual(m.visitchildrenset(b'folder'), set())
161 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
163 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
162 self.assertEqual(m.visitchildrenset(b'dir/subdir'), set())
164 self.assertEqual(m.visitchildrenset(b'dir/subdir'), set())
163 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set())
165 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set())
164
166
165
167
166 class IncludeMatcherTests(unittest.TestCase):
168 class IncludeMatcherTests(unittest.TestCase):
167 def testVisitdirPrefix(self):
169 def testVisitdirPrefix(self):
168 m = matchmod.match(
170 m = matchmod.match(
169 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
171 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
170 )
172 )
171 assert isinstance(m, matchmod.includematcher)
173 assert isinstance(m, matchmod.includematcher)
172 self.assertTrue(m.visitdir(b''))
174 self.assertTrue(m.visitdir(b''))
173 self.assertTrue(m.visitdir(b'dir'))
175 self.assertTrue(m.visitdir(b'dir'))
174 self.assertEqual(m.visitdir(b'dir/subdir'), b'all')
176 self.assertEqual(m.visitdir(b'dir/subdir'), b'all')
175 # OPT: This should probably be 'all' if its parent is?
177 # OPT: This should probably be 'all' if its parent is?
176 self.assertTrue(m.visitdir(b'dir/subdir/x'))
178 self.assertTrue(m.visitdir(b'dir/subdir/x'))
177 self.assertFalse(m.visitdir(b'folder'))
179 self.assertFalse(m.visitdir(b'folder'))
178
180
179 def testVisitchildrensetPrefix(self):
181 def testVisitchildrensetPrefix(self):
180 m = matchmod.match(
182 m = matchmod.match(
181 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
183 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
182 )
184 )
183 assert isinstance(m, matchmod.includematcher)
185 assert isinstance(m, matchmod.includematcher)
184 self.assertEqual(m.visitchildrenset(b''), {b'dir'})
186 self.assertEqual(m.visitchildrenset(b''), {b'dir'})
185 self.assertEqual(m.visitchildrenset(b'dir'), {b'subdir'})
187 self.assertEqual(m.visitchildrenset(b'dir'), {b'subdir'})
186 self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'all')
188 self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'all')
187 # OPT: This should probably be 'all' if its parent is?
189 # OPT: This should probably be 'all' if its parent is?
188 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this')
190 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this')
189 self.assertEqual(m.visitchildrenset(b'folder'), set())
191 self.assertEqual(m.visitchildrenset(b'folder'), set())
190
192
191 def testVisitdirRootfilesin(self):
193 def testVisitdirRootfilesin(self):
192 m = matchmod.match(
194 m = matchmod.match(
193 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir/subdir']
195 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir/subdir']
194 )
196 )
195 assert isinstance(m, matchmod.includematcher)
197 assert isinstance(m, matchmod.includematcher)
196 self.assertTrue(m.visitdir(b''))
198 self.assertTrue(m.visitdir(b''))
197 self.assertTrue(m.visitdir(b'dir'))
199 self.assertTrue(m.visitdir(b'dir'))
198 self.assertTrue(m.visitdir(b'dir/subdir'))
200 self.assertTrue(m.visitdir(b'dir/subdir'))
199 self.assertFalse(m.visitdir(b'dir/subdir/x'))
201 self.assertFalse(m.visitdir(b'dir/subdir/x'))
200 self.assertFalse(m.visitdir(b'folder'))
202 self.assertFalse(m.visitdir(b'folder'))
201
203
202 def testVisitchildrensetRootfilesin(self):
204 def testVisitchildrensetRootfilesin(self):
203 m = matchmod.match(
205 m = matchmod.match(
204 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir/subdir']
206 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir/subdir']
205 )
207 )
206 assert isinstance(m, matchmod.includematcher)
208 assert isinstance(m, matchmod.includematcher)
207 self.assertEqual(m.visitchildrenset(b''), {b'dir'})
209 self.assertEqual(m.visitchildrenset(b''), {b'dir'})
208 self.assertEqual(m.visitchildrenset(b'dir'), {b'subdir'})
210 self.assertEqual(m.visitchildrenset(b'dir'), {b'subdir'})
209 self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'this')
211 self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'this')
210 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set())
212 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set())
211 self.assertEqual(m.visitchildrenset(b'folder'), set())
213 self.assertEqual(m.visitchildrenset(b'folder'), set())
212
214
213 def testVisitdirGlob(self):
215 def testVisitdirGlob(self):
214 m = matchmod.match(
216 m = matchmod.match(
215 util.localpath(b'/repo'), b'', include=[b'glob:dir/z*']
217 util.localpath(b'/repo'), b'', include=[b'glob:dir/z*']
216 )
218 )
217 assert isinstance(m, matchmod.includematcher)
219 assert isinstance(m, matchmod.includematcher)
218 self.assertTrue(m.visitdir(b''))
220 self.assertTrue(m.visitdir(b''))
219 self.assertTrue(m.visitdir(b'dir'))
221 self.assertTrue(m.visitdir(b'dir'))
220 self.assertFalse(m.visitdir(b'folder'))
222 self.assertFalse(m.visitdir(b'folder'))
221 # OPT: these should probably be False.
223 # OPT: these should probably be False.
222 self.assertTrue(m.visitdir(b'dir/subdir'))
224 self.assertTrue(m.visitdir(b'dir/subdir'))
223 self.assertTrue(m.visitdir(b'dir/subdir/x'))
225 self.assertTrue(m.visitdir(b'dir/subdir/x'))
224
226
225 def testVisitchildrensetGlob(self):
227 def testVisitchildrensetGlob(self):
226 m = matchmod.match(
228 m = matchmod.match(
227 util.localpath(b'/repo'), b'', include=[b'glob:dir/z*']
229 util.localpath(b'/repo'), b'', include=[b'glob:dir/z*']
228 )
230 )
229 assert isinstance(m, matchmod.includematcher)
231 assert isinstance(m, matchmod.includematcher)
230 self.assertEqual(m.visitchildrenset(b''), {b'dir'})
232 self.assertEqual(m.visitchildrenset(b''), {b'dir'})
231 self.assertEqual(m.visitchildrenset(b'folder'), set())
233 self.assertEqual(m.visitchildrenset(b'folder'), set())
232 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
234 self.assertEqual(m.visitchildrenset(b'dir'), b'this')
233 # OPT: these should probably be set().
235 # OPT: these should probably be set().
234 self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'this')
236 self.assertEqual(m.visitchildrenset(b'dir/subdir'), b'this')
235 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this')
237 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), b'this')
236
238
237 def testVisitdirFilepath(self):
239 def testVisitdirFilepath(self):
238 m = matchmod.match(
240 m = matchmod.match(
239 util.localpath(b'/repo'), b'', include=[b'filepath:dir/z']
241 util.localpath(b'/repo'), b'', include=[b'filepath:dir/z']
240 )
242 )
241 assert isinstance(m, matchmod.includematcher)
243 assert isinstance(m, matchmod.includematcher)
242 self.assertTrue(m.visitdir(b''))
244 self.assertTrue(m.visitdir(b''))
243 self.assertTrue(m.visitdir(b'dir'))
245 self.assertTrue(m.visitdir(b'dir'))
244 self.assertFalse(m.visitdir(b'folder'))
246 self.assertFalse(m.visitdir(b'folder'))
245 self.assertFalse(m.visitdir(b'dir/subdir'))
247 self.assertFalse(m.visitdir(b'dir/subdir'))
246 self.assertFalse(m.visitdir(b'dir/subdir/x'))
248 self.assertFalse(m.visitdir(b'dir/subdir/x'))
247
249
248 def testVisitchildrensetFilepath(self):
250 def testVisitchildrensetFilepath(self):
249 m = matchmod.match(
251 m = matchmod.match(
250 util.localpath(b'/repo'), b'', include=[b'filepath:dir/z']
252 util.localpath(b'/repo'), b'', include=[b'filepath:dir/z']
251 )
253 )
252 assert isinstance(m, matchmod.includematcher)
254 assert isinstance(m, matchmod.includematcher)
253 self.assertEqual(m.visitchildrenset(b''), {b'dir'})
255 self.assertEqual(m.visitchildrenset(b''), {b'dir'})
254 self.assertEqual(m.visitchildrenset(b'folder'), set())
256 self.assertEqual(m.visitchildrenset(b'folder'), set())
255 self.assertEqual(m.visitchildrenset(b'dir'), {b'z'})
257 self.assertEqual(m.visitchildrenset(b'dir'), {b'z'})
256 self.assertEqual(m.visitchildrenset(b'dir/subdir'), set())
258 self.assertEqual(m.visitchildrenset(b'dir/subdir'), set())
257 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set())
259 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set())
258
260
259
261
260 class ExactMatcherTests(unittest.TestCase):
262 class ExactMatcherTests(unittest.TestCase):
261 def testVisitdir(self):
263 def testVisitdir(self):
262 m = matchmod.exact(files=[b'dir/subdir/foo.txt'])
264 m = matchmod.exact(files=[b'dir/subdir/foo.txt'])
263 assert isinstance(m, matchmod.exactmatcher)
265 assert isinstance(m, matchmod.exactmatcher)
264 self.assertTrue(m.visitdir(b''))
266 self.assertTrue(m.visitdir(b''))
265 self.assertTrue(m.visitdir(b'dir'))
267 self.assertTrue(m.visitdir(b'dir'))
266 self.assertTrue(m.visitdir(b'dir/subdir'))
268 self.assertTrue(m.visitdir(b'dir/subdir'))
267 self.assertFalse(m.visitdir(b'dir/subdir/foo.txt'))
269 self.assertFalse(m.visitdir(b'dir/subdir/foo.txt'))
268 self.assertFalse(m.visitdir(b'dir/foo'))
270 self.assertFalse(m.visitdir(b'dir/foo'))
269 self.assertFalse(m.visitdir(b'dir/subdir/x'))
271 self.assertFalse(m.visitdir(b'dir/subdir/x'))
270 self.assertFalse(m.visitdir(b'folder'))
272 self.assertFalse(m.visitdir(b'folder'))
271
273
272 def testVisitchildrenset(self):
274 def testVisitchildrenset(self):
273 m = matchmod.exact(files=[b'dir/subdir/foo.txt'])
275 m = matchmod.exact(files=[b'dir/subdir/foo.txt'])
274 assert isinstance(m, matchmod.exactmatcher)
276 assert isinstance(m, matchmod.exactmatcher)
275 self.assertEqual(m.visitchildrenset(b''), {b'dir'})
277 self.assertEqual(m.visitchildrenset(b''), {b'dir'})
276 self.assertEqual(m.visitchildrenset(b'dir'), {b'subdir'})
278 self.assertEqual(m.visitchildrenset(b'dir'), {b'subdir'})
277 self.assertEqual(m.visitchildrenset(b'dir/subdir'), {b'foo.txt'})
279 self.assertEqual(m.visitchildrenset(b'dir/subdir'), {b'foo.txt'})
278 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set())
280 self.assertEqual(m.visitchildrenset(b'dir/subdir/x'), set())
279 self.assertEqual(m.visitchildrenset(b'dir/subdir/foo.txt'), set())
281 self.assertEqual(m.visitchildrenset(b'dir/subdir/foo.txt'), set())
280 self.assertEqual(m.visitchildrenset(b'folder'), set())
282 self.assertEqual(m.visitchildrenset(b'folder'), set())
281
283
282 def testVisitchildrensetFilesAndDirs(self):
284 def testVisitchildrensetFilesAndDirs(self):
283 m = matchmod.exact(
285 m = matchmod.exact(
284 files=[
286 files=[
285 b'rootfile.txt',
287 b'rootfile.txt',
286 b'a/file1.txt',
288 b'a/file1.txt',
287 b'a/b/file2.txt',
289 b'a/b/file2.txt',
288 # no file in a/b/c
290 # no file in a/b/c
289 b'a/b/c/d/file4.txt',
291 b'a/b/c/d/file4.txt',
290 ]
292 ]
291 )
293 )
292 assert isinstance(m, matchmod.exactmatcher)
294 assert isinstance(m, matchmod.exactmatcher)
293 self.assertEqual(m.visitchildrenset(b''), {b'a', b'rootfile.txt'})
295 self.assertEqual(m.visitchildrenset(b''), {b'a', b'rootfile.txt'})
294 self.assertEqual(m.visitchildrenset(b'a'), {b'b', b'file1.txt'})
296 self.assertEqual(m.visitchildrenset(b'a'), {b'b', b'file1.txt'})
295 self.assertEqual(m.visitchildrenset(b'a/b'), {b'c', b'file2.txt'})
297 self.assertEqual(m.visitchildrenset(b'a/b'), {b'c', b'file2.txt'})
296 self.assertEqual(m.visitchildrenset(b'a/b/c'), {b'd'})
298 self.assertEqual(m.visitchildrenset(b'a/b/c'), {b'd'})
297 self.assertEqual(m.visitchildrenset(b'a/b/c/d'), {b'file4.txt'})
299 self.assertEqual(m.visitchildrenset(b'a/b/c/d'), {b'file4.txt'})
298 self.assertEqual(m.visitchildrenset(b'a/b/c/d/e'), set())
300 self.assertEqual(m.visitchildrenset(b'a/b/c/d/e'), set())
299 self.assertEqual(m.visitchildrenset(b'folder'), set())
301 self.assertEqual(m.visitchildrenset(b'folder'), set())
300
302
301
303
302 class DifferenceMatcherTests(unittest.TestCase):
304 class DifferenceMatcherTests(unittest.TestCase):
303 def testVisitdirM2always(self):
305 def testVisitdirM2always(self):
304 m1 = matchmod.alwaysmatcher()
306 m1 = matchmod.alwaysmatcher()
305 m2 = matchmod.alwaysmatcher()
307 m2 = matchmod.alwaysmatcher()
306 dm = matchmod.differencematcher(m1, m2)
308 dm = matchmod.differencematcher(m1, m2)
307 # dm should be equivalent to a nevermatcher.
309 # dm should be equivalent to a nevermatcher.
308 self.assertFalse(dm.visitdir(b''))
310 self.assertFalse(dm.visitdir(b''))
309 self.assertFalse(dm.visitdir(b'dir'))
311 self.assertFalse(dm.visitdir(b'dir'))
310 self.assertFalse(dm.visitdir(b'dir/subdir'))
312 self.assertFalse(dm.visitdir(b'dir/subdir'))
311 self.assertFalse(dm.visitdir(b'dir/subdir/z'))
313 self.assertFalse(dm.visitdir(b'dir/subdir/z'))
312 self.assertFalse(dm.visitdir(b'dir/foo'))
314 self.assertFalse(dm.visitdir(b'dir/foo'))
313 self.assertFalse(dm.visitdir(b'dir/subdir/x'))
315 self.assertFalse(dm.visitdir(b'dir/subdir/x'))
314 self.assertFalse(dm.visitdir(b'folder'))
316 self.assertFalse(dm.visitdir(b'folder'))
315
317
316 def testVisitchildrensetM2always(self):
318 def testVisitchildrensetM2always(self):
317 m1 = matchmod.alwaysmatcher()
319 m1 = matchmod.alwaysmatcher()
318 m2 = matchmod.alwaysmatcher()
320 m2 = matchmod.alwaysmatcher()
319 dm = matchmod.differencematcher(m1, m2)
321 dm = matchmod.differencematcher(m1, m2)
320 # dm should be equivalent to a nevermatcher.
322 # dm should be equivalent to a nevermatcher.
321 self.assertEqual(dm.visitchildrenset(b''), set())
323 self.assertEqual(dm.visitchildrenset(b''), set())
322 self.assertEqual(dm.visitchildrenset(b'dir'), set())
324 self.assertEqual(dm.visitchildrenset(b'dir'), set())
323 self.assertEqual(dm.visitchildrenset(b'dir/subdir'), set())
325 self.assertEqual(dm.visitchildrenset(b'dir/subdir'), set())
324 self.assertEqual(dm.visitchildrenset(b'dir/subdir/z'), set())
326 self.assertEqual(dm.visitchildrenset(b'dir/subdir/z'), set())
325 self.assertEqual(dm.visitchildrenset(b'dir/foo'), set())
327 self.assertEqual(dm.visitchildrenset(b'dir/foo'), set())
326 self.assertEqual(dm.visitchildrenset(b'dir/subdir/x'), set())
328 self.assertEqual(dm.visitchildrenset(b'dir/subdir/x'), set())
327 self.assertEqual(dm.visitchildrenset(b'folder'), set())
329 self.assertEqual(dm.visitchildrenset(b'folder'), set())
328
330
329 def testVisitdirM2never(self):
331 def testVisitdirM2never(self):
330 m1 = matchmod.alwaysmatcher()
332 m1 = matchmod.alwaysmatcher()
331 m2 = matchmod.nevermatcher()
333 m2 = matchmod.nevermatcher()
332 dm = matchmod.differencematcher(m1, m2)
334 dm = matchmod.differencematcher(m1, m2)
333 # dm should be equivalent to a alwaysmatcher.
335 # dm should be equivalent to a alwaysmatcher.
334 #
336 #
335 # We're testing Equal-to-True instead of just 'assertTrue' since
337 # We're testing Equal-to-True instead of just 'assertTrue' since
336 # assertTrue does NOT verify that it's a bool, just that it's truthy.
338 # assertTrue does NOT verify that it's a bool, just that it's truthy.
337 # While we may want to eventually make these return 'all', they should
339 # While we may want to eventually make these return 'all', they should
338 # not currently do so.
340 # not currently do so.
339 self.assertEqual(dm.visitdir(b''), b'all')
341 self.assertEqual(dm.visitdir(b''), b'all')
340 self.assertEqual(dm.visitdir(b'dir'), b'all')
342 self.assertEqual(dm.visitdir(b'dir'), b'all')
341 self.assertEqual(dm.visitdir(b'dir/subdir'), b'all')
343 self.assertEqual(dm.visitdir(b'dir/subdir'), b'all')
342 self.assertEqual(dm.visitdir(b'dir/subdir/z'), b'all')
344 self.assertEqual(dm.visitdir(b'dir/subdir/z'), b'all')
343 self.assertEqual(dm.visitdir(b'dir/foo'), b'all')
345 self.assertEqual(dm.visitdir(b'dir/foo'), b'all')
344 self.assertEqual(dm.visitdir(b'dir/subdir/x'), b'all')
346 self.assertEqual(dm.visitdir(b'dir/subdir/x'), b'all')
345 self.assertEqual(dm.visitdir(b'folder'), b'all')
347 self.assertEqual(dm.visitdir(b'folder'), b'all')
346
348
347 def testVisitchildrensetM2never(self):
349 def testVisitchildrensetM2never(self):
348 m1 = matchmod.alwaysmatcher()
350 m1 = matchmod.alwaysmatcher()
349 m2 = matchmod.nevermatcher()
351 m2 = matchmod.nevermatcher()
350 dm = matchmod.differencematcher(m1, m2)
352 dm = matchmod.differencematcher(m1, m2)
351 # dm should be equivalent to a alwaysmatcher.
353 # dm should be equivalent to a alwaysmatcher.
352 self.assertEqual(dm.visitchildrenset(b''), b'all')
354 self.assertEqual(dm.visitchildrenset(b''), b'all')
353 self.assertEqual(dm.visitchildrenset(b'dir'), b'all')
355 self.assertEqual(dm.visitchildrenset(b'dir'), b'all')
354 self.assertEqual(dm.visitchildrenset(b'dir/subdir'), b'all')
356 self.assertEqual(dm.visitchildrenset(b'dir/subdir'), b'all')
355 self.assertEqual(dm.visitchildrenset(b'dir/subdir/z'), b'all')
357 self.assertEqual(dm.visitchildrenset(b'dir/subdir/z'), b'all')
356 self.assertEqual(dm.visitchildrenset(b'dir/foo'), b'all')
358 self.assertEqual(dm.visitchildrenset(b'dir/foo'), b'all')
357 self.assertEqual(dm.visitchildrenset(b'dir/subdir/x'), b'all')
359 self.assertEqual(dm.visitchildrenset(b'dir/subdir/x'), b'all')
358 self.assertEqual(dm.visitchildrenset(b'folder'), b'all')
360 self.assertEqual(dm.visitchildrenset(b'folder'), b'all')
359
361
360 def testVisitdirM2SubdirPrefix(self):
362 def testVisitdirM2SubdirPrefix(self):
361 m1 = matchmod.alwaysmatcher()
363 m1 = matchmod.alwaysmatcher()
362 m2 = matchmod.match(
364 m2 = matchmod.match(
363 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
365 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
364 )
366 )
365 dm = matchmod.differencematcher(m1, m2)
367 dm = matchmod.differencematcher(m1, m2)
366 self.assertEqual(dm.visitdir(b''), True)
368 self.assertEqual(dm.visitdir(b''), True)
367 self.assertEqual(dm.visitdir(b'dir'), True)
369 self.assertEqual(dm.visitdir(b'dir'), True)
368 self.assertFalse(dm.visitdir(b'dir/subdir'))
370 self.assertFalse(dm.visitdir(b'dir/subdir'))
369 # OPT: We should probably return False for these; we don't because
371 # OPT: We should probably return False for these; we don't because
370 # patternmatcher.visitdir() (our m2) doesn't return 'all' for subdirs of
372 # patternmatcher.visitdir() (our m2) doesn't return 'all' for subdirs of
371 # an 'all' pattern, just True.
373 # an 'all' pattern, just True.
372 self.assertEqual(dm.visitdir(b'dir/subdir/z'), True)
374 self.assertEqual(dm.visitdir(b'dir/subdir/z'), True)
373 self.assertEqual(dm.visitdir(b'dir/subdir/x'), True)
375 self.assertEqual(dm.visitdir(b'dir/subdir/x'), True)
374 self.assertEqual(dm.visitdir(b'dir/foo'), b'all')
376 self.assertEqual(dm.visitdir(b'dir/foo'), b'all')
375 self.assertEqual(dm.visitdir(b'folder'), b'all')
377 self.assertEqual(dm.visitdir(b'folder'), b'all')
376
378
377 def testVisitchildrensetM2SubdirPrefix(self):
379 def testVisitchildrensetM2SubdirPrefix(self):
378 m1 = matchmod.alwaysmatcher()
380 m1 = matchmod.alwaysmatcher()
379 m2 = matchmod.match(
381 m2 = matchmod.match(
380 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
382 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
381 )
383 )
382 dm = matchmod.differencematcher(m1, m2)
384 dm = matchmod.differencematcher(m1, m2)
383 self.assertEqual(dm.visitchildrenset(b''), b'this')
385 self.assertEqual(dm.visitchildrenset(b''), b'this')
384 self.assertEqual(dm.visitchildrenset(b'dir'), b'this')
386 self.assertEqual(dm.visitchildrenset(b'dir'), b'this')
385 self.assertEqual(dm.visitchildrenset(b'dir/subdir'), set())
387 self.assertEqual(dm.visitchildrenset(b'dir/subdir'), set())
386 self.assertEqual(dm.visitchildrenset(b'dir/foo'), b'all')
388 self.assertEqual(dm.visitchildrenset(b'dir/foo'), b'all')
387 self.assertEqual(dm.visitchildrenset(b'folder'), b'all')
389 self.assertEqual(dm.visitchildrenset(b'folder'), b'all')
388 # OPT: We should probably return set() for these; we don't because
390 # OPT: We should probably return set() for these; we don't because
389 # patternmatcher.visitdir() (our m2) doesn't return 'all' for subdirs of
391 # patternmatcher.visitdir() (our m2) doesn't return 'all' for subdirs of
390 # an 'all' pattern, just 'this'.
392 # an 'all' pattern, just 'this'.
391 self.assertEqual(dm.visitchildrenset(b'dir/subdir/z'), b'this')
393 self.assertEqual(dm.visitchildrenset(b'dir/subdir/z'), b'this')
392 self.assertEqual(dm.visitchildrenset(b'dir/subdir/x'), b'this')
394 self.assertEqual(dm.visitchildrenset(b'dir/subdir/x'), b'this')
393
395
394 # We're using includematcher instead of patterns because it behaves slightly
396 # We're using includematcher instead of patterns because it behaves slightly
395 # better (giving narrower results) than patternmatcher.
397 # better (giving narrower results) than patternmatcher.
396 def testVisitdirIncludeInclude(self):
398 def testVisitdirIncludeInclude(self):
397 m1 = matchmod.match(
399 m1 = matchmod.match(
398 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
400 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
399 )
401 )
400 m2 = matchmod.match(
402 m2 = matchmod.match(
401 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
403 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
402 )
404 )
403 dm = matchmod.differencematcher(m1, m2)
405 dm = matchmod.differencematcher(m1, m2)
404 self.assertEqual(dm.visitdir(b''), True)
406 self.assertEqual(dm.visitdir(b''), True)
405 self.assertEqual(dm.visitdir(b'dir'), True)
407 self.assertEqual(dm.visitdir(b'dir'), True)
406 self.assertEqual(dm.visitdir(b'dir/subdir'), b'all')
408 self.assertEqual(dm.visitdir(b'dir/subdir'), b'all')
407 self.assertFalse(dm.visitdir(b'dir/foo'))
409 self.assertFalse(dm.visitdir(b'dir/foo'))
408 self.assertFalse(dm.visitdir(b'folder'))
410 self.assertFalse(dm.visitdir(b'folder'))
409 # OPT: We should probably return False for these; we don't because
411 # OPT: We should probably return False for these; we don't because
410 # patternmatcher.visitdir() (our m2) doesn't return 'all' for subdirs of
412 # patternmatcher.visitdir() (our m2) doesn't return 'all' for subdirs of
411 # an 'all' pattern, just True.
413 # an 'all' pattern, just True.
412 self.assertEqual(dm.visitdir(b'dir/subdir/z'), True)
414 self.assertEqual(dm.visitdir(b'dir/subdir/z'), True)
413 self.assertEqual(dm.visitdir(b'dir/subdir/x'), True)
415 self.assertEqual(dm.visitdir(b'dir/subdir/x'), True)
414
416
415 def testVisitchildrensetIncludeInclude(self):
417 def testVisitchildrensetIncludeInclude(self):
416 m1 = matchmod.match(
418 m1 = matchmod.match(
417 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
419 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
418 )
420 )
419 m2 = matchmod.match(
421 m2 = matchmod.match(
420 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
422 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
421 )
423 )
422 dm = matchmod.differencematcher(m1, m2)
424 dm = matchmod.differencematcher(m1, m2)
423 self.assertEqual(dm.visitchildrenset(b''), {b'dir'})
425 self.assertEqual(dm.visitchildrenset(b''), {b'dir'})
424 self.assertEqual(dm.visitchildrenset(b'dir'), {b'subdir'})
426 self.assertEqual(dm.visitchildrenset(b'dir'), {b'subdir'})
425 self.assertEqual(dm.visitchildrenset(b'dir/subdir'), b'all')
427 self.assertEqual(dm.visitchildrenset(b'dir/subdir'), b'all')
426 self.assertEqual(dm.visitchildrenset(b'dir/foo'), set())
428 self.assertEqual(dm.visitchildrenset(b'dir/foo'), set())
427 self.assertEqual(dm.visitchildrenset(b'folder'), set())
429 self.assertEqual(dm.visitchildrenset(b'folder'), set())
428 # OPT: We should probably return set() for these; we don't because
430 # OPT: We should probably return set() for these; we don't because
429 # patternmatcher.visitdir() (our m2) doesn't return 'all' for subdirs of
431 # patternmatcher.visitdir() (our m2) doesn't return 'all' for subdirs of
430 # an 'all' pattern, just 'this'.
432 # an 'all' pattern, just 'this'.
431 self.assertEqual(dm.visitchildrenset(b'dir/subdir/z'), b'this')
433 self.assertEqual(dm.visitchildrenset(b'dir/subdir/z'), b'this')
432 self.assertEqual(dm.visitchildrenset(b'dir/subdir/x'), b'this')
434 self.assertEqual(dm.visitchildrenset(b'dir/subdir/x'), b'this')
433
435
434
436
435 class IntersectionMatcherTests(unittest.TestCase):
437 class IntersectionMatcherTests(unittest.TestCase):
436 def testVisitdirM2always(self):
438 def testVisitdirM2always(self):
437 m1 = matchmod.alwaysmatcher()
439 m1 = matchmod.alwaysmatcher()
438 m2 = matchmod.alwaysmatcher()
440 m2 = matchmod.alwaysmatcher()
439 im = matchmod.intersectmatchers(m1, m2)
441 im = matchmod.intersectmatchers(m1, m2)
440 # im should be equivalent to a alwaysmatcher.
442 # im should be equivalent to a alwaysmatcher.
441 self.assertEqual(im.visitdir(b''), b'all')
443 self.assertEqual(im.visitdir(b''), b'all')
442 self.assertEqual(im.visitdir(b'dir'), b'all')
444 self.assertEqual(im.visitdir(b'dir'), b'all')
443 self.assertEqual(im.visitdir(b'dir/subdir'), b'all')
445 self.assertEqual(im.visitdir(b'dir/subdir'), b'all')
444 self.assertEqual(im.visitdir(b'dir/subdir/z'), b'all')
446 self.assertEqual(im.visitdir(b'dir/subdir/z'), b'all')
445 self.assertEqual(im.visitdir(b'dir/foo'), b'all')
447 self.assertEqual(im.visitdir(b'dir/foo'), b'all')
446 self.assertEqual(im.visitdir(b'dir/subdir/x'), b'all')
448 self.assertEqual(im.visitdir(b'dir/subdir/x'), b'all')
447 self.assertEqual(im.visitdir(b'folder'), b'all')
449 self.assertEqual(im.visitdir(b'folder'), b'all')
448
450
449 def testVisitchildrensetM2always(self):
451 def testVisitchildrensetM2always(self):
450 m1 = matchmod.alwaysmatcher()
452 m1 = matchmod.alwaysmatcher()
451 m2 = matchmod.alwaysmatcher()
453 m2 = matchmod.alwaysmatcher()
452 im = matchmod.intersectmatchers(m1, m2)
454 im = matchmod.intersectmatchers(m1, m2)
453 # im should be equivalent to a alwaysmatcher.
455 # im should be equivalent to a alwaysmatcher.
454 self.assertEqual(im.visitchildrenset(b''), b'all')
456 self.assertEqual(im.visitchildrenset(b''), b'all')
455 self.assertEqual(im.visitchildrenset(b'dir'), b'all')
457 self.assertEqual(im.visitchildrenset(b'dir'), b'all')
456 self.assertEqual(im.visitchildrenset(b'dir/subdir'), b'all')
458 self.assertEqual(im.visitchildrenset(b'dir/subdir'), b'all')
457 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), b'all')
459 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), b'all')
458 self.assertEqual(im.visitchildrenset(b'dir/foo'), b'all')
460 self.assertEqual(im.visitchildrenset(b'dir/foo'), b'all')
459 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), b'all')
461 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), b'all')
460 self.assertEqual(im.visitchildrenset(b'folder'), b'all')
462 self.assertEqual(im.visitchildrenset(b'folder'), b'all')
461
463
462 def testVisitdirM2never(self):
464 def testVisitdirM2never(self):
463 m1 = matchmod.alwaysmatcher()
465 m1 = matchmod.alwaysmatcher()
464 m2 = matchmod.nevermatcher()
466 m2 = matchmod.nevermatcher()
465 im = matchmod.intersectmatchers(m1, m2)
467 im = matchmod.intersectmatchers(m1, m2)
466 # im should be equivalent to a nevermatcher.
468 # im should be equivalent to a nevermatcher.
467 self.assertFalse(im.visitdir(b''))
469 self.assertFalse(im.visitdir(b''))
468 self.assertFalse(im.visitdir(b'dir'))
470 self.assertFalse(im.visitdir(b'dir'))
469 self.assertFalse(im.visitdir(b'dir/subdir'))
471 self.assertFalse(im.visitdir(b'dir/subdir'))
470 self.assertFalse(im.visitdir(b'dir/subdir/z'))
472 self.assertFalse(im.visitdir(b'dir/subdir/z'))
471 self.assertFalse(im.visitdir(b'dir/foo'))
473 self.assertFalse(im.visitdir(b'dir/foo'))
472 self.assertFalse(im.visitdir(b'dir/subdir/x'))
474 self.assertFalse(im.visitdir(b'dir/subdir/x'))
473 self.assertFalse(im.visitdir(b'folder'))
475 self.assertFalse(im.visitdir(b'folder'))
474
476
475 def testVisitchildrensetM2never(self):
477 def testVisitchildrensetM2never(self):
476 m1 = matchmod.alwaysmatcher()
478 m1 = matchmod.alwaysmatcher()
477 m2 = matchmod.nevermatcher()
479 m2 = matchmod.nevermatcher()
478 im = matchmod.intersectmatchers(m1, m2)
480 im = matchmod.intersectmatchers(m1, m2)
479 # im should be equivalent to a nevermqtcher.
481 # im should be equivalent to a nevermqtcher.
480 self.assertEqual(im.visitchildrenset(b''), set())
482 self.assertEqual(im.visitchildrenset(b''), set())
481 self.assertEqual(im.visitchildrenset(b'dir'), set())
483 self.assertEqual(im.visitchildrenset(b'dir'), set())
482 self.assertEqual(im.visitchildrenset(b'dir/subdir'), set())
484 self.assertEqual(im.visitchildrenset(b'dir/subdir'), set())
483 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), set())
485 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), set())
484 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
486 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
485 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), set())
487 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), set())
486 self.assertEqual(im.visitchildrenset(b'folder'), set())
488 self.assertEqual(im.visitchildrenset(b'folder'), set())
487
489
488 def testVisitdirM2SubdirPrefix(self):
490 def testVisitdirM2SubdirPrefix(self):
489 m1 = matchmod.alwaysmatcher()
491 m1 = matchmod.alwaysmatcher()
490 m2 = matchmod.match(
492 m2 = matchmod.match(
491 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
493 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
492 )
494 )
493 im = matchmod.intersectmatchers(m1, m2)
495 im = matchmod.intersectmatchers(m1, m2)
494 self.assertEqual(im.visitdir(b''), True)
496 self.assertEqual(im.visitdir(b''), True)
495 self.assertEqual(im.visitdir(b'dir'), True)
497 self.assertEqual(im.visitdir(b'dir'), True)
496 self.assertEqual(im.visitdir(b'dir/subdir'), b'all')
498 self.assertEqual(im.visitdir(b'dir/subdir'), b'all')
497 self.assertFalse(im.visitdir(b'dir/foo'))
499 self.assertFalse(im.visitdir(b'dir/foo'))
498 self.assertFalse(im.visitdir(b'folder'))
500 self.assertFalse(im.visitdir(b'folder'))
499 # OPT: We should probably return 'all' for these; we don't because
501 # OPT: We should probably return 'all' for these; we don't because
500 # patternmatcher.visitdir() (our m2) doesn't return 'all' for subdirs of
502 # patternmatcher.visitdir() (our m2) doesn't return 'all' for subdirs of
501 # an 'all' pattern, just True.
503 # an 'all' pattern, just True.
502 self.assertEqual(im.visitdir(b'dir/subdir/z'), True)
504 self.assertEqual(im.visitdir(b'dir/subdir/z'), True)
503 self.assertEqual(im.visitdir(b'dir/subdir/x'), True)
505 self.assertEqual(im.visitdir(b'dir/subdir/x'), True)
504
506
505 def testVisitchildrensetM2SubdirPrefix(self):
507 def testVisitchildrensetM2SubdirPrefix(self):
506 m1 = matchmod.alwaysmatcher()
508 m1 = matchmod.alwaysmatcher()
507 m2 = matchmod.match(
509 m2 = matchmod.match(
508 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
510 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
509 )
511 )
510 im = matchmod.intersectmatchers(m1, m2)
512 im = matchmod.intersectmatchers(m1, m2)
511 self.assertEqual(im.visitchildrenset(b''), {b'dir'})
513 self.assertEqual(im.visitchildrenset(b''), {b'dir'})
512 self.assertEqual(im.visitchildrenset(b'dir'), {b'subdir'})
514 self.assertEqual(im.visitchildrenset(b'dir'), {b'subdir'})
513 self.assertEqual(im.visitchildrenset(b'dir/subdir'), b'all')
515 self.assertEqual(im.visitchildrenset(b'dir/subdir'), b'all')
514 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
516 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
515 self.assertEqual(im.visitchildrenset(b'folder'), set())
517 self.assertEqual(im.visitchildrenset(b'folder'), set())
516 # OPT: We should probably return 'all' for these
518 # OPT: We should probably return 'all' for these
517 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), b'this')
519 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), b'this')
518 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), b'this')
520 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), b'this')
519
521
520 # We're using includematcher instead of patterns because it behaves slightly
522 # We're using includematcher instead of patterns because it behaves slightly
521 # better (giving narrower results) than patternmatcher.
523 # better (giving narrower results) than patternmatcher.
522 def testVisitdirIncludeInclude(self):
524 def testVisitdirIncludeInclude(self):
523 m1 = matchmod.match(
525 m1 = matchmod.match(
524 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
526 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
525 )
527 )
526 m2 = matchmod.match(
528 m2 = matchmod.match(
527 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
529 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
528 )
530 )
529 im = matchmod.intersectmatchers(m1, m2)
531 im = matchmod.intersectmatchers(m1, m2)
530 self.assertEqual(im.visitdir(b''), True)
532 self.assertEqual(im.visitdir(b''), True)
531 self.assertEqual(im.visitdir(b'dir'), True)
533 self.assertEqual(im.visitdir(b'dir'), True)
532 self.assertFalse(im.visitdir(b'dir/subdir'))
534 self.assertFalse(im.visitdir(b'dir/subdir'))
533 self.assertFalse(im.visitdir(b'dir/foo'))
535 self.assertFalse(im.visitdir(b'dir/foo'))
534 self.assertFalse(im.visitdir(b'folder'))
536 self.assertFalse(im.visitdir(b'folder'))
535 self.assertFalse(im.visitdir(b'dir/subdir/z'))
537 self.assertFalse(im.visitdir(b'dir/subdir/z'))
536 self.assertFalse(im.visitdir(b'dir/subdir/x'))
538 self.assertFalse(im.visitdir(b'dir/subdir/x'))
537
539
538 def testVisitchildrensetIncludeInclude(self):
540 def testVisitchildrensetIncludeInclude(self):
539 m1 = matchmod.match(
541 m1 = matchmod.match(
540 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
542 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
541 )
543 )
542 m2 = matchmod.match(
544 m2 = matchmod.match(
543 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
545 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
544 )
546 )
545 im = matchmod.intersectmatchers(m1, m2)
547 im = matchmod.intersectmatchers(m1, m2)
546 self.assertEqual(im.visitchildrenset(b''), {b'dir'})
548 self.assertEqual(im.visitchildrenset(b''), {b'dir'})
547 self.assertEqual(im.visitchildrenset(b'dir'), b'this')
549 self.assertEqual(im.visitchildrenset(b'dir'), b'this')
548 self.assertEqual(im.visitchildrenset(b'dir/subdir'), set())
550 self.assertEqual(im.visitchildrenset(b'dir/subdir'), set())
549 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
551 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
550 self.assertEqual(im.visitchildrenset(b'folder'), set())
552 self.assertEqual(im.visitchildrenset(b'folder'), set())
551 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), set())
553 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), set())
552 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), set())
554 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), set())
553
555
554 # We're using includematcher instead of patterns because it behaves slightly
556 # We're using includematcher instead of patterns because it behaves slightly
555 # better (giving narrower results) than patternmatcher.
557 # better (giving narrower results) than patternmatcher.
556 def testVisitdirIncludeInclude2(self):
558 def testVisitdirIncludeInclude2(self):
557 m1 = matchmod.match(
559 m1 = matchmod.match(
558 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
560 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
559 )
561 )
560 m2 = matchmod.match(
562 m2 = matchmod.match(
561 util.localpath(b'/repo'), b'', include=[b'path:folder']
563 util.localpath(b'/repo'), b'', include=[b'path:folder']
562 )
564 )
563 im = matchmod.intersectmatchers(m1, m2)
565 im = matchmod.intersectmatchers(m1, m2)
564 # FIXME: is True correct here?
566 # FIXME: is True correct here?
565 self.assertEqual(im.visitdir(b''), True)
567 self.assertEqual(im.visitdir(b''), True)
566 self.assertFalse(im.visitdir(b'dir'))
568 self.assertFalse(im.visitdir(b'dir'))
567 self.assertFalse(im.visitdir(b'dir/subdir'))
569 self.assertFalse(im.visitdir(b'dir/subdir'))
568 self.assertFalse(im.visitdir(b'dir/foo'))
570 self.assertFalse(im.visitdir(b'dir/foo'))
569 self.assertFalse(im.visitdir(b'folder'))
571 self.assertFalse(im.visitdir(b'folder'))
570 self.assertFalse(im.visitdir(b'dir/subdir/z'))
572 self.assertFalse(im.visitdir(b'dir/subdir/z'))
571 self.assertFalse(im.visitdir(b'dir/subdir/x'))
573 self.assertFalse(im.visitdir(b'dir/subdir/x'))
572
574
573 def testVisitchildrensetIncludeInclude2(self):
575 def testVisitchildrensetIncludeInclude2(self):
574 m1 = matchmod.match(
576 m1 = matchmod.match(
575 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
577 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
576 )
578 )
577 m2 = matchmod.match(
579 m2 = matchmod.match(
578 util.localpath(b'/repo'), b'', include=[b'path:folder']
580 util.localpath(b'/repo'), b'', include=[b'path:folder']
579 )
581 )
580 im = matchmod.intersectmatchers(m1, m2)
582 im = matchmod.intersectmatchers(m1, m2)
581 # FIXME: is set() correct here?
583 # FIXME: is set() correct here?
582 self.assertEqual(im.visitchildrenset(b''), set())
584 self.assertEqual(im.visitchildrenset(b''), set())
583 self.assertEqual(im.visitchildrenset(b'dir'), set())
585 self.assertEqual(im.visitchildrenset(b'dir'), set())
584 self.assertEqual(im.visitchildrenset(b'dir/subdir'), set())
586 self.assertEqual(im.visitchildrenset(b'dir/subdir'), set())
585 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
587 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
586 self.assertEqual(im.visitchildrenset(b'folder'), set())
588 self.assertEqual(im.visitchildrenset(b'folder'), set())
587 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), set())
589 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), set())
588 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), set())
590 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), set())
589
591
590 # We're using includematcher instead of patterns because it behaves slightly
592 # We're using includematcher instead of patterns because it behaves slightly
591 # better (giving narrower results) than patternmatcher.
593 # better (giving narrower results) than patternmatcher.
592 def testVisitdirIncludeInclude3(self):
594 def testVisitdirIncludeInclude3(self):
593 m1 = matchmod.match(
595 m1 = matchmod.match(
594 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
596 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
595 )
597 )
596 m2 = matchmod.match(
598 m2 = matchmod.match(
597 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
599 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
598 )
600 )
599 im = matchmod.intersectmatchers(m1, m2)
601 im = matchmod.intersectmatchers(m1, m2)
600 self.assertEqual(im.visitdir(b''), True)
602 self.assertEqual(im.visitdir(b''), True)
601 self.assertEqual(im.visitdir(b'dir'), True)
603 self.assertEqual(im.visitdir(b'dir'), True)
602 self.assertEqual(im.visitdir(b'dir/subdir'), True)
604 self.assertEqual(im.visitdir(b'dir/subdir'), True)
603 self.assertFalse(im.visitdir(b'dir/foo'))
605 self.assertFalse(im.visitdir(b'dir/foo'))
604 self.assertFalse(im.visitdir(b'folder'))
606 self.assertFalse(im.visitdir(b'folder'))
605 self.assertFalse(im.visitdir(b'dir/subdir/z'))
607 self.assertFalse(im.visitdir(b'dir/subdir/z'))
606 # OPT: this should probably be 'all' not True.
608 # OPT: this should probably be 'all' not True.
607 self.assertEqual(im.visitdir(b'dir/subdir/x'), True)
609 self.assertEqual(im.visitdir(b'dir/subdir/x'), True)
608
610
609 def testVisitchildrensetIncludeInclude3(self):
611 def testVisitchildrensetIncludeInclude3(self):
610 m1 = matchmod.match(
612 m1 = matchmod.match(
611 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
613 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
612 )
614 )
613 m2 = matchmod.match(
615 m2 = matchmod.match(
614 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
616 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
615 )
617 )
616 im = matchmod.intersectmatchers(m1, m2)
618 im = matchmod.intersectmatchers(m1, m2)
617 self.assertEqual(im.visitchildrenset(b''), {b'dir'})
619 self.assertEqual(im.visitchildrenset(b''), {b'dir'})
618 self.assertEqual(im.visitchildrenset(b'dir'), {b'subdir'})
620 self.assertEqual(im.visitchildrenset(b'dir'), {b'subdir'})
619 self.assertEqual(im.visitchildrenset(b'dir/subdir'), {b'x'})
621 self.assertEqual(im.visitchildrenset(b'dir/subdir'), {b'x'})
620 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
622 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
621 self.assertEqual(im.visitchildrenset(b'folder'), set())
623 self.assertEqual(im.visitchildrenset(b'folder'), set())
622 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), set())
624 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), set())
623 # OPT: this should probably be 'all' not 'this'.
625 # OPT: this should probably be 'all' not 'this'.
624 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), b'this')
626 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), b'this')
625
627
626 # We're using includematcher instead of patterns because it behaves slightly
628 # We're using includematcher instead of patterns because it behaves slightly
627 # better (giving narrower results) than patternmatcher.
629 # better (giving narrower results) than patternmatcher.
628 def testVisitdirIncludeInclude4(self):
630 def testVisitdirIncludeInclude4(self):
629 m1 = matchmod.match(
631 m1 = matchmod.match(
630 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
632 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
631 )
633 )
632 m2 = matchmod.match(
634 m2 = matchmod.match(
633 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/z']
635 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/z']
634 )
636 )
635 im = matchmod.intersectmatchers(m1, m2)
637 im = matchmod.intersectmatchers(m1, m2)
636 # OPT: these next three could probably be False as well.
638 # OPT: these next three could probably be False as well.
637 self.assertEqual(im.visitdir(b''), True)
639 self.assertEqual(im.visitdir(b''), True)
638 self.assertEqual(im.visitdir(b'dir'), True)
640 self.assertEqual(im.visitdir(b'dir'), True)
639 self.assertEqual(im.visitdir(b'dir/subdir'), True)
641 self.assertEqual(im.visitdir(b'dir/subdir'), True)
640 self.assertFalse(im.visitdir(b'dir/foo'))
642 self.assertFalse(im.visitdir(b'dir/foo'))
641 self.assertFalse(im.visitdir(b'folder'))
643 self.assertFalse(im.visitdir(b'folder'))
642 self.assertFalse(im.visitdir(b'dir/subdir/z'))
644 self.assertFalse(im.visitdir(b'dir/subdir/z'))
643 self.assertFalse(im.visitdir(b'dir/subdir/x'))
645 self.assertFalse(im.visitdir(b'dir/subdir/x'))
644
646
645 def testVisitchildrensetIncludeInclude4(self):
647 def testVisitchildrensetIncludeInclude4(self):
646 m1 = matchmod.match(
648 m1 = matchmod.match(
647 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
649 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
648 )
650 )
649 m2 = matchmod.match(
651 m2 = matchmod.match(
650 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/z']
652 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/z']
651 )
653 )
652 im = matchmod.intersectmatchers(m1, m2)
654 im = matchmod.intersectmatchers(m1, m2)
653 # OPT: these next two could probably be set() as well.
655 # OPT: these next two could probably be set() as well.
654 self.assertEqual(im.visitchildrenset(b''), {b'dir'})
656 self.assertEqual(im.visitchildrenset(b''), {b'dir'})
655 self.assertEqual(im.visitchildrenset(b'dir'), {b'subdir'})
657 self.assertEqual(im.visitchildrenset(b'dir'), {b'subdir'})
656 self.assertEqual(im.visitchildrenset(b'dir/subdir'), set())
658 self.assertEqual(im.visitchildrenset(b'dir/subdir'), set())
657 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
659 self.assertEqual(im.visitchildrenset(b'dir/foo'), set())
658 self.assertEqual(im.visitchildrenset(b'folder'), set())
660 self.assertEqual(im.visitchildrenset(b'folder'), set())
659 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), set())
661 self.assertEqual(im.visitchildrenset(b'dir/subdir/z'), set())
660 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), set())
662 self.assertEqual(im.visitchildrenset(b'dir/subdir/x'), set())
661
663
662
664
663 class UnionMatcherTests(unittest.TestCase):
665 class UnionMatcherTests(unittest.TestCase):
664 def testVisitdirM2always(self):
666 def testVisitdirM2always(self):
665 m1 = matchmod.alwaysmatcher()
667 m1 = matchmod.alwaysmatcher()
666 m2 = matchmod.alwaysmatcher()
668 m2 = matchmod.alwaysmatcher()
667 um = matchmod.unionmatcher([m1, m2])
669 um = matchmod.unionmatcher([m1, m2])
668 # um should be equivalent to a alwaysmatcher.
670 # um should be equivalent to a alwaysmatcher.
669 self.assertEqual(um.visitdir(b''), b'all')
671 self.assertEqual(um.visitdir(b''), b'all')
670 self.assertEqual(um.visitdir(b'dir'), b'all')
672 self.assertEqual(um.visitdir(b'dir'), b'all')
671 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
673 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
672 self.assertEqual(um.visitdir(b'dir/subdir/z'), b'all')
674 self.assertEqual(um.visitdir(b'dir/subdir/z'), b'all')
673 self.assertEqual(um.visitdir(b'dir/foo'), b'all')
675 self.assertEqual(um.visitdir(b'dir/foo'), b'all')
674 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
676 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
675 self.assertEqual(um.visitdir(b'folder'), b'all')
677 self.assertEqual(um.visitdir(b'folder'), b'all')
676
678
677 def testVisitchildrensetM2always(self):
679 def testVisitchildrensetM2always(self):
678 m1 = matchmod.alwaysmatcher()
680 m1 = matchmod.alwaysmatcher()
679 m2 = matchmod.alwaysmatcher()
681 m2 = matchmod.alwaysmatcher()
680 um = matchmod.unionmatcher([m1, m2])
682 um = matchmod.unionmatcher([m1, m2])
681 # um should be equivalent to a alwaysmatcher.
683 # um should be equivalent to a alwaysmatcher.
682 self.assertEqual(um.visitchildrenset(b''), b'all')
684 self.assertEqual(um.visitchildrenset(b''), b'all')
683 self.assertEqual(um.visitchildrenset(b'dir'), b'all')
685 self.assertEqual(um.visitchildrenset(b'dir'), b'all')
684 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
686 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
685 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'all')
687 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'all')
686 self.assertEqual(um.visitchildrenset(b'dir/foo'), b'all')
688 self.assertEqual(um.visitchildrenset(b'dir/foo'), b'all')
687 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
689 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
688 self.assertEqual(um.visitchildrenset(b'folder'), b'all')
690 self.assertEqual(um.visitchildrenset(b'folder'), b'all')
689
691
690 def testVisitdirM1never(self):
692 def testVisitdirM1never(self):
691 m1 = matchmod.nevermatcher()
693 m1 = matchmod.nevermatcher()
692 m2 = matchmod.alwaysmatcher()
694 m2 = matchmod.alwaysmatcher()
693 um = matchmod.unionmatcher([m1, m2])
695 um = matchmod.unionmatcher([m1, m2])
694 # um should be equivalent to a alwaysmatcher.
696 # um should be equivalent to a alwaysmatcher.
695 self.assertEqual(um.visitdir(b''), b'all')
697 self.assertEqual(um.visitdir(b''), b'all')
696 self.assertEqual(um.visitdir(b'dir'), b'all')
698 self.assertEqual(um.visitdir(b'dir'), b'all')
697 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
699 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
698 self.assertEqual(um.visitdir(b'dir/subdir/z'), b'all')
700 self.assertEqual(um.visitdir(b'dir/subdir/z'), b'all')
699 self.assertEqual(um.visitdir(b'dir/foo'), b'all')
701 self.assertEqual(um.visitdir(b'dir/foo'), b'all')
700 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
702 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
701 self.assertEqual(um.visitdir(b'folder'), b'all')
703 self.assertEqual(um.visitdir(b'folder'), b'all')
702
704
703 def testVisitchildrensetM1never(self):
705 def testVisitchildrensetM1never(self):
704 m1 = matchmod.nevermatcher()
706 m1 = matchmod.nevermatcher()
705 m2 = matchmod.alwaysmatcher()
707 m2 = matchmod.alwaysmatcher()
706 um = matchmod.unionmatcher([m1, m2])
708 um = matchmod.unionmatcher([m1, m2])
707 # um should be equivalent to a alwaysmatcher.
709 # um should be equivalent to a alwaysmatcher.
708 self.assertEqual(um.visitchildrenset(b''), b'all')
710 self.assertEqual(um.visitchildrenset(b''), b'all')
709 self.assertEqual(um.visitchildrenset(b'dir'), b'all')
711 self.assertEqual(um.visitchildrenset(b'dir'), b'all')
710 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
712 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
711 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'all')
713 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'all')
712 self.assertEqual(um.visitchildrenset(b'dir/foo'), b'all')
714 self.assertEqual(um.visitchildrenset(b'dir/foo'), b'all')
713 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
715 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
714 self.assertEqual(um.visitchildrenset(b'folder'), b'all')
716 self.assertEqual(um.visitchildrenset(b'folder'), b'all')
715
717
716 def testVisitdirM2never(self):
718 def testVisitdirM2never(self):
717 m1 = matchmod.alwaysmatcher()
719 m1 = matchmod.alwaysmatcher()
718 m2 = matchmod.nevermatcher()
720 m2 = matchmod.nevermatcher()
719 um = matchmod.unionmatcher([m1, m2])
721 um = matchmod.unionmatcher([m1, m2])
720 # um should be equivalent to a alwaysmatcher.
722 # um should be equivalent to a alwaysmatcher.
721 self.assertEqual(um.visitdir(b''), b'all')
723 self.assertEqual(um.visitdir(b''), b'all')
722 self.assertEqual(um.visitdir(b'dir'), b'all')
724 self.assertEqual(um.visitdir(b'dir'), b'all')
723 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
725 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
724 self.assertEqual(um.visitdir(b'dir/subdir/z'), b'all')
726 self.assertEqual(um.visitdir(b'dir/subdir/z'), b'all')
725 self.assertEqual(um.visitdir(b'dir/foo'), b'all')
727 self.assertEqual(um.visitdir(b'dir/foo'), b'all')
726 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
728 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
727 self.assertEqual(um.visitdir(b'folder'), b'all')
729 self.assertEqual(um.visitdir(b'folder'), b'all')
728
730
729 def testVisitchildrensetM2never(self):
731 def testVisitchildrensetM2never(self):
730 m1 = matchmod.alwaysmatcher()
732 m1 = matchmod.alwaysmatcher()
731 m2 = matchmod.nevermatcher()
733 m2 = matchmod.nevermatcher()
732 um = matchmod.unionmatcher([m1, m2])
734 um = matchmod.unionmatcher([m1, m2])
733 # um should be equivalent to a alwaysmatcher.
735 # um should be equivalent to a alwaysmatcher.
734 self.assertEqual(um.visitchildrenset(b''), b'all')
736 self.assertEqual(um.visitchildrenset(b''), b'all')
735 self.assertEqual(um.visitchildrenset(b'dir'), b'all')
737 self.assertEqual(um.visitchildrenset(b'dir'), b'all')
736 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
738 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
737 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'all')
739 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'all')
738 self.assertEqual(um.visitchildrenset(b'dir/foo'), b'all')
740 self.assertEqual(um.visitchildrenset(b'dir/foo'), b'all')
739 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
741 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
740 self.assertEqual(um.visitchildrenset(b'folder'), b'all')
742 self.assertEqual(um.visitchildrenset(b'folder'), b'all')
741
743
742 def testVisitdirM2SubdirPrefix(self):
744 def testVisitdirM2SubdirPrefix(self):
743 m1 = matchmod.alwaysmatcher()
745 m1 = matchmod.alwaysmatcher()
744 m2 = matchmod.match(
746 m2 = matchmod.match(
745 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
747 util.localpath(b'/repo'), b'', patterns=[b'path:dir/subdir']
746 )
748 )
747 um = matchmod.unionmatcher([m1, m2])
749 um = matchmod.unionmatcher([m1, m2])
748 self.assertEqual(um.visitdir(b''), b'all')
750 self.assertEqual(um.visitdir(b''), b'all')
749 self.assertEqual(um.visitdir(b'dir'), b'all')
751 self.assertEqual(um.visitdir(b'dir'), b'all')
750 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
752 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
751 self.assertEqual(um.visitdir(b'dir/foo'), b'all')
753 self.assertEqual(um.visitdir(b'dir/foo'), b'all')
752 self.assertEqual(um.visitdir(b'folder'), b'all')
754 self.assertEqual(um.visitdir(b'folder'), b'all')
753 self.assertEqual(um.visitdir(b'dir/subdir/z'), b'all')
755 self.assertEqual(um.visitdir(b'dir/subdir/z'), b'all')
754 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
756 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
755
757
756 def testVisitchildrensetM2SubdirPrefix(self):
758 def testVisitchildrensetM2SubdirPrefix(self):
757 m1 = matchmod.alwaysmatcher()
759 m1 = matchmod.alwaysmatcher()
758 m2 = matchmod.match(
760 m2 = matchmod.match(
759 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
761 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
760 )
762 )
761 um = matchmod.unionmatcher([m1, m2])
763 um = matchmod.unionmatcher([m1, m2])
762 self.assertEqual(um.visitchildrenset(b''), b'all')
764 self.assertEqual(um.visitchildrenset(b''), b'all')
763 self.assertEqual(um.visitchildrenset(b'dir'), b'all')
765 self.assertEqual(um.visitchildrenset(b'dir'), b'all')
764 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
766 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
765 self.assertEqual(um.visitchildrenset(b'dir/foo'), b'all')
767 self.assertEqual(um.visitchildrenset(b'dir/foo'), b'all')
766 self.assertEqual(um.visitchildrenset(b'folder'), b'all')
768 self.assertEqual(um.visitchildrenset(b'folder'), b'all')
767 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'all')
769 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'all')
768 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
770 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
769
771
770 # We're using includematcher instead of patterns because it behaves slightly
772 # We're using includematcher instead of patterns because it behaves slightly
771 # better (giving narrower results) than patternmatcher.
773 # better (giving narrower results) than patternmatcher.
772 def testVisitdirIncludeInclude(self):
774 def testVisitdirIncludeInclude(self):
773 m1 = matchmod.match(
775 m1 = matchmod.match(
774 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
776 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
775 )
777 )
776 m2 = matchmod.match(
778 m2 = matchmod.match(
777 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
779 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
778 )
780 )
779 um = matchmod.unionmatcher([m1, m2])
781 um = matchmod.unionmatcher([m1, m2])
780 self.assertEqual(um.visitdir(b''), True)
782 self.assertEqual(um.visitdir(b''), True)
781 self.assertEqual(um.visitdir(b'dir'), True)
783 self.assertEqual(um.visitdir(b'dir'), True)
782 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
784 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
783 self.assertFalse(um.visitdir(b'dir/foo'))
785 self.assertFalse(um.visitdir(b'dir/foo'))
784 self.assertFalse(um.visitdir(b'folder'))
786 self.assertFalse(um.visitdir(b'folder'))
785 # OPT: These two should probably be 'all' not True.
787 # OPT: These two should probably be 'all' not True.
786 self.assertEqual(um.visitdir(b'dir/subdir/z'), True)
788 self.assertEqual(um.visitdir(b'dir/subdir/z'), True)
787 self.assertEqual(um.visitdir(b'dir/subdir/x'), True)
789 self.assertEqual(um.visitdir(b'dir/subdir/x'), True)
788
790
789 def testVisitchildrensetIncludeInclude(self):
791 def testVisitchildrensetIncludeInclude(self):
790 m1 = matchmod.match(
792 m1 = matchmod.match(
791 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
793 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
792 )
794 )
793 m2 = matchmod.match(
795 m2 = matchmod.match(
794 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
796 util.localpath(b'/repo'), b'', include=[b'rootfilesin:dir']
795 )
797 )
796 um = matchmod.unionmatcher([m1, m2])
798 um = matchmod.unionmatcher([m1, m2])
797 self.assertEqual(um.visitchildrenset(b''), {b'dir'})
799 self.assertEqual(um.visitchildrenset(b''), {b'dir'})
798 self.assertEqual(um.visitchildrenset(b'dir'), b'this')
800 self.assertEqual(um.visitchildrenset(b'dir'), b'this')
799 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
801 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
800 self.assertEqual(um.visitchildrenset(b'dir/foo'), set())
802 self.assertEqual(um.visitchildrenset(b'dir/foo'), set())
801 self.assertEqual(um.visitchildrenset(b'folder'), set())
803 self.assertEqual(um.visitchildrenset(b'folder'), set())
802 # OPT: These next two could be 'all' instead of 'this'.
804 # OPT: These next two could be 'all' instead of 'this'.
803 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'this')
805 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'this')
804 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'this')
806 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'this')
805
807
806 # We're using includematcher instead of patterns because it behaves slightly
808 # We're using includematcher instead of patterns because it behaves slightly
807 # better (giving narrower results) than patternmatcher.
809 # better (giving narrower results) than patternmatcher.
808 def testVisitdirIncludeInclude2(self):
810 def testVisitdirIncludeInclude2(self):
809 m1 = matchmod.match(
811 m1 = matchmod.match(
810 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
812 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
811 )
813 )
812 m2 = matchmod.match(
814 m2 = matchmod.match(
813 util.localpath(b'/repo'), b'', include=[b'path:folder']
815 util.localpath(b'/repo'), b'', include=[b'path:folder']
814 )
816 )
815 um = matchmod.unionmatcher([m1, m2])
817 um = matchmod.unionmatcher([m1, m2])
816 self.assertEqual(um.visitdir(b''), True)
818 self.assertEqual(um.visitdir(b''), True)
817 self.assertEqual(um.visitdir(b'dir'), True)
819 self.assertEqual(um.visitdir(b'dir'), True)
818 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
820 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
819 self.assertFalse(um.visitdir(b'dir/foo'))
821 self.assertFalse(um.visitdir(b'dir/foo'))
820 self.assertEqual(um.visitdir(b'folder'), b'all')
822 self.assertEqual(um.visitdir(b'folder'), b'all')
821 # OPT: These should probably be 'all' not True.
823 # OPT: These should probably be 'all' not True.
822 self.assertEqual(um.visitdir(b'dir/subdir/z'), True)
824 self.assertEqual(um.visitdir(b'dir/subdir/z'), True)
823 self.assertEqual(um.visitdir(b'dir/subdir/x'), True)
825 self.assertEqual(um.visitdir(b'dir/subdir/x'), True)
824
826
825 def testVisitchildrensetIncludeInclude2(self):
827 def testVisitchildrensetIncludeInclude2(self):
826 m1 = matchmod.match(
828 m1 = matchmod.match(
827 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
829 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
828 )
830 )
829 m2 = matchmod.match(
831 m2 = matchmod.match(
830 util.localpath(b'/repo'), b'', include=[b'path:folder']
832 util.localpath(b'/repo'), b'', include=[b'path:folder']
831 )
833 )
832 um = matchmod.unionmatcher([m1, m2])
834 um = matchmod.unionmatcher([m1, m2])
833 self.assertEqual(um.visitchildrenset(b''), {b'folder', b'dir'})
835 self.assertEqual(um.visitchildrenset(b''), {b'folder', b'dir'})
834 self.assertEqual(um.visitchildrenset(b'dir'), {b'subdir'})
836 self.assertEqual(um.visitchildrenset(b'dir'), {b'subdir'})
835 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
837 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
836 self.assertEqual(um.visitchildrenset(b'dir/foo'), set())
838 self.assertEqual(um.visitchildrenset(b'dir/foo'), set())
837 self.assertEqual(um.visitchildrenset(b'folder'), b'all')
839 self.assertEqual(um.visitchildrenset(b'folder'), b'all')
838 # OPT: These next two could be 'all' instead of 'this'.
840 # OPT: These next two could be 'all' instead of 'this'.
839 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'this')
841 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'this')
840 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'this')
842 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'this')
841
843
842 # We're using includematcher instead of patterns because it behaves slightly
844 # We're using includematcher instead of patterns because it behaves slightly
843 # better (giving narrower results) than patternmatcher.
845 # better (giving narrower results) than patternmatcher.
844 def testVisitdirIncludeInclude3(self):
846 def testVisitdirIncludeInclude3(self):
845 m1 = matchmod.match(
847 m1 = matchmod.match(
846 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
848 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
847 )
849 )
848 m2 = matchmod.match(
850 m2 = matchmod.match(
849 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
851 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
850 )
852 )
851 um = matchmod.unionmatcher([m1, m2])
853 um = matchmod.unionmatcher([m1, m2])
852 self.assertEqual(um.visitdir(b''), True)
854 self.assertEqual(um.visitdir(b''), True)
853 self.assertEqual(um.visitdir(b'dir'), True)
855 self.assertEqual(um.visitdir(b'dir'), True)
854 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
856 self.assertEqual(um.visitdir(b'dir/subdir'), b'all')
855 self.assertFalse(um.visitdir(b'dir/foo'))
857 self.assertFalse(um.visitdir(b'dir/foo'))
856 self.assertFalse(um.visitdir(b'folder'))
858 self.assertFalse(um.visitdir(b'folder'))
857 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
859 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
858 # OPT: this should probably be 'all' not True.
860 # OPT: this should probably be 'all' not True.
859 self.assertEqual(um.visitdir(b'dir/subdir/z'), True)
861 self.assertEqual(um.visitdir(b'dir/subdir/z'), True)
860
862
861 def testVisitchildrensetIncludeInclude3(self):
863 def testVisitchildrensetIncludeInclude3(self):
862 m1 = matchmod.match(
864 m1 = matchmod.match(
863 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
865 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
864 )
866 )
865 m2 = matchmod.match(
867 m2 = matchmod.match(
866 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
868 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
867 )
869 )
868 um = matchmod.unionmatcher([m1, m2])
870 um = matchmod.unionmatcher([m1, m2])
869 self.assertEqual(um.visitchildrenset(b''), {b'dir'})
871 self.assertEqual(um.visitchildrenset(b''), {b'dir'})
870 self.assertEqual(um.visitchildrenset(b'dir'), {b'subdir'})
872 self.assertEqual(um.visitchildrenset(b'dir'), {b'subdir'})
871 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
873 self.assertEqual(um.visitchildrenset(b'dir/subdir'), b'all')
872 self.assertEqual(um.visitchildrenset(b'dir/foo'), set())
874 self.assertEqual(um.visitchildrenset(b'dir/foo'), set())
873 self.assertEqual(um.visitchildrenset(b'folder'), set())
875 self.assertEqual(um.visitchildrenset(b'folder'), set())
874 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
876 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
875 # OPT: this should probably be 'all' not 'this'.
877 # OPT: this should probably be 'all' not 'this'.
876 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'this')
878 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'this')
877
879
878 # We're using includematcher instead of patterns because it behaves slightly
880 # We're using includematcher instead of patterns because it behaves slightly
879 # better (giving narrower results) than patternmatcher.
881 # better (giving narrower results) than patternmatcher.
880 def testVisitdirIncludeInclude4(self):
882 def testVisitdirIncludeInclude4(self):
881 m1 = matchmod.match(
883 m1 = matchmod.match(
882 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
884 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
883 )
885 )
884 m2 = matchmod.match(
886 m2 = matchmod.match(
885 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/z']
887 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/z']
886 )
888 )
887 um = matchmod.unionmatcher([m1, m2])
889 um = matchmod.unionmatcher([m1, m2])
888 # OPT: these next three could probably be False as well.
890 # OPT: these next three could probably be False as well.
889 self.assertEqual(um.visitdir(b''), True)
891 self.assertEqual(um.visitdir(b''), True)
890 self.assertEqual(um.visitdir(b'dir'), True)
892 self.assertEqual(um.visitdir(b'dir'), True)
891 self.assertEqual(um.visitdir(b'dir/subdir'), True)
893 self.assertEqual(um.visitdir(b'dir/subdir'), True)
892 self.assertFalse(um.visitdir(b'dir/foo'))
894 self.assertFalse(um.visitdir(b'dir/foo'))
893 self.assertFalse(um.visitdir(b'folder'))
895 self.assertFalse(um.visitdir(b'folder'))
894 self.assertEqual(um.visitdir(b'dir/subdir/z'), b'all')
896 self.assertEqual(um.visitdir(b'dir/subdir/z'), b'all')
895 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
897 self.assertEqual(um.visitdir(b'dir/subdir/x'), b'all')
896
898
897 def testVisitchildrensetIncludeInclude4(self):
899 def testVisitchildrensetIncludeInclude4(self):
898 m1 = matchmod.match(
900 m1 = matchmod.match(
899 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
901 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/x']
900 )
902 )
901 m2 = matchmod.match(
903 m2 = matchmod.match(
902 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/z']
904 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir/z']
903 )
905 )
904 um = matchmod.unionmatcher([m1, m2])
906 um = matchmod.unionmatcher([m1, m2])
905 self.assertEqual(um.visitchildrenset(b''), {b'dir'})
907 self.assertEqual(um.visitchildrenset(b''), {b'dir'})
906 self.assertEqual(um.visitchildrenset(b'dir'), {b'subdir'})
908 self.assertEqual(um.visitchildrenset(b'dir'), {b'subdir'})
907 self.assertEqual(um.visitchildrenset(b'dir/subdir'), {b'x', b'z'})
909 self.assertEqual(um.visitchildrenset(b'dir/subdir'), {b'x', b'z'})
908 self.assertEqual(um.visitchildrenset(b'dir/foo'), set())
910 self.assertEqual(um.visitchildrenset(b'dir/foo'), set())
909 self.assertEqual(um.visitchildrenset(b'folder'), set())
911 self.assertEqual(um.visitchildrenset(b'folder'), set())
910 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'all')
912 self.assertEqual(um.visitchildrenset(b'dir/subdir/z'), b'all')
911 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
913 self.assertEqual(um.visitchildrenset(b'dir/subdir/x'), b'all')
912
914
913
915
914 class SubdirMatcherTests(unittest.TestCase):
916 class SubdirMatcherTests(unittest.TestCase):
915 def testVisitdir(self):
917 def testVisitdir(self):
916 m = matchmod.match(
918 m = matchmod.match(
917 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
919 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
918 )
920 )
919 sm = matchmod.subdirmatcher(b'dir', m)
921 sm = matchmod.subdirmatcher(b'dir', m)
920
922
921 self.assertEqual(sm.visitdir(b''), True)
923 self.assertEqual(sm.visitdir(b''), True)
922 self.assertEqual(sm.visitdir(b'subdir'), b'all')
924 self.assertEqual(sm.visitdir(b'subdir'), b'all')
923 # OPT: These next two should probably be 'all' not True.
925 # OPT: These next two should probably be 'all' not True.
924 self.assertEqual(sm.visitdir(b'subdir/x'), True)
926 self.assertEqual(sm.visitdir(b'subdir/x'), True)
925 self.assertEqual(sm.visitdir(b'subdir/z'), True)
927 self.assertEqual(sm.visitdir(b'subdir/z'), True)
926 self.assertFalse(sm.visitdir(b'foo'))
928 self.assertFalse(sm.visitdir(b'foo'))
927
929
928 def testVisitchildrenset(self):
930 def testVisitchildrenset(self):
929 m = matchmod.match(
931 m = matchmod.match(
930 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
932 util.localpath(b'/repo'), b'', include=[b'path:dir/subdir']
931 )
933 )
932 sm = matchmod.subdirmatcher(b'dir', m)
934 sm = matchmod.subdirmatcher(b'dir', m)
933
935
934 self.assertEqual(sm.visitchildrenset(b''), {b'subdir'})
936 self.assertEqual(sm.visitchildrenset(b''), {b'subdir'})
935 self.assertEqual(sm.visitchildrenset(b'subdir'), b'all')
937 self.assertEqual(sm.visitchildrenset(b'subdir'), b'all')
936 # OPT: These next two should probably be 'all' not 'this'.
938 # OPT: These next two should probably be 'all' not 'this'.
937 self.assertEqual(sm.visitchildrenset(b'subdir/x'), b'this')
939 self.assertEqual(sm.visitchildrenset(b'subdir/x'), b'this')
938 self.assertEqual(sm.visitchildrenset(b'subdir/z'), b'this')
940 self.assertEqual(sm.visitchildrenset(b'subdir/z'), b'this')
939 self.assertEqual(sm.visitchildrenset(b'foo'), set())
941 self.assertEqual(sm.visitchildrenset(b'foo'), set())
940
942
941
943
942 class PrefixdirMatcherTests(unittest.TestCase):
944 class PrefixdirMatcherTests(unittest.TestCase):
943 def testVisitdir(self):
945 def testVisitdir(self):
944 m = matchmod.match(
946 m = matchmod.match(
945 util.localpath(b'/root/d'),
947 util.localpath(b'/root/d'),
946 b'e/f',
948 b'e/f',
947 [b'../a.txt', b'b.txt'],
949 [b'../a.txt', b'b.txt'],
948 auditor=noop_auditor,
950 auditor=noop_auditor,
949 )
951 )
950 pm = matchmod.prefixdirmatcher(b'd', m)
952 pm = matchmod.prefixdirmatcher(b'd', m)
951
953
952 # `m` elides 'd' because it's part of the root, and the rest of the
954 # `m` elides 'd' because it's part of the root, and the rest of the
953 # patterns are relative.
955 # patterns are relative.
954 self.assertEqual(bool(m(b'a.txt')), False)
956 self.assertEqual(bool(m(b'a.txt')), False)
955 self.assertEqual(bool(m(b'b.txt')), False)
957 self.assertEqual(bool(m(b'b.txt')), False)
956 self.assertEqual(bool(m(b'e/a.txt')), True)
958 self.assertEqual(bool(m(b'e/a.txt')), True)
957 self.assertEqual(bool(m(b'e/b.txt')), False)
959 self.assertEqual(bool(m(b'e/b.txt')), False)
958 self.assertEqual(bool(m(b'e/f/b.txt')), True)
960 self.assertEqual(bool(m(b'e/f/b.txt')), True)
959
961
960 # The prefix matcher re-adds 'd' to the paths, so they need to be
962 # The prefix matcher re-adds 'd' to the paths, so they need to be
961 # specified when using the prefixdirmatcher.
963 # specified when using the prefixdirmatcher.
962 self.assertEqual(bool(pm(b'a.txt')), False)
964 self.assertEqual(bool(pm(b'a.txt')), False)
963 self.assertEqual(bool(pm(b'b.txt')), False)
965 self.assertEqual(bool(pm(b'b.txt')), False)
964 self.assertEqual(bool(pm(b'd/e/a.txt')), True)
966 self.assertEqual(bool(pm(b'd/e/a.txt')), True)
965 self.assertEqual(bool(pm(b'd/e/b.txt')), False)
967 self.assertEqual(bool(pm(b'd/e/b.txt')), False)
966 self.assertEqual(bool(pm(b'd/e/f/b.txt')), True)
968 self.assertEqual(bool(pm(b'd/e/f/b.txt')), True)
967
969
968 self.assertEqual(m.visitdir(b''), True)
970 self.assertEqual(m.visitdir(b''), True)
969 self.assertEqual(m.visitdir(b'e'), True)
971 self.assertEqual(m.visitdir(b'e'), True)
970 self.assertEqual(m.visitdir(b'e/f'), True)
972 self.assertEqual(m.visitdir(b'e/f'), True)
971 self.assertEqual(m.visitdir(b'e/f/g'), False)
973 self.assertEqual(m.visitdir(b'e/f/g'), False)
972
974
973 self.assertEqual(pm.visitdir(b''), True)
975 self.assertEqual(pm.visitdir(b''), True)
974 self.assertEqual(pm.visitdir(b'd'), True)
976 self.assertEqual(pm.visitdir(b'd'), True)
975 self.assertEqual(pm.visitdir(b'd/e'), True)
977 self.assertEqual(pm.visitdir(b'd/e'), True)
976 self.assertEqual(pm.visitdir(b'd/e/f'), True)
978 self.assertEqual(pm.visitdir(b'd/e/f'), True)
977 self.assertEqual(pm.visitdir(b'd/e/f/g'), False)
979 self.assertEqual(pm.visitdir(b'd/e/f/g'), False)
978
980
979 def testVisitchildrenset(self):
981 def testVisitchildrenset(self):
980 m = matchmod.match(
982 m = matchmod.match(
981 util.localpath(b'/root/d'),
983 util.localpath(b'/root/d'),
982 b'e/f',
984 b'e/f',
983 [b'../a.txt', b'b.txt'],
985 [b'../a.txt', b'b.txt'],
984 auditor=noop_auditor,
986 auditor=noop_auditor,
985 )
987 )
986 pm = matchmod.prefixdirmatcher(b'd', m)
988 pm = matchmod.prefixdirmatcher(b'd', m)
987
989
988 # OPT: visitchildrenset could possibly return {'e'} and {'f'} for these
990 # OPT: visitchildrenset could possibly return {'e'} and {'f'} for these
989 # next two, respectively; patternmatcher does not have this
991 # next two, respectively; patternmatcher does not have this
990 # optimization.
992 # optimization.
991 self.assertEqual(m.visitchildrenset(b''), b'this')
993 self.assertEqual(m.visitchildrenset(b''), b'this')
992 self.assertEqual(m.visitchildrenset(b'e'), b'this')
994 self.assertEqual(m.visitchildrenset(b'e'), b'this')
993 self.assertEqual(m.visitchildrenset(b'e/f'), b'this')
995 self.assertEqual(m.visitchildrenset(b'e/f'), b'this')
994 self.assertEqual(m.visitchildrenset(b'e/f/g'), set())
996 self.assertEqual(m.visitchildrenset(b'e/f/g'), set())
995
997
996 # OPT: visitchildrenset could possibly return {'d'}, {'e'}, and {'f'}
998 # OPT: visitchildrenset could possibly return {'d'}, {'e'}, and {'f'}
997 # for these next three, respectively; patternmatcher does not have this
999 # for these next three, respectively; patternmatcher does not have this
998 # optimization.
1000 # optimization.
999 self.assertEqual(pm.visitchildrenset(b''), b'this')
1001 self.assertEqual(pm.visitchildrenset(b''), b'this')
1000 self.assertEqual(pm.visitchildrenset(b'd'), b'this')
1002 self.assertEqual(pm.visitchildrenset(b'd'), b'this')
1001 self.assertEqual(pm.visitchildrenset(b'd/e'), b'this')
1003 self.assertEqual(pm.visitchildrenset(b'd/e'), b'this')
1002 self.assertEqual(pm.visitchildrenset(b'd/e/f'), b'this')
1004 self.assertEqual(pm.visitchildrenset(b'd/e/f'), b'this')
1003 self.assertEqual(pm.visitchildrenset(b'd/e/f/g'), set())
1005 self.assertEqual(pm.visitchildrenset(b'd/e/f/g'), set())
1004
1006
1005
1007
1006 if __name__ == '__main__':
1008 if __name__ == '__main__':
1007 silenttestrunner.main(__name__)
1009 silenttestrunner.main(__name__)
@@ -1,62 +1,64 b''
1 import io
1 import io
2 import unittest
2 import unittest
3
3
4 import silenttestrunner
4 import silenttestrunner
5
5
6 from mercurial import (
6 from mercurial import (
7 wireprotoserver,
7 wireprotoserver,
8 wireprotov1server,
8 wireprotov1server,
9 )
9 )
10
10
11 from mercurial.utils import procutil
11 from mercurial.utils import procutil
12
12
13
13
14 class SSHServerGetArgsTests(unittest.TestCase):
14 class SSHServerGetArgsTests(unittest.TestCase):
15 def testparseknown(self):
15 def testparseknown(self):
16 tests = [
16 tests = [
17 (b'* 0\nnodes 0\n', [b'', {}]),
17 (b'* 0\nnodes 0\n', [b'', {}]),
18 (
18 (
19 b'* 0\nnodes 40\n1111111111111111111111111111111111111111\n',
19 b'* 0\nnodes 40\n1111111111111111111111111111111111111111\n',
20 [b'1111111111111111111111111111111111111111', {}],
20 [b'1111111111111111111111111111111111111111', {}],
21 ),
21 ),
22 ]
22 ]
23 for input, expected in tests:
23 for input, expected in tests:
24 self.assertparse(b'known', input, expected)
24 self.assertparse(b'known', input, expected)
25
25
26 def assertparse(self, cmd, input, expected):
26 def assertparse(self, cmd, input, expected):
27 server = mockserver(input)
27 server = mockserver(input)
28 proto = wireprotoserver.sshv1protocolhandler(
28 ui = server._ui
29 server._ui, server._fin, server._fout
29 proto = wireprotoserver.sshv1protocolhandler(ui, ui.fin, ui.fout)
30 )
31 _func, spec = wireprotov1server.commands[cmd]
30 _func, spec = wireprotov1server.commands[cmd]
32 self.assertEqual(proto.getargs(spec), expected)
31 self.assertEqual(proto.getargs(spec), expected)
33
32
34
33
35 def mockserver(inbytes):
34 def mockserver(inbytes):
36 ui = mockui(inbytes)
35 ui = mockui(inbytes)
37 repo = mockrepo(ui)
36 repo = mockrepo(ui)
37 # note: this test unfortunately doesn't really test anything about
38 # `sshserver` class anymore: the entirety of logic of that class lives
39 # in `serveuntil`, and that function is not even called by this test.
38 return wireprotoserver.sshserver(ui, repo)
40 return wireprotoserver.sshserver(ui, repo)
39
41
40
42
41 class mockrepo:
43 class mockrepo:
42 def __init__(self, ui):
44 def __init__(self, ui):
43 self.ui = ui
45 self.ui = ui
44
46
45
47
46 class mockui:
48 class mockui:
47 def __init__(self, inbytes):
49 def __init__(self, inbytes):
48 self.fin = io.BytesIO(inbytes)
50 self.fin = io.BytesIO(inbytes)
49 self.fout = io.BytesIO()
51 self.fout = io.BytesIO()
50 self.ferr = io.BytesIO()
52 self.ferr = io.BytesIO()
51
53
52 def protectfinout(self):
54 def protectfinout(self):
53 return self.fin, self.fout
55 return self.fin, self.fout
54
56
55 def restorefinout(self, fin, fout):
57 def restorefinout(self, fin, fout):
56 pass
58 pass
57
59
58
60
59 if __name__ == '__main__':
61 if __name__ == '__main__':
60 # Don't call into msvcrt to set BytesIO to binary mode
62 # Don't call into msvcrt to set BytesIO to binary mode
61 procutil.setbinary = lambda fp: True
63 procutil.setbinary = lambda fp: True
62 silenttestrunner.main(__name__)
64 silenttestrunner.main(__name__)
@@ -1,1031 +1,1051 b''
1 #testcases dirstate-v1 dirstate-v2
1 #testcases dirstate-v1 dirstate-v2
2
2
3 #if dirstate-v2
3 #if dirstate-v2
4 $ cat >> $HGRCPATH << EOF
4 $ cat >> $HGRCPATH << EOF
5 > [format]
5 > [format]
6 > use-dirstate-v2=1
6 > use-dirstate-v2=1
7 > [storage]
7 > [storage]
8 > dirstate-v2.slow-path=allow
8 > dirstate-v2.slow-path=allow
9 > EOF
9 > EOF
10 #endif
10 #endif
11
11
12 $ hg init repo1
12 $ hg init repo1
13 $ cd repo1
13 $ cd repo1
14 $ mkdir a b a/1 b/1 b/2
14 $ mkdir a b a/1 b/1 b/2
15 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
15 $ touch in_root a/in_a b/in_b a/1/in_a_1 b/1/in_b_1 b/2/in_b_2
16
16
17 hg status in repo root:
17 hg status in repo root:
18
18
19 $ hg status
19 $ hg status
20 ? a/1/in_a_1
20 ? a/1/in_a_1
21 ? a/in_a
21 ? a/in_a
22 ? b/1/in_b_1
22 ? b/1/in_b_1
23 ? b/2/in_b_2
23 ? b/2/in_b_2
24 ? b/in_b
24 ? b/in_b
25 ? in_root
25 ? in_root
26
26
27 hg status . in repo root:
27 hg status . in repo root:
28
28
29 $ hg status .
29 $ hg status .
30 ? a/1/in_a_1
30 ? a/1/in_a_1
31 ? a/in_a
31 ? a/in_a
32 ? b/1/in_b_1
32 ? b/1/in_b_1
33 ? b/2/in_b_2
33 ? b/2/in_b_2
34 ? b/in_b
34 ? b/in_b
35 ? in_root
35 ? in_root
36
36
37 $ hg status --cwd a
37 $ hg status --cwd a
38 ? a/1/in_a_1
38 ? a/1/in_a_1
39 ? a/in_a
39 ? a/in_a
40 ? b/1/in_b_1
40 ? b/1/in_b_1
41 ? b/2/in_b_2
41 ? b/2/in_b_2
42 ? b/in_b
42 ? b/in_b
43 ? in_root
43 ? in_root
44 $ hg status --cwd a .
44 $ hg status --cwd a .
45 ? 1/in_a_1
45 ? 1/in_a_1
46 ? in_a
46 ? in_a
47 $ hg status --cwd a ..
47 $ hg status --cwd a ..
48 ? 1/in_a_1
48 ? 1/in_a_1
49 ? in_a
49 ? in_a
50 ? ../b/1/in_b_1
50 ? ../b/1/in_b_1
51 ? ../b/2/in_b_2
51 ? ../b/2/in_b_2
52 ? ../b/in_b
52 ? ../b/in_b
53 ? ../in_root
53 ? ../in_root
54
54
55 $ hg status --cwd b
55 $ hg status --cwd b
56 ? a/1/in_a_1
56 ? a/1/in_a_1
57 ? a/in_a
57 ? a/in_a
58 ? b/1/in_b_1
58 ? b/1/in_b_1
59 ? b/2/in_b_2
59 ? b/2/in_b_2
60 ? b/in_b
60 ? b/in_b
61 ? in_root
61 ? in_root
62 $ hg status --cwd b .
62 $ hg status --cwd b .
63 ? 1/in_b_1
63 ? 1/in_b_1
64 ? 2/in_b_2
64 ? 2/in_b_2
65 ? in_b
65 ? in_b
66 $ hg status --cwd b ..
66 $ hg status --cwd b ..
67 ? ../a/1/in_a_1
67 ? ../a/1/in_a_1
68 ? ../a/in_a
68 ? ../a/in_a
69 ? 1/in_b_1
69 ? 1/in_b_1
70 ? 2/in_b_2
70 ? 2/in_b_2
71 ? in_b
71 ? in_b
72 ? ../in_root
72 ? ../in_root
73
73
74 $ hg status --cwd a/1
74 $ hg status --cwd a/1
75 ? a/1/in_a_1
75 ? a/1/in_a_1
76 ? a/in_a
76 ? a/in_a
77 ? b/1/in_b_1
77 ? b/1/in_b_1
78 ? b/2/in_b_2
78 ? b/2/in_b_2
79 ? b/in_b
79 ? b/in_b
80 ? in_root
80 ? in_root
81 $ hg status --cwd a/1 .
81 $ hg status --cwd a/1 .
82 ? in_a_1
82 ? in_a_1
83 $ hg status --cwd a/1 ..
83 $ hg status --cwd a/1 ..
84 ? in_a_1
84 ? in_a_1
85 ? ../in_a
85 ? ../in_a
86
86
87 $ hg status --cwd b/1
87 $ hg status --cwd b/1
88 ? a/1/in_a_1
88 ? a/1/in_a_1
89 ? a/in_a
89 ? a/in_a
90 ? b/1/in_b_1
90 ? b/1/in_b_1
91 ? b/2/in_b_2
91 ? b/2/in_b_2
92 ? b/in_b
92 ? b/in_b
93 ? in_root
93 ? in_root
94 $ hg status --cwd b/1 .
94 $ hg status --cwd b/1 .
95 ? in_b_1
95 ? in_b_1
96 $ hg status --cwd b/1 ..
96 $ hg status --cwd b/1 ..
97 ? in_b_1
97 ? in_b_1
98 ? ../2/in_b_2
98 ? ../2/in_b_2
99 ? ../in_b
99 ? ../in_b
100
100
101 $ hg status --cwd b/2
101 $ hg status --cwd b/2
102 ? a/1/in_a_1
102 ? a/1/in_a_1
103 ? a/in_a
103 ? a/in_a
104 ? b/1/in_b_1
104 ? b/1/in_b_1
105 ? b/2/in_b_2
105 ? b/2/in_b_2
106 ? b/in_b
106 ? b/in_b
107 ? in_root
107 ? in_root
108 $ hg status --cwd b/2 .
108 $ hg status --cwd b/2 .
109 ? in_b_2
109 ? in_b_2
110 $ hg status --cwd b/2 ..
110 $ hg status --cwd b/2 ..
111 ? ../1/in_b_1
111 ? ../1/in_b_1
112 ? in_b_2
112 ? in_b_2
113 ? ../in_b
113 ? ../in_b
114
114
115 combining patterns with root and patterns without a root works
115 combining patterns with root and patterns without a root works
116
116
117 $ hg st a/in_a re:.*b$
117 $ hg st a/in_a re:.*b$
118 ? a/in_a
118 ? a/in_a
119 ? b/in_b
119 ? b/in_b
120
120
121 tweaking defaults works
121 tweaking defaults works
122 $ hg status --cwd a --config ui.tweakdefaults=yes
122 $ hg status --cwd a --config ui.tweakdefaults=yes
123 ? 1/in_a_1
123 ? 1/in_a_1
124 ? in_a
124 ? in_a
125 ? ../b/1/in_b_1
125 ? ../b/1/in_b_1
126 ? ../b/2/in_b_2
126 ? ../b/2/in_b_2
127 ? ../b/in_b
127 ? ../b/in_b
128 ? ../in_root
128 ? ../in_root
129 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
129 $ HGPLAIN=1 hg status --cwd a --config ui.tweakdefaults=yes
130 ? a/1/in_a_1 (glob)
130 ? a/1/in_a_1 (glob)
131 ? a/in_a (glob)
131 ? a/in_a (glob)
132 ? b/1/in_b_1 (glob)
132 ? b/1/in_b_1 (glob)
133 ? b/2/in_b_2 (glob)
133 ? b/2/in_b_2 (glob)
134 ? b/in_b (glob)
134 ? b/in_b (glob)
135 ? in_root
135 ? in_root
136 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
136 $ HGPLAINEXCEPT=tweakdefaults hg status --cwd a --config ui.tweakdefaults=yes
137 ? 1/in_a_1
137 ? 1/in_a_1
138 ? in_a
138 ? in_a
139 ? ../b/1/in_b_1
139 ? ../b/1/in_b_1
140 ? ../b/2/in_b_2
140 ? ../b/2/in_b_2
141 ? ../b/in_b
141 ? ../b/in_b
142 ? ../in_root (glob)
142 ? ../in_root (glob)
143
143
144 relative paths can be requested
144 relative paths can be requested
145
145
146 $ hg status --cwd a --config ui.relative-paths=yes
146 $ hg status --cwd a --config ui.relative-paths=yes
147 ? 1/in_a_1
147 ? 1/in_a_1
148 ? in_a
148 ? in_a
149 ? ../b/1/in_b_1
149 ? ../b/1/in_b_1
150 ? ../b/2/in_b_2
150 ? ../b/2/in_b_2
151 ? ../b/in_b
151 ? ../b/in_b
152 ? ../in_root
152 ? ../in_root
153
153
154 $ hg status --cwd a . --config ui.relative-paths=legacy
154 $ hg status --cwd a . --config ui.relative-paths=legacy
155 ? 1/in_a_1
155 ? 1/in_a_1
156 ? in_a
156 ? in_a
157 $ hg status --cwd a . --config ui.relative-paths=no
157 $ hg status --cwd a . --config ui.relative-paths=no
158 ? a/1/in_a_1
158 ? a/1/in_a_1
159 ? a/in_a
159 ? a/in_a
160
160
161 commands.status.relative overrides ui.relative-paths
161 commands.status.relative overrides ui.relative-paths
162
162
163 $ cat >> $HGRCPATH <<EOF
163 $ cat >> $HGRCPATH <<EOF
164 > [ui]
164 > [ui]
165 > relative-paths = False
165 > relative-paths = False
166 > [commands]
166 > [commands]
167 > status.relative = True
167 > status.relative = True
168 > EOF
168 > EOF
169 $ hg status --cwd a
169 $ hg status --cwd a
170 ? 1/in_a_1
170 ? 1/in_a_1
171 ? in_a
171 ? in_a
172 ? ../b/1/in_b_1
172 ? ../b/1/in_b_1
173 ? ../b/2/in_b_2
173 ? ../b/2/in_b_2
174 ? ../b/in_b
174 ? ../b/in_b
175 ? ../in_root
175 ? ../in_root
176 $ HGPLAIN=1 hg status --cwd a
176 $ HGPLAIN=1 hg status --cwd a
177 ? a/1/in_a_1 (glob)
177 ? a/1/in_a_1 (glob)
178 ? a/in_a (glob)
178 ? a/in_a (glob)
179 ? b/1/in_b_1 (glob)
179 ? b/1/in_b_1 (glob)
180 ? b/2/in_b_2 (glob)
180 ? b/2/in_b_2 (glob)
181 ? b/in_b (glob)
181 ? b/in_b (glob)
182 ? in_root
182 ? in_root
183
183
184 if relative paths are explicitly off, tweakdefaults doesn't change it
184 if relative paths are explicitly off, tweakdefaults doesn't change it
185 $ cat >> $HGRCPATH <<EOF
185 $ cat >> $HGRCPATH <<EOF
186 > [commands]
186 > [commands]
187 > status.relative = False
187 > status.relative = False
188 > EOF
188 > EOF
189 $ hg status --cwd a --config ui.tweakdefaults=yes
189 $ hg status --cwd a --config ui.tweakdefaults=yes
190 ? a/1/in_a_1
190 ? a/1/in_a_1
191 ? a/in_a
191 ? a/in_a
192 ? b/1/in_b_1
192 ? b/1/in_b_1
193 ? b/2/in_b_2
193 ? b/2/in_b_2
194 ? b/in_b
194 ? b/in_b
195 ? in_root
195 ? in_root
196
196
197 $ cd ..
197 $ cd ..
198
198
199 $ hg init repo2
199 $ hg init repo2
200 $ cd repo2
200 $ cd repo2
201 $ touch modified removed deleted ignored
201 $ touch modified removed deleted ignored
202 $ echo "^ignored$" > .hgignore
202 $ echo "^ignored$" > .hgignore
203 $ hg ci -A -m 'initial checkin'
203 $ hg ci -A -m 'initial checkin'
204 adding .hgignore
204 adding .hgignore
205 adding deleted
205 adding deleted
206 adding modified
206 adding modified
207 adding removed
207 adding removed
208 $ touch modified added unknown ignored
208 $ touch modified added unknown ignored
209 $ hg add added
209 $ hg add added
210 $ hg remove removed
210 $ hg remove removed
211 $ rm deleted
211 $ rm deleted
212
212
213 hg status:
213 hg status:
214
214
215 $ hg status
215 $ hg status
216 A added
216 A added
217 R removed
217 R removed
218 ! deleted
218 ! deleted
219 ? unknown
219 ? unknown
220
220
221 hg status -n:
221 hg status -n:
222 $ env RHG_ON_UNSUPPORTED=abort hg status -n
222 $ env RHG_ON_UNSUPPORTED=abort hg status -n
223 added
223 added
224 removed
224 removed
225 deleted
225 deleted
226 unknown
226 unknown
227
227
228 hg status modified added removed deleted unknown never-existed ignored:
228 hg status modified added removed deleted unknown never-existed ignored:
229
229
230 $ hg status modified added removed deleted unknown never-existed ignored
230 $ hg status modified added removed deleted unknown never-existed ignored
231 never-existed: * (glob)
231 never-existed: * (glob)
232 A added
232 A added
233 R removed
233 R removed
234 ! deleted
234 ! deleted
235 ? unknown
235 ? unknown
236
236
237 $ hg copy modified copied
237 $ hg copy modified copied
238
238
239 hg status -C:
239 hg status -C:
240
240
241 $ hg status -C
241 $ hg status -C
242 A added
242 A added
243 A copied
243 A copied
244 modified
244 modified
245 R removed
245 R removed
246 ! deleted
246 ! deleted
247 ? unknown
247 ? unknown
248
248
249 hg status -0:
249 hg status -0:
250
250
251 $ hg status -0 --config rhg.on-unsupported=abort
251 $ hg status -0 --config rhg.on-unsupported=abort
252 A added\x00A copied\x00R removed\x00! deleted\x00? unknown\x00 (no-eol) (esc)
252 A added\x00A copied\x00R removed\x00! deleted\x00? unknown\x00 (no-eol) (esc)
253
253
254 hg status -A:
254 hg status -A:
255
255
256 $ hg status -A
256 $ hg status -A
257 A added
257 A added
258 A copied
258 A copied
259 modified
259 modified
260 R removed
260 R removed
261 ! deleted
261 ! deleted
262 ? unknown
262 ? unknown
263 I ignored
263 I ignored
264 C .hgignore
264 C .hgignore
265 C modified
265 C modified
266
266
267 $ hg status -A -T '{status} {path} {node|shortest}\n'
267 $ hg status -A -T '{status} {path} {node|shortest}\n'
268 A added ffff
268 A added ffff
269 A copied ffff
269 A copied ffff
270 R removed ffff
270 R removed ffff
271 ! deleted ffff
271 ! deleted ffff
272 ? unknown ffff
272 ? unknown ffff
273 I ignored ffff
273 I ignored ffff
274 C .hgignore ffff
274 C .hgignore ffff
275 C modified ffff
275 C modified ffff
276
276
277 $ hg status -A -Tjson
277 $ hg status -A -Tjson
278 [
278 [
279 {
279 {
280 "itemtype": "file",
280 "itemtype": "file",
281 "path": "added",
281 "path": "added",
282 "status": "A"
282 "status": "A"
283 },
283 },
284 {
284 {
285 "itemtype": "file",
285 "itemtype": "file",
286 "path": "copied",
286 "path": "copied",
287 "source": "modified",
287 "source": "modified",
288 "status": "A"
288 "status": "A"
289 },
289 },
290 {
290 {
291 "itemtype": "file",
291 "itemtype": "file",
292 "path": "removed",
292 "path": "removed",
293 "status": "R"
293 "status": "R"
294 },
294 },
295 {
295 {
296 "itemtype": "file",
296 "itemtype": "file",
297 "path": "deleted",
297 "path": "deleted",
298 "status": "!"
298 "status": "!"
299 },
299 },
300 {
300 {
301 "itemtype": "file",
301 "itemtype": "file",
302 "path": "unknown",
302 "path": "unknown",
303 "status": "?"
303 "status": "?"
304 },
304 },
305 {
305 {
306 "itemtype": "file",
306 "itemtype": "file",
307 "path": "ignored",
307 "path": "ignored",
308 "status": "I"
308 "status": "I"
309 },
309 },
310 {
310 {
311 "itemtype": "file",
311 "itemtype": "file",
312 "path": ".hgignore",
312 "path": ".hgignore",
313 "status": "C"
313 "status": "C"
314 },
314 },
315 {
315 {
316 "itemtype": "file",
316 "itemtype": "file",
317 "path": "modified",
317 "path": "modified",
318 "status": "C"
318 "status": "C"
319 }
319 }
320 ]
320 ]
321
321
322 $ hg status -A -Tpickle > pickle
322 $ hg status -A -Tpickle > pickle
323 >>> import pickle
323 >>> import pickle
324 >>> from mercurial import util
324 >>> from mercurial import util
325 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
325 >>> data = sorted((x[b'status'].decode(), x[b'path'].decode()) for x in pickle.load(open("pickle", r"rb")))
326 >>> for s, p in data: print("%s %s" % (s, p))
326 >>> for s, p in data: print("%s %s" % (s, p))
327 ! deleted
327 ! deleted
328 ? pickle
328 ? pickle
329 ? unknown
329 ? unknown
330 A added
330 A added
331 A copied
331 A copied
332 C .hgignore
332 C .hgignore
333 C modified
333 C modified
334 I ignored
334 I ignored
335 R removed
335 R removed
336 $ rm pickle
336 $ rm pickle
337
337
338 $ echo "^ignoreddir$" > .hgignore
338 $ echo "^ignoreddir$" > .hgignore
339 $ mkdir ignoreddir
339 $ mkdir ignoreddir
340 $ touch ignoreddir/file
340 $ touch ignoreddir/file
341
341
342 Test templater support:
342 Test templater support:
343
343
344 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
344 $ hg status -AT "[{status}]\t{if(source, '{source} -> ')}{path}\n"
345 [M] .hgignore
345 [M] .hgignore
346 [A] added
346 [A] added
347 [A] modified -> copied
347 [A] modified -> copied
348 [R] removed
348 [R] removed
349 [!] deleted
349 [!] deleted
350 [?] ignored
350 [?] ignored
351 [?] unknown
351 [?] unknown
352 [I] ignoreddir/file
352 [I] ignoreddir/file
353 [C] modified
353 [C] modified
354 $ hg status -AT default
354 $ hg status -AT default
355 M .hgignore
355 M .hgignore
356 A added
356 A added
357 A copied
357 A copied
358 modified
358 modified
359 R removed
359 R removed
360 ! deleted
360 ! deleted
361 ? ignored
361 ? ignored
362 ? unknown
362 ? unknown
363 I ignoreddir/file
363 I ignoreddir/file
364 C modified
364 C modified
365 $ hg status -T compact
365 $ hg status -T compact
366 abort: "status" not in template map
366 abort: "status" not in template map
367 [255]
367 [255]
368
368
369 hg status ignoreddir/file:
369 hg status ignoreddir/file:
370
370
371 $ hg status ignoreddir/file
371 $ hg status ignoreddir/file
372
372
373 hg status -i ignoreddir/file:
373 hg status -i ignoreddir/file:
374
374
375 $ hg status -i ignoreddir/file
375 $ hg status -i ignoreddir/file
376 I ignoreddir/file
376 I ignoreddir/file
377 $ cd ..
377 $ cd ..
378
378
379 Check 'status -q' and some combinations
379 Check 'status -q' and some combinations
380
380
381 $ hg init repo3
381 $ hg init repo3
382 $ cd repo3
382 $ cd repo3
383 $ touch modified removed deleted ignored
383 $ touch modified removed deleted ignored
384 $ echo "^ignored$" > .hgignore
384 $ echo "^ignored$" > .hgignore
385 $ hg commit -A -m 'initial checkin'
385 $ hg commit -A -m 'initial checkin'
386 adding .hgignore
386 adding .hgignore
387 adding deleted
387 adding deleted
388 adding modified
388 adding modified
389 adding removed
389 adding removed
390 $ touch added unknown ignored
390 $ touch added unknown ignored
391 $ hg add added
391 $ hg add added
392 $ echo "test" >> modified
392 $ echo "test" >> modified
393 $ hg remove removed
393 $ hg remove removed
394 $ rm deleted
394 $ rm deleted
395 $ hg copy modified copied
395 $ hg copy modified copied
396
396
397 Specify working directory revision explicitly, that should be the same as
397 Specify working directory revision explicitly, that should be the same as
398 "hg status"
398 "hg status"
399
399
400 $ hg status --change "wdir()"
400 $ hg status --change "wdir()"
401 M modified
401 M modified
402 A added
402 A added
403 A copied
403 A copied
404 R removed
404 R removed
405 ! deleted
405 ! deleted
406 ? unknown
406 ? unknown
407
407
408 Run status with 2 different flags.
408 Run status with 2 different flags.
409 Check if result is the same or different.
409 Check if result is the same or different.
410 If result is not as expected, raise error
410 If result is not as expected, raise error
411
411
412 $ assert() {
412 $ assert() {
413 > hg status $1 > ../a
413 > hg status $1 > ../a
414 > hg status $2 > ../b
414 > hg status $2 > ../b
415 > if diff ../a ../b > /dev/null; then
415 > if diff ../a ../b > /dev/null; then
416 > out=0
416 > out=0
417 > else
417 > else
418 > out=1
418 > out=1
419 > fi
419 > fi
420 > if [ $3 -eq 0 ]; then
420 > if [ $3 -eq 0 ]; then
421 > df="same"
421 > df="same"
422 > else
422 > else
423 > df="different"
423 > df="different"
424 > fi
424 > fi
425 > if [ $out -ne $3 ]; then
425 > if [ $out -ne $3 ]; then
426 > echo "Error on $1 and $2, should be $df."
426 > echo "Error on $1 and $2, should be $df."
427 > fi
427 > fi
428 > }
428 > }
429
429
430 Assert flag1 flag2 [0-same | 1-different]
430 Assert flag1 flag2 [0-same | 1-different]
431
431
432 $ assert "-q" "-mard" 0
432 $ assert "-q" "-mard" 0
433 $ assert "-A" "-marduicC" 0
433 $ assert "-A" "-marduicC" 0
434 $ assert "-qA" "-mardcC" 0
434 $ assert "-qA" "-mardcC" 0
435 $ assert "-qAui" "-A" 0
435 $ assert "-qAui" "-A" 0
436 $ assert "-qAu" "-marducC" 0
436 $ assert "-qAu" "-marducC" 0
437 $ assert "-qAi" "-mardicC" 0
437 $ assert "-qAi" "-mardicC" 0
438 $ assert "-qu" "-u" 0
438 $ assert "-qu" "-u" 0
439 $ assert "-q" "-u" 1
439 $ assert "-q" "-u" 1
440 $ assert "-m" "-a" 1
440 $ assert "-m" "-a" 1
441 $ assert "-r" "-d" 1
441 $ assert "-r" "-d" 1
442 $ cd ..
442 $ cd ..
443
443
444 $ hg init repo4
444 $ hg init repo4
445 $ cd repo4
445 $ cd repo4
446 $ touch modified removed deleted
446 $ touch modified removed deleted
447 $ hg ci -q -A -m 'initial checkin'
447 $ hg ci -q -A -m 'initial checkin'
448 $ touch added unknown
448 $ touch added unknown
449 $ hg add added
449 $ hg add added
450 $ hg remove removed
450 $ hg remove removed
451 $ rm deleted
451 $ rm deleted
452 $ echo x > modified
452 $ echo x > modified
453 $ hg copy modified copied
453 $ hg copy modified copied
454 $ hg ci -m 'test checkin' -d "1000001 0"
454 $ hg ci -m 'test checkin' -d "1000001 0"
455 $ rm *
455 $ rm *
456 $ touch unrelated
456 $ touch unrelated
457 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
457 $ hg ci -q -A -m 'unrelated checkin' -d "1000002 0"
458
458
459 hg status --change 1:
459 hg status --change 1:
460
460
461 $ hg status --change 1
461 $ hg status --change 1
462 M modified
462 M modified
463 A added
463 A added
464 A copied
464 A copied
465 R removed
465 R removed
466
466
467 hg status --change 1 unrelated:
467 hg status --change 1 unrelated:
468
468
469 $ hg status --change 1 unrelated
469 $ hg status --change 1 unrelated
470
470
471 hg status -C --change 1 added modified copied removed deleted:
471 hg status -C --change 1 added modified copied removed deleted:
472
472
473 $ hg status -C --change 1 added modified copied removed deleted
473 $ hg status -C --change 1 added modified copied removed deleted
474 M modified
474 M modified
475 A added
475 A added
476 A copied
476 A copied
477 modified
477 modified
478 R removed
478 R removed
479
479
480 hg status -A --change 1 and revset:
480 hg status -A --change 1 and revset:
481
481
482 $ hg status -A --change '1|1'
482 $ hg status -A --change '1|1'
483 M modified
483 M modified
484 A added
484 A added
485 A copied
485 A copied
486 modified
486 modified
487 R removed
487 R removed
488 C deleted
488 C deleted
489
489
490 $ cd ..
490 $ cd ..
491
491
492 hg status with --rev and reverted changes:
492 hg status with --rev and reverted changes:
493
493
494 $ hg init reverted-changes-repo
494 $ hg init reverted-changes-repo
495 $ cd reverted-changes-repo
495 $ cd reverted-changes-repo
496 $ echo a > file
496 $ echo a > file
497 $ hg add file
497 $ hg add file
498 $ hg ci -m a
498 $ hg ci -m a
499 $ echo b > file
499 $ echo b > file
500 $ hg ci -m b
500 $ hg ci -m b
501
501
502 reverted file should appear clean
502 reverted file should appear clean
503
503
504 $ hg revert -r 0 .
504 $ hg revert -r 0 .
505 reverting file
505 reverting file
506 $ hg status -A --rev 0
506 $ hg status -A --rev 0
507 C file
507 C file
508
508
509 #if execbit
509 #if execbit
510 reverted file with changed flag should appear modified
510 reverted file with changed flag should appear modified
511
511
512 $ chmod +x file
512 $ chmod +x file
513 $ hg status -A --rev 0
513 $ hg status -A --rev 0
514 M file
514 M file
515
515
516 $ hg revert -r 0 .
516 $ hg revert -r 0 .
517 reverting file
517 reverting file
518
518
519 reverted and committed file with changed flag should appear modified
519 reverted and committed file with changed flag should appear modified
520
520
521 $ hg co -C .
521 $ hg co -C .
522 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
522 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
523 $ chmod +x file
523 $ chmod +x file
524 $ hg ci -m 'change flag'
524 $ hg ci -m 'change flag'
525 $ hg status -A --rev 1 --rev 2
525 $ hg status -A --rev 1 --rev 2
526 M file
526 M file
527 $ hg diff -r 1 -r 2
527 $ hg diff -r 1 -r 2
528
528
529 #endif
529 #endif
530
530
531 $ cd ..
531 $ cd ..
532
532
533 hg status of binary file starting with '\1\n', a separator for metadata:
533 hg status of binary file starting with '\1\n', a separator for metadata:
534
534
535 $ hg init repo5
535 $ hg init repo5
536 $ cd repo5
536 $ cd repo5
537 >>> open("010a", r"wb").write(b"\1\nfoo") and None
537 >>> open("010a", r"wb").write(b"\1\nfoo") and None
538 $ hg ci -q -A -m 'initial checkin'
538 $ hg ci -q -A -m 'initial checkin'
539 $ hg status -A
539 $ hg status -A
540 C 010a
540 C 010a
541
541
542 >>> open("010a", r"wb").write(b"\1\nbar") and None
542 >>> open("010a", r"wb").write(b"\1\nbar") and None
543 $ hg status -A
543 $ hg status -A
544 M 010a
544 M 010a
545 $ hg ci -q -m 'modify 010a'
545 $ hg ci -q -m 'modify 010a'
546 $ hg status -A --rev 0:1
546 $ hg status -A --rev 0:1
547 M 010a
547 M 010a
548
548
549 $ touch empty
549 $ touch empty
550 $ hg ci -q -A -m 'add another file'
550 $ hg ci -q -A -m 'add another file'
551 $ hg status -A --rev 1:2 010a
551 $ hg status -A --rev 1:2 010a
552 C 010a
552 C 010a
553
553
554 $ cd ..
554 $ cd ..
555
555
556 test "hg status" with "directory pattern" which matches against files
556 test "hg status" with "directory pattern" which matches against files
557 only known on target revision.
557 only known on target revision.
558
558
559 $ hg init repo6
559 $ hg init repo6
560 $ cd repo6
560 $ cd repo6
561
561
562 $ echo a > a.txt
562 $ echo a > a.txt
563 $ hg add a.txt
563 $ hg add a.txt
564 $ hg commit -m '#0'
564 $ hg commit -m '#0'
565 $ mkdir -p 1/2/3/4/5
565 $ mkdir -p 1/2/3/4/5
566 $ echo b > 1/2/3/4/5/b.txt
566 $ echo b > 1/2/3/4/5/b.txt
567 $ hg add 1/2/3/4/5/b.txt
567 $ hg add 1/2/3/4/5/b.txt
568 $ hg commit -m '#1'
568 $ hg commit -m '#1'
569
569
570 $ hg update -C 0 > /dev/null
570 $ hg update -C 0 > /dev/null
571 $ hg status -A
571 $ hg status -A
572 C a.txt
572 C a.txt
573
573
574 the directory matching against specified pattern should be removed,
574 the directory matching against specified pattern should be removed,
575 because directory existence prevents 'dirstate.walk()' from showing
575 because directory existence prevents 'dirstate.walk()' from showing
576 warning message about such pattern.
576 warning message about such pattern.
577
577
578 $ test ! -d 1
578 $ test ! -d 1
579 $ hg status -A --rev 1 1/2/3/4/5/b.txt
579 $ hg status -A --rev 1 1/2/3/4/5/b.txt
580 R 1/2/3/4/5/b.txt
580 R 1/2/3/4/5/b.txt
581 $ hg status -A --rev 1 1/2/3/4/5
581 $ hg status -A --rev 1 1/2/3/4/5
582 R 1/2/3/4/5/b.txt
582 R 1/2/3/4/5/b.txt
583 $ hg status -A --rev 1 1/2/3
583 $ hg status -A --rev 1 1/2/3
584 R 1/2/3/4/5/b.txt
584 R 1/2/3/4/5/b.txt
585 $ hg status -A --rev 1 1
585 $ hg status -A --rev 1 1
586 R 1/2/3/4/5/b.txt
586 R 1/2/3/4/5/b.txt
587
587
588 $ hg status --config ui.formatdebug=True --rev 1 1
588 $ hg status --config ui.formatdebug=True --rev 1 1
589 status = [
589 status = [
590 {
590 {
591 'itemtype': 'file',
591 'itemtype': 'file',
592 'path': '1/2/3/4/5/b.txt',
592 'path': '1/2/3/4/5/b.txt',
593 'status': 'R'
593 'status': 'R'
594 },
594 },
595 ]
595 ]
596
596
597 #if windows
597 #if windows
598 $ hg --config ui.slash=false status -A --rev 1 1
598 $ hg --config ui.slash=false status -A --rev 1 1
599 R 1\2\3\4\5\b.txt
599 R 1\2\3\4\5\b.txt
600 #endif
600 #endif
601
601
602 $ cd ..
602 $ cd ..
603
603
604 Status after move overwriting a file (issue4458)
604 Status after move overwriting a file (issue4458)
605 =================================================
605 =================================================
606
606
607
607
608 $ hg init issue4458
608 $ hg init issue4458
609 $ cd issue4458
609 $ cd issue4458
610 $ echo a > a
610 $ echo a > a
611 $ echo b > b
611 $ echo b > b
612 $ hg commit -Am base
612 $ hg commit -Am base
613 adding a
613 adding a
614 adding b
614 adding b
615
615
616
616
617 with --force
617 with --force
618
618
619 $ hg mv b --force a
619 $ hg mv b --force a
620 $ hg st --copies
620 $ hg st --copies
621 M a
621 M a
622 b
622 b
623 R b
623 R b
624 $ hg revert --all
624 $ hg revert --all
625 reverting a
625 reverting a
626 undeleting b
626 undeleting b
627 $ rm *.orig
627 $ rm *.orig
628
628
629 without force
629 without force
630
630
631 $ hg rm a
631 $ hg rm a
632 $ hg st --copies
632 $ hg st --copies
633 R a
633 R a
634 $ hg mv b a
634 $ hg mv b a
635 $ hg st --copies
635 $ hg st --copies
636 M a
636 M a
637 b
637 b
638 R b
638 R b
639
639
640 using ui.statuscopies setting
640 using ui.statuscopies setting
641 $ hg st --config ui.statuscopies=true
641 $ hg st --config ui.statuscopies=true
642 M a
642 M a
643 b
643 b
644 R b
644 R b
645 $ hg st --config ui.statuscopies=true --no-copies
645 $ hg st --config ui.statuscopies=true --no-copies
646 M a
646 M a
647 R b
647 R b
648 $ hg st --config ui.statuscopies=false
648 $ hg st --config ui.statuscopies=false
649 M a
649 M a
650 R b
650 R b
651 $ hg st --config ui.statuscopies=false --copies
651 $ hg st --config ui.statuscopies=false --copies
652 M a
652 M a
653 b
653 b
654 R b
654 R b
655 $ hg st --config ui.tweakdefaults=yes
655 $ hg st --config ui.tweakdefaults=yes
656 M a
656 M a
657 b
657 b
658 R b
658 R b
659
659
660 using log status template (issue5155)
660 using log status template (issue5155)
661 $ hg log -Tstatus -r 'wdir()' -C
661 $ hg log -Tstatus -r 'wdir()' -C
662 changeset: 2147483647:ffffffffffff
662 changeset: 2147483647:ffffffffffff
663 parent: 0:8c55c58b4c0e
663 parent: 0:8c55c58b4c0e
664 user: test
664 user: test
665 date: * (glob)
665 date: * (glob)
666 files:
666 files:
667 M a
667 M a
668 b
668 b
669 R b
669 R b
670
670
671 $ hg log -GTstatus -r 'wdir()' -C
671 $ hg log -GTstatus -r 'wdir()' -C
672 o changeset: 2147483647:ffffffffffff
672 o changeset: 2147483647:ffffffffffff
673 | parent: 0:8c55c58b4c0e
673 | parent: 0:8c55c58b4c0e
674 ~ user: test
674 ~ user: test
675 date: * (glob)
675 date: * (glob)
676 files:
676 files:
677 M a
677 M a
678 b
678 b
679 R b
679 R b
680
680
681
681
682 Other "bug" highlight, the revision status does not report the copy information.
682 Other "bug" highlight, the revision status does not report the copy information.
683 This is buggy behavior.
683 This is buggy behavior.
684
684
685 $ hg commit -m 'blah'
685 $ hg commit -m 'blah'
686 $ hg st --copies --change .
686 $ hg st --copies --change .
687 M a
687 M a
688 R b
688 R b
689
689
690 using log status template, the copy information is displayed correctly.
690 using log status template, the copy information is displayed correctly.
691 $ hg log -Tstatus -r. -C
691 $ hg log -Tstatus -r. -C
692 changeset: 1:6685fde43d21
692 changeset: 1:6685fde43d21
693 tag: tip
693 tag: tip
694 user: test
694 user: test
695 date: * (glob)
695 date: * (glob)
696 summary: blah
696 summary: blah
697 files:
697 files:
698 M a
698 M a
699 b
699 b
700 R b
700 R b
701
701
702
702
703 $ cd ..
703 $ cd ..
704
704
705 Make sure .hg doesn't show up even as a symlink
705 Make sure .hg doesn't show up even as a symlink
706
706
707 $ hg init repo0
707 $ hg init repo0
708 $ mkdir symlink-repo0
708 $ mkdir symlink-repo0
709 $ cd symlink-repo0
709 $ cd symlink-repo0
710 $ ln -s ../repo0/.hg
710 $ ln -s ../repo0/.hg
711 $ hg status
711 $ hg status
712
712
713 If the size hasnt changed but mtime has, status needs to read the contents
713 If the size hasnt changed but mtime has, status needs to read the contents
714 of the file to check whether it has changed
714 of the file to check whether it has changed
715
715
716 $ echo 1 > a
716 $ echo 1 > a
717 $ echo 1 > b
717 $ echo 1 > b
718 $ touch -t 200102030000 a b
718 $ touch -t 200102030000 a b
719 $ hg commit -Aqm '#0'
719 $ hg commit -Aqm '#0'
720 $ echo 2 > a
720 $ echo 2 > a
721 $ touch -t 200102040000 a b
721 $ touch -t 200102040000 a b
722 $ hg status
722 $ hg status
723 M a
723 M a
724
724
725 Asking specifically for the status of a deleted/removed file
725 Asking specifically for the status of a deleted/removed file
726
726
727 $ rm a
727 $ rm a
728 $ rm b
728 $ rm b
729 $ hg status a
729 $ hg status a
730 ! a
730 ! a
731 $ hg rm a
731 $ hg rm a
732 $ hg rm b
732 $ hg rm b
733 $ hg status a
733 $ hg status a
734 R a
734 R a
735 $ hg commit -qm '#1'
735 $ hg commit -qm '#1'
736 $ hg status a
736 $ hg status a
737 a: $ENOENT$
737 a: $ENOENT$
738
738
739 Check using include flag with pattern when status does not need to traverse
739 Check using include flag with pattern when status does not need to traverse
740 the working directory (issue6483)
740 the working directory (issue6483)
741
741
742 $ cd ..
742 $ cd ..
743 $ hg init issue6483
743 $ hg init issue6483
744 $ cd issue6483
744 $ cd issue6483
745 $ touch a.py b.rs
745 $ touch a.py b.rs
746 $ hg add a.py b.rs
746 $ hg add a.py b.rs
747 $ hg st -aI "*.py"
747 $ hg st -aI "*.py"
748 A a.py
748 A a.py
749
749
750 Also check exclude pattern
750 Also check exclude pattern
751
751
752 $ hg st -aX "*.rs"
752 $ hg st -aX "*.rs"
753 A a.py
753 A a.py
754
754
755 issue6335
755 issue6335
756 When a directory containing a tracked file gets symlinked, as of 5.8
756 When a directory containing a tracked file gets symlinked, as of 5.8
757 `hg st` only gives the correct answer about clean (or deleted) files
757 `hg st` only gives the correct answer about clean (or deleted) files
758 if also listing unknowns.
758 if also listing unknowns.
759 The tree-based dirstate and status algorithm fix this:
759 The tree-based dirstate and status algorithm fix this:
760
760
761 #if symlink no-dirstate-v1 rust
761 #if symlink no-dirstate-v1 rust
762
762
763 $ cd ..
763 $ cd ..
764 $ hg init issue6335
764 $ hg init issue6335
765 $ cd issue6335
765 $ cd issue6335
766 $ mkdir foo
766 $ mkdir foo
767 $ touch foo/a
767 $ touch foo/a
768 $ hg ci -Ama
768 $ hg ci -Ama
769 adding foo/a
769 adding foo/a
770 $ mv foo bar
770 $ mv foo bar
771 $ ln -s bar foo
771 $ ln -s bar foo
772 $ hg status
772 $ hg status
773 ! foo/a
773 ! foo/a
774 ? bar/a
774 ? bar/a
775 ? foo
775 ? foo
776
776
777 $ hg status -c # incorrect output without the Rust implementation
777 $ hg status -c # incorrect output without the Rust implementation
778 $ hg status -cu
778 $ hg status -cu
779 ? bar/a
779 ? bar/a
780 ? foo
780 ? foo
781 $ hg status -d # incorrect output without the Rust implementation
781 $ hg status -d # incorrect output without the Rust implementation
782 ! foo/a
782 ! foo/a
783 $ hg status -du
783 $ hg status -du
784 ! foo/a
784 ! foo/a
785 ? bar/a
785 ? bar/a
786 ? foo
786 ? foo
787
787
788 #endif
788 #endif
789
789
790
790
791 Create a repo with files in each possible status
791 Create a repo with files in each possible status
792
792
793 $ cd ..
793 $ cd ..
794 $ hg init repo7
794 $ hg init repo7
795 $ cd repo7
795 $ cd repo7
796 $ mkdir subdir
796 $ mkdir subdir
797 $ touch clean modified deleted removed
797 $ touch clean modified deleted removed
798 $ touch subdir/clean subdir/modified subdir/deleted subdir/removed
798 $ touch subdir/clean subdir/modified subdir/deleted subdir/removed
799 $ echo ignored > .hgignore
799 $ echo ignored > .hgignore
800 $ hg ci -Aqm '#0'
800 $ hg ci -Aqm '#0'
801 $ echo 1 > modified
801 $ echo 1 > modified
802 $ echo 1 > subdir/modified
802 $ echo 1 > subdir/modified
803 $ rm deleted
803 $ rm deleted
804 $ rm subdir/deleted
804 $ rm subdir/deleted
805 $ hg rm removed
805 $ hg rm removed
806 $ hg rm subdir/removed
806 $ hg rm subdir/removed
807 $ touch unknown ignored
807 $ touch unknown ignored
808 $ touch subdir/unknown subdir/ignored
808 $ touch subdir/unknown subdir/ignored
809
809
810 Check the output
810 Check the output
811
811
812 $ hg status
812 $ hg status
813 M modified
813 M modified
814 M subdir/modified
814 M subdir/modified
815 R removed
815 R removed
816 R subdir/removed
816 R subdir/removed
817 ! deleted
817 ! deleted
818 ! subdir/deleted
818 ! subdir/deleted
819 ? subdir/unknown
819 ? subdir/unknown
820 ? unknown
820 ? unknown
821
821
822 $ hg status -mard
822 $ hg status -mard
823 M modified
823 M modified
824 M subdir/modified
824 M subdir/modified
825 R removed
825 R removed
826 R subdir/removed
826 R subdir/removed
827 ! deleted
827 ! deleted
828 ! subdir/deleted
828 ! subdir/deleted
829
829
830 $ hg status -A
830 $ hg status -A
831 M modified
831 M modified
832 M subdir/modified
832 M subdir/modified
833 R removed
833 R removed
834 R subdir/removed
834 R subdir/removed
835 ! deleted
835 ! deleted
836 ! subdir/deleted
836 ! subdir/deleted
837 ? subdir/unknown
837 ? subdir/unknown
838 ? unknown
838 ? unknown
839 I ignored
839 I ignored
840 I subdir/ignored
840 I subdir/ignored
841 C .hgignore
841 C .hgignore
842 C clean
842 C clean
843 C subdir/clean
843 C subdir/clean
844
844
845 Test various matchers interatction with dirstate code:
846
847 $ hg status path:subdir
848 M subdir/modified
849 R subdir/removed
850 ! subdir/deleted
851 ? subdir/unknown
852
853 $ hg status 'glob:subdir/*'
854 M subdir/modified
855 R subdir/removed
856 ! subdir/deleted
857 ? subdir/unknown
858
859 $ hg status rootfilesin:subdir
860 M subdir/modified
861 R subdir/removed
862 ! subdir/deleted
863 ? subdir/unknown
864
845 Note: `hg status some-name` creates a patternmatcher which is not supported
865 Note: `hg status some-name` creates a patternmatcher which is not supported
846 yet by the Rust implementation of status, but includematcher is supported.
866 yet by the Rust implementation of status, but includematcher is supported.
847 --include is used below for that reason
867 --include is used below for that reason
848
868
849 #if unix-permissions
869 #if unix-permissions
850
870
851 Not having permission to read a directory that contains tracked files makes
871 Not having permission to read a directory that contains tracked files makes
852 status emit a warning then behave as if the directory was empty or removed
872 status emit a warning then behave as if the directory was empty or removed
853 entirely:
873 entirely:
854
874
855 $ chmod 0 subdir
875 $ chmod 0 subdir
856 $ hg status --include subdir
876 $ hg status --include subdir
857 subdir: $EACCES$
877 subdir: $EACCES$
858 R subdir/removed
878 R subdir/removed
859 ! subdir/clean
879 ! subdir/clean
860 ! subdir/deleted
880 ! subdir/deleted
861 ! subdir/modified
881 ! subdir/modified
862 $ chmod 755 subdir
882 $ chmod 755 subdir
863
883
864 #endif
884 #endif
865
885
866 Remove a directory that contains tracked files
886 Remove a directory that contains tracked files
867
887
868 $ rm -r subdir
888 $ rm -r subdir
869 $ hg status --include subdir
889 $ hg status --include subdir
870 R subdir/removed
890 R subdir/removed
871 ! subdir/clean
891 ! subdir/clean
872 ! subdir/deleted
892 ! subdir/deleted
873 ! subdir/modified
893 ! subdir/modified
874
894
875 and replace it by a file
895 and replace it by a file
876
896
877 $ touch subdir
897 $ touch subdir
878 $ hg status --include subdir
898 $ hg status --include subdir
879 R subdir/removed
899 R subdir/removed
880 ! subdir/clean
900 ! subdir/clean
881 ! subdir/deleted
901 ! subdir/deleted
882 ! subdir/modified
902 ! subdir/modified
883 ? subdir
903 ? subdir
884
904
885 Replaced a deleted or removed file with a directory
905 Replaced a deleted or removed file with a directory
886
906
887 $ mkdir deleted removed
907 $ mkdir deleted removed
888 $ touch deleted/1 removed/1
908 $ touch deleted/1 removed/1
889 $ hg status --include deleted --include removed
909 $ hg status --include deleted --include removed
890 R removed
910 R removed
891 ! deleted
911 ! deleted
892 ? deleted/1
912 ? deleted/1
893 ? removed/1
913 ? removed/1
894 $ hg add removed/1
914 $ hg add removed/1
895 $ hg status --include deleted --include removed
915 $ hg status --include deleted --include removed
896 A removed/1
916 A removed/1
897 R removed
917 R removed
898 ! deleted
918 ! deleted
899 ? deleted/1
919 ? deleted/1
900
920
901 Deeply nested files in an ignored directory are still listed on request
921 Deeply nested files in an ignored directory are still listed on request
902
922
903 $ echo ignored-dir >> .hgignore
923 $ echo ignored-dir >> .hgignore
904 $ mkdir ignored-dir
924 $ mkdir ignored-dir
905 $ mkdir ignored-dir/subdir
925 $ mkdir ignored-dir/subdir
906 $ touch ignored-dir/subdir/1
926 $ touch ignored-dir/subdir/1
907 $ hg status --ignored
927 $ hg status --ignored
908 I ignored
928 I ignored
909 I ignored-dir/subdir/1
929 I ignored-dir/subdir/1
910
930
911 Check using include flag while listing ignored composes correctly (issue6514)
931 Check using include flag while listing ignored composes correctly (issue6514)
912
932
913 $ cd ..
933 $ cd ..
914 $ hg init issue6514
934 $ hg init issue6514
915 $ cd issue6514
935 $ cd issue6514
916 $ mkdir ignored-folder
936 $ mkdir ignored-folder
917 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
937 $ touch A.hs B.hs C.hs ignored-folder/other.txt ignored-folder/ctest.hs
918 $ cat >.hgignore <<EOF
938 $ cat >.hgignore <<EOF
919 > A.hs
939 > A.hs
920 > B.hs
940 > B.hs
921 > ignored-folder/
941 > ignored-folder/
922 > EOF
942 > EOF
923 $ hg st -i -I 're:.*\.hs$'
943 $ hg st -i -I 're:.*\.hs$'
924 I A.hs
944 I A.hs
925 I B.hs
945 I B.hs
926 I ignored-folder/ctest.hs
946 I ignored-folder/ctest.hs
927
947
928 #if rust dirstate-v2
948 #if rust dirstate-v2
929
949
930 Check read_dir caching
950 Check read_dir caching
931
951
932 $ cd ..
952 $ cd ..
933 $ hg init repo8
953 $ hg init repo8
934 $ cd repo8
954 $ cd repo8
935 $ mkdir subdir
955 $ mkdir subdir
936 $ touch subdir/a subdir/b
956 $ touch subdir/a subdir/b
937 $ hg ci -Aqm '#0'
957 $ hg ci -Aqm '#0'
938
958
939 The cached mtime is initially unset
959 The cached mtime is initially unset
940
960
941 $ hg debugdirstate --all --no-dates | grep '^ '
961 $ hg debugdirstate --all --no-dates | grep '^ '
942 0 -1 unset subdir
962 0 -1 unset subdir
943
963
944 It is still not set when there are unknown files
964 It is still not set when there are unknown files
945
965
946 $ touch subdir/unknown
966 $ touch subdir/unknown
947 $ hg status
967 $ hg status
948 ? subdir/unknown
968 ? subdir/unknown
949 $ hg debugdirstate --all --no-dates | grep '^ '
969 $ hg debugdirstate --all --no-dates | grep '^ '
950 0 -1 unset subdir
970 0 -1 unset subdir
951
971
952 Now the directory is eligible for caching, so its mtime is saved in the dirstate
972 Now the directory is eligible for caching, so its mtime is saved in the dirstate
953
973
954 $ rm subdir/unknown
974 $ rm subdir/unknown
955 $ sleep 0.1 # ensure the kernels internal clock for mtimes has ticked
975 $ sleep 0.1 # ensure the kernels internal clock for mtimes has ticked
956 $ hg status
976 $ hg status
957 $ hg debugdirstate --all --no-dates | grep '^ '
977 $ hg debugdirstate --all --no-dates | grep '^ '
958 0 -1 set subdir
978 0 -1 set subdir
959
979
960 This time the command should be ever so slightly faster since it does not need `read_dir("subdir")`
980 This time the command should be ever so slightly faster since it does not need `read_dir("subdir")`
961
981
962 $ hg status
982 $ hg status
963
983
964 Creating a new file changes the directorys mtime, invalidating the cache
984 Creating a new file changes the directorys mtime, invalidating the cache
965
985
966 $ touch subdir/unknown
986 $ touch subdir/unknown
967 $ hg status
987 $ hg status
968 ? subdir/unknown
988 ? subdir/unknown
969
989
970 $ rm subdir/unknown
990 $ rm subdir/unknown
971 $ hg status
991 $ hg status
972
992
973 Removing a node from the dirstate resets the cache for its parent directory
993 Removing a node from the dirstate resets the cache for its parent directory
974
994
975 $ hg forget subdir/a
995 $ hg forget subdir/a
976 $ hg debugdirstate --all --no-dates | grep '^ '
996 $ hg debugdirstate --all --no-dates | grep '^ '
977 0 -1 set subdir
997 0 -1 set subdir
978 $ hg ci -qm '#1'
998 $ hg ci -qm '#1'
979 $ hg debugdirstate --all --no-dates | grep '^ '
999 $ hg debugdirstate --all --no-dates | grep '^ '
980 0 -1 unset subdir
1000 0 -1 unset subdir
981 $ hg status
1001 $ hg status
982 ? subdir/a
1002 ? subdir/a
983
1003
984 Changing the hgignore rules makes us recompute the status (and rewrite the dirstate).
1004 Changing the hgignore rules makes us recompute the status (and rewrite the dirstate).
985
1005
986 $ rm subdir/a
1006 $ rm subdir/a
987 $ mkdir another-subdir
1007 $ mkdir another-subdir
988 $ touch another-subdir/something-else
1008 $ touch another-subdir/something-else
989
1009
990 $ cat > "$TESTTMP"/extra-hgignore <<EOF
1010 $ cat > "$TESTTMP"/extra-hgignore <<EOF
991 > something-else
1011 > something-else
992 > EOF
1012 > EOF
993
1013
994 $ hg status --config ui.ignore.global="$TESTTMP"/extra-hgignore
1014 $ hg status --config ui.ignore.global="$TESTTMP"/extra-hgignore
995 $ hg debugdirstate --all --no-dates | grep '^ '
1015 $ hg debugdirstate --all --no-dates | grep '^ '
996 0 -1 set subdir
1016 0 -1 set subdir
997
1017
998 $ hg status
1018 $ hg status
999 ? another-subdir/something-else
1019 ? another-subdir/something-else
1000
1020
1001 One invocation of status is enough to populate the cache even if it's invalidated
1021 One invocation of status is enough to populate the cache even if it's invalidated
1002 in the same run.
1022 in the same run.
1003
1023
1004 $ hg debugdirstate --all --no-dates | grep '^ '
1024 $ hg debugdirstate --all --no-dates | grep '^ '
1005 0 -1 set subdir
1025 0 -1 set subdir
1006
1026
1007 #endif
1027 #endif
1008
1028
1009
1029
1010 Test copy source formatting.
1030 Test copy source formatting.
1011 $ cd ..
1031 $ cd ..
1012 $ hg init copy-source-repo
1032 $ hg init copy-source-repo
1013 $ cd copy-source-repo
1033 $ cd copy-source-repo
1014 $ mkdir -p foo/bar
1034 $ mkdir -p foo/bar
1015 $ cd foo/bar
1035 $ cd foo/bar
1016 $ touch file
1036 $ touch file
1017 $ hg addremove
1037 $ hg addremove
1018 adding foo/bar/file
1038 adding foo/bar/file
1019 $ hg commit -m 'add file'
1039 $ hg commit -m 'add file'
1020 $ hg mv file copy
1040 $ hg mv file copy
1021
1041
1022 Copy source respects relative path setting.
1042 Copy source respects relative path setting.
1023 $ hg st --config ui.statuscopies=true --config commands.status.relative=true
1043 $ hg st --config ui.statuscopies=true --config commands.status.relative=true
1024 A copy
1044 A copy
1025 file
1045 file
1026 R file
1046 R file
1027
1047
1028 Copy source is not shown when --no-status is passed.
1048 Copy source is not shown when --no-status is passed.
1029 $ hg st --config ui.statuscopies=true --no-status
1049 $ hg st --config ui.statuscopies=true --no-status
1030 foo/bar/copy
1050 foo/bar/copy
1031 foo/bar/file
1051 foo/bar/file
General Comments 0
You need to be logged in to leave comments. Login now