##// END OF EJS Templates
interfaces: create a new folder for interfaces and move repository.py in it...
Pulkit Goyal -
r43078:268662aa default
parent child Browse files
Show More
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -1,748 +1,749
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2
2
3 from __future__ import absolute_import, print_function
3 from __future__ import absolute_import, print_function
4
4
5 import ast
5 import ast
6 import collections
6 import collections
7 import os
7 import os
8 import sys
8 import sys
9
9
10 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
10 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
11 # to work when run from a virtualenv. The modules were chosen empirically
11 # to work when run from a virtualenv. The modules were chosen empirically
12 # so that the return value matches the return value without virtualenv.
12 # so that the return value matches the return value without virtualenv.
13 if True: # disable lexical sorting checks
13 if True: # disable lexical sorting checks
14 try:
14 try:
15 import BaseHTTPServer as basehttpserver
15 import BaseHTTPServer as basehttpserver
16 except ImportError:
16 except ImportError:
17 basehttpserver = None
17 basehttpserver = None
18 import zlib
18 import zlib
19
19
20 import testparseutil
20 import testparseutil
21
21
22 # Whitelist of modules that symbols can be directly imported from.
22 # Whitelist of modules that symbols can be directly imported from.
23 allowsymbolimports = (
23 allowsymbolimports = (
24 '__future__',
24 '__future__',
25 'bzrlib',
25 'bzrlib',
26 'hgclient',
26 'hgclient',
27 'mercurial',
27 'mercurial',
28 'mercurial.hgweb.common',
28 'mercurial.hgweb.common',
29 'mercurial.hgweb.request',
29 'mercurial.hgweb.request',
30 'mercurial.i18n',
30 'mercurial.i18n',
31 'mercurial.interfaces',
31 'mercurial.node',
32 'mercurial.node',
32 # for revlog to re-export constant to extensions
33 # for revlog to re-export constant to extensions
33 'mercurial.revlogutils.constants',
34 'mercurial.revlogutils.constants',
34 'mercurial.revlogutils.flagutil',
35 'mercurial.revlogutils.flagutil',
35 # for cffi modules to re-export pure functions
36 # for cffi modules to re-export pure functions
36 'mercurial.pure.base85',
37 'mercurial.pure.base85',
37 'mercurial.pure.bdiff',
38 'mercurial.pure.bdiff',
38 'mercurial.pure.mpatch',
39 'mercurial.pure.mpatch',
39 'mercurial.pure.osutil',
40 'mercurial.pure.osutil',
40 'mercurial.pure.parsers',
41 'mercurial.pure.parsers',
41 # third-party imports should be directly imported
42 # third-party imports should be directly imported
42 'mercurial.thirdparty',
43 'mercurial.thirdparty',
43 'mercurial.thirdparty.attr',
44 'mercurial.thirdparty.attr',
44 'mercurial.thirdparty.zope',
45 'mercurial.thirdparty.zope',
45 'mercurial.thirdparty.zope.interface',
46 'mercurial.thirdparty.zope.interface',
46 )
47 )
47
48
48 # Whitelist of symbols that can be directly imported.
49 # Whitelist of symbols that can be directly imported.
49 directsymbols = (
50 directsymbols = (
50 'demandimport',
51 'demandimport',
51 )
52 )
52
53
53 # Modules that must be aliased because they are commonly confused with
54 # Modules that must be aliased because they are commonly confused with
54 # common variables and can create aliasing and readability issues.
55 # common variables and can create aliasing and readability issues.
55 requirealias = {
56 requirealias = {
56 'ui': 'uimod',
57 'ui': 'uimod',
57 }
58 }
58
59
59 def usingabsolute(root):
60 def usingabsolute(root):
60 """Whether absolute imports are being used."""
61 """Whether absolute imports are being used."""
61 if sys.version_info[0] >= 3:
62 if sys.version_info[0] >= 3:
62 return True
63 return True
63
64
64 for node in ast.walk(root):
65 for node in ast.walk(root):
65 if isinstance(node, ast.ImportFrom):
66 if isinstance(node, ast.ImportFrom):
66 if node.module == '__future__':
67 if node.module == '__future__':
67 for n in node.names:
68 for n in node.names:
68 if n.name == 'absolute_import':
69 if n.name == 'absolute_import':
69 return True
70 return True
70
71
71 return False
72 return False
72
73
73 def walklocal(root):
74 def walklocal(root):
74 """Recursively yield all descendant nodes but not in a different scope"""
75 """Recursively yield all descendant nodes but not in a different scope"""
75 todo = collections.deque(ast.iter_child_nodes(root))
76 todo = collections.deque(ast.iter_child_nodes(root))
76 yield root, False
77 yield root, False
77 while todo:
78 while todo:
78 node = todo.popleft()
79 node = todo.popleft()
79 newscope = isinstance(node, ast.FunctionDef)
80 newscope = isinstance(node, ast.FunctionDef)
80 if not newscope:
81 if not newscope:
81 todo.extend(ast.iter_child_nodes(node))
82 todo.extend(ast.iter_child_nodes(node))
82 yield node, newscope
83 yield node, newscope
83
84
84 def dotted_name_of_path(path):
85 def dotted_name_of_path(path):
85 """Given a relative path to a source file, return its dotted module name.
86 """Given a relative path to a source file, return its dotted module name.
86
87
87 >>> dotted_name_of_path('mercurial/error.py')
88 >>> dotted_name_of_path('mercurial/error.py')
88 'mercurial.error'
89 'mercurial.error'
89 >>> dotted_name_of_path('zlibmodule.so')
90 >>> dotted_name_of_path('zlibmodule.so')
90 'zlib'
91 'zlib'
91 """
92 """
92 parts = path.replace(os.sep, '/').split('/')
93 parts = path.replace(os.sep, '/').split('/')
93 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
94 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
94 if parts[-1].endswith('module'):
95 if parts[-1].endswith('module'):
95 parts[-1] = parts[-1][:-6]
96 parts[-1] = parts[-1][:-6]
96 return '.'.join(parts)
97 return '.'.join(parts)
97
98
98 def fromlocalfunc(modulename, localmods):
99 def fromlocalfunc(modulename, localmods):
99 """Get a function to examine which locally defined module the
100 """Get a function to examine which locally defined module the
100 target source imports via a specified name.
101 target source imports via a specified name.
101
102
102 `modulename` is an `dotted_name_of_path()`-ed source file path,
103 `modulename` is an `dotted_name_of_path()`-ed source file path,
103 which may have `.__init__` at the end of it, of the target source.
104 which may have `.__init__` at the end of it, of the target source.
104
105
105 `localmods` is a set of absolute `dotted_name_of_path()`-ed source file
106 `localmods` is a set of absolute `dotted_name_of_path()`-ed source file
106 paths of locally defined (= Mercurial specific) modules.
107 paths of locally defined (= Mercurial specific) modules.
107
108
108 This function assumes that module names not existing in
109 This function assumes that module names not existing in
109 `localmods` are from the Python standard library.
110 `localmods` are from the Python standard library.
110
111
111 This function returns the function, which takes `name` argument,
112 This function returns the function, which takes `name` argument,
112 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
113 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
113 matches against locally defined module. Otherwise, it returns
114 matches against locally defined module. Otherwise, it returns
114 False.
115 False.
115
116
116 It is assumed that `name` doesn't have `.__init__`.
117 It is assumed that `name` doesn't have `.__init__`.
117
118
118 `absname` is an absolute module name of specified `name`
119 `absname` is an absolute module name of specified `name`
119 (e.g. "hgext.convert"). This can be used to compose prefix for sub
120 (e.g. "hgext.convert"). This can be used to compose prefix for sub
120 modules or so.
121 modules or so.
121
122
122 `dottedpath` is a `dotted_name_of_path()`-ed source file path
123 `dottedpath` is a `dotted_name_of_path()`-ed source file path
123 (e.g. "hgext.convert.__init__") of `name`. This is used to look
124 (e.g. "hgext.convert.__init__") of `name`. This is used to look
124 module up in `localmods` again.
125 module up in `localmods` again.
125
126
126 `hassubmod` is whether it may have sub modules under it (for
127 `hassubmod` is whether it may have sub modules under it (for
127 convenient, even though this is also equivalent to "absname !=
128 convenient, even though this is also equivalent to "absname !=
128 dottednpath")
129 dottednpath")
129
130
130 >>> localmods = {'foo.__init__', 'foo.foo1',
131 >>> localmods = {'foo.__init__', 'foo.foo1',
131 ... 'foo.bar.__init__', 'foo.bar.bar1',
132 ... 'foo.bar.__init__', 'foo.bar.bar1',
132 ... 'baz.__init__', 'baz.baz1'}
133 ... 'baz.__init__', 'baz.baz1'}
133 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
134 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
134 >>> # relative
135 >>> # relative
135 >>> fromlocal('foo1')
136 >>> fromlocal('foo1')
136 ('foo.foo1', 'foo.foo1', False)
137 ('foo.foo1', 'foo.foo1', False)
137 >>> fromlocal('bar')
138 >>> fromlocal('bar')
138 ('foo.bar', 'foo.bar.__init__', True)
139 ('foo.bar', 'foo.bar.__init__', True)
139 >>> fromlocal('bar.bar1')
140 >>> fromlocal('bar.bar1')
140 ('foo.bar.bar1', 'foo.bar.bar1', False)
141 ('foo.bar.bar1', 'foo.bar.bar1', False)
141 >>> # absolute
142 >>> # absolute
142 >>> fromlocal('baz')
143 >>> fromlocal('baz')
143 ('baz', 'baz.__init__', True)
144 ('baz', 'baz.__init__', True)
144 >>> fromlocal('baz.baz1')
145 >>> fromlocal('baz.baz1')
145 ('baz.baz1', 'baz.baz1', False)
146 ('baz.baz1', 'baz.baz1', False)
146 >>> # unknown = maybe standard library
147 >>> # unknown = maybe standard library
147 >>> fromlocal('os')
148 >>> fromlocal('os')
148 False
149 False
149 >>> fromlocal(None, 1)
150 >>> fromlocal(None, 1)
150 ('foo', 'foo.__init__', True)
151 ('foo', 'foo.__init__', True)
151 >>> fromlocal('foo1', 1)
152 >>> fromlocal('foo1', 1)
152 ('foo.foo1', 'foo.foo1', False)
153 ('foo.foo1', 'foo.foo1', False)
153 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
154 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
154 >>> fromlocal2(None, 2)
155 >>> fromlocal2(None, 2)
155 ('foo', 'foo.__init__', True)
156 ('foo', 'foo.__init__', True)
156 >>> fromlocal2('bar2', 1)
157 >>> fromlocal2('bar2', 1)
157 False
158 False
158 >>> fromlocal2('bar', 2)
159 >>> fromlocal2('bar', 2)
159 ('foo.bar', 'foo.bar.__init__', True)
160 ('foo.bar', 'foo.bar.__init__', True)
160 """
161 """
161 if not isinstance(modulename, str):
162 if not isinstance(modulename, str):
162 modulename = modulename.decode('ascii')
163 modulename = modulename.decode('ascii')
163 prefix = '.'.join(modulename.split('.')[:-1])
164 prefix = '.'.join(modulename.split('.')[:-1])
164 if prefix:
165 if prefix:
165 prefix += '.'
166 prefix += '.'
166 def fromlocal(name, level=0):
167 def fromlocal(name, level=0):
167 # name is false value when relative imports are used.
168 # name is false value when relative imports are used.
168 if not name:
169 if not name:
169 # If relative imports are used, level must not be absolute.
170 # If relative imports are used, level must not be absolute.
170 assert level > 0
171 assert level > 0
171 candidates = ['.'.join(modulename.split('.')[:-level])]
172 candidates = ['.'.join(modulename.split('.')[:-level])]
172 else:
173 else:
173 if not level:
174 if not level:
174 # Check relative name first.
175 # Check relative name first.
175 candidates = [prefix + name, name]
176 candidates = [prefix + name, name]
176 else:
177 else:
177 candidates = ['.'.join(modulename.split('.')[:-level]) +
178 candidates = ['.'.join(modulename.split('.')[:-level]) +
178 '.' + name]
179 '.' + name]
179
180
180 for n in candidates:
181 for n in candidates:
181 if n in localmods:
182 if n in localmods:
182 return (n, n, False)
183 return (n, n, False)
183 dottedpath = n + '.__init__'
184 dottedpath = n + '.__init__'
184 if dottedpath in localmods:
185 if dottedpath in localmods:
185 return (n, dottedpath, True)
186 return (n, dottedpath, True)
186 return False
187 return False
187 return fromlocal
188 return fromlocal
188
189
189 def populateextmods(localmods):
190 def populateextmods(localmods):
190 """Populate C extension modules based on pure modules"""
191 """Populate C extension modules based on pure modules"""
191 newlocalmods = set(localmods)
192 newlocalmods = set(localmods)
192 for n in localmods:
193 for n in localmods:
193 if n.startswith('mercurial.pure.'):
194 if n.startswith('mercurial.pure.'):
194 m = n[len('mercurial.pure.'):]
195 m = n[len('mercurial.pure.'):]
195 newlocalmods.add('mercurial.cext.' + m)
196 newlocalmods.add('mercurial.cext.' + m)
196 newlocalmods.add('mercurial.cffi._' + m)
197 newlocalmods.add('mercurial.cffi._' + m)
197 return newlocalmods
198 return newlocalmods
198
199
199 def list_stdlib_modules():
200 def list_stdlib_modules():
200 """List the modules present in the stdlib.
201 """List the modules present in the stdlib.
201
202
202 >>> py3 = sys.version_info[0] >= 3
203 >>> py3 = sys.version_info[0] >= 3
203 >>> mods = set(list_stdlib_modules())
204 >>> mods = set(list_stdlib_modules())
204 >>> 'BaseHTTPServer' in mods or py3
205 >>> 'BaseHTTPServer' in mods or py3
205 True
206 True
206
207
207 os.path isn't really a module, so it's missing:
208 os.path isn't really a module, so it's missing:
208
209
209 >>> 'os.path' in mods
210 >>> 'os.path' in mods
210 False
211 False
211
212
212 sys requires special treatment, because it's baked into the
213 sys requires special treatment, because it's baked into the
213 interpreter, but it should still appear:
214 interpreter, but it should still appear:
214
215
215 >>> 'sys' in mods
216 >>> 'sys' in mods
216 True
217 True
217
218
218 >>> 'collections' in mods
219 >>> 'collections' in mods
219 True
220 True
220
221
221 >>> 'cStringIO' in mods or py3
222 >>> 'cStringIO' in mods or py3
222 True
223 True
223
224
224 >>> 'cffi' in mods
225 >>> 'cffi' in mods
225 True
226 True
226 """
227 """
227 for m in sys.builtin_module_names:
228 for m in sys.builtin_module_names:
228 yield m
229 yield m
229 # These modules only exist on windows, but we should always
230 # These modules only exist on windows, but we should always
230 # consider them stdlib.
231 # consider them stdlib.
231 for m in ['msvcrt', '_winreg']:
232 for m in ['msvcrt', '_winreg']:
232 yield m
233 yield m
233 yield '__builtin__'
234 yield '__builtin__'
234 yield 'builtins' # python3 only
235 yield 'builtins' # python3 only
235 yield 'importlib.abc' # python3 only
236 yield 'importlib.abc' # python3 only
236 yield 'importlib.machinery' # python3 only
237 yield 'importlib.machinery' # python3 only
237 yield 'importlib.util' # python3 only
238 yield 'importlib.util' # python3 only
238 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
239 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
239 yield m
240 yield m
240 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
241 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
241 yield m
242 yield m
242 for m in ['cffi']:
243 for m in ['cffi']:
243 yield m
244 yield m
244 stdlib_prefixes = {sys.prefix, sys.exec_prefix}
245 stdlib_prefixes = {sys.prefix, sys.exec_prefix}
245 # We need to supplement the list of prefixes for the search to work
246 # We need to supplement the list of prefixes for the search to work
246 # when run from within a virtualenv.
247 # when run from within a virtualenv.
247 for mod in (basehttpserver, zlib):
248 for mod in (basehttpserver, zlib):
248 if mod is None:
249 if mod is None:
249 continue
250 continue
250 try:
251 try:
251 # Not all module objects have a __file__ attribute.
252 # Not all module objects have a __file__ attribute.
252 filename = mod.__file__
253 filename = mod.__file__
253 except AttributeError:
254 except AttributeError:
254 continue
255 continue
255 dirname = os.path.dirname(filename)
256 dirname = os.path.dirname(filename)
256 for prefix in stdlib_prefixes:
257 for prefix in stdlib_prefixes:
257 if dirname.startswith(prefix):
258 if dirname.startswith(prefix):
258 # Then this directory is redundant.
259 # Then this directory is redundant.
259 break
260 break
260 else:
261 else:
261 stdlib_prefixes.add(dirname)
262 stdlib_prefixes.add(dirname)
262 sourceroot = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
263 sourceroot = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
263 for libpath in sys.path:
264 for libpath in sys.path:
264 # We want to walk everything in sys.path that starts with something in
265 # We want to walk everything in sys.path that starts with something in
265 # stdlib_prefixes, but not directories from the hg sources.
266 # stdlib_prefixes, but not directories from the hg sources.
266 if (os.path.abspath(libpath).startswith(sourceroot)
267 if (os.path.abspath(libpath).startswith(sourceroot)
267 or not any(libpath.startswith(p) for p in stdlib_prefixes)):
268 or not any(libpath.startswith(p) for p in stdlib_prefixes)):
268 continue
269 continue
269 for top, dirs, files in os.walk(libpath):
270 for top, dirs, files in os.walk(libpath):
270 for i, d in reversed(list(enumerate(dirs))):
271 for i, d in reversed(list(enumerate(dirs))):
271 if (not os.path.exists(os.path.join(top, d, '__init__.py'))
272 if (not os.path.exists(os.path.join(top, d, '__init__.py'))
272 or top == libpath and d in ('hgdemandimport', 'hgext',
273 or top == libpath and d in ('hgdemandimport', 'hgext',
273 'mercurial')):
274 'mercurial')):
274 del dirs[i]
275 del dirs[i]
275 for name in files:
276 for name in files:
276 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
277 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
277 continue
278 continue
278 if name.startswith('__init__.py'):
279 if name.startswith('__init__.py'):
279 full_path = top
280 full_path = top
280 else:
281 else:
281 full_path = os.path.join(top, name)
282 full_path = os.path.join(top, name)
282 rel_path = full_path[len(libpath) + 1:]
283 rel_path = full_path[len(libpath) + 1:]
283 mod = dotted_name_of_path(rel_path)
284 mod = dotted_name_of_path(rel_path)
284 yield mod
285 yield mod
285
286
286 stdlib_modules = set(list_stdlib_modules())
287 stdlib_modules = set(list_stdlib_modules())
287
288
288 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
289 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
289 """Given the source of a file as a string, yield the names
290 """Given the source of a file as a string, yield the names
290 imported by that file.
291 imported by that file.
291
292
292 Args:
293 Args:
293 source: The python source to examine as a string.
294 source: The python source to examine as a string.
294 modulename: of specified python source (may have `__init__`)
295 modulename: of specified python source (may have `__init__`)
295 localmods: set of locally defined module names (may have `__init__`)
296 localmods: set of locally defined module names (may have `__init__`)
296 ignore_nested: If true, import statements that do not start in
297 ignore_nested: If true, import statements that do not start in
297 column zero will be ignored.
298 column zero will be ignored.
298
299
299 Returns:
300 Returns:
300 A list of absolute module names imported by the given source.
301 A list of absolute module names imported by the given source.
301
302
302 >>> f = 'foo/xxx.py'
303 >>> f = 'foo/xxx.py'
303 >>> modulename = 'foo.xxx'
304 >>> modulename = 'foo.xxx'
304 >>> localmods = {'foo.__init__': True,
305 >>> localmods = {'foo.__init__': True,
305 ... 'foo.foo1': True, 'foo.foo2': True,
306 ... 'foo.foo1': True, 'foo.foo2': True,
306 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
307 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
307 ... 'baz.__init__': True, 'baz.baz1': True }
308 ... 'baz.__init__': True, 'baz.baz1': True }
308 >>> # standard library (= not locally defined ones)
309 >>> # standard library (= not locally defined ones)
309 >>> sorted(imported_modules(
310 >>> sorted(imported_modules(
310 ... 'from stdlib1 import foo, bar; import stdlib2',
311 ... 'from stdlib1 import foo, bar; import stdlib2',
311 ... modulename, f, localmods))
312 ... modulename, f, localmods))
312 []
313 []
313 >>> # relative importing
314 >>> # relative importing
314 >>> sorted(imported_modules(
315 >>> sorted(imported_modules(
315 ... 'import foo1; from bar import bar1',
316 ... 'import foo1; from bar import bar1',
316 ... modulename, f, localmods))
317 ... modulename, f, localmods))
317 ['foo.bar.bar1', 'foo.foo1']
318 ['foo.bar.bar1', 'foo.foo1']
318 >>> sorted(imported_modules(
319 >>> sorted(imported_modules(
319 ... 'from bar.bar1 import name1, name2, name3',
320 ... 'from bar.bar1 import name1, name2, name3',
320 ... modulename, f, localmods))
321 ... modulename, f, localmods))
321 ['foo.bar.bar1']
322 ['foo.bar.bar1']
322 >>> # absolute importing
323 >>> # absolute importing
323 >>> sorted(imported_modules(
324 >>> sorted(imported_modules(
324 ... 'from baz import baz1, name1',
325 ... 'from baz import baz1, name1',
325 ... modulename, f, localmods))
326 ... modulename, f, localmods))
326 ['baz.__init__', 'baz.baz1']
327 ['baz.__init__', 'baz.baz1']
327 >>> # mixed importing, even though it shouldn't be recommended
328 >>> # mixed importing, even though it shouldn't be recommended
328 >>> sorted(imported_modules(
329 >>> sorted(imported_modules(
329 ... 'import stdlib, foo1, baz',
330 ... 'import stdlib, foo1, baz',
330 ... modulename, f, localmods))
331 ... modulename, f, localmods))
331 ['baz.__init__', 'foo.foo1']
332 ['baz.__init__', 'foo.foo1']
332 >>> # ignore_nested
333 >>> # ignore_nested
333 >>> sorted(imported_modules(
334 >>> sorted(imported_modules(
334 ... '''import foo
335 ... '''import foo
335 ... def wat():
336 ... def wat():
336 ... import bar
337 ... import bar
337 ... ''', modulename, f, localmods))
338 ... ''', modulename, f, localmods))
338 ['foo.__init__', 'foo.bar.__init__']
339 ['foo.__init__', 'foo.bar.__init__']
339 >>> sorted(imported_modules(
340 >>> sorted(imported_modules(
340 ... '''import foo
341 ... '''import foo
341 ... def wat():
342 ... def wat():
342 ... import bar
343 ... import bar
343 ... ''', modulename, f, localmods, ignore_nested=True))
344 ... ''', modulename, f, localmods, ignore_nested=True))
344 ['foo.__init__']
345 ['foo.__init__']
345 """
346 """
346 fromlocal = fromlocalfunc(modulename, localmods)
347 fromlocal = fromlocalfunc(modulename, localmods)
347 for node in ast.walk(ast.parse(source, f)):
348 for node in ast.walk(ast.parse(source, f)):
348 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
349 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
349 continue
350 continue
350 if isinstance(node, ast.Import):
351 if isinstance(node, ast.Import):
351 for n in node.names:
352 for n in node.names:
352 found = fromlocal(n.name)
353 found = fromlocal(n.name)
353 if not found:
354 if not found:
354 # this should import standard library
355 # this should import standard library
355 continue
356 continue
356 yield found[1]
357 yield found[1]
357 elif isinstance(node, ast.ImportFrom):
358 elif isinstance(node, ast.ImportFrom):
358 found = fromlocal(node.module, node.level)
359 found = fromlocal(node.module, node.level)
359 if not found:
360 if not found:
360 # this should import standard library
361 # this should import standard library
361 continue
362 continue
362
363
363 absname, dottedpath, hassubmod = found
364 absname, dottedpath, hassubmod = found
364 if not hassubmod:
365 if not hassubmod:
365 # "dottedpath" is not a package; must be imported
366 # "dottedpath" is not a package; must be imported
366 yield dottedpath
367 yield dottedpath
367 # examination of "node.names" should be redundant
368 # examination of "node.names" should be redundant
368 # e.g.: from mercurial.node import nullid, nullrev
369 # e.g.: from mercurial.node import nullid, nullrev
369 continue
370 continue
370
371
371 modnotfound = False
372 modnotfound = False
372 prefix = absname + '.'
373 prefix = absname + '.'
373 for n in node.names:
374 for n in node.names:
374 found = fromlocal(prefix + n.name)
375 found = fromlocal(prefix + n.name)
375 if not found:
376 if not found:
376 # this should be a function or a property of "node.module"
377 # this should be a function or a property of "node.module"
377 modnotfound = True
378 modnotfound = True
378 continue
379 continue
379 yield found[1]
380 yield found[1]
380 if modnotfound:
381 if modnotfound:
381 # "dottedpath" is a package, but imported because of non-module
382 # "dottedpath" is a package, but imported because of non-module
382 # lookup
383 # lookup
383 yield dottedpath
384 yield dottedpath
384
385
385 def verify_import_convention(module, source, localmods):
386 def verify_import_convention(module, source, localmods):
386 """Verify imports match our established coding convention.
387 """Verify imports match our established coding convention.
387
388
388 We have 2 conventions: legacy and modern. The modern convention is in
389 We have 2 conventions: legacy and modern. The modern convention is in
389 effect when using absolute imports.
390 effect when using absolute imports.
390
391
391 The legacy convention only looks for mixed imports. The modern convention
392 The legacy convention only looks for mixed imports. The modern convention
392 is much more thorough.
393 is much more thorough.
393 """
394 """
394 root = ast.parse(source)
395 root = ast.parse(source)
395 absolute = usingabsolute(root)
396 absolute = usingabsolute(root)
396
397
397 if absolute:
398 if absolute:
398 return verify_modern_convention(module, root, localmods)
399 return verify_modern_convention(module, root, localmods)
399 else:
400 else:
400 return verify_stdlib_on_own_line(root)
401 return verify_stdlib_on_own_line(root)
401
402
402 def verify_modern_convention(module, root, localmods, root_col_offset=0):
403 def verify_modern_convention(module, root, localmods, root_col_offset=0):
403 """Verify a file conforms to the modern import convention rules.
404 """Verify a file conforms to the modern import convention rules.
404
405
405 The rules of the modern convention are:
406 The rules of the modern convention are:
406
407
407 * Ordering is stdlib followed by local imports. Each group is lexically
408 * Ordering is stdlib followed by local imports. Each group is lexically
408 sorted.
409 sorted.
409 * Importing multiple modules via "import X, Y" is not allowed: use
410 * Importing multiple modules via "import X, Y" is not allowed: use
410 separate import statements.
411 separate import statements.
411 * Importing multiple modules via "from X import ..." is allowed if using
412 * Importing multiple modules via "from X import ..." is allowed if using
412 parenthesis and one entry per line.
413 parenthesis and one entry per line.
413 * Only 1 relative import statement per import level ("from .", "from ..")
414 * Only 1 relative import statement per import level ("from .", "from ..")
414 is allowed.
415 is allowed.
415 * Relative imports from higher levels must occur before lower levels. e.g.
416 * Relative imports from higher levels must occur before lower levels. e.g.
416 "from .." must be before "from .".
417 "from .." must be before "from .".
417 * Imports from peer packages should use relative import (e.g. do not
418 * Imports from peer packages should use relative import (e.g. do not
418 "import mercurial.foo" from a "mercurial.*" module).
419 "import mercurial.foo" from a "mercurial.*" module).
419 * Symbols can only be imported from specific modules (see
420 * Symbols can only be imported from specific modules (see
420 `allowsymbolimports`). For other modules, first import the module then
421 `allowsymbolimports`). For other modules, first import the module then
421 assign the symbol to a module-level variable. In addition, these imports
422 assign the symbol to a module-level variable. In addition, these imports
422 must be performed before other local imports. This rule only
423 must be performed before other local imports. This rule only
423 applies to import statements outside of any blocks.
424 applies to import statements outside of any blocks.
424 * Relative imports from the standard library are not allowed, unless that
425 * Relative imports from the standard library are not allowed, unless that
425 library is also a local module.
426 library is also a local module.
426 * Certain modules must be aliased to alternate names to avoid aliasing
427 * Certain modules must be aliased to alternate names to avoid aliasing
427 and readability problems. See `requirealias`.
428 and readability problems. See `requirealias`.
428 """
429 """
429 if not isinstance(module, str):
430 if not isinstance(module, str):
430 module = module.decode('ascii')
431 module = module.decode('ascii')
431 topmodule = module.split('.')[0]
432 topmodule = module.split('.')[0]
432 fromlocal = fromlocalfunc(module, localmods)
433 fromlocal = fromlocalfunc(module, localmods)
433
434
434 # Whether a local/non-stdlib import has been performed.
435 # Whether a local/non-stdlib import has been performed.
435 seenlocal = None
436 seenlocal = None
436 # Whether a local/non-stdlib, non-symbol import has been seen.
437 # Whether a local/non-stdlib, non-symbol import has been seen.
437 seennonsymbollocal = False
438 seennonsymbollocal = False
438 # The last name to be imported (for sorting).
439 # The last name to be imported (for sorting).
439 lastname = None
440 lastname = None
440 laststdlib = None
441 laststdlib = None
441 # Relative import levels encountered so far.
442 # Relative import levels encountered so far.
442 seenlevels = set()
443 seenlevels = set()
443
444
444 for node, newscope in walklocal(root):
445 for node, newscope in walklocal(root):
445 def msg(fmt, *args):
446 def msg(fmt, *args):
446 return (fmt % args, node.lineno)
447 return (fmt % args, node.lineno)
447 if newscope:
448 if newscope:
448 # Check for local imports in function
449 # Check for local imports in function
449 for r in verify_modern_convention(module, node, localmods,
450 for r in verify_modern_convention(module, node, localmods,
450 node.col_offset + 4):
451 node.col_offset + 4):
451 yield r
452 yield r
452 elif isinstance(node, ast.Import):
453 elif isinstance(node, ast.Import):
453 # Disallow "import foo, bar" and require separate imports
454 # Disallow "import foo, bar" and require separate imports
454 # for each module.
455 # for each module.
455 if len(node.names) > 1:
456 if len(node.names) > 1:
456 yield msg('multiple imported names: %s',
457 yield msg('multiple imported names: %s',
457 ', '.join(n.name for n in node.names))
458 ', '.join(n.name for n in node.names))
458
459
459 name = node.names[0].name
460 name = node.names[0].name
460 asname = node.names[0].asname
461 asname = node.names[0].asname
461
462
462 stdlib = name in stdlib_modules
463 stdlib = name in stdlib_modules
463
464
464 # Ignore sorting rules on imports inside blocks.
465 # Ignore sorting rules on imports inside blocks.
465 if node.col_offset == root_col_offset:
466 if node.col_offset == root_col_offset:
466 if lastname and name < lastname and laststdlib == stdlib:
467 if lastname and name < lastname and laststdlib == stdlib:
467 yield msg('imports not lexically sorted: %s < %s',
468 yield msg('imports not lexically sorted: %s < %s',
468 name, lastname)
469 name, lastname)
469
470
470 lastname = name
471 lastname = name
471 laststdlib = stdlib
472 laststdlib = stdlib
472
473
473 # stdlib imports should be before local imports.
474 # stdlib imports should be before local imports.
474 if stdlib and seenlocal and node.col_offset == root_col_offset:
475 if stdlib and seenlocal and node.col_offset == root_col_offset:
475 yield msg('stdlib import "%s" follows local import: %s',
476 yield msg('stdlib import "%s" follows local import: %s',
476 name, seenlocal)
477 name, seenlocal)
477
478
478 if not stdlib:
479 if not stdlib:
479 seenlocal = name
480 seenlocal = name
480
481
481 # Import of sibling modules should use relative imports.
482 # Import of sibling modules should use relative imports.
482 topname = name.split('.')[0]
483 topname = name.split('.')[0]
483 if topname == topmodule:
484 if topname == topmodule:
484 yield msg('import should be relative: %s', name)
485 yield msg('import should be relative: %s', name)
485
486
486 if name in requirealias and asname != requirealias[name]:
487 if name in requirealias and asname != requirealias[name]:
487 yield msg('%s module must be "as" aliased to %s',
488 yield msg('%s module must be "as" aliased to %s',
488 name, requirealias[name])
489 name, requirealias[name])
489
490
490 elif isinstance(node, ast.ImportFrom):
491 elif isinstance(node, ast.ImportFrom):
491 # Resolve the full imported module name.
492 # Resolve the full imported module name.
492 if node.level > 0:
493 if node.level > 0:
493 fullname = '.'.join(module.split('.')[:-node.level])
494 fullname = '.'.join(module.split('.')[:-node.level])
494 if node.module:
495 if node.module:
495 fullname += '.%s' % node.module
496 fullname += '.%s' % node.module
496 else:
497 else:
497 assert node.module
498 assert node.module
498 fullname = node.module
499 fullname = node.module
499
500
500 topname = fullname.split('.')[0]
501 topname = fullname.split('.')[0]
501 if topname == topmodule:
502 if topname == topmodule:
502 yield msg('import should be relative: %s', fullname)
503 yield msg('import should be relative: %s', fullname)
503
504
504 # __future__ is special since it needs to come first and use
505 # __future__ is special since it needs to come first and use
505 # symbol import.
506 # symbol import.
506 if fullname != '__future__':
507 if fullname != '__future__':
507 if not fullname or (
508 if not fullname or (
508 fullname in stdlib_modules
509 fullname in stdlib_modules
509 and fullname not in localmods
510 and fullname not in localmods
510 and fullname + '.__init__' not in localmods):
511 and fullname + '.__init__' not in localmods):
511 yield msg('relative import of stdlib module')
512 yield msg('relative import of stdlib module')
512 else:
513 else:
513 seenlocal = fullname
514 seenlocal = fullname
514
515
515 # Direct symbol import is only allowed from certain modules and
516 # Direct symbol import is only allowed from certain modules and
516 # must occur before non-symbol imports.
517 # must occur before non-symbol imports.
517 found = fromlocal(node.module, node.level)
518 found = fromlocal(node.module, node.level)
518 if found and found[2]: # node.module is a package
519 if found and found[2]: # node.module is a package
519 prefix = found[0] + '.'
520 prefix = found[0] + '.'
520 symbols = (n.name for n in node.names
521 symbols = (n.name for n in node.names
521 if not fromlocal(prefix + n.name))
522 if not fromlocal(prefix + n.name))
522 else:
523 else:
523 symbols = (n.name for n in node.names)
524 symbols = (n.name for n in node.names)
524 symbols = [sym for sym in symbols if sym not in directsymbols]
525 symbols = [sym for sym in symbols if sym not in directsymbols]
525 if node.module and node.col_offset == root_col_offset:
526 if node.module and node.col_offset == root_col_offset:
526 if symbols and fullname not in allowsymbolimports:
527 if symbols and fullname not in allowsymbolimports:
527 yield msg('direct symbol import %s from %s',
528 yield msg('direct symbol import %s from %s',
528 ', '.join(symbols), fullname)
529 ', '.join(symbols), fullname)
529
530
530 if symbols and seennonsymbollocal:
531 if symbols and seennonsymbollocal:
531 yield msg('symbol import follows non-symbol import: %s',
532 yield msg('symbol import follows non-symbol import: %s',
532 fullname)
533 fullname)
533 if not symbols and fullname not in stdlib_modules:
534 if not symbols and fullname not in stdlib_modules:
534 seennonsymbollocal = True
535 seennonsymbollocal = True
535
536
536 if not node.module:
537 if not node.module:
537 assert node.level
538 assert node.level
538
539
539 # Only allow 1 group per level.
540 # Only allow 1 group per level.
540 if (node.level in seenlevels
541 if (node.level in seenlevels
541 and node.col_offset == root_col_offset):
542 and node.col_offset == root_col_offset):
542 yield msg('multiple "from %s import" statements',
543 yield msg('multiple "from %s import" statements',
543 '.' * node.level)
544 '.' * node.level)
544
545
545 # Higher-level groups come before lower-level groups.
546 # Higher-level groups come before lower-level groups.
546 if any(node.level > l for l in seenlevels):
547 if any(node.level > l for l in seenlevels):
547 yield msg('higher-level import should come first: %s',
548 yield msg('higher-level import should come first: %s',
548 fullname)
549 fullname)
549
550
550 seenlevels.add(node.level)
551 seenlevels.add(node.level)
551
552
552 # Entries in "from .X import ( ... )" lists must be lexically
553 # Entries in "from .X import ( ... )" lists must be lexically
553 # sorted.
554 # sorted.
554 lastentryname = None
555 lastentryname = None
555
556
556 for n in node.names:
557 for n in node.names:
557 if lastentryname and n.name < lastentryname:
558 if lastentryname and n.name < lastentryname:
558 yield msg('imports from %s not lexically sorted: %s < %s',
559 yield msg('imports from %s not lexically sorted: %s < %s',
559 fullname, n.name, lastentryname)
560 fullname, n.name, lastentryname)
560
561
561 lastentryname = n.name
562 lastentryname = n.name
562
563
563 if n.name in requirealias and n.asname != requirealias[n.name]:
564 if n.name in requirealias and n.asname != requirealias[n.name]:
564 yield msg('%s from %s must be "as" aliased to %s',
565 yield msg('%s from %s must be "as" aliased to %s',
565 n.name, fullname, requirealias[n.name])
566 n.name, fullname, requirealias[n.name])
566
567
567 def verify_stdlib_on_own_line(root):
568 def verify_stdlib_on_own_line(root):
568 """Given some python source, verify that stdlib imports are done
569 """Given some python source, verify that stdlib imports are done
569 in separate statements from relative local module imports.
570 in separate statements from relative local module imports.
570
571
571 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
572 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
572 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
573 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
573 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
574 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
574 []
575 []
575 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
576 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
576 []
577 []
577 """
578 """
578 for node in ast.walk(root):
579 for node in ast.walk(root):
579 if isinstance(node, ast.Import):
580 if isinstance(node, ast.Import):
580 from_stdlib = {False: [], True: []}
581 from_stdlib = {False: [], True: []}
581 for n in node.names:
582 for n in node.names:
582 from_stdlib[n.name in stdlib_modules].append(n.name)
583 from_stdlib[n.name in stdlib_modules].append(n.name)
583 if from_stdlib[True] and from_stdlib[False]:
584 if from_stdlib[True] and from_stdlib[False]:
584 yield ('mixed imports\n stdlib: %s\n relative: %s' %
585 yield ('mixed imports\n stdlib: %s\n relative: %s' %
585 (', '.join(sorted(from_stdlib[True])),
586 (', '.join(sorted(from_stdlib[True])),
586 ', '.join(sorted(from_stdlib[False]))), node.lineno)
587 ', '.join(sorted(from_stdlib[False]))), node.lineno)
587
588
588 class CircularImport(Exception):
589 class CircularImport(Exception):
589 pass
590 pass
590
591
591 def checkmod(mod, imports):
592 def checkmod(mod, imports):
592 shortest = {}
593 shortest = {}
593 visit = [[mod]]
594 visit = [[mod]]
594 while visit:
595 while visit:
595 path = visit.pop(0)
596 path = visit.pop(0)
596 for i in sorted(imports.get(path[-1], [])):
597 for i in sorted(imports.get(path[-1], [])):
597 if len(path) < shortest.get(i, 1000):
598 if len(path) < shortest.get(i, 1000):
598 shortest[i] = len(path)
599 shortest[i] = len(path)
599 if i in path:
600 if i in path:
600 if i == path[0]:
601 if i == path[0]:
601 raise CircularImport(path)
602 raise CircularImport(path)
602 continue
603 continue
603 visit.append(path + [i])
604 visit.append(path + [i])
604
605
605 def rotatecycle(cycle):
606 def rotatecycle(cycle):
606 """arrange a cycle so that the lexicographically first module listed first
607 """arrange a cycle so that the lexicographically first module listed first
607
608
608 >>> rotatecycle(['foo', 'bar'])
609 >>> rotatecycle(['foo', 'bar'])
609 ['bar', 'foo', 'bar']
610 ['bar', 'foo', 'bar']
610 """
611 """
611 lowest = min(cycle)
612 lowest = min(cycle)
612 idx = cycle.index(lowest)
613 idx = cycle.index(lowest)
613 return cycle[idx:] + cycle[:idx] + [lowest]
614 return cycle[idx:] + cycle[:idx] + [lowest]
614
615
615 def find_cycles(imports):
616 def find_cycles(imports):
616 """Find cycles in an already-loaded import graph.
617 """Find cycles in an already-loaded import graph.
617
618
618 All module names recorded in `imports` should be absolute one.
619 All module names recorded in `imports` should be absolute one.
619
620
620 >>> from __future__ import print_function
621 >>> from __future__ import print_function
621 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
622 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
622 ... 'top.bar': ['top.baz', 'sys'],
623 ... 'top.bar': ['top.baz', 'sys'],
623 ... 'top.baz': ['top.foo'],
624 ... 'top.baz': ['top.foo'],
624 ... 'top.qux': ['top.foo']}
625 ... 'top.qux': ['top.foo']}
625 >>> print('\\n'.join(sorted(find_cycles(imports))))
626 >>> print('\\n'.join(sorted(find_cycles(imports))))
626 top.bar -> top.baz -> top.foo -> top.bar
627 top.bar -> top.baz -> top.foo -> top.bar
627 top.foo -> top.qux -> top.foo
628 top.foo -> top.qux -> top.foo
628 """
629 """
629 cycles = set()
630 cycles = set()
630 for mod in sorted(imports.keys()):
631 for mod in sorted(imports.keys()):
631 try:
632 try:
632 checkmod(mod, imports)
633 checkmod(mod, imports)
633 except CircularImport as e:
634 except CircularImport as e:
634 cycle = e.args[0]
635 cycle = e.args[0]
635 cycles.add(" -> ".join(rotatecycle(cycle)))
636 cycles.add(" -> ".join(rotatecycle(cycle)))
636 return cycles
637 return cycles
637
638
638 def _cycle_sortkey(c):
639 def _cycle_sortkey(c):
639 return len(c), c
640 return len(c), c
640
641
641 def embedded(f, modname, src):
642 def embedded(f, modname, src):
642 """Extract embedded python code
643 """Extract embedded python code
643
644
644 >>> def _forcestr(thing):
645 >>> def _forcestr(thing):
645 ... if not isinstance(thing, str):
646 ... if not isinstance(thing, str):
646 ... return thing.decode('ascii')
647 ... return thing.decode('ascii')
647 ... return thing
648 ... return thing
648 >>> def test(fn, lines):
649 >>> def test(fn, lines):
649 ... for s, m, f, l in embedded(fn, b"example", lines):
650 ... for s, m, f, l in embedded(fn, b"example", lines):
650 ... print("%s %s %d" % (_forcestr(m), _forcestr(f), l))
651 ... print("%s %s %d" % (_forcestr(m), _forcestr(f), l))
651 ... print(repr(_forcestr(s)))
652 ... print(repr(_forcestr(s)))
652 >>> lines = [
653 >>> lines = [
653 ... 'comment',
654 ... 'comment',
654 ... ' >>> from __future__ import print_function',
655 ... ' >>> from __future__ import print_function',
655 ... " >>> ' multiline",
656 ... " >>> ' multiline",
656 ... " ... string'",
657 ... " ... string'",
657 ... ' ',
658 ... ' ',
658 ... 'comment',
659 ... 'comment',
659 ... ' $ cat > foo.py <<EOF',
660 ... ' $ cat > foo.py <<EOF',
660 ... ' > from __future__ import print_function',
661 ... ' > from __future__ import print_function',
661 ... ' > EOF',
662 ... ' > EOF',
662 ... ]
663 ... ]
663 >>> test(b"example.t", lines)
664 >>> test(b"example.t", lines)
664 example[2] doctest.py 1
665 example[2] doctest.py 1
665 "from __future__ import print_function\\n' multiline\\nstring'\\n\\n"
666 "from __future__ import print_function\\n' multiline\\nstring'\\n\\n"
666 example[8] foo.py 7
667 example[8] foo.py 7
667 'from __future__ import print_function\\n'
668 'from __future__ import print_function\\n'
668 """
669 """
669 errors = []
670 errors = []
670 for name, starts, ends, code in testparseutil.pyembedded(f, src, errors):
671 for name, starts, ends, code in testparseutil.pyembedded(f, src, errors):
671 if not name:
672 if not name:
672 # use 'doctest.py', in order to make already existing
673 # use 'doctest.py', in order to make already existing
673 # doctest above pass instantly
674 # doctest above pass instantly
674 name = 'doctest.py'
675 name = 'doctest.py'
675 # "starts" is "line number" (1-origin), but embedded() is
676 # "starts" is "line number" (1-origin), but embedded() is
676 # expected to return "line offset" (0-origin). Therefore, this
677 # expected to return "line offset" (0-origin). Therefore, this
677 # yields "starts - 1".
678 # yields "starts - 1".
678 if not isinstance(modname, str):
679 if not isinstance(modname, str):
679 modname = modname.decode('utf8')
680 modname = modname.decode('utf8')
680 yield code, "%s[%d]" % (modname, starts), name, starts - 1
681 yield code, "%s[%d]" % (modname, starts), name, starts - 1
681
682
682 def sources(f, modname):
683 def sources(f, modname):
683 """Yields possibly multiple sources from a filepath
684 """Yields possibly multiple sources from a filepath
684
685
685 input: filepath, modulename
686 input: filepath, modulename
686 yields: script(string), modulename, filepath, linenumber
687 yields: script(string), modulename, filepath, linenumber
687
688
688 For embedded scripts, the modulename and filepath will be different
689 For embedded scripts, the modulename and filepath will be different
689 from the function arguments. linenumber is an offset relative to
690 from the function arguments. linenumber is an offset relative to
690 the input file.
691 the input file.
691 """
692 """
692 py = False
693 py = False
693 if not f.endswith('.t'):
694 if not f.endswith('.t'):
694 with open(f, 'rb') as src:
695 with open(f, 'rb') as src:
695 yield src.read(), modname, f, 0
696 yield src.read(), modname, f, 0
696 py = True
697 py = True
697 if py or f.endswith('.t'):
698 if py or f.endswith('.t'):
698 with open(f, 'r') as src:
699 with open(f, 'r') as src:
699 for script, modname, t, line in embedded(f, modname, src):
700 for script, modname, t, line in embedded(f, modname, src):
700 yield script, modname.encode('utf8'), t, line
701 yield script, modname.encode('utf8'), t, line
701
702
702 def main(argv):
703 def main(argv):
703 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
704 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
704 print('Usage: %s {-|file [file] [file] ...}')
705 print('Usage: %s {-|file [file] [file] ...}')
705 return 1
706 return 1
706 if argv[1] == '-':
707 if argv[1] == '-':
707 argv = argv[:1]
708 argv = argv[:1]
708 argv.extend(l.rstrip() for l in sys.stdin.readlines())
709 argv.extend(l.rstrip() for l in sys.stdin.readlines())
709 localmodpaths = {}
710 localmodpaths = {}
710 used_imports = {}
711 used_imports = {}
711 any_errors = False
712 any_errors = False
712 for source_path in argv[1:]:
713 for source_path in argv[1:]:
713 modname = dotted_name_of_path(source_path)
714 modname = dotted_name_of_path(source_path)
714 localmodpaths[modname] = source_path
715 localmodpaths[modname] = source_path
715 localmods = populateextmods(localmodpaths)
716 localmods = populateextmods(localmodpaths)
716 for localmodname, source_path in sorted(localmodpaths.items()):
717 for localmodname, source_path in sorted(localmodpaths.items()):
717 if not isinstance(localmodname, bytes):
718 if not isinstance(localmodname, bytes):
718 # This is only safe because all hg's files are ascii
719 # This is only safe because all hg's files are ascii
719 localmodname = localmodname.encode('ascii')
720 localmodname = localmodname.encode('ascii')
720 for src, modname, name, line in sources(source_path, localmodname):
721 for src, modname, name, line in sources(source_path, localmodname):
721 try:
722 try:
722 used_imports[modname] = sorted(
723 used_imports[modname] = sorted(
723 imported_modules(src, modname, name, localmods,
724 imported_modules(src, modname, name, localmods,
724 ignore_nested=True))
725 ignore_nested=True))
725 for error, lineno in verify_import_convention(modname, src,
726 for error, lineno in verify_import_convention(modname, src,
726 localmods):
727 localmods):
727 any_errors = True
728 any_errors = True
728 print('%s:%d: %s' % (source_path, lineno + line, error))
729 print('%s:%d: %s' % (source_path, lineno + line, error))
729 except SyntaxError as e:
730 except SyntaxError as e:
730 print('%s:%d: SyntaxError: %s' %
731 print('%s:%d: SyntaxError: %s' %
731 (source_path, e.lineno + line, e))
732 (source_path, e.lineno + line, e))
732 cycles = find_cycles(used_imports)
733 cycles = find_cycles(used_imports)
733 if cycles:
734 if cycles:
734 firstmods = set()
735 firstmods = set()
735 for c in sorted(cycles, key=_cycle_sortkey):
736 for c in sorted(cycles, key=_cycle_sortkey):
736 first = c.split()[0]
737 first = c.split()[0]
737 # As a rough cut, ignore any cycle that starts with the
738 # As a rough cut, ignore any cycle that starts with the
738 # same module as some other cycle. Otherwise we see lots
739 # same module as some other cycle. Otherwise we see lots
739 # of cycles that are effectively duplicates.
740 # of cycles that are effectively duplicates.
740 if first in firstmods:
741 if first in firstmods:
741 continue
742 continue
742 print('Import cycle:', c)
743 print('Import cycle:', c)
743 firstmods.add(first)
744 firstmods.add(first)
744 any_errors = True
745 any_errors = True
745 return any_errors != 0
746 return any_errors != 0
746
747
747 if __name__ == '__main__':
748 if __name__ == '__main__':
748 sys.exit(int(main(sys.argv)))
749 sys.exit(int(main(sys.argv)))
@@ -1,383 +1,386
1 # lfs - hash-preserving large file support using Git-LFS protocol
1 # lfs - hash-preserving large file support using Git-LFS protocol
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
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 """lfs - large file support (EXPERIMENTAL)
8 """lfs - large file support (EXPERIMENTAL)
9
9
10 This extension allows large files to be tracked outside of the normal
10 This extension allows large files to be tracked outside of the normal
11 repository storage and stored on a centralized server, similar to the
11 repository storage and stored on a centralized server, similar to the
12 ``largefiles`` extension. The ``git-lfs`` protocol is used when
12 ``largefiles`` extension. The ``git-lfs`` protocol is used when
13 communicating with the server, so existing git infrastructure can be
13 communicating with the server, so existing git infrastructure can be
14 harnessed. Even though the files are stored outside of the repository,
14 harnessed. Even though the files are stored outside of the repository,
15 they are still integrity checked in the same manner as normal files.
15 they are still integrity checked in the same manner as normal files.
16
16
17 The files stored outside of the repository are downloaded on demand,
17 The files stored outside of the repository are downloaded on demand,
18 which reduces the time to clone, and possibly the local disk usage.
18 which reduces the time to clone, and possibly the local disk usage.
19 This changes fundamental workflows in a DVCS, so careful thought
19 This changes fundamental workflows in a DVCS, so careful thought
20 should be given before deploying it. :hg:`convert` can be used to
20 should be given before deploying it. :hg:`convert` can be used to
21 convert LFS repositories to normal repositories that no longer
21 convert LFS repositories to normal repositories that no longer
22 require this extension, and do so without changing the commit hashes.
22 require this extension, and do so without changing the commit hashes.
23 This allows the extension to be disabled if the centralized workflow
23 This allows the extension to be disabled if the centralized workflow
24 becomes burdensome. However, the pre and post convert clones will
24 becomes burdensome. However, the pre and post convert clones will
25 not be able to communicate with each other unless the extension is
25 not be able to communicate with each other unless the extension is
26 enabled on both.
26 enabled on both.
27
27
28 To start a new repository, or to add LFS files to an existing one, just
28 To start a new repository, or to add LFS files to an existing one, just
29 create an ``.hglfs`` file as described below in the root directory of
29 create an ``.hglfs`` file as described below in the root directory of
30 the repository. Typically, this file should be put under version
30 the repository. Typically, this file should be put under version
31 control, so that the settings will propagate to other repositories with
31 control, so that the settings will propagate to other repositories with
32 push and pull. During any commit, Mercurial will consult this file to
32 push and pull. During any commit, Mercurial will consult this file to
33 determine if an added or modified file should be stored externally. The
33 determine if an added or modified file should be stored externally. The
34 type of storage depends on the characteristics of the file at each
34 type of storage depends on the characteristics of the file at each
35 commit. A file that is near a size threshold may switch back and forth
35 commit. A file that is near a size threshold may switch back and forth
36 between LFS and normal storage, as needed.
36 between LFS and normal storage, as needed.
37
37
38 Alternately, both normal repositories and largefile controlled
38 Alternately, both normal repositories and largefile controlled
39 repositories can be converted to LFS by using :hg:`convert` and the
39 repositories can be converted to LFS by using :hg:`convert` and the
40 ``lfs.track`` config option described below. The ``.hglfs`` file
40 ``lfs.track`` config option described below. The ``.hglfs`` file
41 should then be created and added, to control subsequent LFS selection.
41 should then be created and added, to control subsequent LFS selection.
42 The hashes are also unchanged in this case. The LFS and non-LFS
42 The hashes are also unchanged in this case. The LFS and non-LFS
43 repositories can be distinguished because the LFS repository will
43 repositories can be distinguished because the LFS repository will
44 abort any command if this extension is disabled.
44 abort any command if this extension is disabled.
45
45
46 Committed LFS files are held locally, until the repository is pushed.
46 Committed LFS files are held locally, until the repository is pushed.
47 Prior to pushing the normal repository data, the LFS files that are
47 Prior to pushing the normal repository data, the LFS files that are
48 tracked by the outgoing commits are automatically uploaded to the
48 tracked by the outgoing commits are automatically uploaded to the
49 configured central server. No LFS files are transferred on
49 configured central server. No LFS files are transferred on
50 :hg:`pull` or :hg:`clone`. Instead, the files are downloaded on
50 :hg:`pull` or :hg:`clone`. Instead, the files are downloaded on
51 demand as they need to be read, if a cached copy cannot be found
51 demand as they need to be read, if a cached copy cannot be found
52 locally. Both committing and downloading an LFS file will link the
52 locally. Both committing and downloading an LFS file will link the
53 file to a usercache, to speed up future access. See the `usercache`
53 file to a usercache, to speed up future access. See the `usercache`
54 config setting described below.
54 config setting described below.
55
55
56 .hglfs::
56 .hglfs::
57
57
58 The extension reads its configuration from a versioned ``.hglfs``
58 The extension reads its configuration from a versioned ``.hglfs``
59 configuration file found in the root of the working directory. The
59 configuration file found in the root of the working directory. The
60 ``.hglfs`` file uses the same syntax as all other Mercurial
60 ``.hglfs`` file uses the same syntax as all other Mercurial
61 configuration files. It uses a single section, ``[track]``.
61 configuration files. It uses a single section, ``[track]``.
62
62
63 The ``[track]`` section specifies which files are stored as LFS (or
63 The ``[track]`` section specifies which files are stored as LFS (or
64 not). Each line is keyed by a file pattern, with a predicate value.
64 not). Each line is keyed by a file pattern, with a predicate value.
65 The first file pattern match is used, so put more specific patterns
65 The first file pattern match is used, so put more specific patterns
66 first. The available predicates are ``all()``, ``none()``, and
66 first. The available predicates are ``all()``, ``none()``, and
67 ``size()``. See "hg help filesets.size" for the latter.
67 ``size()``. See "hg help filesets.size" for the latter.
68
68
69 Example versioned ``.hglfs`` file::
69 Example versioned ``.hglfs`` file::
70
70
71 [track]
71 [track]
72 # No Makefile or python file, anywhere, will be LFS
72 # No Makefile or python file, anywhere, will be LFS
73 **Makefile = none()
73 **Makefile = none()
74 **.py = none()
74 **.py = none()
75
75
76 **.zip = all()
76 **.zip = all()
77 **.exe = size(">1MB")
77 **.exe = size(">1MB")
78
78
79 # Catchall for everything not matched above
79 # Catchall for everything not matched above
80 ** = size(">10MB")
80 ** = size(">10MB")
81
81
82 Configs::
82 Configs::
83
83
84 [lfs]
84 [lfs]
85 # Remote endpoint. Multiple protocols are supported:
85 # Remote endpoint. Multiple protocols are supported:
86 # - http(s)://user:pass@example.com/path
86 # - http(s)://user:pass@example.com/path
87 # git-lfs endpoint
87 # git-lfs endpoint
88 # - file:///tmp/path
88 # - file:///tmp/path
89 # local filesystem, usually for testing
89 # local filesystem, usually for testing
90 # if unset, lfs will assume the remote repository also handles blob storage
90 # if unset, lfs will assume the remote repository also handles blob storage
91 # for http(s) URLs. Otherwise, lfs will prompt to set this when it must
91 # for http(s) URLs. Otherwise, lfs will prompt to set this when it must
92 # use this value.
92 # use this value.
93 # (default: unset)
93 # (default: unset)
94 url = https://example.com/repo.git/info/lfs
94 url = https://example.com/repo.git/info/lfs
95
95
96 # Which files to track in LFS. Path tests are "**.extname" for file
96 # Which files to track in LFS. Path tests are "**.extname" for file
97 # extensions, and "path:under/some/directory" for path prefix. Both
97 # extensions, and "path:under/some/directory" for path prefix. Both
98 # are relative to the repository root.
98 # are relative to the repository root.
99 # File size can be tested with the "size()" fileset, and tests can be
99 # File size can be tested with the "size()" fileset, and tests can be
100 # joined with fileset operators. (See "hg help filesets.operators".)
100 # joined with fileset operators. (See "hg help filesets.operators".)
101 #
101 #
102 # Some examples:
102 # Some examples:
103 # - all() # everything
103 # - all() # everything
104 # - none() # nothing
104 # - none() # nothing
105 # - size(">20MB") # larger than 20MB
105 # - size(">20MB") # larger than 20MB
106 # - !**.txt # anything not a *.txt file
106 # - !**.txt # anything not a *.txt file
107 # - **.zip | **.tar.gz | **.7z # some types of compressed files
107 # - **.zip | **.tar.gz | **.7z # some types of compressed files
108 # - path:bin # files under "bin" in the project root
108 # - path:bin # files under "bin" in the project root
109 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
109 # - (**.php & size(">2MB")) | (**.js & size(">5MB")) | **.tar.gz
110 # | (path:bin & !path:/bin/README) | size(">1GB")
110 # | (path:bin & !path:/bin/README) | size(">1GB")
111 # (default: none())
111 # (default: none())
112 #
112 #
113 # This is ignored if there is a tracked '.hglfs' file, and this setting
113 # This is ignored if there is a tracked '.hglfs' file, and this setting
114 # will eventually be deprecated and removed.
114 # will eventually be deprecated and removed.
115 track = size(">10M")
115 track = size(">10M")
116
116
117 # how many times to retry before giving up on transferring an object
117 # how many times to retry before giving up on transferring an object
118 retry = 5
118 retry = 5
119
119
120 # the local directory to store lfs files for sharing across local clones.
120 # the local directory to store lfs files for sharing across local clones.
121 # If not set, the cache is located in an OS specific cache location.
121 # If not set, the cache is located in an OS specific cache location.
122 usercache = /path/to/global/cache
122 usercache = /path/to/global/cache
123 """
123 """
124
124
125 from __future__ import absolute_import
125 from __future__ import absolute_import
126
126
127 import sys
127 import sys
128
128
129 from mercurial.i18n import _
129 from mercurial.i18n import _
130
130
131 from mercurial import (
131 from mercurial import (
132 config,
132 config,
133 context,
133 context,
134 error,
134 error,
135 exchange,
135 exchange,
136 extensions,
136 extensions,
137 exthelper,
137 exthelper,
138 filelog,
138 filelog,
139 filesetlang,
139 filesetlang,
140 localrepo,
140 localrepo,
141 minifileset,
141 minifileset,
142 node,
142 node,
143 pycompat,
143 pycompat,
144 repository,
145 revlog,
144 revlog,
146 scmutil,
145 scmutil,
147 templateutil,
146 templateutil,
148 util,
147 util,
149 )
148 )
150
149
150 from mercurial.interfaces import (
151 repository,
152 )
153
151 from . import (
154 from . import (
152 blobstore,
155 blobstore,
153 wireprotolfsserver,
156 wireprotolfsserver,
154 wrapper,
157 wrapper,
155 )
158 )
156
159
157 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
160 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
158 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
161 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
159 # be specifying the version(s) of Mercurial they are tested with, or
162 # be specifying the version(s) of Mercurial they are tested with, or
160 # leave the attribute unspecified.
163 # leave the attribute unspecified.
161 testedwith = 'ships-with-hg-core'
164 testedwith = 'ships-with-hg-core'
162
165
163 eh = exthelper.exthelper()
166 eh = exthelper.exthelper()
164 eh.merge(wrapper.eh)
167 eh.merge(wrapper.eh)
165 eh.merge(wireprotolfsserver.eh)
168 eh.merge(wireprotolfsserver.eh)
166
169
167 cmdtable = eh.cmdtable
170 cmdtable = eh.cmdtable
168 configtable = eh.configtable
171 configtable = eh.configtable
169 extsetup = eh.finalextsetup
172 extsetup = eh.finalextsetup
170 uisetup = eh.finaluisetup
173 uisetup = eh.finaluisetup
171 filesetpredicate = eh.filesetpredicate
174 filesetpredicate = eh.filesetpredicate
172 reposetup = eh.finalreposetup
175 reposetup = eh.finalreposetup
173 templatekeyword = eh.templatekeyword
176 templatekeyword = eh.templatekeyword
174
177
175 eh.configitem('experimental', 'lfs.serve',
178 eh.configitem('experimental', 'lfs.serve',
176 default=True,
179 default=True,
177 )
180 )
178 eh.configitem('experimental', 'lfs.user-agent',
181 eh.configitem('experimental', 'lfs.user-agent',
179 default=None,
182 default=None,
180 )
183 )
181 eh.configitem('experimental', 'lfs.disableusercache',
184 eh.configitem('experimental', 'lfs.disableusercache',
182 default=False,
185 default=False,
183 )
186 )
184 eh.configitem('experimental', 'lfs.worker-enable',
187 eh.configitem('experimental', 'lfs.worker-enable',
185 default=False,
188 default=False,
186 )
189 )
187
190
188 eh.configitem('lfs', 'url',
191 eh.configitem('lfs', 'url',
189 default=None,
192 default=None,
190 )
193 )
191 eh.configitem('lfs', 'usercache',
194 eh.configitem('lfs', 'usercache',
192 default=None,
195 default=None,
193 )
196 )
194 # Deprecated
197 # Deprecated
195 eh.configitem('lfs', 'threshold',
198 eh.configitem('lfs', 'threshold',
196 default=None,
199 default=None,
197 )
200 )
198 eh.configitem('lfs', 'track',
201 eh.configitem('lfs', 'track',
199 default='none()',
202 default='none()',
200 )
203 )
201 eh.configitem('lfs', 'retry',
204 eh.configitem('lfs', 'retry',
202 default=5,
205 default=5,
203 )
206 )
204
207
205 lfsprocessor = (
208 lfsprocessor = (
206 wrapper.readfromstore,
209 wrapper.readfromstore,
207 wrapper.writetostore,
210 wrapper.writetostore,
208 wrapper.bypasscheckhash,
211 wrapper.bypasscheckhash,
209 )
212 )
210
213
211 def featuresetup(ui, supported):
214 def featuresetup(ui, supported):
212 # don't die on seeing a repo with the lfs requirement
215 # don't die on seeing a repo with the lfs requirement
213 supported |= {'lfs'}
216 supported |= {'lfs'}
214
217
215 @eh.uisetup
218 @eh.uisetup
216 def _uisetup(ui):
219 def _uisetup(ui):
217 localrepo.featuresetupfuncs.add(featuresetup)
220 localrepo.featuresetupfuncs.add(featuresetup)
218
221
219 @eh.reposetup
222 @eh.reposetup
220 def _reposetup(ui, repo):
223 def _reposetup(ui, repo):
221 # Nothing to do with a remote repo
224 # Nothing to do with a remote repo
222 if not repo.local():
225 if not repo.local():
223 return
226 return
224
227
225 repo.svfs.lfslocalblobstore = blobstore.local(repo)
228 repo.svfs.lfslocalblobstore = blobstore.local(repo)
226 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
229 repo.svfs.lfsremoteblobstore = blobstore.remote(repo)
227
230
228 class lfsrepo(repo.__class__):
231 class lfsrepo(repo.__class__):
229 @localrepo.unfilteredmethod
232 @localrepo.unfilteredmethod
230 def commitctx(self, ctx, error=False, origctx=None):
233 def commitctx(self, ctx, error=False, origctx=None):
231 repo.svfs.options['lfstrack'] = _trackedmatcher(self)
234 repo.svfs.options['lfstrack'] = _trackedmatcher(self)
232 return super(lfsrepo, self).commitctx(ctx, error, origctx=origctx)
235 return super(lfsrepo, self).commitctx(ctx, error, origctx=origctx)
233
236
234 repo.__class__ = lfsrepo
237 repo.__class__ = lfsrepo
235
238
236 if 'lfs' not in repo.requirements:
239 if 'lfs' not in repo.requirements:
237 def checkrequireslfs(ui, repo, **kwargs):
240 def checkrequireslfs(ui, repo, **kwargs):
238 if 'lfs' in repo.requirements:
241 if 'lfs' in repo.requirements:
239 return 0
242 return 0
240
243
241 last = kwargs.get(r'node_last')
244 last = kwargs.get(r'node_last')
242 _bin = node.bin
245 _bin = node.bin
243 if last:
246 if last:
244 s = repo.set('%n:%n', _bin(kwargs[r'node']), _bin(last))
247 s = repo.set('%n:%n', _bin(kwargs[r'node']), _bin(last))
245 else:
248 else:
246 s = repo.set('%n', _bin(kwargs[r'node']))
249 s = repo.set('%n', _bin(kwargs[r'node']))
247 match = repo._storenarrowmatch
250 match = repo._storenarrowmatch
248 for ctx in s:
251 for ctx in s:
249 # TODO: is there a way to just walk the files in the commit?
252 # TODO: is there a way to just walk the files in the commit?
250 if any(ctx[f].islfs() for f in ctx.files()
253 if any(ctx[f].islfs() for f in ctx.files()
251 if f in ctx and match(f)):
254 if f in ctx and match(f)):
252 repo.requirements.add('lfs')
255 repo.requirements.add('lfs')
253 repo.features.add(repository.REPO_FEATURE_LFS)
256 repo.features.add(repository.REPO_FEATURE_LFS)
254 repo._writerequirements()
257 repo._writerequirements()
255 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
258 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
256 break
259 break
257
260
258 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
261 ui.setconfig('hooks', 'commit.lfs', checkrequireslfs, 'lfs')
259 ui.setconfig('hooks', 'pretxnchangegroup.lfs', checkrequireslfs, 'lfs')
262 ui.setconfig('hooks', 'pretxnchangegroup.lfs', checkrequireslfs, 'lfs')
260 else:
263 else:
261 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
264 repo.prepushoutgoinghooks.add('lfs', wrapper.prepush)
262
265
263 def _trackedmatcher(repo):
266 def _trackedmatcher(repo):
264 """Return a function (path, size) -> bool indicating whether or not to
267 """Return a function (path, size) -> bool indicating whether or not to
265 track a given file with lfs."""
268 track a given file with lfs."""
266 if not repo.wvfs.exists('.hglfs'):
269 if not repo.wvfs.exists('.hglfs'):
267 # No '.hglfs' in wdir. Fallback to config for now.
270 # No '.hglfs' in wdir. Fallback to config for now.
268 trackspec = repo.ui.config('lfs', 'track')
271 trackspec = repo.ui.config('lfs', 'track')
269
272
270 # deprecated config: lfs.threshold
273 # deprecated config: lfs.threshold
271 threshold = repo.ui.configbytes('lfs', 'threshold')
274 threshold = repo.ui.configbytes('lfs', 'threshold')
272 if threshold:
275 if threshold:
273 filesetlang.parse(trackspec) # make sure syntax errors are confined
276 filesetlang.parse(trackspec) # make sure syntax errors are confined
274 trackspec = "(%s) | size('>%d')" % (trackspec, threshold)
277 trackspec = "(%s) | size('>%d')" % (trackspec, threshold)
275
278
276 return minifileset.compile(trackspec)
279 return minifileset.compile(trackspec)
277
280
278 data = repo.wvfs.tryread('.hglfs')
281 data = repo.wvfs.tryread('.hglfs')
279 if not data:
282 if not data:
280 return lambda p, s: False
283 return lambda p, s: False
281
284
282 # Parse errors here will abort with a message that points to the .hglfs file
285 # Parse errors here will abort with a message that points to the .hglfs file
283 # and line number.
286 # and line number.
284 cfg = config.config()
287 cfg = config.config()
285 cfg.parse('.hglfs', data)
288 cfg.parse('.hglfs', data)
286
289
287 try:
290 try:
288 rules = [(minifileset.compile(pattern), minifileset.compile(rule))
291 rules = [(minifileset.compile(pattern), minifileset.compile(rule))
289 for pattern, rule in cfg.items('track')]
292 for pattern, rule in cfg.items('track')]
290 except error.ParseError as e:
293 except error.ParseError as e:
291 # The original exception gives no indicator that the error is in the
294 # The original exception gives no indicator that the error is in the
292 # .hglfs file, so add that.
295 # .hglfs file, so add that.
293
296
294 # TODO: See if the line number of the file can be made available.
297 # TODO: See if the line number of the file can be made available.
295 raise error.Abort(_('parse error in .hglfs: %s') % e)
298 raise error.Abort(_('parse error in .hglfs: %s') % e)
296
299
297 def _match(path, size):
300 def _match(path, size):
298 for pat, rule in rules:
301 for pat, rule in rules:
299 if pat(path, size):
302 if pat(path, size):
300 return rule(path, size)
303 return rule(path, size)
301
304
302 return False
305 return False
303
306
304 return _match
307 return _match
305
308
306 # Called by remotefilelog
309 # Called by remotefilelog
307 def wrapfilelog(filelog):
310 def wrapfilelog(filelog):
308 wrapfunction = extensions.wrapfunction
311 wrapfunction = extensions.wrapfunction
309
312
310 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
313 wrapfunction(filelog, 'addrevision', wrapper.filelogaddrevision)
311 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
314 wrapfunction(filelog, 'renamed', wrapper.filelogrenamed)
312 wrapfunction(filelog, 'size', wrapper.filelogsize)
315 wrapfunction(filelog, 'size', wrapper.filelogsize)
313
316
314 @eh.wrapfunction(localrepo, 'resolverevlogstorevfsoptions')
317 @eh.wrapfunction(localrepo, 'resolverevlogstorevfsoptions')
315 def _resolverevlogstorevfsoptions(orig, ui, requirements, features):
318 def _resolverevlogstorevfsoptions(orig, ui, requirements, features):
316 opts = orig(ui, requirements, features)
319 opts = orig(ui, requirements, features)
317 for name, module in extensions.extensions(ui):
320 for name, module in extensions.extensions(ui):
318 if module is sys.modules[__name__]:
321 if module is sys.modules[__name__]:
319 if revlog.REVIDX_EXTSTORED in opts[b'flagprocessors']:
322 if revlog.REVIDX_EXTSTORED in opts[b'flagprocessors']:
320 msg = (_(b"cannot register multiple processors on flag '%#x'.")
323 msg = (_(b"cannot register multiple processors on flag '%#x'.")
321 % revlog.REVIDX_EXTSTORED)
324 % revlog.REVIDX_EXTSTORED)
322 raise error.Abort(msg)
325 raise error.Abort(msg)
323
326
324 opts[b'flagprocessors'][revlog.REVIDX_EXTSTORED] = lfsprocessor
327 opts[b'flagprocessors'][revlog.REVIDX_EXTSTORED] = lfsprocessor
325 break
328 break
326
329
327 return opts
330 return opts
328
331
329 @eh.extsetup
332 @eh.extsetup
330 def _extsetup(ui):
333 def _extsetup(ui):
331 wrapfilelog(filelog.filelog)
334 wrapfilelog(filelog.filelog)
332
335
333 context.basefilectx.islfs = wrapper.filectxislfs
336 context.basefilectx.islfs = wrapper.filectxislfs
334
337
335 scmutil.fileprefetchhooks.add('lfs', wrapper._prefetchfiles)
338 scmutil.fileprefetchhooks.add('lfs', wrapper._prefetchfiles)
336
339
337 # Make bundle choose changegroup3 instead of changegroup2. This affects
340 # Make bundle choose changegroup3 instead of changegroup2. This affects
338 # "hg bundle" command. Note: it does not cover all bundle formats like
341 # "hg bundle" command. Note: it does not cover all bundle formats like
339 # "packed1". Using "packed1" with lfs will likely cause trouble.
342 # "packed1". Using "packed1" with lfs will likely cause trouble.
340 exchange._bundlespeccontentopts["v2"]["cg.version"] = "03"
343 exchange._bundlespeccontentopts["v2"]["cg.version"] = "03"
341
344
342 @eh.filesetpredicate('lfs()')
345 @eh.filesetpredicate('lfs()')
343 def lfsfileset(mctx, x):
346 def lfsfileset(mctx, x):
344 """File that uses LFS storage."""
347 """File that uses LFS storage."""
345 # i18n: "lfs" is a keyword
348 # i18n: "lfs" is a keyword
346 filesetlang.getargs(x, 0, 0, _("lfs takes no arguments"))
349 filesetlang.getargs(x, 0, 0, _("lfs takes no arguments"))
347 ctx = mctx.ctx
350 ctx = mctx.ctx
348 def lfsfilep(f):
351 def lfsfilep(f):
349 return wrapper.pointerfromctx(ctx, f, removed=True) is not None
352 return wrapper.pointerfromctx(ctx, f, removed=True) is not None
350 return mctx.predicate(lfsfilep, predrepr='<lfs>')
353 return mctx.predicate(lfsfilep, predrepr='<lfs>')
351
354
352 @eh.templatekeyword('lfs_files', requires={'ctx'})
355 @eh.templatekeyword('lfs_files', requires={'ctx'})
353 def lfsfiles(context, mapping):
356 def lfsfiles(context, mapping):
354 """List of strings. All files modified, added, or removed by this
357 """List of strings. All files modified, added, or removed by this
355 changeset."""
358 changeset."""
356 ctx = context.resource(mapping, 'ctx')
359 ctx = context.resource(mapping, 'ctx')
357
360
358 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
361 pointers = wrapper.pointersfromctx(ctx, removed=True) # {path: pointer}
359 files = sorted(pointers.keys())
362 files = sorted(pointers.keys())
360
363
361 def pointer(v):
364 def pointer(v):
362 # In the file spec, version is first and the other keys are sorted.
365 # In the file spec, version is first and the other keys are sorted.
363 sortkeyfunc = lambda x: (x[0] != 'version', x)
366 sortkeyfunc = lambda x: (x[0] != 'version', x)
364 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
367 items = sorted(pointers[v].iteritems(), key=sortkeyfunc)
365 return util.sortdict(items)
368 return util.sortdict(items)
366
369
367 makemap = lambda v: {
370 makemap = lambda v: {
368 'file': v,
371 'file': v,
369 'lfsoid': pointers[v].oid() if pointers[v] else None,
372 'lfsoid': pointers[v].oid() if pointers[v] else None,
370 'lfspointer': templateutil.hybriddict(pointer(v)),
373 'lfspointer': templateutil.hybriddict(pointer(v)),
371 }
374 }
372
375
373 # TODO: make the separator ', '?
376 # TODO: make the separator ', '?
374 f = templateutil._showcompatlist(context, mapping, 'lfs_file', files)
377 f = templateutil._showcompatlist(context, mapping, 'lfs_file', files)
375 return templateutil.hybrid(f, files, makemap, pycompat.identity)
378 return templateutil.hybrid(f, files, makemap, pycompat.identity)
376
379
377 @eh.command('debuglfsupload',
380 @eh.command('debuglfsupload',
378 [('r', 'rev', [], _('upload large files introduced by REV'))])
381 [('r', 'rev', [], _('upload large files introduced by REV'))])
379 def debuglfsupload(ui, repo, **opts):
382 def debuglfsupload(ui, repo, **opts):
380 """upload lfs blobs added by the working copy parent or given revisions"""
383 """upload lfs blobs added by the working copy parent or given revisions"""
381 revs = opts.get(r'rev', [])
384 revs = opts.get(r'rev', [])
382 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
385 pointers = wrapper.extractpointers(repo, scmutil.revrange(repo, revs))
383 wrapper.uploadblobs(repo, pointers)
386 wrapper.uploadblobs(repo, pointers)
@@ -1,446 +1,449
1 # wrapper.py - methods wrapping core mercurial logic
1 # wrapper.py - methods wrapping core mercurial logic
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import hashlib
10 import hashlib
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial.node import bin, hex, nullid, short
13 from mercurial.node import bin, hex, nullid, short
14
14
15 from mercurial import (
15 from mercurial import (
16 bundle2,
16 bundle2,
17 changegroup,
17 changegroup,
18 cmdutil,
18 cmdutil,
19 context,
19 context,
20 error,
20 error,
21 exchange,
21 exchange,
22 exthelper,
22 exthelper,
23 localrepo,
23 localrepo,
24 repository,
25 revlog,
24 revlog,
26 scmutil,
25 scmutil,
27 upgrade,
26 upgrade,
28 util,
27 util,
29 vfs as vfsmod,
28 vfs as vfsmod,
30 wireprotov1server,
29 wireprotov1server,
31 )
30 )
32
31
32 from mercurial.interfaces import (
33 repository,
34 )
35
33 from mercurial.utils import (
36 from mercurial.utils import (
34 storageutil,
37 storageutil,
35 stringutil,
38 stringutil,
36 )
39 )
37
40
38 from ..largefiles import lfutil
41 from ..largefiles import lfutil
39
42
40 from . import (
43 from . import (
41 blobstore,
44 blobstore,
42 pointer,
45 pointer,
43 )
46 )
44
47
45 eh = exthelper.exthelper()
48 eh = exthelper.exthelper()
46
49
47 @eh.wrapfunction(localrepo, 'makefilestorage')
50 @eh.wrapfunction(localrepo, 'makefilestorage')
48 def localrepomakefilestorage(orig, requirements, features, **kwargs):
51 def localrepomakefilestorage(orig, requirements, features, **kwargs):
49 if b'lfs' in requirements:
52 if b'lfs' in requirements:
50 features.add(repository.REPO_FEATURE_LFS)
53 features.add(repository.REPO_FEATURE_LFS)
51
54
52 return orig(requirements=requirements, features=features, **kwargs)
55 return orig(requirements=requirements, features=features, **kwargs)
53
56
54 @eh.wrapfunction(changegroup, 'allsupportedversions')
57 @eh.wrapfunction(changegroup, 'allsupportedversions')
55 def allsupportedversions(orig, ui):
58 def allsupportedversions(orig, ui):
56 versions = orig(ui)
59 versions = orig(ui)
57 versions.add('03')
60 versions.add('03')
58 return versions
61 return versions
59
62
60 @eh.wrapfunction(wireprotov1server, '_capabilities')
63 @eh.wrapfunction(wireprotov1server, '_capabilities')
61 def _capabilities(orig, repo, proto):
64 def _capabilities(orig, repo, proto):
62 '''Wrap server command to announce lfs server capability'''
65 '''Wrap server command to announce lfs server capability'''
63 caps = orig(repo, proto)
66 caps = orig(repo, proto)
64 if util.safehasattr(repo.svfs, 'lfslocalblobstore'):
67 if util.safehasattr(repo.svfs, 'lfslocalblobstore'):
65 # Advertise a slightly different capability when lfs is *required*, so
68 # Advertise a slightly different capability when lfs is *required*, so
66 # that the client knows it MUST load the extension. If lfs is not
69 # that the client knows it MUST load the extension. If lfs is not
67 # required on the server, there's no reason to autoload the extension
70 # required on the server, there's no reason to autoload the extension
68 # on the client.
71 # on the client.
69 if b'lfs' in repo.requirements:
72 if b'lfs' in repo.requirements:
70 caps.append('lfs-serve')
73 caps.append('lfs-serve')
71
74
72 caps.append('lfs')
75 caps.append('lfs')
73 return caps
76 return caps
74
77
75 def bypasscheckhash(self, text):
78 def bypasscheckhash(self, text):
76 return False
79 return False
77
80
78 def readfromstore(self, text):
81 def readfromstore(self, text):
79 """Read filelog content from local blobstore transform for flagprocessor.
82 """Read filelog content from local blobstore transform for flagprocessor.
80
83
81 Default tranform for flagprocessor, returning contents from blobstore.
84 Default tranform for flagprocessor, returning contents from blobstore.
82 Returns a 2-typle (text, validatehash) where validatehash is True as the
85 Returns a 2-typle (text, validatehash) where validatehash is True as the
83 contents of the blobstore should be checked using checkhash.
86 contents of the blobstore should be checked using checkhash.
84 """
87 """
85 p = pointer.deserialize(text)
88 p = pointer.deserialize(text)
86 oid = p.oid()
89 oid = p.oid()
87 store = self.opener.lfslocalblobstore
90 store = self.opener.lfslocalblobstore
88 if not store.has(oid):
91 if not store.has(oid):
89 p.filename = self.filename
92 p.filename = self.filename
90 self.opener.lfsremoteblobstore.readbatch([p], store)
93 self.opener.lfsremoteblobstore.readbatch([p], store)
91
94
92 # The caller will validate the content
95 # The caller will validate the content
93 text = store.read(oid, verify=False)
96 text = store.read(oid, verify=False)
94
97
95 # pack hg filelog metadata
98 # pack hg filelog metadata
96 hgmeta = {}
99 hgmeta = {}
97 for k in p.keys():
100 for k in p.keys():
98 if k.startswith('x-hg-'):
101 if k.startswith('x-hg-'):
99 name = k[len('x-hg-'):]
102 name = k[len('x-hg-'):]
100 hgmeta[name] = p[k]
103 hgmeta[name] = p[k]
101 if hgmeta or text.startswith('\1\n'):
104 if hgmeta or text.startswith('\1\n'):
102 text = storageutil.packmeta(hgmeta, text)
105 text = storageutil.packmeta(hgmeta, text)
103
106
104 return (text, True)
107 return (text, True)
105
108
106 def writetostore(self, text):
109 def writetostore(self, text):
107 # hg filelog metadata (includes rename, etc)
110 # hg filelog metadata (includes rename, etc)
108 hgmeta, offset = storageutil.parsemeta(text)
111 hgmeta, offset = storageutil.parsemeta(text)
109 if offset and offset > 0:
112 if offset and offset > 0:
110 # lfs blob does not contain hg filelog metadata
113 # lfs blob does not contain hg filelog metadata
111 text = text[offset:]
114 text = text[offset:]
112
115
113 # git-lfs only supports sha256
116 # git-lfs only supports sha256
114 oid = hex(hashlib.sha256(text).digest())
117 oid = hex(hashlib.sha256(text).digest())
115 self.opener.lfslocalblobstore.write(oid, text)
118 self.opener.lfslocalblobstore.write(oid, text)
116
119
117 # replace contents with metadata
120 # replace contents with metadata
118 longoid = 'sha256:%s' % oid
121 longoid = 'sha256:%s' % oid
119 metadata = pointer.gitlfspointer(oid=longoid, size='%d' % len(text))
122 metadata = pointer.gitlfspointer(oid=longoid, size='%d' % len(text))
120
123
121 # by default, we expect the content to be binary. however, LFS could also
124 # by default, we expect the content to be binary. however, LFS could also
122 # be used for non-binary content. add a special entry for non-binary data.
125 # be used for non-binary content. add a special entry for non-binary data.
123 # this will be used by filectx.isbinary().
126 # this will be used by filectx.isbinary().
124 if not stringutil.binary(text):
127 if not stringutil.binary(text):
125 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
128 # not hg filelog metadata (affecting commit hash), no "x-hg-" prefix
126 metadata['x-is-binary'] = '0'
129 metadata['x-is-binary'] = '0'
127
130
128 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
131 # translate hg filelog metadata to lfs metadata with "x-hg-" prefix
129 if hgmeta is not None:
132 if hgmeta is not None:
130 for k, v in hgmeta.iteritems():
133 for k, v in hgmeta.iteritems():
131 metadata['x-hg-%s' % k] = v
134 metadata['x-hg-%s' % k] = v
132
135
133 rawtext = metadata.serialize()
136 rawtext = metadata.serialize()
134 return (rawtext, False)
137 return (rawtext, False)
135
138
136 def _islfs(rlog, node=None, rev=None):
139 def _islfs(rlog, node=None, rev=None):
137 if rev is None:
140 if rev is None:
138 if node is None:
141 if node is None:
139 # both None - likely working copy content where node is not ready
142 # both None - likely working copy content where node is not ready
140 return False
143 return False
141 rev = rlog._revlog.rev(node)
144 rev = rlog._revlog.rev(node)
142 else:
145 else:
143 node = rlog._revlog.node(rev)
146 node = rlog._revlog.node(rev)
144 if node == nullid:
147 if node == nullid:
145 return False
148 return False
146 flags = rlog._revlog.flags(rev)
149 flags = rlog._revlog.flags(rev)
147 return bool(flags & revlog.REVIDX_EXTSTORED)
150 return bool(flags & revlog.REVIDX_EXTSTORED)
148
151
149 # Wrapping may also be applied by remotefilelog
152 # Wrapping may also be applied by remotefilelog
150 def filelogaddrevision(orig, self, text, transaction, link, p1, p2,
153 def filelogaddrevision(orig, self, text, transaction, link, p1, p2,
151 cachedelta=None, node=None,
154 cachedelta=None, node=None,
152 flags=revlog.REVIDX_DEFAULT_FLAGS, **kwds):
155 flags=revlog.REVIDX_DEFAULT_FLAGS, **kwds):
153 # The matcher isn't available if reposetup() wasn't called.
156 # The matcher isn't available if reposetup() wasn't called.
154 lfstrack = self._revlog.opener.options.get('lfstrack')
157 lfstrack = self._revlog.opener.options.get('lfstrack')
155
158
156 if lfstrack:
159 if lfstrack:
157 textlen = len(text)
160 textlen = len(text)
158 # exclude hg rename meta from file size
161 # exclude hg rename meta from file size
159 meta, offset = storageutil.parsemeta(text)
162 meta, offset = storageutil.parsemeta(text)
160 if offset:
163 if offset:
161 textlen -= offset
164 textlen -= offset
162
165
163 if lfstrack(self._revlog.filename, textlen):
166 if lfstrack(self._revlog.filename, textlen):
164 flags |= revlog.REVIDX_EXTSTORED
167 flags |= revlog.REVIDX_EXTSTORED
165
168
166 return orig(self, text, transaction, link, p1, p2, cachedelta=cachedelta,
169 return orig(self, text, transaction, link, p1, p2, cachedelta=cachedelta,
167 node=node, flags=flags, **kwds)
170 node=node, flags=flags, **kwds)
168
171
169 # Wrapping may also be applied by remotefilelog
172 # Wrapping may also be applied by remotefilelog
170 def filelogrenamed(orig, self, node):
173 def filelogrenamed(orig, self, node):
171 if _islfs(self, node):
174 if _islfs(self, node):
172 rawtext = self._revlog.rawdata(node)
175 rawtext = self._revlog.rawdata(node)
173 if not rawtext:
176 if not rawtext:
174 return False
177 return False
175 metadata = pointer.deserialize(rawtext)
178 metadata = pointer.deserialize(rawtext)
176 if 'x-hg-copy' in metadata and 'x-hg-copyrev' in metadata:
179 if 'x-hg-copy' in metadata and 'x-hg-copyrev' in metadata:
177 return metadata['x-hg-copy'], bin(metadata['x-hg-copyrev'])
180 return metadata['x-hg-copy'], bin(metadata['x-hg-copyrev'])
178 else:
181 else:
179 return False
182 return False
180 return orig(self, node)
183 return orig(self, node)
181
184
182 # Wrapping may also be applied by remotefilelog
185 # Wrapping may also be applied by remotefilelog
183 def filelogsize(orig, self, rev):
186 def filelogsize(orig, self, rev):
184 if _islfs(self, rev=rev):
187 if _islfs(self, rev=rev):
185 # fast path: use lfs metadata to answer size
188 # fast path: use lfs metadata to answer size
186 rawtext = self._revlog.rawdata(rev)
189 rawtext = self._revlog.rawdata(rev)
187 metadata = pointer.deserialize(rawtext)
190 metadata = pointer.deserialize(rawtext)
188 return int(metadata['size'])
191 return int(metadata['size'])
189 return orig(self, rev)
192 return orig(self, rev)
190
193
191 @eh.wrapfunction(context.basefilectx, 'cmp')
194 @eh.wrapfunction(context.basefilectx, 'cmp')
192 def filectxcmp(orig, self, fctx):
195 def filectxcmp(orig, self, fctx):
193 """returns True if text is different than fctx"""
196 """returns True if text is different than fctx"""
194 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
197 # some fctx (ex. hg-git) is not based on basefilectx and do not have islfs
195 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
198 if self.islfs() and getattr(fctx, 'islfs', lambda: False)():
196 # fast path: check LFS oid
199 # fast path: check LFS oid
197 p1 = pointer.deserialize(self.rawdata())
200 p1 = pointer.deserialize(self.rawdata())
198 p2 = pointer.deserialize(fctx.rawdata())
201 p2 = pointer.deserialize(fctx.rawdata())
199 return p1.oid() != p2.oid()
202 return p1.oid() != p2.oid()
200 return orig(self, fctx)
203 return orig(self, fctx)
201
204
202 @eh.wrapfunction(context.basefilectx, 'isbinary')
205 @eh.wrapfunction(context.basefilectx, 'isbinary')
203 def filectxisbinary(orig, self):
206 def filectxisbinary(orig, self):
204 if self.islfs():
207 if self.islfs():
205 # fast path: use lfs metadata to answer isbinary
208 # fast path: use lfs metadata to answer isbinary
206 metadata = pointer.deserialize(self.rawdata())
209 metadata = pointer.deserialize(self.rawdata())
207 # if lfs metadata says nothing, assume it's binary by default
210 # if lfs metadata says nothing, assume it's binary by default
208 return bool(int(metadata.get('x-is-binary', 1)))
211 return bool(int(metadata.get('x-is-binary', 1)))
209 return orig(self)
212 return orig(self)
210
213
211 def filectxislfs(self):
214 def filectxislfs(self):
212 return _islfs(self.filelog(), self.filenode())
215 return _islfs(self.filelog(), self.filenode())
213
216
214 @eh.wrapfunction(cmdutil, '_updatecatformatter')
217 @eh.wrapfunction(cmdutil, '_updatecatformatter')
215 def _updatecatformatter(orig, fm, ctx, matcher, path, decode):
218 def _updatecatformatter(orig, fm, ctx, matcher, path, decode):
216 orig(fm, ctx, matcher, path, decode)
219 orig(fm, ctx, matcher, path, decode)
217 fm.data(rawdata=ctx[path].rawdata())
220 fm.data(rawdata=ctx[path].rawdata())
218
221
219 @eh.wrapfunction(scmutil, 'wrapconvertsink')
222 @eh.wrapfunction(scmutil, 'wrapconvertsink')
220 def convertsink(orig, sink):
223 def convertsink(orig, sink):
221 sink = orig(sink)
224 sink = orig(sink)
222 if sink.repotype == 'hg':
225 if sink.repotype == 'hg':
223 class lfssink(sink.__class__):
226 class lfssink(sink.__class__):
224 def putcommit(self, files, copies, parents, commit, source, revmap,
227 def putcommit(self, files, copies, parents, commit, source, revmap,
225 full, cleanp2):
228 full, cleanp2):
226 pc = super(lfssink, self).putcommit
229 pc = super(lfssink, self).putcommit
227 node = pc(files, copies, parents, commit, source, revmap, full,
230 node = pc(files, copies, parents, commit, source, revmap, full,
228 cleanp2)
231 cleanp2)
229
232
230 if 'lfs' not in self.repo.requirements:
233 if 'lfs' not in self.repo.requirements:
231 ctx = self.repo[node]
234 ctx = self.repo[node]
232
235
233 # The file list may contain removed files, so check for
236 # The file list may contain removed files, so check for
234 # membership before assuming it is in the context.
237 # membership before assuming it is in the context.
235 if any(f in ctx and ctx[f].islfs() for f, n in files):
238 if any(f in ctx and ctx[f].islfs() for f, n in files):
236 self.repo.requirements.add('lfs')
239 self.repo.requirements.add('lfs')
237 self.repo._writerequirements()
240 self.repo._writerequirements()
238
241
239 return node
242 return node
240
243
241 sink.__class__ = lfssink
244 sink.__class__ = lfssink
242
245
243 return sink
246 return sink
244
247
245 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
248 # bundlerepo uses "vfsmod.readonlyvfs(othervfs)", we need to make sure lfs
246 # options and blob stores are passed from othervfs to the new readonlyvfs.
249 # options and blob stores are passed from othervfs to the new readonlyvfs.
247 @eh.wrapfunction(vfsmod.readonlyvfs, '__init__')
250 @eh.wrapfunction(vfsmod.readonlyvfs, '__init__')
248 def vfsinit(orig, self, othervfs):
251 def vfsinit(orig, self, othervfs):
249 orig(self, othervfs)
252 orig(self, othervfs)
250 # copy lfs related options
253 # copy lfs related options
251 for k, v in othervfs.options.items():
254 for k, v in othervfs.options.items():
252 if k.startswith('lfs'):
255 if k.startswith('lfs'):
253 self.options[k] = v
256 self.options[k] = v
254 # also copy lfs blobstores. note: this can run before reposetup, so lfs
257 # also copy lfs blobstores. note: this can run before reposetup, so lfs
255 # blobstore attributes are not always ready at this time.
258 # blobstore attributes are not always ready at this time.
256 for name in ['lfslocalblobstore', 'lfsremoteblobstore']:
259 for name in ['lfslocalblobstore', 'lfsremoteblobstore']:
257 if util.safehasattr(othervfs, name):
260 if util.safehasattr(othervfs, name):
258 setattr(self, name, getattr(othervfs, name))
261 setattr(self, name, getattr(othervfs, name))
259
262
260 def _prefetchfiles(repo, revs, match):
263 def _prefetchfiles(repo, revs, match):
261 """Ensure that required LFS blobs are present, fetching them as a group if
264 """Ensure that required LFS blobs are present, fetching them as a group if
262 needed."""
265 needed."""
263 if not util.safehasattr(repo.svfs, 'lfslocalblobstore'):
266 if not util.safehasattr(repo.svfs, 'lfslocalblobstore'):
264 return
267 return
265
268
266 pointers = []
269 pointers = []
267 oids = set()
270 oids = set()
268 localstore = repo.svfs.lfslocalblobstore
271 localstore = repo.svfs.lfslocalblobstore
269
272
270 for rev in revs:
273 for rev in revs:
271 ctx = repo[rev]
274 ctx = repo[rev]
272 for f in ctx.walk(match):
275 for f in ctx.walk(match):
273 p = pointerfromctx(ctx, f)
276 p = pointerfromctx(ctx, f)
274 if p and p.oid() not in oids and not localstore.has(p.oid()):
277 if p and p.oid() not in oids and not localstore.has(p.oid()):
275 p.filename = f
278 p.filename = f
276 pointers.append(p)
279 pointers.append(p)
277 oids.add(p.oid())
280 oids.add(p.oid())
278
281
279 if pointers:
282 if pointers:
280 # Recalculating the repo store here allows 'paths.default' that is set
283 # Recalculating the repo store here allows 'paths.default' that is set
281 # on the repo by a clone command to be used for the update.
284 # on the repo by a clone command to be used for the update.
282 blobstore.remote(repo).readbatch(pointers, localstore)
285 blobstore.remote(repo).readbatch(pointers, localstore)
283
286
284 def _canskipupload(repo):
287 def _canskipupload(repo):
285 # Skip if this hasn't been passed to reposetup()
288 # Skip if this hasn't been passed to reposetup()
286 if not util.safehasattr(repo.svfs, 'lfsremoteblobstore'):
289 if not util.safehasattr(repo.svfs, 'lfsremoteblobstore'):
287 return True
290 return True
288
291
289 # if remotestore is a null store, upload is a no-op and can be skipped
292 # if remotestore is a null store, upload is a no-op and can be skipped
290 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
293 return isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
291
294
292 def candownload(repo):
295 def candownload(repo):
293 # Skip if this hasn't been passed to reposetup()
296 # Skip if this hasn't been passed to reposetup()
294 if not util.safehasattr(repo.svfs, 'lfsremoteblobstore'):
297 if not util.safehasattr(repo.svfs, 'lfsremoteblobstore'):
295 return False
298 return False
296
299
297 # if remotestore is a null store, downloads will lead to nothing
300 # if remotestore is a null store, downloads will lead to nothing
298 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
301 return not isinstance(repo.svfs.lfsremoteblobstore, blobstore._nullremote)
299
302
300 def uploadblobsfromrevs(repo, revs):
303 def uploadblobsfromrevs(repo, revs):
301 '''upload lfs blobs introduced by revs
304 '''upload lfs blobs introduced by revs
302
305
303 Note: also used by other extensions e. g. infinitepush. avoid renaming.
306 Note: also used by other extensions e. g. infinitepush. avoid renaming.
304 '''
307 '''
305 if _canskipupload(repo):
308 if _canskipupload(repo):
306 return
309 return
307 pointers = extractpointers(repo, revs)
310 pointers = extractpointers(repo, revs)
308 uploadblobs(repo, pointers)
311 uploadblobs(repo, pointers)
309
312
310 def prepush(pushop):
313 def prepush(pushop):
311 """Prepush hook.
314 """Prepush hook.
312
315
313 Read through the revisions to push, looking for filelog entries that can be
316 Read through the revisions to push, looking for filelog entries that can be
314 deserialized into metadata so that we can block the push on their upload to
317 deserialized into metadata so that we can block the push on their upload to
315 the remote blobstore.
318 the remote blobstore.
316 """
319 """
317 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
320 return uploadblobsfromrevs(pushop.repo, pushop.outgoing.missing)
318
321
319 @eh.wrapfunction(exchange, 'push')
322 @eh.wrapfunction(exchange, 'push')
320 def push(orig, repo, remote, *args, **kwargs):
323 def push(orig, repo, remote, *args, **kwargs):
321 """bail on push if the extension isn't enabled on remote when needed, and
324 """bail on push if the extension isn't enabled on remote when needed, and
322 update the remote store based on the destination path."""
325 update the remote store based on the destination path."""
323 if 'lfs' in repo.requirements:
326 if 'lfs' in repo.requirements:
324 # If the remote peer is for a local repo, the requirement tests in the
327 # If the remote peer is for a local repo, the requirement tests in the
325 # base class method enforce lfs support. Otherwise, some revisions in
328 # base class method enforce lfs support. Otherwise, some revisions in
326 # this repo use lfs, and the remote repo needs the extension loaded.
329 # this repo use lfs, and the remote repo needs the extension loaded.
327 if not remote.local() and not remote.capable('lfs'):
330 if not remote.local() and not remote.capable('lfs'):
328 # This is a copy of the message in exchange.push() when requirements
331 # This is a copy of the message in exchange.push() when requirements
329 # are missing between local repos.
332 # are missing between local repos.
330 m = _("required features are not supported in the destination: %s")
333 m = _("required features are not supported in the destination: %s")
331 raise error.Abort(m % 'lfs',
334 raise error.Abort(m % 'lfs',
332 hint=_('enable the lfs extension on the server'))
335 hint=_('enable the lfs extension on the server'))
333
336
334 # Repositories where this extension is disabled won't have the field.
337 # Repositories where this extension is disabled won't have the field.
335 # But if there's a requirement, then the extension must be loaded AND
338 # But if there's a requirement, then the extension must be loaded AND
336 # there may be blobs to push.
339 # there may be blobs to push.
337 remotestore = repo.svfs.lfsremoteblobstore
340 remotestore = repo.svfs.lfsremoteblobstore
338 try:
341 try:
339 repo.svfs.lfsremoteblobstore = blobstore.remote(repo, remote.url())
342 repo.svfs.lfsremoteblobstore = blobstore.remote(repo, remote.url())
340 return orig(repo, remote, *args, **kwargs)
343 return orig(repo, remote, *args, **kwargs)
341 finally:
344 finally:
342 repo.svfs.lfsremoteblobstore = remotestore
345 repo.svfs.lfsremoteblobstore = remotestore
343 else:
346 else:
344 return orig(repo, remote, *args, **kwargs)
347 return orig(repo, remote, *args, **kwargs)
345
348
346 # when writing a bundle via "hg bundle" command, upload related LFS blobs
349 # when writing a bundle via "hg bundle" command, upload related LFS blobs
347 @eh.wrapfunction(bundle2, 'writenewbundle')
350 @eh.wrapfunction(bundle2, 'writenewbundle')
348 def writenewbundle(orig, ui, repo, source, filename, bundletype, outgoing,
351 def writenewbundle(orig, ui, repo, source, filename, bundletype, outgoing,
349 *args, **kwargs):
352 *args, **kwargs):
350 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
353 """upload LFS blobs added by outgoing revisions on 'hg bundle'"""
351 uploadblobsfromrevs(repo, outgoing.missing)
354 uploadblobsfromrevs(repo, outgoing.missing)
352 return orig(ui, repo, source, filename, bundletype, outgoing, *args,
355 return orig(ui, repo, source, filename, bundletype, outgoing, *args,
353 **kwargs)
356 **kwargs)
354
357
355 def extractpointers(repo, revs):
358 def extractpointers(repo, revs):
356 """return a list of lfs pointers added by given revs"""
359 """return a list of lfs pointers added by given revs"""
357 repo.ui.debug('lfs: computing set of blobs to upload\n')
360 repo.ui.debug('lfs: computing set of blobs to upload\n')
358 pointers = {}
361 pointers = {}
359
362
360 makeprogress = repo.ui.makeprogress
363 makeprogress = repo.ui.makeprogress
361 with makeprogress(_('lfs search'), _('changesets'), len(revs)) as progress:
364 with makeprogress(_('lfs search'), _('changesets'), len(revs)) as progress:
362 for r in revs:
365 for r in revs:
363 ctx = repo[r]
366 ctx = repo[r]
364 for p in pointersfromctx(ctx).values():
367 for p in pointersfromctx(ctx).values():
365 pointers[p.oid()] = p
368 pointers[p.oid()] = p
366 progress.increment()
369 progress.increment()
367 return sorted(pointers.values(), key=lambda p: p.oid())
370 return sorted(pointers.values(), key=lambda p: p.oid())
368
371
369 def pointerfromctx(ctx, f, removed=False):
372 def pointerfromctx(ctx, f, removed=False):
370 """return a pointer for the named file from the given changectx, or None if
373 """return a pointer for the named file from the given changectx, or None if
371 the file isn't LFS.
374 the file isn't LFS.
372
375
373 Optionally, the pointer for a file deleted from the context can be returned.
376 Optionally, the pointer for a file deleted from the context can be returned.
374 Since no such pointer is actually stored, and to distinguish from a non LFS
377 Since no such pointer is actually stored, and to distinguish from a non LFS
375 file, this pointer is represented by an empty dict.
378 file, this pointer is represented by an empty dict.
376 """
379 """
377 _ctx = ctx
380 _ctx = ctx
378 if f not in ctx:
381 if f not in ctx:
379 if not removed:
382 if not removed:
380 return None
383 return None
381 if f in ctx.p1():
384 if f in ctx.p1():
382 _ctx = ctx.p1()
385 _ctx = ctx.p1()
383 elif f in ctx.p2():
386 elif f in ctx.p2():
384 _ctx = ctx.p2()
387 _ctx = ctx.p2()
385 else:
388 else:
386 return None
389 return None
387 fctx = _ctx[f]
390 fctx = _ctx[f]
388 if not _islfs(fctx.filelog(), fctx.filenode()):
391 if not _islfs(fctx.filelog(), fctx.filenode()):
389 return None
392 return None
390 try:
393 try:
391 p = pointer.deserialize(fctx.rawdata())
394 p = pointer.deserialize(fctx.rawdata())
392 if ctx == _ctx:
395 if ctx == _ctx:
393 return p
396 return p
394 return {}
397 return {}
395 except pointer.InvalidPointer as ex:
398 except pointer.InvalidPointer as ex:
396 raise error.Abort(_('lfs: corrupted pointer (%s@%s): %s\n')
399 raise error.Abort(_('lfs: corrupted pointer (%s@%s): %s\n')
397 % (f, short(_ctx.node()), ex))
400 % (f, short(_ctx.node()), ex))
398
401
399 def pointersfromctx(ctx, removed=False):
402 def pointersfromctx(ctx, removed=False):
400 """return a dict {path: pointer} for given single changectx.
403 """return a dict {path: pointer} for given single changectx.
401
404
402 If ``removed`` == True and the LFS file was removed from ``ctx``, the value
405 If ``removed`` == True and the LFS file was removed from ``ctx``, the value
403 stored for the path is an empty dict.
406 stored for the path is an empty dict.
404 """
407 """
405 result = {}
408 result = {}
406 m = ctx.repo().narrowmatch()
409 m = ctx.repo().narrowmatch()
407
410
408 # TODO: consider manifest.fastread() instead
411 # TODO: consider manifest.fastread() instead
409 for f in ctx.files():
412 for f in ctx.files():
410 if not m(f):
413 if not m(f):
411 continue
414 continue
412 p = pointerfromctx(ctx, f, removed=removed)
415 p = pointerfromctx(ctx, f, removed=removed)
413 if p is not None:
416 if p is not None:
414 result[f] = p
417 result[f] = p
415 return result
418 return result
416
419
417 def uploadblobs(repo, pointers):
420 def uploadblobs(repo, pointers):
418 """upload given pointers from local blobstore"""
421 """upload given pointers from local blobstore"""
419 if not pointers:
422 if not pointers:
420 return
423 return
421
424
422 remoteblob = repo.svfs.lfsremoteblobstore
425 remoteblob = repo.svfs.lfsremoteblobstore
423 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
426 remoteblob.writebatch(pointers, repo.svfs.lfslocalblobstore)
424
427
425 @eh.wrapfunction(upgrade, '_finishdatamigration')
428 @eh.wrapfunction(upgrade, '_finishdatamigration')
426 def upgradefinishdatamigration(orig, ui, srcrepo, dstrepo, requirements):
429 def upgradefinishdatamigration(orig, ui, srcrepo, dstrepo, requirements):
427 orig(ui, srcrepo, dstrepo, requirements)
430 orig(ui, srcrepo, dstrepo, requirements)
428
431
429 # Skip if this hasn't been passed to reposetup()
432 # Skip if this hasn't been passed to reposetup()
430 if (util.safehasattr(srcrepo.svfs, 'lfslocalblobstore') and
433 if (util.safehasattr(srcrepo.svfs, 'lfslocalblobstore') and
431 util.safehasattr(dstrepo.svfs, 'lfslocalblobstore')):
434 util.safehasattr(dstrepo.svfs, 'lfslocalblobstore')):
432 srclfsvfs = srcrepo.svfs.lfslocalblobstore.vfs
435 srclfsvfs = srcrepo.svfs.lfslocalblobstore.vfs
433 dstlfsvfs = dstrepo.svfs.lfslocalblobstore.vfs
436 dstlfsvfs = dstrepo.svfs.lfslocalblobstore.vfs
434
437
435 for dirpath, dirs, files in srclfsvfs.walk():
438 for dirpath, dirs, files in srclfsvfs.walk():
436 for oid in files:
439 for oid in files:
437 ui.write(_('copying lfs blob %s\n') % oid)
440 ui.write(_('copying lfs blob %s\n') % oid)
438 lfutil.link(srclfsvfs.join(oid), dstlfsvfs.join(oid))
441 lfutil.link(srclfsvfs.join(oid), dstlfsvfs.join(oid))
439
442
440 @eh.wrapfunction(upgrade, 'preservedrequirements')
443 @eh.wrapfunction(upgrade, 'preservedrequirements')
441 @eh.wrapfunction(upgrade, 'supporteddestrequirements')
444 @eh.wrapfunction(upgrade, 'supporteddestrequirements')
442 def upgraderequirements(orig, repo):
445 def upgraderequirements(orig, repo):
443 reqs = orig(repo)
446 reqs = orig(repo)
444 if 'lfs' in repo.requirements:
447 if 'lfs' in repo.requirements:
445 reqs.add('lfs')
448 reqs.add('lfs')
446 return reqs
449 return reqs
@@ -1,71 +1,74
1 # __init__.py - narrowhg extension
1 # __init__.py - narrowhg extension
2 #
2 #
3 # Copyright 2017 Google, Inc.
3 # Copyright 2017 Google, Inc.
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 '''create clones which fetch history data for subset of files (EXPERIMENTAL)'''
7 '''create clones which fetch history data for subset of files (EXPERIMENTAL)'''
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
11 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
12 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
12 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
13 # be specifying the version(s) of Mercurial they are tested with, or
13 # be specifying the version(s) of Mercurial they are tested with, or
14 # leave the attribute unspecified.
14 # leave the attribute unspecified.
15 testedwith = 'ships-with-hg-core'
15 testedwith = 'ships-with-hg-core'
16
16
17 from mercurial import (
17 from mercurial import (
18 localrepo,
18 localrepo,
19 registrar,
19 registrar,
20 )
21
22 from mercurial.interfaces import (
20 repository,
23 repository,
21 )
24 )
22
25
23 from . import (
26 from . import (
24 narrowbundle2,
27 narrowbundle2,
25 narrowcommands,
28 narrowcommands,
26 narrowrepo,
29 narrowrepo,
27 narrowtemplates,
30 narrowtemplates,
28 narrowwirepeer,
31 narrowwirepeer,
29 )
32 )
30
33
31 configtable = {}
34 configtable = {}
32 configitem = registrar.configitem(configtable)
35 configitem = registrar.configitem(configtable)
33 # Narrowhg *has* support for serving ellipsis nodes (which are used at
36 # Narrowhg *has* support for serving ellipsis nodes (which are used at
34 # least by Google's internal server), but that support is pretty
37 # least by Google's internal server), but that support is pretty
35 # fragile and has a lot of problems on real-world repositories that
38 # fragile and has a lot of problems on real-world repositories that
36 # have complex graph topologies. This could probably be corrected, but
39 # have complex graph topologies. This could probably be corrected, but
37 # absent someone needing the full support for ellipsis nodes in
40 # absent someone needing the full support for ellipsis nodes in
38 # repositories with merges, it's unlikely this work will get done. As
41 # repositories with merges, it's unlikely this work will get done. As
39 # of this writining in late 2017, all repositories large enough for
42 # of this writining in late 2017, all repositories large enough for
40 # ellipsis nodes to be a hard requirement also enforce strictly linear
43 # ellipsis nodes to be a hard requirement also enforce strictly linear
41 # history for other scaling reasons.
44 # history for other scaling reasons.
42 configitem('experimental', 'narrowservebrokenellipses',
45 configitem('experimental', 'narrowservebrokenellipses',
43 default=False,
46 default=False,
44 alias=[('narrow', 'serveellipses')],
47 alias=[('narrow', 'serveellipses')],
45 )
48 )
46
49
47 # Export the commands table for Mercurial to see.
50 # Export the commands table for Mercurial to see.
48 cmdtable = narrowcommands.table
51 cmdtable = narrowcommands.table
49
52
50 def featuresetup(ui, features):
53 def featuresetup(ui, features):
51 features.add(repository.NARROW_REQUIREMENT)
54 features.add(repository.NARROW_REQUIREMENT)
52
55
53 def uisetup(ui):
56 def uisetup(ui):
54 """Wraps user-facing mercurial commands with narrow-aware versions."""
57 """Wraps user-facing mercurial commands with narrow-aware versions."""
55 localrepo.featuresetupfuncs.add(featuresetup)
58 localrepo.featuresetupfuncs.add(featuresetup)
56 narrowbundle2.setup()
59 narrowbundle2.setup()
57 narrowcommands.setup()
60 narrowcommands.setup()
58 narrowwirepeer.uisetup()
61 narrowwirepeer.uisetup()
59
62
60 def reposetup(ui, repo):
63 def reposetup(ui, repo):
61 """Wraps local repositories with narrow repo support."""
64 """Wraps local repositories with narrow repo support."""
62 if not repo.local():
65 if not repo.local():
63 return
66 return
64
67
65 repo.ui.setconfig('experimental', 'narrow', True, 'narrow-ext')
68 repo.ui.setconfig('experimental', 'narrow', True, 'narrow-ext')
66 if repository.NARROW_REQUIREMENT in repo.requirements:
69 if repository.NARROW_REQUIREMENT in repo.requirements:
67 narrowrepo.wraprepo(repo)
70 narrowrepo.wraprepo(repo)
68 narrowwirepeer.reposetup(repo)
71 narrowwirepeer.reposetup(repo)
69
72
70 templatekeyword = narrowtemplates.templatekeyword
73 templatekeyword = narrowtemplates.templatekeyword
71 revsetpredicate = narrowtemplates.revsetpredicate
74 revsetpredicate = narrowtemplates.revsetpredicate
@@ -1,301 +1,303
1 # narrowbundle2.py - bundle2 extensions for narrow repository support
1 # narrowbundle2.py - bundle2 extensions for narrow repository support
2 #
2 #
3 # Copyright 2017 Google, Inc.
3 # Copyright 2017 Google, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import struct
11 import struct
12
12
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial.node import (
14 from mercurial.node import (
15 bin,
15 bin,
16 nullid,
16 nullid,
17 )
17 )
18 from mercurial import (
18 from mercurial import (
19 bundle2,
19 bundle2,
20 changegroup,
20 changegroup,
21 error,
21 error,
22 exchange,
22 exchange,
23 localrepo,
23 localrepo,
24 narrowspec,
24 narrowspec,
25 repair,
25 repair,
26 repository,
27 util,
26 util,
28 wireprototypes,
27 wireprototypes,
29 )
28 )
29 from mercurial.interfaces import (
30 repository,
31 )
30 from mercurial.utils import (
32 from mercurial.utils import (
31 stringutil,
33 stringutil,
32 )
34 )
33
35
34 _NARROWACL_SECTION = 'narrowacl'
36 _NARROWACL_SECTION = 'narrowacl'
35 _CHANGESPECPART = 'narrow:changespec'
37 _CHANGESPECPART = 'narrow:changespec'
36 _RESSPECS = 'narrow:responsespec'
38 _RESSPECS = 'narrow:responsespec'
37 _SPECPART = 'narrow:spec'
39 _SPECPART = 'narrow:spec'
38 _SPECPART_INCLUDE = 'include'
40 _SPECPART_INCLUDE = 'include'
39 _SPECPART_EXCLUDE = 'exclude'
41 _SPECPART_EXCLUDE = 'exclude'
40 _KILLNODESIGNAL = 'KILL'
42 _KILLNODESIGNAL = 'KILL'
41 _DONESIGNAL = 'DONE'
43 _DONESIGNAL = 'DONE'
42 _ELIDEDCSHEADER = '>20s20s20sl' # cset id, p1, p2, len(text)
44 _ELIDEDCSHEADER = '>20s20s20sl' # cset id, p1, p2, len(text)
43 _ELIDEDMFHEADER = '>20s20s20s20sl' # manifest id, p1, p2, link id, len(text)
45 _ELIDEDMFHEADER = '>20s20s20s20sl' # manifest id, p1, p2, link id, len(text)
44 _CSHEADERSIZE = struct.calcsize(_ELIDEDCSHEADER)
46 _CSHEADERSIZE = struct.calcsize(_ELIDEDCSHEADER)
45 _MFHEADERSIZE = struct.calcsize(_ELIDEDMFHEADER)
47 _MFHEADERSIZE = struct.calcsize(_ELIDEDMFHEADER)
46
48
47 # Serve a changegroup for a client with a narrow clone.
49 # Serve a changegroup for a client with a narrow clone.
48 def getbundlechangegrouppart_narrow(bundler, repo, source,
50 def getbundlechangegrouppart_narrow(bundler, repo, source,
49 bundlecaps=None, b2caps=None, heads=None,
51 bundlecaps=None, b2caps=None, heads=None,
50 common=None, **kwargs):
52 common=None, **kwargs):
51 assert repo.ui.configbool('experimental', 'narrowservebrokenellipses')
53 assert repo.ui.configbool('experimental', 'narrowservebrokenellipses')
52
54
53 cgversions = b2caps.get('changegroup')
55 cgversions = b2caps.get('changegroup')
54 cgversions = [v for v in cgversions
56 cgversions = [v for v in cgversions
55 if v in changegroup.supportedoutgoingversions(repo)]
57 if v in changegroup.supportedoutgoingversions(repo)]
56 if not cgversions:
58 if not cgversions:
57 raise ValueError(_('no common changegroup version'))
59 raise ValueError(_('no common changegroup version'))
58 version = max(cgversions)
60 version = max(cgversions)
59
61
60 oldinclude = sorted(filter(bool, kwargs.get(r'oldincludepats', [])))
62 oldinclude = sorted(filter(bool, kwargs.get(r'oldincludepats', [])))
61 oldexclude = sorted(filter(bool, kwargs.get(r'oldexcludepats', [])))
63 oldexclude = sorted(filter(bool, kwargs.get(r'oldexcludepats', [])))
62 newinclude = sorted(filter(bool, kwargs.get(r'includepats', [])))
64 newinclude = sorted(filter(bool, kwargs.get(r'includepats', [])))
63 newexclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
65 newexclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
64 known = {bin(n) for n in kwargs.get(r'known', [])}
66 known = {bin(n) for n in kwargs.get(r'known', [])}
65 generateellipsesbundle2(bundler, repo, oldinclude, oldexclude, newinclude,
67 generateellipsesbundle2(bundler, repo, oldinclude, oldexclude, newinclude,
66 newexclude, version, common, heads, known,
68 newexclude, version, common, heads, known,
67 kwargs.get(r'depth', None))
69 kwargs.get(r'depth', None))
68
70
69 def generateellipsesbundle2(bundler, repo, oldinclude, oldexclude, newinclude,
71 def generateellipsesbundle2(bundler, repo, oldinclude, oldexclude, newinclude,
70 newexclude, version, common, heads, known, depth):
72 newexclude, version, common, heads, known, depth):
71 newmatch = narrowspec.match(repo.root, include=newinclude,
73 newmatch = narrowspec.match(repo.root, include=newinclude,
72 exclude=newexclude)
74 exclude=newexclude)
73 if depth is not None:
75 if depth is not None:
74 depth = int(depth)
76 depth = int(depth)
75 if depth < 1:
77 if depth < 1:
76 raise error.Abort(_('depth must be positive, got %d') % depth)
78 raise error.Abort(_('depth must be positive, got %d') % depth)
77
79
78 heads = set(heads or repo.heads())
80 heads = set(heads or repo.heads())
79 common = set(common or [nullid])
81 common = set(common or [nullid])
80 if known and (oldinclude != newinclude or oldexclude != newexclude):
82 if known and (oldinclude != newinclude or oldexclude != newexclude):
81 # Steps:
83 # Steps:
82 # 1. Send kill for "$known & ::common"
84 # 1. Send kill for "$known & ::common"
83 #
85 #
84 # 2. Send changegroup for ::common
86 # 2. Send changegroup for ::common
85 #
87 #
86 # 3. Proceed.
88 # 3. Proceed.
87 #
89 #
88 # In the future, we can send kills for only the specific
90 # In the future, we can send kills for only the specific
89 # nodes we know should go away or change shape, and then
91 # nodes we know should go away or change shape, and then
90 # send a data stream that tells the client something like this:
92 # send a data stream that tells the client something like this:
91 #
93 #
92 # a) apply this changegroup
94 # a) apply this changegroup
93 # b) apply nodes XXX, YYY, ZZZ that you already have
95 # b) apply nodes XXX, YYY, ZZZ that you already have
94 # c) goto a
96 # c) goto a
95 #
97 #
96 # until they've built up the full new state.
98 # until they've built up the full new state.
97 # Convert to revnums and intersect with "common". The client should
99 # Convert to revnums and intersect with "common". The client should
98 # have made it a subset of "common" already, but let's be safe.
100 # have made it a subset of "common" already, but let's be safe.
99 known = set(repo.revs("%ln & ::%ln", known, common))
101 known = set(repo.revs("%ln & ::%ln", known, common))
100 # TODO: we could send only roots() of this set, and the
102 # TODO: we could send only roots() of this set, and the
101 # list of nodes in common, and the client could work out
103 # list of nodes in common, and the client could work out
102 # what to strip, instead of us explicitly sending every
104 # what to strip, instead of us explicitly sending every
103 # single node.
105 # single node.
104 deadrevs = known
106 deadrevs = known
105 def genkills():
107 def genkills():
106 for r in deadrevs:
108 for r in deadrevs:
107 yield _KILLNODESIGNAL
109 yield _KILLNODESIGNAL
108 yield repo.changelog.node(r)
110 yield repo.changelog.node(r)
109 yield _DONESIGNAL
111 yield _DONESIGNAL
110 bundler.newpart(_CHANGESPECPART, data=genkills())
112 bundler.newpart(_CHANGESPECPART, data=genkills())
111 newvisit, newfull, newellipsis = exchange._computeellipsis(
113 newvisit, newfull, newellipsis = exchange._computeellipsis(
112 repo, set(), common, known, newmatch)
114 repo, set(), common, known, newmatch)
113 if newvisit:
115 if newvisit:
114 packer = changegroup.getbundler(version, repo,
116 packer = changegroup.getbundler(version, repo,
115 matcher=newmatch,
117 matcher=newmatch,
116 ellipses=True,
118 ellipses=True,
117 shallow=depth is not None,
119 shallow=depth is not None,
118 ellipsisroots=newellipsis,
120 ellipsisroots=newellipsis,
119 fullnodes=newfull)
121 fullnodes=newfull)
120 cgdata = packer.generate(common, newvisit, False, 'narrow_widen')
122 cgdata = packer.generate(common, newvisit, False, 'narrow_widen')
121
123
122 part = bundler.newpart('changegroup', data=cgdata)
124 part = bundler.newpart('changegroup', data=cgdata)
123 part.addparam('version', version)
125 part.addparam('version', version)
124 if 'treemanifest' in repo.requirements:
126 if 'treemanifest' in repo.requirements:
125 part.addparam('treemanifest', '1')
127 part.addparam('treemanifest', '1')
126
128
127 visitnodes, relevant_nodes, ellipsisroots = exchange._computeellipsis(
129 visitnodes, relevant_nodes, ellipsisroots = exchange._computeellipsis(
128 repo, common, heads, set(), newmatch, depth=depth)
130 repo, common, heads, set(), newmatch, depth=depth)
129
131
130 repo.ui.debug('Found %d relevant revs\n' % len(relevant_nodes))
132 repo.ui.debug('Found %d relevant revs\n' % len(relevant_nodes))
131 if visitnodes:
133 if visitnodes:
132 packer = changegroup.getbundler(version, repo,
134 packer = changegroup.getbundler(version, repo,
133 matcher=newmatch,
135 matcher=newmatch,
134 ellipses=True,
136 ellipses=True,
135 shallow=depth is not None,
137 shallow=depth is not None,
136 ellipsisroots=ellipsisroots,
138 ellipsisroots=ellipsisroots,
137 fullnodes=relevant_nodes)
139 fullnodes=relevant_nodes)
138 cgdata = packer.generate(common, visitnodes, False, 'narrow_widen')
140 cgdata = packer.generate(common, visitnodes, False, 'narrow_widen')
139
141
140 part = bundler.newpart('changegroup', data=cgdata)
142 part = bundler.newpart('changegroup', data=cgdata)
141 part.addparam('version', version)
143 part.addparam('version', version)
142 if 'treemanifest' in repo.requirements:
144 if 'treemanifest' in repo.requirements:
143 part.addparam('treemanifest', '1')
145 part.addparam('treemanifest', '1')
144
146
145 @bundle2.parthandler(_SPECPART, (_SPECPART_INCLUDE, _SPECPART_EXCLUDE))
147 @bundle2.parthandler(_SPECPART, (_SPECPART_INCLUDE, _SPECPART_EXCLUDE))
146 def _handlechangespec_2(op, inpart):
148 def _handlechangespec_2(op, inpart):
147 # XXX: This bundle2 handling is buggy and should be removed after hg5.2 is
149 # XXX: This bundle2 handling is buggy and should be removed after hg5.2 is
148 # released. New servers will send a mandatory bundle2 part named
150 # released. New servers will send a mandatory bundle2 part named
149 # 'Narrowspec' and will send specs as data instead of params.
151 # 'Narrowspec' and will send specs as data instead of params.
150 # Refer to issue5952 and 6019
152 # Refer to issue5952 and 6019
151 includepats = set(inpart.params.get(_SPECPART_INCLUDE, '').splitlines())
153 includepats = set(inpart.params.get(_SPECPART_INCLUDE, '').splitlines())
152 excludepats = set(inpart.params.get(_SPECPART_EXCLUDE, '').splitlines())
154 excludepats = set(inpart.params.get(_SPECPART_EXCLUDE, '').splitlines())
153 narrowspec.validatepatterns(includepats)
155 narrowspec.validatepatterns(includepats)
154 narrowspec.validatepatterns(excludepats)
156 narrowspec.validatepatterns(excludepats)
155
157
156 if not repository.NARROW_REQUIREMENT in op.repo.requirements:
158 if not repository.NARROW_REQUIREMENT in op.repo.requirements:
157 op.repo.requirements.add(repository.NARROW_REQUIREMENT)
159 op.repo.requirements.add(repository.NARROW_REQUIREMENT)
158 op.repo._writerequirements()
160 op.repo._writerequirements()
159 op.repo.setnarrowpats(includepats, excludepats)
161 op.repo.setnarrowpats(includepats, excludepats)
160 narrowspec.copytoworkingcopy(op.repo)
162 narrowspec.copytoworkingcopy(op.repo)
161
163
162 @bundle2.parthandler(_RESSPECS)
164 @bundle2.parthandler(_RESSPECS)
163 def _handlenarrowspecs(op, inpart):
165 def _handlenarrowspecs(op, inpart):
164 data = inpart.read()
166 data = inpart.read()
165 inc, exc = data.split('\0')
167 inc, exc = data.split('\0')
166 includepats = set(inc.splitlines())
168 includepats = set(inc.splitlines())
167 excludepats = set(exc.splitlines())
169 excludepats = set(exc.splitlines())
168 narrowspec.validatepatterns(includepats)
170 narrowspec.validatepatterns(includepats)
169 narrowspec.validatepatterns(excludepats)
171 narrowspec.validatepatterns(excludepats)
170
172
171 if repository.NARROW_REQUIREMENT not in op.repo.requirements:
173 if repository.NARROW_REQUIREMENT not in op.repo.requirements:
172 op.repo.requirements.add(repository.NARROW_REQUIREMENT)
174 op.repo.requirements.add(repository.NARROW_REQUIREMENT)
173 op.repo._writerequirements()
175 op.repo._writerequirements()
174 op.repo.setnarrowpats(includepats, excludepats)
176 op.repo.setnarrowpats(includepats, excludepats)
175 narrowspec.copytoworkingcopy(op.repo)
177 narrowspec.copytoworkingcopy(op.repo)
176
178
177 @bundle2.parthandler(_CHANGESPECPART)
179 @bundle2.parthandler(_CHANGESPECPART)
178 def _handlechangespec(op, inpart):
180 def _handlechangespec(op, inpart):
179 repo = op.repo
181 repo = op.repo
180 cl = repo.changelog
182 cl = repo.changelog
181
183
182 # changesets which need to be stripped entirely. either they're no longer
184 # changesets which need to be stripped entirely. either they're no longer
183 # needed in the new narrow spec, or the server is sending a replacement
185 # needed in the new narrow spec, or the server is sending a replacement
184 # in the changegroup part.
186 # in the changegroup part.
185 clkills = set()
187 clkills = set()
186
188
187 # A changespec part contains all the updates to ellipsis nodes
189 # A changespec part contains all the updates to ellipsis nodes
188 # that will happen as a result of widening or narrowing a
190 # that will happen as a result of widening or narrowing a
189 # repo. All the changes that this block encounters are ellipsis
191 # repo. All the changes that this block encounters are ellipsis
190 # nodes or flags to kill an existing ellipsis.
192 # nodes or flags to kill an existing ellipsis.
191 chunksignal = changegroup.readexactly(inpart, 4)
193 chunksignal = changegroup.readexactly(inpart, 4)
192 while chunksignal != _DONESIGNAL:
194 while chunksignal != _DONESIGNAL:
193 if chunksignal == _KILLNODESIGNAL:
195 if chunksignal == _KILLNODESIGNAL:
194 # a node used to be an ellipsis but isn't anymore
196 # a node used to be an ellipsis but isn't anymore
195 ck = changegroup.readexactly(inpart, 20)
197 ck = changegroup.readexactly(inpart, 20)
196 if cl.hasnode(ck):
198 if cl.hasnode(ck):
197 clkills.add(ck)
199 clkills.add(ck)
198 else:
200 else:
199 raise error.Abort(
201 raise error.Abort(
200 _('unexpected changespec node chunk type: %s') % chunksignal)
202 _('unexpected changespec node chunk type: %s') % chunksignal)
201 chunksignal = changegroup.readexactly(inpart, 4)
203 chunksignal = changegroup.readexactly(inpart, 4)
202
204
203 if clkills:
205 if clkills:
204 # preserve bookmarks that repair.strip() would otherwise strip
206 # preserve bookmarks that repair.strip() would otherwise strip
205 op._bookmarksbackup = repo._bookmarks
207 op._bookmarksbackup = repo._bookmarks
206 class dummybmstore(dict):
208 class dummybmstore(dict):
207 def applychanges(self, repo, tr, changes):
209 def applychanges(self, repo, tr, changes):
208 pass
210 pass
209 localrepo.localrepository._bookmarks.set(repo, dummybmstore())
211 localrepo.localrepository._bookmarks.set(repo, dummybmstore())
210 chgrpfile = repair.strip(op.ui, repo, list(clkills), backup=True,
212 chgrpfile = repair.strip(op.ui, repo, list(clkills), backup=True,
211 topic='widen')
213 topic='widen')
212 if chgrpfile:
214 if chgrpfile:
213 op._widen_uninterr = repo.ui.uninterruptible()
215 op._widen_uninterr = repo.ui.uninterruptible()
214 op._widen_uninterr.__enter__()
216 op._widen_uninterr.__enter__()
215 # presence of _widen_bundle attribute activates widen handler later
217 # presence of _widen_bundle attribute activates widen handler later
216 op._widen_bundle = chgrpfile
218 op._widen_bundle = chgrpfile
217 # Set the new narrowspec if we're widening. The setnewnarrowpats() method
219 # Set the new narrowspec if we're widening. The setnewnarrowpats() method
218 # will currently always be there when using the core+narrowhg server, but
220 # will currently always be there when using the core+narrowhg server, but
219 # other servers may include a changespec part even when not widening (e.g.
221 # other servers may include a changespec part even when not widening (e.g.
220 # because we're deepening a shallow repo).
222 # because we're deepening a shallow repo).
221 if util.safehasattr(repo, 'setnewnarrowpats'):
223 if util.safehasattr(repo, 'setnewnarrowpats'):
222 repo.setnewnarrowpats()
224 repo.setnewnarrowpats()
223
225
224 def handlechangegroup_widen(op, inpart):
226 def handlechangegroup_widen(op, inpart):
225 """Changegroup exchange handler which restores temporarily-stripped nodes"""
227 """Changegroup exchange handler which restores temporarily-stripped nodes"""
226 # We saved a bundle with stripped node data we must now restore.
228 # We saved a bundle with stripped node data we must now restore.
227 # This approach is based on mercurial/repair.py@6ee26a53c111.
229 # This approach is based on mercurial/repair.py@6ee26a53c111.
228 repo = op.repo
230 repo = op.repo
229 ui = op.ui
231 ui = op.ui
230
232
231 chgrpfile = op._widen_bundle
233 chgrpfile = op._widen_bundle
232 del op._widen_bundle
234 del op._widen_bundle
233 vfs = repo.vfs
235 vfs = repo.vfs
234
236
235 ui.note(_("adding branch\n"))
237 ui.note(_("adding branch\n"))
236 f = vfs.open(chgrpfile, "rb")
238 f = vfs.open(chgrpfile, "rb")
237 try:
239 try:
238 gen = exchange.readbundle(ui, f, chgrpfile, vfs)
240 gen = exchange.readbundle(ui, f, chgrpfile, vfs)
239 if not ui.verbose:
241 if not ui.verbose:
240 # silence internal shuffling chatter
242 # silence internal shuffling chatter
241 ui.pushbuffer()
243 ui.pushbuffer()
242 if isinstance(gen, bundle2.unbundle20):
244 if isinstance(gen, bundle2.unbundle20):
243 with repo.transaction('strip') as tr:
245 with repo.transaction('strip') as tr:
244 bundle2.processbundle(repo, gen, lambda: tr)
246 bundle2.processbundle(repo, gen, lambda: tr)
245 else:
247 else:
246 gen.apply(repo, 'strip', 'bundle:' + vfs.join(chgrpfile), True)
248 gen.apply(repo, 'strip', 'bundle:' + vfs.join(chgrpfile), True)
247 if not ui.verbose:
249 if not ui.verbose:
248 ui.popbuffer()
250 ui.popbuffer()
249 finally:
251 finally:
250 f.close()
252 f.close()
251
253
252 # remove undo files
254 # remove undo files
253 for undovfs, undofile in repo.undofiles():
255 for undovfs, undofile in repo.undofiles():
254 try:
256 try:
255 undovfs.unlink(undofile)
257 undovfs.unlink(undofile)
256 except OSError as e:
258 except OSError as e:
257 if e.errno != errno.ENOENT:
259 if e.errno != errno.ENOENT:
258 ui.warn(_('error removing %s: %s\n') %
260 ui.warn(_('error removing %s: %s\n') %
259 (undovfs.join(undofile), stringutil.forcebytestr(e)))
261 (undovfs.join(undofile), stringutil.forcebytestr(e)))
260
262
261 # Remove partial backup only if there were no exceptions
263 # Remove partial backup only if there were no exceptions
262 op._widen_uninterr.__exit__(None, None, None)
264 op._widen_uninterr.__exit__(None, None, None)
263 vfs.unlink(chgrpfile)
265 vfs.unlink(chgrpfile)
264
266
265 def setup():
267 def setup():
266 """Enable narrow repo support in bundle2-related extension points."""
268 """Enable narrow repo support in bundle2-related extension points."""
267 getbundleargs = wireprototypes.GETBUNDLE_ARGUMENTS
269 getbundleargs = wireprototypes.GETBUNDLE_ARGUMENTS
268
270
269 getbundleargs['narrow'] = 'boolean'
271 getbundleargs['narrow'] = 'boolean'
270 getbundleargs['depth'] = 'plain'
272 getbundleargs['depth'] = 'plain'
271 getbundleargs['oldincludepats'] = 'csv'
273 getbundleargs['oldincludepats'] = 'csv'
272 getbundleargs['oldexcludepats'] = 'csv'
274 getbundleargs['oldexcludepats'] = 'csv'
273 getbundleargs['known'] = 'csv'
275 getbundleargs['known'] = 'csv'
274
276
275 # Extend changegroup serving to handle requests from narrow clients.
277 # Extend changegroup serving to handle requests from narrow clients.
276 origcgfn = exchange.getbundle2partsmapping['changegroup']
278 origcgfn = exchange.getbundle2partsmapping['changegroup']
277 def wrappedcgfn(*args, **kwargs):
279 def wrappedcgfn(*args, **kwargs):
278 repo = args[1]
280 repo = args[1]
279 if repo.ui.has_section(_NARROWACL_SECTION):
281 if repo.ui.has_section(_NARROWACL_SECTION):
280 kwargs = exchange.applynarrowacl(repo, kwargs)
282 kwargs = exchange.applynarrowacl(repo, kwargs)
281
283
282 if (kwargs.get(r'narrow', False) and
284 if (kwargs.get(r'narrow', False) and
283 repo.ui.configbool('experimental', 'narrowservebrokenellipses')):
285 repo.ui.configbool('experimental', 'narrowservebrokenellipses')):
284 getbundlechangegrouppart_narrow(*args, **kwargs)
286 getbundlechangegrouppart_narrow(*args, **kwargs)
285 else:
287 else:
286 origcgfn(*args, **kwargs)
288 origcgfn(*args, **kwargs)
287 exchange.getbundle2partsmapping['changegroup'] = wrappedcgfn
289 exchange.getbundle2partsmapping['changegroup'] = wrappedcgfn
288
290
289 # Extend changegroup receiver so client can fixup after widen requests.
291 # Extend changegroup receiver so client can fixup after widen requests.
290 origcghandler = bundle2.parthandlermapping['changegroup']
292 origcghandler = bundle2.parthandlermapping['changegroup']
291 def wrappedcghandler(op, inpart):
293 def wrappedcghandler(op, inpart):
292 origcghandler(op, inpart)
294 origcghandler(op, inpart)
293 if util.safehasattr(op, '_widen_bundle'):
295 if util.safehasattr(op, '_widen_bundle'):
294 handlechangegroup_widen(op, inpart)
296 handlechangegroup_widen(op, inpart)
295 if util.safehasattr(op, '_bookmarksbackup'):
297 if util.safehasattr(op, '_bookmarksbackup'):
296 localrepo.localrepository._bookmarks.set(op.repo,
298 localrepo.localrepository._bookmarks.set(op.repo,
297 op._bookmarksbackup)
299 op._bookmarksbackup)
298 del op._bookmarksbackup
300 del op._bookmarksbackup
299
301
300 wrappedcghandler.params = origcghandler.params
302 wrappedcghandler.params = origcghandler.params
301 bundle2.parthandlermapping['changegroup'] = wrappedcghandler
303 bundle2.parthandlermapping['changegroup'] = wrappedcghandler
@@ -1,478 +1,480
1 # narrowcommands.py - command modifications for narrowhg extension
1 # narrowcommands.py - command modifications for narrowhg extension
2 #
2 #
3 # Copyright 2017 Google, Inc.
3 # Copyright 2017 Google, Inc.
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 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import itertools
9 import itertools
10 import os
10 import os
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 bundle2,
14 bundle2,
15 cmdutil,
15 cmdutil,
16 commands,
16 commands,
17 discovery,
17 discovery,
18 encoding,
18 encoding,
19 error,
19 error,
20 exchange,
20 exchange,
21 extensions,
21 extensions,
22 hg,
22 hg,
23 narrowspec,
23 narrowspec,
24 node,
24 node,
25 pycompat,
25 pycompat,
26 registrar,
26 registrar,
27 repair,
27 repair,
28 repository,
29 repoview,
28 repoview,
30 sparse,
29 sparse,
31 util,
30 util,
32 wireprototypes,
31 wireprototypes,
33 )
32 )
33 from mercurial.interfaces import (
34 repository,
35 )
34
36
35 table = {}
37 table = {}
36 command = registrar.command(table)
38 command = registrar.command(table)
37
39
38 def setup():
40 def setup():
39 """Wraps user-facing mercurial commands with narrow-aware versions."""
41 """Wraps user-facing mercurial commands with narrow-aware versions."""
40
42
41 entry = extensions.wrapcommand(commands.table, 'clone', clonenarrowcmd)
43 entry = extensions.wrapcommand(commands.table, 'clone', clonenarrowcmd)
42 entry[1].append(('', 'narrow', None,
44 entry[1].append(('', 'narrow', None,
43 _("create a narrow clone of select files")))
45 _("create a narrow clone of select files")))
44 entry[1].append(('', 'depth', '',
46 entry[1].append(('', 'depth', '',
45 _("limit the history fetched by distance from heads")))
47 _("limit the history fetched by distance from heads")))
46 entry[1].append(('', 'narrowspec', '',
48 entry[1].append(('', 'narrowspec', '',
47 _("read narrowspecs from file")))
49 _("read narrowspecs from file")))
48 # TODO(durin42): unify sparse/narrow --include/--exclude logic a bit
50 # TODO(durin42): unify sparse/narrow --include/--exclude logic a bit
49 if 'sparse' not in extensions.enabled():
51 if 'sparse' not in extensions.enabled():
50 entry[1].append(('', 'include', [],
52 entry[1].append(('', 'include', [],
51 _("specifically fetch this file/directory")))
53 _("specifically fetch this file/directory")))
52 entry[1].append(
54 entry[1].append(
53 ('', 'exclude', [],
55 ('', 'exclude', [],
54 _("do not fetch this file/directory, even if included")))
56 _("do not fetch this file/directory, even if included")))
55
57
56 entry = extensions.wrapcommand(commands.table, 'pull', pullnarrowcmd)
58 entry = extensions.wrapcommand(commands.table, 'pull', pullnarrowcmd)
57 entry[1].append(('', 'depth', '',
59 entry[1].append(('', 'depth', '',
58 _("limit the history fetched by distance from heads")))
60 _("limit the history fetched by distance from heads")))
59
61
60 extensions.wrapcommand(commands.table, 'archive', archivenarrowcmd)
62 extensions.wrapcommand(commands.table, 'archive', archivenarrowcmd)
61
63
62 def clonenarrowcmd(orig, ui, repo, *args, **opts):
64 def clonenarrowcmd(orig, ui, repo, *args, **opts):
63 """Wraps clone command, so 'hg clone' first wraps localrepo.clone()."""
65 """Wraps clone command, so 'hg clone' first wraps localrepo.clone()."""
64 opts = pycompat.byteskwargs(opts)
66 opts = pycompat.byteskwargs(opts)
65 wrappedextraprepare = util.nullcontextmanager()
67 wrappedextraprepare = util.nullcontextmanager()
66 narrowspecfile = opts['narrowspec']
68 narrowspecfile = opts['narrowspec']
67
69
68 if narrowspecfile:
70 if narrowspecfile:
69 filepath = os.path.join(encoding.getcwd(), narrowspecfile)
71 filepath = os.path.join(encoding.getcwd(), narrowspecfile)
70 ui.status(_("reading narrowspec from '%s'\n") % filepath)
72 ui.status(_("reading narrowspec from '%s'\n") % filepath)
71 try:
73 try:
72 fdata = util.readfile(filepath)
74 fdata = util.readfile(filepath)
73 except IOError as inst:
75 except IOError as inst:
74 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
76 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
75 (filepath, encoding.strtolocal(inst.strerror)))
77 (filepath, encoding.strtolocal(inst.strerror)))
76
78
77 includes, excludes, profiles = sparse.parseconfig(ui, fdata, 'narrow')
79 includes, excludes, profiles = sparse.parseconfig(ui, fdata, 'narrow')
78 if profiles:
80 if profiles:
79 raise error.Abort(_("cannot specify other files using '%include' in"
81 raise error.Abort(_("cannot specify other files using '%include' in"
80 " narrowspec"))
82 " narrowspec"))
81
83
82 narrowspec.validatepatterns(includes)
84 narrowspec.validatepatterns(includes)
83 narrowspec.validatepatterns(excludes)
85 narrowspec.validatepatterns(excludes)
84
86
85 # narrowspec is passed so we should assume that user wants narrow clone
87 # narrowspec is passed so we should assume that user wants narrow clone
86 opts['narrow'] = True
88 opts['narrow'] = True
87 opts['include'].extend(includes)
89 opts['include'].extend(includes)
88 opts['exclude'].extend(excludes)
90 opts['exclude'].extend(excludes)
89
91
90 if opts['narrow']:
92 if opts['narrow']:
91 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
93 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
92 orig(pullop, kwargs)
94 orig(pullop, kwargs)
93
95
94 if opts.get('depth'):
96 if opts.get('depth'):
95 kwargs['depth'] = opts['depth']
97 kwargs['depth'] = opts['depth']
96 wrappedextraprepare = extensions.wrappedfunction(exchange,
98 wrappedextraprepare = extensions.wrappedfunction(exchange,
97 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
99 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
98
100
99 with wrappedextraprepare:
101 with wrappedextraprepare:
100 return orig(ui, repo, *args, **pycompat.strkwargs(opts))
102 return orig(ui, repo, *args, **pycompat.strkwargs(opts))
101
103
102 def pullnarrowcmd(orig, ui, repo, *args, **opts):
104 def pullnarrowcmd(orig, ui, repo, *args, **opts):
103 """Wraps pull command to allow modifying narrow spec."""
105 """Wraps pull command to allow modifying narrow spec."""
104 wrappedextraprepare = util.nullcontextmanager()
106 wrappedextraprepare = util.nullcontextmanager()
105 if repository.NARROW_REQUIREMENT in repo.requirements:
107 if repository.NARROW_REQUIREMENT in repo.requirements:
106
108
107 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
109 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
108 orig(pullop, kwargs)
110 orig(pullop, kwargs)
109 if opts.get(r'depth'):
111 if opts.get(r'depth'):
110 kwargs['depth'] = opts[r'depth']
112 kwargs['depth'] = opts[r'depth']
111 wrappedextraprepare = extensions.wrappedfunction(exchange,
113 wrappedextraprepare = extensions.wrappedfunction(exchange,
112 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
114 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
113
115
114 with wrappedextraprepare:
116 with wrappedextraprepare:
115 return orig(ui, repo, *args, **opts)
117 return orig(ui, repo, *args, **opts)
116
118
117 def archivenarrowcmd(orig, ui, repo, *args, **opts):
119 def archivenarrowcmd(orig, ui, repo, *args, **opts):
118 """Wraps archive command to narrow the default includes."""
120 """Wraps archive command to narrow the default includes."""
119 if repository.NARROW_REQUIREMENT in repo.requirements:
121 if repository.NARROW_REQUIREMENT in repo.requirements:
120 repo_includes, repo_excludes = repo.narrowpats
122 repo_includes, repo_excludes = repo.narrowpats
121 includes = set(opts.get(r'include', []))
123 includes = set(opts.get(r'include', []))
122 excludes = set(opts.get(r'exclude', []))
124 excludes = set(opts.get(r'exclude', []))
123 includes, excludes, unused_invalid = narrowspec.restrictpatterns(
125 includes, excludes, unused_invalid = narrowspec.restrictpatterns(
124 includes, excludes, repo_includes, repo_excludes)
126 includes, excludes, repo_includes, repo_excludes)
125 if includes:
127 if includes:
126 opts[r'include'] = includes
128 opts[r'include'] = includes
127 if excludes:
129 if excludes:
128 opts[r'exclude'] = excludes
130 opts[r'exclude'] = excludes
129 return orig(ui, repo, *args, **opts)
131 return orig(ui, repo, *args, **opts)
130
132
131 def pullbundle2extraprepare(orig, pullop, kwargs):
133 def pullbundle2extraprepare(orig, pullop, kwargs):
132 repo = pullop.repo
134 repo = pullop.repo
133 if repository.NARROW_REQUIREMENT not in repo.requirements:
135 if repository.NARROW_REQUIREMENT not in repo.requirements:
134 return orig(pullop, kwargs)
136 return orig(pullop, kwargs)
135
137
136 if wireprototypes.NARROWCAP not in pullop.remote.capabilities():
138 if wireprototypes.NARROWCAP not in pullop.remote.capabilities():
137 raise error.Abort(_("server does not support narrow clones"))
139 raise error.Abort(_("server does not support narrow clones"))
138 orig(pullop, kwargs)
140 orig(pullop, kwargs)
139 kwargs['narrow'] = True
141 kwargs['narrow'] = True
140 include, exclude = repo.narrowpats
142 include, exclude = repo.narrowpats
141 kwargs['oldincludepats'] = include
143 kwargs['oldincludepats'] = include
142 kwargs['oldexcludepats'] = exclude
144 kwargs['oldexcludepats'] = exclude
143 if include:
145 if include:
144 kwargs['includepats'] = include
146 kwargs['includepats'] = include
145 if exclude:
147 if exclude:
146 kwargs['excludepats'] = exclude
148 kwargs['excludepats'] = exclude
147 # calculate known nodes only in ellipses cases because in non-ellipses cases
149 # calculate known nodes only in ellipses cases because in non-ellipses cases
148 # we have all the nodes
150 # we have all the nodes
149 if wireprototypes.ELLIPSESCAP1 in pullop.remote.capabilities():
151 if wireprototypes.ELLIPSESCAP1 in pullop.remote.capabilities():
150 kwargs['known'] = [node.hex(ctx.node()) for ctx in
152 kwargs['known'] = [node.hex(ctx.node()) for ctx in
151 repo.set('::%ln', pullop.common)
153 repo.set('::%ln', pullop.common)
152 if ctx.node() != node.nullid]
154 if ctx.node() != node.nullid]
153 if not kwargs['known']:
155 if not kwargs['known']:
154 # Mercurial serializes an empty list as '' and deserializes it as
156 # Mercurial serializes an empty list as '' and deserializes it as
155 # [''], so delete it instead to avoid handling the empty string on
157 # [''], so delete it instead to avoid handling the empty string on
156 # the server.
158 # the server.
157 del kwargs['known']
159 del kwargs['known']
158
160
159 extensions.wrapfunction(exchange,'_pullbundle2extraprepare',
161 extensions.wrapfunction(exchange,'_pullbundle2extraprepare',
160 pullbundle2extraprepare)
162 pullbundle2extraprepare)
161
163
162 def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
164 def _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
163 newincludes, newexcludes, force):
165 newincludes, newexcludes, force):
164 oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes)
166 oldmatch = narrowspec.match(repo.root, oldincludes, oldexcludes)
165 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
167 newmatch = narrowspec.match(repo.root, newincludes, newexcludes)
166
168
167 # This is essentially doing "hg outgoing" to find all local-only
169 # This is essentially doing "hg outgoing" to find all local-only
168 # commits. We will then check that the local-only commits don't
170 # commits. We will then check that the local-only commits don't
169 # have any changes to files that will be untracked.
171 # have any changes to files that will be untracked.
170 unfi = repo.unfiltered()
172 unfi = repo.unfiltered()
171 outgoing = discovery.findcommonoutgoing(unfi, remote,
173 outgoing = discovery.findcommonoutgoing(unfi, remote,
172 commoninc=commoninc)
174 commoninc=commoninc)
173 ui.status(_('looking for local changes to affected paths\n'))
175 ui.status(_('looking for local changes to affected paths\n'))
174 localnodes = []
176 localnodes = []
175 for n in itertools.chain(outgoing.missing, outgoing.excluded):
177 for n in itertools.chain(outgoing.missing, outgoing.excluded):
176 if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()):
178 if any(oldmatch(f) and not newmatch(f) for f in unfi[n].files()):
177 localnodes.append(n)
179 localnodes.append(n)
178 revstostrip = unfi.revs('descendants(%ln)', localnodes)
180 revstostrip = unfi.revs('descendants(%ln)', localnodes)
179 hiddenrevs = repoview.filterrevs(repo, 'visible')
181 hiddenrevs = repoview.filterrevs(repo, 'visible')
180 visibletostrip = list(repo.changelog.node(r)
182 visibletostrip = list(repo.changelog.node(r)
181 for r in (revstostrip - hiddenrevs))
183 for r in (revstostrip - hiddenrevs))
182 if visibletostrip:
184 if visibletostrip:
183 ui.status(_('The following changeset(s) or their ancestors have '
185 ui.status(_('The following changeset(s) or their ancestors have '
184 'local changes not on the remote:\n'))
186 'local changes not on the remote:\n'))
185 maxnodes = 10
187 maxnodes = 10
186 if ui.verbose or len(visibletostrip) <= maxnodes:
188 if ui.verbose or len(visibletostrip) <= maxnodes:
187 for n in visibletostrip:
189 for n in visibletostrip:
188 ui.status('%s\n' % node.short(n))
190 ui.status('%s\n' % node.short(n))
189 else:
191 else:
190 for n in visibletostrip[:maxnodes]:
192 for n in visibletostrip[:maxnodes]:
191 ui.status('%s\n' % node.short(n))
193 ui.status('%s\n' % node.short(n))
192 ui.status(_('...and %d more, use --verbose to list all\n') %
194 ui.status(_('...and %d more, use --verbose to list all\n') %
193 (len(visibletostrip) - maxnodes))
195 (len(visibletostrip) - maxnodes))
194 if not force:
196 if not force:
195 raise error.Abort(_('local changes found'),
197 raise error.Abort(_('local changes found'),
196 hint=_('use --force-delete-local-changes to '
198 hint=_('use --force-delete-local-changes to '
197 'ignore'))
199 'ignore'))
198
200
199 with ui.uninterruptible():
201 with ui.uninterruptible():
200 if revstostrip:
202 if revstostrip:
201 tostrip = [unfi.changelog.node(r) for r in revstostrip]
203 tostrip = [unfi.changelog.node(r) for r in revstostrip]
202 if repo['.'].node() in tostrip:
204 if repo['.'].node() in tostrip:
203 # stripping working copy, so move to a different commit first
205 # stripping working copy, so move to a different commit first
204 urev = max(repo.revs('(::%n) - %ln + null',
206 urev = max(repo.revs('(::%n) - %ln + null',
205 repo['.'].node(), visibletostrip))
207 repo['.'].node(), visibletostrip))
206 hg.clean(repo, urev)
208 hg.clean(repo, urev)
207 overrides = {('devel', 'strip-obsmarkers'): False}
209 overrides = {('devel', 'strip-obsmarkers'): False}
208 with ui.configoverride(overrides, 'narrow'):
210 with ui.configoverride(overrides, 'narrow'):
209 repair.strip(ui, unfi, tostrip, topic='narrow')
211 repair.strip(ui, unfi, tostrip, topic='narrow')
210
212
211 todelete = []
213 todelete = []
212 for f, f2, size in repo.store.datafiles():
214 for f, f2, size in repo.store.datafiles():
213 if f.startswith('data/'):
215 if f.startswith('data/'):
214 file = f[5:-2]
216 file = f[5:-2]
215 if not newmatch(file):
217 if not newmatch(file):
216 todelete.append(f)
218 todelete.append(f)
217 elif f.startswith('meta/'):
219 elif f.startswith('meta/'):
218 dir = f[5:-13]
220 dir = f[5:-13]
219 dirs = sorted(util.dirs({dir})) + [dir]
221 dirs = sorted(util.dirs({dir})) + [dir]
220 include = True
222 include = True
221 for d in dirs:
223 for d in dirs:
222 visit = newmatch.visitdir(d)
224 visit = newmatch.visitdir(d)
223 if not visit:
225 if not visit:
224 include = False
226 include = False
225 break
227 break
226 if visit == 'all':
228 if visit == 'all':
227 break
229 break
228 if not include:
230 if not include:
229 todelete.append(f)
231 todelete.append(f)
230
232
231 repo.destroying()
233 repo.destroying()
232
234
233 with repo.transaction('narrowing'):
235 with repo.transaction('narrowing'):
234 # Update narrowspec before removing revlogs, so repo won't be
236 # Update narrowspec before removing revlogs, so repo won't be
235 # corrupt in case of crash
237 # corrupt in case of crash
236 repo.setnarrowpats(newincludes, newexcludes)
238 repo.setnarrowpats(newincludes, newexcludes)
237
239
238 for f in todelete:
240 for f in todelete:
239 ui.status(_('deleting %s\n') % f)
241 ui.status(_('deleting %s\n') % f)
240 util.unlinkpath(repo.svfs.join(f))
242 util.unlinkpath(repo.svfs.join(f))
241 repo.store.markremoved(f)
243 repo.store.markremoved(f)
242
244
243 narrowspec.updateworkingcopy(repo, assumeclean=True)
245 narrowspec.updateworkingcopy(repo, assumeclean=True)
244 narrowspec.copytoworkingcopy(repo)
246 narrowspec.copytoworkingcopy(repo)
245
247
246 repo.destroyed()
248 repo.destroyed()
247
249
248 def _widen(ui, repo, remote, commoninc, oldincludes, oldexcludes,
250 def _widen(ui, repo, remote, commoninc, oldincludes, oldexcludes,
249 newincludes, newexcludes):
251 newincludes, newexcludes):
250 # for now we assume that if a server has ellipses enabled, we will be
252 # for now we assume that if a server has ellipses enabled, we will be
251 # exchanging ellipses nodes. In future we should add ellipses as a client
253 # exchanging ellipses nodes. In future we should add ellipses as a client
252 # side requirement (maybe) to distinguish a client is shallow or not and
254 # side requirement (maybe) to distinguish a client is shallow or not and
253 # then send that information to server whether we want ellipses or not.
255 # then send that information to server whether we want ellipses or not.
254 # Theoretically a non-ellipses repo should be able to use narrow
256 # Theoretically a non-ellipses repo should be able to use narrow
255 # functionality from an ellipses enabled server
257 # functionality from an ellipses enabled server
256 remotecap = remote.capabilities()
258 remotecap = remote.capabilities()
257 ellipsesremote = any(cap in remotecap
259 ellipsesremote = any(cap in remotecap
258 for cap in wireprototypes.SUPPORTED_ELLIPSESCAP)
260 for cap in wireprototypes.SUPPORTED_ELLIPSESCAP)
259
261
260 # check whether we are talking to a server which supports old version of
262 # check whether we are talking to a server which supports old version of
261 # ellipses capabilities
263 # ellipses capabilities
262 isoldellipses = (ellipsesremote and wireprototypes.ELLIPSESCAP1 in
264 isoldellipses = (ellipsesremote and wireprototypes.ELLIPSESCAP1 in
263 remotecap and wireprototypes.ELLIPSESCAP not in remotecap)
265 remotecap and wireprototypes.ELLIPSESCAP not in remotecap)
264
266
265 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
267 def pullbundle2extraprepare_widen(orig, pullop, kwargs):
266 orig(pullop, kwargs)
268 orig(pullop, kwargs)
267 # The old{in,ex}cludepats have already been set by orig()
269 # The old{in,ex}cludepats have already been set by orig()
268 kwargs['includepats'] = newincludes
270 kwargs['includepats'] = newincludes
269 kwargs['excludepats'] = newexcludes
271 kwargs['excludepats'] = newexcludes
270 wrappedextraprepare = extensions.wrappedfunction(exchange,
272 wrappedextraprepare = extensions.wrappedfunction(exchange,
271 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
273 '_pullbundle2extraprepare', pullbundle2extraprepare_widen)
272
274
273 # define a function that narrowbundle2 can call after creating the
275 # define a function that narrowbundle2 can call after creating the
274 # backup bundle, but before applying the bundle from the server
276 # backup bundle, but before applying the bundle from the server
275 def setnewnarrowpats():
277 def setnewnarrowpats():
276 repo.setnarrowpats(newincludes, newexcludes)
278 repo.setnarrowpats(newincludes, newexcludes)
277 repo.setnewnarrowpats = setnewnarrowpats
279 repo.setnewnarrowpats = setnewnarrowpats
278 # silence the devel-warning of applying an empty changegroup
280 # silence the devel-warning of applying an empty changegroup
279 overrides = {('devel', 'all-warnings'): False}
281 overrides = {('devel', 'all-warnings'): False}
280
282
281 common = commoninc[0]
283 common = commoninc[0]
282 with ui.uninterruptible():
284 with ui.uninterruptible():
283 if ellipsesremote:
285 if ellipsesremote:
284 ds = repo.dirstate
286 ds = repo.dirstate
285 p1, p2 = ds.p1(), ds.p2()
287 p1, p2 = ds.p1(), ds.p2()
286 with ds.parentchange():
288 with ds.parentchange():
287 ds.setparents(node.nullid, node.nullid)
289 ds.setparents(node.nullid, node.nullid)
288 if isoldellipses:
290 if isoldellipses:
289 with wrappedextraprepare:
291 with wrappedextraprepare:
290 exchange.pull(repo, remote, heads=common)
292 exchange.pull(repo, remote, heads=common)
291 else:
293 else:
292 known = []
294 known = []
293 if ellipsesremote:
295 if ellipsesremote:
294 known = [node.hex(ctx.node()) for ctx in
296 known = [node.hex(ctx.node()) for ctx in
295 repo.set('::%ln', common)
297 repo.set('::%ln', common)
296 if ctx.node() != node.nullid]
298 if ctx.node() != node.nullid]
297 with remote.commandexecutor() as e:
299 with remote.commandexecutor() as e:
298 bundle = e.callcommand('narrow_widen', {
300 bundle = e.callcommand('narrow_widen', {
299 'oldincludes': oldincludes,
301 'oldincludes': oldincludes,
300 'oldexcludes': oldexcludes,
302 'oldexcludes': oldexcludes,
301 'newincludes': newincludes,
303 'newincludes': newincludes,
302 'newexcludes': newexcludes,
304 'newexcludes': newexcludes,
303 'cgversion': '03',
305 'cgversion': '03',
304 'commonheads': common,
306 'commonheads': common,
305 'known': known,
307 'known': known,
306 'ellipses': ellipsesremote,
308 'ellipses': ellipsesremote,
307 }).result()
309 }).result()
308
310
309 trmanager = exchange.transactionmanager(repo, 'widen', remote.url())
311 trmanager = exchange.transactionmanager(repo, 'widen', remote.url())
310 with trmanager, repo.ui.configoverride(overrides, 'widen'):
312 with trmanager, repo.ui.configoverride(overrides, 'widen'):
311 op = bundle2.bundleoperation(repo, trmanager.transaction,
313 op = bundle2.bundleoperation(repo, trmanager.transaction,
312 source='widen')
314 source='widen')
313 # TODO: we should catch error.Abort here
315 # TODO: we should catch error.Abort here
314 bundle2.processbundle(repo, bundle, op=op)
316 bundle2.processbundle(repo, bundle, op=op)
315
317
316 if ellipsesremote:
318 if ellipsesremote:
317 with ds.parentchange():
319 with ds.parentchange():
318 ds.setparents(p1, p2)
320 ds.setparents(p1, p2)
319
321
320 with repo.transaction('widening'):
322 with repo.transaction('widening'):
321 repo.setnewnarrowpats()
323 repo.setnewnarrowpats()
322 narrowspec.updateworkingcopy(repo)
324 narrowspec.updateworkingcopy(repo)
323 narrowspec.copytoworkingcopy(repo)
325 narrowspec.copytoworkingcopy(repo)
324
326
325 # TODO(rdamazio): Make new matcher format and update description
327 # TODO(rdamazio): Make new matcher format and update description
326 @command('tracked',
328 @command('tracked',
327 [('', 'addinclude', [], _('new paths to include')),
329 [('', 'addinclude', [], _('new paths to include')),
328 ('', 'removeinclude', [], _('old paths to no longer include')),
330 ('', 'removeinclude', [], _('old paths to no longer include')),
329 ('', 'addexclude', [], _('new paths to exclude')),
331 ('', 'addexclude', [], _('new paths to exclude')),
330 ('', 'import-rules', '', _('import narrowspecs from a file')),
332 ('', 'import-rules', '', _('import narrowspecs from a file')),
331 ('', 'removeexclude', [], _('old paths to no longer exclude')),
333 ('', 'removeexclude', [], _('old paths to no longer exclude')),
332 ('', 'clear', False, _('whether to replace the existing narrowspec')),
334 ('', 'clear', False, _('whether to replace the existing narrowspec')),
333 ('', 'force-delete-local-changes', False,
335 ('', 'force-delete-local-changes', False,
334 _('forces deletion of local changes when narrowing')),
336 _('forces deletion of local changes when narrowing')),
335 ('', 'update-working-copy', False,
337 ('', 'update-working-copy', False,
336 _('update working copy when the store has changed')),
338 _('update working copy when the store has changed')),
337 ] + commands.remoteopts,
339 ] + commands.remoteopts,
338 _('[OPTIONS]... [REMOTE]'),
340 _('[OPTIONS]... [REMOTE]'),
339 inferrepo=True)
341 inferrepo=True)
340 def trackedcmd(ui, repo, remotepath=None, *pats, **opts):
342 def trackedcmd(ui, repo, remotepath=None, *pats, **opts):
341 """show or change the current narrowspec
343 """show or change the current narrowspec
342
344
343 With no argument, shows the current narrowspec entries, one per line. Each
345 With no argument, shows the current narrowspec entries, one per line. Each
344 line will be prefixed with 'I' or 'X' for included or excluded patterns,
346 line will be prefixed with 'I' or 'X' for included or excluded patterns,
345 respectively.
347 respectively.
346
348
347 The narrowspec is comprised of expressions to match remote files and/or
349 The narrowspec is comprised of expressions to match remote files and/or
348 directories that should be pulled into your client.
350 directories that should be pulled into your client.
349 The narrowspec has *include* and *exclude* expressions, with excludes always
351 The narrowspec has *include* and *exclude* expressions, with excludes always
350 trumping includes: that is, if a file matches an exclude expression, it will
352 trumping includes: that is, if a file matches an exclude expression, it will
351 be excluded even if it also matches an include expression.
353 be excluded even if it also matches an include expression.
352 Excluding files that were never included has no effect.
354 Excluding files that were never included has no effect.
353
355
354 Each included or excluded entry is in the format described by
356 Each included or excluded entry is in the format described by
355 'hg help patterns'.
357 'hg help patterns'.
356
358
357 The options allow you to add or remove included and excluded expressions.
359 The options allow you to add or remove included and excluded expressions.
358
360
359 If --clear is specified, then all previous includes and excludes are DROPPED
361 If --clear is specified, then all previous includes and excludes are DROPPED
360 and replaced by the new ones specified to --addinclude and --addexclude.
362 and replaced by the new ones specified to --addinclude and --addexclude.
361 If --clear is specified without any further options, the narrowspec will be
363 If --clear is specified without any further options, the narrowspec will be
362 empty and will not match any files.
364 empty and will not match any files.
363
365
364 --import-rules accepts a path to a file containing rules, allowing you to
366 --import-rules accepts a path to a file containing rules, allowing you to
365 add --addinclude, --addexclude rules in bulk. Like the other include and
367 add --addinclude, --addexclude rules in bulk. Like the other include and
366 exclude switches, the changes are applied immediately.
368 exclude switches, the changes are applied immediately.
367 """
369 """
368 opts = pycompat.byteskwargs(opts)
370 opts = pycompat.byteskwargs(opts)
369 if repository.NARROW_REQUIREMENT not in repo.requirements:
371 if repository.NARROW_REQUIREMENT not in repo.requirements:
370 raise error.Abort(_('the tracked command is only supported on '
372 raise error.Abort(_('the tracked command is only supported on '
371 'repositories cloned with --narrow'))
373 'repositories cloned with --narrow'))
372
374
373 # Before supporting, decide whether it "hg tracked --clear" should mean
375 # Before supporting, decide whether it "hg tracked --clear" should mean
374 # tracking no paths or all paths.
376 # tracking no paths or all paths.
375 if opts['clear']:
377 if opts['clear']:
376 raise error.Abort(_('the --clear option is not yet supported'))
378 raise error.Abort(_('the --clear option is not yet supported'))
377
379
378 # import rules from a file
380 # import rules from a file
379 newrules = opts.get('import_rules')
381 newrules = opts.get('import_rules')
380 if newrules:
382 if newrules:
381 try:
383 try:
382 filepath = os.path.join(encoding.getcwd(), newrules)
384 filepath = os.path.join(encoding.getcwd(), newrules)
383 fdata = util.readfile(filepath)
385 fdata = util.readfile(filepath)
384 except IOError as inst:
386 except IOError as inst:
385 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
387 raise error.Abort(_("cannot read narrowspecs from '%s': %s") %
386 (filepath, encoding.strtolocal(inst.strerror)))
388 (filepath, encoding.strtolocal(inst.strerror)))
387 includepats, excludepats, profiles = sparse.parseconfig(ui, fdata,
389 includepats, excludepats, profiles = sparse.parseconfig(ui, fdata,
388 'narrow')
390 'narrow')
389 if profiles:
391 if profiles:
390 raise error.Abort(_("including other spec files using '%include' "
392 raise error.Abort(_("including other spec files using '%include' "
391 "is not supported in narrowspec"))
393 "is not supported in narrowspec"))
392 opts['addinclude'].extend(includepats)
394 opts['addinclude'].extend(includepats)
393 opts['addexclude'].extend(excludepats)
395 opts['addexclude'].extend(excludepats)
394
396
395 addedincludes = narrowspec.parsepatterns(opts['addinclude'])
397 addedincludes = narrowspec.parsepatterns(opts['addinclude'])
396 removedincludes = narrowspec.parsepatterns(opts['removeinclude'])
398 removedincludes = narrowspec.parsepatterns(opts['removeinclude'])
397 addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
399 addedexcludes = narrowspec.parsepatterns(opts['addexclude'])
398 removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
400 removedexcludes = narrowspec.parsepatterns(opts['removeexclude'])
399
401
400 update_working_copy = opts['update_working_copy']
402 update_working_copy = opts['update_working_copy']
401 only_show = not (addedincludes or removedincludes or addedexcludes or
403 only_show = not (addedincludes or removedincludes or addedexcludes or
402 removedexcludes or newrules or update_working_copy)
404 removedexcludes or newrules or update_working_copy)
403
405
404 oldincludes, oldexcludes = repo.narrowpats
406 oldincludes, oldexcludes = repo.narrowpats
405
407
406 # filter the user passed additions and deletions into actual additions and
408 # filter the user passed additions and deletions into actual additions and
407 # deletions of excludes and includes
409 # deletions of excludes and includes
408 addedincludes -= oldincludes
410 addedincludes -= oldincludes
409 removedincludes &= oldincludes
411 removedincludes &= oldincludes
410 addedexcludes -= oldexcludes
412 addedexcludes -= oldexcludes
411 removedexcludes &= oldexcludes
413 removedexcludes &= oldexcludes
412
414
413 widening = addedincludes or removedexcludes
415 widening = addedincludes or removedexcludes
414 narrowing = removedincludes or addedexcludes
416 narrowing = removedincludes or addedexcludes
415
417
416 # Only print the current narrowspec.
418 # Only print the current narrowspec.
417 if only_show:
419 if only_show:
418 ui.pager('tracked')
420 ui.pager('tracked')
419 fm = ui.formatter('narrow', opts)
421 fm = ui.formatter('narrow', opts)
420 for i in sorted(oldincludes):
422 for i in sorted(oldincludes):
421 fm.startitem()
423 fm.startitem()
422 fm.write('status', '%s ', 'I', label='narrow.included')
424 fm.write('status', '%s ', 'I', label='narrow.included')
423 fm.write('pat', '%s\n', i, label='narrow.included')
425 fm.write('pat', '%s\n', i, label='narrow.included')
424 for i in sorted(oldexcludes):
426 for i in sorted(oldexcludes):
425 fm.startitem()
427 fm.startitem()
426 fm.write('status', '%s ', 'X', label='narrow.excluded')
428 fm.write('status', '%s ', 'X', label='narrow.excluded')
427 fm.write('pat', '%s\n', i, label='narrow.excluded')
429 fm.write('pat', '%s\n', i, label='narrow.excluded')
428 fm.end()
430 fm.end()
429 return 0
431 return 0
430
432
431 if update_working_copy:
433 if update_working_copy:
432 with repo.wlock(), repo.lock(), repo.transaction('narrow-wc'):
434 with repo.wlock(), repo.lock(), repo.transaction('narrow-wc'):
433 narrowspec.updateworkingcopy(repo)
435 narrowspec.updateworkingcopy(repo)
434 narrowspec.copytoworkingcopy(repo)
436 narrowspec.copytoworkingcopy(repo)
435 return 0
437 return 0
436
438
437 if not widening and not narrowing:
439 if not widening and not narrowing:
438 ui.status(_("nothing to widen or narrow\n"))
440 ui.status(_("nothing to widen or narrow\n"))
439 return 0
441 return 0
440
442
441 with repo.wlock(), repo.lock():
443 with repo.wlock(), repo.lock():
442 cmdutil.bailifchanged(repo)
444 cmdutil.bailifchanged(repo)
443
445
444 # Find the revisions we have in common with the remote. These will
446 # Find the revisions we have in common with the remote. These will
445 # be used for finding local-only changes for narrowing. They will
447 # be used for finding local-only changes for narrowing. They will
446 # also define the set of revisions to update for widening.
448 # also define the set of revisions to update for widening.
447 remotepath = ui.expandpath(remotepath or 'default')
449 remotepath = ui.expandpath(remotepath or 'default')
448 url, branches = hg.parseurl(remotepath)
450 url, branches = hg.parseurl(remotepath)
449 ui.status(_('comparing with %s\n') % util.hidepassword(url))
451 ui.status(_('comparing with %s\n') % util.hidepassword(url))
450 remote = hg.peer(repo, opts, url)
452 remote = hg.peer(repo, opts, url)
451
453
452 # check narrow support before doing anything if widening needs to be
454 # check narrow support before doing anything if widening needs to be
453 # performed. In future we should also abort if client is ellipses and
455 # performed. In future we should also abort if client is ellipses and
454 # server does not support ellipses
456 # server does not support ellipses
455 if widening and wireprototypes.NARROWCAP not in remote.capabilities():
457 if widening and wireprototypes.NARROWCAP not in remote.capabilities():
456 raise error.Abort(_("server does not support narrow clones"))
458 raise error.Abort(_("server does not support narrow clones"))
457
459
458 commoninc = discovery.findcommonincoming(repo, remote)
460 commoninc = discovery.findcommonincoming(repo, remote)
459
461
460 if narrowing:
462 if narrowing:
461 newincludes = oldincludes - removedincludes
463 newincludes = oldincludes - removedincludes
462 newexcludes = oldexcludes | addedexcludes
464 newexcludes = oldexcludes | addedexcludes
463 _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
465 _narrow(ui, repo, remote, commoninc, oldincludes, oldexcludes,
464 newincludes, newexcludes,
466 newincludes, newexcludes,
465 opts['force_delete_local_changes'])
467 opts['force_delete_local_changes'])
466 # _narrow() updated the narrowspec and _widen() below needs to
468 # _narrow() updated the narrowspec and _widen() below needs to
467 # use the updated values as its base (otherwise removed includes
469 # use the updated values as its base (otherwise removed includes
468 # and addedexcludes will be lost in the resulting narrowspec)
470 # and addedexcludes will be lost in the resulting narrowspec)
469 oldincludes = newincludes
471 oldincludes = newincludes
470 oldexcludes = newexcludes
472 oldexcludes = newexcludes
471
473
472 if widening:
474 if widening:
473 newincludes = oldincludes | addedincludes
475 newincludes = oldincludes | addedincludes
474 newexcludes = oldexcludes - removedexcludes
476 newexcludes = oldexcludes - removedexcludes
475 _widen(ui, repo, remote, commoninc, oldincludes, oldexcludes,
477 _widen(ui, repo, remote, commoninc, oldincludes, oldexcludes,
476 newincludes, newexcludes)
478 newincludes, newexcludes)
477
479
478 return 0
480 return 0
@@ -1,1174 +1,1176
1 # sqlitestore.py - Storage backend that uses SQLite
1 # sqlitestore.py - Storage backend that uses SQLite
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 """store repository data in SQLite (EXPERIMENTAL)
8 """store repository data in SQLite (EXPERIMENTAL)
9
9
10 The sqlitestore extension enables the storage of repository data in SQLite.
10 The sqlitestore extension enables the storage of repository data in SQLite.
11
11
12 This extension is HIGHLY EXPERIMENTAL. There are NO BACKWARDS COMPATIBILITY
12 This extension is HIGHLY EXPERIMENTAL. There are NO BACKWARDS COMPATIBILITY
13 GUARANTEES. This means that repositories created with this extension may
13 GUARANTEES. This means that repositories created with this extension may
14 only be usable with the exact version of this extension/Mercurial that was
14 only be usable with the exact version of this extension/Mercurial that was
15 used. The extension attempts to enforce this in order to prevent repository
15 used. The extension attempts to enforce this in order to prevent repository
16 corruption.
16 corruption.
17
17
18 In addition, several features are not yet supported or have known bugs:
18 In addition, several features are not yet supported or have known bugs:
19
19
20 * Only some data is stored in SQLite. Changeset, manifest, and other repository
20 * Only some data is stored in SQLite. Changeset, manifest, and other repository
21 data is not yet stored in SQLite.
21 data is not yet stored in SQLite.
22 * Transactions are not robust. If the process is aborted at the right time
22 * Transactions are not robust. If the process is aborted at the right time
23 during transaction close/rollback, the repository could be in an inconsistent
23 during transaction close/rollback, the repository could be in an inconsistent
24 state. This problem will diminish once all repository data is tracked by
24 state. This problem will diminish once all repository data is tracked by
25 SQLite.
25 SQLite.
26 * Bundle repositories do not work (the ability to use e.g.
26 * Bundle repositories do not work (the ability to use e.g.
27 `hg -R <bundle-file> log` to automatically overlay a bundle on top of the
27 `hg -R <bundle-file> log` to automatically overlay a bundle on top of the
28 existing repository).
28 existing repository).
29 * Various other features don't work.
29 * Various other features don't work.
30
30
31 This extension should work for basic clone/pull, update, and commit workflows.
31 This extension should work for basic clone/pull, update, and commit workflows.
32 Some history rewriting operations may fail due to lack of support for bundle
32 Some history rewriting operations may fail due to lack of support for bundle
33 repositories.
33 repositories.
34
34
35 To use, activate the extension and set the ``storage.new-repo-backend`` config
35 To use, activate the extension and set the ``storage.new-repo-backend`` config
36 option to ``sqlite`` to enable new repositories to use SQLite for storage.
36 option to ``sqlite`` to enable new repositories to use SQLite for storage.
37 """
37 """
38
38
39 # To run the test suite with repos using SQLite by default, execute the
39 # To run the test suite with repos using SQLite by default, execute the
40 # following:
40 # following:
41 #
41 #
42 # HGREPOFEATURES="sqlitestore" run-tests.py \
42 # HGREPOFEATURES="sqlitestore" run-tests.py \
43 # --extra-config-opt extensions.sqlitestore= \
43 # --extra-config-opt extensions.sqlitestore= \
44 # --extra-config-opt storage.new-repo-backend=sqlite
44 # --extra-config-opt storage.new-repo-backend=sqlite
45
45
46 from __future__ import absolute_import
46 from __future__ import absolute_import
47
47
48 import hashlib
48 import hashlib
49 import sqlite3
49 import sqlite3
50 import struct
50 import struct
51 import threading
51 import threading
52 import zlib
52 import zlib
53
53
54 from mercurial.i18n import _
54 from mercurial.i18n import _
55 from mercurial.node import (
55 from mercurial.node import (
56 nullid,
56 nullid,
57 nullrev,
57 nullrev,
58 short,
58 short,
59 )
59 )
60 from mercurial.thirdparty import (
60 from mercurial.thirdparty import (
61 attr,
61 attr,
62 )
62 )
63 from mercurial import (
63 from mercurial import (
64 ancestor,
64 ancestor,
65 dagop,
65 dagop,
66 encoding,
66 encoding,
67 error,
67 error,
68 extensions,
68 extensions,
69 localrepo,
69 localrepo,
70 mdiff,
70 mdiff,
71 pycompat,
71 pycompat,
72 registrar,
72 registrar,
73 repository,
74 util,
73 util,
75 verify,
74 verify,
76 )
75 )
76 from mercurial.interfaces import (
77 repository,
78 )
77 from mercurial.utils import (
79 from mercurial.utils import (
78 interfaceutil,
80 interfaceutil,
79 storageutil,
81 storageutil,
80 )
82 )
81
83
82 try:
84 try:
83 from mercurial import zstd
85 from mercurial import zstd
84 zstd.__version__
86 zstd.__version__
85 except ImportError:
87 except ImportError:
86 zstd = None
88 zstd = None
87
89
88 configtable = {}
90 configtable = {}
89 configitem = registrar.configitem(configtable)
91 configitem = registrar.configitem(configtable)
90
92
91 # experimental config: storage.sqlite.compression
93 # experimental config: storage.sqlite.compression
92 configitem('storage', 'sqlite.compression',
94 configitem('storage', 'sqlite.compression',
93 default='zstd' if zstd else 'zlib',
95 default='zstd' if zstd else 'zlib',
94 experimental=True)
96 experimental=True)
95
97
96 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
98 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
97 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
99 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
98 # be specifying the version(s) of Mercurial they are tested with, or
100 # be specifying the version(s) of Mercurial they are tested with, or
99 # leave the attribute unspecified.
101 # leave the attribute unspecified.
100 testedwith = 'ships-with-hg-core'
102 testedwith = 'ships-with-hg-core'
101
103
102 REQUIREMENT = b'exp-sqlite-001'
104 REQUIREMENT = b'exp-sqlite-001'
103 REQUIREMENT_ZSTD = b'exp-sqlite-comp-001=zstd'
105 REQUIREMENT_ZSTD = b'exp-sqlite-comp-001=zstd'
104 REQUIREMENT_ZLIB = b'exp-sqlite-comp-001=zlib'
106 REQUIREMENT_ZLIB = b'exp-sqlite-comp-001=zlib'
105 REQUIREMENT_NONE = b'exp-sqlite-comp-001=none'
107 REQUIREMENT_NONE = b'exp-sqlite-comp-001=none'
106 REQUIREMENT_SHALLOW_FILES = b'exp-sqlite-shallow-files'
108 REQUIREMENT_SHALLOW_FILES = b'exp-sqlite-shallow-files'
107
109
108 CURRENT_SCHEMA_VERSION = 1
110 CURRENT_SCHEMA_VERSION = 1
109
111
110 COMPRESSION_NONE = 1
112 COMPRESSION_NONE = 1
111 COMPRESSION_ZSTD = 2
113 COMPRESSION_ZSTD = 2
112 COMPRESSION_ZLIB = 3
114 COMPRESSION_ZLIB = 3
113
115
114 FLAG_CENSORED = 1
116 FLAG_CENSORED = 1
115 FLAG_MISSING_P1 = 2
117 FLAG_MISSING_P1 = 2
116 FLAG_MISSING_P2 = 4
118 FLAG_MISSING_P2 = 4
117
119
118 CREATE_SCHEMA = [
120 CREATE_SCHEMA = [
119 # Deltas are stored as content-indexed blobs.
121 # Deltas are stored as content-indexed blobs.
120 # compression column holds COMPRESSION_* constant for how the
122 # compression column holds COMPRESSION_* constant for how the
121 # delta is encoded.
123 # delta is encoded.
122
124
123 r'CREATE TABLE delta ('
125 r'CREATE TABLE delta ('
124 r' id INTEGER PRIMARY KEY, '
126 r' id INTEGER PRIMARY KEY, '
125 r' compression INTEGER NOT NULL, '
127 r' compression INTEGER NOT NULL, '
126 r' hash BLOB UNIQUE ON CONFLICT ABORT, '
128 r' hash BLOB UNIQUE ON CONFLICT ABORT, '
127 r' delta BLOB NOT NULL '
129 r' delta BLOB NOT NULL '
128 r')',
130 r')',
129
131
130 # Tracked paths are denormalized to integers to avoid redundant
132 # Tracked paths are denormalized to integers to avoid redundant
131 # storage of the path name.
133 # storage of the path name.
132 r'CREATE TABLE filepath ('
134 r'CREATE TABLE filepath ('
133 r' id INTEGER PRIMARY KEY, '
135 r' id INTEGER PRIMARY KEY, '
134 r' path BLOB NOT NULL '
136 r' path BLOB NOT NULL '
135 r')',
137 r')',
136
138
137 r'CREATE UNIQUE INDEX filepath_path '
139 r'CREATE UNIQUE INDEX filepath_path '
138 r' ON filepath (path)',
140 r' ON filepath (path)',
139
141
140 # We have a single table for all file revision data.
142 # We have a single table for all file revision data.
141 # Each file revision is uniquely described by a (path, rev) and
143 # Each file revision is uniquely described by a (path, rev) and
142 # (path, node).
144 # (path, node).
143 #
145 #
144 # Revision data is stored as a pointer to the delta producing this
146 # Revision data is stored as a pointer to the delta producing this
145 # revision and the file revision whose delta should be applied before
147 # revision and the file revision whose delta should be applied before
146 # that one. One can reconstruct the delta chain by recursively following
148 # that one. One can reconstruct the delta chain by recursively following
147 # the delta base revision pointers until one encounters NULL.
149 # the delta base revision pointers until one encounters NULL.
148 #
150 #
149 # flags column holds bitwise integer flags controlling storage options.
151 # flags column holds bitwise integer flags controlling storage options.
150 # These flags are defined by the FLAG_* constants.
152 # These flags are defined by the FLAG_* constants.
151 r'CREATE TABLE fileindex ('
153 r'CREATE TABLE fileindex ('
152 r' id INTEGER PRIMARY KEY, '
154 r' id INTEGER PRIMARY KEY, '
153 r' pathid INTEGER REFERENCES filepath(id), '
155 r' pathid INTEGER REFERENCES filepath(id), '
154 r' revnum INTEGER NOT NULL, '
156 r' revnum INTEGER NOT NULL, '
155 r' p1rev INTEGER NOT NULL, '
157 r' p1rev INTEGER NOT NULL, '
156 r' p2rev INTEGER NOT NULL, '
158 r' p2rev INTEGER NOT NULL, '
157 r' linkrev INTEGER NOT NULL, '
159 r' linkrev INTEGER NOT NULL, '
158 r' flags INTEGER NOT NULL, '
160 r' flags INTEGER NOT NULL, '
159 r' deltaid INTEGER REFERENCES delta(id), '
161 r' deltaid INTEGER REFERENCES delta(id), '
160 r' deltabaseid INTEGER REFERENCES fileindex(id), '
162 r' deltabaseid INTEGER REFERENCES fileindex(id), '
161 r' node BLOB NOT NULL '
163 r' node BLOB NOT NULL '
162 r')',
164 r')',
163
165
164 r'CREATE UNIQUE INDEX fileindex_pathrevnum '
166 r'CREATE UNIQUE INDEX fileindex_pathrevnum '
165 r' ON fileindex (pathid, revnum)',
167 r' ON fileindex (pathid, revnum)',
166
168
167 r'CREATE UNIQUE INDEX fileindex_pathnode '
169 r'CREATE UNIQUE INDEX fileindex_pathnode '
168 r' ON fileindex (pathid, node)',
170 r' ON fileindex (pathid, node)',
169
171
170 # Provide a view over all file data for convenience.
172 # Provide a view over all file data for convenience.
171 r'CREATE VIEW filedata AS '
173 r'CREATE VIEW filedata AS '
172 r'SELECT '
174 r'SELECT '
173 r' fileindex.id AS id, '
175 r' fileindex.id AS id, '
174 r' filepath.id AS pathid, '
176 r' filepath.id AS pathid, '
175 r' filepath.path AS path, '
177 r' filepath.path AS path, '
176 r' fileindex.revnum AS revnum, '
178 r' fileindex.revnum AS revnum, '
177 r' fileindex.node AS node, '
179 r' fileindex.node AS node, '
178 r' fileindex.p1rev AS p1rev, '
180 r' fileindex.p1rev AS p1rev, '
179 r' fileindex.p2rev AS p2rev, '
181 r' fileindex.p2rev AS p2rev, '
180 r' fileindex.linkrev AS linkrev, '
182 r' fileindex.linkrev AS linkrev, '
181 r' fileindex.flags AS flags, '
183 r' fileindex.flags AS flags, '
182 r' fileindex.deltaid AS deltaid, '
184 r' fileindex.deltaid AS deltaid, '
183 r' fileindex.deltabaseid AS deltabaseid '
185 r' fileindex.deltabaseid AS deltabaseid '
184 r'FROM filepath, fileindex '
186 r'FROM filepath, fileindex '
185 r'WHERE fileindex.pathid=filepath.id',
187 r'WHERE fileindex.pathid=filepath.id',
186
188
187 r'PRAGMA user_version=%d' % CURRENT_SCHEMA_VERSION,
189 r'PRAGMA user_version=%d' % CURRENT_SCHEMA_VERSION,
188 ]
190 ]
189
191
190 def resolvedeltachain(db, pathid, node, revisioncache,
192 def resolvedeltachain(db, pathid, node, revisioncache,
191 stoprids, zstddctx=None):
193 stoprids, zstddctx=None):
192 """Resolve a delta chain for a file node."""
194 """Resolve a delta chain for a file node."""
193
195
194 # TODO the "not in ({stops})" here is possibly slowing down the query
196 # TODO the "not in ({stops})" here is possibly slowing down the query
195 # because it needs to perform the lookup on every recursive invocation.
197 # because it needs to perform the lookup on every recursive invocation.
196 # This could possibly be faster if we created a temporary query with
198 # This could possibly be faster if we created a temporary query with
197 # baseid "poisoned" to null and limited the recursive filter to
199 # baseid "poisoned" to null and limited the recursive filter to
198 # "is not null".
200 # "is not null".
199 res = db.execute(
201 res = db.execute(
200 r'WITH RECURSIVE '
202 r'WITH RECURSIVE '
201 r' deltachain(deltaid, baseid) AS ('
203 r' deltachain(deltaid, baseid) AS ('
202 r' SELECT deltaid, deltabaseid FROM fileindex '
204 r' SELECT deltaid, deltabaseid FROM fileindex '
203 r' WHERE pathid=? AND node=? '
205 r' WHERE pathid=? AND node=? '
204 r' UNION ALL '
206 r' UNION ALL '
205 r' SELECT fileindex.deltaid, deltabaseid '
207 r' SELECT fileindex.deltaid, deltabaseid '
206 r' FROM fileindex, deltachain '
208 r' FROM fileindex, deltachain '
207 r' WHERE '
209 r' WHERE '
208 r' fileindex.id=deltachain.baseid '
210 r' fileindex.id=deltachain.baseid '
209 r' AND deltachain.baseid IS NOT NULL '
211 r' AND deltachain.baseid IS NOT NULL '
210 r' AND fileindex.id NOT IN ({stops}) '
212 r' AND fileindex.id NOT IN ({stops}) '
211 r' ) '
213 r' ) '
212 r'SELECT deltachain.baseid, compression, delta '
214 r'SELECT deltachain.baseid, compression, delta '
213 r'FROM deltachain, delta '
215 r'FROM deltachain, delta '
214 r'WHERE delta.id=deltachain.deltaid'.format(
216 r'WHERE delta.id=deltachain.deltaid'.format(
215 stops=r','.join([r'?'] * len(stoprids))),
217 stops=r','.join([r'?'] * len(stoprids))),
216 tuple([pathid, node] + list(stoprids.keys())))
218 tuple([pathid, node] + list(stoprids.keys())))
217
219
218 deltas = []
220 deltas = []
219 lastdeltabaseid = None
221 lastdeltabaseid = None
220
222
221 for deltabaseid, compression, delta in res:
223 for deltabaseid, compression, delta in res:
222 lastdeltabaseid = deltabaseid
224 lastdeltabaseid = deltabaseid
223
225
224 if compression == COMPRESSION_ZSTD:
226 if compression == COMPRESSION_ZSTD:
225 delta = zstddctx.decompress(delta)
227 delta = zstddctx.decompress(delta)
226 elif compression == COMPRESSION_NONE:
228 elif compression == COMPRESSION_NONE:
227 delta = delta
229 delta = delta
228 elif compression == COMPRESSION_ZLIB:
230 elif compression == COMPRESSION_ZLIB:
229 delta = zlib.decompress(delta)
231 delta = zlib.decompress(delta)
230 else:
232 else:
231 raise SQLiteStoreError('unhandled compression type: %d' %
233 raise SQLiteStoreError('unhandled compression type: %d' %
232 compression)
234 compression)
233
235
234 deltas.append(delta)
236 deltas.append(delta)
235
237
236 if lastdeltabaseid in stoprids:
238 if lastdeltabaseid in stoprids:
237 basetext = revisioncache[stoprids[lastdeltabaseid]]
239 basetext = revisioncache[stoprids[lastdeltabaseid]]
238 else:
240 else:
239 basetext = deltas.pop()
241 basetext = deltas.pop()
240
242
241 deltas.reverse()
243 deltas.reverse()
242 fulltext = mdiff.patches(basetext, deltas)
244 fulltext = mdiff.patches(basetext, deltas)
243
245
244 # SQLite returns buffer instances for blob columns on Python 2. This
246 # SQLite returns buffer instances for blob columns on Python 2. This
245 # type can propagate through the delta application layer. Because
247 # type can propagate through the delta application layer. Because
246 # downstream callers assume revisions are bytes, cast as needed.
248 # downstream callers assume revisions are bytes, cast as needed.
247 if not isinstance(fulltext, bytes):
249 if not isinstance(fulltext, bytes):
248 fulltext = bytes(delta)
250 fulltext = bytes(delta)
249
251
250 return fulltext
252 return fulltext
251
253
252 def insertdelta(db, compression, hash, delta):
254 def insertdelta(db, compression, hash, delta):
253 try:
255 try:
254 return db.execute(
256 return db.execute(
255 r'INSERT INTO delta (compression, hash, delta) '
257 r'INSERT INTO delta (compression, hash, delta) '
256 r'VALUES (?, ?, ?)',
258 r'VALUES (?, ?, ?)',
257 (compression, hash, delta)).lastrowid
259 (compression, hash, delta)).lastrowid
258 except sqlite3.IntegrityError:
260 except sqlite3.IntegrityError:
259 return db.execute(
261 return db.execute(
260 r'SELECT id FROM delta WHERE hash=?',
262 r'SELECT id FROM delta WHERE hash=?',
261 (hash,)).fetchone()[0]
263 (hash,)).fetchone()[0]
262
264
263 class SQLiteStoreError(error.StorageError):
265 class SQLiteStoreError(error.StorageError):
264 pass
266 pass
265
267
266 @attr.s
268 @attr.s
267 class revisionentry(object):
269 class revisionentry(object):
268 rid = attr.ib()
270 rid = attr.ib()
269 rev = attr.ib()
271 rev = attr.ib()
270 node = attr.ib()
272 node = attr.ib()
271 p1rev = attr.ib()
273 p1rev = attr.ib()
272 p2rev = attr.ib()
274 p2rev = attr.ib()
273 p1node = attr.ib()
275 p1node = attr.ib()
274 p2node = attr.ib()
276 p2node = attr.ib()
275 linkrev = attr.ib()
277 linkrev = attr.ib()
276 flags = attr.ib()
278 flags = attr.ib()
277
279
278 @interfaceutil.implementer(repository.irevisiondelta)
280 @interfaceutil.implementer(repository.irevisiondelta)
279 @attr.s(slots=True)
281 @attr.s(slots=True)
280 class sqliterevisiondelta(object):
282 class sqliterevisiondelta(object):
281 node = attr.ib()
283 node = attr.ib()
282 p1node = attr.ib()
284 p1node = attr.ib()
283 p2node = attr.ib()
285 p2node = attr.ib()
284 basenode = attr.ib()
286 basenode = attr.ib()
285 flags = attr.ib()
287 flags = attr.ib()
286 baserevisionsize = attr.ib()
288 baserevisionsize = attr.ib()
287 revision = attr.ib()
289 revision = attr.ib()
288 delta = attr.ib()
290 delta = attr.ib()
289 linknode = attr.ib(default=None)
291 linknode = attr.ib(default=None)
290
292
291 @interfaceutil.implementer(repository.iverifyproblem)
293 @interfaceutil.implementer(repository.iverifyproblem)
292 @attr.s(frozen=True)
294 @attr.s(frozen=True)
293 class sqliteproblem(object):
295 class sqliteproblem(object):
294 warning = attr.ib(default=None)
296 warning = attr.ib(default=None)
295 error = attr.ib(default=None)
297 error = attr.ib(default=None)
296 node = attr.ib(default=None)
298 node = attr.ib(default=None)
297
299
298 @interfaceutil.implementer(repository.ifilestorage)
300 @interfaceutil.implementer(repository.ifilestorage)
299 class sqlitefilestore(object):
301 class sqlitefilestore(object):
300 """Implements storage for an individual tracked path."""
302 """Implements storage for an individual tracked path."""
301
303
302 def __init__(self, db, path, compression):
304 def __init__(self, db, path, compression):
303 self._db = db
305 self._db = db
304 self._path = path
306 self._path = path
305
307
306 self._pathid = None
308 self._pathid = None
307
309
308 # revnum -> node
310 # revnum -> node
309 self._revtonode = {}
311 self._revtonode = {}
310 # node -> revnum
312 # node -> revnum
311 self._nodetorev = {}
313 self._nodetorev = {}
312 # node -> data structure
314 # node -> data structure
313 self._revisions = {}
315 self._revisions = {}
314
316
315 self._revisioncache = util.lrucachedict(10)
317 self._revisioncache = util.lrucachedict(10)
316
318
317 self._compengine = compression
319 self._compengine = compression
318
320
319 if compression == 'zstd':
321 if compression == 'zstd':
320 self._cctx = zstd.ZstdCompressor(level=3)
322 self._cctx = zstd.ZstdCompressor(level=3)
321 self._dctx = zstd.ZstdDecompressor()
323 self._dctx = zstd.ZstdDecompressor()
322 else:
324 else:
323 self._cctx = None
325 self._cctx = None
324 self._dctx = None
326 self._dctx = None
325
327
326 self._refreshindex()
328 self._refreshindex()
327
329
328 def _refreshindex(self):
330 def _refreshindex(self):
329 self._revtonode = {}
331 self._revtonode = {}
330 self._nodetorev = {}
332 self._nodetorev = {}
331 self._revisions = {}
333 self._revisions = {}
332
334
333 res = list(self._db.execute(
335 res = list(self._db.execute(
334 r'SELECT id FROM filepath WHERE path=?', (self._path,)))
336 r'SELECT id FROM filepath WHERE path=?', (self._path,)))
335
337
336 if not res:
338 if not res:
337 self._pathid = None
339 self._pathid = None
338 return
340 return
339
341
340 self._pathid = res[0][0]
342 self._pathid = res[0][0]
341
343
342 res = self._db.execute(
344 res = self._db.execute(
343 r'SELECT id, revnum, node, p1rev, p2rev, linkrev, flags '
345 r'SELECT id, revnum, node, p1rev, p2rev, linkrev, flags '
344 r'FROM fileindex '
346 r'FROM fileindex '
345 r'WHERE pathid=? '
347 r'WHERE pathid=? '
346 r'ORDER BY revnum ASC',
348 r'ORDER BY revnum ASC',
347 (self._pathid,))
349 (self._pathid,))
348
350
349 for i, row in enumerate(res):
351 for i, row in enumerate(res):
350 rid, rev, node, p1rev, p2rev, linkrev, flags = row
352 rid, rev, node, p1rev, p2rev, linkrev, flags = row
351
353
352 if i != rev:
354 if i != rev:
353 raise SQLiteStoreError(_('sqlite database has inconsistent '
355 raise SQLiteStoreError(_('sqlite database has inconsistent '
354 'revision numbers'))
356 'revision numbers'))
355
357
356 if p1rev == nullrev:
358 if p1rev == nullrev:
357 p1node = nullid
359 p1node = nullid
358 else:
360 else:
359 p1node = self._revtonode[p1rev]
361 p1node = self._revtonode[p1rev]
360
362
361 if p2rev == nullrev:
363 if p2rev == nullrev:
362 p2node = nullid
364 p2node = nullid
363 else:
365 else:
364 p2node = self._revtonode[p2rev]
366 p2node = self._revtonode[p2rev]
365
367
366 entry = revisionentry(
368 entry = revisionentry(
367 rid=rid,
369 rid=rid,
368 rev=rev,
370 rev=rev,
369 node=node,
371 node=node,
370 p1rev=p1rev,
372 p1rev=p1rev,
371 p2rev=p2rev,
373 p2rev=p2rev,
372 p1node=p1node,
374 p1node=p1node,
373 p2node=p2node,
375 p2node=p2node,
374 linkrev=linkrev,
376 linkrev=linkrev,
375 flags=flags)
377 flags=flags)
376
378
377 self._revtonode[rev] = node
379 self._revtonode[rev] = node
378 self._nodetorev[node] = rev
380 self._nodetorev[node] = rev
379 self._revisions[node] = entry
381 self._revisions[node] = entry
380
382
381 # Start of ifileindex interface.
383 # Start of ifileindex interface.
382
384
383 def __len__(self):
385 def __len__(self):
384 return len(self._revisions)
386 return len(self._revisions)
385
387
386 def __iter__(self):
388 def __iter__(self):
387 return iter(pycompat.xrange(len(self._revisions)))
389 return iter(pycompat.xrange(len(self._revisions)))
388
390
389 def hasnode(self, node):
391 def hasnode(self, node):
390 if node == nullid:
392 if node == nullid:
391 return False
393 return False
392
394
393 return node in self._nodetorev
395 return node in self._nodetorev
394
396
395 def revs(self, start=0, stop=None):
397 def revs(self, start=0, stop=None):
396 return storageutil.iterrevs(len(self._revisions), start=start,
398 return storageutil.iterrevs(len(self._revisions), start=start,
397 stop=stop)
399 stop=stop)
398
400
399 def parents(self, node):
401 def parents(self, node):
400 if node == nullid:
402 if node == nullid:
401 return nullid, nullid
403 return nullid, nullid
402
404
403 if node not in self._revisions:
405 if node not in self._revisions:
404 raise error.LookupError(node, self._path, _('no node'))
406 raise error.LookupError(node, self._path, _('no node'))
405
407
406 entry = self._revisions[node]
408 entry = self._revisions[node]
407 return entry.p1node, entry.p2node
409 return entry.p1node, entry.p2node
408
410
409 def parentrevs(self, rev):
411 def parentrevs(self, rev):
410 if rev == nullrev:
412 if rev == nullrev:
411 return nullrev, nullrev
413 return nullrev, nullrev
412
414
413 if rev not in self._revtonode:
415 if rev not in self._revtonode:
414 raise IndexError(rev)
416 raise IndexError(rev)
415
417
416 entry = self._revisions[self._revtonode[rev]]
418 entry = self._revisions[self._revtonode[rev]]
417 return entry.p1rev, entry.p2rev
419 return entry.p1rev, entry.p2rev
418
420
419 def rev(self, node):
421 def rev(self, node):
420 if node == nullid:
422 if node == nullid:
421 return nullrev
423 return nullrev
422
424
423 if node not in self._nodetorev:
425 if node not in self._nodetorev:
424 raise error.LookupError(node, self._path, _('no node'))
426 raise error.LookupError(node, self._path, _('no node'))
425
427
426 return self._nodetorev[node]
428 return self._nodetorev[node]
427
429
428 def node(self, rev):
430 def node(self, rev):
429 if rev == nullrev:
431 if rev == nullrev:
430 return nullid
432 return nullid
431
433
432 if rev not in self._revtonode:
434 if rev not in self._revtonode:
433 raise IndexError(rev)
435 raise IndexError(rev)
434
436
435 return self._revtonode[rev]
437 return self._revtonode[rev]
436
438
437 def lookup(self, node):
439 def lookup(self, node):
438 return storageutil.fileidlookup(self, node, self._path)
440 return storageutil.fileidlookup(self, node, self._path)
439
441
440 def linkrev(self, rev):
442 def linkrev(self, rev):
441 if rev == nullrev:
443 if rev == nullrev:
442 return nullrev
444 return nullrev
443
445
444 if rev not in self._revtonode:
446 if rev not in self._revtonode:
445 raise IndexError(rev)
447 raise IndexError(rev)
446
448
447 entry = self._revisions[self._revtonode[rev]]
449 entry = self._revisions[self._revtonode[rev]]
448 return entry.linkrev
450 return entry.linkrev
449
451
450 def iscensored(self, rev):
452 def iscensored(self, rev):
451 if rev == nullrev:
453 if rev == nullrev:
452 return False
454 return False
453
455
454 if rev not in self._revtonode:
456 if rev not in self._revtonode:
455 raise IndexError(rev)
457 raise IndexError(rev)
456
458
457 return self._revisions[self._revtonode[rev]].flags & FLAG_CENSORED
459 return self._revisions[self._revtonode[rev]].flags & FLAG_CENSORED
458
460
459 def commonancestorsheads(self, node1, node2):
461 def commonancestorsheads(self, node1, node2):
460 rev1 = self.rev(node1)
462 rev1 = self.rev(node1)
461 rev2 = self.rev(node2)
463 rev2 = self.rev(node2)
462
464
463 ancestors = ancestor.commonancestorsheads(self.parentrevs, rev1, rev2)
465 ancestors = ancestor.commonancestorsheads(self.parentrevs, rev1, rev2)
464 return pycompat.maplist(self.node, ancestors)
466 return pycompat.maplist(self.node, ancestors)
465
467
466 def descendants(self, revs):
468 def descendants(self, revs):
467 # TODO we could implement this using a recursive SQL query, which
469 # TODO we could implement this using a recursive SQL query, which
468 # might be faster.
470 # might be faster.
469 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
471 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
470
472
471 def heads(self, start=None, stop=None):
473 def heads(self, start=None, stop=None):
472 if start is None and stop is None:
474 if start is None and stop is None:
473 if not len(self):
475 if not len(self):
474 return [nullid]
476 return [nullid]
475
477
476 startrev = self.rev(start) if start is not None else nullrev
478 startrev = self.rev(start) if start is not None else nullrev
477 stoprevs = {self.rev(n) for n in stop or []}
479 stoprevs = {self.rev(n) for n in stop or []}
478
480
479 revs = dagop.headrevssubset(self.revs, self.parentrevs,
481 revs = dagop.headrevssubset(self.revs, self.parentrevs,
480 startrev=startrev, stoprevs=stoprevs)
482 startrev=startrev, stoprevs=stoprevs)
481
483
482 return [self.node(rev) for rev in revs]
484 return [self.node(rev) for rev in revs]
483
485
484 def children(self, node):
486 def children(self, node):
485 rev = self.rev(node)
487 rev = self.rev(node)
486
488
487 res = self._db.execute(
489 res = self._db.execute(
488 r'SELECT'
490 r'SELECT'
489 r' node '
491 r' node '
490 r' FROM filedata '
492 r' FROM filedata '
491 r' WHERE path=? AND (p1rev=? OR p2rev=?) '
493 r' WHERE path=? AND (p1rev=? OR p2rev=?) '
492 r' ORDER BY revnum ASC',
494 r' ORDER BY revnum ASC',
493 (self._path, rev, rev))
495 (self._path, rev, rev))
494
496
495 return [row[0] for row in res]
497 return [row[0] for row in res]
496
498
497 # End of ifileindex interface.
499 # End of ifileindex interface.
498
500
499 # Start of ifiledata interface.
501 # Start of ifiledata interface.
500
502
501 def size(self, rev):
503 def size(self, rev):
502 if rev == nullrev:
504 if rev == nullrev:
503 return 0
505 return 0
504
506
505 if rev not in self._revtonode:
507 if rev not in self._revtonode:
506 raise IndexError(rev)
508 raise IndexError(rev)
507
509
508 node = self._revtonode[rev]
510 node = self._revtonode[rev]
509
511
510 if self.renamed(node):
512 if self.renamed(node):
511 return len(self.read(node))
513 return len(self.read(node))
512
514
513 return len(self.revision(node))
515 return len(self.revision(node))
514
516
515 def revision(self, node, raw=False, _verifyhash=True):
517 def revision(self, node, raw=False, _verifyhash=True):
516 if node in (nullid, nullrev):
518 if node in (nullid, nullrev):
517 return b''
519 return b''
518
520
519 if isinstance(node, int):
521 if isinstance(node, int):
520 node = self.node(node)
522 node = self.node(node)
521
523
522 if node not in self._nodetorev:
524 if node not in self._nodetorev:
523 raise error.LookupError(node, self._path, _('no node'))
525 raise error.LookupError(node, self._path, _('no node'))
524
526
525 if node in self._revisioncache:
527 if node in self._revisioncache:
526 return self._revisioncache[node]
528 return self._revisioncache[node]
527
529
528 # Because we have a fulltext revision cache, we are able to
530 # Because we have a fulltext revision cache, we are able to
529 # short-circuit delta chain traversal and decompression as soon as
531 # short-circuit delta chain traversal and decompression as soon as
530 # we encounter a revision in the cache.
532 # we encounter a revision in the cache.
531
533
532 stoprids = {self._revisions[n].rid: n
534 stoprids = {self._revisions[n].rid: n
533 for n in self._revisioncache}
535 for n in self._revisioncache}
534
536
535 if not stoprids:
537 if not stoprids:
536 stoprids[-1] = None
538 stoprids[-1] = None
537
539
538 fulltext = resolvedeltachain(self._db, self._pathid, node,
540 fulltext = resolvedeltachain(self._db, self._pathid, node,
539 self._revisioncache, stoprids,
541 self._revisioncache, stoprids,
540 zstddctx=self._dctx)
542 zstddctx=self._dctx)
541
543
542 # Don't verify hashes if parent nodes were rewritten, as the hash
544 # Don't verify hashes if parent nodes were rewritten, as the hash
543 # wouldn't verify.
545 # wouldn't verify.
544 if self._revisions[node].flags & (FLAG_MISSING_P1 | FLAG_MISSING_P2):
546 if self._revisions[node].flags & (FLAG_MISSING_P1 | FLAG_MISSING_P2):
545 _verifyhash = False
547 _verifyhash = False
546
548
547 if _verifyhash:
549 if _verifyhash:
548 self._checkhash(fulltext, node)
550 self._checkhash(fulltext, node)
549 self._revisioncache[node] = fulltext
551 self._revisioncache[node] = fulltext
550
552
551 return fulltext
553 return fulltext
552
554
553 def rawdata(self, *args, **kwargs):
555 def rawdata(self, *args, **kwargs):
554 return self.revision(*args, **kwargs)
556 return self.revision(*args, **kwargs)
555
557
556 def read(self, node):
558 def read(self, node):
557 return storageutil.filtermetadata(self.revision(node))
559 return storageutil.filtermetadata(self.revision(node))
558
560
559 def renamed(self, node):
561 def renamed(self, node):
560 return storageutil.filerevisioncopied(self, node)
562 return storageutil.filerevisioncopied(self, node)
561
563
562 def cmp(self, node, fulltext):
564 def cmp(self, node, fulltext):
563 return not storageutil.filedataequivalent(self, node, fulltext)
565 return not storageutil.filedataequivalent(self, node, fulltext)
564
566
565 def emitrevisions(self, nodes, nodesorder=None, revisiondata=False,
567 def emitrevisions(self, nodes, nodesorder=None, revisiondata=False,
566 assumehaveparentrevisions=False,
568 assumehaveparentrevisions=False,
567 deltamode=repository.CG_DELTAMODE_STD):
569 deltamode=repository.CG_DELTAMODE_STD):
568 if nodesorder not in ('nodes', 'storage', 'linear', None):
570 if nodesorder not in ('nodes', 'storage', 'linear', None):
569 raise error.ProgrammingError('unhandled value for nodesorder: %s' %
571 raise error.ProgrammingError('unhandled value for nodesorder: %s' %
570 nodesorder)
572 nodesorder)
571
573
572 nodes = [n for n in nodes if n != nullid]
574 nodes = [n for n in nodes if n != nullid]
573
575
574 if not nodes:
576 if not nodes:
575 return
577 return
576
578
577 # TODO perform in a single query.
579 # TODO perform in a single query.
578 res = self._db.execute(
580 res = self._db.execute(
579 r'SELECT revnum, deltaid FROM fileindex '
581 r'SELECT revnum, deltaid FROM fileindex '
580 r'WHERE pathid=? '
582 r'WHERE pathid=? '
581 r' AND node in (%s)' % (r','.join([r'?'] * len(nodes))),
583 r' AND node in (%s)' % (r','.join([r'?'] * len(nodes))),
582 tuple([self._pathid] + nodes))
584 tuple([self._pathid] + nodes))
583
585
584 deltabases = {}
586 deltabases = {}
585
587
586 for rev, deltaid in res:
588 for rev, deltaid in res:
587 res = self._db.execute(
589 res = self._db.execute(
588 r'SELECT revnum from fileindex WHERE pathid=? AND deltaid=?',
590 r'SELECT revnum from fileindex WHERE pathid=? AND deltaid=?',
589 (self._pathid, deltaid))
591 (self._pathid, deltaid))
590 deltabases[rev] = res.fetchone()[0]
592 deltabases[rev] = res.fetchone()[0]
591
593
592 # TODO define revdifffn so we can use delta from storage.
594 # TODO define revdifffn so we can use delta from storage.
593 for delta in storageutil.emitrevisions(
595 for delta in storageutil.emitrevisions(
594 self, nodes, nodesorder, sqliterevisiondelta,
596 self, nodes, nodesorder, sqliterevisiondelta,
595 deltaparentfn=deltabases.__getitem__,
597 deltaparentfn=deltabases.__getitem__,
596 revisiondata=revisiondata,
598 revisiondata=revisiondata,
597 assumehaveparentrevisions=assumehaveparentrevisions,
599 assumehaveparentrevisions=assumehaveparentrevisions,
598 deltamode=deltamode):
600 deltamode=deltamode):
599
601
600 yield delta
602 yield delta
601
603
602 # End of ifiledata interface.
604 # End of ifiledata interface.
603
605
604 # Start of ifilemutation interface.
606 # Start of ifilemutation interface.
605
607
606 def add(self, filedata, meta, transaction, linkrev, p1, p2):
608 def add(self, filedata, meta, transaction, linkrev, p1, p2):
607 if meta or filedata.startswith(b'\x01\n'):
609 if meta or filedata.startswith(b'\x01\n'):
608 filedata = storageutil.packmeta(meta, filedata)
610 filedata = storageutil.packmeta(meta, filedata)
609
611
610 return self.addrevision(filedata, transaction, linkrev, p1, p2)
612 return self.addrevision(filedata, transaction, linkrev, p1, p2)
611
613
612 def addrevision(self, revisiondata, transaction, linkrev, p1, p2, node=None,
614 def addrevision(self, revisiondata, transaction, linkrev, p1, p2, node=None,
613 flags=0, cachedelta=None):
615 flags=0, cachedelta=None):
614 if flags:
616 if flags:
615 raise SQLiteStoreError(_('flags not supported on revisions'))
617 raise SQLiteStoreError(_('flags not supported on revisions'))
616
618
617 validatehash = node is not None
619 validatehash = node is not None
618 node = node or storageutil.hashrevisionsha1(revisiondata, p1, p2)
620 node = node or storageutil.hashrevisionsha1(revisiondata, p1, p2)
619
621
620 if validatehash:
622 if validatehash:
621 self._checkhash(revisiondata, node, p1, p2)
623 self._checkhash(revisiondata, node, p1, p2)
622
624
623 if node in self._nodetorev:
625 if node in self._nodetorev:
624 return node
626 return node
625
627
626 node = self._addrawrevision(node, revisiondata, transaction, linkrev,
628 node = self._addrawrevision(node, revisiondata, transaction, linkrev,
627 p1, p2)
629 p1, p2)
628
630
629 self._revisioncache[node] = revisiondata
631 self._revisioncache[node] = revisiondata
630 return node
632 return node
631
633
632 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None,
634 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None,
633 maybemissingparents=False):
635 maybemissingparents=False):
634 nodes = []
636 nodes = []
635
637
636 for node, p1, p2, linknode, deltabase, delta, wireflags in deltas:
638 for node, p1, p2, linknode, deltabase, delta, wireflags in deltas:
637 storeflags = 0
639 storeflags = 0
638
640
639 if wireflags & repository.REVISION_FLAG_CENSORED:
641 if wireflags & repository.REVISION_FLAG_CENSORED:
640 storeflags |= FLAG_CENSORED
642 storeflags |= FLAG_CENSORED
641
643
642 if wireflags & ~repository.REVISION_FLAG_CENSORED:
644 if wireflags & ~repository.REVISION_FLAG_CENSORED:
643 raise SQLiteStoreError('unhandled revision flag')
645 raise SQLiteStoreError('unhandled revision flag')
644
646
645 if maybemissingparents:
647 if maybemissingparents:
646 if p1 != nullid and not self.hasnode(p1):
648 if p1 != nullid and not self.hasnode(p1):
647 p1 = nullid
649 p1 = nullid
648 storeflags |= FLAG_MISSING_P1
650 storeflags |= FLAG_MISSING_P1
649
651
650 if p2 != nullid and not self.hasnode(p2):
652 if p2 != nullid and not self.hasnode(p2):
651 p2 = nullid
653 p2 = nullid
652 storeflags |= FLAG_MISSING_P2
654 storeflags |= FLAG_MISSING_P2
653
655
654 baserev = self.rev(deltabase)
656 baserev = self.rev(deltabase)
655
657
656 # If base is censored, delta must be full replacement in a single
658 # If base is censored, delta must be full replacement in a single
657 # patch operation.
659 # patch operation.
658 if baserev != nullrev and self.iscensored(baserev):
660 if baserev != nullrev and self.iscensored(baserev):
659 hlen = struct.calcsize('>lll')
661 hlen = struct.calcsize('>lll')
660 oldlen = len(self.rawdata(deltabase, _verifyhash=False))
662 oldlen = len(self.rawdata(deltabase, _verifyhash=False))
661 newlen = len(delta) - hlen
663 newlen = len(delta) - hlen
662
664
663 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
665 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
664 raise error.CensoredBaseError(self._path,
666 raise error.CensoredBaseError(self._path,
665 deltabase)
667 deltabase)
666
668
667 if (not (storeflags & FLAG_CENSORED)
669 if (not (storeflags & FLAG_CENSORED)
668 and storageutil.deltaiscensored(
670 and storageutil.deltaiscensored(
669 delta, baserev, lambda x: len(self.rawdata(x)))):
671 delta, baserev, lambda x: len(self.rawdata(x)))):
670 storeflags |= FLAG_CENSORED
672 storeflags |= FLAG_CENSORED
671
673
672 linkrev = linkmapper(linknode)
674 linkrev = linkmapper(linknode)
673
675
674 nodes.append(node)
676 nodes.append(node)
675
677
676 if node in self._revisions:
678 if node in self._revisions:
677 # Possibly reset parents to make them proper.
679 # Possibly reset parents to make them proper.
678 entry = self._revisions[node]
680 entry = self._revisions[node]
679
681
680 if entry.flags & FLAG_MISSING_P1 and p1 != nullid:
682 if entry.flags & FLAG_MISSING_P1 and p1 != nullid:
681 entry.p1node = p1
683 entry.p1node = p1
682 entry.p1rev = self._nodetorev[p1]
684 entry.p1rev = self._nodetorev[p1]
683 entry.flags &= ~FLAG_MISSING_P1
685 entry.flags &= ~FLAG_MISSING_P1
684
686
685 self._db.execute(
687 self._db.execute(
686 r'UPDATE fileindex SET p1rev=?, flags=? '
688 r'UPDATE fileindex SET p1rev=?, flags=? '
687 r'WHERE id=?',
689 r'WHERE id=?',
688 (self._nodetorev[p1], entry.flags, entry.rid))
690 (self._nodetorev[p1], entry.flags, entry.rid))
689
691
690 if entry.flags & FLAG_MISSING_P2 and p2 != nullid:
692 if entry.flags & FLAG_MISSING_P2 and p2 != nullid:
691 entry.p2node = p2
693 entry.p2node = p2
692 entry.p2rev = self._nodetorev[p2]
694 entry.p2rev = self._nodetorev[p2]
693 entry.flags &= ~FLAG_MISSING_P2
695 entry.flags &= ~FLAG_MISSING_P2
694
696
695 self._db.execute(
697 self._db.execute(
696 r'UPDATE fileindex SET p2rev=?, flags=? '
698 r'UPDATE fileindex SET p2rev=?, flags=? '
697 r'WHERE id=?',
699 r'WHERE id=?',
698 (self._nodetorev[p1], entry.flags, entry.rid))
700 (self._nodetorev[p1], entry.flags, entry.rid))
699
701
700 continue
702 continue
701
703
702 if deltabase == nullid:
704 if deltabase == nullid:
703 text = mdiff.patch(b'', delta)
705 text = mdiff.patch(b'', delta)
704 storedelta = None
706 storedelta = None
705 else:
707 else:
706 text = None
708 text = None
707 storedelta = (deltabase, delta)
709 storedelta = (deltabase, delta)
708
710
709 self._addrawrevision(node, text, transaction, linkrev, p1, p2,
711 self._addrawrevision(node, text, transaction, linkrev, p1, p2,
710 storedelta=storedelta, flags=storeflags)
712 storedelta=storedelta, flags=storeflags)
711
713
712 if addrevisioncb:
714 if addrevisioncb:
713 addrevisioncb(self, node)
715 addrevisioncb(self, node)
714
716
715 return nodes
717 return nodes
716
718
717 def censorrevision(self, tr, censornode, tombstone=b''):
719 def censorrevision(self, tr, censornode, tombstone=b''):
718 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
720 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
719
721
720 # This restriction is cargo culted from revlogs and makes no sense for
722 # This restriction is cargo culted from revlogs and makes no sense for
721 # SQLite, since columns can be resized at will.
723 # SQLite, since columns can be resized at will.
722 if len(tombstone) > len(self.rawdata(censornode)):
724 if len(tombstone) > len(self.rawdata(censornode)):
723 raise error.Abort(_('censor tombstone must be no longer than '
725 raise error.Abort(_('censor tombstone must be no longer than '
724 'censored data'))
726 'censored data'))
725
727
726 # We need to replace the censored revision's data with the tombstone.
728 # We need to replace the censored revision's data with the tombstone.
727 # But replacing that data will have implications for delta chains that
729 # But replacing that data will have implications for delta chains that
728 # reference it.
730 # reference it.
729 #
731 #
730 # While "better," more complex strategies are possible, we do something
732 # While "better," more complex strategies are possible, we do something
731 # simple: we find delta chain children of the censored revision and we
733 # simple: we find delta chain children of the censored revision and we
732 # replace those incremental deltas with fulltexts of their corresponding
734 # replace those incremental deltas with fulltexts of their corresponding
733 # revision. Then we delete the now-unreferenced delta and original
735 # revision. Then we delete the now-unreferenced delta and original
734 # revision and insert a replacement.
736 # revision and insert a replacement.
735
737
736 # Find the delta to be censored.
738 # Find the delta to be censored.
737 censoreddeltaid = self._db.execute(
739 censoreddeltaid = self._db.execute(
738 r'SELECT deltaid FROM fileindex WHERE id=?',
740 r'SELECT deltaid FROM fileindex WHERE id=?',
739 (self._revisions[censornode].rid,)).fetchone()[0]
741 (self._revisions[censornode].rid,)).fetchone()[0]
740
742
741 # Find all its delta chain children.
743 # Find all its delta chain children.
742 # TODO once we support storing deltas for !files, we'll need to look
744 # TODO once we support storing deltas for !files, we'll need to look
743 # for those delta chains too.
745 # for those delta chains too.
744 rows = list(self._db.execute(
746 rows = list(self._db.execute(
745 r'SELECT id, pathid, node FROM fileindex '
747 r'SELECT id, pathid, node FROM fileindex '
746 r'WHERE deltabaseid=? OR deltaid=?',
748 r'WHERE deltabaseid=? OR deltaid=?',
747 (censoreddeltaid, censoreddeltaid)))
749 (censoreddeltaid, censoreddeltaid)))
748
750
749 for row in rows:
751 for row in rows:
750 rid, pathid, node = row
752 rid, pathid, node = row
751
753
752 fulltext = resolvedeltachain(self._db, pathid, node, {}, {-1: None},
754 fulltext = resolvedeltachain(self._db, pathid, node, {}, {-1: None},
753 zstddctx=self._dctx)
755 zstddctx=self._dctx)
754
756
755 deltahash = hashlib.sha1(fulltext).digest()
757 deltahash = hashlib.sha1(fulltext).digest()
756
758
757 if self._compengine == 'zstd':
759 if self._compengine == 'zstd':
758 deltablob = self._cctx.compress(fulltext)
760 deltablob = self._cctx.compress(fulltext)
759 compression = COMPRESSION_ZSTD
761 compression = COMPRESSION_ZSTD
760 elif self._compengine == 'zlib':
762 elif self._compengine == 'zlib':
761 deltablob = zlib.compress(fulltext)
763 deltablob = zlib.compress(fulltext)
762 compression = COMPRESSION_ZLIB
764 compression = COMPRESSION_ZLIB
763 elif self._compengine == 'none':
765 elif self._compengine == 'none':
764 deltablob = fulltext
766 deltablob = fulltext
765 compression = COMPRESSION_NONE
767 compression = COMPRESSION_NONE
766 else:
768 else:
767 raise error.ProgrammingError('unhandled compression engine: %s'
769 raise error.ProgrammingError('unhandled compression engine: %s'
768 % self._compengine)
770 % self._compengine)
769
771
770 if len(deltablob) >= len(fulltext):
772 if len(deltablob) >= len(fulltext):
771 deltablob = fulltext
773 deltablob = fulltext
772 compression = COMPRESSION_NONE
774 compression = COMPRESSION_NONE
773
775
774 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
776 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
775
777
776 self._db.execute(
778 self._db.execute(
777 r'UPDATE fileindex SET deltaid=?, deltabaseid=NULL '
779 r'UPDATE fileindex SET deltaid=?, deltabaseid=NULL '
778 r'WHERE id=?', (deltaid, rid))
780 r'WHERE id=?', (deltaid, rid))
779
781
780 # Now create the tombstone delta and replace the delta on the censored
782 # Now create the tombstone delta and replace the delta on the censored
781 # node.
783 # node.
782 deltahash = hashlib.sha1(tombstone).digest()
784 deltahash = hashlib.sha1(tombstone).digest()
783 tombstonedeltaid = insertdelta(self._db, COMPRESSION_NONE,
785 tombstonedeltaid = insertdelta(self._db, COMPRESSION_NONE,
784 deltahash, tombstone)
786 deltahash, tombstone)
785
787
786 flags = self._revisions[censornode].flags
788 flags = self._revisions[censornode].flags
787 flags |= FLAG_CENSORED
789 flags |= FLAG_CENSORED
788
790
789 self._db.execute(
791 self._db.execute(
790 r'UPDATE fileindex SET flags=?, deltaid=?, deltabaseid=NULL '
792 r'UPDATE fileindex SET flags=?, deltaid=?, deltabaseid=NULL '
791 r'WHERE pathid=? AND node=?',
793 r'WHERE pathid=? AND node=?',
792 (flags, tombstonedeltaid, self._pathid, censornode))
794 (flags, tombstonedeltaid, self._pathid, censornode))
793
795
794 self._db.execute(
796 self._db.execute(
795 r'DELETE FROM delta WHERE id=?', (censoreddeltaid,))
797 r'DELETE FROM delta WHERE id=?', (censoreddeltaid,))
796
798
797 self._refreshindex()
799 self._refreshindex()
798 self._revisioncache.clear()
800 self._revisioncache.clear()
799
801
800 def getstrippoint(self, minlink):
802 def getstrippoint(self, minlink):
801 return storageutil.resolvestripinfo(minlink, len(self) - 1,
803 return storageutil.resolvestripinfo(minlink, len(self) - 1,
802 [self.rev(n) for n in self.heads()],
804 [self.rev(n) for n in self.heads()],
803 self.linkrev,
805 self.linkrev,
804 self.parentrevs)
806 self.parentrevs)
805
807
806 def strip(self, minlink, transaction):
808 def strip(self, minlink, transaction):
807 if not len(self):
809 if not len(self):
808 return
810 return
809
811
810 rev, _ignored = self.getstrippoint(minlink)
812 rev, _ignored = self.getstrippoint(minlink)
811
813
812 if rev == len(self):
814 if rev == len(self):
813 return
815 return
814
816
815 for rev in self.revs(rev):
817 for rev in self.revs(rev):
816 self._db.execute(
818 self._db.execute(
817 r'DELETE FROM fileindex WHERE pathid=? AND node=?',
819 r'DELETE FROM fileindex WHERE pathid=? AND node=?',
818 (self._pathid, self.node(rev)))
820 (self._pathid, self.node(rev)))
819
821
820 # TODO how should we garbage collect data in delta table?
822 # TODO how should we garbage collect data in delta table?
821
823
822 self._refreshindex()
824 self._refreshindex()
823
825
824 # End of ifilemutation interface.
826 # End of ifilemutation interface.
825
827
826 # Start of ifilestorage interface.
828 # Start of ifilestorage interface.
827
829
828 def files(self):
830 def files(self):
829 return []
831 return []
830
832
831 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
833 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
832 revisionscount=False, trackedsize=False,
834 revisionscount=False, trackedsize=False,
833 storedsize=False):
835 storedsize=False):
834 d = {}
836 d = {}
835
837
836 if exclusivefiles:
838 if exclusivefiles:
837 d['exclusivefiles'] = []
839 d['exclusivefiles'] = []
838
840
839 if sharedfiles:
841 if sharedfiles:
840 # TODO list sqlite file(s) here.
842 # TODO list sqlite file(s) here.
841 d['sharedfiles'] = []
843 d['sharedfiles'] = []
842
844
843 if revisionscount:
845 if revisionscount:
844 d['revisionscount'] = len(self)
846 d['revisionscount'] = len(self)
845
847
846 if trackedsize:
848 if trackedsize:
847 d['trackedsize'] = sum(len(self.revision(node))
849 d['trackedsize'] = sum(len(self.revision(node))
848 for node in self._nodetorev)
850 for node in self._nodetorev)
849
851
850 if storedsize:
852 if storedsize:
851 # TODO implement this?
853 # TODO implement this?
852 d['storedsize'] = None
854 d['storedsize'] = None
853
855
854 return d
856 return d
855
857
856 def verifyintegrity(self, state):
858 def verifyintegrity(self, state):
857 state['skipread'] = set()
859 state['skipread'] = set()
858
860
859 for rev in self:
861 for rev in self:
860 node = self.node(rev)
862 node = self.node(rev)
861
863
862 try:
864 try:
863 self.revision(node)
865 self.revision(node)
864 except Exception as e:
866 except Exception as e:
865 yield sqliteproblem(
867 yield sqliteproblem(
866 error=_('unpacking %s: %s') % (short(node), e),
868 error=_('unpacking %s: %s') % (short(node), e),
867 node=node)
869 node=node)
868
870
869 state['skipread'].add(node)
871 state['skipread'].add(node)
870
872
871 # End of ifilestorage interface.
873 # End of ifilestorage interface.
872
874
873 def _checkhash(self, fulltext, node, p1=None, p2=None):
875 def _checkhash(self, fulltext, node, p1=None, p2=None):
874 if p1 is None and p2 is None:
876 if p1 is None and p2 is None:
875 p1, p2 = self.parents(node)
877 p1, p2 = self.parents(node)
876
878
877 if node == storageutil.hashrevisionsha1(fulltext, p1, p2):
879 if node == storageutil.hashrevisionsha1(fulltext, p1, p2):
878 return
880 return
879
881
880 try:
882 try:
881 del self._revisioncache[node]
883 del self._revisioncache[node]
882 except KeyError:
884 except KeyError:
883 pass
885 pass
884
886
885 if storageutil.iscensoredtext(fulltext):
887 if storageutil.iscensoredtext(fulltext):
886 raise error.CensoredNodeError(self._path, node, fulltext)
888 raise error.CensoredNodeError(self._path, node, fulltext)
887
889
888 raise SQLiteStoreError(_('integrity check failed on %s') %
890 raise SQLiteStoreError(_('integrity check failed on %s') %
889 self._path)
891 self._path)
890
892
891 def _addrawrevision(self, node, revisiondata, transaction, linkrev,
893 def _addrawrevision(self, node, revisiondata, transaction, linkrev,
892 p1, p2, storedelta=None, flags=0):
894 p1, p2, storedelta=None, flags=0):
893 if self._pathid is None:
895 if self._pathid is None:
894 res = self._db.execute(
896 res = self._db.execute(
895 r'INSERT INTO filepath (path) VALUES (?)', (self._path,))
897 r'INSERT INTO filepath (path) VALUES (?)', (self._path,))
896 self._pathid = res.lastrowid
898 self._pathid = res.lastrowid
897
899
898 # For simplicity, always store a delta against p1.
900 # For simplicity, always store a delta against p1.
899 # TODO we need a lot more logic here to make behavior reasonable.
901 # TODO we need a lot more logic here to make behavior reasonable.
900
902
901 if storedelta:
903 if storedelta:
902 deltabase, delta = storedelta
904 deltabase, delta = storedelta
903
905
904 if isinstance(deltabase, int):
906 if isinstance(deltabase, int):
905 deltabase = self.node(deltabase)
907 deltabase = self.node(deltabase)
906
908
907 else:
909 else:
908 assert revisiondata is not None
910 assert revisiondata is not None
909 deltabase = p1
911 deltabase = p1
910
912
911 if deltabase == nullid:
913 if deltabase == nullid:
912 delta = revisiondata
914 delta = revisiondata
913 else:
915 else:
914 delta = mdiff.textdiff(self.revision(self.rev(deltabase)),
916 delta = mdiff.textdiff(self.revision(self.rev(deltabase)),
915 revisiondata)
917 revisiondata)
916
918
917 # File index stores a pointer to its delta and the parent delta.
919 # File index stores a pointer to its delta and the parent delta.
918 # The parent delta is stored via a pointer to the fileindex PK.
920 # The parent delta is stored via a pointer to the fileindex PK.
919 if deltabase == nullid:
921 if deltabase == nullid:
920 baseid = None
922 baseid = None
921 else:
923 else:
922 baseid = self._revisions[deltabase].rid
924 baseid = self._revisions[deltabase].rid
923
925
924 # Deltas are stored with a hash of their content. This allows
926 # Deltas are stored with a hash of their content. This allows
925 # us to de-duplicate. The table is configured to ignore conflicts
927 # us to de-duplicate. The table is configured to ignore conflicts
926 # and it is faster to just insert and silently noop than to look
928 # and it is faster to just insert and silently noop than to look
927 # first.
929 # first.
928 deltahash = hashlib.sha1(delta).digest()
930 deltahash = hashlib.sha1(delta).digest()
929
931
930 if self._compengine == 'zstd':
932 if self._compengine == 'zstd':
931 deltablob = self._cctx.compress(delta)
933 deltablob = self._cctx.compress(delta)
932 compression = COMPRESSION_ZSTD
934 compression = COMPRESSION_ZSTD
933 elif self._compengine == 'zlib':
935 elif self._compengine == 'zlib':
934 deltablob = zlib.compress(delta)
936 deltablob = zlib.compress(delta)
935 compression = COMPRESSION_ZLIB
937 compression = COMPRESSION_ZLIB
936 elif self._compengine == 'none':
938 elif self._compengine == 'none':
937 deltablob = delta
939 deltablob = delta
938 compression = COMPRESSION_NONE
940 compression = COMPRESSION_NONE
939 else:
941 else:
940 raise error.ProgrammingError('unhandled compression engine: %s' %
942 raise error.ProgrammingError('unhandled compression engine: %s' %
941 self._compengine)
943 self._compengine)
942
944
943 # Don't store compressed data if it isn't practical.
945 # Don't store compressed data if it isn't practical.
944 if len(deltablob) >= len(delta):
946 if len(deltablob) >= len(delta):
945 deltablob = delta
947 deltablob = delta
946 compression = COMPRESSION_NONE
948 compression = COMPRESSION_NONE
947
949
948 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
950 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
949
951
950 rev = len(self)
952 rev = len(self)
951
953
952 if p1 == nullid:
954 if p1 == nullid:
953 p1rev = nullrev
955 p1rev = nullrev
954 else:
956 else:
955 p1rev = self._nodetorev[p1]
957 p1rev = self._nodetorev[p1]
956
958
957 if p2 == nullid:
959 if p2 == nullid:
958 p2rev = nullrev
960 p2rev = nullrev
959 else:
961 else:
960 p2rev = self._nodetorev[p2]
962 p2rev = self._nodetorev[p2]
961
963
962 rid = self._db.execute(
964 rid = self._db.execute(
963 r'INSERT INTO fileindex ('
965 r'INSERT INTO fileindex ('
964 r' pathid, revnum, node, p1rev, p2rev, linkrev, flags, '
966 r' pathid, revnum, node, p1rev, p2rev, linkrev, flags, '
965 r' deltaid, deltabaseid) '
967 r' deltaid, deltabaseid) '
966 r' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
968 r' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
967 (self._pathid, rev, node, p1rev, p2rev, linkrev, flags,
969 (self._pathid, rev, node, p1rev, p2rev, linkrev, flags,
968 deltaid, baseid)
970 deltaid, baseid)
969 ).lastrowid
971 ).lastrowid
970
972
971 entry = revisionentry(
973 entry = revisionentry(
972 rid=rid,
974 rid=rid,
973 rev=rev,
975 rev=rev,
974 node=node,
976 node=node,
975 p1rev=p1rev,
977 p1rev=p1rev,
976 p2rev=p2rev,
978 p2rev=p2rev,
977 p1node=p1,
979 p1node=p1,
978 p2node=p2,
980 p2node=p2,
979 linkrev=linkrev,
981 linkrev=linkrev,
980 flags=flags)
982 flags=flags)
981
983
982 self._nodetorev[node] = rev
984 self._nodetorev[node] = rev
983 self._revtonode[rev] = node
985 self._revtonode[rev] = node
984 self._revisions[node] = entry
986 self._revisions[node] = entry
985
987
986 return node
988 return node
987
989
988 class sqliterepository(localrepo.localrepository):
990 class sqliterepository(localrepo.localrepository):
989 def cancopy(self):
991 def cancopy(self):
990 return False
992 return False
991
993
992 def transaction(self, *args, **kwargs):
994 def transaction(self, *args, **kwargs):
993 current = self.currenttransaction()
995 current = self.currenttransaction()
994
996
995 tr = super(sqliterepository, self).transaction(*args, **kwargs)
997 tr = super(sqliterepository, self).transaction(*args, **kwargs)
996
998
997 if current:
999 if current:
998 return tr
1000 return tr
999
1001
1000 self._dbconn.execute(r'BEGIN TRANSACTION')
1002 self._dbconn.execute(r'BEGIN TRANSACTION')
1001
1003
1002 def committransaction(_):
1004 def committransaction(_):
1003 self._dbconn.commit()
1005 self._dbconn.commit()
1004
1006
1005 tr.addfinalize('sqlitestore', committransaction)
1007 tr.addfinalize('sqlitestore', committransaction)
1006
1008
1007 return tr
1009 return tr
1008
1010
1009 @property
1011 @property
1010 def _dbconn(self):
1012 def _dbconn(self):
1011 # SQLite connections can only be used on the thread that created
1013 # SQLite connections can only be used on the thread that created
1012 # them. In most cases, this "just works." However, hgweb uses
1014 # them. In most cases, this "just works." However, hgweb uses
1013 # multiple threads.
1015 # multiple threads.
1014 tid = threading.current_thread().ident
1016 tid = threading.current_thread().ident
1015
1017
1016 if self._db:
1018 if self._db:
1017 if self._db[0] == tid:
1019 if self._db[0] == tid:
1018 return self._db[1]
1020 return self._db[1]
1019
1021
1020 db = makedb(self.svfs.join('db.sqlite'))
1022 db = makedb(self.svfs.join('db.sqlite'))
1021 self._db = (tid, db)
1023 self._db = (tid, db)
1022
1024
1023 return db
1025 return db
1024
1026
1025 def makedb(path):
1027 def makedb(path):
1026 """Construct a database handle for a database at path."""
1028 """Construct a database handle for a database at path."""
1027
1029
1028 db = sqlite3.connect(encoding.strfromlocal(path))
1030 db = sqlite3.connect(encoding.strfromlocal(path))
1029 db.text_factory = bytes
1031 db.text_factory = bytes
1030
1032
1031 res = db.execute(r'PRAGMA user_version').fetchone()[0]
1033 res = db.execute(r'PRAGMA user_version').fetchone()[0]
1032
1034
1033 # New database.
1035 # New database.
1034 if res == 0:
1036 if res == 0:
1035 for statement in CREATE_SCHEMA:
1037 for statement in CREATE_SCHEMA:
1036 db.execute(statement)
1038 db.execute(statement)
1037
1039
1038 db.commit()
1040 db.commit()
1039
1041
1040 elif res == CURRENT_SCHEMA_VERSION:
1042 elif res == CURRENT_SCHEMA_VERSION:
1041 pass
1043 pass
1042
1044
1043 else:
1045 else:
1044 raise error.Abort(_('sqlite database has unrecognized version'))
1046 raise error.Abort(_('sqlite database has unrecognized version'))
1045
1047
1046 db.execute(r'PRAGMA journal_mode=WAL')
1048 db.execute(r'PRAGMA journal_mode=WAL')
1047
1049
1048 return db
1050 return db
1049
1051
1050 def featuresetup(ui, supported):
1052 def featuresetup(ui, supported):
1051 supported.add(REQUIREMENT)
1053 supported.add(REQUIREMENT)
1052
1054
1053 if zstd:
1055 if zstd:
1054 supported.add(REQUIREMENT_ZSTD)
1056 supported.add(REQUIREMENT_ZSTD)
1055
1057
1056 supported.add(REQUIREMENT_ZLIB)
1058 supported.add(REQUIREMENT_ZLIB)
1057 supported.add(REQUIREMENT_NONE)
1059 supported.add(REQUIREMENT_NONE)
1058 supported.add(REQUIREMENT_SHALLOW_FILES)
1060 supported.add(REQUIREMENT_SHALLOW_FILES)
1059 supported.add(repository.NARROW_REQUIREMENT)
1061 supported.add(repository.NARROW_REQUIREMENT)
1060
1062
1061 def newreporequirements(orig, ui, createopts):
1063 def newreporequirements(orig, ui, createopts):
1062 if createopts['backend'] != 'sqlite':
1064 if createopts['backend'] != 'sqlite':
1063 return orig(ui, createopts)
1065 return orig(ui, createopts)
1064
1066
1065 # This restriction can be lifted once we have more confidence.
1067 # This restriction can be lifted once we have more confidence.
1066 if 'sharedrepo' in createopts:
1068 if 'sharedrepo' in createopts:
1067 raise error.Abort(_('shared repositories not supported with SQLite '
1069 raise error.Abort(_('shared repositories not supported with SQLite '
1068 'store'))
1070 'store'))
1069
1071
1070 # This filtering is out of an abundance of caution: we want to ensure
1072 # This filtering is out of an abundance of caution: we want to ensure
1071 # we honor creation options and we do that by annotating exactly the
1073 # we honor creation options and we do that by annotating exactly the
1072 # creation options we recognize.
1074 # creation options we recognize.
1073 known = {
1075 known = {
1074 'narrowfiles',
1076 'narrowfiles',
1075 'backend',
1077 'backend',
1076 'shallowfilestore',
1078 'shallowfilestore',
1077 }
1079 }
1078
1080
1079 unsupported = set(createopts) - known
1081 unsupported = set(createopts) - known
1080 if unsupported:
1082 if unsupported:
1081 raise error.Abort(_('SQLite store does not support repo creation '
1083 raise error.Abort(_('SQLite store does not support repo creation '
1082 'option: %s') % ', '.join(sorted(unsupported)))
1084 'option: %s') % ', '.join(sorted(unsupported)))
1083
1085
1084 # Since we're a hybrid store that still relies on revlogs, we fall back
1086 # Since we're a hybrid store that still relies on revlogs, we fall back
1085 # to using the revlogv1 backend's storage requirements then adding our
1087 # to using the revlogv1 backend's storage requirements then adding our
1086 # own requirement.
1088 # own requirement.
1087 createopts['backend'] = 'revlogv1'
1089 createopts['backend'] = 'revlogv1'
1088 requirements = orig(ui, createopts)
1090 requirements = orig(ui, createopts)
1089 requirements.add(REQUIREMENT)
1091 requirements.add(REQUIREMENT)
1090
1092
1091 compression = ui.config('storage', 'sqlite.compression')
1093 compression = ui.config('storage', 'sqlite.compression')
1092
1094
1093 if compression == 'zstd' and not zstd:
1095 if compression == 'zstd' and not zstd:
1094 raise error.Abort(_('storage.sqlite.compression set to "zstd" but '
1096 raise error.Abort(_('storage.sqlite.compression set to "zstd" but '
1095 'zstandard compression not available to this '
1097 'zstandard compression not available to this '
1096 'Mercurial install'))
1098 'Mercurial install'))
1097
1099
1098 if compression == 'zstd':
1100 if compression == 'zstd':
1099 requirements.add(REQUIREMENT_ZSTD)
1101 requirements.add(REQUIREMENT_ZSTD)
1100 elif compression == 'zlib':
1102 elif compression == 'zlib':
1101 requirements.add(REQUIREMENT_ZLIB)
1103 requirements.add(REQUIREMENT_ZLIB)
1102 elif compression == 'none':
1104 elif compression == 'none':
1103 requirements.add(REQUIREMENT_NONE)
1105 requirements.add(REQUIREMENT_NONE)
1104 else:
1106 else:
1105 raise error.Abort(_('unknown compression engine defined in '
1107 raise error.Abort(_('unknown compression engine defined in '
1106 'storage.sqlite.compression: %s') % compression)
1108 'storage.sqlite.compression: %s') % compression)
1107
1109
1108 if createopts.get('shallowfilestore'):
1110 if createopts.get('shallowfilestore'):
1109 requirements.add(REQUIREMENT_SHALLOW_FILES)
1111 requirements.add(REQUIREMENT_SHALLOW_FILES)
1110
1112
1111 return requirements
1113 return requirements
1112
1114
1113 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
1115 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
1114 class sqlitefilestorage(object):
1116 class sqlitefilestorage(object):
1115 """Repository file storage backed by SQLite."""
1117 """Repository file storage backed by SQLite."""
1116 def file(self, path):
1118 def file(self, path):
1117 if path[0] == b'/':
1119 if path[0] == b'/':
1118 path = path[1:]
1120 path = path[1:]
1119
1121
1120 if REQUIREMENT_ZSTD in self.requirements:
1122 if REQUIREMENT_ZSTD in self.requirements:
1121 compression = 'zstd'
1123 compression = 'zstd'
1122 elif REQUIREMENT_ZLIB in self.requirements:
1124 elif REQUIREMENT_ZLIB in self.requirements:
1123 compression = 'zlib'
1125 compression = 'zlib'
1124 elif REQUIREMENT_NONE in self.requirements:
1126 elif REQUIREMENT_NONE in self.requirements:
1125 compression = 'none'
1127 compression = 'none'
1126 else:
1128 else:
1127 raise error.Abort(_('unable to determine what compression engine '
1129 raise error.Abort(_('unable to determine what compression engine '
1128 'to use for SQLite storage'))
1130 'to use for SQLite storage'))
1129
1131
1130 return sqlitefilestore(self._dbconn, path, compression)
1132 return sqlitefilestore(self._dbconn, path, compression)
1131
1133
1132 def makefilestorage(orig, requirements, features, **kwargs):
1134 def makefilestorage(orig, requirements, features, **kwargs):
1133 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
1135 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
1134 if REQUIREMENT in requirements:
1136 if REQUIREMENT in requirements:
1135 if REQUIREMENT_SHALLOW_FILES in requirements:
1137 if REQUIREMENT_SHALLOW_FILES in requirements:
1136 features.add(repository.REPO_FEATURE_SHALLOW_FILE_STORAGE)
1138 features.add(repository.REPO_FEATURE_SHALLOW_FILE_STORAGE)
1137
1139
1138 return sqlitefilestorage
1140 return sqlitefilestorage
1139 else:
1141 else:
1140 return orig(requirements=requirements, features=features, **kwargs)
1142 return orig(requirements=requirements, features=features, **kwargs)
1141
1143
1142 def makemain(orig, ui, requirements, **kwargs):
1144 def makemain(orig, ui, requirements, **kwargs):
1143 if REQUIREMENT in requirements:
1145 if REQUIREMENT in requirements:
1144 if REQUIREMENT_ZSTD in requirements and not zstd:
1146 if REQUIREMENT_ZSTD in requirements and not zstd:
1145 raise error.Abort(_('repository uses zstandard compression, which '
1147 raise error.Abort(_('repository uses zstandard compression, which '
1146 'is not available to this Mercurial install'))
1148 'is not available to this Mercurial install'))
1147
1149
1148 return sqliterepository
1150 return sqliterepository
1149
1151
1150 return orig(requirements=requirements, **kwargs)
1152 return orig(requirements=requirements, **kwargs)
1151
1153
1152 def verifierinit(orig, self, *args, **kwargs):
1154 def verifierinit(orig, self, *args, **kwargs):
1153 orig(self, *args, **kwargs)
1155 orig(self, *args, **kwargs)
1154
1156
1155 # We don't care that files in the store don't align with what is
1157 # We don't care that files in the store don't align with what is
1156 # advertised. So suppress these warnings.
1158 # advertised. So suppress these warnings.
1157 self.warnorphanstorefiles = False
1159 self.warnorphanstorefiles = False
1158
1160
1159 def extsetup(ui):
1161 def extsetup(ui):
1160 localrepo.featuresetupfuncs.add(featuresetup)
1162 localrepo.featuresetupfuncs.add(featuresetup)
1161 extensions.wrapfunction(localrepo, 'newreporequirements',
1163 extensions.wrapfunction(localrepo, 'newreporequirements',
1162 newreporequirements)
1164 newreporequirements)
1163 extensions.wrapfunction(localrepo, 'makefilestorage',
1165 extensions.wrapfunction(localrepo, 'makefilestorage',
1164 makefilestorage)
1166 makefilestorage)
1165 extensions.wrapfunction(localrepo, 'makemain',
1167 extensions.wrapfunction(localrepo, 'makemain',
1166 makemain)
1168 makemain)
1167 extensions.wrapfunction(verify.verifier, '__init__',
1169 extensions.wrapfunction(verify.verifier, '__init__',
1168 verifierinit)
1170 verifierinit)
1169
1171
1170 def reposetup(ui, repo):
1172 def reposetup(ui, repo):
1171 if isinstance(repo, sqliterepository):
1173 if isinstance(repo, sqliterepository):
1172 repo._db = None
1174 repo._db = None
1173
1175
1174 # TODO check for bundlerepository?
1176 # TODO check for bundlerepository?
@@ -1,1423 +1,1426
1 # changegroup.py - Mercurial changegroup manipulation functions
1 # changegroup.py - Mercurial changegroup manipulation functions
2 #
2 #
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006 Matt Mackall <mpm@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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import struct
11 import struct
12 import weakref
12 import weakref
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import (
15 from .node import (
16 hex,
16 hex,
17 nullid,
17 nullid,
18 nullrev,
18 nullrev,
19 short,
19 short,
20 )
20 )
21
21
22 from . import (
22 from . import (
23 error,
23 error,
24 match as matchmod,
24 match as matchmod,
25 mdiff,
25 mdiff,
26 phases,
26 phases,
27 pycompat,
27 pycompat,
28 util,
29 )
30
31 from .interfaces import (
28 repository,
32 repository,
29 util,
30 )
33 )
31
34
32 _CHANGEGROUPV1_DELTA_HEADER = struct.Struct("20s20s20s20s")
35 _CHANGEGROUPV1_DELTA_HEADER = struct.Struct("20s20s20s20s")
33 _CHANGEGROUPV2_DELTA_HEADER = struct.Struct("20s20s20s20s20s")
36 _CHANGEGROUPV2_DELTA_HEADER = struct.Struct("20s20s20s20s20s")
34 _CHANGEGROUPV3_DELTA_HEADER = struct.Struct(">20s20s20s20s20sH")
37 _CHANGEGROUPV3_DELTA_HEADER = struct.Struct(">20s20s20s20s20sH")
35
38
36 LFS_REQUIREMENT = 'lfs'
39 LFS_REQUIREMENT = 'lfs'
37
40
38 readexactly = util.readexactly
41 readexactly = util.readexactly
39
42
40 def getchunk(stream):
43 def getchunk(stream):
41 """return the next chunk from stream as a string"""
44 """return the next chunk from stream as a string"""
42 d = readexactly(stream, 4)
45 d = readexactly(stream, 4)
43 l = struct.unpack(">l", d)[0]
46 l = struct.unpack(">l", d)[0]
44 if l <= 4:
47 if l <= 4:
45 if l:
48 if l:
46 raise error.Abort(_("invalid chunk length %d") % l)
49 raise error.Abort(_("invalid chunk length %d") % l)
47 return ""
50 return ""
48 return readexactly(stream, l - 4)
51 return readexactly(stream, l - 4)
49
52
50 def chunkheader(length):
53 def chunkheader(length):
51 """return a changegroup chunk header (string)"""
54 """return a changegroup chunk header (string)"""
52 return struct.pack(">l", length + 4)
55 return struct.pack(">l", length + 4)
53
56
54 def closechunk():
57 def closechunk():
55 """return a changegroup chunk header (string) for a zero-length chunk"""
58 """return a changegroup chunk header (string) for a zero-length chunk"""
56 return struct.pack(">l", 0)
59 return struct.pack(">l", 0)
57
60
58 def _fileheader(path):
61 def _fileheader(path):
59 """Obtain a changegroup chunk header for a named path."""
62 """Obtain a changegroup chunk header for a named path."""
60 return chunkheader(len(path)) + path
63 return chunkheader(len(path)) + path
61
64
62 def writechunks(ui, chunks, filename, vfs=None):
65 def writechunks(ui, chunks, filename, vfs=None):
63 """Write chunks to a file and return its filename.
66 """Write chunks to a file and return its filename.
64
67
65 The stream is assumed to be a bundle file.
68 The stream is assumed to be a bundle file.
66 Existing files will not be overwritten.
69 Existing files will not be overwritten.
67 If no filename is specified, a temporary file is created.
70 If no filename is specified, a temporary file is created.
68 """
71 """
69 fh = None
72 fh = None
70 cleanup = None
73 cleanup = None
71 try:
74 try:
72 if filename:
75 if filename:
73 if vfs:
76 if vfs:
74 fh = vfs.open(filename, "wb")
77 fh = vfs.open(filename, "wb")
75 else:
78 else:
76 # Increase default buffer size because default is usually
79 # Increase default buffer size because default is usually
77 # small (4k is common on Linux).
80 # small (4k is common on Linux).
78 fh = open(filename, "wb", 131072)
81 fh = open(filename, "wb", 131072)
79 else:
82 else:
80 fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg")
83 fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg")
81 fh = os.fdopen(fd, r"wb")
84 fh = os.fdopen(fd, r"wb")
82 cleanup = filename
85 cleanup = filename
83 for c in chunks:
86 for c in chunks:
84 fh.write(c)
87 fh.write(c)
85 cleanup = None
88 cleanup = None
86 return filename
89 return filename
87 finally:
90 finally:
88 if fh is not None:
91 if fh is not None:
89 fh.close()
92 fh.close()
90 if cleanup is not None:
93 if cleanup is not None:
91 if filename and vfs:
94 if filename and vfs:
92 vfs.unlink(cleanup)
95 vfs.unlink(cleanup)
93 else:
96 else:
94 os.unlink(cleanup)
97 os.unlink(cleanup)
95
98
96 class cg1unpacker(object):
99 class cg1unpacker(object):
97 """Unpacker for cg1 changegroup streams.
100 """Unpacker for cg1 changegroup streams.
98
101
99 A changegroup unpacker handles the framing of the revision data in
102 A changegroup unpacker handles the framing of the revision data in
100 the wire format. Most consumers will want to use the apply()
103 the wire format. Most consumers will want to use the apply()
101 method to add the changes from the changegroup to a repository.
104 method to add the changes from the changegroup to a repository.
102
105
103 If you're forwarding a changegroup unmodified to another consumer,
106 If you're forwarding a changegroup unmodified to another consumer,
104 use getchunks(), which returns an iterator of changegroup
107 use getchunks(), which returns an iterator of changegroup
105 chunks. This is mostly useful for cases where you need to know the
108 chunks. This is mostly useful for cases where you need to know the
106 data stream has ended by observing the end of the changegroup.
109 data stream has ended by observing the end of the changegroup.
107
110
108 deltachunk() is useful only if you're applying delta data. Most
111 deltachunk() is useful only if you're applying delta data. Most
109 consumers should prefer apply() instead.
112 consumers should prefer apply() instead.
110
113
111 A few other public methods exist. Those are used only for
114 A few other public methods exist. Those are used only for
112 bundlerepo and some debug commands - their use is discouraged.
115 bundlerepo and some debug commands - their use is discouraged.
113 """
116 """
114 deltaheader = _CHANGEGROUPV1_DELTA_HEADER
117 deltaheader = _CHANGEGROUPV1_DELTA_HEADER
115 deltaheadersize = deltaheader.size
118 deltaheadersize = deltaheader.size
116 version = '01'
119 version = '01'
117 _grouplistcount = 1 # One list of files after the manifests
120 _grouplistcount = 1 # One list of files after the manifests
118
121
119 def __init__(self, fh, alg, extras=None):
122 def __init__(self, fh, alg, extras=None):
120 if alg is None:
123 if alg is None:
121 alg = 'UN'
124 alg = 'UN'
122 if alg not in util.compengines.supportedbundletypes:
125 if alg not in util.compengines.supportedbundletypes:
123 raise error.Abort(_('unknown stream compression type: %s')
126 raise error.Abort(_('unknown stream compression type: %s')
124 % alg)
127 % alg)
125 if alg == 'BZ':
128 if alg == 'BZ':
126 alg = '_truncatedBZ'
129 alg = '_truncatedBZ'
127
130
128 compengine = util.compengines.forbundletype(alg)
131 compengine = util.compengines.forbundletype(alg)
129 self._stream = compengine.decompressorreader(fh)
132 self._stream = compengine.decompressorreader(fh)
130 self._type = alg
133 self._type = alg
131 self.extras = extras or {}
134 self.extras = extras or {}
132 self.callback = None
135 self.callback = None
133
136
134 # These methods (compressed, read, seek, tell) all appear to only
137 # These methods (compressed, read, seek, tell) all appear to only
135 # be used by bundlerepo, but it's a little hard to tell.
138 # be used by bundlerepo, but it's a little hard to tell.
136 def compressed(self):
139 def compressed(self):
137 return self._type is not None and self._type != 'UN'
140 return self._type is not None and self._type != 'UN'
138 def read(self, l):
141 def read(self, l):
139 return self._stream.read(l)
142 return self._stream.read(l)
140 def seek(self, pos):
143 def seek(self, pos):
141 return self._stream.seek(pos)
144 return self._stream.seek(pos)
142 def tell(self):
145 def tell(self):
143 return self._stream.tell()
146 return self._stream.tell()
144 def close(self):
147 def close(self):
145 return self._stream.close()
148 return self._stream.close()
146
149
147 def _chunklength(self):
150 def _chunklength(self):
148 d = readexactly(self._stream, 4)
151 d = readexactly(self._stream, 4)
149 l = struct.unpack(">l", d)[0]
152 l = struct.unpack(">l", d)[0]
150 if l <= 4:
153 if l <= 4:
151 if l:
154 if l:
152 raise error.Abort(_("invalid chunk length %d") % l)
155 raise error.Abort(_("invalid chunk length %d") % l)
153 return 0
156 return 0
154 if self.callback:
157 if self.callback:
155 self.callback()
158 self.callback()
156 return l - 4
159 return l - 4
157
160
158 def changelogheader(self):
161 def changelogheader(self):
159 """v10 does not have a changelog header chunk"""
162 """v10 does not have a changelog header chunk"""
160 return {}
163 return {}
161
164
162 def manifestheader(self):
165 def manifestheader(self):
163 """v10 does not have a manifest header chunk"""
166 """v10 does not have a manifest header chunk"""
164 return {}
167 return {}
165
168
166 def filelogheader(self):
169 def filelogheader(self):
167 """return the header of the filelogs chunk, v10 only has the filename"""
170 """return the header of the filelogs chunk, v10 only has the filename"""
168 l = self._chunklength()
171 l = self._chunklength()
169 if not l:
172 if not l:
170 return {}
173 return {}
171 fname = readexactly(self._stream, l)
174 fname = readexactly(self._stream, l)
172 return {'filename': fname}
175 return {'filename': fname}
173
176
174 def _deltaheader(self, headertuple, prevnode):
177 def _deltaheader(self, headertuple, prevnode):
175 node, p1, p2, cs = headertuple
178 node, p1, p2, cs = headertuple
176 if prevnode is None:
179 if prevnode is None:
177 deltabase = p1
180 deltabase = p1
178 else:
181 else:
179 deltabase = prevnode
182 deltabase = prevnode
180 flags = 0
183 flags = 0
181 return node, p1, p2, deltabase, cs, flags
184 return node, p1, p2, deltabase, cs, flags
182
185
183 def deltachunk(self, prevnode):
186 def deltachunk(self, prevnode):
184 l = self._chunklength()
187 l = self._chunklength()
185 if not l:
188 if not l:
186 return {}
189 return {}
187 headerdata = readexactly(self._stream, self.deltaheadersize)
190 headerdata = readexactly(self._stream, self.deltaheadersize)
188 header = self.deltaheader.unpack(headerdata)
191 header = self.deltaheader.unpack(headerdata)
189 delta = readexactly(self._stream, l - self.deltaheadersize)
192 delta = readexactly(self._stream, l - self.deltaheadersize)
190 node, p1, p2, deltabase, cs, flags = self._deltaheader(header, prevnode)
193 node, p1, p2, deltabase, cs, flags = self._deltaheader(header, prevnode)
191 return (node, p1, p2, cs, deltabase, delta, flags)
194 return (node, p1, p2, cs, deltabase, delta, flags)
192
195
193 def getchunks(self):
196 def getchunks(self):
194 """returns all the chunks contains in the bundle
197 """returns all the chunks contains in the bundle
195
198
196 Used when you need to forward the binary stream to a file or another
199 Used when you need to forward the binary stream to a file or another
197 network API. To do so, it parse the changegroup data, otherwise it will
200 network API. To do so, it parse the changegroup data, otherwise it will
198 block in case of sshrepo because it don't know the end of the stream.
201 block in case of sshrepo because it don't know the end of the stream.
199 """
202 """
200 # For changegroup 1 and 2, we expect 3 parts: changelog, manifestlog,
203 # For changegroup 1 and 2, we expect 3 parts: changelog, manifestlog,
201 # and a list of filelogs. For changegroup 3, we expect 4 parts:
204 # and a list of filelogs. For changegroup 3, we expect 4 parts:
202 # changelog, manifestlog, a list of tree manifestlogs, and a list of
205 # changelog, manifestlog, a list of tree manifestlogs, and a list of
203 # filelogs.
206 # filelogs.
204 #
207 #
205 # Changelog and manifestlog parts are terminated with empty chunks. The
208 # Changelog and manifestlog parts are terminated with empty chunks. The
206 # tree and file parts are a list of entry sections. Each entry section
209 # tree and file parts are a list of entry sections. Each entry section
207 # is a series of chunks terminating in an empty chunk. The list of these
210 # is a series of chunks terminating in an empty chunk. The list of these
208 # entry sections is terminated in yet another empty chunk, so we know
211 # entry sections is terminated in yet another empty chunk, so we know
209 # we've reached the end of the tree/file list when we reach an empty
212 # we've reached the end of the tree/file list when we reach an empty
210 # chunk that was proceeded by no non-empty chunks.
213 # chunk that was proceeded by no non-empty chunks.
211
214
212 parts = 0
215 parts = 0
213 while parts < 2 + self._grouplistcount:
216 while parts < 2 + self._grouplistcount:
214 noentries = True
217 noentries = True
215 while True:
218 while True:
216 chunk = getchunk(self)
219 chunk = getchunk(self)
217 if not chunk:
220 if not chunk:
218 # The first two empty chunks represent the end of the
221 # The first two empty chunks represent the end of the
219 # changelog and the manifestlog portions. The remaining
222 # changelog and the manifestlog portions. The remaining
220 # empty chunks represent either A) the end of individual
223 # empty chunks represent either A) the end of individual
221 # tree or file entries in the file list, or B) the end of
224 # tree or file entries in the file list, or B) the end of
222 # the entire list. It's the end of the entire list if there
225 # the entire list. It's the end of the entire list if there
223 # were no entries (i.e. noentries is True).
226 # were no entries (i.e. noentries is True).
224 if parts < 2:
227 if parts < 2:
225 parts += 1
228 parts += 1
226 elif noentries:
229 elif noentries:
227 parts += 1
230 parts += 1
228 break
231 break
229 noentries = False
232 noentries = False
230 yield chunkheader(len(chunk))
233 yield chunkheader(len(chunk))
231 pos = 0
234 pos = 0
232 while pos < len(chunk):
235 while pos < len(chunk):
233 next = pos + 2**20
236 next = pos + 2**20
234 yield chunk[pos:next]
237 yield chunk[pos:next]
235 pos = next
238 pos = next
236 yield closechunk()
239 yield closechunk()
237
240
238 def _unpackmanifests(self, repo, revmap, trp, prog):
241 def _unpackmanifests(self, repo, revmap, trp, prog):
239 self.callback = prog.increment
242 self.callback = prog.increment
240 # no need to check for empty manifest group here:
243 # no need to check for empty manifest group here:
241 # if the result of the merge of 1 and 2 is the same in 3 and 4,
244 # if the result of the merge of 1 and 2 is the same in 3 and 4,
242 # no new manifest will be created and the manifest group will
245 # no new manifest will be created and the manifest group will
243 # be empty during the pull
246 # be empty during the pull
244 self.manifestheader()
247 self.manifestheader()
245 deltas = self.deltaiter()
248 deltas = self.deltaiter()
246 repo.manifestlog.getstorage(b'').addgroup(deltas, revmap, trp)
249 repo.manifestlog.getstorage(b'').addgroup(deltas, revmap, trp)
247 prog.complete()
250 prog.complete()
248 self.callback = None
251 self.callback = None
249
252
250 def apply(self, repo, tr, srctype, url, targetphase=phases.draft,
253 def apply(self, repo, tr, srctype, url, targetphase=phases.draft,
251 expectedtotal=None):
254 expectedtotal=None):
252 """Add the changegroup returned by source.read() to this repo.
255 """Add the changegroup returned by source.read() to this repo.
253 srctype is a string like 'push', 'pull', or 'unbundle'. url is
256 srctype is a string like 'push', 'pull', or 'unbundle'. url is
254 the URL of the repo where this changegroup is coming from.
257 the URL of the repo where this changegroup is coming from.
255
258
256 Return an integer summarizing the change to this repo:
259 Return an integer summarizing the change to this repo:
257 - nothing changed or no source: 0
260 - nothing changed or no source: 0
258 - more heads than before: 1+added heads (2..n)
261 - more heads than before: 1+added heads (2..n)
259 - fewer heads than before: -1-removed heads (-2..-n)
262 - fewer heads than before: -1-removed heads (-2..-n)
260 - number of heads stays the same: 1
263 - number of heads stays the same: 1
261 """
264 """
262 repo = repo.unfiltered()
265 repo = repo.unfiltered()
263 def csmap(x):
266 def csmap(x):
264 repo.ui.debug("add changeset %s\n" % short(x))
267 repo.ui.debug("add changeset %s\n" % short(x))
265 return len(cl)
268 return len(cl)
266
269
267 def revmap(x):
270 def revmap(x):
268 return cl.rev(x)
271 return cl.rev(x)
269
272
270 changesets = files = revisions = 0
273 changesets = files = revisions = 0
271
274
272 try:
275 try:
273 # The transaction may already carry source information. In this
276 # The transaction may already carry source information. In this
274 # case we use the top level data. We overwrite the argument
277 # case we use the top level data. We overwrite the argument
275 # because we need to use the top level value (if they exist)
278 # because we need to use the top level value (if they exist)
276 # in this function.
279 # in this function.
277 srctype = tr.hookargs.setdefault('source', srctype)
280 srctype = tr.hookargs.setdefault('source', srctype)
278 tr.hookargs.setdefault('url', url)
281 tr.hookargs.setdefault('url', url)
279 repo.hook('prechangegroup',
282 repo.hook('prechangegroup',
280 throw=True, **pycompat.strkwargs(tr.hookargs))
283 throw=True, **pycompat.strkwargs(tr.hookargs))
281
284
282 # write changelog data to temp files so concurrent readers
285 # write changelog data to temp files so concurrent readers
283 # will not see an inconsistent view
286 # will not see an inconsistent view
284 cl = repo.changelog
287 cl = repo.changelog
285 cl.delayupdate(tr)
288 cl.delayupdate(tr)
286 oldheads = set(cl.heads())
289 oldheads = set(cl.heads())
287
290
288 trp = weakref.proxy(tr)
291 trp = weakref.proxy(tr)
289 # pull off the changeset group
292 # pull off the changeset group
290 repo.ui.status(_("adding changesets\n"))
293 repo.ui.status(_("adding changesets\n"))
291 clstart = len(cl)
294 clstart = len(cl)
292 progress = repo.ui.makeprogress(_('changesets'), unit=_('chunks'),
295 progress = repo.ui.makeprogress(_('changesets'), unit=_('chunks'),
293 total=expectedtotal)
296 total=expectedtotal)
294 self.callback = progress.increment
297 self.callback = progress.increment
295
298
296 efiles = set()
299 efiles = set()
297 def onchangelog(cl, node):
300 def onchangelog(cl, node):
298 efiles.update(cl.readfiles(node))
301 efiles.update(cl.readfiles(node))
299
302
300 self.changelogheader()
303 self.changelogheader()
301 deltas = self.deltaiter()
304 deltas = self.deltaiter()
302 cgnodes = cl.addgroup(deltas, csmap, trp, addrevisioncb=onchangelog)
305 cgnodes = cl.addgroup(deltas, csmap, trp, addrevisioncb=onchangelog)
303 efiles = len(efiles)
306 efiles = len(efiles)
304
307
305 if not cgnodes:
308 if not cgnodes:
306 repo.ui.develwarn('applied empty changelog from changegroup',
309 repo.ui.develwarn('applied empty changelog from changegroup',
307 config='warn-empty-changegroup')
310 config='warn-empty-changegroup')
308 clend = len(cl)
311 clend = len(cl)
309 changesets = clend - clstart
312 changesets = clend - clstart
310 progress.complete()
313 progress.complete()
311 self.callback = None
314 self.callback = None
312
315
313 # pull off the manifest group
316 # pull off the manifest group
314 repo.ui.status(_("adding manifests\n"))
317 repo.ui.status(_("adding manifests\n"))
315 # We know that we'll never have more manifests than we had
318 # We know that we'll never have more manifests than we had
316 # changesets.
319 # changesets.
317 progress = repo.ui.makeprogress(_('manifests'), unit=_('chunks'),
320 progress = repo.ui.makeprogress(_('manifests'), unit=_('chunks'),
318 total=changesets)
321 total=changesets)
319 self._unpackmanifests(repo, revmap, trp, progress)
322 self._unpackmanifests(repo, revmap, trp, progress)
320
323
321 needfiles = {}
324 needfiles = {}
322 if repo.ui.configbool('server', 'validate'):
325 if repo.ui.configbool('server', 'validate'):
323 cl = repo.changelog
326 cl = repo.changelog
324 ml = repo.manifestlog
327 ml = repo.manifestlog
325 # validate incoming csets have their manifests
328 # validate incoming csets have their manifests
326 for cset in pycompat.xrange(clstart, clend):
329 for cset in pycompat.xrange(clstart, clend):
327 mfnode = cl.changelogrevision(cset).manifest
330 mfnode = cl.changelogrevision(cset).manifest
328 mfest = ml[mfnode].readdelta()
331 mfest = ml[mfnode].readdelta()
329 # store file cgnodes we must see
332 # store file cgnodes we must see
330 for f, n in mfest.iteritems():
333 for f, n in mfest.iteritems():
331 needfiles.setdefault(f, set()).add(n)
334 needfiles.setdefault(f, set()).add(n)
332
335
333 # process the files
336 # process the files
334 repo.ui.status(_("adding file changes\n"))
337 repo.ui.status(_("adding file changes\n"))
335 newrevs, newfiles = _addchangegroupfiles(
338 newrevs, newfiles = _addchangegroupfiles(
336 repo, self, revmap, trp, efiles, needfiles)
339 repo, self, revmap, trp, efiles, needfiles)
337 revisions += newrevs
340 revisions += newrevs
338 files += newfiles
341 files += newfiles
339
342
340 deltaheads = 0
343 deltaheads = 0
341 if oldheads:
344 if oldheads:
342 heads = cl.heads()
345 heads = cl.heads()
343 deltaheads = len(heads) - len(oldheads)
346 deltaheads = len(heads) - len(oldheads)
344 for h in heads:
347 for h in heads:
345 if h not in oldheads and repo[h].closesbranch():
348 if h not in oldheads and repo[h].closesbranch():
346 deltaheads -= 1
349 deltaheads -= 1
347 htext = ""
350 htext = ""
348 if deltaheads:
351 if deltaheads:
349 htext = _(" (%+d heads)") % deltaheads
352 htext = _(" (%+d heads)") % deltaheads
350
353
351 repo.ui.status(_("added %d changesets"
354 repo.ui.status(_("added %d changesets"
352 " with %d changes to %d files%s\n")
355 " with %d changes to %d files%s\n")
353 % (changesets, revisions, files, htext))
356 % (changesets, revisions, files, htext))
354 repo.invalidatevolatilesets()
357 repo.invalidatevolatilesets()
355
358
356 if changesets > 0:
359 if changesets > 0:
357 if 'node' not in tr.hookargs:
360 if 'node' not in tr.hookargs:
358 tr.hookargs['node'] = hex(cl.node(clstart))
361 tr.hookargs['node'] = hex(cl.node(clstart))
359 tr.hookargs['node_last'] = hex(cl.node(clend - 1))
362 tr.hookargs['node_last'] = hex(cl.node(clend - 1))
360 hookargs = dict(tr.hookargs)
363 hookargs = dict(tr.hookargs)
361 else:
364 else:
362 hookargs = dict(tr.hookargs)
365 hookargs = dict(tr.hookargs)
363 hookargs['node'] = hex(cl.node(clstart))
366 hookargs['node'] = hex(cl.node(clstart))
364 hookargs['node_last'] = hex(cl.node(clend - 1))
367 hookargs['node_last'] = hex(cl.node(clend - 1))
365 repo.hook('pretxnchangegroup',
368 repo.hook('pretxnchangegroup',
366 throw=True, **pycompat.strkwargs(hookargs))
369 throw=True, **pycompat.strkwargs(hookargs))
367
370
368 added = [cl.node(r) for r in pycompat.xrange(clstart, clend)]
371 added = [cl.node(r) for r in pycompat.xrange(clstart, clend)]
369 phaseall = None
372 phaseall = None
370 if srctype in ('push', 'serve'):
373 if srctype in ('push', 'serve'):
371 # Old servers can not push the boundary themselves.
374 # Old servers can not push the boundary themselves.
372 # New servers won't push the boundary if changeset already
375 # New servers won't push the boundary if changeset already
373 # exists locally as secret
376 # exists locally as secret
374 #
377 #
375 # We should not use added here but the list of all change in
378 # We should not use added here but the list of all change in
376 # the bundle
379 # the bundle
377 if repo.publishing():
380 if repo.publishing():
378 targetphase = phaseall = phases.public
381 targetphase = phaseall = phases.public
379 else:
382 else:
380 # closer target phase computation
383 # closer target phase computation
381
384
382 # Those changesets have been pushed from the
385 # Those changesets have been pushed from the
383 # outside, their phases are going to be pushed
386 # outside, their phases are going to be pushed
384 # alongside. Therefor `targetphase` is
387 # alongside. Therefor `targetphase` is
385 # ignored.
388 # ignored.
386 targetphase = phaseall = phases.draft
389 targetphase = phaseall = phases.draft
387 if added:
390 if added:
388 phases.registernew(repo, tr, targetphase, added)
391 phases.registernew(repo, tr, targetphase, added)
389 if phaseall is not None:
392 if phaseall is not None:
390 phases.advanceboundary(repo, tr, phaseall, cgnodes)
393 phases.advanceboundary(repo, tr, phaseall, cgnodes)
391
394
392 if changesets > 0:
395 if changesets > 0:
393
396
394 def runhooks():
397 def runhooks():
395 # These hooks run when the lock releases, not when the
398 # These hooks run when the lock releases, not when the
396 # transaction closes. So it's possible for the changelog
399 # transaction closes. So it's possible for the changelog
397 # to have changed since we last saw it.
400 # to have changed since we last saw it.
398 if clstart >= len(repo):
401 if clstart >= len(repo):
399 return
402 return
400
403
401 repo.hook("changegroup", **pycompat.strkwargs(hookargs))
404 repo.hook("changegroup", **pycompat.strkwargs(hookargs))
402
405
403 for n in added:
406 for n in added:
404 args = hookargs.copy()
407 args = hookargs.copy()
405 args['node'] = hex(n)
408 args['node'] = hex(n)
406 del args['node_last']
409 del args['node_last']
407 repo.hook("incoming", **pycompat.strkwargs(args))
410 repo.hook("incoming", **pycompat.strkwargs(args))
408
411
409 newheads = [h for h in repo.heads()
412 newheads = [h for h in repo.heads()
410 if h not in oldheads]
413 if h not in oldheads]
411 repo.ui.log("incoming",
414 repo.ui.log("incoming",
412 "%d incoming changes - new heads: %s\n",
415 "%d incoming changes - new heads: %s\n",
413 len(added),
416 len(added),
414 ', '.join([hex(c[:6]) for c in newheads]))
417 ', '.join([hex(c[:6]) for c in newheads]))
415
418
416 tr.addpostclose('changegroup-runhooks-%020i' % clstart,
419 tr.addpostclose('changegroup-runhooks-%020i' % clstart,
417 lambda tr: repo._afterlock(runhooks))
420 lambda tr: repo._afterlock(runhooks))
418 finally:
421 finally:
419 repo.ui.flush()
422 repo.ui.flush()
420 # never return 0 here:
423 # never return 0 here:
421 if deltaheads < 0:
424 if deltaheads < 0:
422 ret = deltaheads - 1
425 ret = deltaheads - 1
423 else:
426 else:
424 ret = deltaheads + 1
427 ret = deltaheads + 1
425 return ret
428 return ret
426
429
427 def deltaiter(self):
430 def deltaiter(self):
428 """
431 """
429 returns an iterator of the deltas in this changegroup
432 returns an iterator of the deltas in this changegroup
430
433
431 Useful for passing to the underlying storage system to be stored.
434 Useful for passing to the underlying storage system to be stored.
432 """
435 """
433 chain = None
436 chain = None
434 for chunkdata in iter(lambda: self.deltachunk(chain), {}):
437 for chunkdata in iter(lambda: self.deltachunk(chain), {}):
435 # Chunkdata: (node, p1, p2, cs, deltabase, delta, flags)
438 # Chunkdata: (node, p1, p2, cs, deltabase, delta, flags)
436 yield chunkdata
439 yield chunkdata
437 chain = chunkdata[0]
440 chain = chunkdata[0]
438
441
439 class cg2unpacker(cg1unpacker):
442 class cg2unpacker(cg1unpacker):
440 """Unpacker for cg2 streams.
443 """Unpacker for cg2 streams.
441
444
442 cg2 streams add support for generaldelta, so the delta header
445 cg2 streams add support for generaldelta, so the delta header
443 format is slightly different. All other features about the data
446 format is slightly different. All other features about the data
444 remain the same.
447 remain the same.
445 """
448 """
446 deltaheader = _CHANGEGROUPV2_DELTA_HEADER
449 deltaheader = _CHANGEGROUPV2_DELTA_HEADER
447 deltaheadersize = deltaheader.size
450 deltaheadersize = deltaheader.size
448 version = '02'
451 version = '02'
449
452
450 def _deltaheader(self, headertuple, prevnode):
453 def _deltaheader(self, headertuple, prevnode):
451 node, p1, p2, deltabase, cs = headertuple
454 node, p1, p2, deltabase, cs = headertuple
452 flags = 0
455 flags = 0
453 return node, p1, p2, deltabase, cs, flags
456 return node, p1, p2, deltabase, cs, flags
454
457
455 class cg3unpacker(cg2unpacker):
458 class cg3unpacker(cg2unpacker):
456 """Unpacker for cg3 streams.
459 """Unpacker for cg3 streams.
457
460
458 cg3 streams add support for exchanging treemanifests and revlog
461 cg3 streams add support for exchanging treemanifests and revlog
459 flags. It adds the revlog flags to the delta header and an empty chunk
462 flags. It adds the revlog flags to the delta header and an empty chunk
460 separating manifests and files.
463 separating manifests and files.
461 """
464 """
462 deltaheader = _CHANGEGROUPV3_DELTA_HEADER
465 deltaheader = _CHANGEGROUPV3_DELTA_HEADER
463 deltaheadersize = deltaheader.size
466 deltaheadersize = deltaheader.size
464 version = '03'
467 version = '03'
465 _grouplistcount = 2 # One list of manifests and one list of files
468 _grouplistcount = 2 # One list of manifests and one list of files
466
469
467 def _deltaheader(self, headertuple, prevnode):
470 def _deltaheader(self, headertuple, prevnode):
468 node, p1, p2, deltabase, cs, flags = headertuple
471 node, p1, p2, deltabase, cs, flags = headertuple
469 return node, p1, p2, deltabase, cs, flags
472 return node, p1, p2, deltabase, cs, flags
470
473
471 def _unpackmanifests(self, repo, revmap, trp, prog):
474 def _unpackmanifests(self, repo, revmap, trp, prog):
472 super(cg3unpacker, self)._unpackmanifests(repo, revmap, trp, prog)
475 super(cg3unpacker, self)._unpackmanifests(repo, revmap, trp, prog)
473 for chunkdata in iter(self.filelogheader, {}):
476 for chunkdata in iter(self.filelogheader, {}):
474 # If we get here, there are directory manifests in the changegroup
477 # If we get here, there are directory manifests in the changegroup
475 d = chunkdata["filename"]
478 d = chunkdata["filename"]
476 repo.ui.debug("adding %s revisions\n" % d)
479 repo.ui.debug("adding %s revisions\n" % d)
477 deltas = self.deltaiter()
480 deltas = self.deltaiter()
478 if not repo.manifestlog.getstorage(d).addgroup(deltas, revmap, trp):
481 if not repo.manifestlog.getstorage(d).addgroup(deltas, revmap, trp):
479 raise error.Abort(_("received dir revlog group is empty"))
482 raise error.Abort(_("received dir revlog group is empty"))
480
483
481 class headerlessfixup(object):
484 class headerlessfixup(object):
482 def __init__(self, fh, h):
485 def __init__(self, fh, h):
483 self._h = h
486 self._h = h
484 self._fh = fh
487 self._fh = fh
485 def read(self, n):
488 def read(self, n):
486 if self._h:
489 if self._h:
487 d, self._h = self._h[:n], self._h[n:]
490 d, self._h = self._h[:n], self._h[n:]
488 if len(d) < n:
491 if len(d) < n:
489 d += readexactly(self._fh, n - len(d))
492 d += readexactly(self._fh, n - len(d))
490 return d
493 return d
491 return readexactly(self._fh, n)
494 return readexactly(self._fh, n)
492
495
493 def _revisiondeltatochunks(delta, headerfn):
496 def _revisiondeltatochunks(delta, headerfn):
494 """Serialize a revisiondelta to changegroup chunks."""
497 """Serialize a revisiondelta to changegroup chunks."""
495
498
496 # The captured revision delta may be encoded as a delta against
499 # The captured revision delta may be encoded as a delta against
497 # a base revision or as a full revision. The changegroup format
500 # a base revision or as a full revision. The changegroup format
498 # requires that everything on the wire be deltas. So for full
501 # requires that everything on the wire be deltas. So for full
499 # revisions, we need to invent a header that says to rewrite
502 # revisions, we need to invent a header that says to rewrite
500 # data.
503 # data.
501
504
502 if delta.delta is not None:
505 if delta.delta is not None:
503 prefix, data = b'', delta.delta
506 prefix, data = b'', delta.delta
504 elif delta.basenode == nullid:
507 elif delta.basenode == nullid:
505 data = delta.revision
508 data = delta.revision
506 prefix = mdiff.trivialdiffheader(len(data))
509 prefix = mdiff.trivialdiffheader(len(data))
507 else:
510 else:
508 data = delta.revision
511 data = delta.revision
509 prefix = mdiff.replacediffheader(delta.baserevisionsize,
512 prefix = mdiff.replacediffheader(delta.baserevisionsize,
510 len(data))
513 len(data))
511
514
512 meta = headerfn(delta)
515 meta = headerfn(delta)
513
516
514 yield chunkheader(len(meta) + len(prefix) + len(data))
517 yield chunkheader(len(meta) + len(prefix) + len(data))
515 yield meta
518 yield meta
516 if prefix:
519 if prefix:
517 yield prefix
520 yield prefix
518 yield data
521 yield data
519
522
520 def _sortnodesellipsis(store, nodes, cl, lookup):
523 def _sortnodesellipsis(store, nodes, cl, lookup):
521 """Sort nodes for changegroup generation."""
524 """Sort nodes for changegroup generation."""
522 # Ellipses serving mode.
525 # Ellipses serving mode.
523 #
526 #
524 # In a perfect world, we'd generate better ellipsis-ified graphs
527 # In a perfect world, we'd generate better ellipsis-ified graphs
525 # for non-changelog revlogs. In practice, we haven't started doing
528 # for non-changelog revlogs. In practice, we haven't started doing
526 # that yet, so the resulting DAGs for the manifestlog and filelogs
529 # that yet, so the resulting DAGs for the manifestlog and filelogs
527 # are actually full of bogus parentage on all the ellipsis
530 # are actually full of bogus parentage on all the ellipsis
528 # nodes. This has the side effect that, while the contents are
531 # nodes. This has the side effect that, while the contents are
529 # correct, the individual DAGs might be completely out of whack in
532 # correct, the individual DAGs might be completely out of whack in
530 # a case like 882681bc3166 and its ancestors (back about 10
533 # a case like 882681bc3166 and its ancestors (back about 10
531 # revisions or so) in the main hg repo.
534 # revisions or so) in the main hg repo.
532 #
535 #
533 # The one invariant we *know* holds is that the new (potentially
536 # The one invariant we *know* holds is that the new (potentially
534 # bogus) DAG shape will be valid if we order the nodes in the
537 # bogus) DAG shape will be valid if we order the nodes in the
535 # order that they're introduced in dramatis personae by the
538 # order that they're introduced in dramatis personae by the
536 # changelog, so what we do is we sort the non-changelog histories
539 # changelog, so what we do is we sort the non-changelog histories
537 # by the order in which they are used by the changelog.
540 # by the order in which they are used by the changelog.
538 key = lambda n: cl.rev(lookup(n))
541 key = lambda n: cl.rev(lookup(n))
539 return sorted(nodes, key=key)
542 return sorted(nodes, key=key)
540
543
541 def _resolvenarrowrevisioninfo(cl, store, ischangelog, rev, linkrev,
544 def _resolvenarrowrevisioninfo(cl, store, ischangelog, rev, linkrev,
542 linknode, clrevtolocalrev, fullclnodes,
545 linknode, clrevtolocalrev, fullclnodes,
543 precomputedellipsis):
546 precomputedellipsis):
544 linkparents = precomputedellipsis[linkrev]
547 linkparents = precomputedellipsis[linkrev]
545 def local(clrev):
548 def local(clrev):
546 """Turn a changelog revnum into a local revnum.
549 """Turn a changelog revnum into a local revnum.
547
550
548 The ellipsis dag is stored as revnums on the changelog,
551 The ellipsis dag is stored as revnums on the changelog,
549 but when we're producing ellipsis entries for
552 but when we're producing ellipsis entries for
550 non-changelog revlogs, we need to turn those numbers into
553 non-changelog revlogs, we need to turn those numbers into
551 something local. This does that for us, and during the
554 something local. This does that for us, and during the
552 changelog sending phase will also expand the stored
555 changelog sending phase will also expand the stored
553 mappings as needed.
556 mappings as needed.
554 """
557 """
555 if clrev == nullrev:
558 if clrev == nullrev:
556 return nullrev
559 return nullrev
557
560
558 if ischangelog:
561 if ischangelog:
559 return clrev
562 return clrev
560
563
561 # Walk the ellipsis-ized changelog breadth-first looking for a
564 # Walk the ellipsis-ized changelog breadth-first looking for a
562 # change that has been linked from the current revlog.
565 # change that has been linked from the current revlog.
563 #
566 #
564 # For a flat manifest revlog only a single step should be necessary
567 # For a flat manifest revlog only a single step should be necessary
565 # as all relevant changelog entries are relevant to the flat
568 # as all relevant changelog entries are relevant to the flat
566 # manifest.
569 # manifest.
567 #
570 #
568 # For a filelog or tree manifest dirlog however not every changelog
571 # For a filelog or tree manifest dirlog however not every changelog
569 # entry will have been relevant, so we need to skip some changelog
572 # entry will have been relevant, so we need to skip some changelog
570 # nodes even after ellipsis-izing.
573 # nodes even after ellipsis-izing.
571 walk = [clrev]
574 walk = [clrev]
572 while walk:
575 while walk:
573 p = walk[0]
576 p = walk[0]
574 walk = walk[1:]
577 walk = walk[1:]
575 if p in clrevtolocalrev:
578 if p in clrevtolocalrev:
576 return clrevtolocalrev[p]
579 return clrevtolocalrev[p]
577 elif p in fullclnodes:
580 elif p in fullclnodes:
578 walk.extend([pp for pp in cl.parentrevs(p)
581 walk.extend([pp for pp in cl.parentrevs(p)
579 if pp != nullrev])
582 if pp != nullrev])
580 elif p in precomputedellipsis:
583 elif p in precomputedellipsis:
581 walk.extend([pp for pp in precomputedellipsis[p]
584 walk.extend([pp for pp in precomputedellipsis[p]
582 if pp != nullrev])
585 if pp != nullrev])
583 else:
586 else:
584 # In this case, we've got an ellipsis with parents
587 # In this case, we've got an ellipsis with parents
585 # outside the current bundle (likely an
588 # outside the current bundle (likely an
586 # incremental pull). We "know" that we can use the
589 # incremental pull). We "know" that we can use the
587 # value of this same revlog at whatever revision
590 # value of this same revlog at whatever revision
588 # is pointed to by linknode. "Know" is in scare
591 # is pointed to by linknode. "Know" is in scare
589 # quotes because I haven't done enough examination
592 # quotes because I haven't done enough examination
590 # of edge cases to convince myself this is really
593 # of edge cases to convince myself this is really
591 # a fact - it works for all the (admittedly
594 # a fact - it works for all the (admittedly
592 # thorough) cases in our testsuite, but I would be
595 # thorough) cases in our testsuite, but I would be
593 # somewhat unsurprised to find a case in the wild
596 # somewhat unsurprised to find a case in the wild
594 # where this breaks down a bit. That said, I don't
597 # where this breaks down a bit. That said, I don't
595 # know if it would hurt anything.
598 # know if it would hurt anything.
596 for i in pycompat.xrange(rev, 0, -1):
599 for i in pycompat.xrange(rev, 0, -1):
597 if store.linkrev(i) == clrev:
600 if store.linkrev(i) == clrev:
598 return i
601 return i
599 # We failed to resolve a parent for this node, so
602 # We failed to resolve a parent for this node, so
600 # we crash the changegroup construction.
603 # we crash the changegroup construction.
601 raise error.Abort(
604 raise error.Abort(
602 'unable to resolve parent while packing %r %r'
605 'unable to resolve parent while packing %r %r'
603 ' for changeset %r' % (store.indexfile, rev, clrev))
606 ' for changeset %r' % (store.indexfile, rev, clrev))
604
607
605 return nullrev
608 return nullrev
606
609
607 if not linkparents or (
610 if not linkparents or (
608 store.parentrevs(rev) == (nullrev, nullrev)):
611 store.parentrevs(rev) == (nullrev, nullrev)):
609 p1, p2 = nullrev, nullrev
612 p1, p2 = nullrev, nullrev
610 elif len(linkparents) == 1:
613 elif len(linkparents) == 1:
611 p1, = sorted(local(p) for p in linkparents)
614 p1, = sorted(local(p) for p in linkparents)
612 p2 = nullrev
615 p2 = nullrev
613 else:
616 else:
614 p1, p2 = sorted(local(p) for p in linkparents)
617 p1, p2 = sorted(local(p) for p in linkparents)
615
618
616 p1node, p2node = store.node(p1), store.node(p2)
619 p1node, p2node = store.node(p1), store.node(p2)
617
620
618 return p1node, p2node, linknode
621 return p1node, p2node, linknode
619
622
620 def deltagroup(repo, store, nodes, ischangelog, lookup, forcedeltaparentprev,
623 def deltagroup(repo, store, nodes, ischangelog, lookup, forcedeltaparentprev,
621 topic=None,
624 topic=None,
622 ellipses=False, clrevtolocalrev=None, fullclnodes=None,
625 ellipses=False, clrevtolocalrev=None, fullclnodes=None,
623 precomputedellipsis=None):
626 precomputedellipsis=None):
624 """Calculate deltas for a set of revisions.
627 """Calculate deltas for a set of revisions.
625
628
626 Is a generator of ``revisiondelta`` instances.
629 Is a generator of ``revisiondelta`` instances.
627
630
628 If topic is not None, progress detail will be generated using this
631 If topic is not None, progress detail will be generated using this
629 topic name (e.g. changesets, manifests, etc).
632 topic name (e.g. changesets, manifests, etc).
630 """
633 """
631 if not nodes:
634 if not nodes:
632 return
635 return
633
636
634 cl = repo.changelog
637 cl = repo.changelog
635
638
636 if ischangelog:
639 if ischangelog:
637 # `hg log` shows changesets in storage order. To preserve order
640 # `hg log` shows changesets in storage order. To preserve order
638 # across clones, send out changesets in storage order.
641 # across clones, send out changesets in storage order.
639 nodesorder = 'storage'
642 nodesorder = 'storage'
640 elif ellipses:
643 elif ellipses:
641 nodes = _sortnodesellipsis(store, nodes, cl, lookup)
644 nodes = _sortnodesellipsis(store, nodes, cl, lookup)
642 nodesorder = 'nodes'
645 nodesorder = 'nodes'
643 else:
646 else:
644 nodesorder = None
647 nodesorder = None
645
648
646 # Perform ellipses filtering and revision massaging. We do this before
649 # Perform ellipses filtering and revision massaging. We do this before
647 # emitrevisions() because a) filtering out revisions creates less work
650 # emitrevisions() because a) filtering out revisions creates less work
648 # for emitrevisions() b) dropping revisions would break emitrevisions()'s
651 # for emitrevisions() b) dropping revisions would break emitrevisions()'s
649 # assumptions about delta choices and we would possibly send a delta
652 # assumptions about delta choices and we would possibly send a delta
650 # referencing a missing base revision.
653 # referencing a missing base revision.
651 #
654 #
652 # Also, calling lookup() has side-effects with regards to populating
655 # Also, calling lookup() has side-effects with regards to populating
653 # data structures. If we don't call lookup() for each node or if we call
656 # data structures. If we don't call lookup() for each node or if we call
654 # lookup() after the first pass through each node, things can break -
657 # lookup() after the first pass through each node, things can break -
655 # possibly intermittently depending on the python hash seed! For that
658 # possibly intermittently depending on the python hash seed! For that
656 # reason, we store a mapping of all linknodes during the initial node
659 # reason, we store a mapping of all linknodes during the initial node
657 # pass rather than use lookup() on the output side.
660 # pass rather than use lookup() on the output side.
658 if ellipses:
661 if ellipses:
659 filtered = []
662 filtered = []
660 adjustedparents = {}
663 adjustedparents = {}
661 linknodes = {}
664 linknodes = {}
662
665
663 for node in nodes:
666 for node in nodes:
664 rev = store.rev(node)
667 rev = store.rev(node)
665 linknode = lookup(node)
668 linknode = lookup(node)
666 linkrev = cl.rev(linknode)
669 linkrev = cl.rev(linknode)
667 clrevtolocalrev[linkrev] = rev
670 clrevtolocalrev[linkrev] = rev
668
671
669 # If linknode is in fullclnodes, it means the corresponding
672 # If linknode is in fullclnodes, it means the corresponding
670 # changeset was a full changeset and is being sent unaltered.
673 # changeset was a full changeset and is being sent unaltered.
671 if linknode in fullclnodes:
674 if linknode in fullclnodes:
672 linknodes[node] = linknode
675 linknodes[node] = linknode
673
676
674 # If the corresponding changeset wasn't in the set computed
677 # If the corresponding changeset wasn't in the set computed
675 # as relevant to us, it should be dropped outright.
678 # as relevant to us, it should be dropped outright.
676 elif linkrev not in precomputedellipsis:
679 elif linkrev not in precomputedellipsis:
677 continue
680 continue
678
681
679 else:
682 else:
680 # We could probably do this later and avoid the dict
683 # We could probably do this later and avoid the dict
681 # holding state. But it likely doesn't matter.
684 # holding state. But it likely doesn't matter.
682 p1node, p2node, linknode = _resolvenarrowrevisioninfo(
685 p1node, p2node, linknode = _resolvenarrowrevisioninfo(
683 cl, store, ischangelog, rev, linkrev, linknode,
686 cl, store, ischangelog, rev, linkrev, linknode,
684 clrevtolocalrev, fullclnodes, precomputedellipsis)
687 clrevtolocalrev, fullclnodes, precomputedellipsis)
685
688
686 adjustedparents[node] = (p1node, p2node)
689 adjustedparents[node] = (p1node, p2node)
687 linknodes[node] = linknode
690 linknodes[node] = linknode
688
691
689 filtered.append(node)
692 filtered.append(node)
690
693
691 nodes = filtered
694 nodes = filtered
692
695
693 # We expect the first pass to be fast, so we only engage the progress
696 # We expect the first pass to be fast, so we only engage the progress
694 # meter for constructing the revision deltas.
697 # meter for constructing the revision deltas.
695 progress = None
698 progress = None
696 if topic is not None:
699 if topic is not None:
697 progress = repo.ui.makeprogress(topic, unit=_('chunks'),
700 progress = repo.ui.makeprogress(topic, unit=_('chunks'),
698 total=len(nodes))
701 total=len(nodes))
699
702
700 configtarget = repo.ui.config('devel', 'bundle.delta')
703 configtarget = repo.ui.config('devel', 'bundle.delta')
701 if configtarget not in ('', 'p1', 'full'):
704 if configtarget not in ('', 'p1', 'full'):
702 msg = _("""config "devel.bundle.delta" as unknown value: %s""")
705 msg = _("""config "devel.bundle.delta" as unknown value: %s""")
703 repo.ui.warn(msg % configtarget)
706 repo.ui.warn(msg % configtarget)
704
707
705 deltamode = repository.CG_DELTAMODE_STD
708 deltamode = repository.CG_DELTAMODE_STD
706 if forcedeltaparentprev:
709 if forcedeltaparentprev:
707 deltamode = repository.CG_DELTAMODE_PREV
710 deltamode = repository.CG_DELTAMODE_PREV
708 elif configtarget == 'p1':
711 elif configtarget == 'p1':
709 deltamode = repository.CG_DELTAMODE_P1
712 deltamode = repository.CG_DELTAMODE_P1
710 elif configtarget == 'full':
713 elif configtarget == 'full':
711 deltamode = repository.CG_DELTAMODE_FULL
714 deltamode = repository.CG_DELTAMODE_FULL
712
715
713 revisions = store.emitrevisions(
716 revisions = store.emitrevisions(
714 nodes,
717 nodes,
715 nodesorder=nodesorder,
718 nodesorder=nodesorder,
716 revisiondata=True,
719 revisiondata=True,
717 assumehaveparentrevisions=not ellipses,
720 assumehaveparentrevisions=not ellipses,
718 deltamode=deltamode)
721 deltamode=deltamode)
719
722
720 for i, revision in enumerate(revisions):
723 for i, revision in enumerate(revisions):
721 if progress:
724 if progress:
722 progress.update(i + 1)
725 progress.update(i + 1)
723
726
724 if ellipses:
727 if ellipses:
725 linknode = linknodes[revision.node]
728 linknode = linknodes[revision.node]
726
729
727 if revision.node in adjustedparents:
730 if revision.node in adjustedparents:
728 p1node, p2node = adjustedparents[revision.node]
731 p1node, p2node = adjustedparents[revision.node]
729 revision.p1node = p1node
732 revision.p1node = p1node
730 revision.p2node = p2node
733 revision.p2node = p2node
731 revision.flags |= repository.REVISION_FLAG_ELLIPSIS
734 revision.flags |= repository.REVISION_FLAG_ELLIPSIS
732
735
733 else:
736 else:
734 linknode = lookup(revision.node)
737 linknode = lookup(revision.node)
735
738
736 revision.linknode = linknode
739 revision.linknode = linknode
737 yield revision
740 yield revision
738
741
739 if progress:
742 if progress:
740 progress.complete()
743 progress.complete()
741
744
742 class cgpacker(object):
745 class cgpacker(object):
743 def __init__(self, repo, oldmatcher, matcher, version,
746 def __init__(self, repo, oldmatcher, matcher, version,
744 builddeltaheader, manifestsend,
747 builddeltaheader, manifestsend,
745 forcedeltaparentprev=False,
748 forcedeltaparentprev=False,
746 bundlecaps=None, ellipses=False,
749 bundlecaps=None, ellipses=False,
747 shallow=False, ellipsisroots=None, fullnodes=None):
750 shallow=False, ellipsisroots=None, fullnodes=None):
748 """Given a source repo, construct a bundler.
751 """Given a source repo, construct a bundler.
749
752
750 oldmatcher is a matcher that matches on files the client already has.
753 oldmatcher is a matcher that matches on files the client already has.
751 These will not be included in the changegroup.
754 These will not be included in the changegroup.
752
755
753 matcher is a matcher that matches on files to include in the
756 matcher is a matcher that matches on files to include in the
754 changegroup. Used to facilitate sparse changegroups.
757 changegroup. Used to facilitate sparse changegroups.
755
758
756 forcedeltaparentprev indicates whether delta parents must be against
759 forcedeltaparentprev indicates whether delta parents must be against
757 the previous revision in a delta group. This should only be used for
760 the previous revision in a delta group. This should only be used for
758 compatibility with changegroup version 1.
761 compatibility with changegroup version 1.
759
762
760 builddeltaheader is a callable that constructs the header for a group
763 builddeltaheader is a callable that constructs the header for a group
761 delta.
764 delta.
762
765
763 manifestsend is a chunk to send after manifests have been fully emitted.
766 manifestsend is a chunk to send after manifests have been fully emitted.
764
767
765 ellipses indicates whether ellipsis serving mode is enabled.
768 ellipses indicates whether ellipsis serving mode is enabled.
766
769
767 bundlecaps is optional and can be used to specify the set of
770 bundlecaps is optional and can be used to specify the set of
768 capabilities which can be used to build the bundle. While bundlecaps is
771 capabilities which can be used to build the bundle. While bundlecaps is
769 unused in core Mercurial, extensions rely on this feature to communicate
772 unused in core Mercurial, extensions rely on this feature to communicate
770 capabilities to customize the changegroup packer.
773 capabilities to customize the changegroup packer.
771
774
772 shallow indicates whether shallow data might be sent. The packer may
775 shallow indicates whether shallow data might be sent. The packer may
773 need to pack file contents not introduced by the changes being packed.
776 need to pack file contents not introduced by the changes being packed.
774
777
775 fullnodes is the set of changelog nodes which should not be ellipsis
778 fullnodes is the set of changelog nodes which should not be ellipsis
776 nodes. We store this rather than the set of nodes that should be
779 nodes. We store this rather than the set of nodes that should be
777 ellipsis because for very large histories we expect this to be
780 ellipsis because for very large histories we expect this to be
778 significantly smaller.
781 significantly smaller.
779 """
782 """
780 assert oldmatcher
783 assert oldmatcher
781 assert matcher
784 assert matcher
782 self._oldmatcher = oldmatcher
785 self._oldmatcher = oldmatcher
783 self._matcher = matcher
786 self._matcher = matcher
784
787
785 self.version = version
788 self.version = version
786 self._forcedeltaparentprev = forcedeltaparentprev
789 self._forcedeltaparentprev = forcedeltaparentprev
787 self._builddeltaheader = builddeltaheader
790 self._builddeltaheader = builddeltaheader
788 self._manifestsend = manifestsend
791 self._manifestsend = manifestsend
789 self._ellipses = ellipses
792 self._ellipses = ellipses
790
793
791 # Set of capabilities we can use to build the bundle.
794 # Set of capabilities we can use to build the bundle.
792 if bundlecaps is None:
795 if bundlecaps is None:
793 bundlecaps = set()
796 bundlecaps = set()
794 self._bundlecaps = bundlecaps
797 self._bundlecaps = bundlecaps
795 self._isshallow = shallow
798 self._isshallow = shallow
796 self._fullclnodes = fullnodes
799 self._fullclnodes = fullnodes
797
800
798 # Maps ellipsis revs to their roots at the changelog level.
801 # Maps ellipsis revs to their roots at the changelog level.
799 self._precomputedellipsis = ellipsisroots
802 self._precomputedellipsis = ellipsisroots
800
803
801 self._repo = repo
804 self._repo = repo
802
805
803 if self._repo.ui.verbose and not self._repo.ui.debugflag:
806 if self._repo.ui.verbose and not self._repo.ui.debugflag:
804 self._verbosenote = self._repo.ui.note
807 self._verbosenote = self._repo.ui.note
805 else:
808 else:
806 self._verbosenote = lambda s: None
809 self._verbosenote = lambda s: None
807
810
808 def generate(self, commonrevs, clnodes, fastpathlinkrev, source,
811 def generate(self, commonrevs, clnodes, fastpathlinkrev, source,
809 changelog=True):
812 changelog=True):
810 """Yield a sequence of changegroup byte chunks.
813 """Yield a sequence of changegroup byte chunks.
811 If changelog is False, changelog data won't be added to changegroup
814 If changelog is False, changelog data won't be added to changegroup
812 """
815 """
813
816
814 repo = self._repo
817 repo = self._repo
815 cl = repo.changelog
818 cl = repo.changelog
816
819
817 self._verbosenote(_('uncompressed size of bundle content:\n'))
820 self._verbosenote(_('uncompressed size of bundle content:\n'))
818 size = 0
821 size = 0
819
822
820 clstate, deltas = self._generatechangelog(cl, clnodes,
823 clstate, deltas = self._generatechangelog(cl, clnodes,
821 generate=changelog)
824 generate=changelog)
822 for delta in deltas:
825 for delta in deltas:
823 for chunk in _revisiondeltatochunks(delta,
826 for chunk in _revisiondeltatochunks(delta,
824 self._builddeltaheader):
827 self._builddeltaheader):
825 size += len(chunk)
828 size += len(chunk)
826 yield chunk
829 yield chunk
827
830
828 close = closechunk()
831 close = closechunk()
829 size += len(close)
832 size += len(close)
830 yield closechunk()
833 yield closechunk()
831
834
832 self._verbosenote(_('%8.i (changelog)\n') % size)
835 self._verbosenote(_('%8.i (changelog)\n') % size)
833
836
834 clrevorder = clstate['clrevorder']
837 clrevorder = clstate['clrevorder']
835 manifests = clstate['manifests']
838 manifests = clstate['manifests']
836 changedfiles = clstate['changedfiles']
839 changedfiles = clstate['changedfiles']
837
840
838 # We need to make sure that the linkrev in the changegroup refers to
841 # We need to make sure that the linkrev in the changegroup refers to
839 # the first changeset that introduced the manifest or file revision.
842 # the first changeset that introduced the manifest or file revision.
840 # The fastpath is usually safer than the slowpath, because the filelogs
843 # The fastpath is usually safer than the slowpath, because the filelogs
841 # are walked in revlog order.
844 # are walked in revlog order.
842 #
845 #
843 # When taking the slowpath when the manifest revlog uses generaldelta,
846 # When taking the slowpath when the manifest revlog uses generaldelta,
844 # the manifest may be walked in the "wrong" order. Without 'clrevorder',
847 # the manifest may be walked in the "wrong" order. Without 'clrevorder',
845 # we would get an incorrect linkrev (see fix in cc0ff93d0c0c).
848 # we would get an incorrect linkrev (see fix in cc0ff93d0c0c).
846 #
849 #
847 # When taking the fastpath, we are only vulnerable to reordering
850 # When taking the fastpath, we are only vulnerable to reordering
848 # of the changelog itself. The changelog never uses generaldelta and is
851 # of the changelog itself. The changelog never uses generaldelta and is
849 # never reordered. To handle this case, we simply take the slowpath,
852 # never reordered. To handle this case, we simply take the slowpath,
850 # which already has the 'clrevorder' logic. This was also fixed in
853 # which already has the 'clrevorder' logic. This was also fixed in
851 # cc0ff93d0c0c.
854 # cc0ff93d0c0c.
852
855
853 # Treemanifests don't work correctly with fastpathlinkrev
856 # Treemanifests don't work correctly with fastpathlinkrev
854 # either, because we don't discover which directory nodes to
857 # either, because we don't discover which directory nodes to
855 # send along with files. This could probably be fixed.
858 # send along with files. This could probably be fixed.
856 fastpathlinkrev = fastpathlinkrev and (
859 fastpathlinkrev = fastpathlinkrev and (
857 'treemanifest' not in repo.requirements)
860 'treemanifest' not in repo.requirements)
858
861
859 fnodes = {} # needed file nodes
862 fnodes = {} # needed file nodes
860
863
861 size = 0
864 size = 0
862 it = self.generatemanifests(
865 it = self.generatemanifests(
863 commonrevs, clrevorder, fastpathlinkrev, manifests, fnodes, source,
866 commonrevs, clrevorder, fastpathlinkrev, manifests, fnodes, source,
864 clstate['clrevtomanifestrev'])
867 clstate['clrevtomanifestrev'])
865
868
866 for tree, deltas in it:
869 for tree, deltas in it:
867 if tree:
870 if tree:
868 assert self.version == b'03'
871 assert self.version == b'03'
869 chunk = _fileheader(tree)
872 chunk = _fileheader(tree)
870 size += len(chunk)
873 size += len(chunk)
871 yield chunk
874 yield chunk
872
875
873 for delta in deltas:
876 for delta in deltas:
874 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
877 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
875 for chunk in chunks:
878 for chunk in chunks:
876 size += len(chunk)
879 size += len(chunk)
877 yield chunk
880 yield chunk
878
881
879 close = closechunk()
882 close = closechunk()
880 size += len(close)
883 size += len(close)
881 yield close
884 yield close
882
885
883 self._verbosenote(_('%8.i (manifests)\n') % size)
886 self._verbosenote(_('%8.i (manifests)\n') % size)
884 yield self._manifestsend
887 yield self._manifestsend
885
888
886 mfdicts = None
889 mfdicts = None
887 if self._ellipses and self._isshallow:
890 if self._ellipses and self._isshallow:
888 mfdicts = [(self._repo.manifestlog[n].read(), lr)
891 mfdicts = [(self._repo.manifestlog[n].read(), lr)
889 for (n, lr) in manifests.iteritems()]
892 for (n, lr) in manifests.iteritems()]
890
893
891 manifests.clear()
894 manifests.clear()
892 clrevs = set(cl.rev(x) for x in clnodes)
895 clrevs = set(cl.rev(x) for x in clnodes)
893
896
894 it = self.generatefiles(changedfiles, commonrevs,
897 it = self.generatefiles(changedfiles, commonrevs,
895 source, mfdicts, fastpathlinkrev,
898 source, mfdicts, fastpathlinkrev,
896 fnodes, clrevs)
899 fnodes, clrevs)
897
900
898 for path, deltas in it:
901 for path, deltas in it:
899 h = _fileheader(path)
902 h = _fileheader(path)
900 size = len(h)
903 size = len(h)
901 yield h
904 yield h
902
905
903 for delta in deltas:
906 for delta in deltas:
904 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
907 chunks = _revisiondeltatochunks(delta, self._builddeltaheader)
905 for chunk in chunks:
908 for chunk in chunks:
906 size += len(chunk)
909 size += len(chunk)
907 yield chunk
910 yield chunk
908
911
909 close = closechunk()
912 close = closechunk()
910 size += len(close)
913 size += len(close)
911 yield close
914 yield close
912
915
913 self._verbosenote(_('%8.i %s\n') % (size, path))
916 self._verbosenote(_('%8.i %s\n') % (size, path))
914
917
915 yield closechunk()
918 yield closechunk()
916
919
917 if clnodes:
920 if clnodes:
918 repo.hook('outgoing', node=hex(clnodes[0]), source=source)
921 repo.hook('outgoing', node=hex(clnodes[0]), source=source)
919
922
920 def _generatechangelog(self, cl, nodes, generate=True):
923 def _generatechangelog(self, cl, nodes, generate=True):
921 """Generate data for changelog chunks.
924 """Generate data for changelog chunks.
922
925
923 Returns a 2-tuple of a dict containing state and an iterable of
926 Returns a 2-tuple of a dict containing state and an iterable of
924 byte chunks. The state will not be fully populated until the
927 byte chunks. The state will not be fully populated until the
925 chunk stream has been fully consumed.
928 chunk stream has been fully consumed.
926
929
927 if generate is False, the state will be fully populated and no chunk
930 if generate is False, the state will be fully populated and no chunk
928 stream will be yielded
931 stream will be yielded
929 """
932 """
930 clrevorder = {}
933 clrevorder = {}
931 manifests = {}
934 manifests = {}
932 mfl = self._repo.manifestlog
935 mfl = self._repo.manifestlog
933 changedfiles = set()
936 changedfiles = set()
934 clrevtomanifestrev = {}
937 clrevtomanifestrev = {}
935
938
936 state = {
939 state = {
937 'clrevorder': clrevorder,
940 'clrevorder': clrevorder,
938 'manifests': manifests,
941 'manifests': manifests,
939 'changedfiles': changedfiles,
942 'changedfiles': changedfiles,
940 'clrevtomanifestrev': clrevtomanifestrev,
943 'clrevtomanifestrev': clrevtomanifestrev,
941 }
944 }
942
945
943 if not (generate or self._ellipses):
946 if not (generate or self._ellipses):
944 # sort the nodes in storage order
947 # sort the nodes in storage order
945 nodes = sorted(nodes, key=cl.rev)
948 nodes = sorted(nodes, key=cl.rev)
946 for node in nodes:
949 for node in nodes:
947 c = cl.changelogrevision(node)
950 c = cl.changelogrevision(node)
948 clrevorder[node] = len(clrevorder)
951 clrevorder[node] = len(clrevorder)
949 # record the first changeset introducing this manifest version
952 # record the first changeset introducing this manifest version
950 manifests.setdefault(c.manifest, node)
953 manifests.setdefault(c.manifest, node)
951 # Record a complete list of potentially-changed files in
954 # Record a complete list of potentially-changed files in
952 # this manifest.
955 # this manifest.
953 changedfiles.update(c.files)
956 changedfiles.update(c.files)
954
957
955 return state, ()
958 return state, ()
956
959
957 # Callback for the changelog, used to collect changed files and
960 # Callback for the changelog, used to collect changed files and
958 # manifest nodes.
961 # manifest nodes.
959 # Returns the linkrev node (identity in the changelog case).
962 # Returns the linkrev node (identity in the changelog case).
960 def lookupcl(x):
963 def lookupcl(x):
961 c = cl.changelogrevision(x)
964 c = cl.changelogrevision(x)
962 clrevorder[x] = len(clrevorder)
965 clrevorder[x] = len(clrevorder)
963
966
964 if self._ellipses:
967 if self._ellipses:
965 # Only update manifests if x is going to be sent. Otherwise we
968 # Only update manifests if x is going to be sent. Otherwise we
966 # end up with bogus linkrevs specified for manifests and
969 # end up with bogus linkrevs specified for manifests and
967 # we skip some manifest nodes that we should otherwise
970 # we skip some manifest nodes that we should otherwise
968 # have sent.
971 # have sent.
969 if (x in self._fullclnodes
972 if (x in self._fullclnodes
970 or cl.rev(x) in self._precomputedellipsis):
973 or cl.rev(x) in self._precomputedellipsis):
971
974
972 manifestnode = c.manifest
975 manifestnode = c.manifest
973 # Record the first changeset introducing this manifest
976 # Record the first changeset introducing this manifest
974 # version.
977 # version.
975 manifests.setdefault(manifestnode, x)
978 manifests.setdefault(manifestnode, x)
976 # Set this narrow-specific dict so we have the lowest
979 # Set this narrow-specific dict so we have the lowest
977 # manifest revnum to look up for this cl revnum. (Part of
980 # manifest revnum to look up for this cl revnum. (Part of
978 # mapping changelog ellipsis parents to manifest ellipsis
981 # mapping changelog ellipsis parents to manifest ellipsis
979 # parents)
982 # parents)
980 clrevtomanifestrev.setdefault(
983 clrevtomanifestrev.setdefault(
981 cl.rev(x), mfl.rev(manifestnode))
984 cl.rev(x), mfl.rev(manifestnode))
982 # We can't trust the changed files list in the changeset if the
985 # We can't trust the changed files list in the changeset if the
983 # client requested a shallow clone.
986 # client requested a shallow clone.
984 if self._isshallow:
987 if self._isshallow:
985 changedfiles.update(mfl[c.manifest].read().keys())
988 changedfiles.update(mfl[c.manifest].read().keys())
986 else:
989 else:
987 changedfiles.update(c.files)
990 changedfiles.update(c.files)
988 else:
991 else:
989 # record the first changeset introducing this manifest version
992 # record the first changeset introducing this manifest version
990 manifests.setdefault(c.manifest, x)
993 manifests.setdefault(c.manifest, x)
991 # Record a complete list of potentially-changed files in
994 # Record a complete list of potentially-changed files in
992 # this manifest.
995 # this manifest.
993 changedfiles.update(c.files)
996 changedfiles.update(c.files)
994
997
995 return x
998 return x
996
999
997 gen = deltagroup(
1000 gen = deltagroup(
998 self._repo, cl, nodes, True, lookupcl,
1001 self._repo, cl, nodes, True, lookupcl,
999 self._forcedeltaparentprev,
1002 self._forcedeltaparentprev,
1000 ellipses=self._ellipses,
1003 ellipses=self._ellipses,
1001 topic=_('changesets'),
1004 topic=_('changesets'),
1002 clrevtolocalrev={},
1005 clrevtolocalrev={},
1003 fullclnodes=self._fullclnodes,
1006 fullclnodes=self._fullclnodes,
1004 precomputedellipsis=self._precomputedellipsis)
1007 precomputedellipsis=self._precomputedellipsis)
1005
1008
1006 return state, gen
1009 return state, gen
1007
1010
1008 def generatemanifests(self, commonrevs, clrevorder, fastpathlinkrev,
1011 def generatemanifests(self, commonrevs, clrevorder, fastpathlinkrev,
1009 manifests, fnodes, source, clrevtolocalrev):
1012 manifests, fnodes, source, clrevtolocalrev):
1010 """Returns an iterator of changegroup chunks containing manifests.
1013 """Returns an iterator of changegroup chunks containing manifests.
1011
1014
1012 `source` is unused here, but is used by extensions like remotefilelog to
1015 `source` is unused here, but is used by extensions like remotefilelog to
1013 change what is sent based in pulls vs pushes, etc.
1016 change what is sent based in pulls vs pushes, etc.
1014 """
1017 """
1015 repo = self._repo
1018 repo = self._repo
1016 mfl = repo.manifestlog
1019 mfl = repo.manifestlog
1017 tmfnodes = {'': manifests}
1020 tmfnodes = {'': manifests}
1018
1021
1019 # Callback for the manifest, used to collect linkrevs for filelog
1022 # Callback for the manifest, used to collect linkrevs for filelog
1020 # revisions.
1023 # revisions.
1021 # Returns the linkrev node (collected in lookupcl).
1024 # Returns the linkrev node (collected in lookupcl).
1022 def makelookupmflinknode(tree, nodes):
1025 def makelookupmflinknode(tree, nodes):
1023 if fastpathlinkrev:
1026 if fastpathlinkrev:
1024 assert not tree
1027 assert not tree
1025 return manifests.__getitem__
1028 return manifests.__getitem__
1026
1029
1027 def lookupmflinknode(x):
1030 def lookupmflinknode(x):
1028 """Callback for looking up the linknode for manifests.
1031 """Callback for looking up the linknode for manifests.
1029
1032
1030 Returns the linkrev node for the specified manifest.
1033 Returns the linkrev node for the specified manifest.
1031
1034
1032 SIDE EFFECT:
1035 SIDE EFFECT:
1033
1036
1034 1) fclnodes gets populated with the list of relevant
1037 1) fclnodes gets populated with the list of relevant
1035 file nodes if we're not using fastpathlinkrev
1038 file nodes if we're not using fastpathlinkrev
1036 2) When treemanifests are in use, collects treemanifest nodes
1039 2) When treemanifests are in use, collects treemanifest nodes
1037 to send
1040 to send
1038
1041
1039 Note that this means manifests must be completely sent to
1042 Note that this means manifests must be completely sent to
1040 the client before you can trust the list of files and
1043 the client before you can trust the list of files and
1041 treemanifests to send.
1044 treemanifests to send.
1042 """
1045 """
1043 clnode = nodes[x]
1046 clnode = nodes[x]
1044 mdata = mfl.get(tree, x).readfast(shallow=True)
1047 mdata = mfl.get(tree, x).readfast(shallow=True)
1045 for p, n, fl in mdata.iterentries():
1048 for p, n, fl in mdata.iterentries():
1046 if fl == 't': # subdirectory manifest
1049 if fl == 't': # subdirectory manifest
1047 subtree = tree + p + '/'
1050 subtree = tree + p + '/'
1048 tmfclnodes = tmfnodes.setdefault(subtree, {})
1051 tmfclnodes = tmfnodes.setdefault(subtree, {})
1049 tmfclnode = tmfclnodes.setdefault(n, clnode)
1052 tmfclnode = tmfclnodes.setdefault(n, clnode)
1050 if clrevorder[clnode] < clrevorder[tmfclnode]:
1053 if clrevorder[clnode] < clrevorder[tmfclnode]:
1051 tmfclnodes[n] = clnode
1054 tmfclnodes[n] = clnode
1052 else:
1055 else:
1053 f = tree + p
1056 f = tree + p
1054 fclnodes = fnodes.setdefault(f, {})
1057 fclnodes = fnodes.setdefault(f, {})
1055 fclnode = fclnodes.setdefault(n, clnode)
1058 fclnode = fclnodes.setdefault(n, clnode)
1056 if clrevorder[clnode] < clrevorder[fclnode]:
1059 if clrevorder[clnode] < clrevorder[fclnode]:
1057 fclnodes[n] = clnode
1060 fclnodes[n] = clnode
1058 return clnode
1061 return clnode
1059 return lookupmflinknode
1062 return lookupmflinknode
1060
1063
1061 while tmfnodes:
1064 while tmfnodes:
1062 tree, nodes = tmfnodes.popitem()
1065 tree, nodes = tmfnodes.popitem()
1063
1066
1064 should_visit = self._matcher.visitdir(tree[:-1])
1067 should_visit = self._matcher.visitdir(tree[:-1])
1065 if tree and not should_visit:
1068 if tree and not should_visit:
1066 continue
1069 continue
1067
1070
1068 store = mfl.getstorage(tree)
1071 store = mfl.getstorage(tree)
1069
1072
1070 if not should_visit:
1073 if not should_visit:
1071 # No nodes to send because this directory is out of
1074 # No nodes to send because this directory is out of
1072 # the client's view of the repository (probably
1075 # the client's view of the repository (probably
1073 # because of narrow clones). Do this even for the root
1076 # because of narrow clones). Do this even for the root
1074 # directory (tree=='')
1077 # directory (tree=='')
1075 prunednodes = []
1078 prunednodes = []
1076 else:
1079 else:
1077 # Avoid sending any manifest nodes we can prove the
1080 # Avoid sending any manifest nodes we can prove the
1078 # client already has by checking linkrevs. See the
1081 # client already has by checking linkrevs. See the
1079 # related comment in generatefiles().
1082 # related comment in generatefiles().
1080 prunednodes = self._prunemanifests(store, nodes, commonrevs)
1083 prunednodes = self._prunemanifests(store, nodes, commonrevs)
1081
1084
1082 if tree and not prunednodes:
1085 if tree and not prunednodes:
1083 continue
1086 continue
1084
1087
1085 lookupfn = makelookupmflinknode(tree, nodes)
1088 lookupfn = makelookupmflinknode(tree, nodes)
1086
1089
1087 deltas = deltagroup(
1090 deltas = deltagroup(
1088 self._repo, store, prunednodes, False, lookupfn,
1091 self._repo, store, prunednodes, False, lookupfn,
1089 self._forcedeltaparentprev,
1092 self._forcedeltaparentprev,
1090 ellipses=self._ellipses,
1093 ellipses=self._ellipses,
1091 topic=_('manifests'),
1094 topic=_('manifests'),
1092 clrevtolocalrev=clrevtolocalrev,
1095 clrevtolocalrev=clrevtolocalrev,
1093 fullclnodes=self._fullclnodes,
1096 fullclnodes=self._fullclnodes,
1094 precomputedellipsis=self._precomputedellipsis)
1097 precomputedellipsis=self._precomputedellipsis)
1095
1098
1096 if not self._oldmatcher.visitdir(store.tree[:-1]):
1099 if not self._oldmatcher.visitdir(store.tree[:-1]):
1097 yield tree, deltas
1100 yield tree, deltas
1098 else:
1101 else:
1099 # 'deltas' is a generator and we need to consume it even if
1102 # 'deltas' is a generator and we need to consume it even if
1100 # we are not going to send it because a side-effect is that
1103 # we are not going to send it because a side-effect is that
1101 # it updates tmdnodes (via lookupfn)
1104 # it updates tmdnodes (via lookupfn)
1102 for d in deltas:
1105 for d in deltas:
1103 pass
1106 pass
1104 if not tree:
1107 if not tree:
1105 yield tree, []
1108 yield tree, []
1106
1109
1107 def _prunemanifests(self, store, nodes, commonrevs):
1110 def _prunemanifests(self, store, nodes, commonrevs):
1108 if not self._ellipses:
1111 if not self._ellipses:
1109 # In non-ellipses case and large repositories, it is better to
1112 # In non-ellipses case and large repositories, it is better to
1110 # prevent calling of store.rev and store.linkrev on a lot of
1113 # prevent calling of store.rev and store.linkrev on a lot of
1111 # nodes as compared to sending some extra data
1114 # nodes as compared to sending some extra data
1112 return nodes.copy()
1115 return nodes.copy()
1113 # This is split out as a separate method to allow filtering
1116 # This is split out as a separate method to allow filtering
1114 # commonrevs in extension code.
1117 # commonrevs in extension code.
1115 #
1118 #
1116 # TODO(augie): this shouldn't be required, instead we should
1119 # TODO(augie): this shouldn't be required, instead we should
1117 # make filtering of revisions to send delegated to the store
1120 # make filtering of revisions to send delegated to the store
1118 # layer.
1121 # layer.
1119 frev, flr = store.rev, store.linkrev
1122 frev, flr = store.rev, store.linkrev
1120 return [n for n in nodes if flr(frev(n)) not in commonrevs]
1123 return [n for n in nodes if flr(frev(n)) not in commonrevs]
1121
1124
1122 # The 'source' parameter is useful for extensions
1125 # The 'source' parameter is useful for extensions
1123 def generatefiles(self, changedfiles, commonrevs, source,
1126 def generatefiles(self, changedfiles, commonrevs, source,
1124 mfdicts, fastpathlinkrev, fnodes, clrevs):
1127 mfdicts, fastpathlinkrev, fnodes, clrevs):
1125 changedfiles = [f for f in changedfiles
1128 changedfiles = [f for f in changedfiles
1126 if self._matcher(f) and not self._oldmatcher(f)]
1129 if self._matcher(f) and not self._oldmatcher(f)]
1127
1130
1128 if not fastpathlinkrev:
1131 if not fastpathlinkrev:
1129 def normallinknodes(unused, fname):
1132 def normallinknodes(unused, fname):
1130 return fnodes.get(fname, {})
1133 return fnodes.get(fname, {})
1131 else:
1134 else:
1132 cln = self._repo.changelog.node
1135 cln = self._repo.changelog.node
1133
1136
1134 def normallinknodes(store, fname):
1137 def normallinknodes(store, fname):
1135 flinkrev = store.linkrev
1138 flinkrev = store.linkrev
1136 fnode = store.node
1139 fnode = store.node
1137 revs = ((r, flinkrev(r)) for r in store)
1140 revs = ((r, flinkrev(r)) for r in store)
1138 return dict((fnode(r), cln(lr))
1141 return dict((fnode(r), cln(lr))
1139 for r, lr in revs if lr in clrevs)
1142 for r, lr in revs if lr in clrevs)
1140
1143
1141 clrevtolocalrev = {}
1144 clrevtolocalrev = {}
1142
1145
1143 if self._isshallow:
1146 if self._isshallow:
1144 # In a shallow clone, the linknodes callback needs to also include
1147 # In a shallow clone, the linknodes callback needs to also include
1145 # those file nodes that are in the manifests we sent but weren't
1148 # those file nodes that are in the manifests we sent but weren't
1146 # introduced by those manifests.
1149 # introduced by those manifests.
1147 commonctxs = [self._repo[c] for c in commonrevs]
1150 commonctxs = [self._repo[c] for c in commonrevs]
1148 clrev = self._repo.changelog.rev
1151 clrev = self._repo.changelog.rev
1149
1152
1150 def linknodes(flog, fname):
1153 def linknodes(flog, fname):
1151 for c in commonctxs:
1154 for c in commonctxs:
1152 try:
1155 try:
1153 fnode = c.filenode(fname)
1156 fnode = c.filenode(fname)
1154 clrevtolocalrev[c.rev()] = flog.rev(fnode)
1157 clrevtolocalrev[c.rev()] = flog.rev(fnode)
1155 except error.ManifestLookupError:
1158 except error.ManifestLookupError:
1156 pass
1159 pass
1157 links = normallinknodes(flog, fname)
1160 links = normallinknodes(flog, fname)
1158 if len(links) != len(mfdicts):
1161 if len(links) != len(mfdicts):
1159 for mf, lr in mfdicts:
1162 for mf, lr in mfdicts:
1160 fnode = mf.get(fname, None)
1163 fnode = mf.get(fname, None)
1161 if fnode in links:
1164 if fnode in links:
1162 links[fnode] = min(links[fnode], lr, key=clrev)
1165 links[fnode] = min(links[fnode], lr, key=clrev)
1163 elif fnode:
1166 elif fnode:
1164 links[fnode] = lr
1167 links[fnode] = lr
1165 return links
1168 return links
1166 else:
1169 else:
1167 linknodes = normallinknodes
1170 linknodes = normallinknodes
1168
1171
1169 repo = self._repo
1172 repo = self._repo
1170 progress = repo.ui.makeprogress(_('files'), unit=_('files'),
1173 progress = repo.ui.makeprogress(_('files'), unit=_('files'),
1171 total=len(changedfiles))
1174 total=len(changedfiles))
1172 for i, fname in enumerate(sorted(changedfiles)):
1175 for i, fname in enumerate(sorted(changedfiles)):
1173 filerevlog = repo.file(fname)
1176 filerevlog = repo.file(fname)
1174 if not filerevlog:
1177 if not filerevlog:
1175 raise error.Abort(_("empty or missing file data for %s") %
1178 raise error.Abort(_("empty or missing file data for %s") %
1176 fname)
1179 fname)
1177
1180
1178 clrevtolocalrev.clear()
1181 clrevtolocalrev.clear()
1179
1182
1180 linkrevnodes = linknodes(filerevlog, fname)
1183 linkrevnodes = linknodes(filerevlog, fname)
1181 # Lookup for filenodes, we collected the linkrev nodes above in the
1184 # Lookup for filenodes, we collected the linkrev nodes above in the
1182 # fastpath case and with lookupmf in the slowpath case.
1185 # fastpath case and with lookupmf in the slowpath case.
1183 def lookupfilelog(x):
1186 def lookupfilelog(x):
1184 return linkrevnodes[x]
1187 return linkrevnodes[x]
1185
1188
1186 frev, flr = filerevlog.rev, filerevlog.linkrev
1189 frev, flr = filerevlog.rev, filerevlog.linkrev
1187 # Skip sending any filenode we know the client already
1190 # Skip sending any filenode we know the client already
1188 # has. This avoids over-sending files relatively
1191 # has. This avoids over-sending files relatively
1189 # inexpensively, so it's not a problem if we under-filter
1192 # inexpensively, so it's not a problem if we under-filter
1190 # here.
1193 # here.
1191 filenodes = [n for n in linkrevnodes
1194 filenodes = [n for n in linkrevnodes
1192 if flr(frev(n)) not in commonrevs]
1195 if flr(frev(n)) not in commonrevs]
1193
1196
1194 if not filenodes:
1197 if not filenodes:
1195 continue
1198 continue
1196
1199
1197 progress.update(i + 1, item=fname)
1200 progress.update(i + 1, item=fname)
1198
1201
1199 deltas = deltagroup(
1202 deltas = deltagroup(
1200 self._repo, filerevlog, filenodes, False, lookupfilelog,
1203 self._repo, filerevlog, filenodes, False, lookupfilelog,
1201 self._forcedeltaparentprev,
1204 self._forcedeltaparentprev,
1202 ellipses=self._ellipses,
1205 ellipses=self._ellipses,
1203 clrevtolocalrev=clrevtolocalrev,
1206 clrevtolocalrev=clrevtolocalrev,
1204 fullclnodes=self._fullclnodes,
1207 fullclnodes=self._fullclnodes,
1205 precomputedellipsis=self._precomputedellipsis)
1208 precomputedellipsis=self._precomputedellipsis)
1206
1209
1207 yield fname, deltas
1210 yield fname, deltas
1208
1211
1209 progress.complete()
1212 progress.complete()
1210
1213
1211 def _makecg1packer(repo, oldmatcher, matcher, bundlecaps,
1214 def _makecg1packer(repo, oldmatcher, matcher, bundlecaps,
1212 ellipses=False, shallow=False, ellipsisroots=None,
1215 ellipses=False, shallow=False, ellipsisroots=None,
1213 fullnodes=None):
1216 fullnodes=None):
1214 builddeltaheader = lambda d: _CHANGEGROUPV1_DELTA_HEADER.pack(
1217 builddeltaheader = lambda d: _CHANGEGROUPV1_DELTA_HEADER.pack(
1215 d.node, d.p1node, d.p2node, d.linknode)
1218 d.node, d.p1node, d.p2node, d.linknode)
1216
1219
1217 return cgpacker(repo, oldmatcher, matcher, b'01',
1220 return cgpacker(repo, oldmatcher, matcher, b'01',
1218 builddeltaheader=builddeltaheader,
1221 builddeltaheader=builddeltaheader,
1219 manifestsend=b'',
1222 manifestsend=b'',
1220 forcedeltaparentprev=True,
1223 forcedeltaparentprev=True,
1221 bundlecaps=bundlecaps,
1224 bundlecaps=bundlecaps,
1222 ellipses=ellipses,
1225 ellipses=ellipses,
1223 shallow=shallow,
1226 shallow=shallow,
1224 ellipsisroots=ellipsisroots,
1227 ellipsisroots=ellipsisroots,
1225 fullnodes=fullnodes)
1228 fullnodes=fullnodes)
1226
1229
1227 def _makecg2packer(repo, oldmatcher, matcher, bundlecaps,
1230 def _makecg2packer(repo, oldmatcher, matcher, bundlecaps,
1228 ellipses=False, shallow=False, ellipsisroots=None,
1231 ellipses=False, shallow=False, ellipsisroots=None,
1229 fullnodes=None):
1232 fullnodes=None):
1230 builddeltaheader = lambda d: _CHANGEGROUPV2_DELTA_HEADER.pack(
1233 builddeltaheader = lambda d: _CHANGEGROUPV2_DELTA_HEADER.pack(
1231 d.node, d.p1node, d.p2node, d.basenode, d.linknode)
1234 d.node, d.p1node, d.p2node, d.basenode, d.linknode)
1232
1235
1233 return cgpacker(repo, oldmatcher, matcher, b'02',
1236 return cgpacker(repo, oldmatcher, matcher, b'02',
1234 builddeltaheader=builddeltaheader,
1237 builddeltaheader=builddeltaheader,
1235 manifestsend=b'',
1238 manifestsend=b'',
1236 bundlecaps=bundlecaps,
1239 bundlecaps=bundlecaps,
1237 ellipses=ellipses,
1240 ellipses=ellipses,
1238 shallow=shallow,
1241 shallow=shallow,
1239 ellipsisroots=ellipsisroots,
1242 ellipsisroots=ellipsisroots,
1240 fullnodes=fullnodes)
1243 fullnodes=fullnodes)
1241
1244
1242 def _makecg3packer(repo, oldmatcher, matcher, bundlecaps,
1245 def _makecg3packer(repo, oldmatcher, matcher, bundlecaps,
1243 ellipses=False, shallow=False, ellipsisroots=None,
1246 ellipses=False, shallow=False, ellipsisroots=None,
1244 fullnodes=None):
1247 fullnodes=None):
1245 builddeltaheader = lambda d: _CHANGEGROUPV3_DELTA_HEADER.pack(
1248 builddeltaheader = lambda d: _CHANGEGROUPV3_DELTA_HEADER.pack(
1246 d.node, d.p1node, d.p2node, d.basenode, d.linknode, d.flags)
1249 d.node, d.p1node, d.p2node, d.basenode, d.linknode, d.flags)
1247
1250
1248 return cgpacker(repo, oldmatcher, matcher, b'03',
1251 return cgpacker(repo, oldmatcher, matcher, b'03',
1249 builddeltaheader=builddeltaheader,
1252 builddeltaheader=builddeltaheader,
1250 manifestsend=closechunk(),
1253 manifestsend=closechunk(),
1251 bundlecaps=bundlecaps,
1254 bundlecaps=bundlecaps,
1252 ellipses=ellipses,
1255 ellipses=ellipses,
1253 shallow=shallow,
1256 shallow=shallow,
1254 ellipsisroots=ellipsisroots,
1257 ellipsisroots=ellipsisroots,
1255 fullnodes=fullnodes)
1258 fullnodes=fullnodes)
1256
1259
1257 _packermap = {'01': (_makecg1packer, cg1unpacker),
1260 _packermap = {'01': (_makecg1packer, cg1unpacker),
1258 # cg2 adds support for exchanging generaldelta
1261 # cg2 adds support for exchanging generaldelta
1259 '02': (_makecg2packer, cg2unpacker),
1262 '02': (_makecg2packer, cg2unpacker),
1260 # cg3 adds support for exchanging revlog flags and treemanifests
1263 # cg3 adds support for exchanging revlog flags and treemanifests
1261 '03': (_makecg3packer, cg3unpacker),
1264 '03': (_makecg3packer, cg3unpacker),
1262 }
1265 }
1263
1266
1264 def allsupportedversions(repo):
1267 def allsupportedversions(repo):
1265 versions = set(_packermap.keys())
1268 versions = set(_packermap.keys())
1266 if not (repo.ui.configbool('experimental', 'changegroup3') or
1269 if not (repo.ui.configbool('experimental', 'changegroup3') or
1267 repo.ui.configbool('experimental', 'treemanifest') or
1270 repo.ui.configbool('experimental', 'treemanifest') or
1268 'treemanifest' in repo.requirements):
1271 'treemanifest' in repo.requirements):
1269 versions.discard('03')
1272 versions.discard('03')
1270 return versions
1273 return versions
1271
1274
1272 # Changegroup versions that can be applied to the repo
1275 # Changegroup versions that can be applied to the repo
1273 def supportedincomingversions(repo):
1276 def supportedincomingversions(repo):
1274 return allsupportedversions(repo)
1277 return allsupportedversions(repo)
1275
1278
1276 # Changegroup versions that can be created from the repo
1279 # Changegroup versions that can be created from the repo
1277 def supportedoutgoingversions(repo):
1280 def supportedoutgoingversions(repo):
1278 versions = allsupportedversions(repo)
1281 versions = allsupportedversions(repo)
1279 if 'treemanifest' in repo.requirements:
1282 if 'treemanifest' in repo.requirements:
1280 # Versions 01 and 02 support only flat manifests and it's just too
1283 # Versions 01 and 02 support only flat manifests and it's just too
1281 # expensive to convert between the flat manifest and tree manifest on
1284 # expensive to convert between the flat manifest and tree manifest on
1282 # the fly. Since tree manifests are hashed differently, all of history
1285 # the fly. Since tree manifests are hashed differently, all of history
1283 # would have to be converted. Instead, we simply don't even pretend to
1286 # would have to be converted. Instead, we simply don't even pretend to
1284 # support versions 01 and 02.
1287 # support versions 01 and 02.
1285 versions.discard('01')
1288 versions.discard('01')
1286 versions.discard('02')
1289 versions.discard('02')
1287 if repository.NARROW_REQUIREMENT in repo.requirements:
1290 if repository.NARROW_REQUIREMENT in repo.requirements:
1288 # Versions 01 and 02 don't support revlog flags, and we need to
1291 # Versions 01 and 02 don't support revlog flags, and we need to
1289 # support that for stripping and unbundling to work.
1292 # support that for stripping and unbundling to work.
1290 versions.discard('01')
1293 versions.discard('01')
1291 versions.discard('02')
1294 versions.discard('02')
1292 if LFS_REQUIREMENT in repo.requirements:
1295 if LFS_REQUIREMENT in repo.requirements:
1293 # Versions 01 and 02 don't support revlog flags, and we need to
1296 # Versions 01 and 02 don't support revlog flags, and we need to
1294 # mark LFS entries with REVIDX_EXTSTORED.
1297 # mark LFS entries with REVIDX_EXTSTORED.
1295 versions.discard('01')
1298 versions.discard('01')
1296 versions.discard('02')
1299 versions.discard('02')
1297
1300
1298 return versions
1301 return versions
1299
1302
1300 def localversion(repo):
1303 def localversion(repo):
1301 # Finds the best version to use for bundles that are meant to be used
1304 # Finds the best version to use for bundles that are meant to be used
1302 # locally, such as those from strip and shelve, and temporary bundles.
1305 # locally, such as those from strip and shelve, and temporary bundles.
1303 return max(supportedoutgoingversions(repo))
1306 return max(supportedoutgoingversions(repo))
1304
1307
1305 def safeversion(repo):
1308 def safeversion(repo):
1306 # Finds the smallest version that it's safe to assume clients of the repo
1309 # Finds the smallest version that it's safe to assume clients of the repo
1307 # will support. For example, all hg versions that support generaldelta also
1310 # will support. For example, all hg versions that support generaldelta also
1308 # support changegroup 02.
1311 # support changegroup 02.
1309 versions = supportedoutgoingversions(repo)
1312 versions = supportedoutgoingversions(repo)
1310 if 'generaldelta' in repo.requirements:
1313 if 'generaldelta' in repo.requirements:
1311 versions.discard('01')
1314 versions.discard('01')
1312 assert versions
1315 assert versions
1313 return min(versions)
1316 return min(versions)
1314
1317
1315 def getbundler(version, repo, bundlecaps=None, oldmatcher=None,
1318 def getbundler(version, repo, bundlecaps=None, oldmatcher=None,
1316 matcher=None, ellipses=False, shallow=False,
1319 matcher=None, ellipses=False, shallow=False,
1317 ellipsisroots=None, fullnodes=None):
1320 ellipsisroots=None, fullnodes=None):
1318 assert version in supportedoutgoingversions(repo)
1321 assert version in supportedoutgoingversions(repo)
1319
1322
1320 if matcher is None:
1323 if matcher is None:
1321 matcher = matchmod.always()
1324 matcher = matchmod.always()
1322 if oldmatcher is None:
1325 if oldmatcher is None:
1323 oldmatcher = matchmod.never()
1326 oldmatcher = matchmod.never()
1324
1327
1325 if version == '01' and not matcher.always():
1328 if version == '01' and not matcher.always():
1326 raise error.ProgrammingError('version 01 changegroups do not support '
1329 raise error.ProgrammingError('version 01 changegroups do not support '
1327 'sparse file matchers')
1330 'sparse file matchers')
1328
1331
1329 if ellipses and version in (b'01', b'02'):
1332 if ellipses and version in (b'01', b'02'):
1330 raise error.Abort(
1333 raise error.Abort(
1331 _('ellipsis nodes require at least cg3 on client and server, '
1334 _('ellipsis nodes require at least cg3 on client and server, '
1332 'but negotiated version %s') % version)
1335 'but negotiated version %s') % version)
1333
1336
1334 # Requested files could include files not in the local store. So
1337 # Requested files could include files not in the local store. So
1335 # filter those out.
1338 # filter those out.
1336 matcher = repo.narrowmatch(matcher)
1339 matcher = repo.narrowmatch(matcher)
1337
1340
1338 fn = _packermap[version][0]
1341 fn = _packermap[version][0]
1339 return fn(repo, oldmatcher, matcher, bundlecaps, ellipses=ellipses,
1342 return fn(repo, oldmatcher, matcher, bundlecaps, ellipses=ellipses,
1340 shallow=shallow, ellipsisroots=ellipsisroots,
1343 shallow=shallow, ellipsisroots=ellipsisroots,
1341 fullnodes=fullnodes)
1344 fullnodes=fullnodes)
1342
1345
1343 def getunbundler(version, fh, alg, extras=None):
1346 def getunbundler(version, fh, alg, extras=None):
1344 return _packermap[version][1](fh, alg, extras=extras)
1347 return _packermap[version][1](fh, alg, extras=extras)
1345
1348
1346 def _changegroupinfo(repo, nodes, source):
1349 def _changegroupinfo(repo, nodes, source):
1347 if repo.ui.verbose or source == 'bundle':
1350 if repo.ui.verbose or source == 'bundle':
1348 repo.ui.status(_("%d changesets found\n") % len(nodes))
1351 repo.ui.status(_("%d changesets found\n") % len(nodes))
1349 if repo.ui.debugflag:
1352 if repo.ui.debugflag:
1350 repo.ui.debug("list of changesets:\n")
1353 repo.ui.debug("list of changesets:\n")
1351 for node in nodes:
1354 for node in nodes:
1352 repo.ui.debug("%s\n" % hex(node))
1355 repo.ui.debug("%s\n" % hex(node))
1353
1356
1354 def makechangegroup(repo, outgoing, version, source, fastpath=False,
1357 def makechangegroup(repo, outgoing, version, source, fastpath=False,
1355 bundlecaps=None):
1358 bundlecaps=None):
1356 cgstream = makestream(repo, outgoing, version, source,
1359 cgstream = makestream(repo, outgoing, version, source,
1357 fastpath=fastpath, bundlecaps=bundlecaps)
1360 fastpath=fastpath, bundlecaps=bundlecaps)
1358 return getunbundler(version, util.chunkbuffer(cgstream), None,
1361 return getunbundler(version, util.chunkbuffer(cgstream), None,
1359 {'clcount': len(outgoing.missing) })
1362 {'clcount': len(outgoing.missing) })
1360
1363
1361 def makestream(repo, outgoing, version, source, fastpath=False,
1364 def makestream(repo, outgoing, version, source, fastpath=False,
1362 bundlecaps=None, matcher=None):
1365 bundlecaps=None, matcher=None):
1363 bundler = getbundler(version, repo, bundlecaps=bundlecaps,
1366 bundler = getbundler(version, repo, bundlecaps=bundlecaps,
1364 matcher=matcher)
1367 matcher=matcher)
1365
1368
1366 repo = repo.unfiltered()
1369 repo = repo.unfiltered()
1367 commonrevs = outgoing.common
1370 commonrevs = outgoing.common
1368 csets = outgoing.missing
1371 csets = outgoing.missing
1369 heads = outgoing.missingheads
1372 heads = outgoing.missingheads
1370 # We go through the fast path if we get told to, or if all (unfiltered
1373 # We go through the fast path if we get told to, or if all (unfiltered
1371 # heads have been requested (since we then know there all linkrevs will
1374 # heads have been requested (since we then know there all linkrevs will
1372 # be pulled by the client).
1375 # be pulled by the client).
1373 heads.sort()
1376 heads.sort()
1374 fastpathlinkrev = fastpath or (
1377 fastpathlinkrev = fastpath or (
1375 repo.filtername is None and heads == sorted(repo.heads()))
1378 repo.filtername is None and heads == sorted(repo.heads()))
1376
1379
1377 repo.hook('preoutgoing', throw=True, source=source)
1380 repo.hook('preoutgoing', throw=True, source=source)
1378 _changegroupinfo(repo, csets, source)
1381 _changegroupinfo(repo, csets, source)
1379 return bundler.generate(commonrevs, csets, fastpathlinkrev, source)
1382 return bundler.generate(commonrevs, csets, fastpathlinkrev, source)
1380
1383
1381 def _addchangegroupfiles(repo, source, revmap, trp, expectedfiles, needfiles):
1384 def _addchangegroupfiles(repo, source, revmap, trp, expectedfiles, needfiles):
1382 revisions = 0
1385 revisions = 0
1383 files = 0
1386 files = 0
1384 progress = repo.ui.makeprogress(_('files'), unit=_('files'),
1387 progress = repo.ui.makeprogress(_('files'), unit=_('files'),
1385 total=expectedfiles)
1388 total=expectedfiles)
1386 for chunkdata in iter(source.filelogheader, {}):
1389 for chunkdata in iter(source.filelogheader, {}):
1387 files += 1
1390 files += 1
1388 f = chunkdata["filename"]
1391 f = chunkdata["filename"]
1389 repo.ui.debug("adding %s revisions\n" % f)
1392 repo.ui.debug("adding %s revisions\n" % f)
1390 progress.increment()
1393 progress.increment()
1391 fl = repo.file(f)
1394 fl = repo.file(f)
1392 o = len(fl)
1395 o = len(fl)
1393 try:
1396 try:
1394 deltas = source.deltaiter()
1397 deltas = source.deltaiter()
1395 if not fl.addgroup(deltas, revmap, trp):
1398 if not fl.addgroup(deltas, revmap, trp):
1396 raise error.Abort(_("received file revlog group is empty"))
1399 raise error.Abort(_("received file revlog group is empty"))
1397 except error.CensoredBaseError as e:
1400 except error.CensoredBaseError as e:
1398 raise error.Abort(_("received delta base is censored: %s") % e)
1401 raise error.Abort(_("received delta base is censored: %s") % e)
1399 revisions += len(fl) - o
1402 revisions += len(fl) - o
1400 if f in needfiles:
1403 if f in needfiles:
1401 needs = needfiles[f]
1404 needs = needfiles[f]
1402 for new in pycompat.xrange(o, len(fl)):
1405 for new in pycompat.xrange(o, len(fl)):
1403 n = fl.node(new)
1406 n = fl.node(new)
1404 if n in needs:
1407 if n in needs:
1405 needs.remove(n)
1408 needs.remove(n)
1406 else:
1409 else:
1407 raise error.Abort(
1410 raise error.Abort(
1408 _("received spurious file revlog entry"))
1411 _("received spurious file revlog entry"))
1409 if not needs:
1412 if not needs:
1410 del needfiles[f]
1413 del needfiles[f]
1411 progress.complete()
1414 progress.complete()
1412
1415
1413 for f, needs in needfiles.iteritems():
1416 for f, needs in needfiles.iteritems():
1414 fl = repo.file(f)
1417 fl = repo.file(f)
1415 for n in needs:
1418 for n in needs:
1416 try:
1419 try:
1417 fl.rev(n)
1420 fl.rev(n)
1418 except error.LookupError:
1421 except error.LookupError:
1419 raise error.Abort(
1422 raise error.Abort(
1420 _('missing file data for %s:%s - run hg verify') %
1423 _('missing file data for %s:%s - run hg verify') %
1421 (f, hex(n)))
1424 (f, hex(n)))
1422
1425
1423 return revisions, files
1426 return revisions, files
@@ -1,2701 +1,2703
1 # exchange.py - utility to exchange data between repos.
1 # exchange.py - utility to exchange data between repos.
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import collections
10 import collections
11 import hashlib
11 import hashlib
12
12
13 from .i18n import _
13 from .i18n import _
14 from .node import (
14 from .node import (
15 bin,
15 bin,
16 hex,
16 hex,
17 nullid,
17 nullid,
18 nullrev,
18 nullrev,
19 )
19 )
20 from .thirdparty import (
20 from .thirdparty import (
21 attr,
21 attr,
22 )
22 )
23 from . import (
23 from . import (
24 bookmarks as bookmod,
24 bookmarks as bookmod,
25 bundle2,
25 bundle2,
26 changegroup,
26 changegroup,
27 discovery,
27 discovery,
28 error,
28 error,
29 exchangev2,
29 exchangev2,
30 lock as lockmod,
30 lock as lockmod,
31 logexchange,
31 logexchange,
32 narrowspec,
32 narrowspec,
33 obsolete,
33 obsolete,
34 phases,
34 phases,
35 pushkey,
35 pushkey,
36 pycompat,
36 pycompat,
37 repository,
38 scmutil,
37 scmutil,
39 sslutil,
38 sslutil,
40 streamclone,
39 streamclone,
41 url as urlmod,
40 url as urlmod,
42 util,
41 util,
43 wireprototypes,
42 wireprototypes,
44 )
43 )
44 from .interfaces import (
45 repository,
46 )
45 from .utils import (
47 from .utils import (
46 stringutil,
48 stringutil,
47 )
49 )
48
50
49 urlerr = util.urlerr
51 urlerr = util.urlerr
50 urlreq = util.urlreq
52 urlreq = util.urlreq
51
53
52 _NARROWACL_SECTION = 'narrowacl'
54 _NARROWACL_SECTION = 'narrowacl'
53
55
54 # Maps bundle version human names to changegroup versions.
56 # Maps bundle version human names to changegroup versions.
55 _bundlespeccgversions = {'v1': '01',
57 _bundlespeccgversions = {'v1': '01',
56 'v2': '02',
58 'v2': '02',
57 'packed1': 's1',
59 'packed1': 's1',
58 'bundle2': '02', #legacy
60 'bundle2': '02', #legacy
59 }
61 }
60
62
61 # Maps bundle version with content opts to choose which part to bundle
63 # Maps bundle version with content opts to choose which part to bundle
62 _bundlespeccontentopts = {
64 _bundlespeccontentopts = {
63 'v1': {
65 'v1': {
64 'changegroup': True,
66 'changegroup': True,
65 'cg.version': '01',
67 'cg.version': '01',
66 'obsolescence': False,
68 'obsolescence': False,
67 'phases': False,
69 'phases': False,
68 'tagsfnodescache': False,
70 'tagsfnodescache': False,
69 'revbranchcache': False
71 'revbranchcache': False
70 },
72 },
71 'v2': {
73 'v2': {
72 'changegroup': True,
74 'changegroup': True,
73 'cg.version': '02',
75 'cg.version': '02',
74 'obsolescence': False,
76 'obsolescence': False,
75 'phases': False,
77 'phases': False,
76 'tagsfnodescache': True,
78 'tagsfnodescache': True,
77 'revbranchcache': True
79 'revbranchcache': True
78 },
80 },
79 'packed1' : {
81 'packed1' : {
80 'cg.version': 's1'
82 'cg.version': 's1'
81 }
83 }
82 }
84 }
83 _bundlespeccontentopts['bundle2'] = _bundlespeccontentopts['v2']
85 _bundlespeccontentopts['bundle2'] = _bundlespeccontentopts['v2']
84
86
85 _bundlespecvariants = {"streamv2": {"changegroup": False, "streamv2": True,
87 _bundlespecvariants = {"streamv2": {"changegroup": False, "streamv2": True,
86 "tagsfnodescache": False,
88 "tagsfnodescache": False,
87 "revbranchcache": False}}
89 "revbranchcache": False}}
88
90
89 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
91 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
90 _bundlespecv1compengines = {'gzip', 'bzip2', 'none'}
92 _bundlespecv1compengines = {'gzip', 'bzip2', 'none'}
91
93
92 @attr.s
94 @attr.s
93 class bundlespec(object):
95 class bundlespec(object):
94 compression = attr.ib()
96 compression = attr.ib()
95 wirecompression = attr.ib()
97 wirecompression = attr.ib()
96 version = attr.ib()
98 version = attr.ib()
97 wireversion = attr.ib()
99 wireversion = attr.ib()
98 params = attr.ib()
100 params = attr.ib()
99 contentopts = attr.ib()
101 contentopts = attr.ib()
100
102
101 def parsebundlespec(repo, spec, strict=True):
103 def parsebundlespec(repo, spec, strict=True):
102 """Parse a bundle string specification into parts.
104 """Parse a bundle string specification into parts.
103
105
104 Bundle specifications denote a well-defined bundle/exchange format.
106 Bundle specifications denote a well-defined bundle/exchange format.
105 The content of a given specification should not change over time in
107 The content of a given specification should not change over time in
106 order to ensure that bundles produced by a newer version of Mercurial are
108 order to ensure that bundles produced by a newer version of Mercurial are
107 readable from an older version.
109 readable from an older version.
108
110
109 The string currently has the form:
111 The string currently has the form:
110
112
111 <compression>-<type>[;<parameter0>[;<parameter1>]]
113 <compression>-<type>[;<parameter0>[;<parameter1>]]
112
114
113 Where <compression> is one of the supported compression formats
115 Where <compression> is one of the supported compression formats
114 and <type> is (currently) a version string. A ";" can follow the type and
116 and <type> is (currently) a version string. A ";" can follow the type and
115 all text afterwards is interpreted as URI encoded, ";" delimited key=value
117 all text afterwards is interpreted as URI encoded, ";" delimited key=value
116 pairs.
118 pairs.
117
119
118 If ``strict`` is True (the default) <compression> is required. Otherwise,
120 If ``strict`` is True (the default) <compression> is required. Otherwise,
119 it is optional.
121 it is optional.
120
122
121 Returns a bundlespec object of (compression, version, parameters).
123 Returns a bundlespec object of (compression, version, parameters).
122 Compression will be ``None`` if not in strict mode and a compression isn't
124 Compression will be ``None`` if not in strict mode and a compression isn't
123 defined.
125 defined.
124
126
125 An ``InvalidBundleSpecification`` is raised when the specification is
127 An ``InvalidBundleSpecification`` is raised when the specification is
126 not syntactically well formed.
128 not syntactically well formed.
127
129
128 An ``UnsupportedBundleSpecification`` is raised when the compression or
130 An ``UnsupportedBundleSpecification`` is raised when the compression or
129 bundle type/version is not recognized.
131 bundle type/version is not recognized.
130
132
131 Note: this function will likely eventually return a more complex data
133 Note: this function will likely eventually return a more complex data
132 structure, including bundle2 part information.
134 structure, including bundle2 part information.
133 """
135 """
134 def parseparams(s):
136 def parseparams(s):
135 if ';' not in s:
137 if ';' not in s:
136 return s, {}
138 return s, {}
137
139
138 params = {}
140 params = {}
139 version, paramstr = s.split(';', 1)
141 version, paramstr = s.split(';', 1)
140
142
141 for p in paramstr.split(';'):
143 for p in paramstr.split(';'):
142 if '=' not in p:
144 if '=' not in p:
143 raise error.InvalidBundleSpecification(
145 raise error.InvalidBundleSpecification(
144 _('invalid bundle specification: '
146 _('invalid bundle specification: '
145 'missing "=" in parameter: %s') % p)
147 'missing "=" in parameter: %s') % p)
146
148
147 key, value = p.split('=', 1)
149 key, value = p.split('=', 1)
148 key = urlreq.unquote(key)
150 key = urlreq.unquote(key)
149 value = urlreq.unquote(value)
151 value = urlreq.unquote(value)
150 params[key] = value
152 params[key] = value
151
153
152 return version, params
154 return version, params
153
155
154
156
155 if strict and '-' not in spec:
157 if strict and '-' not in spec:
156 raise error.InvalidBundleSpecification(
158 raise error.InvalidBundleSpecification(
157 _('invalid bundle specification; '
159 _('invalid bundle specification; '
158 'must be prefixed with compression: %s') % spec)
160 'must be prefixed with compression: %s') % spec)
159
161
160 if '-' in spec:
162 if '-' in spec:
161 compression, version = spec.split('-', 1)
163 compression, version = spec.split('-', 1)
162
164
163 if compression not in util.compengines.supportedbundlenames:
165 if compression not in util.compengines.supportedbundlenames:
164 raise error.UnsupportedBundleSpecification(
166 raise error.UnsupportedBundleSpecification(
165 _('%s compression is not supported') % compression)
167 _('%s compression is not supported') % compression)
166
168
167 version, params = parseparams(version)
169 version, params = parseparams(version)
168
170
169 if version not in _bundlespeccgversions:
171 if version not in _bundlespeccgversions:
170 raise error.UnsupportedBundleSpecification(
172 raise error.UnsupportedBundleSpecification(
171 _('%s is not a recognized bundle version') % version)
173 _('%s is not a recognized bundle version') % version)
172 else:
174 else:
173 # Value could be just the compression or just the version, in which
175 # Value could be just the compression or just the version, in which
174 # case some defaults are assumed (but only when not in strict mode).
176 # case some defaults are assumed (but only when not in strict mode).
175 assert not strict
177 assert not strict
176
178
177 spec, params = parseparams(spec)
179 spec, params = parseparams(spec)
178
180
179 if spec in util.compengines.supportedbundlenames:
181 if spec in util.compengines.supportedbundlenames:
180 compression = spec
182 compression = spec
181 version = 'v1'
183 version = 'v1'
182 # Generaldelta repos require v2.
184 # Generaldelta repos require v2.
183 if 'generaldelta' in repo.requirements:
185 if 'generaldelta' in repo.requirements:
184 version = 'v2'
186 version = 'v2'
185 # Modern compression engines require v2.
187 # Modern compression engines require v2.
186 if compression not in _bundlespecv1compengines:
188 if compression not in _bundlespecv1compengines:
187 version = 'v2'
189 version = 'v2'
188 elif spec in _bundlespeccgversions:
190 elif spec in _bundlespeccgversions:
189 if spec == 'packed1':
191 if spec == 'packed1':
190 compression = 'none'
192 compression = 'none'
191 else:
193 else:
192 compression = 'bzip2'
194 compression = 'bzip2'
193 version = spec
195 version = spec
194 else:
196 else:
195 raise error.UnsupportedBundleSpecification(
197 raise error.UnsupportedBundleSpecification(
196 _('%s is not a recognized bundle specification') % spec)
198 _('%s is not a recognized bundle specification') % spec)
197
199
198 # Bundle version 1 only supports a known set of compression engines.
200 # Bundle version 1 only supports a known set of compression engines.
199 if version == 'v1' and compression not in _bundlespecv1compengines:
201 if version == 'v1' and compression not in _bundlespecv1compengines:
200 raise error.UnsupportedBundleSpecification(
202 raise error.UnsupportedBundleSpecification(
201 _('compression engine %s is not supported on v1 bundles') %
203 _('compression engine %s is not supported on v1 bundles') %
202 compression)
204 compression)
203
205
204 # The specification for packed1 can optionally declare the data formats
206 # The specification for packed1 can optionally declare the data formats
205 # required to apply it. If we see this metadata, compare against what the
207 # required to apply it. If we see this metadata, compare against what the
206 # repo supports and error if the bundle isn't compatible.
208 # repo supports and error if the bundle isn't compatible.
207 if version == 'packed1' and 'requirements' in params:
209 if version == 'packed1' and 'requirements' in params:
208 requirements = set(params['requirements'].split(','))
210 requirements = set(params['requirements'].split(','))
209 missingreqs = requirements - repo.supportedformats
211 missingreqs = requirements - repo.supportedformats
210 if missingreqs:
212 if missingreqs:
211 raise error.UnsupportedBundleSpecification(
213 raise error.UnsupportedBundleSpecification(
212 _('missing support for repository features: %s') %
214 _('missing support for repository features: %s') %
213 ', '.join(sorted(missingreqs)))
215 ', '.join(sorted(missingreqs)))
214
216
215 # Compute contentopts based on the version
217 # Compute contentopts based on the version
216 contentopts = _bundlespeccontentopts.get(version, {}).copy()
218 contentopts = _bundlespeccontentopts.get(version, {}).copy()
217
219
218 # Process the variants
220 # Process the variants
219 if "stream" in params and params["stream"] == "v2":
221 if "stream" in params and params["stream"] == "v2":
220 variant = _bundlespecvariants["streamv2"]
222 variant = _bundlespecvariants["streamv2"]
221 contentopts.update(variant)
223 contentopts.update(variant)
222
224
223 engine = util.compengines.forbundlename(compression)
225 engine = util.compengines.forbundlename(compression)
224 compression, wirecompression = engine.bundletype()
226 compression, wirecompression = engine.bundletype()
225 wireversion = _bundlespeccgversions[version]
227 wireversion = _bundlespeccgversions[version]
226
228
227 return bundlespec(compression, wirecompression, version, wireversion,
229 return bundlespec(compression, wirecompression, version, wireversion,
228 params, contentopts)
230 params, contentopts)
229
231
230 def readbundle(ui, fh, fname, vfs=None):
232 def readbundle(ui, fh, fname, vfs=None):
231 header = changegroup.readexactly(fh, 4)
233 header = changegroup.readexactly(fh, 4)
232
234
233 alg = None
235 alg = None
234 if not fname:
236 if not fname:
235 fname = "stream"
237 fname = "stream"
236 if not header.startswith('HG') and header.startswith('\0'):
238 if not header.startswith('HG') and header.startswith('\0'):
237 fh = changegroup.headerlessfixup(fh, header)
239 fh = changegroup.headerlessfixup(fh, header)
238 header = "HG10"
240 header = "HG10"
239 alg = 'UN'
241 alg = 'UN'
240 elif vfs:
242 elif vfs:
241 fname = vfs.join(fname)
243 fname = vfs.join(fname)
242
244
243 magic, version = header[0:2], header[2:4]
245 magic, version = header[0:2], header[2:4]
244
246
245 if magic != 'HG':
247 if magic != 'HG':
246 raise error.Abort(_('%s: not a Mercurial bundle') % fname)
248 raise error.Abort(_('%s: not a Mercurial bundle') % fname)
247 if version == '10':
249 if version == '10':
248 if alg is None:
250 if alg is None:
249 alg = changegroup.readexactly(fh, 2)
251 alg = changegroup.readexactly(fh, 2)
250 return changegroup.cg1unpacker(fh, alg)
252 return changegroup.cg1unpacker(fh, alg)
251 elif version.startswith('2'):
253 elif version.startswith('2'):
252 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
254 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
253 elif version == 'S1':
255 elif version == 'S1':
254 return streamclone.streamcloneapplier(fh)
256 return streamclone.streamcloneapplier(fh)
255 else:
257 else:
256 raise error.Abort(_('%s: unknown bundle version %s') % (fname, version))
258 raise error.Abort(_('%s: unknown bundle version %s') % (fname, version))
257
259
258 def getbundlespec(ui, fh):
260 def getbundlespec(ui, fh):
259 """Infer the bundlespec from a bundle file handle.
261 """Infer the bundlespec from a bundle file handle.
260
262
261 The input file handle is seeked and the original seek position is not
263 The input file handle is seeked and the original seek position is not
262 restored.
264 restored.
263 """
265 """
264 def speccompression(alg):
266 def speccompression(alg):
265 try:
267 try:
266 return util.compengines.forbundletype(alg).bundletype()[0]
268 return util.compengines.forbundletype(alg).bundletype()[0]
267 except KeyError:
269 except KeyError:
268 return None
270 return None
269
271
270 b = readbundle(ui, fh, None)
272 b = readbundle(ui, fh, None)
271 if isinstance(b, changegroup.cg1unpacker):
273 if isinstance(b, changegroup.cg1unpacker):
272 alg = b._type
274 alg = b._type
273 if alg == '_truncatedBZ':
275 if alg == '_truncatedBZ':
274 alg = 'BZ'
276 alg = 'BZ'
275 comp = speccompression(alg)
277 comp = speccompression(alg)
276 if not comp:
278 if not comp:
277 raise error.Abort(_('unknown compression algorithm: %s') % alg)
279 raise error.Abort(_('unknown compression algorithm: %s') % alg)
278 return '%s-v1' % comp
280 return '%s-v1' % comp
279 elif isinstance(b, bundle2.unbundle20):
281 elif isinstance(b, bundle2.unbundle20):
280 if 'Compression' in b.params:
282 if 'Compression' in b.params:
281 comp = speccompression(b.params['Compression'])
283 comp = speccompression(b.params['Compression'])
282 if not comp:
284 if not comp:
283 raise error.Abort(_('unknown compression algorithm: %s') % comp)
285 raise error.Abort(_('unknown compression algorithm: %s') % comp)
284 else:
286 else:
285 comp = 'none'
287 comp = 'none'
286
288
287 version = None
289 version = None
288 for part in b.iterparts():
290 for part in b.iterparts():
289 if part.type == 'changegroup':
291 if part.type == 'changegroup':
290 version = part.params['version']
292 version = part.params['version']
291 if version in ('01', '02'):
293 if version in ('01', '02'):
292 version = 'v2'
294 version = 'v2'
293 else:
295 else:
294 raise error.Abort(_('changegroup version %s does not have '
296 raise error.Abort(_('changegroup version %s does not have '
295 'a known bundlespec') % version,
297 'a known bundlespec') % version,
296 hint=_('try upgrading your Mercurial '
298 hint=_('try upgrading your Mercurial '
297 'client'))
299 'client'))
298 elif part.type == 'stream2' and version is None:
300 elif part.type == 'stream2' and version is None:
299 # A stream2 part requires to be part of a v2 bundle
301 # A stream2 part requires to be part of a v2 bundle
300 requirements = urlreq.unquote(part.params['requirements'])
302 requirements = urlreq.unquote(part.params['requirements'])
301 splitted = requirements.split()
303 splitted = requirements.split()
302 params = bundle2._formatrequirementsparams(splitted)
304 params = bundle2._formatrequirementsparams(splitted)
303 return 'none-v2;stream=v2;%s' % params
305 return 'none-v2;stream=v2;%s' % params
304
306
305 if not version:
307 if not version:
306 raise error.Abort(_('could not identify changegroup version in '
308 raise error.Abort(_('could not identify changegroup version in '
307 'bundle'))
309 'bundle'))
308
310
309 return '%s-%s' % (comp, version)
311 return '%s-%s' % (comp, version)
310 elif isinstance(b, streamclone.streamcloneapplier):
312 elif isinstance(b, streamclone.streamcloneapplier):
311 requirements = streamclone.readbundle1header(fh)[2]
313 requirements = streamclone.readbundle1header(fh)[2]
312 formatted = bundle2._formatrequirementsparams(requirements)
314 formatted = bundle2._formatrequirementsparams(requirements)
313 return 'none-packed1;%s' % formatted
315 return 'none-packed1;%s' % formatted
314 else:
316 else:
315 raise error.Abort(_('unknown bundle type: %s') % b)
317 raise error.Abort(_('unknown bundle type: %s') % b)
316
318
317 def _computeoutgoing(repo, heads, common):
319 def _computeoutgoing(repo, heads, common):
318 """Computes which revs are outgoing given a set of common
320 """Computes which revs are outgoing given a set of common
319 and a set of heads.
321 and a set of heads.
320
322
321 This is a separate function so extensions can have access to
323 This is a separate function so extensions can have access to
322 the logic.
324 the logic.
323
325
324 Returns a discovery.outgoing object.
326 Returns a discovery.outgoing object.
325 """
327 """
326 cl = repo.changelog
328 cl = repo.changelog
327 if common:
329 if common:
328 hasnode = cl.hasnode
330 hasnode = cl.hasnode
329 common = [n for n in common if hasnode(n)]
331 common = [n for n in common if hasnode(n)]
330 else:
332 else:
331 common = [nullid]
333 common = [nullid]
332 if not heads:
334 if not heads:
333 heads = cl.heads()
335 heads = cl.heads()
334 return discovery.outgoing(repo, common, heads)
336 return discovery.outgoing(repo, common, heads)
335
337
336 def _checkpublish(pushop):
338 def _checkpublish(pushop):
337 repo = pushop.repo
339 repo = pushop.repo
338 ui = repo.ui
340 ui = repo.ui
339 behavior = ui.config('experimental', 'auto-publish')
341 behavior = ui.config('experimental', 'auto-publish')
340 if pushop.publish or behavior not in ('warn', 'confirm', 'abort'):
342 if pushop.publish or behavior not in ('warn', 'confirm', 'abort'):
341 return
343 return
342 remotephases = listkeys(pushop.remote, 'phases')
344 remotephases = listkeys(pushop.remote, 'phases')
343 if not remotephases.get('publishing', False):
345 if not remotephases.get('publishing', False):
344 return
346 return
345
347
346 if pushop.revs is None:
348 if pushop.revs is None:
347 published = repo.filtered('served').revs('not public()')
349 published = repo.filtered('served').revs('not public()')
348 else:
350 else:
349 published = repo.revs('::%ln - public()', pushop.revs)
351 published = repo.revs('::%ln - public()', pushop.revs)
350 if published:
352 if published:
351 if behavior == 'warn':
353 if behavior == 'warn':
352 ui.warn(_('%i changesets about to be published\n')
354 ui.warn(_('%i changesets about to be published\n')
353 % len(published))
355 % len(published))
354 elif behavior == 'confirm':
356 elif behavior == 'confirm':
355 if ui.promptchoice(_('push and publish %i changesets (yn)?'
357 if ui.promptchoice(_('push and publish %i changesets (yn)?'
356 '$$ &Yes $$ &No') % len(published)):
358 '$$ &Yes $$ &No') % len(published)):
357 raise error.Abort(_('user quit'))
359 raise error.Abort(_('user quit'))
358 elif behavior == 'abort':
360 elif behavior == 'abort':
359 msg = _('push would publish %i changesets') % len(published)
361 msg = _('push would publish %i changesets') % len(published)
360 hint = _("use --publish or adjust 'experimental.auto-publish'"
362 hint = _("use --publish or adjust 'experimental.auto-publish'"
361 " config")
363 " config")
362 raise error.Abort(msg, hint=hint)
364 raise error.Abort(msg, hint=hint)
363
365
364 def _forcebundle1(op):
366 def _forcebundle1(op):
365 """return true if a pull/push must use bundle1
367 """return true if a pull/push must use bundle1
366
368
367 This function is used to allow testing of the older bundle version"""
369 This function is used to allow testing of the older bundle version"""
368 ui = op.repo.ui
370 ui = op.repo.ui
369 # The goal is this config is to allow developer to choose the bundle
371 # The goal is this config is to allow developer to choose the bundle
370 # version used during exchanged. This is especially handy during test.
372 # version used during exchanged. This is especially handy during test.
371 # Value is a list of bundle version to be picked from, highest version
373 # Value is a list of bundle version to be picked from, highest version
372 # should be used.
374 # should be used.
373 #
375 #
374 # developer config: devel.legacy.exchange
376 # developer config: devel.legacy.exchange
375 exchange = ui.configlist('devel', 'legacy.exchange')
377 exchange = ui.configlist('devel', 'legacy.exchange')
376 forcebundle1 = 'bundle2' not in exchange and 'bundle1' in exchange
378 forcebundle1 = 'bundle2' not in exchange and 'bundle1' in exchange
377 return forcebundle1 or not op.remote.capable('bundle2')
379 return forcebundle1 or not op.remote.capable('bundle2')
378
380
379 class pushoperation(object):
381 class pushoperation(object):
380 """A object that represent a single push operation
382 """A object that represent a single push operation
381
383
382 Its purpose is to carry push related state and very common operations.
384 Its purpose is to carry push related state and very common operations.
383
385
384 A new pushoperation should be created at the beginning of each push and
386 A new pushoperation should be created at the beginning of each push and
385 discarded afterward.
387 discarded afterward.
386 """
388 """
387
389
388 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
390 def __init__(self, repo, remote, force=False, revs=None, newbranch=False,
389 bookmarks=(), publish=False, pushvars=None):
391 bookmarks=(), publish=False, pushvars=None):
390 # repo we push from
392 # repo we push from
391 self.repo = repo
393 self.repo = repo
392 self.ui = repo.ui
394 self.ui = repo.ui
393 # repo we push to
395 # repo we push to
394 self.remote = remote
396 self.remote = remote
395 # force option provided
397 # force option provided
396 self.force = force
398 self.force = force
397 # revs to be pushed (None is "all")
399 # revs to be pushed (None is "all")
398 self.revs = revs
400 self.revs = revs
399 # bookmark explicitly pushed
401 # bookmark explicitly pushed
400 self.bookmarks = bookmarks
402 self.bookmarks = bookmarks
401 # allow push of new branch
403 # allow push of new branch
402 self.newbranch = newbranch
404 self.newbranch = newbranch
403 # step already performed
405 # step already performed
404 # (used to check what steps have been already performed through bundle2)
406 # (used to check what steps have been already performed through bundle2)
405 self.stepsdone = set()
407 self.stepsdone = set()
406 # Integer version of the changegroup push result
408 # Integer version of the changegroup push result
407 # - None means nothing to push
409 # - None means nothing to push
408 # - 0 means HTTP error
410 # - 0 means HTTP error
409 # - 1 means we pushed and remote head count is unchanged *or*
411 # - 1 means we pushed and remote head count is unchanged *or*
410 # we have outgoing changesets but refused to push
412 # we have outgoing changesets but refused to push
411 # - other values as described by addchangegroup()
413 # - other values as described by addchangegroup()
412 self.cgresult = None
414 self.cgresult = None
413 # Boolean value for the bookmark push
415 # Boolean value for the bookmark push
414 self.bkresult = None
416 self.bkresult = None
415 # discover.outgoing object (contains common and outgoing data)
417 # discover.outgoing object (contains common and outgoing data)
416 self.outgoing = None
418 self.outgoing = None
417 # all remote topological heads before the push
419 # all remote topological heads before the push
418 self.remoteheads = None
420 self.remoteheads = None
419 # Details of the remote branch pre and post push
421 # Details of the remote branch pre and post push
420 #
422 #
421 # mapping: {'branch': ([remoteheads],
423 # mapping: {'branch': ([remoteheads],
422 # [newheads],
424 # [newheads],
423 # [unsyncedheads],
425 # [unsyncedheads],
424 # [discardedheads])}
426 # [discardedheads])}
425 # - branch: the branch name
427 # - branch: the branch name
426 # - remoteheads: the list of remote heads known locally
428 # - remoteheads: the list of remote heads known locally
427 # None if the branch is new
429 # None if the branch is new
428 # - newheads: the new remote heads (known locally) with outgoing pushed
430 # - newheads: the new remote heads (known locally) with outgoing pushed
429 # - unsyncedheads: the list of remote heads unknown locally.
431 # - unsyncedheads: the list of remote heads unknown locally.
430 # - discardedheads: the list of remote heads made obsolete by the push
432 # - discardedheads: the list of remote heads made obsolete by the push
431 self.pushbranchmap = None
433 self.pushbranchmap = None
432 # testable as a boolean indicating if any nodes are missing locally.
434 # testable as a boolean indicating if any nodes are missing locally.
433 self.incoming = None
435 self.incoming = None
434 # summary of the remote phase situation
436 # summary of the remote phase situation
435 self.remotephases = None
437 self.remotephases = None
436 # phases changes that must be pushed along side the changesets
438 # phases changes that must be pushed along side the changesets
437 self.outdatedphases = None
439 self.outdatedphases = None
438 # phases changes that must be pushed if changeset push fails
440 # phases changes that must be pushed if changeset push fails
439 self.fallbackoutdatedphases = None
441 self.fallbackoutdatedphases = None
440 # outgoing obsmarkers
442 # outgoing obsmarkers
441 self.outobsmarkers = set()
443 self.outobsmarkers = set()
442 # outgoing bookmarks
444 # outgoing bookmarks
443 self.outbookmarks = []
445 self.outbookmarks = []
444 # transaction manager
446 # transaction manager
445 self.trmanager = None
447 self.trmanager = None
446 # map { pushkey partid -> callback handling failure}
448 # map { pushkey partid -> callback handling failure}
447 # used to handle exception from mandatory pushkey part failure
449 # used to handle exception from mandatory pushkey part failure
448 self.pkfailcb = {}
450 self.pkfailcb = {}
449 # an iterable of pushvars or None
451 # an iterable of pushvars or None
450 self.pushvars = pushvars
452 self.pushvars = pushvars
451 # publish pushed changesets
453 # publish pushed changesets
452 self.publish = publish
454 self.publish = publish
453
455
454 @util.propertycache
456 @util.propertycache
455 def futureheads(self):
457 def futureheads(self):
456 """future remote heads if the changeset push succeeds"""
458 """future remote heads if the changeset push succeeds"""
457 return self.outgoing.missingheads
459 return self.outgoing.missingheads
458
460
459 @util.propertycache
461 @util.propertycache
460 def fallbackheads(self):
462 def fallbackheads(self):
461 """future remote heads if the changeset push fails"""
463 """future remote heads if the changeset push fails"""
462 if self.revs is None:
464 if self.revs is None:
463 # not target to push, all common are relevant
465 # not target to push, all common are relevant
464 return self.outgoing.commonheads
466 return self.outgoing.commonheads
465 unfi = self.repo.unfiltered()
467 unfi = self.repo.unfiltered()
466 # I want cheads = heads(::missingheads and ::commonheads)
468 # I want cheads = heads(::missingheads and ::commonheads)
467 # (missingheads is revs with secret changeset filtered out)
469 # (missingheads is revs with secret changeset filtered out)
468 #
470 #
469 # This can be expressed as:
471 # This can be expressed as:
470 # cheads = ( (missingheads and ::commonheads)
472 # cheads = ( (missingheads and ::commonheads)
471 # + (commonheads and ::missingheads))"
473 # + (commonheads and ::missingheads))"
472 # )
474 # )
473 #
475 #
474 # while trying to push we already computed the following:
476 # while trying to push we already computed the following:
475 # common = (::commonheads)
477 # common = (::commonheads)
476 # missing = ((commonheads::missingheads) - commonheads)
478 # missing = ((commonheads::missingheads) - commonheads)
477 #
479 #
478 # We can pick:
480 # We can pick:
479 # * missingheads part of common (::commonheads)
481 # * missingheads part of common (::commonheads)
480 common = self.outgoing.common
482 common = self.outgoing.common
481 nm = self.repo.changelog.nodemap
483 nm = self.repo.changelog.nodemap
482 cheads = [node for node in self.revs if nm[node] in common]
484 cheads = [node for node in self.revs if nm[node] in common]
483 # and
485 # and
484 # * commonheads parents on missing
486 # * commonheads parents on missing
485 revset = unfi.set('%ln and parents(roots(%ln))',
487 revset = unfi.set('%ln and parents(roots(%ln))',
486 self.outgoing.commonheads,
488 self.outgoing.commonheads,
487 self.outgoing.missing)
489 self.outgoing.missing)
488 cheads.extend(c.node() for c in revset)
490 cheads.extend(c.node() for c in revset)
489 return cheads
491 return cheads
490
492
491 @property
493 @property
492 def commonheads(self):
494 def commonheads(self):
493 """set of all common heads after changeset bundle push"""
495 """set of all common heads after changeset bundle push"""
494 if self.cgresult:
496 if self.cgresult:
495 return self.futureheads
497 return self.futureheads
496 else:
498 else:
497 return self.fallbackheads
499 return self.fallbackheads
498
500
499 # mapping of message used when pushing bookmark
501 # mapping of message used when pushing bookmark
500 bookmsgmap = {'update': (_("updating bookmark %s\n"),
502 bookmsgmap = {'update': (_("updating bookmark %s\n"),
501 _('updating bookmark %s failed!\n')),
503 _('updating bookmark %s failed!\n')),
502 'export': (_("exporting bookmark %s\n"),
504 'export': (_("exporting bookmark %s\n"),
503 _('exporting bookmark %s failed!\n')),
505 _('exporting bookmark %s failed!\n')),
504 'delete': (_("deleting remote bookmark %s\n"),
506 'delete': (_("deleting remote bookmark %s\n"),
505 _('deleting remote bookmark %s failed!\n')),
507 _('deleting remote bookmark %s failed!\n')),
506 }
508 }
507
509
508
510
509 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
511 def push(repo, remote, force=False, revs=None, newbranch=False, bookmarks=(),
510 publish=False, opargs=None):
512 publish=False, opargs=None):
511 '''Push outgoing changesets (limited by revs) from a local
513 '''Push outgoing changesets (limited by revs) from a local
512 repository to remote. Return an integer:
514 repository to remote. Return an integer:
513 - None means nothing to push
515 - None means nothing to push
514 - 0 means HTTP error
516 - 0 means HTTP error
515 - 1 means we pushed and remote head count is unchanged *or*
517 - 1 means we pushed and remote head count is unchanged *or*
516 we have outgoing changesets but refused to push
518 we have outgoing changesets but refused to push
517 - other values as described by addchangegroup()
519 - other values as described by addchangegroup()
518 '''
520 '''
519 if opargs is None:
521 if opargs is None:
520 opargs = {}
522 opargs = {}
521 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
523 pushop = pushoperation(repo, remote, force, revs, newbranch, bookmarks,
522 publish, **pycompat.strkwargs(opargs))
524 publish, **pycompat.strkwargs(opargs))
523 if pushop.remote.local():
525 if pushop.remote.local():
524 missing = (set(pushop.repo.requirements)
526 missing = (set(pushop.repo.requirements)
525 - pushop.remote.local().supported)
527 - pushop.remote.local().supported)
526 if missing:
528 if missing:
527 msg = _("required features are not"
529 msg = _("required features are not"
528 " supported in the destination:"
530 " supported in the destination:"
529 " %s") % (', '.join(sorted(missing)))
531 " %s") % (', '.join(sorted(missing)))
530 raise error.Abort(msg)
532 raise error.Abort(msg)
531
533
532 if not pushop.remote.canpush():
534 if not pushop.remote.canpush():
533 raise error.Abort(_("destination does not support push"))
535 raise error.Abort(_("destination does not support push"))
534
536
535 if not pushop.remote.capable('unbundle'):
537 if not pushop.remote.capable('unbundle'):
536 raise error.Abort(_('cannot push: destination does not support the '
538 raise error.Abort(_('cannot push: destination does not support the '
537 'unbundle wire protocol command'))
539 'unbundle wire protocol command'))
538
540
539 # get lock as we might write phase data
541 # get lock as we might write phase data
540 wlock = lock = None
542 wlock = lock = None
541 try:
543 try:
542 # bundle2 push may receive a reply bundle touching bookmarks
544 # bundle2 push may receive a reply bundle touching bookmarks
543 # requiring the wlock. Take it now to ensure proper ordering.
545 # requiring the wlock. Take it now to ensure proper ordering.
544 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
546 maypushback = pushop.ui.configbool('experimental', 'bundle2.pushback')
545 if ((not _forcebundle1(pushop)) and
547 if ((not _forcebundle1(pushop)) and
546 maypushback and
548 maypushback and
547 not bookmod.bookmarksinstore(repo)):
549 not bookmod.bookmarksinstore(repo)):
548 wlock = pushop.repo.wlock()
550 wlock = pushop.repo.wlock()
549 lock = pushop.repo.lock()
551 lock = pushop.repo.lock()
550 pushop.trmanager = transactionmanager(pushop.repo,
552 pushop.trmanager = transactionmanager(pushop.repo,
551 'push-response',
553 'push-response',
552 pushop.remote.url())
554 pushop.remote.url())
553 except error.LockUnavailable as err:
555 except error.LockUnavailable as err:
554 # source repo cannot be locked.
556 # source repo cannot be locked.
555 # We do not abort the push, but just disable the local phase
557 # We do not abort the push, but just disable the local phase
556 # synchronisation.
558 # synchronisation.
557 msg = ('cannot lock source repository: %s\n'
559 msg = ('cannot lock source repository: %s\n'
558 % stringutil.forcebytestr(err))
560 % stringutil.forcebytestr(err))
559 pushop.ui.debug(msg)
561 pushop.ui.debug(msg)
560
562
561 with wlock or util.nullcontextmanager():
563 with wlock or util.nullcontextmanager():
562 with lock or util.nullcontextmanager():
564 with lock or util.nullcontextmanager():
563 with pushop.trmanager or util.nullcontextmanager():
565 with pushop.trmanager or util.nullcontextmanager():
564 pushop.repo.checkpush(pushop)
566 pushop.repo.checkpush(pushop)
565 _checkpublish(pushop)
567 _checkpublish(pushop)
566 _pushdiscovery(pushop)
568 _pushdiscovery(pushop)
567 if not _forcebundle1(pushop):
569 if not _forcebundle1(pushop):
568 _pushbundle2(pushop)
570 _pushbundle2(pushop)
569 _pushchangeset(pushop)
571 _pushchangeset(pushop)
570 _pushsyncphase(pushop)
572 _pushsyncphase(pushop)
571 _pushobsolete(pushop)
573 _pushobsolete(pushop)
572 _pushbookmark(pushop)
574 _pushbookmark(pushop)
573
575
574 if repo.ui.configbool('experimental', 'remotenames'):
576 if repo.ui.configbool('experimental', 'remotenames'):
575 logexchange.pullremotenames(repo, remote)
577 logexchange.pullremotenames(repo, remote)
576
578
577 return pushop
579 return pushop
578
580
579 # list of steps to perform discovery before push
581 # list of steps to perform discovery before push
580 pushdiscoveryorder = []
582 pushdiscoveryorder = []
581
583
582 # Mapping between step name and function
584 # Mapping between step name and function
583 #
585 #
584 # This exists to help extensions wrap steps if necessary
586 # This exists to help extensions wrap steps if necessary
585 pushdiscoverymapping = {}
587 pushdiscoverymapping = {}
586
588
587 def pushdiscovery(stepname):
589 def pushdiscovery(stepname):
588 """decorator for function performing discovery before push
590 """decorator for function performing discovery before push
589
591
590 The function is added to the step -> function mapping and appended to the
592 The function is added to the step -> function mapping and appended to the
591 list of steps. Beware that decorated function will be added in order (this
593 list of steps. Beware that decorated function will be added in order (this
592 may matter).
594 may matter).
593
595
594 You can only use this decorator for a new step, if you want to wrap a step
596 You can only use this decorator for a new step, if you want to wrap a step
595 from an extension, change the pushdiscovery dictionary directly."""
597 from an extension, change the pushdiscovery dictionary directly."""
596 def dec(func):
598 def dec(func):
597 assert stepname not in pushdiscoverymapping
599 assert stepname not in pushdiscoverymapping
598 pushdiscoverymapping[stepname] = func
600 pushdiscoverymapping[stepname] = func
599 pushdiscoveryorder.append(stepname)
601 pushdiscoveryorder.append(stepname)
600 return func
602 return func
601 return dec
603 return dec
602
604
603 def _pushdiscovery(pushop):
605 def _pushdiscovery(pushop):
604 """Run all discovery steps"""
606 """Run all discovery steps"""
605 for stepname in pushdiscoveryorder:
607 for stepname in pushdiscoveryorder:
606 step = pushdiscoverymapping[stepname]
608 step = pushdiscoverymapping[stepname]
607 step(pushop)
609 step(pushop)
608
610
609 @pushdiscovery('changeset')
611 @pushdiscovery('changeset')
610 def _pushdiscoverychangeset(pushop):
612 def _pushdiscoverychangeset(pushop):
611 """discover the changeset that need to be pushed"""
613 """discover the changeset that need to be pushed"""
612 fci = discovery.findcommonincoming
614 fci = discovery.findcommonincoming
613 if pushop.revs:
615 if pushop.revs:
614 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force,
616 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force,
615 ancestorsof=pushop.revs)
617 ancestorsof=pushop.revs)
616 else:
618 else:
617 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
619 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
618 common, inc, remoteheads = commoninc
620 common, inc, remoteheads = commoninc
619 fco = discovery.findcommonoutgoing
621 fco = discovery.findcommonoutgoing
620 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
622 outgoing = fco(pushop.repo, pushop.remote, onlyheads=pushop.revs,
621 commoninc=commoninc, force=pushop.force)
623 commoninc=commoninc, force=pushop.force)
622 pushop.outgoing = outgoing
624 pushop.outgoing = outgoing
623 pushop.remoteheads = remoteheads
625 pushop.remoteheads = remoteheads
624 pushop.incoming = inc
626 pushop.incoming = inc
625
627
626 @pushdiscovery('phase')
628 @pushdiscovery('phase')
627 def _pushdiscoveryphase(pushop):
629 def _pushdiscoveryphase(pushop):
628 """discover the phase that needs to be pushed
630 """discover the phase that needs to be pushed
629
631
630 (computed for both success and failure case for changesets push)"""
632 (computed for both success and failure case for changesets push)"""
631 outgoing = pushop.outgoing
633 outgoing = pushop.outgoing
632 unfi = pushop.repo.unfiltered()
634 unfi = pushop.repo.unfiltered()
633 remotephases = listkeys(pushop.remote, 'phases')
635 remotephases = listkeys(pushop.remote, 'phases')
634
636
635 if (pushop.ui.configbool('ui', '_usedassubrepo')
637 if (pushop.ui.configbool('ui', '_usedassubrepo')
636 and remotephases # server supports phases
638 and remotephases # server supports phases
637 and not pushop.outgoing.missing # no changesets to be pushed
639 and not pushop.outgoing.missing # no changesets to be pushed
638 and remotephases.get('publishing', False)):
640 and remotephases.get('publishing', False)):
639 # When:
641 # When:
640 # - this is a subrepo push
642 # - this is a subrepo push
641 # - and remote support phase
643 # - and remote support phase
642 # - and no changeset are to be pushed
644 # - and no changeset are to be pushed
643 # - and remote is publishing
645 # - and remote is publishing
644 # We may be in issue 3781 case!
646 # We may be in issue 3781 case!
645 # We drop the possible phase synchronisation done by
647 # We drop the possible phase synchronisation done by
646 # courtesy to publish changesets possibly locally draft
648 # courtesy to publish changesets possibly locally draft
647 # on the remote.
649 # on the remote.
648 pushop.outdatedphases = []
650 pushop.outdatedphases = []
649 pushop.fallbackoutdatedphases = []
651 pushop.fallbackoutdatedphases = []
650 return
652 return
651
653
652 pushop.remotephases = phases.remotephasessummary(pushop.repo,
654 pushop.remotephases = phases.remotephasessummary(pushop.repo,
653 pushop.fallbackheads,
655 pushop.fallbackheads,
654 remotephases)
656 remotephases)
655 droots = pushop.remotephases.draftroots
657 droots = pushop.remotephases.draftroots
656
658
657 extracond = ''
659 extracond = ''
658 if not pushop.remotephases.publishing:
660 if not pushop.remotephases.publishing:
659 extracond = ' and public()'
661 extracond = ' and public()'
660 revset = 'heads((%%ln::%%ln) %s)' % extracond
662 revset = 'heads((%%ln::%%ln) %s)' % extracond
661 # Get the list of all revs draft on remote by public here.
663 # Get the list of all revs draft on remote by public here.
662 # XXX Beware that revset break if droots is not strictly
664 # XXX Beware that revset break if droots is not strictly
663 # XXX root we may want to ensure it is but it is costly
665 # XXX root we may want to ensure it is but it is costly
664 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
666 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
665 if not pushop.remotephases.publishing and pushop.publish:
667 if not pushop.remotephases.publishing and pushop.publish:
666 future = list(unfi.set('%ln and (not public() or %ln::)',
668 future = list(unfi.set('%ln and (not public() or %ln::)',
667 pushop.futureheads, droots))
669 pushop.futureheads, droots))
668 elif not outgoing.missing:
670 elif not outgoing.missing:
669 future = fallback
671 future = fallback
670 else:
672 else:
671 # adds changeset we are going to push as draft
673 # adds changeset we are going to push as draft
672 #
674 #
673 # should not be necessary for publishing server, but because of an
675 # should not be necessary for publishing server, but because of an
674 # issue fixed in xxxxx we have to do it anyway.
676 # issue fixed in xxxxx we have to do it anyway.
675 fdroots = list(unfi.set('roots(%ln + %ln::)',
677 fdroots = list(unfi.set('roots(%ln + %ln::)',
676 outgoing.missing, droots))
678 outgoing.missing, droots))
677 fdroots = [f.node() for f in fdroots]
679 fdroots = [f.node() for f in fdroots]
678 future = list(unfi.set(revset, fdroots, pushop.futureheads))
680 future = list(unfi.set(revset, fdroots, pushop.futureheads))
679 pushop.outdatedphases = future
681 pushop.outdatedphases = future
680 pushop.fallbackoutdatedphases = fallback
682 pushop.fallbackoutdatedphases = fallback
681
683
682 @pushdiscovery('obsmarker')
684 @pushdiscovery('obsmarker')
683 def _pushdiscoveryobsmarkers(pushop):
685 def _pushdiscoveryobsmarkers(pushop):
684 if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt):
686 if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt):
685 return
687 return
686
688
687 if not pushop.repo.obsstore:
689 if not pushop.repo.obsstore:
688 return
690 return
689
691
690 if 'obsolete' not in listkeys(pushop.remote, 'namespaces'):
692 if 'obsolete' not in listkeys(pushop.remote, 'namespaces'):
691 return
693 return
692
694
693 repo = pushop.repo
695 repo = pushop.repo
694 # very naive computation, that can be quite expensive on big repo.
696 # very naive computation, that can be quite expensive on big repo.
695 # However: evolution is currently slow on them anyway.
697 # However: evolution is currently slow on them anyway.
696 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
698 nodes = (c.node() for c in repo.set('::%ln', pushop.futureheads))
697 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
699 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
698
700
699 @pushdiscovery('bookmarks')
701 @pushdiscovery('bookmarks')
700 def _pushdiscoverybookmarks(pushop):
702 def _pushdiscoverybookmarks(pushop):
701 ui = pushop.ui
703 ui = pushop.ui
702 repo = pushop.repo.unfiltered()
704 repo = pushop.repo.unfiltered()
703 remote = pushop.remote
705 remote = pushop.remote
704 ui.debug("checking for updated bookmarks\n")
706 ui.debug("checking for updated bookmarks\n")
705 ancestors = ()
707 ancestors = ()
706 if pushop.revs:
708 if pushop.revs:
707 revnums = pycompat.maplist(repo.changelog.rev, pushop.revs)
709 revnums = pycompat.maplist(repo.changelog.rev, pushop.revs)
708 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
710 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
709
711
710 remotebookmark = listkeys(remote, 'bookmarks')
712 remotebookmark = listkeys(remote, 'bookmarks')
711
713
712 explicit = {repo._bookmarks.expandname(bookmark)
714 explicit = {repo._bookmarks.expandname(bookmark)
713 for bookmark in pushop.bookmarks}
715 for bookmark in pushop.bookmarks}
714
716
715 remotebookmark = bookmod.unhexlifybookmarks(remotebookmark)
717 remotebookmark = bookmod.unhexlifybookmarks(remotebookmark)
716 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
718 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
717
719
718 def safehex(x):
720 def safehex(x):
719 if x is None:
721 if x is None:
720 return x
722 return x
721 return hex(x)
723 return hex(x)
722
724
723 def hexifycompbookmarks(bookmarks):
725 def hexifycompbookmarks(bookmarks):
724 return [(b, safehex(scid), safehex(dcid))
726 return [(b, safehex(scid), safehex(dcid))
725 for (b, scid, dcid) in bookmarks]
727 for (b, scid, dcid) in bookmarks]
726
728
727 comp = [hexifycompbookmarks(marks) for marks in comp]
729 comp = [hexifycompbookmarks(marks) for marks in comp]
728 return _processcompared(pushop, ancestors, explicit, remotebookmark, comp)
730 return _processcompared(pushop, ancestors, explicit, remotebookmark, comp)
729
731
730 def _processcompared(pushop, pushed, explicit, remotebms, comp):
732 def _processcompared(pushop, pushed, explicit, remotebms, comp):
731 """take decision on bookmark to pull from the remote bookmark
733 """take decision on bookmark to pull from the remote bookmark
732
734
733 Exist to help extensions who want to alter this behavior.
735 Exist to help extensions who want to alter this behavior.
734 """
736 """
735 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
737 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
736
738
737 repo = pushop.repo
739 repo = pushop.repo
738
740
739 for b, scid, dcid in advsrc:
741 for b, scid, dcid in advsrc:
740 if b in explicit:
742 if b in explicit:
741 explicit.remove(b)
743 explicit.remove(b)
742 if not pushed or repo[scid].rev() in pushed:
744 if not pushed or repo[scid].rev() in pushed:
743 pushop.outbookmarks.append((b, dcid, scid))
745 pushop.outbookmarks.append((b, dcid, scid))
744 # search added bookmark
746 # search added bookmark
745 for b, scid, dcid in addsrc:
747 for b, scid, dcid in addsrc:
746 if b in explicit:
748 if b in explicit:
747 explicit.remove(b)
749 explicit.remove(b)
748 pushop.outbookmarks.append((b, '', scid))
750 pushop.outbookmarks.append((b, '', scid))
749 # search for overwritten bookmark
751 # search for overwritten bookmark
750 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
752 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
751 if b in explicit:
753 if b in explicit:
752 explicit.remove(b)
754 explicit.remove(b)
753 pushop.outbookmarks.append((b, dcid, scid))
755 pushop.outbookmarks.append((b, dcid, scid))
754 # search for bookmark to delete
756 # search for bookmark to delete
755 for b, scid, dcid in adddst:
757 for b, scid, dcid in adddst:
756 if b in explicit:
758 if b in explicit:
757 explicit.remove(b)
759 explicit.remove(b)
758 # treat as "deleted locally"
760 # treat as "deleted locally"
759 pushop.outbookmarks.append((b, dcid, ''))
761 pushop.outbookmarks.append((b, dcid, ''))
760 # identical bookmarks shouldn't get reported
762 # identical bookmarks shouldn't get reported
761 for b, scid, dcid in same:
763 for b, scid, dcid in same:
762 if b in explicit:
764 if b in explicit:
763 explicit.remove(b)
765 explicit.remove(b)
764
766
765 if explicit:
767 if explicit:
766 explicit = sorted(explicit)
768 explicit = sorted(explicit)
767 # we should probably list all of them
769 # we should probably list all of them
768 pushop.ui.warn(_('bookmark %s does not exist on the local '
770 pushop.ui.warn(_('bookmark %s does not exist on the local '
769 'or remote repository!\n') % explicit[0])
771 'or remote repository!\n') % explicit[0])
770 pushop.bkresult = 2
772 pushop.bkresult = 2
771
773
772 pushop.outbookmarks.sort()
774 pushop.outbookmarks.sort()
773
775
774 def _pushcheckoutgoing(pushop):
776 def _pushcheckoutgoing(pushop):
775 outgoing = pushop.outgoing
777 outgoing = pushop.outgoing
776 unfi = pushop.repo.unfiltered()
778 unfi = pushop.repo.unfiltered()
777 if not outgoing.missing:
779 if not outgoing.missing:
778 # nothing to push
780 # nothing to push
779 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
781 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
780 return False
782 return False
781 # something to push
783 # something to push
782 if not pushop.force:
784 if not pushop.force:
783 # if repo.obsstore == False --> no obsolete
785 # if repo.obsstore == False --> no obsolete
784 # then, save the iteration
786 # then, save the iteration
785 if unfi.obsstore:
787 if unfi.obsstore:
786 # this message are here for 80 char limit reason
788 # this message are here for 80 char limit reason
787 mso = _("push includes obsolete changeset: %s!")
789 mso = _("push includes obsolete changeset: %s!")
788 mspd = _("push includes phase-divergent changeset: %s!")
790 mspd = _("push includes phase-divergent changeset: %s!")
789 mscd = _("push includes content-divergent changeset: %s!")
791 mscd = _("push includes content-divergent changeset: %s!")
790 mst = {"orphan": _("push includes orphan changeset: %s!"),
792 mst = {"orphan": _("push includes orphan changeset: %s!"),
791 "phase-divergent": mspd,
793 "phase-divergent": mspd,
792 "content-divergent": mscd}
794 "content-divergent": mscd}
793 # If we are to push if there is at least one
795 # If we are to push if there is at least one
794 # obsolete or unstable changeset in missing, at
796 # obsolete or unstable changeset in missing, at
795 # least one of the missinghead will be obsolete or
797 # least one of the missinghead will be obsolete or
796 # unstable. So checking heads only is ok
798 # unstable. So checking heads only is ok
797 for node in outgoing.missingheads:
799 for node in outgoing.missingheads:
798 ctx = unfi[node]
800 ctx = unfi[node]
799 if ctx.obsolete():
801 if ctx.obsolete():
800 raise error.Abort(mso % ctx)
802 raise error.Abort(mso % ctx)
801 elif ctx.isunstable():
803 elif ctx.isunstable():
802 # TODO print more than one instability in the abort
804 # TODO print more than one instability in the abort
803 # message
805 # message
804 raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
806 raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
805
807
806 discovery.checkheads(pushop)
808 discovery.checkheads(pushop)
807 return True
809 return True
808
810
809 # List of names of steps to perform for an outgoing bundle2, order matters.
811 # List of names of steps to perform for an outgoing bundle2, order matters.
810 b2partsgenorder = []
812 b2partsgenorder = []
811
813
812 # Mapping between step name and function
814 # Mapping between step name and function
813 #
815 #
814 # This exists to help extensions wrap steps if necessary
816 # This exists to help extensions wrap steps if necessary
815 b2partsgenmapping = {}
817 b2partsgenmapping = {}
816
818
817 def b2partsgenerator(stepname, idx=None):
819 def b2partsgenerator(stepname, idx=None):
818 """decorator for function generating bundle2 part
820 """decorator for function generating bundle2 part
819
821
820 The function is added to the step -> function mapping and appended to the
822 The function is added to the step -> function mapping and appended to the
821 list of steps. Beware that decorated functions will be added in order
823 list of steps. Beware that decorated functions will be added in order
822 (this may matter).
824 (this may matter).
823
825
824 You can only use this decorator for new steps, if you want to wrap a step
826 You can only use this decorator for new steps, if you want to wrap a step
825 from an extension, attack the b2partsgenmapping dictionary directly."""
827 from an extension, attack the b2partsgenmapping dictionary directly."""
826 def dec(func):
828 def dec(func):
827 assert stepname not in b2partsgenmapping
829 assert stepname not in b2partsgenmapping
828 b2partsgenmapping[stepname] = func
830 b2partsgenmapping[stepname] = func
829 if idx is None:
831 if idx is None:
830 b2partsgenorder.append(stepname)
832 b2partsgenorder.append(stepname)
831 else:
833 else:
832 b2partsgenorder.insert(idx, stepname)
834 b2partsgenorder.insert(idx, stepname)
833 return func
835 return func
834 return dec
836 return dec
835
837
836 def _pushb2ctxcheckheads(pushop, bundler):
838 def _pushb2ctxcheckheads(pushop, bundler):
837 """Generate race condition checking parts
839 """Generate race condition checking parts
838
840
839 Exists as an independent function to aid extensions
841 Exists as an independent function to aid extensions
840 """
842 """
841 # * 'force' do not check for push race,
843 # * 'force' do not check for push race,
842 # * if we don't push anything, there are nothing to check.
844 # * if we don't push anything, there are nothing to check.
843 if not pushop.force and pushop.outgoing.missingheads:
845 if not pushop.force and pushop.outgoing.missingheads:
844 allowunrelated = 'related' in bundler.capabilities.get('checkheads', ())
846 allowunrelated = 'related' in bundler.capabilities.get('checkheads', ())
845 emptyremote = pushop.pushbranchmap is None
847 emptyremote = pushop.pushbranchmap is None
846 if not allowunrelated or emptyremote:
848 if not allowunrelated or emptyremote:
847 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
849 bundler.newpart('check:heads', data=iter(pushop.remoteheads))
848 else:
850 else:
849 affected = set()
851 affected = set()
850 for branch, heads in pushop.pushbranchmap.iteritems():
852 for branch, heads in pushop.pushbranchmap.iteritems():
851 remoteheads, newheads, unsyncedheads, discardedheads = heads
853 remoteheads, newheads, unsyncedheads, discardedheads = heads
852 if remoteheads is not None:
854 if remoteheads is not None:
853 remote = set(remoteheads)
855 remote = set(remoteheads)
854 affected |= set(discardedheads) & remote
856 affected |= set(discardedheads) & remote
855 affected |= remote - set(newheads)
857 affected |= remote - set(newheads)
856 if affected:
858 if affected:
857 data = iter(sorted(affected))
859 data = iter(sorted(affected))
858 bundler.newpart('check:updated-heads', data=data)
860 bundler.newpart('check:updated-heads', data=data)
859
861
860 def _pushing(pushop):
862 def _pushing(pushop):
861 """return True if we are pushing anything"""
863 """return True if we are pushing anything"""
862 return bool(pushop.outgoing.missing
864 return bool(pushop.outgoing.missing
863 or pushop.outdatedphases
865 or pushop.outdatedphases
864 or pushop.outobsmarkers
866 or pushop.outobsmarkers
865 or pushop.outbookmarks)
867 or pushop.outbookmarks)
866
868
867 @b2partsgenerator('check-bookmarks')
869 @b2partsgenerator('check-bookmarks')
868 def _pushb2checkbookmarks(pushop, bundler):
870 def _pushb2checkbookmarks(pushop, bundler):
869 """insert bookmark move checking"""
871 """insert bookmark move checking"""
870 if not _pushing(pushop) or pushop.force:
872 if not _pushing(pushop) or pushop.force:
871 return
873 return
872 b2caps = bundle2.bundle2caps(pushop.remote)
874 b2caps = bundle2.bundle2caps(pushop.remote)
873 hasbookmarkcheck = 'bookmarks' in b2caps
875 hasbookmarkcheck = 'bookmarks' in b2caps
874 if not (pushop.outbookmarks and hasbookmarkcheck):
876 if not (pushop.outbookmarks and hasbookmarkcheck):
875 return
877 return
876 data = []
878 data = []
877 for book, old, new in pushop.outbookmarks:
879 for book, old, new in pushop.outbookmarks:
878 old = bin(old)
880 old = bin(old)
879 data.append((book, old))
881 data.append((book, old))
880 checkdata = bookmod.binaryencode(data)
882 checkdata = bookmod.binaryencode(data)
881 bundler.newpart('check:bookmarks', data=checkdata)
883 bundler.newpart('check:bookmarks', data=checkdata)
882
884
883 @b2partsgenerator('check-phases')
885 @b2partsgenerator('check-phases')
884 def _pushb2checkphases(pushop, bundler):
886 def _pushb2checkphases(pushop, bundler):
885 """insert phase move checking"""
887 """insert phase move checking"""
886 if not _pushing(pushop) or pushop.force:
888 if not _pushing(pushop) or pushop.force:
887 return
889 return
888 b2caps = bundle2.bundle2caps(pushop.remote)
890 b2caps = bundle2.bundle2caps(pushop.remote)
889 hasphaseheads = 'heads' in b2caps.get('phases', ())
891 hasphaseheads = 'heads' in b2caps.get('phases', ())
890 if pushop.remotephases is not None and hasphaseheads:
892 if pushop.remotephases is not None and hasphaseheads:
891 # check that the remote phase has not changed
893 # check that the remote phase has not changed
892 checks = [[] for p in phases.allphases]
894 checks = [[] for p in phases.allphases]
893 checks[phases.public].extend(pushop.remotephases.publicheads)
895 checks[phases.public].extend(pushop.remotephases.publicheads)
894 checks[phases.draft].extend(pushop.remotephases.draftroots)
896 checks[phases.draft].extend(pushop.remotephases.draftroots)
895 if any(checks):
897 if any(checks):
896 for nodes in checks:
898 for nodes in checks:
897 nodes.sort()
899 nodes.sort()
898 checkdata = phases.binaryencode(checks)
900 checkdata = phases.binaryencode(checks)
899 bundler.newpart('check:phases', data=checkdata)
901 bundler.newpart('check:phases', data=checkdata)
900
902
901 @b2partsgenerator('changeset')
903 @b2partsgenerator('changeset')
902 def _pushb2ctx(pushop, bundler):
904 def _pushb2ctx(pushop, bundler):
903 """handle changegroup push through bundle2
905 """handle changegroup push through bundle2
904
906
905 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
907 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
906 """
908 """
907 if 'changesets' in pushop.stepsdone:
909 if 'changesets' in pushop.stepsdone:
908 return
910 return
909 pushop.stepsdone.add('changesets')
911 pushop.stepsdone.add('changesets')
910 # Send known heads to the server for race detection.
912 # Send known heads to the server for race detection.
911 if not _pushcheckoutgoing(pushop):
913 if not _pushcheckoutgoing(pushop):
912 return
914 return
913 pushop.repo.prepushoutgoinghooks(pushop)
915 pushop.repo.prepushoutgoinghooks(pushop)
914
916
915 _pushb2ctxcheckheads(pushop, bundler)
917 _pushb2ctxcheckheads(pushop, bundler)
916
918
917 b2caps = bundle2.bundle2caps(pushop.remote)
919 b2caps = bundle2.bundle2caps(pushop.remote)
918 version = '01'
920 version = '01'
919 cgversions = b2caps.get('changegroup')
921 cgversions = b2caps.get('changegroup')
920 if cgversions: # 3.1 and 3.2 ship with an empty value
922 if cgversions: # 3.1 and 3.2 ship with an empty value
921 cgversions = [v for v in cgversions
923 cgversions = [v for v in cgversions
922 if v in changegroup.supportedoutgoingversions(
924 if v in changegroup.supportedoutgoingversions(
923 pushop.repo)]
925 pushop.repo)]
924 if not cgversions:
926 if not cgversions:
925 raise error.Abort(_('no common changegroup version'))
927 raise error.Abort(_('no common changegroup version'))
926 version = max(cgversions)
928 version = max(cgversions)
927 cgstream = changegroup.makestream(pushop.repo, pushop.outgoing, version,
929 cgstream = changegroup.makestream(pushop.repo, pushop.outgoing, version,
928 'push')
930 'push')
929 cgpart = bundler.newpart('changegroup', data=cgstream)
931 cgpart = bundler.newpart('changegroup', data=cgstream)
930 if cgversions:
932 if cgversions:
931 cgpart.addparam('version', version)
933 cgpart.addparam('version', version)
932 if 'treemanifest' in pushop.repo.requirements:
934 if 'treemanifest' in pushop.repo.requirements:
933 cgpart.addparam('treemanifest', '1')
935 cgpart.addparam('treemanifest', '1')
934 def handlereply(op):
936 def handlereply(op):
935 """extract addchangegroup returns from server reply"""
937 """extract addchangegroup returns from server reply"""
936 cgreplies = op.records.getreplies(cgpart.id)
938 cgreplies = op.records.getreplies(cgpart.id)
937 assert len(cgreplies['changegroup']) == 1
939 assert len(cgreplies['changegroup']) == 1
938 pushop.cgresult = cgreplies['changegroup'][0]['return']
940 pushop.cgresult = cgreplies['changegroup'][0]['return']
939 return handlereply
941 return handlereply
940
942
941 @b2partsgenerator('phase')
943 @b2partsgenerator('phase')
942 def _pushb2phases(pushop, bundler):
944 def _pushb2phases(pushop, bundler):
943 """handle phase push through bundle2"""
945 """handle phase push through bundle2"""
944 if 'phases' in pushop.stepsdone:
946 if 'phases' in pushop.stepsdone:
945 return
947 return
946 b2caps = bundle2.bundle2caps(pushop.remote)
948 b2caps = bundle2.bundle2caps(pushop.remote)
947 ui = pushop.repo.ui
949 ui = pushop.repo.ui
948
950
949 legacyphase = 'phases' in ui.configlist('devel', 'legacy.exchange')
951 legacyphase = 'phases' in ui.configlist('devel', 'legacy.exchange')
950 haspushkey = 'pushkey' in b2caps
952 haspushkey = 'pushkey' in b2caps
951 hasphaseheads = 'heads' in b2caps.get('phases', ())
953 hasphaseheads = 'heads' in b2caps.get('phases', ())
952
954
953 if hasphaseheads and not legacyphase:
955 if hasphaseheads and not legacyphase:
954 return _pushb2phaseheads(pushop, bundler)
956 return _pushb2phaseheads(pushop, bundler)
955 elif haspushkey:
957 elif haspushkey:
956 return _pushb2phasespushkey(pushop, bundler)
958 return _pushb2phasespushkey(pushop, bundler)
957
959
958 def _pushb2phaseheads(pushop, bundler):
960 def _pushb2phaseheads(pushop, bundler):
959 """push phase information through a bundle2 - binary part"""
961 """push phase information through a bundle2 - binary part"""
960 pushop.stepsdone.add('phases')
962 pushop.stepsdone.add('phases')
961 if pushop.outdatedphases:
963 if pushop.outdatedphases:
962 updates = [[] for p in phases.allphases]
964 updates = [[] for p in phases.allphases]
963 updates[0].extend(h.node() for h in pushop.outdatedphases)
965 updates[0].extend(h.node() for h in pushop.outdatedphases)
964 phasedata = phases.binaryencode(updates)
966 phasedata = phases.binaryencode(updates)
965 bundler.newpart('phase-heads', data=phasedata)
967 bundler.newpart('phase-heads', data=phasedata)
966
968
967 def _pushb2phasespushkey(pushop, bundler):
969 def _pushb2phasespushkey(pushop, bundler):
968 """push phase information through a bundle2 - pushkey part"""
970 """push phase information through a bundle2 - pushkey part"""
969 pushop.stepsdone.add('phases')
971 pushop.stepsdone.add('phases')
970 part2node = []
972 part2node = []
971
973
972 def handlefailure(pushop, exc):
974 def handlefailure(pushop, exc):
973 targetid = int(exc.partid)
975 targetid = int(exc.partid)
974 for partid, node in part2node:
976 for partid, node in part2node:
975 if partid == targetid:
977 if partid == targetid:
976 raise error.Abort(_('updating %s to public failed') % node)
978 raise error.Abort(_('updating %s to public failed') % node)
977
979
978 enc = pushkey.encode
980 enc = pushkey.encode
979 for newremotehead in pushop.outdatedphases:
981 for newremotehead in pushop.outdatedphases:
980 part = bundler.newpart('pushkey')
982 part = bundler.newpart('pushkey')
981 part.addparam('namespace', enc('phases'))
983 part.addparam('namespace', enc('phases'))
982 part.addparam('key', enc(newremotehead.hex()))
984 part.addparam('key', enc(newremotehead.hex()))
983 part.addparam('old', enc('%d' % phases.draft))
985 part.addparam('old', enc('%d' % phases.draft))
984 part.addparam('new', enc('%d' % phases.public))
986 part.addparam('new', enc('%d' % phases.public))
985 part2node.append((part.id, newremotehead))
987 part2node.append((part.id, newremotehead))
986 pushop.pkfailcb[part.id] = handlefailure
988 pushop.pkfailcb[part.id] = handlefailure
987
989
988 def handlereply(op):
990 def handlereply(op):
989 for partid, node in part2node:
991 for partid, node in part2node:
990 partrep = op.records.getreplies(partid)
992 partrep = op.records.getreplies(partid)
991 results = partrep['pushkey']
993 results = partrep['pushkey']
992 assert len(results) <= 1
994 assert len(results) <= 1
993 msg = None
995 msg = None
994 if not results:
996 if not results:
995 msg = _('server ignored update of %s to public!\n') % node
997 msg = _('server ignored update of %s to public!\n') % node
996 elif not int(results[0]['return']):
998 elif not int(results[0]['return']):
997 msg = _('updating %s to public failed!\n') % node
999 msg = _('updating %s to public failed!\n') % node
998 if msg is not None:
1000 if msg is not None:
999 pushop.ui.warn(msg)
1001 pushop.ui.warn(msg)
1000 return handlereply
1002 return handlereply
1001
1003
1002 @b2partsgenerator('obsmarkers')
1004 @b2partsgenerator('obsmarkers')
1003 def _pushb2obsmarkers(pushop, bundler):
1005 def _pushb2obsmarkers(pushop, bundler):
1004 if 'obsmarkers' in pushop.stepsdone:
1006 if 'obsmarkers' in pushop.stepsdone:
1005 return
1007 return
1006 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
1008 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
1007 if obsolete.commonversion(remoteversions) is None:
1009 if obsolete.commonversion(remoteversions) is None:
1008 return
1010 return
1009 pushop.stepsdone.add('obsmarkers')
1011 pushop.stepsdone.add('obsmarkers')
1010 if pushop.outobsmarkers:
1012 if pushop.outobsmarkers:
1011 markers = sorted(pushop.outobsmarkers)
1013 markers = sorted(pushop.outobsmarkers)
1012 bundle2.buildobsmarkerspart(bundler, markers)
1014 bundle2.buildobsmarkerspart(bundler, markers)
1013
1015
1014 @b2partsgenerator('bookmarks')
1016 @b2partsgenerator('bookmarks')
1015 def _pushb2bookmarks(pushop, bundler):
1017 def _pushb2bookmarks(pushop, bundler):
1016 """handle bookmark push through bundle2"""
1018 """handle bookmark push through bundle2"""
1017 if 'bookmarks' in pushop.stepsdone:
1019 if 'bookmarks' in pushop.stepsdone:
1018 return
1020 return
1019 b2caps = bundle2.bundle2caps(pushop.remote)
1021 b2caps = bundle2.bundle2caps(pushop.remote)
1020
1022
1021 legacy = pushop.repo.ui.configlist('devel', 'legacy.exchange')
1023 legacy = pushop.repo.ui.configlist('devel', 'legacy.exchange')
1022 legacybooks = 'bookmarks' in legacy
1024 legacybooks = 'bookmarks' in legacy
1023
1025
1024 if not legacybooks and 'bookmarks' in b2caps:
1026 if not legacybooks and 'bookmarks' in b2caps:
1025 return _pushb2bookmarkspart(pushop, bundler)
1027 return _pushb2bookmarkspart(pushop, bundler)
1026 elif 'pushkey' in b2caps:
1028 elif 'pushkey' in b2caps:
1027 return _pushb2bookmarkspushkey(pushop, bundler)
1029 return _pushb2bookmarkspushkey(pushop, bundler)
1028
1030
1029 def _bmaction(old, new):
1031 def _bmaction(old, new):
1030 """small utility for bookmark pushing"""
1032 """small utility for bookmark pushing"""
1031 if not old:
1033 if not old:
1032 return 'export'
1034 return 'export'
1033 elif not new:
1035 elif not new:
1034 return 'delete'
1036 return 'delete'
1035 return 'update'
1037 return 'update'
1036
1038
1037 def _pushb2bookmarkspart(pushop, bundler):
1039 def _pushb2bookmarkspart(pushop, bundler):
1038 pushop.stepsdone.add('bookmarks')
1040 pushop.stepsdone.add('bookmarks')
1039 if not pushop.outbookmarks:
1041 if not pushop.outbookmarks:
1040 return
1042 return
1041
1043
1042 allactions = []
1044 allactions = []
1043 data = []
1045 data = []
1044 for book, old, new in pushop.outbookmarks:
1046 for book, old, new in pushop.outbookmarks:
1045 new = bin(new)
1047 new = bin(new)
1046 data.append((book, new))
1048 data.append((book, new))
1047 allactions.append((book, _bmaction(old, new)))
1049 allactions.append((book, _bmaction(old, new)))
1048 checkdata = bookmod.binaryencode(data)
1050 checkdata = bookmod.binaryencode(data)
1049 bundler.newpart('bookmarks', data=checkdata)
1051 bundler.newpart('bookmarks', data=checkdata)
1050
1052
1051 def handlereply(op):
1053 def handlereply(op):
1052 ui = pushop.ui
1054 ui = pushop.ui
1053 # if success
1055 # if success
1054 for book, action in allactions:
1056 for book, action in allactions:
1055 ui.status(bookmsgmap[action][0] % book)
1057 ui.status(bookmsgmap[action][0] % book)
1056
1058
1057 return handlereply
1059 return handlereply
1058
1060
1059 def _pushb2bookmarkspushkey(pushop, bundler):
1061 def _pushb2bookmarkspushkey(pushop, bundler):
1060 pushop.stepsdone.add('bookmarks')
1062 pushop.stepsdone.add('bookmarks')
1061 part2book = []
1063 part2book = []
1062 enc = pushkey.encode
1064 enc = pushkey.encode
1063
1065
1064 def handlefailure(pushop, exc):
1066 def handlefailure(pushop, exc):
1065 targetid = int(exc.partid)
1067 targetid = int(exc.partid)
1066 for partid, book, action in part2book:
1068 for partid, book, action in part2book:
1067 if partid == targetid:
1069 if partid == targetid:
1068 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
1070 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
1069 # we should not be called for part we did not generated
1071 # we should not be called for part we did not generated
1070 assert False
1072 assert False
1071
1073
1072 for book, old, new in pushop.outbookmarks:
1074 for book, old, new in pushop.outbookmarks:
1073 part = bundler.newpart('pushkey')
1075 part = bundler.newpart('pushkey')
1074 part.addparam('namespace', enc('bookmarks'))
1076 part.addparam('namespace', enc('bookmarks'))
1075 part.addparam('key', enc(book))
1077 part.addparam('key', enc(book))
1076 part.addparam('old', enc(old))
1078 part.addparam('old', enc(old))
1077 part.addparam('new', enc(new))
1079 part.addparam('new', enc(new))
1078 action = 'update'
1080 action = 'update'
1079 if not old:
1081 if not old:
1080 action = 'export'
1082 action = 'export'
1081 elif not new:
1083 elif not new:
1082 action = 'delete'
1084 action = 'delete'
1083 part2book.append((part.id, book, action))
1085 part2book.append((part.id, book, action))
1084 pushop.pkfailcb[part.id] = handlefailure
1086 pushop.pkfailcb[part.id] = handlefailure
1085
1087
1086 def handlereply(op):
1088 def handlereply(op):
1087 ui = pushop.ui
1089 ui = pushop.ui
1088 for partid, book, action in part2book:
1090 for partid, book, action in part2book:
1089 partrep = op.records.getreplies(partid)
1091 partrep = op.records.getreplies(partid)
1090 results = partrep['pushkey']
1092 results = partrep['pushkey']
1091 assert len(results) <= 1
1093 assert len(results) <= 1
1092 if not results:
1094 if not results:
1093 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
1095 pushop.ui.warn(_('server ignored bookmark %s update\n') % book)
1094 else:
1096 else:
1095 ret = int(results[0]['return'])
1097 ret = int(results[0]['return'])
1096 if ret:
1098 if ret:
1097 ui.status(bookmsgmap[action][0] % book)
1099 ui.status(bookmsgmap[action][0] % book)
1098 else:
1100 else:
1099 ui.warn(bookmsgmap[action][1] % book)
1101 ui.warn(bookmsgmap[action][1] % book)
1100 if pushop.bkresult is not None:
1102 if pushop.bkresult is not None:
1101 pushop.bkresult = 1
1103 pushop.bkresult = 1
1102 return handlereply
1104 return handlereply
1103
1105
1104 @b2partsgenerator('pushvars', idx=0)
1106 @b2partsgenerator('pushvars', idx=0)
1105 def _getbundlesendvars(pushop, bundler):
1107 def _getbundlesendvars(pushop, bundler):
1106 '''send shellvars via bundle2'''
1108 '''send shellvars via bundle2'''
1107 pushvars = pushop.pushvars
1109 pushvars = pushop.pushvars
1108 if pushvars:
1110 if pushvars:
1109 shellvars = {}
1111 shellvars = {}
1110 for raw in pushvars:
1112 for raw in pushvars:
1111 if '=' not in raw:
1113 if '=' not in raw:
1112 msg = ("unable to parse variable '%s', should follow "
1114 msg = ("unable to parse variable '%s', should follow "
1113 "'KEY=VALUE' or 'KEY=' format")
1115 "'KEY=VALUE' or 'KEY=' format")
1114 raise error.Abort(msg % raw)
1116 raise error.Abort(msg % raw)
1115 k, v = raw.split('=', 1)
1117 k, v = raw.split('=', 1)
1116 shellvars[k] = v
1118 shellvars[k] = v
1117
1119
1118 part = bundler.newpart('pushvars')
1120 part = bundler.newpart('pushvars')
1119
1121
1120 for key, value in shellvars.iteritems():
1122 for key, value in shellvars.iteritems():
1121 part.addparam(key, value, mandatory=False)
1123 part.addparam(key, value, mandatory=False)
1122
1124
1123 def _pushbundle2(pushop):
1125 def _pushbundle2(pushop):
1124 """push data to the remote using bundle2
1126 """push data to the remote using bundle2
1125
1127
1126 The only currently supported type of data is changegroup but this will
1128 The only currently supported type of data is changegroup but this will
1127 evolve in the future."""
1129 evolve in the future."""
1128 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
1130 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
1129 pushback = (pushop.trmanager
1131 pushback = (pushop.trmanager
1130 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
1132 and pushop.ui.configbool('experimental', 'bundle2.pushback'))
1131
1133
1132 # create reply capability
1134 # create reply capability
1133 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
1135 capsblob = bundle2.encodecaps(bundle2.getrepocaps(pushop.repo,
1134 allowpushback=pushback,
1136 allowpushback=pushback,
1135 role='client'))
1137 role='client'))
1136 bundler.newpart('replycaps', data=capsblob)
1138 bundler.newpart('replycaps', data=capsblob)
1137 replyhandlers = []
1139 replyhandlers = []
1138 for partgenname in b2partsgenorder:
1140 for partgenname in b2partsgenorder:
1139 partgen = b2partsgenmapping[partgenname]
1141 partgen = b2partsgenmapping[partgenname]
1140 ret = partgen(pushop, bundler)
1142 ret = partgen(pushop, bundler)
1141 if callable(ret):
1143 if callable(ret):
1142 replyhandlers.append(ret)
1144 replyhandlers.append(ret)
1143 # do not push if nothing to push
1145 # do not push if nothing to push
1144 if bundler.nbparts <= 1:
1146 if bundler.nbparts <= 1:
1145 return
1147 return
1146 stream = util.chunkbuffer(bundler.getchunks())
1148 stream = util.chunkbuffer(bundler.getchunks())
1147 try:
1149 try:
1148 try:
1150 try:
1149 with pushop.remote.commandexecutor() as e:
1151 with pushop.remote.commandexecutor() as e:
1150 reply = e.callcommand('unbundle', {
1152 reply = e.callcommand('unbundle', {
1151 'bundle': stream,
1153 'bundle': stream,
1152 'heads': ['force'],
1154 'heads': ['force'],
1153 'url': pushop.remote.url(),
1155 'url': pushop.remote.url(),
1154 }).result()
1156 }).result()
1155 except error.BundleValueError as exc:
1157 except error.BundleValueError as exc:
1156 raise error.Abort(_('missing support for %s') % exc)
1158 raise error.Abort(_('missing support for %s') % exc)
1157 try:
1159 try:
1158 trgetter = None
1160 trgetter = None
1159 if pushback:
1161 if pushback:
1160 trgetter = pushop.trmanager.transaction
1162 trgetter = pushop.trmanager.transaction
1161 op = bundle2.processbundle(pushop.repo, reply, trgetter)
1163 op = bundle2.processbundle(pushop.repo, reply, trgetter)
1162 except error.BundleValueError as exc:
1164 except error.BundleValueError as exc:
1163 raise error.Abort(_('missing support for %s') % exc)
1165 raise error.Abort(_('missing support for %s') % exc)
1164 except bundle2.AbortFromPart as exc:
1166 except bundle2.AbortFromPart as exc:
1165 pushop.ui.status(_('remote: %s\n') % exc)
1167 pushop.ui.status(_('remote: %s\n') % exc)
1166 if exc.hint is not None:
1168 if exc.hint is not None:
1167 pushop.ui.status(_('remote: %s\n') % ('(%s)' % exc.hint))
1169 pushop.ui.status(_('remote: %s\n') % ('(%s)' % exc.hint))
1168 raise error.Abort(_('push failed on remote'))
1170 raise error.Abort(_('push failed on remote'))
1169 except error.PushkeyFailed as exc:
1171 except error.PushkeyFailed as exc:
1170 partid = int(exc.partid)
1172 partid = int(exc.partid)
1171 if partid not in pushop.pkfailcb:
1173 if partid not in pushop.pkfailcb:
1172 raise
1174 raise
1173 pushop.pkfailcb[partid](pushop, exc)
1175 pushop.pkfailcb[partid](pushop, exc)
1174 for rephand in replyhandlers:
1176 for rephand in replyhandlers:
1175 rephand(op)
1177 rephand(op)
1176
1178
1177 def _pushchangeset(pushop):
1179 def _pushchangeset(pushop):
1178 """Make the actual push of changeset bundle to remote repo"""
1180 """Make the actual push of changeset bundle to remote repo"""
1179 if 'changesets' in pushop.stepsdone:
1181 if 'changesets' in pushop.stepsdone:
1180 return
1182 return
1181 pushop.stepsdone.add('changesets')
1183 pushop.stepsdone.add('changesets')
1182 if not _pushcheckoutgoing(pushop):
1184 if not _pushcheckoutgoing(pushop):
1183 return
1185 return
1184
1186
1185 # Should have verified this in push().
1187 # Should have verified this in push().
1186 assert pushop.remote.capable('unbundle')
1188 assert pushop.remote.capable('unbundle')
1187
1189
1188 pushop.repo.prepushoutgoinghooks(pushop)
1190 pushop.repo.prepushoutgoinghooks(pushop)
1189 outgoing = pushop.outgoing
1191 outgoing = pushop.outgoing
1190 # TODO: get bundlecaps from remote
1192 # TODO: get bundlecaps from remote
1191 bundlecaps = None
1193 bundlecaps = None
1192 # create a changegroup from local
1194 # create a changegroup from local
1193 if pushop.revs is None and not (outgoing.excluded
1195 if pushop.revs is None and not (outgoing.excluded
1194 or pushop.repo.changelog.filteredrevs):
1196 or pushop.repo.changelog.filteredrevs):
1195 # push everything,
1197 # push everything,
1196 # use the fast path, no race possible on push
1198 # use the fast path, no race possible on push
1197 cg = changegroup.makechangegroup(pushop.repo, outgoing, '01', 'push',
1199 cg = changegroup.makechangegroup(pushop.repo, outgoing, '01', 'push',
1198 fastpath=True, bundlecaps=bundlecaps)
1200 fastpath=True, bundlecaps=bundlecaps)
1199 else:
1201 else:
1200 cg = changegroup.makechangegroup(pushop.repo, outgoing, '01',
1202 cg = changegroup.makechangegroup(pushop.repo, outgoing, '01',
1201 'push', bundlecaps=bundlecaps)
1203 'push', bundlecaps=bundlecaps)
1202
1204
1203 # apply changegroup to remote
1205 # apply changegroup to remote
1204 # local repo finds heads on server, finds out what
1206 # local repo finds heads on server, finds out what
1205 # revs it must push. once revs transferred, if server
1207 # revs it must push. once revs transferred, if server
1206 # finds it has different heads (someone else won
1208 # finds it has different heads (someone else won
1207 # commit/push race), server aborts.
1209 # commit/push race), server aborts.
1208 if pushop.force:
1210 if pushop.force:
1209 remoteheads = ['force']
1211 remoteheads = ['force']
1210 else:
1212 else:
1211 remoteheads = pushop.remoteheads
1213 remoteheads = pushop.remoteheads
1212 # ssh: return remote's addchangegroup()
1214 # ssh: return remote's addchangegroup()
1213 # http: return remote's addchangegroup() or 0 for error
1215 # http: return remote's addchangegroup() or 0 for error
1214 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
1216 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads,
1215 pushop.repo.url())
1217 pushop.repo.url())
1216
1218
1217 def _pushsyncphase(pushop):
1219 def _pushsyncphase(pushop):
1218 """synchronise phase information locally and remotely"""
1220 """synchronise phase information locally and remotely"""
1219 cheads = pushop.commonheads
1221 cheads = pushop.commonheads
1220 # even when we don't push, exchanging phase data is useful
1222 # even when we don't push, exchanging phase data is useful
1221 remotephases = listkeys(pushop.remote, 'phases')
1223 remotephases = listkeys(pushop.remote, 'phases')
1222 if (pushop.ui.configbool('ui', '_usedassubrepo')
1224 if (pushop.ui.configbool('ui', '_usedassubrepo')
1223 and remotephases # server supports phases
1225 and remotephases # server supports phases
1224 and pushop.cgresult is None # nothing was pushed
1226 and pushop.cgresult is None # nothing was pushed
1225 and remotephases.get('publishing', False)):
1227 and remotephases.get('publishing', False)):
1226 # When:
1228 # When:
1227 # - this is a subrepo push
1229 # - this is a subrepo push
1228 # - and remote support phase
1230 # - and remote support phase
1229 # - and no changeset was pushed
1231 # - and no changeset was pushed
1230 # - and remote is publishing
1232 # - and remote is publishing
1231 # We may be in issue 3871 case!
1233 # We may be in issue 3871 case!
1232 # We drop the possible phase synchronisation done by
1234 # We drop the possible phase synchronisation done by
1233 # courtesy to publish changesets possibly locally draft
1235 # courtesy to publish changesets possibly locally draft
1234 # on the remote.
1236 # on the remote.
1235 remotephases = {'publishing': 'True'}
1237 remotephases = {'publishing': 'True'}
1236 if not remotephases: # old server or public only reply from non-publishing
1238 if not remotephases: # old server or public only reply from non-publishing
1237 _localphasemove(pushop, cheads)
1239 _localphasemove(pushop, cheads)
1238 # don't push any phase data as there is nothing to push
1240 # don't push any phase data as there is nothing to push
1239 else:
1241 else:
1240 ana = phases.analyzeremotephases(pushop.repo, cheads,
1242 ana = phases.analyzeremotephases(pushop.repo, cheads,
1241 remotephases)
1243 remotephases)
1242 pheads, droots = ana
1244 pheads, droots = ana
1243 ### Apply remote phase on local
1245 ### Apply remote phase on local
1244 if remotephases.get('publishing', False):
1246 if remotephases.get('publishing', False):
1245 _localphasemove(pushop, cheads)
1247 _localphasemove(pushop, cheads)
1246 else: # publish = False
1248 else: # publish = False
1247 _localphasemove(pushop, pheads)
1249 _localphasemove(pushop, pheads)
1248 _localphasemove(pushop, cheads, phases.draft)
1250 _localphasemove(pushop, cheads, phases.draft)
1249 ### Apply local phase on remote
1251 ### Apply local phase on remote
1250
1252
1251 if pushop.cgresult:
1253 if pushop.cgresult:
1252 if 'phases' in pushop.stepsdone:
1254 if 'phases' in pushop.stepsdone:
1253 # phases already pushed though bundle2
1255 # phases already pushed though bundle2
1254 return
1256 return
1255 outdated = pushop.outdatedphases
1257 outdated = pushop.outdatedphases
1256 else:
1258 else:
1257 outdated = pushop.fallbackoutdatedphases
1259 outdated = pushop.fallbackoutdatedphases
1258
1260
1259 pushop.stepsdone.add('phases')
1261 pushop.stepsdone.add('phases')
1260
1262
1261 # filter heads already turned public by the push
1263 # filter heads already turned public by the push
1262 outdated = [c for c in outdated if c.node() not in pheads]
1264 outdated = [c for c in outdated if c.node() not in pheads]
1263 # fallback to independent pushkey command
1265 # fallback to independent pushkey command
1264 for newremotehead in outdated:
1266 for newremotehead in outdated:
1265 with pushop.remote.commandexecutor() as e:
1267 with pushop.remote.commandexecutor() as e:
1266 r = e.callcommand('pushkey', {
1268 r = e.callcommand('pushkey', {
1267 'namespace': 'phases',
1269 'namespace': 'phases',
1268 'key': newremotehead.hex(),
1270 'key': newremotehead.hex(),
1269 'old': '%d' % phases.draft,
1271 'old': '%d' % phases.draft,
1270 'new': '%d' % phases.public
1272 'new': '%d' % phases.public
1271 }).result()
1273 }).result()
1272
1274
1273 if not r:
1275 if not r:
1274 pushop.ui.warn(_('updating %s to public failed!\n')
1276 pushop.ui.warn(_('updating %s to public failed!\n')
1275 % newremotehead)
1277 % newremotehead)
1276
1278
1277 def _localphasemove(pushop, nodes, phase=phases.public):
1279 def _localphasemove(pushop, nodes, phase=phases.public):
1278 """move <nodes> to <phase> in the local source repo"""
1280 """move <nodes> to <phase> in the local source repo"""
1279 if pushop.trmanager:
1281 if pushop.trmanager:
1280 phases.advanceboundary(pushop.repo,
1282 phases.advanceboundary(pushop.repo,
1281 pushop.trmanager.transaction(),
1283 pushop.trmanager.transaction(),
1282 phase,
1284 phase,
1283 nodes)
1285 nodes)
1284 else:
1286 else:
1285 # repo is not locked, do not change any phases!
1287 # repo is not locked, do not change any phases!
1286 # Informs the user that phases should have been moved when
1288 # Informs the user that phases should have been moved when
1287 # applicable.
1289 # applicable.
1288 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1290 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1289 phasestr = phases.phasenames[phase]
1291 phasestr = phases.phasenames[phase]
1290 if actualmoves:
1292 if actualmoves:
1291 pushop.ui.status(_('cannot lock source repo, skipping '
1293 pushop.ui.status(_('cannot lock source repo, skipping '
1292 'local %s phase update\n') % phasestr)
1294 'local %s phase update\n') % phasestr)
1293
1295
1294 def _pushobsolete(pushop):
1296 def _pushobsolete(pushop):
1295 """utility function to push obsolete markers to a remote"""
1297 """utility function to push obsolete markers to a remote"""
1296 if 'obsmarkers' in pushop.stepsdone:
1298 if 'obsmarkers' in pushop.stepsdone:
1297 return
1299 return
1298 repo = pushop.repo
1300 repo = pushop.repo
1299 remote = pushop.remote
1301 remote = pushop.remote
1300 pushop.stepsdone.add('obsmarkers')
1302 pushop.stepsdone.add('obsmarkers')
1301 if pushop.outobsmarkers:
1303 if pushop.outobsmarkers:
1302 pushop.ui.debug('try to push obsolete markers to remote\n')
1304 pushop.ui.debug('try to push obsolete markers to remote\n')
1303 rslts = []
1305 rslts = []
1304 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1306 remotedata = obsolete._pushkeyescape(sorted(pushop.outobsmarkers))
1305 for key in sorted(remotedata, reverse=True):
1307 for key in sorted(remotedata, reverse=True):
1306 # reverse sort to ensure we end with dump0
1308 # reverse sort to ensure we end with dump0
1307 data = remotedata[key]
1309 data = remotedata[key]
1308 rslts.append(remote.pushkey('obsolete', key, '', data))
1310 rslts.append(remote.pushkey('obsolete', key, '', data))
1309 if [r for r in rslts if not r]:
1311 if [r for r in rslts if not r]:
1310 msg = _('failed to push some obsolete markers!\n')
1312 msg = _('failed to push some obsolete markers!\n')
1311 repo.ui.warn(msg)
1313 repo.ui.warn(msg)
1312
1314
1313 def _pushbookmark(pushop):
1315 def _pushbookmark(pushop):
1314 """Update bookmark position on remote"""
1316 """Update bookmark position on remote"""
1315 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1317 if pushop.cgresult == 0 or 'bookmarks' in pushop.stepsdone:
1316 return
1318 return
1317 pushop.stepsdone.add('bookmarks')
1319 pushop.stepsdone.add('bookmarks')
1318 ui = pushop.ui
1320 ui = pushop.ui
1319 remote = pushop.remote
1321 remote = pushop.remote
1320
1322
1321 for b, old, new in pushop.outbookmarks:
1323 for b, old, new in pushop.outbookmarks:
1322 action = 'update'
1324 action = 'update'
1323 if not old:
1325 if not old:
1324 action = 'export'
1326 action = 'export'
1325 elif not new:
1327 elif not new:
1326 action = 'delete'
1328 action = 'delete'
1327
1329
1328 with remote.commandexecutor() as e:
1330 with remote.commandexecutor() as e:
1329 r = e.callcommand('pushkey', {
1331 r = e.callcommand('pushkey', {
1330 'namespace': 'bookmarks',
1332 'namespace': 'bookmarks',
1331 'key': b,
1333 'key': b,
1332 'old': old,
1334 'old': old,
1333 'new': new,
1335 'new': new,
1334 }).result()
1336 }).result()
1335
1337
1336 if r:
1338 if r:
1337 ui.status(bookmsgmap[action][0] % b)
1339 ui.status(bookmsgmap[action][0] % b)
1338 else:
1340 else:
1339 ui.warn(bookmsgmap[action][1] % b)
1341 ui.warn(bookmsgmap[action][1] % b)
1340 # discovery can have set the value form invalid entry
1342 # discovery can have set the value form invalid entry
1341 if pushop.bkresult is not None:
1343 if pushop.bkresult is not None:
1342 pushop.bkresult = 1
1344 pushop.bkresult = 1
1343
1345
1344 class pulloperation(object):
1346 class pulloperation(object):
1345 """A object that represent a single pull operation
1347 """A object that represent a single pull operation
1346
1348
1347 It purpose is to carry pull related state and very common operation.
1349 It purpose is to carry pull related state and very common operation.
1348
1350
1349 A new should be created at the beginning of each pull and discarded
1351 A new should be created at the beginning of each pull and discarded
1350 afterward.
1352 afterward.
1351 """
1353 """
1352
1354
1353 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1355 def __init__(self, repo, remote, heads=None, force=False, bookmarks=(),
1354 remotebookmarks=None, streamclonerequested=None,
1356 remotebookmarks=None, streamclonerequested=None,
1355 includepats=None, excludepats=None, depth=None):
1357 includepats=None, excludepats=None, depth=None):
1356 # repo we pull into
1358 # repo we pull into
1357 self.repo = repo
1359 self.repo = repo
1358 # repo we pull from
1360 # repo we pull from
1359 self.remote = remote
1361 self.remote = remote
1360 # revision we try to pull (None is "all")
1362 # revision we try to pull (None is "all")
1361 self.heads = heads
1363 self.heads = heads
1362 # bookmark pulled explicitly
1364 # bookmark pulled explicitly
1363 self.explicitbookmarks = [repo._bookmarks.expandname(bookmark)
1365 self.explicitbookmarks = [repo._bookmarks.expandname(bookmark)
1364 for bookmark in bookmarks]
1366 for bookmark in bookmarks]
1365 # do we force pull?
1367 # do we force pull?
1366 self.force = force
1368 self.force = force
1367 # whether a streaming clone was requested
1369 # whether a streaming clone was requested
1368 self.streamclonerequested = streamclonerequested
1370 self.streamclonerequested = streamclonerequested
1369 # transaction manager
1371 # transaction manager
1370 self.trmanager = None
1372 self.trmanager = None
1371 # set of common changeset between local and remote before pull
1373 # set of common changeset between local and remote before pull
1372 self.common = None
1374 self.common = None
1373 # set of pulled head
1375 # set of pulled head
1374 self.rheads = None
1376 self.rheads = None
1375 # list of missing changeset to fetch remotely
1377 # list of missing changeset to fetch remotely
1376 self.fetch = None
1378 self.fetch = None
1377 # remote bookmarks data
1379 # remote bookmarks data
1378 self.remotebookmarks = remotebookmarks
1380 self.remotebookmarks = remotebookmarks
1379 # result of changegroup pulling (used as return code by pull)
1381 # result of changegroup pulling (used as return code by pull)
1380 self.cgresult = None
1382 self.cgresult = None
1381 # list of step already done
1383 # list of step already done
1382 self.stepsdone = set()
1384 self.stepsdone = set()
1383 # Whether we attempted a clone from pre-generated bundles.
1385 # Whether we attempted a clone from pre-generated bundles.
1384 self.clonebundleattempted = False
1386 self.clonebundleattempted = False
1385 # Set of file patterns to include.
1387 # Set of file patterns to include.
1386 self.includepats = includepats
1388 self.includepats = includepats
1387 # Set of file patterns to exclude.
1389 # Set of file patterns to exclude.
1388 self.excludepats = excludepats
1390 self.excludepats = excludepats
1389 # Number of ancestor changesets to pull from each pulled head.
1391 # Number of ancestor changesets to pull from each pulled head.
1390 self.depth = depth
1392 self.depth = depth
1391
1393
1392 @util.propertycache
1394 @util.propertycache
1393 def pulledsubset(self):
1395 def pulledsubset(self):
1394 """heads of the set of changeset target by the pull"""
1396 """heads of the set of changeset target by the pull"""
1395 # compute target subset
1397 # compute target subset
1396 if self.heads is None:
1398 if self.heads is None:
1397 # We pulled every thing possible
1399 # We pulled every thing possible
1398 # sync on everything common
1400 # sync on everything common
1399 c = set(self.common)
1401 c = set(self.common)
1400 ret = list(self.common)
1402 ret = list(self.common)
1401 for n in self.rheads:
1403 for n in self.rheads:
1402 if n not in c:
1404 if n not in c:
1403 ret.append(n)
1405 ret.append(n)
1404 return ret
1406 return ret
1405 else:
1407 else:
1406 # We pulled a specific subset
1408 # We pulled a specific subset
1407 # sync on this subset
1409 # sync on this subset
1408 return self.heads
1410 return self.heads
1409
1411
1410 @util.propertycache
1412 @util.propertycache
1411 def canusebundle2(self):
1413 def canusebundle2(self):
1412 return not _forcebundle1(self)
1414 return not _forcebundle1(self)
1413
1415
1414 @util.propertycache
1416 @util.propertycache
1415 def remotebundle2caps(self):
1417 def remotebundle2caps(self):
1416 return bundle2.bundle2caps(self.remote)
1418 return bundle2.bundle2caps(self.remote)
1417
1419
1418 def gettransaction(self):
1420 def gettransaction(self):
1419 # deprecated; talk to trmanager directly
1421 # deprecated; talk to trmanager directly
1420 return self.trmanager.transaction()
1422 return self.trmanager.transaction()
1421
1423
1422 class transactionmanager(util.transactional):
1424 class transactionmanager(util.transactional):
1423 """An object to manage the life cycle of a transaction
1425 """An object to manage the life cycle of a transaction
1424
1426
1425 It creates the transaction on demand and calls the appropriate hooks when
1427 It creates the transaction on demand and calls the appropriate hooks when
1426 closing the transaction."""
1428 closing the transaction."""
1427 def __init__(self, repo, source, url):
1429 def __init__(self, repo, source, url):
1428 self.repo = repo
1430 self.repo = repo
1429 self.source = source
1431 self.source = source
1430 self.url = url
1432 self.url = url
1431 self._tr = None
1433 self._tr = None
1432
1434
1433 def transaction(self):
1435 def transaction(self):
1434 """Return an open transaction object, constructing if necessary"""
1436 """Return an open transaction object, constructing if necessary"""
1435 if not self._tr:
1437 if not self._tr:
1436 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1438 trname = '%s\n%s' % (self.source, util.hidepassword(self.url))
1437 self._tr = self.repo.transaction(trname)
1439 self._tr = self.repo.transaction(trname)
1438 self._tr.hookargs['source'] = self.source
1440 self._tr.hookargs['source'] = self.source
1439 self._tr.hookargs['url'] = self.url
1441 self._tr.hookargs['url'] = self.url
1440 return self._tr
1442 return self._tr
1441
1443
1442 def close(self):
1444 def close(self):
1443 """close transaction if created"""
1445 """close transaction if created"""
1444 if self._tr is not None:
1446 if self._tr is not None:
1445 self._tr.close()
1447 self._tr.close()
1446
1448
1447 def release(self):
1449 def release(self):
1448 """release transaction if created"""
1450 """release transaction if created"""
1449 if self._tr is not None:
1451 if self._tr is not None:
1450 self._tr.release()
1452 self._tr.release()
1451
1453
1452 def listkeys(remote, namespace):
1454 def listkeys(remote, namespace):
1453 with remote.commandexecutor() as e:
1455 with remote.commandexecutor() as e:
1454 return e.callcommand('listkeys', {'namespace': namespace}).result()
1456 return e.callcommand('listkeys', {'namespace': namespace}).result()
1455
1457
1456 def _fullpullbundle2(repo, pullop):
1458 def _fullpullbundle2(repo, pullop):
1457 # The server may send a partial reply, i.e. when inlining
1459 # The server may send a partial reply, i.e. when inlining
1458 # pre-computed bundles. In that case, update the common
1460 # pre-computed bundles. In that case, update the common
1459 # set based on the results and pull another bundle.
1461 # set based on the results and pull another bundle.
1460 #
1462 #
1461 # There are two indicators that the process is finished:
1463 # There are two indicators that the process is finished:
1462 # - no changeset has been added, or
1464 # - no changeset has been added, or
1463 # - all remote heads are known locally.
1465 # - all remote heads are known locally.
1464 # The head check must use the unfiltered view as obsoletion
1466 # The head check must use the unfiltered view as obsoletion
1465 # markers can hide heads.
1467 # markers can hide heads.
1466 unfi = repo.unfiltered()
1468 unfi = repo.unfiltered()
1467 unficl = unfi.changelog
1469 unficl = unfi.changelog
1468 def headsofdiff(h1, h2):
1470 def headsofdiff(h1, h2):
1469 """Returns heads(h1 % h2)"""
1471 """Returns heads(h1 % h2)"""
1470 res = unfi.set('heads(%ln %% %ln)', h1, h2)
1472 res = unfi.set('heads(%ln %% %ln)', h1, h2)
1471 return set(ctx.node() for ctx in res)
1473 return set(ctx.node() for ctx in res)
1472 def headsofunion(h1, h2):
1474 def headsofunion(h1, h2):
1473 """Returns heads((h1 + h2) - null)"""
1475 """Returns heads((h1 + h2) - null)"""
1474 res = unfi.set('heads((%ln + %ln - null))', h1, h2)
1476 res = unfi.set('heads((%ln + %ln - null))', h1, h2)
1475 return set(ctx.node() for ctx in res)
1477 return set(ctx.node() for ctx in res)
1476 while True:
1478 while True:
1477 old_heads = unficl.heads()
1479 old_heads = unficl.heads()
1478 clstart = len(unficl)
1480 clstart = len(unficl)
1479 _pullbundle2(pullop)
1481 _pullbundle2(pullop)
1480 if repository.NARROW_REQUIREMENT in repo.requirements:
1482 if repository.NARROW_REQUIREMENT in repo.requirements:
1481 # XXX narrow clones filter the heads on the server side during
1483 # XXX narrow clones filter the heads on the server side during
1482 # XXX getbundle and result in partial replies as well.
1484 # XXX getbundle and result in partial replies as well.
1483 # XXX Disable pull bundles in this case as band aid to avoid
1485 # XXX Disable pull bundles in this case as band aid to avoid
1484 # XXX extra round trips.
1486 # XXX extra round trips.
1485 break
1487 break
1486 if clstart == len(unficl):
1488 if clstart == len(unficl):
1487 break
1489 break
1488 if all(unficl.hasnode(n) for n in pullop.rheads):
1490 if all(unficl.hasnode(n) for n in pullop.rheads):
1489 break
1491 break
1490 new_heads = headsofdiff(unficl.heads(), old_heads)
1492 new_heads = headsofdiff(unficl.heads(), old_heads)
1491 pullop.common = headsofunion(new_heads, pullop.common)
1493 pullop.common = headsofunion(new_heads, pullop.common)
1492 pullop.rheads = set(pullop.rheads) - pullop.common
1494 pullop.rheads = set(pullop.rheads) - pullop.common
1493
1495
1494 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1496 def pull(repo, remote, heads=None, force=False, bookmarks=(), opargs=None,
1495 streamclonerequested=None, includepats=None, excludepats=None,
1497 streamclonerequested=None, includepats=None, excludepats=None,
1496 depth=None):
1498 depth=None):
1497 """Fetch repository data from a remote.
1499 """Fetch repository data from a remote.
1498
1500
1499 This is the main function used to retrieve data from a remote repository.
1501 This is the main function used to retrieve data from a remote repository.
1500
1502
1501 ``repo`` is the local repository to clone into.
1503 ``repo`` is the local repository to clone into.
1502 ``remote`` is a peer instance.
1504 ``remote`` is a peer instance.
1503 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1505 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1504 default) means to pull everything from the remote.
1506 default) means to pull everything from the remote.
1505 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1507 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1506 default, all remote bookmarks are pulled.
1508 default, all remote bookmarks are pulled.
1507 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1509 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1508 initialization.
1510 initialization.
1509 ``streamclonerequested`` is a boolean indicating whether a "streaming
1511 ``streamclonerequested`` is a boolean indicating whether a "streaming
1510 clone" is requested. A "streaming clone" is essentially a raw file copy
1512 clone" is requested. A "streaming clone" is essentially a raw file copy
1511 of revlogs from the server. This only works when the local repository is
1513 of revlogs from the server. This only works when the local repository is
1512 empty. The default value of ``None`` means to respect the server
1514 empty. The default value of ``None`` means to respect the server
1513 configuration for preferring stream clones.
1515 configuration for preferring stream clones.
1514 ``includepats`` and ``excludepats`` define explicit file patterns to
1516 ``includepats`` and ``excludepats`` define explicit file patterns to
1515 include and exclude in storage, respectively. If not defined, narrow
1517 include and exclude in storage, respectively. If not defined, narrow
1516 patterns from the repo instance are used, if available.
1518 patterns from the repo instance are used, if available.
1517 ``depth`` is an integer indicating the DAG depth of history we're
1519 ``depth`` is an integer indicating the DAG depth of history we're
1518 interested in. If defined, for each revision specified in ``heads``, we
1520 interested in. If defined, for each revision specified in ``heads``, we
1519 will fetch up to this many of its ancestors and data associated with them.
1521 will fetch up to this many of its ancestors and data associated with them.
1520
1522
1521 Returns the ``pulloperation`` created for this pull.
1523 Returns the ``pulloperation`` created for this pull.
1522 """
1524 """
1523 if opargs is None:
1525 if opargs is None:
1524 opargs = {}
1526 opargs = {}
1525
1527
1526 # We allow the narrow patterns to be passed in explicitly to provide more
1528 # We allow the narrow patterns to be passed in explicitly to provide more
1527 # flexibility for API consumers.
1529 # flexibility for API consumers.
1528 if includepats or excludepats:
1530 if includepats or excludepats:
1529 includepats = includepats or set()
1531 includepats = includepats or set()
1530 excludepats = excludepats or set()
1532 excludepats = excludepats or set()
1531 else:
1533 else:
1532 includepats, excludepats = repo.narrowpats
1534 includepats, excludepats = repo.narrowpats
1533
1535
1534 narrowspec.validatepatterns(includepats)
1536 narrowspec.validatepatterns(includepats)
1535 narrowspec.validatepatterns(excludepats)
1537 narrowspec.validatepatterns(excludepats)
1536
1538
1537 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1539 pullop = pulloperation(repo, remote, heads, force, bookmarks=bookmarks,
1538 streamclonerequested=streamclonerequested,
1540 streamclonerequested=streamclonerequested,
1539 includepats=includepats, excludepats=excludepats,
1541 includepats=includepats, excludepats=excludepats,
1540 depth=depth,
1542 depth=depth,
1541 **pycompat.strkwargs(opargs))
1543 **pycompat.strkwargs(opargs))
1542
1544
1543 peerlocal = pullop.remote.local()
1545 peerlocal = pullop.remote.local()
1544 if peerlocal:
1546 if peerlocal:
1545 missing = set(peerlocal.requirements) - pullop.repo.supported
1547 missing = set(peerlocal.requirements) - pullop.repo.supported
1546 if missing:
1548 if missing:
1547 msg = _("required features are not"
1549 msg = _("required features are not"
1548 " supported in the destination:"
1550 " supported in the destination:"
1549 " %s") % (', '.join(sorted(missing)))
1551 " %s") % (', '.join(sorted(missing)))
1550 raise error.Abort(msg)
1552 raise error.Abort(msg)
1551
1553
1552 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1554 pullop.trmanager = transactionmanager(repo, 'pull', remote.url())
1553 wlock = util.nullcontextmanager()
1555 wlock = util.nullcontextmanager()
1554 if not bookmod.bookmarksinstore(repo):
1556 if not bookmod.bookmarksinstore(repo):
1555 wlock = repo.wlock()
1557 wlock = repo.wlock()
1556 with wlock, repo.lock(), pullop.trmanager:
1558 with wlock, repo.lock(), pullop.trmanager:
1557 # Use the modern wire protocol, if available.
1559 # Use the modern wire protocol, if available.
1558 if remote.capable('command-changesetdata'):
1560 if remote.capable('command-changesetdata'):
1559 exchangev2.pull(pullop)
1561 exchangev2.pull(pullop)
1560 else:
1562 else:
1561 # This should ideally be in _pullbundle2(). However, it needs to run
1563 # This should ideally be in _pullbundle2(). However, it needs to run
1562 # before discovery to avoid extra work.
1564 # before discovery to avoid extra work.
1563 _maybeapplyclonebundle(pullop)
1565 _maybeapplyclonebundle(pullop)
1564 streamclone.maybeperformlegacystreamclone(pullop)
1566 streamclone.maybeperformlegacystreamclone(pullop)
1565 _pulldiscovery(pullop)
1567 _pulldiscovery(pullop)
1566 if pullop.canusebundle2:
1568 if pullop.canusebundle2:
1567 _fullpullbundle2(repo, pullop)
1569 _fullpullbundle2(repo, pullop)
1568 _pullchangeset(pullop)
1570 _pullchangeset(pullop)
1569 _pullphase(pullop)
1571 _pullphase(pullop)
1570 _pullbookmarks(pullop)
1572 _pullbookmarks(pullop)
1571 _pullobsolete(pullop)
1573 _pullobsolete(pullop)
1572
1574
1573 # storing remotenames
1575 # storing remotenames
1574 if repo.ui.configbool('experimental', 'remotenames'):
1576 if repo.ui.configbool('experimental', 'remotenames'):
1575 logexchange.pullremotenames(repo, remote)
1577 logexchange.pullremotenames(repo, remote)
1576
1578
1577 return pullop
1579 return pullop
1578
1580
1579 # list of steps to perform discovery before pull
1581 # list of steps to perform discovery before pull
1580 pulldiscoveryorder = []
1582 pulldiscoveryorder = []
1581
1583
1582 # Mapping between step name and function
1584 # Mapping between step name and function
1583 #
1585 #
1584 # This exists to help extensions wrap steps if necessary
1586 # This exists to help extensions wrap steps if necessary
1585 pulldiscoverymapping = {}
1587 pulldiscoverymapping = {}
1586
1588
1587 def pulldiscovery(stepname):
1589 def pulldiscovery(stepname):
1588 """decorator for function performing discovery before pull
1590 """decorator for function performing discovery before pull
1589
1591
1590 The function is added to the step -> function mapping and appended to the
1592 The function is added to the step -> function mapping and appended to the
1591 list of steps. Beware that decorated function will be added in order (this
1593 list of steps. Beware that decorated function will be added in order (this
1592 may matter).
1594 may matter).
1593
1595
1594 You can only use this decorator for a new step, if you want to wrap a step
1596 You can only use this decorator for a new step, if you want to wrap a step
1595 from an extension, change the pulldiscovery dictionary directly."""
1597 from an extension, change the pulldiscovery dictionary directly."""
1596 def dec(func):
1598 def dec(func):
1597 assert stepname not in pulldiscoverymapping
1599 assert stepname not in pulldiscoverymapping
1598 pulldiscoverymapping[stepname] = func
1600 pulldiscoverymapping[stepname] = func
1599 pulldiscoveryorder.append(stepname)
1601 pulldiscoveryorder.append(stepname)
1600 return func
1602 return func
1601 return dec
1603 return dec
1602
1604
1603 def _pulldiscovery(pullop):
1605 def _pulldiscovery(pullop):
1604 """Run all discovery steps"""
1606 """Run all discovery steps"""
1605 for stepname in pulldiscoveryorder:
1607 for stepname in pulldiscoveryorder:
1606 step = pulldiscoverymapping[stepname]
1608 step = pulldiscoverymapping[stepname]
1607 step(pullop)
1609 step(pullop)
1608
1610
1609 @pulldiscovery('b1:bookmarks')
1611 @pulldiscovery('b1:bookmarks')
1610 def _pullbookmarkbundle1(pullop):
1612 def _pullbookmarkbundle1(pullop):
1611 """fetch bookmark data in bundle1 case
1613 """fetch bookmark data in bundle1 case
1612
1614
1613 If not using bundle2, we have to fetch bookmarks before changeset
1615 If not using bundle2, we have to fetch bookmarks before changeset
1614 discovery to reduce the chance and impact of race conditions."""
1616 discovery to reduce the chance and impact of race conditions."""
1615 if pullop.remotebookmarks is not None:
1617 if pullop.remotebookmarks is not None:
1616 return
1618 return
1617 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1619 if pullop.canusebundle2 and 'listkeys' in pullop.remotebundle2caps:
1618 # all known bundle2 servers now support listkeys, but lets be nice with
1620 # all known bundle2 servers now support listkeys, but lets be nice with
1619 # new implementation.
1621 # new implementation.
1620 return
1622 return
1621 books = listkeys(pullop.remote, 'bookmarks')
1623 books = listkeys(pullop.remote, 'bookmarks')
1622 pullop.remotebookmarks = bookmod.unhexlifybookmarks(books)
1624 pullop.remotebookmarks = bookmod.unhexlifybookmarks(books)
1623
1625
1624
1626
1625 @pulldiscovery('changegroup')
1627 @pulldiscovery('changegroup')
1626 def _pulldiscoverychangegroup(pullop):
1628 def _pulldiscoverychangegroup(pullop):
1627 """discovery phase for the pull
1629 """discovery phase for the pull
1628
1630
1629 Current handle changeset discovery only, will change handle all discovery
1631 Current handle changeset discovery only, will change handle all discovery
1630 at some point."""
1632 at some point."""
1631 tmp = discovery.findcommonincoming(pullop.repo,
1633 tmp = discovery.findcommonincoming(pullop.repo,
1632 pullop.remote,
1634 pullop.remote,
1633 heads=pullop.heads,
1635 heads=pullop.heads,
1634 force=pullop.force)
1636 force=pullop.force)
1635 common, fetch, rheads = tmp
1637 common, fetch, rheads = tmp
1636 nm = pullop.repo.unfiltered().changelog.nodemap
1638 nm = pullop.repo.unfiltered().changelog.nodemap
1637 if fetch and rheads:
1639 if fetch and rheads:
1638 # If a remote heads is filtered locally, put in back in common.
1640 # If a remote heads is filtered locally, put in back in common.
1639 #
1641 #
1640 # This is a hackish solution to catch most of "common but locally
1642 # This is a hackish solution to catch most of "common but locally
1641 # hidden situation". We do not performs discovery on unfiltered
1643 # hidden situation". We do not performs discovery on unfiltered
1642 # repository because it end up doing a pathological amount of round
1644 # repository because it end up doing a pathological amount of round
1643 # trip for w huge amount of changeset we do not care about.
1645 # trip for w huge amount of changeset we do not care about.
1644 #
1646 #
1645 # If a set of such "common but filtered" changeset exist on the server
1647 # If a set of such "common but filtered" changeset exist on the server
1646 # but are not including a remote heads, we'll not be able to detect it,
1648 # but are not including a remote heads, we'll not be able to detect it,
1647 scommon = set(common)
1649 scommon = set(common)
1648 for n in rheads:
1650 for n in rheads:
1649 if n in nm:
1651 if n in nm:
1650 if n not in scommon:
1652 if n not in scommon:
1651 common.append(n)
1653 common.append(n)
1652 if set(rheads).issubset(set(common)):
1654 if set(rheads).issubset(set(common)):
1653 fetch = []
1655 fetch = []
1654 pullop.common = common
1656 pullop.common = common
1655 pullop.fetch = fetch
1657 pullop.fetch = fetch
1656 pullop.rheads = rheads
1658 pullop.rheads = rheads
1657
1659
1658 def _pullbundle2(pullop):
1660 def _pullbundle2(pullop):
1659 """pull data using bundle2
1661 """pull data using bundle2
1660
1662
1661 For now, the only supported data are changegroup."""
1663 For now, the only supported data are changegroup."""
1662 kwargs = {'bundlecaps': caps20to10(pullop.repo, role='client')}
1664 kwargs = {'bundlecaps': caps20to10(pullop.repo, role='client')}
1663
1665
1664 # make ui easier to access
1666 # make ui easier to access
1665 ui = pullop.repo.ui
1667 ui = pullop.repo.ui
1666
1668
1667 # At the moment we don't do stream clones over bundle2. If that is
1669 # At the moment we don't do stream clones over bundle2. If that is
1668 # implemented then here's where the check for that will go.
1670 # implemented then here's where the check for that will go.
1669 streaming = streamclone.canperformstreamclone(pullop, bundle2=True)[0]
1671 streaming = streamclone.canperformstreamclone(pullop, bundle2=True)[0]
1670
1672
1671 # declare pull perimeters
1673 # declare pull perimeters
1672 kwargs['common'] = pullop.common
1674 kwargs['common'] = pullop.common
1673 kwargs['heads'] = pullop.heads or pullop.rheads
1675 kwargs['heads'] = pullop.heads or pullop.rheads
1674
1676
1675 # check server supports narrow and then adding includepats and excludepats
1677 # check server supports narrow and then adding includepats and excludepats
1676 servernarrow = pullop.remote.capable(wireprototypes.NARROWCAP)
1678 servernarrow = pullop.remote.capable(wireprototypes.NARROWCAP)
1677 if servernarrow and pullop.includepats:
1679 if servernarrow and pullop.includepats:
1678 kwargs['includepats'] = pullop.includepats
1680 kwargs['includepats'] = pullop.includepats
1679 if servernarrow and pullop.excludepats:
1681 if servernarrow and pullop.excludepats:
1680 kwargs['excludepats'] = pullop.excludepats
1682 kwargs['excludepats'] = pullop.excludepats
1681
1683
1682 if streaming:
1684 if streaming:
1683 kwargs['cg'] = False
1685 kwargs['cg'] = False
1684 kwargs['stream'] = True
1686 kwargs['stream'] = True
1685 pullop.stepsdone.add('changegroup')
1687 pullop.stepsdone.add('changegroup')
1686 pullop.stepsdone.add('phases')
1688 pullop.stepsdone.add('phases')
1687
1689
1688 else:
1690 else:
1689 # pulling changegroup
1691 # pulling changegroup
1690 pullop.stepsdone.add('changegroup')
1692 pullop.stepsdone.add('changegroup')
1691
1693
1692 kwargs['cg'] = pullop.fetch
1694 kwargs['cg'] = pullop.fetch
1693
1695
1694 legacyphase = 'phases' in ui.configlist('devel', 'legacy.exchange')
1696 legacyphase = 'phases' in ui.configlist('devel', 'legacy.exchange')
1695 hasbinaryphase = 'heads' in pullop.remotebundle2caps.get('phases', ())
1697 hasbinaryphase = 'heads' in pullop.remotebundle2caps.get('phases', ())
1696 if (not legacyphase and hasbinaryphase):
1698 if (not legacyphase and hasbinaryphase):
1697 kwargs['phases'] = True
1699 kwargs['phases'] = True
1698 pullop.stepsdone.add('phases')
1700 pullop.stepsdone.add('phases')
1699
1701
1700 if 'listkeys' in pullop.remotebundle2caps:
1702 if 'listkeys' in pullop.remotebundle2caps:
1701 if 'phases' not in pullop.stepsdone:
1703 if 'phases' not in pullop.stepsdone:
1702 kwargs['listkeys'] = ['phases']
1704 kwargs['listkeys'] = ['phases']
1703
1705
1704 bookmarksrequested = False
1706 bookmarksrequested = False
1705 legacybookmark = 'bookmarks' in ui.configlist('devel', 'legacy.exchange')
1707 legacybookmark = 'bookmarks' in ui.configlist('devel', 'legacy.exchange')
1706 hasbinarybook = 'bookmarks' in pullop.remotebundle2caps
1708 hasbinarybook = 'bookmarks' in pullop.remotebundle2caps
1707
1709
1708 if pullop.remotebookmarks is not None:
1710 if pullop.remotebookmarks is not None:
1709 pullop.stepsdone.add('request-bookmarks')
1711 pullop.stepsdone.add('request-bookmarks')
1710
1712
1711 if ('request-bookmarks' not in pullop.stepsdone
1713 if ('request-bookmarks' not in pullop.stepsdone
1712 and pullop.remotebookmarks is None
1714 and pullop.remotebookmarks is None
1713 and not legacybookmark and hasbinarybook):
1715 and not legacybookmark and hasbinarybook):
1714 kwargs['bookmarks'] = True
1716 kwargs['bookmarks'] = True
1715 bookmarksrequested = True
1717 bookmarksrequested = True
1716
1718
1717 if 'listkeys' in pullop.remotebundle2caps:
1719 if 'listkeys' in pullop.remotebundle2caps:
1718 if 'request-bookmarks' not in pullop.stepsdone:
1720 if 'request-bookmarks' not in pullop.stepsdone:
1719 # make sure to always includes bookmark data when migrating
1721 # make sure to always includes bookmark data when migrating
1720 # `hg incoming --bundle` to using this function.
1722 # `hg incoming --bundle` to using this function.
1721 pullop.stepsdone.add('request-bookmarks')
1723 pullop.stepsdone.add('request-bookmarks')
1722 kwargs.setdefault('listkeys', []).append('bookmarks')
1724 kwargs.setdefault('listkeys', []).append('bookmarks')
1723
1725
1724 # If this is a full pull / clone and the server supports the clone bundles
1726 # If this is a full pull / clone and the server supports the clone bundles
1725 # feature, tell the server whether we attempted a clone bundle. The
1727 # feature, tell the server whether we attempted a clone bundle. The
1726 # presence of this flag indicates the client supports clone bundles. This
1728 # presence of this flag indicates the client supports clone bundles. This
1727 # will enable the server to treat clients that support clone bundles
1729 # will enable the server to treat clients that support clone bundles
1728 # differently from those that don't.
1730 # differently from those that don't.
1729 if (pullop.remote.capable('clonebundles')
1731 if (pullop.remote.capable('clonebundles')
1730 and pullop.heads is None and list(pullop.common) == [nullid]):
1732 and pullop.heads is None and list(pullop.common) == [nullid]):
1731 kwargs['cbattempted'] = pullop.clonebundleattempted
1733 kwargs['cbattempted'] = pullop.clonebundleattempted
1732
1734
1733 if streaming:
1735 if streaming:
1734 pullop.repo.ui.status(_('streaming all changes\n'))
1736 pullop.repo.ui.status(_('streaming all changes\n'))
1735 elif not pullop.fetch:
1737 elif not pullop.fetch:
1736 pullop.repo.ui.status(_("no changes found\n"))
1738 pullop.repo.ui.status(_("no changes found\n"))
1737 pullop.cgresult = 0
1739 pullop.cgresult = 0
1738 else:
1740 else:
1739 if pullop.heads is None and list(pullop.common) == [nullid]:
1741 if pullop.heads is None and list(pullop.common) == [nullid]:
1740 pullop.repo.ui.status(_("requesting all changes\n"))
1742 pullop.repo.ui.status(_("requesting all changes\n"))
1741 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1743 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1742 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1744 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1743 if obsolete.commonversion(remoteversions) is not None:
1745 if obsolete.commonversion(remoteversions) is not None:
1744 kwargs['obsmarkers'] = True
1746 kwargs['obsmarkers'] = True
1745 pullop.stepsdone.add('obsmarkers')
1747 pullop.stepsdone.add('obsmarkers')
1746 _pullbundle2extraprepare(pullop, kwargs)
1748 _pullbundle2extraprepare(pullop, kwargs)
1747
1749
1748 with pullop.remote.commandexecutor() as e:
1750 with pullop.remote.commandexecutor() as e:
1749 args = dict(kwargs)
1751 args = dict(kwargs)
1750 args['source'] = 'pull'
1752 args['source'] = 'pull'
1751 bundle = e.callcommand('getbundle', args).result()
1753 bundle = e.callcommand('getbundle', args).result()
1752
1754
1753 try:
1755 try:
1754 op = bundle2.bundleoperation(pullop.repo, pullop.gettransaction,
1756 op = bundle2.bundleoperation(pullop.repo, pullop.gettransaction,
1755 source='pull')
1757 source='pull')
1756 op.modes['bookmarks'] = 'records'
1758 op.modes['bookmarks'] = 'records'
1757 bundle2.processbundle(pullop.repo, bundle, op=op)
1759 bundle2.processbundle(pullop.repo, bundle, op=op)
1758 except bundle2.AbortFromPart as exc:
1760 except bundle2.AbortFromPart as exc:
1759 pullop.repo.ui.status(_('remote: abort: %s\n') % exc)
1761 pullop.repo.ui.status(_('remote: abort: %s\n') % exc)
1760 raise error.Abort(_('pull failed on remote'), hint=exc.hint)
1762 raise error.Abort(_('pull failed on remote'), hint=exc.hint)
1761 except error.BundleValueError as exc:
1763 except error.BundleValueError as exc:
1762 raise error.Abort(_('missing support for %s') % exc)
1764 raise error.Abort(_('missing support for %s') % exc)
1763
1765
1764 if pullop.fetch:
1766 if pullop.fetch:
1765 pullop.cgresult = bundle2.combinechangegroupresults(op)
1767 pullop.cgresult = bundle2.combinechangegroupresults(op)
1766
1768
1767 # processing phases change
1769 # processing phases change
1768 for namespace, value in op.records['listkeys']:
1770 for namespace, value in op.records['listkeys']:
1769 if namespace == 'phases':
1771 if namespace == 'phases':
1770 _pullapplyphases(pullop, value)
1772 _pullapplyphases(pullop, value)
1771
1773
1772 # processing bookmark update
1774 # processing bookmark update
1773 if bookmarksrequested:
1775 if bookmarksrequested:
1774 books = {}
1776 books = {}
1775 for record in op.records['bookmarks']:
1777 for record in op.records['bookmarks']:
1776 books[record['bookmark']] = record["node"]
1778 books[record['bookmark']] = record["node"]
1777 pullop.remotebookmarks = books
1779 pullop.remotebookmarks = books
1778 else:
1780 else:
1779 for namespace, value in op.records['listkeys']:
1781 for namespace, value in op.records['listkeys']:
1780 if namespace == 'bookmarks':
1782 if namespace == 'bookmarks':
1781 pullop.remotebookmarks = bookmod.unhexlifybookmarks(value)
1783 pullop.remotebookmarks = bookmod.unhexlifybookmarks(value)
1782
1784
1783 # bookmark data were either already there or pulled in the bundle
1785 # bookmark data were either already there or pulled in the bundle
1784 if pullop.remotebookmarks is not None:
1786 if pullop.remotebookmarks is not None:
1785 _pullbookmarks(pullop)
1787 _pullbookmarks(pullop)
1786
1788
1787 def _pullbundle2extraprepare(pullop, kwargs):
1789 def _pullbundle2extraprepare(pullop, kwargs):
1788 """hook function so that extensions can extend the getbundle call"""
1790 """hook function so that extensions can extend the getbundle call"""
1789
1791
1790 def _pullchangeset(pullop):
1792 def _pullchangeset(pullop):
1791 """pull changeset from unbundle into the local repo"""
1793 """pull changeset from unbundle into the local repo"""
1792 # We delay the open of the transaction as late as possible so we
1794 # We delay the open of the transaction as late as possible so we
1793 # don't open transaction for nothing or you break future useful
1795 # don't open transaction for nothing or you break future useful
1794 # rollback call
1796 # rollback call
1795 if 'changegroup' in pullop.stepsdone:
1797 if 'changegroup' in pullop.stepsdone:
1796 return
1798 return
1797 pullop.stepsdone.add('changegroup')
1799 pullop.stepsdone.add('changegroup')
1798 if not pullop.fetch:
1800 if not pullop.fetch:
1799 pullop.repo.ui.status(_("no changes found\n"))
1801 pullop.repo.ui.status(_("no changes found\n"))
1800 pullop.cgresult = 0
1802 pullop.cgresult = 0
1801 return
1803 return
1802 tr = pullop.gettransaction()
1804 tr = pullop.gettransaction()
1803 if pullop.heads is None and list(pullop.common) == [nullid]:
1805 if pullop.heads is None and list(pullop.common) == [nullid]:
1804 pullop.repo.ui.status(_("requesting all changes\n"))
1806 pullop.repo.ui.status(_("requesting all changes\n"))
1805 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1807 elif pullop.heads is None and pullop.remote.capable('changegroupsubset'):
1806 # issue1320, avoid a race if remote changed after discovery
1808 # issue1320, avoid a race if remote changed after discovery
1807 pullop.heads = pullop.rheads
1809 pullop.heads = pullop.rheads
1808
1810
1809 if pullop.remote.capable('getbundle'):
1811 if pullop.remote.capable('getbundle'):
1810 # TODO: get bundlecaps from remote
1812 # TODO: get bundlecaps from remote
1811 cg = pullop.remote.getbundle('pull', common=pullop.common,
1813 cg = pullop.remote.getbundle('pull', common=pullop.common,
1812 heads=pullop.heads or pullop.rheads)
1814 heads=pullop.heads or pullop.rheads)
1813 elif pullop.heads is None:
1815 elif pullop.heads is None:
1814 with pullop.remote.commandexecutor() as e:
1816 with pullop.remote.commandexecutor() as e:
1815 cg = e.callcommand('changegroup', {
1817 cg = e.callcommand('changegroup', {
1816 'nodes': pullop.fetch,
1818 'nodes': pullop.fetch,
1817 'source': 'pull',
1819 'source': 'pull',
1818 }).result()
1820 }).result()
1819
1821
1820 elif not pullop.remote.capable('changegroupsubset'):
1822 elif not pullop.remote.capable('changegroupsubset'):
1821 raise error.Abort(_("partial pull cannot be done because "
1823 raise error.Abort(_("partial pull cannot be done because "
1822 "other repository doesn't support "
1824 "other repository doesn't support "
1823 "changegroupsubset."))
1825 "changegroupsubset."))
1824 else:
1826 else:
1825 with pullop.remote.commandexecutor() as e:
1827 with pullop.remote.commandexecutor() as e:
1826 cg = e.callcommand('changegroupsubset', {
1828 cg = e.callcommand('changegroupsubset', {
1827 'bases': pullop.fetch,
1829 'bases': pullop.fetch,
1828 'heads': pullop.heads,
1830 'heads': pullop.heads,
1829 'source': 'pull',
1831 'source': 'pull',
1830 }).result()
1832 }).result()
1831
1833
1832 bundleop = bundle2.applybundle(pullop.repo, cg, tr, 'pull',
1834 bundleop = bundle2.applybundle(pullop.repo, cg, tr, 'pull',
1833 pullop.remote.url())
1835 pullop.remote.url())
1834 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
1836 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
1835
1837
1836 def _pullphase(pullop):
1838 def _pullphase(pullop):
1837 # Get remote phases data from remote
1839 # Get remote phases data from remote
1838 if 'phases' in pullop.stepsdone:
1840 if 'phases' in pullop.stepsdone:
1839 return
1841 return
1840 remotephases = listkeys(pullop.remote, 'phases')
1842 remotephases = listkeys(pullop.remote, 'phases')
1841 _pullapplyphases(pullop, remotephases)
1843 _pullapplyphases(pullop, remotephases)
1842
1844
1843 def _pullapplyphases(pullop, remotephases):
1845 def _pullapplyphases(pullop, remotephases):
1844 """apply phase movement from observed remote state"""
1846 """apply phase movement from observed remote state"""
1845 if 'phases' in pullop.stepsdone:
1847 if 'phases' in pullop.stepsdone:
1846 return
1848 return
1847 pullop.stepsdone.add('phases')
1849 pullop.stepsdone.add('phases')
1848 publishing = bool(remotephases.get('publishing', False))
1850 publishing = bool(remotephases.get('publishing', False))
1849 if remotephases and not publishing:
1851 if remotephases and not publishing:
1850 # remote is new and non-publishing
1852 # remote is new and non-publishing
1851 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1853 pheads, _dr = phases.analyzeremotephases(pullop.repo,
1852 pullop.pulledsubset,
1854 pullop.pulledsubset,
1853 remotephases)
1855 remotephases)
1854 dheads = pullop.pulledsubset
1856 dheads = pullop.pulledsubset
1855 else:
1857 else:
1856 # Remote is old or publishing all common changesets
1858 # Remote is old or publishing all common changesets
1857 # should be seen as public
1859 # should be seen as public
1858 pheads = pullop.pulledsubset
1860 pheads = pullop.pulledsubset
1859 dheads = []
1861 dheads = []
1860 unfi = pullop.repo.unfiltered()
1862 unfi = pullop.repo.unfiltered()
1861 phase = unfi._phasecache.phase
1863 phase = unfi._phasecache.phase
1862 rev = unfi.changelog.nodemap.get
1864 rev = unfi.changelog.nodemap.get
1863 public = phases.public
1865 public = phases.public
1864 draft = phases.draft
1866 draft = phases.draft
1865
1867
1866 # exclude changesets already public locally and update the others
1868 # exclude changesets already public locally and update the others
1867 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1869 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
1868 if pheads:
1870 if pheads:
1869 tr = pullop.gettransaction()
1871 tr = pullop.gettransaction()
1870 phases.advanceboundary(pullop.repo, tr, public, pheads)
1872 phases.advanceboundary(pullop.repo, tr, public, pheads)
1871
1873
1872 # exclude changesets already draft locally and update the others
1874 # exclude changesets already draft locally and update the others
1873 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1875 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
1874 if dheads:
1876 if dheads:
1875 tr = pullop.gettransaction()
1877 tr = pullop.gettransaction()
1876 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1878 phases.advanceboundary(pullop.repo, tr, draft, dheads)
1877
1879
1878 def _pullbookmarks(pullop):
1880 def _pullbookmarks(pullop):
1879 """process the remote bookmark information to update the local one"""
1881 """process the remote bookmark information to update the local one"""
1880 if 'bookmarks' in pullop.stepsdone:
1882 if 'bookmarks' in pullop.stepsdone:
1881 return
1883 return
1882 pullop.stepsdone.add('bookmarks')
1884 pullop.stepsdone.add('bookmarks')
1883 repo = pullop.repo
1885 repo = pullop.repo
1884 remotebookmarks = pullop.remotebookmarks
1886 remotebookmarks = pullop.remotebookmarks
1885 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1887 bookmod.updatefromremote(repo.ui, repo, remotebookmarks,
1886 pullop.remote.url(),
1888 pullop.remote.url(),
1887 pullop.gettransaction,
1889 pullop.gettransaction,
1888 explicit=pullop.explicitbookmarks)
1890 explicit=pullop.explicitbookmarks)
1889
1891
1890 def _pullobsolete(pullop):
1892 def _pullobsolete(pullop):
1891 """utility function to pull obsolete markers from a remote
1893 """utility function to pull obsolete markers from a remote
1892
1894
1893 The `gettransaction` is function that return the pull transaction, creating
1895 The `gettransaction` is function that return the pull transaction, creating
1894 one if necessary. We return the transaction to inform the calling code that
1896 one if necessary. We return the transaction to inform the calling code that
1895 a new transaction have been created (when applicable).
1897 a new transaction have been created (when applicable).
1896
1898
1897 Exists mostly to allow overriding for experimentation purpose"""
1899 Exists mostly to allow overriding for experimentation purpose"""
1898 if 'obsmarkers' in pullop.stepsdone:
1900 if 'obsmarkers' in pullop.stepsdone:
1899 return
1901 return
1900 pullop.stepsdone.add('obsmarkers')
1902 pullop.stepsdone.add('obsmarkers')
1901 tr = None
1903 tr = None
1902 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1904 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1903 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1905 pullop.repo.ui.debug('fetching remote obsolete markers\n')
1904 remoteobs = listkeys(pullop.remote, 'obsolete')
1906 remoteobs = listkeys(pullop.remote, 'obsolete')
1905 if 'dump0' in remoteobs:
1907 if 'dump0' in remoteobs:
1906 tr = pullop.gettransaction()
1908 tr = pullop.gettransaction()
1907 markers = []
1909 markers = []
1908 for key in sorted(remoteobs, reverse=True):
1910 for key in sorted(remoteobs, reverse=True):
1909 if key.startswith('dump'):
1911 if key.startswith('dump'):
1910 data = util.b85decode(remoteobs[key])
1912 data = util.b85decode(remoteobs[key])
1911 version, newmarks = obsolete._readmarkers(data)
1913 version, newmarks = obsolete._readmarkers(data)
1912 markers += newmarks
1914 markers += newmarks
1913 if markers:
1915 if markers:
1914 pullop.repo.obsstore.add(tr, markers)
1916 pullop.repo.obsstore.add(tr, markers)
1915 pullop.repo.invalidatevolatilesets()
1917 pullop.repo.invalidatevolatilesets()
1916 return tr
1918 return tr
1917
1919
1918 def applynarrowacl(repo, kwargs):
1920 def applynarrowacl(repo, kwargs):
1919 """Apply narrow fetch access control.
1921 """Apply narrow fetch access control.
1920
1922
1921 This massages the named arguments for getbundle wire protocol commands
1923 This massages the named arguments for getbundle wire protocol commands
1922 so requested data is filtered through access control rules.
1924 so requested data is filtered through access control rules.
1923 """
1925 """
1924 ui = repo.ui
1926 ui = repo.ui
1925 # TODO this assumes existence of HTTP and is a layering violation.
1927 # TODO this assumes existence of HTTP and is a layering violation.
1926 username = ui.shortuser(ui.environ.get('REMOTE_USER') or ui.username())
1928 username = ui.shortuser(ui.environ.get('REMOTE_USER') or ui.username())
1927 user_includes = ui.configlist(
1929 user_includes = ui.configlist(
1928 _NARROWACL_SECTION, username + '.includes',
1930 _NARROWACL_SECTION, username + '.includes',
1929 ui.configlist(_NARROWACL_SECTION, 'default.includes'))
1931 ui.configlist(_NARROWACL_SECTION, 'default.includes'))
1930 user_excludes = ui.configlist(
1932 user_excludes = ui.configlist(
1931 _NARROWACL_SECTION, username + '.excludes',
1933 _NARROWACL_SECTION, username + '.excludes',
1932 ui.configlist(_NARROWACL_SECTION, 'default.excludes'))
1934 ui.configlist(_NARROWACL_SECTION, 'default.excludes'))
1933 if not user_includes:
1935 if not user_includes:
1934 raise error.Abort(_("{} configuration for user {} is empty")
1936 raise error.Abort(_("{} configuration for user {} is empty")
1935 .format(_NARROWACL_SECTION, username))
1937 .format(_NARROWACL_SECTION, username))
1936
1938
1937 user_includes = [
1939 user_includes = [
1938 'path:.' if p == '*' else 'path:' + p for p in user_includes]
1940 'path:.' if p == '*' else 'path:' + p for p in user_includes]
1939 user_excludes = [
1941 user_excludes = [
1940 'path:.' if p == '*' else 'path:' + p for p in user_excludes]
1942 'path:.' if p == '*' else 'path:' + p for p in user_excludes]
1941
1943
1942 req_includes = set(kwargs.get(r'includepats', []))
1944 req_includes = set(kwargs.get(r'includepats', []))
1943 req_excludes = set(kwargs.get(r'excludepats', []))
1945 req_excludes = set(kwargs.get(r'excludepats', []))
1944
1946
1945 req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns(
1947 req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns(
1946 req_includes, req_excludes, user_includes, user_excludes)
1948 req_includes, req_excludes, user_includes, user_excludes)
1947
1949
1948 if invalid_includes:
1950 if invalid_includes:
1949 raise error.Abort(
1951 raise error.Abort(
1950 _("The following includes are not accessible for {}: {}")
1952 _("The following includes are not accessible for {}: {}")
1951 .format(username, invalid_includes))
1953 .format(username, invalid_includes))
1952
1954
1953 new_args = {}
1955 new_args = {}
1954 new_args.update(kwargs)
1956 new_args.update(kwargs)
1955 new_args[r'narrow'] = True
1957 new_args[r'narrow'] = True
1956 new_args[r'narrow_acl'] = True
1958 new_args[r'narrow_acl'] = True
1957 new_args[r'includepats'] = req_includes
1959 new_args[r'includepats'] = req_includes
1958 if req_excludes:
1960 if req_excludes:
1959 new_args[r'excludepats'] = req_excludes
1961 new_args[r'excludepats'] = req_excludes
1960
1962
1961 return new_args
1963 return new_args
1962
1964
1963 def _computeellipsis(repo, common, heads, known, match, depth=None):
1965 def _computeellipsis(repo, common, heads, known, match, depth=None):
1964 """Compute the shape of a narrowed DAG.
1966 """Compute the shape of a narrowed DAG.
1965
1967
1966 Args:
1968 Args:
1967 repo: The repository we're transferring.
1969 repo: The repository we're transferring.
1968 common: The roots of the DAG range we're transferring.
1970 common: The roots of the DAG range we're transferring.
1969 May be just [nullid], which means all ancestors of heads.
1971 May be just [nullid], which means all ancestors of heads.
1970 heads: The heads of the DAG range we're transferring.
1972 heads: The heads of the DAG range we're transferring.
1971 match: The narrowmatcher that allows us to identify relevant changes.
1973 match: The narrowmatcher that allows us to identify relevant changes.
1972 depth: If not None, only consider nodes to be full nodes if they are at
1974 depth: If not None, only consider nodes to be full nodes if they are at
1973 most depth changesets away from one of heads.
1975 most depth changesets away from one of heads.
1974
1976
1975 Returns:
1977 Returns:
1976 A tuple of (visitnodes, relevant_nodes, ellipsisroots) where:
1978 A tuple of (visitnodes, relevant_nodes, ellipsisroots) where:
1977
1979
1978 visitnodes: The list of nodes (either full or ellipsis) which
1980 visitnodes: The list of nodes (either full or ellipsis) which
1979 need to be sent to the client.
1981 need to be sent to the client.
1980 relevant_nodes: The set of changelog nodes which change a file inside
1982 relevant_nodes: The set of changelog nodes which change a file inside
1981 the narrowspec. The client needs these as non-ellipsis nodes.
1983 the narrowspec. The client needs these as non-ellipsis nodes.
1982 ellipsisroots: A dict of {rev: parents} that is used in
1984 ellipsisroots: A dict of {rev: parents} that is used in
1983 narrowchangegroup to produce ellipsis nodes with the
1985 narrowchangegroup to produce ellipsis nodes with the
1984 correct parents.
1986 correct parents.
1985 """
1987 """
1986 cl = repo.changelog
1988 cl = repo.changelog
1987 mfl = repo.manifestlog
1989 mfl = repo.manifestlog
1988
1990
1989 clrev = cl.rev
1991 clrev = cl.rev
1990
1992
1991 commonrevs = {clrev(n) for n in common} | {nullrev}
1993 commonrevs = {clrev(n) for n in common} | {nullrev}
1992 headsrevs = {clrev(n) for n in heads}
1994 headsrevs = {clrev(n) for n in heads}
1993
1995
1994 if depth:
1996 if depth:
1995 revdepth = {h: 0 for h in headsrevs}
1997 revdepth = {h: 0 for h in headsrevs}
1996
1998
1997 ellipsisheads = collections.defaultdict(set)
1999 ellipsisheads = collections.defaultdict(set)
1998 ellipsisroots = collections.defaultdict(set)
2000 ellipsisroots = collections.defaultdict(set)
1999
2001
2000 def addroot(head, curchange):
2002 def addroot(head, curchange):
2001 """Add a root to an ellipsis head, splitting heads with 3 roots."""
2003 """Add a root to an ellipsis head, splitting heads with 3 roots."""
2002 ellipsisroots[head].add(curchange)
2004 ellipsisroots[head].add(curchange)
2003 # Recursively split ellipsis heads with 3 roots by finding the
2005 # Recursively split ellipsis heads with 3 roots by finding the
2004 # roots' youngest common descendant which is an elided merge commit.
2006 # roots' youngest common descendant which is an elided merge commit.
2005 # That descendant takes 2 of the 3 roots as its own, and becomes a
2007 # That descendant takes 2 of the 3 roots as its own, and becomes a
2006 # root of the head.
2008 # root of the head.
2007 while len(ellipsisroots[head]) > 2:
2009 while len(ellipsisroots[head]) > 2:
2008 child, roots = splithead(head)
2010 child, roots = splithead(head)
2009 splitroots(head, child, roots)
2011 splitroots(head, child, roots)
2010 head = child # Recurse in case we just added a 3rd root
2012 head = child # Recurse in case we just added a 3rd root
2011
2013
2012 def splitroots(head, child, roots):
2014 def splitroots(head, child, roots):
2013 ellipsisroots[head].difference_update(roots)
2015 ellipsisroots[head].difference_update(roots)
2014 ellipsisroots[head].add(child)
2016 ellipsisroots[head].add(child)
2015 ellipsisroots[child].update(roots)
2017 ellipsisroots[child].update(roots)
2016 ellipsisroots[child].discard(child)
2018 ellipsisroots[child].discard(child)
2017
2019
2018 def splithead(head):
2020 def splithead(head):
2019 r1, r2, r3 = sorted(ellipsisroots[head])
2021 r1, r2, r3 = sorted(ellipsisroots[head])
2020 for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)):
2022 for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)):
2021 mid = repo.revs('sort(merge() & %d::%d & %d::%d, -rev)',
2023 mid = repo.revs('sort(merge() & %d::%d & %d::%d, -rev)',
2022 nr1, head, nr2, head)
2024 nr1, head, nr2, head)
2023 for j in mid:
2025 for j in mid:
2024 if j == nr2:
2026 if j == nr2:
2025 return nr2, (nr1, nr2)
2027 return nr2, (nr1, nr2)
2026 if j not in ellipsisroots or len(ellipsisroots[j]) < 2:
2028 if j not in ellipsisroots or len(ellipsisroots[j]) < 2:
2027 return j, (nr1, nr2)
2029 return j, (nr1, nr2)
2028 raise error.Abort(_('Failed to split up ellipsis node! head: %d, '
2030 raise error.Abort(_('Failed to split up ellipsis node! head: %d, '
2029 'roots: %d %d %d') % (head, r1, r2, r3))
2031 'roots: %d %d %d') % (head, r1, r2, r3))
2030
2032
2031 missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs))
2033 missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs))
2032 visit = reversed(missing)
2034 visit = reversed(missing)
2033 relevant_nodes = set()
2035 relevant_nodes = set()
2034 visitnodes = [cl.node(m) for m in missing]
2036 visitnodes = [cl.node(m) for m in missing]
2035 required = set(headsrevs) | known
2037 required = set(headsrevs) | known
2036 for rev in visit:
2038 for rev in visit:
2037 clrev = cl.changelogrevision(rev)
2039 clrev = cl.changelogrevision(rev)
2038 ps = [prev for prev in cl.parentrevs(rev) if prev != nullrev]
2040 ps = [prev for prev in cl.parentrevs(rev) if prev != nullrev]
2039 if depth is not None:
2041 if depth is not None:
2040 curdepth = revdepth[rev]
2042 curdepth = revdepth[rev]
2041 for p in ps:
2043 for p in ps:
2042 revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1))
2044 revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1))
2043 needed = False
2045 needed = False
2044 shallow_enough = depth is None or revdepth[rev] <= depth
2046 shallow_enough = depth is None or revdepth[rev] <= depth
2045 if shallow_enough:
2047 if shallow_enough:
2046 curmf = mfl[clrev.manifest].read()
2048 curmf = mfl[clrev.manifest].read()
2047 if ps:
2049 if ps:
2048 # We choose to not trust the changed files list in
2050 # We choose to not trust the changed files list in
2049 # changesets because it's not always correct. TODO: could
2051 # changesets because it's not always correct. TODO: could
2050 # we trust it for the non-merge case?
2052 # we trust it for the non-merge case?
2051 p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read()
2053 p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read()
2052 needed = bool(curmf.diff(p1mf, match))
2054 needed = bool(curmf.diff(p1mf, match))
2053 if not needed and len(ps) > 1:
2055 if not needed and len(ps) > 1:
2054 # For merge changes, the list of changed files is not
2056 # For merge changes, the list of changed files is not
2055 # helpful, since we need to emit the merge if a file
2057 # helpful, since we need to emit the merge if a file
2056 # in the narrow spec has changed on either side of the
2058 # in the narrow spec has changed on either side of the
2057 # merge. As a result, we do a manifest diff to check.
2059 # merge. As a result, we do a manifest diff to check.
2058 p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read()
2060 p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read()
2059 needed = bool(curmf.diff(p2mf, match))
2061 needed = bool(curmf.diff(p2mf, match))
2060 else:
2062 else:
2061 # For a root node, we need to include the node if any
2063 # For a root node, we need to include the node if any
2062 # files in the node match the narrowspec.
2064 # files in the node match the narrowspec.
2063 needed = any(curmf.walk(match))
2065 needed = any(curmf.walk(match))
2064
2066
2065 if needed:
2067 if needed:
2066 for head in ellipsisheads[rev]:
2068 for head in ellipsisheads[rev]:
2067 addroot(head, rev)
2069 addroot(head, rev)
2068 for p in ps:
2070 for p in ps:
2069 required.add(p)
2071 required.add(p)
2070 relevant_nodes.add(cl.node(rev))
2072 relevant_nodes.add(cl.node(rev))
2071 else:
2073 else:
2072 if not ps:
2074 if not ps:
2073 ps = [nullrev]
2075 ps = [nullrev]
2074 if rev in required:
2076 if rev in required:
2075 for head in ellipsisheads[rev]:
2077 for head in ellipsisheads[rev]:
2076 addroot(head, rev)
2078 addroot(head, rev)
2077 for p in ps:
2079 for p in ps:
2078 ellipsisheads[p].add(rev)
2080 ellipsisheads[p].add(rev)
2079 else:
2081 else:
2080 for p in ps:
2082 for p in ps:
2081 ellipsisheads[p] |= ellipsisheads[rev]
2083 ellipsisheads[p] |= ellipsisheads[rev]
2082
2084
2083 # add common changesets as roots of their reachable ellipsis heads
2085 # add common changesets as roots of their reachable ellipsis heads
2084 for c in commonrevs:
2086 for c in commonrevs:
2085 for head in ellipsisheads[c]:
2087 for head in ellipsisheads[c]:
2086 addroot(head, c)
2088 addroot(head, c)
2087 return visitnodes, relevant_nodes, ellipsisroots
2089 return visitnodes, relevant_nodes, ellipsisroots
2088
2090
2089 def caps20to10(repo, role):
2091 def caps20to10(repo, role):
2090 """return a set with appropriate options to use bundle20 during getbundle"""
2092 """return a set with appropriate options to use bundle20 during getbundle"""
2091 caps = {'HG20'}
2093 caps = {'HG20'}
2092 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=role))
2094 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=role))
2093 caps.add('bundle2=' + urlreq.quote(capsblob))
2095 caps.add('bundle2=' + urlreq.quote(capsblob))
2094 return caps
2096 return caps
2095
2097
2096 # List of names of steps to perform for a bundle2 for getbundle, order matters.
2098 # List of names of steps to perform for a bundle2 for getbundle, order matters.
2097 getbundle2partsorder = []
2099 getbundle2partsorder = []
2098
2100
2099 # Mapping between step name and function
2101 # Mapping between step name and function
2100 #
2102 #
2101 # This exists to help extensions wrap steps if necessary
2103 # This exists to help extensions wrap steps if necessary
2102 getbundle2partsmapping = {}
2104 getbundle2partsmapping = {}
2103
2105
2104 def getbundle2partsgenerator(stepname, idx=None):
2106 def getbundle2partsgenerator(stepname, idx=None):
2105 """decorator for function generating bundle2 part for getbundle
2107 """decorator for function generating bundle2 part for getbundle
2106
2108
2107 The function is added to the step -> function mapping and appended to the
2109 The function is added to the step -> function mapping and appended to the
2108 list of steps. Beware that decorated functions will be added in order
2110 list of steps. Beware that decorated functions will be added in order
2109 (this may matter).
2111 (this may matter).
2110
2112
2111 You can only use this decorator for new steps, if you want to wrap a step
2113 You can only use this decorator for new steps, if you want to wrap a step
2112 from an extension, attack the getbundle2partsmapping dictionary directly."""
2114 from an extension, attack the getbundle2partsmapping dictionary directly."""
2113 def dec(func):
2115 def dec(func):
2114 assert stepname not in getbundle2partsmapping
2116 assert stepname not in getbundle2partsmapping
2115 getbundle2partsmapping[stepname] = func
2117 getbundle2partsmapping[stepname] = func
2116 if idx is None:
2118 if idx is None:
2117 getbundle2partsorder.append(stepname)
2119 getbundle2partsorder.append(stepname)
2118 else:
2120 else:
2119 getbundle2partsorder.insert(idx, stepname)
2121 getbundle2partsorder.insert(idx, stepname)
2120 return func
2122 return func
2121 return dec
2123 return dec
2122
2124
2123 def bundle2requested(bundlecaps):
2125 def bundle2requested(bundlecaps):
2124 if bundlecaps is not None:
2126 if bundlecaps is not None:
2125 return any(cap.startswith('HG2') for cap in bundlecaps)
2127 return any(cap.startswith('HG2') for cap in bundlecaps)
2126 return False
2128 return False
2127
2129
2128 def getbundlechunks(repo, source, heads=None, common=None, bundlecaps=None,
2130 def getbundlechunks(repo, source, heads=None, common=None, bundlecaps=None,
2129 **kwargs):
2131 **kwargs):
2130 """Return chunks constituting a bundle's raw data.
2132 """Return chunks constituting a bundle's raw data.
2131
2133
2132 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
2134 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
2133 passed.
2135 passed.
2134
2136
2135 Returns a 2-tuple of a dict with metadata about the generated bundle
2137 Returns a 2-tuple of a dict with metadata about the generated bundle
2136 and an iterator over raw chunks (of varying sizes).
2138 and an iterator over raw chunks (of varying sizes).
2137 """
2139 """
2138 kwargs = pycompat.byteskwargs(kwargs)
2140 kwargs = pycompat.byteskwargs(kwargs)
2139 info = {}
2141 info = {}
2140 usebundle2 = bundle2requested(bundlecaps)
2142 usebundle2 = bundle2requested(bundlecaps)
2141 # bundle10 case
2143 # bundle10 case
2142 if not usebundle2:
2144 if not usebundle2:
2143 if bundlecaps and not kwargs.get('cg', True):
2145 if bundlecaps and not kwargs.get('cg', True):
2144 raise ValueError(_('request for bundle10 must include changegroup'))
2146 raise ValueError(_('request for bundle10 must include changegroup'))
2145
2147
2146 if kwargs:
2148 if kwargs:
2147 raise ValueError(_('unsupported getbundle arguments: %s')
2149 raise ValueError(_('unsupported getbundle arguments: %s')
2148 % ', '.join(sorted(kwargs.keys())))
2150 % ', '.join(sorted(kwargs.keys())))
2149 outgoing = _computeoutgoing(repo, heads, common)
2151 outgoing = _computeoutgoing(repo, heads, common)
2150 info['bundleversion'] = 1
2152 info['bundleversion'] = 1
2151 return info, changegroup.makestream(repo, outgoing, '01', source,
2153 return info, changegroup.makestream(repo, outgoing, '01', source,
2152 bundlecaps=bundlecaps)
2154 bundlecaps=bundlecaps)
2153
2155
2154 # bundle20 case
2156 # bundle20 case
2155 info['bundleversion'] = 2
2157 info['bundleversion'] = 2
2156 b2caps = {}
2158 b2caps = {}
2157 for bcaps in bundlecaps:
2159 for bcaps in bundlecaps:
2158 if bcaps.startswith('bundle2='):
2160 if bcaps.startswith('bundle2='):
2159 blob = urlreq.unquote(bcaps[len('bundle2='):])
2161 blob = urlreq.unquote(bcaps[len('bundle2='):])
2160 b2caps.update(bundle2.decodecaps(blob))
2162 b2caps.update(bundle2.decodecaps(blob))
2161 bundler = bundle2.bundle20(repo.ui, b2caps)
2163 bundler = bundle2.bundle20(repo.ui, b2caps)
2162
2164
2163 kwargs['heads'] = heads
2165 kwargs['heads'] = heads
2164 kwargs['common'] = common
2166 kwargs['common'] = common
2165
2167
2166 for name in getbundle2partsorder:
2168 for name in getbundle2partsorder:
2167 func = getbundle2partsmapping[name]
2169 func = getbundle2partsmapping[name]
2168 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
2170 func(bundler, repo, source, bundlecaps=bundlecaps, b2caps=b2caps,
2169 **pycompat.strkwargs(kwargs))
2171 **pycompat.strkwargs(kwargs))
2170
2172
2171 info['prefercompressed'] = bundler.prefercompressed
2173 info['prefercompressed'] = bundler.prefercompressed
2172
2174
2173 return info, bundler.getchunks()
2175 return info, bundler.getchunks()
2174
2176
2175 @getbundle2partsgenerator('stream2')
2177 @getbundle2partsgenerator('stream2')
2176 def _getbundlestream2(bundler, repo, *args, **kwargs):
2178 def _getbundlestream2(bundler, repo, *args, **kwargs):
2177 return bundle2.addpartbundlestream2(bundler, repo, **kwargs)
2179 return bundle2.addpartbundlestream2(bundler, repo, **kwargs)
2178
2180
2179 @getbundle2partsgenerator('changegroup')
2181 @getbundle2partsgenerator('changegroup')
2180 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
2182 def _getbundlechangegrouppart(bundler, repo, source, bundlecaps=None,
2181 b2caps=None, heads=None, common=None, **kwargs):
2183 b2caps=None, heads=None, common=None, **kwargs):
2182 """add a changegroup part to the requested bundle"""
2184 """add a changegroup part to the requested bundle"""
2183 if not kwargs.get(r'cg', True):
2185 if not kwargs.get(r'cg', True):
2184 return
2186 return
2185
2187
2186 version = '01'
2188 version = '01'
2187 cgversions = b2caps.get('changegroup')
2189 cgversions = b2caps.get('changegroup')
2188 if cgversions: # 3.1 and 3.2 ship with an empty value
2190 if cgversions: # 3.1 and 3.2 ship with an empty value
2189 cgversions = [v for v in cgversions
2191 cgversions = [v for v in cgversions
2190 if v in changegroup.supportedoutgoingversions(repo)]
2192 if v in changegroup.supportedoutgoingversions(repo)]
2191 if not cgversions:
2193 if not cgversions:
2192 raise error.Abort(_('no common changegroup version'))
2194 raise error.Abort(_('no common changegroup version'))
2193 version = max(cgversions)
2195 version = max(cgversions)
2194
2196
2195 outgoing = _computeoutgoing(repo, heads, common)
2197 outgoing = _computeoutgoing(repo, heads, common)
2196 if not outgoing.missing:
2198 if not outgoing.missing:
2197 return
2199 return
2198
2200
2199 if kwargs.get(r'narrow', False):
2201 if kwargs.get(r'narrow', False):
2200 include = sorted(filter(bool, kwargs.get(r'includepats', [])))
2202 include = sorted(filter(bool, kwargs.get(r'includepats', [])))
2201 exclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
2203 exclude = sorted(filter(bool, kwargs.get(r'excludepats', [])))
2202 matcher = narrowspec.match(repo.root, include=include, exclude=exclude)
2204 matcher = narrowspec.match(repo.root, include=include, exclude=exclude)
2203 else:
2205 else:
2204 matcher = None
2206 matcher = None
2205
2207
2206 cgstream = changegroup.makestream(repo, outgoing, version, source,
2208 cgstream = changegroup.makestream(repo, outgoing, version, source,
2207 bundlecaps=bundlecaps, matcher=matcher)
2209 bundlecaps=bundlecaps, matcher=matcher)
2208
2210
2209 part = bundler.newpart('changegroup', data=cgstream)
2211 part = bundler.newpart('changegroup', data=cgstream)
2210 if cgversions:
2212 if cgversions:
2211 part.addparam('version', version)
2213 part.addparam('version', version)
2212
2214
2213 part.addparam('nbchanges', '%d' % len(outgoing.missing),
2215 part.addparam('nbchanges', '%d' % len(outgoing.missing),
2214 mandatory=False)
2216 mandatory=False)
2215
2217
2216 if 'treemanifest' in repo.requirements:
2218 if 'treemanifest' in repo.requirements:
2217 part.addparam('treemanifest', '1')
2219 part.addparam('treemanifest', '1')
2218
2220
2219 if (kwargs.get(r'narrow', False) and kwargs.get(r'narrow_acl', False)
2221 if (kwargs.get(r'narrow', False) and kwargs.get(r'narrow_acl', False)
2220 and (include or exclude)):
2222 and (include or exclude)):
2221 # this is mandatory because otherwise ACL clients won't work
2223 # this is mandatory because otherwise ACL clients won't work
2222 narrowspecpart = bundler.newpart('Narrow:responsespec')
2224 narrowspecpart = bundler.newpart('Narrow:responsespec')
2223 narrowspecpart.data = '%s\0%s' % ('\n'.join(include),
2225 narrowspecpart.data = '%s\0%s' % ('\n'.join(include),
2224 '\n'.join(exclude))
2226 '\n'.join(exclude))
2225
2227
2226 @getbundle2partsgenerator('bookmarks')
2228 @getbundle2partsgenerator('bookmarks')
2227 def _getbundlebookmarkpart(bundler, repo, source, bundlecaps=None,
2229 def _getbundlebookmarkpart(bundler, repo, source, bundlecaps=None,
2228 b2caps=None, **kwargs):
2230 b2caps=None, **kwargs):
2229 """add a bookmark part to the requested bundle"""
2231 """add a bookmark part to the requested bundle"""
2230 if not kwargs.get(r'bookmarks', False):
2232 if not kwargs.get(r'bookmarks', False):
2231 return
2233 return
2232 if 'bookmarks' not in b2caps:
2234 if 'bookmarks' not in b2caps:
2233 raise error.Abort(_('no common bookmarks exchange method'))
2235 raise error.Abort(_('no common bookmarks exchange method'))
2234 books = bookmod.listbinbookmarks(repo)
2236 books = bookmod.listbinbookmarks(repo)
2235 data = bookmod.binaryencode(books)
2237 data = bookmod.binaryencode(books)
2236 if data:
2238 if data:
2237 bundler.newpart('bookmarks', data=data)
2239 bundler.newpart('bookmarks', data=data)
2238
2240
2239 @getbundle2partsgenerator('listkeys')
2241 @getbundle2partsgenerator('listkeys')
2240 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
2242 def _getbundlelistkeysparts(bundler, repo, source, bundlecaps=None,
2241 b2caps=None, **kwargs):
2243 b2caps=None, **kwargs):
2242 """add parts containing listkeys namespaces to the requested bundle"""
2244 """add parts containing listkeys namespaces to the requested bundle"""
2243 listkeys = kwargs.get(r'listkeys', ())
2245 listkeys = kwargs.get(r'listkeys', ())
2244 for namespace in listkeys:
2246 for namespace in listkeys:
2245 part = bundler.newpart('listkeys')
2247 part = bundler.newpart('listkeys')
2246 part.addparam('namespace', namespace)
2248 part.addparam('namespace', namespace)
2247 keys = repo.listkeys(namespace).items()
2249 keys = repo.listkeys(namespace).items()
2248 part.data = pushkey.encodekeys(keys)
2250 part.data = pushkey.encodekeys(keys)
2249
2251
2250 @getbundle2partsgenerator('obsmarkers')
2252 @getbundle2partsgenerator('obsmarkers')
2251 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
2253 def _getbundleobsmarkerpart(bundler, repo, source, bundlecaps=None,
2252 b2caps=None, heads=None, **kwargs):
2254 b2caps=None, heads=None, **kwargs):
2253 """add an obsolescence markers part to the requested bundle"""
2255 """add an obsolescence markers part to the requested bundle"""
2254 if kwargs.get(r'obsmarkers', False):
2256 if kwargs.get(r'obsmarkers', False):
2255 if heads is None:
2257 if heads is None:
2256 heads = repo.heads()
2258 heads = repo.heads()
2257 subset = [c.node() for c in repo.set('::%ln', heads)]
2259 subset = [c.node() for c in repo.set('::%ln', heads)]
2258 markers = repo.obsstore.relevantmarkers(subset)
2260 markers = repo.obsstore.relevantmarkers(subset)
2259 markers = sorted(markers)
2261 markers = sorted(markers)
2260 bundle2.buildobsmarkerspart(bundler, markers)
2262 bundle2.buildobsmarkerspart(bundler, markers)
2261
2263
2262 @getbundle2partsgenerator('phases')
2264 @getbundle2partsgenerator('phases')
2263 def _getbundlephasespart(bundler, repo, source, bundlecaps=None,
2265 def _getbundlephasespart(bundler, repo, source, bundlecaps=None,
2264 b2caps=None, heads=None, **kwargs):
2266 b2caps=None, heads=None, **kwargs):
2265 """add phase heads part to the requested bundle"""
2267 """add phase heads part to the requested bundle"""
2266 if kwargs.get(r'phases', False):
2268 if kwargs.get(r'phases', False):
2267 if not 'heads' in b2caps.get('phases'):
2269 if not 'heads' in b2caps.get('phases'):
2268 raise error.Abort(_('no common phases exchange method'))
2270 raise error.Abort(_('no common phases exchange method'))
2269 if heads is None:
2271 if heads is None:
2270 heads = repo.heads()
2272 heads = repo.heads()
2271
2273
2272 headsbyphase = collections.defaultdict(set)
2274 headsbyphase = collections.defaultdict(set)
2273 if repo.publishing():
2275 if repo.publishing():
2274 headsbyphase[phases.public] = heads
2276 headsbyphase[phases.public] = heads
2275 else:
2277 else:
2276 # find the appropriate heads to move
2278 # find the appropriate heads to move
2277
2279
2278 phase = repo._phasecache.phase
2280 phase = repo._phasecache.phase
2279 node = repo.changelog.node
2281 node = repo.changelog.node
2280 rev = repo.changelog.rev
2282 rev = repo.changelog.rev
2281 for h in heads:
2283 for h in heads:
2282 headsbyphase[phase(repo, rev(h))].add(h)
2284 headsbyphase[phase(repo, rev(h))].add(h)
2283 seenphases = list(headsbyphase.keys())
2285 seenphases = list(headsbyphase.keys())
2284
2286
2285 # We do not handle anything but public and draft phase for now)
2287 # We do not handle anything but public and draft phase for now)
2286 if seenphases:
2288 if seenphases:
2287 assert max(seenphases) <= phases.draft
2289 assert max(seenphases) <= phases.draft
2288
2290
2289 # if client is pulling non-public changesets, we need to find
2291 # if client is pulling non-public changesets, we need to find
2290 # intermediate public heads.
2292 # intermediate public heads.
2291 draftheads = headsbyphase.get(phases.draft, set())
2293 draftheads = headsbyphase.get(phases.draft, set())
2292 if draftheads:
2294 if draftheads:
2293 publicheads = headsbyphase.get(phases.public, set())
2295 publicheads = headsbyphase.get(phases.public, set())
2294
2296
2295 revset = 'heads(only(%ln, %ln) and public())'
2297 revset = 'heads(only(%ln, %ln) and public())'
2296 extraheads = repo.revs(revset, draftheads, publicheads)
2298 extraheads = repo.revs(revset, draftheads, publicheads)
2297 for r in extraheads:
2299 for r in extraheads:
2298 headsbyphase[phases.public].add(node(r))
2300 headsbyphase[phases.public].add(node(r))
2299
2301
2300 # transform data in a format used by the encoding function
2302 # transform data in a format used by the encoding function
2301 phasemapping = []
2303 phasemapping = []
2302 for phase in phases.allphases:
2304 for phase in phases.allphases:
2303 phasemapping.append(sorted(headsbyphase[phase]))
2305 phasemapping.append(sorted(headsbyphase[phase]))
2304
2306
2305 # generate the actual part
2307 # generate the actual part
2306 phasedata = phases.binaryencode(phasemapping)
2308 phasedata = phases.binaryencode(phasemapping)
2307 bundler.newpart('phase-heads', data=phasedata)
2309 bundler.newpart('phase-heads', data=phasedata)
2308
2310
2309 @getbundle2partsgenerator('hgtagsfnodes')
2311 @getbundle2partsgenerator('hgtagsfnodes')
2310 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
2312 def _getbundletagsfnodes(bundler, repo, source, bundlecaps=None,
2311 b2caps=None, heads=None, common=None,
2313 b2caps=None, heads=None, common=None,
2312 **kwargs):
2314 **kwargs):
2313 """Transfer the .hgtags filenodes mapping.
2315 """Transfer the .hgtags filenodes mapping.
2314
2316
2315 Only values for heads in this bundle will be transferred.
2317 Only values for heads in this bundle will be transferred.
2316
2318
2317 The part data consists of pairs of 20 byte changeset node and .hgtags
2319 The part data consists of pairs of 20 byte changeset node and .hgtags
2318 filenodes raw values.
2320 filenodes raw values.
2319 """
2321 """
2320 # Don't send unless:
2322 # Don't send unless:
2321 # - changeset are being exchanged,
2323 # - changeset are being exchanged,
2322 # - the client supports it.
2324 # - the client supports it.
2323 if not (kwargs.get(r'cg', True) and 'hgtagsfnodes' in b2caps):
2325 if not (kwargs.get(r'cg', True) and 'hgtagsfnodes' in b2caps):
2324 return
2326 return
2325
2327
2326 outgoing = _computeoutgoing(repo, heads, common)
2328 outgoing = _computeoutgoing(repo, heads, common)
2327 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
2329 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
2328
2330
2329 @getbundle2partsgenerator('cache:rev-branch-cache')
2331 @getbundle2partsgenerator('cache:rev-branch-cache')
2330 def _getbundlerevbranchcache(bundler, repo, source, bundlecaps=None,
2332 def _getbundlerevbranchcache(bundler, repo, source, bundlecaps=None,
2331 b2caps=None, heads=None, common=None,
2333 b2caps=None, heads=None, common=None,
2332 **kwargs):
2334 **kwargs):
2333 """Transfer the rev-branch-cache mapping
2335 """Transfer the rev-branch-cache mapping
2334
2336
2335 The payload is a series of data related to each branch
2337 The payload is a series of data related to each branch
2336
2338
2337 1) branch name length
2339 1) branch name length
2338 2) number of open heads
2340 2) number of open heads
2339 3) number of closed heads
2341 3) number of closed heads
2340 4) open heads nodes
2342 4) open heads nodes
2341 5) closed heads nodes
2343 5) closed heads nodes
2342 """
2344 """
2343 # Don't send unless:
2345 # Don't send unless:
2344 # - changeset are being exchanged,
2346 # - changeset are being exchanged,
2345 # - the client supports it.
2347 # - the client supports it.
2346 # - narrow bundle isn't in play (not currently compatible).
2348 # - narrow bundle isn't in play (not currently compatible).
2347 if (not kwargs.get(r'cg', True)
2349 if (not kwargs.get(r'cg', True)
2348 or 'rev-branch-cache' not in b2caps
2350 or 'rev-branch-cache' not in b2caps
2349 or kwargs.get(r'narrow', False)
2351 or kwargs.get(r'narrow', False)
2350 or repo.ui.has_section(_NARROWACL_SECTION)):
2352 or repo.ui.has_section(_NARROWACL_SECTION)):
2351 return
2353 return
2352
2354
2353 outgoing = _computeoutgoing(repo, heads, common)
2355 outgoing = _computeoutgoing(repo, heads, common)
2354 bundle2.addpartrevbranchcache(repo, bundler, outgoing)
2356 bundle2.addpartrevbranchcache(repo, bundler, outgoing)
2355
2357
2356 def check_heads(repo, their_heads, context):
2358 def check_heads(repo, their_heads, context):
2357 """check if the heads of a repo have been modified
2359 """check if the heads of a repo have been modified
2358
2360
2359 Used by peer for unbundling.
2361 Used by peer for unbundling.
2360 """
2362 """
2361 heads = repo.heads()
2363 heads = repo.heads()
2362 heads_hash = hashlib.sha1(''.join(sorted(heads))).digest()
2364 heads_hash = hashlib.sha1(''.join(sorted(heads))).digest()
2363 if not (their_heads == ['force'] or their_heads == heads or
2365 if not (their_heads == ['force'] or their_heads == heads or
2364 their_heads == ['hashed', heads_hash]):
2366 their_heads == ['hashed', heads_hash]):
2365 # someone else committed/pushed/unbundled while we
2367 # someone else committed/pushed/unbundled while we
2366 # were transferring data
2368 # were transferring data
2367 raise error.PushRaced('repository changed while %s - '
2369 raise error.PushRaced('repository changed while %s - '
2368 'please try again' % context)
2370 'please try again' % context)
2369
2371
2370 def unbundle(repo, cg, heads, source, url):
2372 def unbundle(repo, cg, heads, source, url):
2371 """Apply a bundle to a repo.
2373 """Apply a bundle to a repo.
2372
2374
2373 this function makes sure the repo is locked during the application and have
2375 this function makes sure the repo is locked during the application and have
2374 mechanism to check that no push race occurred between the creation of the
2376 mechanism to check that no push race occurred between the creation of the
2375 bundle and its application.
2377 bundle and its application.
2376
2378
2377 If the push was raced as PushRaced exception is raised."""
2379 If the push was raced as PushRaced exception is raised."""
2378 r = 0
2380 r = 0
2379 # need a transaction when processing a bundle2 stream
2381 # need a transaction when processing a bundle2 stream
2380 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
2382 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
2381 lockandtr = [None, None, None]
2383 lockandtr = [None, None, None]
2382 recordout = None
2384 recordout = None
2383 # quick fix for output mismatch with bundle2 in 3.4
2385 # quick fix for output mismatch with bundle2 in 3.4
2384 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture')
2386 captureoutput = repo.ui.configbool('experimental', 'bundle2-output-capture')
2385 if url.startswith('remote:http:') or url.startswith('remote:https:'):
2387 if url.startswith('remote:http:') or url.startswith('remote:https:'):
2386 captureoutput = True
2388 captureoutput = True
2387 try:
2389 try:
2388 # note: outside bundle1, 'heads' is expected to be empty and this
2390 # note: outside bundle1, 'heads' is expected to be empty and this
2389 # 'check_heads' call wil be a no-op
2391 # 'check_heads' call wil be a no-op
2390 check_heads(repo, heads, 'uploading changes')
2392 check_heads(repo, heads, 'uploading changes')
2391 # push can proceed
2393 # push can proceed
2392 if not isinstance(cg, bundle2.unbundle20):
2394 if not isinstance(cg, bundle2.unbundle20):
2393 # legacy case: bundle1 (changegroup 01)
2395 # legacy case: bundle1 (changegroup 01)
2394 txnname = "\n".join([source, util.hidepassword(url)])
2396 txnname = "\n".join([source, util.hidepassword(url)])
2395 with repo.lock(), repo.transaction(txnname) as tr:
2397 with repo.lock(), repo.transaction(txnname) as tr:
2396 op = bundle2.applybundle(repo, cg, tr, source, url)
2398 op = bundle2.applybundle(repo, cg, tr, source, url)
2397 r = bundle2.combinechangegroupresults(op)
2399 r = bundle2.combinechangegroupresults(op)
2398 else:
2400 else:
2399 r = None
2401 r = None
2400 try:
2402 try:
2401 def gettransaction():
2403 def gettransaction():
2402 if not lockandtr[2]:
2404 if not lockandtr[2]:
2403 if not bookmod.bookmarksinstore(repo):
2405 if not bookmod.bookmarksinstore(repo):
2404 lockandtr[0] = repo.wlock()
2406 lockandtr[0] = repo.wlock()
2405 lockandtr[1] = repo.lock()
2407 lockandtr[1] = repo.lock()
2406 lockandtr[2] = repo.transaction(source)
2408 lockandtr[2] = repo.transaction(source)
2407 lockandtr[2].hookargs['source'] = source
2409 lockandtr[2].hookargs['source'] = source
2408 lockandtr[2].hookargs['url'] = url
2410 lockandtr[2].hookargs['url'] = url
2409 lockandtr[2].hookargs['bundle2'] = '1'
2411 lockandtr[2].hookargs['bundle2'] = '1'
2410 return lockandtr[2]
2412 return lockandtr[2]
2411
2413
2412 # Do greedy locking by default until we're satisfied with lazy
2414 # Do greedy locking by default until we're satisfied with lazy
2413 # locking.
2415 # locking.
2414 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
2416 if not repo.ui.configbool('experimental', 'bundle2lazylocking'):
2415 gettransaction()
2417 gettransaction()
2416
2418
2417 op = bundle2.bundleoperation(repo, gettransaction,
2419 op = bundle2.bundleoperation(repo, gettransaction,
2418 captureoutput=captureoutput,
2420 captureoutput=captureoutput,
2419 source='push')
2421 source='push')
2420 try:
2422 try:
2421 op = bundle2.processbundle(repo, cg, op=op)
2423 op = bundle2.processbundle(repo, cg, op=op)
2422 finally:
2424 finally:
2423 r = op.reply
2425 r = op.reply
2424 if captureoutput and r is not None:
2426 if captureoutput and r is not None:
2425 repo.ui.pushbuffer(error=True, subproc=True)
2427 repo.ui.pushbuffer(error=True, subproc=True)
2426 def recordout(output):
2428 def recordout(output):
2427 r.newpart('output', data=output, mandatory=False)
2429 r.newpart('output', data=output, mandatory=False)
2428 if lockandtr[2] is not None:
2430 if lockandtr[2] is not None:
2429 lockandtr[2].close()
2431 lockandtr[2].close()
2430 except BaseException as exc:
2432 except BaseException as exc:
2431 exc.duringunbundle2 = True
2433 exc.duringunbundle2 = True
2432 if captureoutput and r is not None:
2434 if captureoutput and r is not None:
2433 parts = exc._bundle2salvagedoutput = r.salvageoutput()
2435 parts = exc._bundle2salvagedoutput = r.salvageoutput()
2434 def recordout(output):
2436 def recordout(output):
2435 part = bundle2.bundlepart('output', data=output,
2437 part = bundle2.bundlepart('output', data=output,
2436 mandatory=False)
2438 mandatory=False)
2437 parts.append(part)
2439 parts.append(part)
2438 raise
2440 raise
2439 finally:
2441 finally:
2440 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
2442 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
2441 if recordout is not None:
2443 if recordout is not None:
2442 recordout(repo.ui.popbuffer())
2444 recordout(repo.ui.popbuffer())
2443 return r
2445 return r
2444
2446
2445 def _maybeapplyclonebundle(pullop):
2447 def _maybeapplyclonebundle(pullop):
2446 """Apply a clone bundle from a remote, if possible."""
2448 """Apply a clone bundle from a remote, if possible."""
2447
2449
2448 repo = pullop.repo
2450 repo = pullop.repo
2449 remote = pullop.remote
2451 remote = pullop.remote
2450
2452
2451 if not repo.ui.configbool('ui', 'clonebundles'):
2453 if not repo.ui.configbool('ui', 'clonebundles'):
2452 return
2454 return
2453
2455
2454 # Only run if local repo is empty.
2456 # Only run if local repo is empty.
2455 if len(repo):
2457 if len(repo):
2456 return
2458 return
2457
2459
2458 if pullop.heads:
2460 if pullop.heads:
2459 return
2461 return
2460
2462
2461 if not remote.capable('clonebundles'):
2463 if not remote.capable('clonebundles'):
2462 return
2464 return
2463
2465
2464 with remote.commandexecutor() as e:
2466 with remote.commandexecutor() as e:
2465 res = e.callcommand('clonebundles', {}).result()
2467 res = e.callcommand('clonebundles', {}).result()
2466
2468
2467 # If we call the wire protocol command, that's good enough to record the
2469 # If we call the wire protocol command, that's good enough to record the
2468 # attempt.
2470 # attempt.
2469 pullop.clonebundleattempted = True
2471 pullop.clonebundleattempted = True
2470
2472
2471 entries = parseclonebundlesmanifest(repo, res)
2473 entries = parseclonebundlesmanifest(repo, res)
2472 if not entries:
2474 if not entries:
2473 repo.ui.note(_('no clone bundles available on remote; '
2475 repo.ui.note(_('no clone bundles available on remote; '
2474 'falling back to regular clone\n'))
2476 'falling back to regular clone\n'))
2475 return
2477 return
2476
2478
2477 entries = filterclonebundleentries(
2479 entries = filterclonebundleentries(
2478 repo, entries, streamclonerequested=pullop.streamclonerequested)
2480 repo, entries, streamclonerequested=pullop.streamclonerequested)
2479
2481
2480 if not entries:
2482 if not entries:
2481 # There is a thundering herd concern here. However, if a server
2483 # There is a thundering herd concern here. However, if a server
2482 # operator doesn't advertise bundles appropriate for its clients,
2484 # operator doesn't advertise bundles appropriate for its clients,
2483 # they deserve what's coming. Furthermore, from a client's
2485 # they deserve what's coming. Furthermore, from a client's
2484 # perspective, no automatic fallback would mean not being able to
2486 # perspective, no automatic fallback would mean not being able to
2485 # clone!
2487 # clone!
2486 repo.ui.warn(_('no compatible clone bundles available on server; '
2488 repo.ui.warn(_('no compatible clone bundles available on server; '
2487 'falling back to regular clone\n'))
2489 'falling back to regular clone\n'))
2488 repo.ui.warn(_('(you may want to report this to the server '
2490 repo.ui.warn(_('(you may want to report this to the server '
2489 'operator)\n'))
2491 'operator)\n'))
2490 return
2492 return
2491
2493
2492 entries = sortclonebundleentries(repo.ui, entries)
2494 entries = sortclonebundleentries(repo.ui, entries)
2493
2495
2494 url = entries[0]['URL']
2496 url = entries[0]['URL']
2495 repo.ui.status(_('applying clone bundle from %s\n') % url)
2497 repo.ui.status(_('applying clone bundle from %s\n') % url)
2496 if trypullbundlefromurl(repo.ui, repo, url):
2498 if trypullbundlefromurl(repo.ui, repo, url):
2497 repo.ui.status(_('finished applying clone bundle\n'))
2499 repo.ui.status(_('finished applying clone bundle\n'))
2498 # Bundle failed.
2500 # Bundle failed.
2499 #
2501 #
2500 # We abort by default to avoid the thundering herd of
2502 # We abort by default to avoid the thundering herd of
2501 # clients flooding a server that was expecting expensive
2503 # clients flooding a server that was expecting expensive
2502 # clone load to be offloaded.
2504 # clone load to be offloaded.
2503 elif repo.ui.configbool('ui', 'clonebundlefallback'):
2505 elif repo.ui.configbool('ui', 'clonebundlefallback'):
2504 repo.ui.warn(_('falling back to normal clone\n'))
2506 repo.ui.warn(_('falling back to normal clone\n'))
2505 else:
2507 else:
2506 raise error.Abort(_('error applying bundle'),
2508 raise error.Abort(_('error applying bundle'),
2507 hint=_('if this error persists, consider contacting '
2509 hint=_('if this error persists, consider contacting '
2508 'the server operator or disable clone '
2510 'the server operator or disable clone '
2509 'bundles via '
2511 'bundles via '
2510 '"--config ui.clonebundles=false"'))
2512 '"--config ui.clonebundles=false"'))
2511
2513
2512 def parseclonebundlesmanifest(repo, s):
2514 def parseclonebundlesmanifest(repo, s):
2513 """Parses the raw text of a clone bundles manifest.
2515 """Parses the raw text of a clone bundles manifest.
2514
2516
2515 Returns a list of dicts. The dicts have a ``URL`` key corresponding
2517 Returns a list of dicts. The dicts have a ``URL`` key corresponding
2516 to the URL and other keys are the attributes for the entry.
2518 to the URL and other keys are the attributes for the entry.
2517 """
2519 """
2518 m = []
2520 m = []
2519 for line in s.splitlines():
2521 for line in s.splitlines():
2520 fields = line.split()
2522 fields = line.split()
2521 if not fields:
2523 if not fields:
2522 continue
2524 continue
2523 attrs = {'URL': fields[0]}
2525 attrs = {'URL': fields[0]}
2524 for rawattr in fields[1:]:
2526 for rawattr in fields[1:]:
2525 key, value = rawattr.split('=', 1)
2527 key, value = rawattr.split('=', 1)
2526 key = urlreq.unquote(key)
2528 key = urlreq.unquote(key)
2527 value = urlreq.unquote(value)
2529 value = urlreq.unquote(value)
2528 attrs[key] = value
2530 attrs[key] = value
2529
2531
2530 # Parse BUNDLESPEC into components. This makes client-side
2532 # Parse BUNDLESPEC into components. This makes client-side
2531 # preferences easier to specify since you can prefer a single
2533 # preferences easier to specify since you can prefer a single
2532 # component of the BUNDLESPEC.
2534 # component of the BUNDLESPEC.
2533 if key == 'BUNDLESPEC':
2535 if key == 'BUNDLESPEC':
2534 try:
2536 try:
2535 bundlespec = parsebundlespec(repo, value)
2537 bundlespec = parsebundlespec(repo, value)
2536 attrs['COMPRESSION'] = bundlespec.compression
2538 attrs['COMPRESSION'] = bundlespec.compression
2537 attrs['VERSION'] = bundlespec.version
2539 attrs['VERSION'] = bundlespec.version
2538 except error.InvalidBundleSpecification:
2540 except error.InvalidBundleSpecification:
2539 pass
2541 pass
2540 except error.UnsupportedBundleSpecification:
2542 except error.UnsupportedBundleSpecification:
2541 pass
2543 pass
2542
2544
2543 m.append(attrs)
2545 m.append(attrs)
2544
2546
2545 return m
2547 return m
2546
2548
2547 def isstreamclonespec(bundlespec):
2549 def isstreamclonespec(bundlespec):
2548 # Stream clone v1
2550 # Stream clone v1
2549 if (bundlespec.wirecompression == 'UN' and bundlespec.wireversion == 's1'):
2551 if (bundlespec.wirecompression == 'UN' and bundlespec.wireversion == 's1'):
2550 return True
2552 return True
2551
2553
2552 # Stream clone v2
2554 # Stream clone v2
2553 if (bundlespec.wirecompression == 'UN' and
2555 if (bundlespec.wirecompression == 'UN' and
2554 bundlespec.wireversion == '02' and
2556 bundlespec.wireversion == '02' and
2555 bundlespec.contentopts.get('streamv2')):
2557 bundlespec.contentopts.get('streamv2')):
2556 return True
2558 return True
2557
2559
2558 return False
2560 return False
2559
2561
2560 def filterclonebundleentries(repo, entries, streamclonerequested=False):
2562 def filterclonebundleentries(repo, entries, streamclonerequested=False):
2561 """Remove incompatible clone bundle manifest entries.
2563 """Remove incompatible clone bundle manifest entries.
2562
2564
2563 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
2565 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
2564 and returns a new list consisting of only the entries that this client
2566 and returns a new list consisting of only the entries that this client
2565 should be able to apply.
2567 should be able to apply.
2566
2568
2567 There is no guarantee we'll be able to apply all returned entries because
2569 There is no guarantee we'll be able to apply all returned entries because
2568 the metadata we use to filter on may be missing or wrong.
2570 the metadata we use to filter on may be missing or wrong.
2569 """
2571 """
2570 newentries = []
2572 newentries = []
2571 for entry in entries:
2573 for entry in entries:
2572 spec = entry.get('BUNDLESPEC')
2574 spec = entry.get('BUNDLESPEC')
2573 if spec:
2575 if spec:
2574 try:
2576 try:
2575 bundlespec = parsebundlespec(repo, spec, strict=True)
2577 bundlespec = parsebundlespec(repo, spec, strict=True)
2576
2578
2577 # If a stream clone was requested, filter out non-streamclone
2579 # If a stream clone was requested, filter out non-streamclone
2578 # entries.
2580 # entries.
2579 if streamclonerequested and not isstreamclonespec(bundlespec):
2581 if streamclonerequested and not isstreamclonespec(bundlespec):
2580 repo.ui.debug('filtering %s because not a stream clone\n' %
2582 repo.ui.debug('filtering %s because not a stream clone\n' %
2581 entry['URL'])
2583 entry['URL'])
2582 continue
2584 continue
2583
2585
2584 except error.InvalidBundleSpecification as e:
2586 except error.InvalidBundleSpecification as e:
2585 repo.ui.debug(stringutil.forcebytestr(e) + '\n')
2587 repo.ui.debug(stringutil.forcebytestr(e) + '\n')
2586 continue
2588 continue
2587 except error.UnsupportedBundleSpecification as e:
2589 except error.UnsupportedBundleSpecification as e:
2588 repo.ui.debug('filtering %s because unsupported bundle '
2590 repo.ui.debug('filtering %s because unsupported bundle '
2589 'spec: %s\n' % (
2591 'spec: %s\n' % (
2590 entry['URL'], stringutil.forcebytestr(e)))
2592 entry['URL'], stringutil.forcebytestr(e)))
2591 continue
2593 continue
2592 # If we don't have a spec and requested a stream clone, we don't know
2594 # If we don't have a spec and requested a stream clone, we don't know
2593 # what the entry is so don't attempt to apply it.
2595 # what the entry is so don't attempt to apply it.
2594 elif streamclonerequested:
2596 elif streamclonerequested:
2595 repo.ui.debug('filtering %s because cannot determine if a stream '
2597 repo.ui.debug('filtering %s because cannot determine if a stream '
2596 'clone bundle\n' % entry['URL'])
2598 'clone bundle\n' % entry['URL'])
2597 continue
2599 continue
2598
2600
2599 if 'REQUIRESNI' in entry and not sslutil.hassni:
2601 if 'REQUIRESNI' in entry and not sslutil.hassni:
2600 repo.ui.debug('filtering %s because SNI not supported\n' %
2602 repo.ui.debug('filtering %s because SNI not supported\n' %
2601 entry['URL'])
2603 entry['URL'])
2602 continue
2604 continue
2603
2605
2604 newentries.append(entry)
2606 newentries.append(entry)
2605
2607
2606 return newentries
2608 return newentries
2607
2609
2608 class clonebundleentry(object):
2610 class clonebundleentry(object):
2609 """Represents an item in a clone bundles manifest.
2611 """Represents an item in a clone bundles manifest.
2610
2612
2611 This rich class is needed to support sorting since sorted() in Python 3
2613 This rich class is needed to support sorting since sorted() in Python 3
2612 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
2614 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
2613 won't work.
2615 won't work.
2614 """
2616 """
2615
2617
2616 def __init__(self, value, prefers):
2618 def __init__(self, value, prefers):
2617 self.value = value
2619 self.value = value
2618 self.prefers = prefers
2620 self.prefers = prefers
2619
2621
2620 def _cmp(self, other):
2622 def _cmp(self, other):
2621 for prefkey, prefvalue in self.prefers:
2623 for prefkey, prefvalue in self.prefers:
2622 avalue = self.value.get(prefkey)
2624 avalue = self.value.get(prefkey)
2623 bvalue = other.value.get(prefkey)
2625 bvalue = other.value.get(prefkey)
2624
2626
2625 # Special case for b missing attribute and a matches exactly.
2627 # Special case for b missing attribute and a matches exactly.
2626 if avalue is not None and bvalue is None and avalue == prefvalue:
2628 if avalue is not None and bvalue is None and avalue == prefvalue:
2627 return -1
2629 return -1
2628
2630
2629 # Special case for a missing attribute and b matches exactly.
2631 # Special case for a missing attribute and b matches exactly.
2630 if bvalue is not None and avalue is None and bvalue == prefvalue:
2632 if bvalue is not None and avalue is None and bvalue == prefvalue:
2631 return 1
2633 return 1
2632
2634
2633 # We can't compare unless attribute present on both.
2635 # We can't compare unless attribute present on both.
2634 if avalue is None or bvalue is None:
2636 if avalue is None or bvalue is None:
2635 continue
2637 continue
2636
2638
2637 # Same values should fall back to next attribute.
2639 # Same values should fall back to next attribute.
2638 if avalue == bvalue:
2640 if avalue == bvalue:
2639 continue
2641 continue
2640
2642
2641 # Exact matches come first.
2643 # Exact matches come first.
2642 if avalue == prefvalue:
2644 if avalue == prefvalue:
2643 return -1
2645 return -1
2644 if bvalue == prefvalue:
2646 if bvalue == prefvalue:
2645 return 1
2647 return 1
2646
2648
2647 # Fall back to next attribute.
2649 # Fall back to next attribute.
2648 continue
2650 continue
2649
2651
2650 # If we got here we couldn't sort by attributes and prefers. Fall
2652 # If we got here we couldn't sort by attributes and prefers. Fall
2651 # back to index order.
2653 # back to index order.
2652 return 0
2654 return 0
2653
2655
2654 def __lt__(self, other):
2656 def __lt__(self, other):
2655 return self._cmp(other) < 0
2657 return self._cmp(other) < 0
2656
2658
2657 def __gt__(self, other):
2659 def __gt__(self, other):
2658 return self._cmp(other) > 0
2660 return self._cmp(other) > 0
2659
2661
2660 def __eq__(self, other):
2662 def __eq__(self, other):
2661 return self._cmp(other) == 0
2663 return self._cmp(other) == 0
2662
2664
2663 def __le__(self, other):
2665 def __le__(self, other):
2664 return self._cmp(other) <= 0
2666 return self._cmp(other) <= 0
2665
2667
2666 def __ge__(self, other):
2668 def __ge__(self, other):
2667 return self._cmp(other) >= 0
2669 return self._cmp(other) >= 0
2668
2670
2669 def __ne__(self, other):
2671 def __ne__(self, other):
2670 return self._cmp(other) != 0
2672 return self._cmp(other) != 0
2671
2673
2672 def sortclonebundleentries(ui, entries):
2674 def sortclonebundleentries(ui, entries):
2673 prefers = ui.configlist('ui', 'clonebundleprefers')
2675 prefers = ui.configlist('ui', 'clonebundleprefers')
2674 if not prefers:
2676 if not prefers:
2675 return list(entries)
2677 return list(entries)
2676
2678
2677 prefers = [p.split('=', 1) for p in prefers]
2679 prefers = [p.split('=', 1) for p in prefers]
2678
2680
2679 items = sorted(clonebundleentry(v, prefers) for v in entries)
2681 items = sorted(clonebundleentry(v, prefers) for v in entries)
2680 return [i.value for i in items]
2682 return [i.value for i in items]
2681
2683
2682 def trypullbundlefromurl(ui, repo, url):
2684 def trypullbundlefromurl(ui, repo, url):
2683 """Attempt to apply a bundle from a URL."""
2685 """Attempt to apply a bundle from a URL."""
2684 with repo.lock(), repo.transaction('bundleurl') as tr:
2686 with repo.lock(), repo.transaction('bundleurl') as tr:
2685 try:
2687 try:
2686 fh = urlmod.open(ui, url)
2688 fh = urlmod.open(ui, url)
2687 cg = readbundle(ui, fh, 'stream')
2689 cg = readbundle(ui, fh, 'stream')
2688
2690
2689 if isinstance(cg, streamclone.streamcloneapplier):
2691 if isinstance(cg, streamclone.streamcloneapplier):
2690 cg.apply(repo)
2692 cg.apply(repo)
2691 else:
2693 else:
2692 bundle2.applybundle(repo, cg, tr, 'clonebundles', url)
2694 bundle2.applybundle(repo, cg, tr, 'clonebundles', url)
2693 return True
2695 return True
2694 except urlerr.httperror as e:
2696 except urlerr.httperror as e:
2695 ui.warn(_('HTTP error fetching bundle: %s\n') %
2697 ui.warn(_('HTTP error fetching bundle: %s\n') %
2696 stringutil.forcebytestr(e))
2698 stringutil.forcebytestr(e))
2697 except urlerr.urlerror as e:
2699 except urlerr.urlerror as e:
2698 ui.warn(_('error fetching bundle: %s\n') %
2700 ui.warn(_('error fetching bundle: %s\n') %
2699 stringutil.forcebytestr(e.reason))
2701 stringutil.forcebytestr(e.reason))
2700
2702
2701 return False
2703 return False
@@ -1,697 +1,699
1 # exchangev2.py - repository exchange for wire protocol version 2
1 # exchangev2.py - repository exchange for wire protocol version 2
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import collections
10 import collections
11 import weakref
11 import weakref
12
12
13 from .i18n import _
13 from .i18n import _
14 from .node import (
14 from .node import (
15 nullid,
15 nullid,
16 short,
16 short,
17 )
17 )
18 from . import (
18 from . import (
19 bookmarks,
19 bookmarks,
20 error,
20 error,
21 mdiff,
21 mdiff,
22 narrowspec,
22 narrowspec,
23 phases,
23 phases,
24 pycompat,
24 pycompat,
25 setdiscovery,
26 )
27 from .interfaces import (
25 repository,
28 repository,
26 setdiscovery,
27 )
29 )
28
30
29 def pull(pullop):
31 def pull(pullop):
30 """Pull using wire protocol version 2."""
32 """Pull using wire protocol version 2."""
31 repo = pullop.repo
33 repo = pullop.repo
32 remote = pullop.remote
34 remote = pullop.remote
33
35
34 usingrawchangelogandmanifest = _checkuserawstorefiledata(pullop)
36 usingrawchangelogandmanifest = _checkuserawstorefiledata(pullop)
35
37
36 # If this is a clone and it was requested to perform a "stream clone",
38 # If this is a clone and it was requested to perform a "stream clone",
37 # we obtain the raw files data from the remote then fall back to an
39 # we obtain the raw files data from the remote then fall back to an
38 # incremental pull. This is somewhat hacky and is not nearly robust enough
40 # incremental pull. This is somewhat hacky and is not nearly robust enough
39 # for long-term usage.
41 # for long-term usage.
40 if usingrawchangelogandmanifest:
42 if usingrawchangelogandmanifest:
41 with repo.transaction('clone'):
43 with repo.transaction('clone'):
42 _fetchrawstorefiles(repo, remote)
44 _fetchrawstorefiles(repo, remote)
43 repo.invalidate(clearfilecache=True)
45 repo.invalidate(clearfilecache=True)
44
46
45 tr = pullop.trmanager.transaction()
47 tr = pullop.trmanager.transaction()
46
48
47 # We don't use the repo's narrow matcher here because the patterns passed
49 # We don't use the repo's narrow matcher here because the patterns passed
48 # to exchange.pull() could be different.
50 # to exchange.pull() could be different.
49 narrowmatcher = narrowspec.match(repo.root,
51 narrowmatcher = narrowspec.match(repo.root,
50 # Empty maps to nevermatcher. So always
52 # Empty maps to nevermatcher. So always
51 # set includes if missing.
53 # set includes if missing.
52 pullop.includepats or {'path:.'},
54 pullop.includepats or {'path:.'},
53 pullop.excludepats)
55 pullop.excludepats)
54
56
55 if pullop.includepats or pullop.excludepats:
57 if pullop.includepats or pullop.excludepats:
56 pathfilter = {}
58 pathfilter = {}
57 if pullop.includepats:
59 if pullop.includepats:
58 pathfilter[b'include'] = sorted(pullop.includepats)
60 pathfilter[b'include'] = sorted(pullop.includepats)
59 if pullop.excludepats:
61 if pullop.excludepats:
60 pathfilter[b'exclude'] = sorted(pullop.excludepats)
62 pathfilter[b'exclude'] = sorted(pullop.excludepats)
61 else:
63 else:
62 pathfilter = None
64 pathfilter = None
63
65
64 # Figure out what needs to be fetched.
66 # Figure out what needs to be fetched.
65 common, fetch, remoteheads = _pullchangesetdiscovery(
67 common, fetch, remoteheads = _pullchangesetdiscovery(
66 repo, remote, pullop.heads, abortwhenunrelated=pullop.force)
68 repo, remote, pullop.heads, abortwhenunrelated=pullop.force)
67
69
68 # And fetch the data.
70 # And fetch the data.
69 pullheads = pullop.heads or remoteheads
71 pullheads = pullop.heads or remoteheads
70 csetres = _fetchchangesets(repo, tr, remote, common, fetch, pullheads)
72 csetres = _fetchchangesets(repo, tr, remote, common, fetch, pullheads)
71
73
72 # New revisions are written to the changelog. But all other updates
74 # New revisions are written to the changelog. But all other updates
73 # are deferred. Do those now.
75 # are deferred. Do those now.
74
76
75 # Ensure all new changesets are draft by default. If the repo is
77 # Ensure all new changesets are draft by default. If the repo is
76 # publishing, the phase will be adjusted by the loop below.
78 # publishing, the phase will be adjusted by the loop below.
77 if csetres['added']:
79 if csetres['added']:
78 phases.registernew(repo, tr, phases.draft, csetres['added'])
80 phases.registernew(repo, tr, phases.draft, csetres['added'])
79
81
80 # And adjust the phase of all changesets accordingly.
82 # And adjust the phase of all changesets accordingly.
81 for phase in phases.phasenames:
83 for phase in phases.phasenames:
82 if phase == b'secret' or not csetres['nodesbyphase'][phase]:
84 if phase == b'secret' or not csetres['nodesbyphase'][phase]:
83 continue
85 continue
84
86
85 phases.advanceboundary(repo, tr, phases.phasenames.index(phase),
87 phases.advanceboundary(repo, tr, phases.phasenames.index(phase),
86 csetres['nodesbyphase'][phase])
88 csetres['nodesbyphase'][phase])
87
89
88 # Write bookmark updates.
90 # Write bookmark updates.
89 bookmarks.updatefromremote(repo.ui, repo, csetres['bookmarks'],
91 bookmarks.updatefromremote(repo.ui, repo, csetres['bookmarks'],
90 remote.url(), pullop.gettransaction,
92 remote.url(), pullop.gettransaction,
91 explicit=pullop.explicitbookmarks)
93 explicit=pullop.explicitbookmarks)
92
94
93 manres = _fetchmanifests(repo, tr, remote, csetres['manifestnodes'])
95 manres = _fetchmanifests(repo, tr, remote, csetres['manifestnodes'])
94
96
95 # We don't properly support shallow changeset and manifest yet. So we apply
97 # We don't properly support shallow changeset and manifest yet. So we apply
96 # depth limiting locally.
98 # depth limiting locally.
97 if pullop.depth:
99 if pullop.depth:
98 relevantcsetnodes = set()
100 relevantcsetnodes = set()
99 clnode = repo.changelog.node
101 clnode = repo.changelog.node
100
102
101 for rev in repo.revs(b'ancestors(%ln, %s)',
103 for rev in repo.revs(b'ancestors(%ln, %s)',
102 pullheads, pullop.depth - 1):
104 pullheads, pullop.depth - 1):
103 relevantcsetnodes.add(clnode(rev))
105 relevantcsetnodes.add(clnode(rev))
104
106
105 csetrelevantfilter = lambda n: n in relevantcsetnodes
107 csetrelevantfilter = lambda n: n in relevantcsetnodes
106
108
107 else:
109 else:
108 csetrelevantfilter = lambda n: True
110 csetrelevantfilter = lambda n: True
109
111
110 # If obtaining the raw store files, we need to scan the full repo to
112 # If obtaining the raw store files, we need to scan the full repo to
111 # derive all the changesets, manifests, and linkrevs.
113 # derive all the changesets, manifests, and linkrevs.
112 if usingrawchangelogandmanifest:
114 if usingrawchangelogandmanifest:
113 csetsforfiles = []
115 csetsforfiles = []
114 mnodesforfiles = []
116 mnodesforfiles = []
115 manifestlinkrevs = {}
117 manifestlinkrevs = {}
116
118
117 for rev in repo:
119 for rev in repo:
118 ctx = repo[rev]
120 ctx = repo[rev]
119 node = ctx.node()
121 node = ctx.node()
120
122
121 if not csetrelevantfilter(node):
123 if not csetrelevantfilter(node):
122 continue
124 continue
123
125
124 mnode = ctx.manifestnode()
126 mnode = ctx.manifestnode()
125
127
126 csetsforfiles.append(node)
128 csetsforfiles.append(node)
127 mnodesforfiles.append(mnode)
129 mnodesforfiles.append(mnode)
128 manifestlinkrevs[mnode] = rev
130 manifestlinkrevs[mnode] = rev
129
131
130 else:
132 else:
131 csetsforfiles = [n for n in csetres['added'] if csetrelevantfilter(n)]
133 csetsforfiles = [n for n in csetres['added'] if csetrelevantfilter(n)]
132 mnodesforfiles = manres['added']
134 mnodesforfiles = manres['added']
133 manifestlinkrevs = manres['linkrevs']
135 manifestlinkrevs = manres['linkrevs']
134
136
135 # Find all file nodes referenced by added manifests and fetch those
137 # Find all file nodes referenced by added manifests and fetch those
136 # revisions.
138 # revisions.
137 fnodes = _derivefilesfrommanifests(repo, narrowmatcher, mnodesforfiles)
139 fnodes = _derivefilesfrommanifests(repo, narrowmatcher, mnodesforfiles)
138 _fetchfilesfromcsets(repo, tr, remote, pathfilter, fnodes, csetsforfiles,
140 _fetchfilesfromcsets(repo, tr, remote, pathfilter, fnodes, csetsforfiles,
139 manifestlinkrevs, shallow=bool(pullop.depth))
141 manifestlinkrevs, shallow=bool(pullop.depth))
140
142
141 def _checkuserawstorefiledata(pullop):
143 def _checkuserawstorefiledata(pullop):
142 """Check whether we should use rawstorefiledata command to retrieve data."""
144 """Check whether we should use rawstorefiledata command to retrieve data."""
143
145
144 repo = pullop.repo
146 repo = pullop.repo
145 remote = pullop.remote
147 remote = pullop.remote
146
148
147 # Command to obtain raw store data isn't available.
149 # Command to obtain raw store data isn't available.
148 if b'rawstorefiledata' not in remote.apidescriptor[b'commands']:
150 if b'rawstorefiledata' not in remote.apidescriptor[b'commands']:
149 return False
151 return False
150
152
151 # Only honor if user requested stream clone operation.
153 # Only honor if user requested stream clone operation.
152 if not pullop.streamclonerequested:
154 if not pullop.streamclonerequested:
153 return False
155 return False
154
156
155 # Only works on empty repos.
157 # Only works on empty repos.
156 if len(repo):
158 if len(repo):
157 return False
159 return False
158
160
159 # TODO This is super hacky. There needs to be a storage API for this. We
161 # TODO This is super hacky. There needs to be a storage API for this. We
160 # also need to check for compatibility with the remote.
162 # also need to check for compatibility with the remote.
161 if b'revlogv1' not in repo.requirements:
163 if b'revlogv1' not in repo.requirements:
162 return False
164 return False
163
165
164 return True
166 return True
165
167
166 def _fetchrawstorefiles(repo, remote):
168 def _fetchrawstorefiles(repo, remote):
167 with remote.commandexecutor() as e:
169 with remote.commandexecutor() as e:
168 objs = e.callcommand(b'rawstorefiledata', {
170 objs = e.callcommand(b'rawstorefiledata', {
169 b'files': [b'changelog', b'manifestlog'],
171 b'files': [b'changelog', b'manifestlog'],
170 }).result()
172 }).result()
171
173
172 # First object is a summary of files data that follows.
174 # First object is a summary of files data that follows.
173 overall = next(objs)
175 overall = next(objs)
174
176
175 progress = repo.ui.makeprogress(_('clone'), total=overall[b'totalsize'],
177 progress = repo.ui.makeprogress(_('clone'), total=overall[b'totalsize'],
176 unit=_('bytes'))
178 unit=_('bytes'))
177 with progress:
179 with progress:
178 progress.update(0)
180 progress.update(0)
179
181
180 # Next are pairs of file metadata, data.
182 # Next are pairs of file metadata, data.
181 while True:
183 while True:
182 try:
184 try:
183 filemeta = next(objs)
185 filemeta = next(objs)
184 except StopIteration:
186 except StopIteration:
185 break
187 break
186
188
187 for k in (b'location', b'path', b'size'):
189 for k in (b'location', b'path', b'size'):
188 if k not in filemeta:
190 if k not in filemeta:
189 raise error.Abort(_(b'remote file data missing key: %s')
191 raise error.Abort(_(b'remote file data missing key: %s')
190 % k)
192 % k)
191
193
192 if filemeta[b'location'] == b'store':
194 if filemeta[b'location'] == b'store':
193 vfs = repo.svfs
195 vfs = repo.svfs
194 else:
196 else:
195 raise error.Abort(_(b'invalid location for raw file data: '
197 raise error.Abort(_(b'invalid location for raw file data: '
196 b'%s') % filemeta[b'location'])
198 b'%s') % filemeta[b'location'])
197
199
198 bytesremaining = filemeta[b'size']
200 bytesremaining = filemeta[b'size']
199
201
200 with vfs.open(filemeta[b'path'], b'wb') as fh:
202 with vfs.open(filemeta[b'path'], b'wb') as fh:
201 while True:
203 while True:
202 try:
204 try:
203 chunk = next(objs)
205 chunk = next(objs)
204 except StopIteration:
206 except StopIteration:
205 break
207 break
206
208
207 bytesremaining -= len(chunk)
209 bytesremaining -= len(chunk)
208
210
209 if bytesremaining < 0:
211 if bytesremaining < 0:
210 raise error.Abort(_(
212 raise error.Abort(_(
211 b'received invalid number of bytes for file '
213 b'received invalid number of bytes for file '
212 b'data; expected %d, got extra') %
214 b'data; expected %d, got extra') %
213 filemeta[b'size'])
215 filemeta[b'size'])
214
216
215 progress.increment(step=len(chunk))
217 progress.increment(step=len(chunk))
216 fh.write(chunk)
218 fh.write(chunk)
217
219
218 try:
220 try:
219 if chunk.islast:
221 if chunk.islast:
220 break
222 break
221 except AttributeError:
223 except AttributeError:
222 raise error.Abort(_(
224 raise error.Abort(_(
223 b'did not receive indefinite length bytestring '
225 b'did not receive indefinite length bytestring '
224 b'for file data'))
226 b'for file data'))
225
227
226 if bytesremaining:
228 if bytesremaining:
227 raise error.Abort(_(b'received invalid number of bytes for'
229 raise error.Abort(_(b'received invalid number of bytes for'
228 b'file data; expected %d got %d') %
230 b'file data; expected %d got %d') %
229 (filemeta[b'size'],
231 (filemeta[b'size'],
230 filemeta[b'size'] - bytesremaining))
232 filemeta[b'size'] - bytesremaining))
231
233
232 def _pullchangesetdiscovery(repo, remote, heads, abortwhenunrelated=True):
234 def _pullchangesetdiscovery(repo, remote, heads, abortwhenunrelated=True):
233 """Determine which changesets need to be pulled."""
235 """Determine which changesets need to be pulled."""
234
236
235 if heads:
237 if heads:
236 knownnode = repo.changelog.hasnode
238 knownnode = repo.changelog.hasnode
237 if all(knownnode(head) for head in heads):
239 if all(knownnode(head) for head in heads):
238 return heads, False, heads
240 return heads, False, heads
239
241
240 # TODO wire protocol version 2 is capable of more efficient discovery
242 # TODO wire protocol version 2 is capable of more efficient discovery
241 # than setdiscovery. Consider implementing something better.
243 # than setdiscovery. Consider implementing something better.
242 common, fetch, remoteheads = setdiscovery.findcommonheads(
244 common, fetch, remoteheads = setdiscovery.findcommonheads(
243 repo.ui, repo, remote, abortwhenunrelated=abortwhenunrelated)
245 repo.ui, repo, remote, abortwhenunrelated=abortwhenunrelated)
244
246
245 common = set(common)
247 common = set(common)
246 remoteheads = set(remoteheads)
248 remoteheads = set(remoteheads)
247
249
248 # If a remote head is filtered locally, put it back in the common set.
250 # If a remote head is filtered locally, put it back in the common set.
249 # See the comment in exchange._pulldiscoverychangegroup() for more.
251 # See the comment in exchange._pulldiscoverychangegroup() for more.
250
252
251 if fetch and remoteheads:
253 if fetch and remoteheads:
252 nodemap = repo.unfiltered().changelog.nodemap
254 nodemap = repo.unfiltered().changelog.nodemap
253
255
254 common |= {head for head in remoteheads if head in nodemap}
256 common |= {head for head in remoteheads if head in nodemap}
255
257
256 if set(remoteheads).issubset(common):
258 if set(remoteheads).issubset(common):
257 fetch = []
259 fetch = []
258
260
259 common.discard(nullid)
261 common.discard(nullid)
260
262
261 return common, fetch, remoteheads
263 return common, fetch, remoteheads
262
264
263 def _fetchchangesets(repo, tr, remote, common, fetch, remoteheads):
265 def _fetchchangesets(repo, tr, remote, common, fetch, remoteheads):
264 # TODO consider adding a step here where we obtain the DAG shape first
266 # TODO consider adding a step here where we obtain the DAG shape first
265 # (or ask the server to slice changesets into chunks for us) so that
267 # (or ask the server to slice changesets into chunks for us) so that
266 # we can perform multiple fetches in batches. This will facilitate
268 # we can perform multiple fetches in batches. This will facilitate
267 # resuming interrupted clones, higher server-side cache hit rates due
269 # resuming interrupted clones, higher server-side cache hit rates due
268 # to smaller segments, etc.
270 # to smaller segments, etc.
269 with remote.commandexecutor() as e:
271 with remote.commandexecutor() as e:
270 objs = e.callcommand(b'changesetdata', {
272 objs = e.callcommand(b'changesetdata', {
271 b'revisions': [{
273 b'revisions': [{
272 b'type': b'changesetdagrange',
274 b'type': b'changesetdagrange',
273 b'roots': sorted(common),
275 b'roots': sorted(common),
274 b'heads': sorted(remoteheads),
276 b'heads': sorted(remoteheads),
275 }],
277 }],
276 b'fields': {b'bookmarks', b'parents', b'phase', b'revision'},
278 b'fields': {b'bookmarks', b'parents', b'phase', b'revision'},
277 }).result()
279 }).result()
278
280
279 # The context manager waits on all response data when exiting. So
281 # The context manager waits on all response data when exiting. So
280 # we need to remain in the context manager in order to stream data.
282 # we need to remain in the context manager in order to stream data.
281 return _processchangesetdata(repo, tr, objs)
283 return _processchangesetdata(repo, tr, objs)
282
284
283 def _processchangesetdata(repo, tr, objs):
285 def _processchangesetdata(repo, tr, objs):
284 repo.hook('prechangegroup', throw=True,
286 repo.hook('prechangegroup', throw=True,
285 **pycompat.strkwargs(tr.hookargs))
287 **pycompat.strkwargs(tr.hookargs))
286
288
287 urepo = repo.unfiltered()
289 urepo = repo.unfiltered()
288 cl = urepo.changelog
290 cl = urepo.changelog
289
291
290 cl.delayupdate(tr)
292 cl.delayupdate(tr)
291
293
292 # The first emitted object is a header describing the data that
294 # The first emitted object is a header describing the data that
293 # follows.
295 # follows.
294 meta = next(objs)
296 meta = next(objs)
295
297
296 progress = repo.ui.makeprogress(_('changesets'),
298 progress = repo.ui.makeprogress(_('changesets'),
297 unit=_('chunks'),
299 unit=_('chunks'),
298 total=meta.get(b'totalitems'))
300 total=meta.get(b'totalitems'))
299
301
300 manifestnodes = {}
302 manifestnodes = {}
301
303
302 def linkrev(node):
304 def linkrev(node):
303 repo.ui.debug('add changeset %s\n' % short(node))
305 repo.ui.debug('add changeset %s\n' % short(node))
304 # Linkrev for changelog is always self.
306 # Linkrev for changelog is always self.
305 return len(cl)
307 return len(cl)
306
308
307 def onchangeset(cl, node):
309 def onchangeset(cl, node):
308 progress.increment()
310 progress.increment()
309
311
310 revision = cl.changelogrevision(node)
312 revision = cl.changelogrevision(node)
311
313
312 # We need to preserve the mapping of changelog revision to node
314 # We need to preserve the mapping of changelog revision to node
313 # so we can set the linkrev accordingly when manifests are added.
315 # so we can set the linkrev accordingly when manifests are added.
314 manifestnodes[cl.rev(node)] = revision.manifest
316 manifestnodes[cl.rev(node)] = revision.manifest
315
317
316 nodesbyphase = {phase: set() for phase in phases.phasenames}
318 nodesbyphase = {phase: set() for phase in phases.phasenames}
317 remotebookmarks = {}
319 remotebookmarks = {}
318
320
319 # addgroup() expects a 7-tuple describing revisions. This normalizes
321 # addgroup() expects a 7-tuple describing revisions. This normalizes
320 # the wire data to that format.
322 # the wire data to that format.
321 #
323 #
322 # This loop also aggregates non-revision metadata, such as phase
324 # This loop also aggregates non-revision metadata, such as phase
323 # data.
325 # data.
324 def iterrevisions():
326 def iterrevisions():
325 for cset in objs:
327 for cset in objs:
326 node = cset[b'node']
328 node = cset[b'node']
327
329
328 if b'phase' in cset:
330 if b'phase' in cset:
329 nodesbyphase[cset[b'phase']].add(node)
331 nodesbyphase[cset[b'phase']].add(node)
330
332
331 for mark in cset.get(b'bookmarks', []):
333 for mark in cset.get(b'bookmarks', []):
332 remotebookmarks[mark] = node
334 remotebookmarks[mark] = node
333
335
334 # TODO add mechanism for extensions to examine records so they
336 # TODO add mechanism for extensions to examine records so they
335 # can siphon off custom data fields.
337 # can siphon off custom data fields.
336
338
337 extrafields = {}
339 extrafields = {}
338
340
339 for field, size in cset.get(b'fieldsfollowing', []):
341 for field, size in cset.get(b'fieldsfollowing', []):
340 extrafields[field] = next(objs)
342 extrafields[field] = next(objs)
341
343
342 # Some entries might only be metadata only updates.
344 # Some entries might only be metadata only updates.
343 if b'revision' not in extrafields:
345 if b'revision' not in extrafields:
344 continue
346 continue
345
347
346 data = extrafields[b'revision']
348 data = extrafields[b'revision']
347
349
348 yield (
350 yield (
349 node,
351 node,
350 cset[b'parents'][0],
352 cset[b'parents'][0],
351 cset[b'parents'][1],
353 cset[b'parents'][1],
352 # Linknode is always itself for changesets.
354 # Linknode is always itself for changesets.
353 cset[b'node'],
355 cset[b'node'],
354 # We always send full revisions. So delta base is not set.
356 # We always send full revisions. So delta base is not set.
355 nullid,
357 nullid,
356 mdiff.trivialdiffheader(len(data)) + data,
358 mdiff.trivialdiffheader(len(data)) + data,
357 # Flags not yet supported.
359 # Flags not yet supported.
358 0,
360 0,
359 )
361 )
360
362
361 added = cl.addgroup(iterrevisions(), linkrev, weakref.proxy(tr),
363 added = cl.addgroup(iterrevisions(), linkrev, weakref.proxy(tr),
362 addrevisioncb=onchangeset)
364 addrevisioncb=onchangeset)
363
365
364 progress.complete()
366 progress.complete()
365
367
366 return {
368 return {
367 'added': added,
369 'added': added,
368 'nodesbyphase': nodesbyphase,
370 'nodesbyphase': nodesbyphase,
369 'bookmarks': remotebookmarks,
371 'bookmarks': remotebookmarks,
370 'manifestnodes': manifestnodes,
372 'manifestnodes': manifestnodes,
371 }
373 }
372
374
373 def _fetchmanifests(repo, tr, remote, manifestnodes):
375 def _fetchmanifests(repo, tr, remote, manifestnodes):
374 rootmanifest = repo.manifestlog.getstorage(b'')
376 rootmanifest = repo.manifestlog.getstorage(b'')
375
377
376 # Some manifests can be shared between changesets. Filter out revisions
378 # Some manifests can be shared between changesets. Filter out revisions
377 # we already know about.
379 # we already know about.
378 fetchnodes = []
380 fetchnodes = []
379 linkrevs = {}
381 linkrevs = {}
380 seen = set()
382 seen = set()
381
383
382 for clrev, node in sorted(manifestnodes.iteritems()):
384 for clrev, node in sorted(manifestnodes.iteritems()):
383 if node in seen:
385 if node in seen:
384 continue
386 continue
385
387
386 try:
388 try:
387 rootmanifest.rev(node)
389 rootmanifest.rev(node)
388 except error.LookupError:
390 except error.LookupError:
389 fetchnodes.append(node)
391 fetchnodes.append(node)
390 linkrevs[node] = clrev
392 linkrevs[node] = clrev
391
393
392 seen.add(node)
394 seen.add(node)
393
395
394 # TODO handle tree manifests
396 # TODO handle tree manifests
395
397
396 # addgroup() expects 7-tuple describing revisions. This normalizes
398 # addgroup() expects 7-tuple describing revisions. This normalizes
397 # the wire data to that format.
399 # the wire data to that format.
398 def iterrevisions(objs, progress):
400 def iterrevisions(objs, progress):
399 for manifest in objs:
401 for manifest in objs:
400 node = manifest[b'node']
402 node = manifest[b'node']
401
403
402 extrafields = {}
404 extrafields = {}
403
405
404 for field, size in manifest.get(b'fieldsfollowing', []):
406 for field, size in manifest.get(b'fieldsfollowing', []):
405 extrafields[field] = next(objs)
407 extrafields[field] = next(objs)
406
408
407 if b'delta' in extrafields:
409 if b'delta' in extrafields:
408 basenode = manifest[b'deltabasenode']
410 basenode = manifest[b'deltabasenode']
409 delta = extrafields[b'delta']
411 delta = extrafields[b'delta']
410 elif b'revision' in extrafields:
412 elif b'revision' in extrafields:
411 basenode = nullid
413 basenode = nullid
412 revision = extrafields[b'revision']
414 revision = extrafields[b'revision']
413 delta = mdiff.trivialdiffheader(len(revision)) + revision
415 delta = mdiff.trivialdiffheader(len(revision)) + revision
414 else:
416 else:
415 continue
417 continue
416
418
417 yield (
419 yield (
418 node,
420 node,
419 manifest[b'parents'][0],
421 manifest[b'parents'][0],
420 manifest[b'parents'][1],
422 manifest[b'parents'][1],
421 # The value passed in is passed to the lookup function passed
423 # The value passed in is passed to the lookup function passed
422 # to addgroup(). We already have a map of manifest node to
424 # to addgroup(). We already have a map of manifest node to
423 # changelog revision number. So we just pass in the
425 # changelog revision number. So we just pass in the
424 # manifest node here and use linkrevs.__getitem__ as the
426 # manifest node here and use linkrevs.__getitem__ as the
425 # resolution function.
427 # resolution function.
426 node,
428 node,
427 basenode,
429 basenode,
428 delta,
430 delta,
429 # Flags not yet supported.
431 # Flags not yet supported.
430 0
432 0
431 )
433 )
432
434
433 progress.increment()
435 progress.increment()
434
436
435 progress = repo.ui.makeprogress(_('manifests'), unit=_('chunks'),
437 progress = repo.ui.makeprogress(_('manifests'), unit=_('chunks'),
436 total=len(fetchnodes))
438 total=len(fetchnodes))
437
439
438 commandmeta = remote.apidescriptor[b'commands'][b'manifestdata']
440 commandmeta = remote.apidescriptor[b'commands'][b'manifestdata']
439 batchsize = commandmeta.get(b'recommendedbatchsize', 10000)
441 batchsize = commandmeta.get(b'recommendedbatchsize', 10000)
440 # TODO make size configurable on client?
442 # TODO make size configurable on client?
441
443
442 # We send commands 1 at a time to the remote. This is not the most
444 # We send commands 1 at a time to the remote. This is not the most
443 # efficient because we incur a round trip at the end of each batch.
445 # efficient because we incur a round trip at the end of each batch.
444 # However, the existing frame-based reactor keeps consuming server
446 # However, the existing frame-based reactor keeps consuming server
445 # data in the background. And this results in response data buffering
447 # data in the background. And this results in response data buffering
446 # in memory. This can consume gigabytes of memory.
448 # in memory. This can consume gigabytes of memory.
447 # TODO send multiple commands in a request once background buffering
449 # TODO send multiple commands in a request once background buffering
448 # issues are resolved.
450 # issues are resolved.
449
451
450 added = []
452 added = []
451
453
452 for i in pycompat.xrange(0, len(fetchnodes), batchsize):
454 for i in pycompat.xrange(0, len(fetchnodes), batchsize):
453 batch = [node for node in fetchnodes[i:i + batchsize]]
455 batch = [node for node in fetchnodes[i:i + batchsize]]
454 if not batch:
456 if not batch:
455 continue
457 continue
456
458
457 with remote.commandexecutor() as e:
459 with remote.commandexecutor() as e:
458 objs = e.callcommand(b'manifestdata', {
460 objs = e.callcommand(b'manifestdata', {
459 b'tree': b'',
461 b'tree': b'',
460 b'nodes': batch,
462 b'nodes': batch,
461 b'fields': {b'parents', b'revision'},
463 b'fields': {b'parents', b'revision'},
462 b'haveparents': True,
464 b'haveparents': True,
463 }).result()
465 }).result()
464
466
465 # Chomp off header object.
467 # Chomp off header object.
466 next(objs)
468 next(objs)
467
469
468 added.extend(rootmanifest.addgroup(
470 added.extend(rootmanifest.addgroup(
469 iterrevisions(objs, progress),
471 iterrevisions(objs, progress),
470 linkrevs.__getitem__,
472 linkrevs.__getitem__,
471 weakref.proxy(tr)))
473 weakref.proxy(tr)))
472
474
473 progress.complete()
475 progress.complete()
474
476
475 return {
477 return {
476 'added': added,
478 'added': added,
477 'linkrevs': linkrevs,
479 'linkrevs': linkrevs,
478 }
480 }
479
481
480 def _derivefilesfrommanifests(repo, matcher, manifestnodes):
482 def _derivefilesfrommanifests(repo, matcher, manifestnodes):
481 """Determine what file nodes are relevant given a set of manifest nodes.
483 """Determine what file nodes are relevant given a set of manifest nodes.
482
484
483 Returns a dict mapping file paths to dicts of file node to first manifest
485 Returns a dict mapping file paths to dicts of file node to first manifest
484 node.
486 node.
485 """
487 """
486 ml = repo.manifestlog
488 ml = repo.manifestlog
487 fnodes = collections.defaultdict(dict)
489 fnodes = collections.defaultdict(dict)
488
490
489 progress = repo.ui.makeprogress(
491 progress = repo.ui.makeprogress(
490 _('scanning manifests'), total=len(manifestnodes))
492 _('scanning manifests'), total=len(manifestnodes))
491
493
492 with progress:
494 with progress:
493 for manifestnode in manifestnodes:
495 for manifestnode in manifestnodes:
494 m = ml.get(b'', manifestnode)
496 m = ml.get(b'', manifestnode)
495
497
496 # TODO this will pull in unwanted nodes because it takes the storage
498 # TODO this will pull in unwanted nodes because it takes the storage
497 # delta into consideration. What we really want is something that
499 # delta into consideration. What we really want is something that
498 # takes the delta between the manifest's parents. And ideally we
500 # takes the delta between the manifest's parents. And ideally we
499 # would ignore file nodes that are known locally. For now, ignore
501 # would ignore file nodes that are known locally. For now, ignore
500 # both these limitations. This will result in incremental fetches
502 # both these limitations. This will result in incremental fetches
501 # requesting data we already have. So this is far from ideal.
503 # requesting data we already have. So this is far from ideal.
502 md = m.readfast()
504 md = m.readfast()
503
505
504 for path, fnode in md.items():
506 for path, fnode in md.items():
505 if matcher(path):
507 if matcher(path):
506 fnodes[path].setdefault(fnode, manifestnode)
508 fnodes[path].setdefault(fnode, manifestnode)
507
509
508 progress.increment()
510 progress.increment()
509
511
510 return fnodes
512 return fnodes
511
513
512 def _fetchfiles(repo, tr, remote, fnodes, linkrevs):
514 def _fetchfiles(repo, tr, remote, fnodes, linkrevs):
513 """Fetch file data from explicit file revisions."""
515 """Fetch file data from explicit file revisions."""
514 def iterrevisions(objs, progress):
516 def iterrevisions(objs, progress):
515 for filerevision in objs:
517 for filerevision in objs:
516 node = filerevision[b'node']
518 node = filerevision[b'node']
517
519
518 extrafields = {}
520 extrafields = {}
519
521
520 for field, size in filerevision.get(b'fieldsfollowing', []):
522 for field, size in filerevision.get(b'fieldsfollowing', []):
521 extrafields[field] = next(objs)
523 extrafields[field] = next(objs)
522
524
523 if b'delta' in extrafields:
525 if b'delta' in extrafields:
524 basenode = filerevision[b'deltabasenode']
526 basenode = filerevision[b'deltabasenode']
525 delta = extrafields[b'delta']
527 delta = extrafields[b'delta']
526 elif b'revision' in extrafields:
528 elif b'revision' in extrafields:
527 basenode = nullid
529 basenode = nullid
528 revision = extrafields[b'revision']
530 revision = extrafields[b'revision']
529 delta = mdiff.trivialdiffheader(len(revision)) + revision
531 delta = mdiff.trivialdiffheader(len(revision)) + revision
530 else:
532 else:
531 continue
533 continue
532
534
533 yield (
535 yield (
534 node,
536 node,
535 filerevision[b'parents'][0],
537 filerevision[b'parents'][0],
536 filerevision[b'parents'][1],
538 filerevision[b'parents'][1],
537 node,
539 node,
538 basenode,
540 basenode,
539 delta,
541 delta,
540 # Flags not yet supported.
542 # Flags not yet supported.
541 0,
543 0,
542 )
544 )
543
545
544 progress.increment()
546 progress.increment()
545
547
546 progress = repo.ui.makeprogress(
548 progress = repo.ui.makeprogress(
547 _('files'), unit=_('chunks'),
549 _('files'), unit=_('chunks'),
548 total=sum(len(v) for v in fnodes.itervalues()))
550 total=sum(len(v) for v in fnodes.itervalues()))
549
551
550 # TODO make batch size configurable
552 # TODO make batch size configurable
551 batchsize = 10000
553 batchsize = 10000
552 fnodeslist = [x for x in sorted(fnodes.items())]
554 fnodeslist = [x for x in sorted(fnodes.items())]
553
555
554 for i in pycompat.xrange(0, len(fnodeslist), batchsize):
556 for i in pycompat.xrange(0, len(fnodeslist), batchsize):
555 batch = [x for x in fnodeslist[i:i + batchsize]]
557 batch = [x for x in fnodeslist[i:i + batchsize]]
556 if not batch:
558 if not batch:
557 continue
559 continue
558
560
559 with remote.commandexecutor() as e:
561 with remote.commandexecutor() as e:
560 fs = []
562 fs = []
561 locallinkrevs = {}
563 locallinkrevs = {}
562
564
563 for path, nodes in batch:
565 for path, nodes in batch:
564 fs.append((path, e.callcommand(b'filedata', {
566 fs.append((path, e.callcommand(b'filedata', {
565 b'path': path,
567 b'path': path,
566 b'nodes': sorted(nodes),
568 b'nodes': sorted(nodes),
567 b'fields': {b'parents', b'revision'},
569 b'fields': {b'parents', b'revision'},
568 b'haveparents': True,
570 b'haveparents': True,
569 })))
571 })))
570
572
571 locallinkrevs[path] = {
573 locallinkrevs[path] = {
572 node: linkrevs[manifestnode]
574 node: linkrevs[manifestnode]
573 for node, manifestnode in nodes.iteritems()}
575 for node, manifestnode in nodes.iteritems()}
574
576
575 for path, f in fs:
577 for path, f in fs:
576 objs = f.result()
578 objs = f.result()
577
579
578 # Chomp off header objects.
580 # Chomp off header objects.
579 next(objs)
581 next(objs)
580
582
581 store = repo.file(path)
583 store = repo.file(path)
582 store.addgroup(
584 store.addgroup(
583 iterrevisions(objs, progress),
585 iterrevisions(objs, progress),
584 locallinkrevs[path].__getitem__,
586 locallinkrevs[path].__getitem__,
585 weakref.proxy(tr))
587 weakref.proxy(tr))
586
588
587 def _fetchfilesfromcsets(repo, tr, remote, pathfilter, fnodes, csets,
589 def _fetchfilesfromcsets(repo, tr, remote, pathfilter, fnodes, csets,
588 manlinkrevs, shallow=False):
590 manlinkrevs, shallow=False):
589 """Fetch file data from explicit changeset revisions."""
591 """Fetch file data from explicit changeset revisions."""
590
592
591 def iterrevisions(objs, remaining, progress):
593 def iterrevisions(objs, remaining, progress):
592 while remaining:
594 while remaining:
593 filerevision = next(objs)
595 filerevision = next(objs)
594
596
595 node = filerevision[b'node']
597 node = filerevision[b'node']
596
598
597 extrafields = {}
599 extrafields = {}
598
600
599 for field, size in filerevision.get(b'fieldsfollowing', []):
601 for field, size in filerevision.get(b'fieldsfollowing', []):
600 extrafields[field] = next(objs)
602 extrafields[field] = next(objs)
601
603
602 if b'delta' in extrafields:
604 if b'delta' in extrafields:
603 basenode = filerevision[b'deltabasenode']
605 basenode = filerevision[b'deltabasenode']
604 delta = extrafields[b'delta']
606 delta = extrafields[b'delta']
605 elif b'revision' in extrafields:
607 elif b'revision' in extrafields:
606 basenode = nullid
608 basenode = nullid
607 revision = extrafields[b'revision']
609 revision = extrafields[b'revision']
608 delta = mdiff.trivialdiffheader(len(revision)) + revision
610 delta = mdiff.trivialdiffheader(len(revision)) + revision
609 else:
611 else:
610 continue
612 continue
611
613
612 if b'linknode' in filerevision:
614 if b'linknode' in filerevision:
613 linknode = filerevision[b'linknode']
615 linknode = filerevision[b'linknode']
614 else:
616 else:
615 linknode = node
617 linknode = node
616
618
617 yield (
619 yield (
618 node,
620 node,
619 filerevision[b'parents'][0],
621 filerevision[b'parents'][0],
620 filerevision[b'parents'][1],
622 filerevision[b'parents'][1],
621 linknode,
623 linknode,
622 basenode,
624 basenode,
623 delta,
625 delta,
624 # Flags not yet supported.
626 # Flags not yet supported.
625 0,
627 0,
626 )
628 )
627
629
628 progress.increment()
630 progress.increment()
629 remaining -= 1
631 remaining -= 1
630
632
631 progress = repo.ui.makeprogress(
633 progress = repo.ui.makeprogress(
632 _('files'), unit=_('chunks'),
634 _('files'), unit=_('chunks'),
633 total=sum(len(v) for v in fnodes.itervalues()))
635 total=sum(len(v) for v in fnodes.itervalues()))
634
636
635 commandmeta = remote.apidescriptor[b'commands'][b'filesdata']
637 commandmeta = remote.apidescriptor[b'commands'][b'filesdata']
636 batchsize = commandmeta.get(b'recommendedbatchsize', 50000)
638 batchsize = commandmeta.get(b'recommendedbatchsize', 50000)
637
639
638 shallowfiles = repository.REPO_FEATURE_SHALLOW_FILE_STORAGE in repo.features
640 shallowfiles = repository.REPO_FEATURE_SHALLOW_FILE_STORAGE in repo.features
639 fields = {b'parents', b'revision'}
641 fields = {b'parents', b'revision'}
640 clrev = repo.changelog.rev
642 clrev = repo.changelog.rev
641
643
642 # There are no guarantees that we'll have ancestor revisions if
644 # There are no guarantees that we'll have ancestor revisions if
643 # a) this repo has shallow file storage b) shallow data fetching is enabled.
645 # a) this repo has shallow file storage b) shallow data fetching is enabled.
644 # Force remote to not delta against possibly unknown revisions when these
646 # Force remote to not delta against possibly unknown revisions when these
645 # conditions hold.
647 # conditions hold.
646 haveparents = not (shallowfiles or shallow)
648 haveparents = not (shallowfiles or shallow)
647
649
648 # Similarly, we may not have calculated linkrevs for all incoming file
650 # Similarly, we may not have calculated linkrevs for all incoming file
649 # revisions. Ask the remote to do work for us in this case.
651 # revisions. Ask the remote to do work for us in this case.
650 if not haveparents:
652 if not haveparents:
651 fields.add(b'linknode')
653 fields.add(b'linknode')
652
654
653 for i in pycompat.xrange(0, len(csets), batchsize):
655 for i in pycompat.xrange(0, len(csets), batchsize):
654 batch = [x for x in csets[i:i + batchsize]]
656 batch = [x for x in csets[i:i + batchsize]]
655 if not batch:
657 if not batch:
656 continue
658 continue
657
659
658 with remote.commandexecutor() as e:
660 with remote.commandexecutor() as e:
659 args = {
661 args = {
660 b'revisions': [{
662 b'revisions': [{
661 b'type': b'changesetexplicit',
663 b'type': b'changesetexplicit',
662 b'nodes': batch,
664 b'nodes': batch,
663 }],
665 }],
664 b'fields': fields,
666 b'fields': fields,
665 b'haveparents': haveparents,
667 b'haveparents': haveparents,
666 }
668 }
667
669
668 if pathfilter:
670 if pathfilter:
669 args[b'pathfilter'] = pathfilter
671 args[b'pathfilter'] = pathfilter
670
672
671 objs = e.callcommand(b'filesdata', args).result()
673 objs = e.callcommand(b'filesdata', args).result()
672
674
673 # First object is an overall header.
675 # First object is an overall header.
674 overall = next(objs)
676 overall = next(objs)
675
677
676 # We have overall['totalpaths'] segments.
678 # We have overall['totalpaths'] segments.
677 for i in pycompat.xrange(overall[b'totalpaths']):
679 for i in pycompat.xrange(overall[b'totalpaths']):
678 header = next(objs)
680 header = next(objs)
679
681
680 path = header[b'path']
682 path = header[b'path']
681 store = repo.file(path)
683 store = repo.file(path)
682
684
683 linkrevs = {
685 linkrevs = {
684 fnode: manlinkrevs[mnode]
686 fnode: manlinkrevs[mnode]
685 for fnode, mnode in fnodes[path].iteritems()}
687 for fnode, mnode in fnodes[path].iteritems()}
686
688
687 def getlinkrev(node):
689 def getlinkrev(node):
688 if node in linkrevs:
690 if node in linkrevs:
689 return linkrevs[node]
691 return linkrevs[node]
690 else:
692 else:
691 return clrev(node)
693 return clrev(node)
692
694
693 store.addgroup(iterrevisions(objs, header[b'totalitems'],
695 store.addgroup(iterrevisions(objs, header[b'totalitems'],
694 progress),
696 progress),
695 getlinkrev,
697 getlinkrev,
696 weakref.proxy(tr),
698 weakref.proxy(tr),
697 maybemissingparents=shallow)
699 maybemissingparents=shallow)
@@ -1,242 +1,244
1 # filelog.py - file history class for mercurial
1 # filelog.py - file history class for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from .i18n import _
10 from .i18n import _
11 from .node import (
11 from .node import (
12 nullid,
12 nullid,
13 nullrev,
13 nullrev,
14 )
14 )
15 from . import (
15 from . import (
16 error,
16 error,
17 revlog,
18 )
19 from .interfaces import (
17 repository,
20 repository,
18 revlog,
19 )
21 )
20 from .utils import (
22 from .utils import (
21 interfaceutil,
23 interfaceutil,
22 storageutil,
24 storageutil,
23 )
25 )
24
26
25 @interfaceutil.implementer(repository.ifilestorage)
27 @interfaceutil.implementer(repository.ifilestorage)
26 class filelog(object):
28 class filelog(object):
27 def __init__(self, opener, path):
29 def __init__(self, opener, path):
28 self._revlog = revlog.revlog(opener,
30 self._revlog = revlog.revlog(opener,
29 '/'.join(('data', path + '.i')),
31 '/'.join(('data', path + '.i')),
30 censorable=True)
32 censorable=True)
31 # Full name of the user visible file, relative to the repository root.
33 # Full name of the user visible file, relative to the repository root.
32 # Used by LFS.
34 # Used by LFS.
33 self._revlog.filename = path
35 self._revlog.filename = path
34
36
35 def __len__(self):
37 def __len__(self):
36 return len(self._revlog)
38 return len(self._revlog)
37
39
38 def __iter__(self):
40 def __iter__(self):
39 return self._revlog.__iter__()
41 return self._revlog.__iter__()
40
42
41 def hasnode(self, node):
43 def hasnode(self, node):
42 if node in (nullid, nullrev):
44 if node in (nullid, nullrev):
43 return False
45 return False
44
46
45 try:
47 try:
46 self._revlog.rev(node)
48 self._revlog.rev(node)
47 return True
49 return True
48 except (TypeError, ValueError, IndexError, error.LookupError):
50 except (TypeError, ValueError, IndexError, error.LookupError):
49 return False
51 return False
50
52
51 def revs(self, start=0, stop=None):
53 def revs(self, start=0, stop=None):
52 return self._revlog.revs(start=start, stop=stop)
54 return self._revlog.revs(start=start, stop=stop)
53
55
54 def parents(self, node):
56 def parents(self, node):
55 return self._revlog.parents(node)
57 return self._revlog.parents(node)
56
58
57 def parentrevs(self, rev):
59 def parentrevs(self, rev):
58 return self._revlog.parentrevs(rev)
60 return self._revlog.parentrevs(rev)
59
61
60 def rev(self, node):
62 def rev(self, node):
61 return self._revlog.rev(node)
63 return self._revlog.rev(node)
62
64
63 def node(self, rev):
65 def node(self, rev):
64 return self._revlog.node(rev)
66 return self._revlog.node(rev)
65
67
66 def lookup(self, node):
68 def lookup(self, node):
67 return storageutil.fileidlookup(self._revlog, node,
69 return storageutil.fileidlookup(self._revlog, node,
68 self._revlog.indexfile)
70 self._revlog.indexfile)
69
71
70 def linkrev(self, rev):
72 def linkrev(self, rev):
71 return self._revlog.linkrev(rev)
73 return self._revlog.linkrev(rev)
72
74
73 def commonancestorsheads(self, node1, node2):
75 def commonancestorsheads(self, node1, node2):
74 return self._revlog.commonancestorsheads(node1, node2)
76 return self._revlog.commonancestorsheads(node1, node2)
75
77
76 # Used by dagop.blockdescendants().
78 # Used by dagop.blockdescendants().
77 def descendants(self, revs):
79 def descendants(self, revs):
78 return self._revlog.descendants(revs)
80 return self._revlog.descendants(revs)
79
81
80 def heads(self, start=None, stop=None):
82 def heads(self, start=None, stop=None):
81 return self._revlog.heads(start, stop)
83 return self._revlog.heads(start, stop)
82
84
83 # Used by hgweb, children extension.
85 # Used by hgweb, children extension.
84 def children(self, node):
86 def children(self, node):
85 return self._revlog.children(node)
87 return self._revlog.children(node)
86
88
87 def iscensored(self, rev):
89 def iscensored(self, rev):
88 return self._revlog.iscensored(rev)
90 return self._revlog.iscensored(rev)
89
91
90 def revision(self, node, _df=None, raw=False):
92 def revision(self, node, _df=None, raw=False):
91 return self._revlog.revision(node, _df=_df, raw=raw)
93 return self._revlog.revision(node, _df=_df, raw=raw)
92
94
93 def rawdata(self, node, _df=None):
95 def rawdata(self, node, _df=None):
94 return self._revlog.rawdata(node, _df=_df)
96 return self._revlog.rawdata(node, _df=_df)
95
97
96 def emitrevisions(self, nodes, nodesorder=None,
98 def emitrevisions(self, nodes, nodesorder=None,
97 revisiondata=False, assumehaveparentrevisions=False,
99 revisiondata=False, assumehaveparentrevisions=False,
98 deltamode=repository.CG_DELTAMODE_STD):
100 deltamode=repository.CG_DELTAMODE_STD):
99 return self._revlog.emitrevisions(
101 return self._revlog.emitrevisions(
100 nodes, nodesorder=nodesorder, revisiondata=revisiondata,
102 nodes, nodesorder=nodesorder, revisiondata=revisiondata,
101 assumehaveparentrevisions=assumehaveparentrevisions,
103 assumehaveparentrevisions=assumehaveparentrevisions,
102 deltamode=deltamode)
104 deltamode=deltamode)
103
105
104 def addrevision(self, revisiondata, transaction, linkrev, p1, p2,
106 def addrevision(self, revisiondata, transaction, linkrev, p1, p2,
105 node=None, flags=revlog.REVIDX_DEFAULT_FLAGS,
107 node=None, flags=revlog.REVIDX_DEFAULT_FLAGS,
106 cachedelta=None):
108 cachedelta=None):
107 return self._revlog.addrevision(revisiondata, transaction, linkrev,
109 return self._revlog.addrevision(revisiondata, transaction, linkrev,
108 p1, p2, node=node, flags=flags,
110 p1, p2, node=node, flags=flags,
109 cachedelta=cachedelta)
111 cachedelta=cachedelta)
110
112
111 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None,
113 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None,
112 maybemissingparents=False):
114 maybemissingparents=False):
113 if maybemissingparents:
115 if maybemissingparents:
114 raise error.Abort(_('revlog storage does not support missing '
116 raise error.Abort(_('revlog storage does not support missing '
115 'parents write mode'))
117 'parents write mode'))
116
118
117 return self._revlog.addgroup(deltas, linkmapper, transaction,
119 return self._revlog.addgroup(deltas, linkmapper, transaction,
118 addrevisioncb=addrevisioncb)
120 addrevisioncb=addrevisioncb)
119
121
120 def getstrippoint(self, minlink):
122 def getstrippoint(self, minlink):
121 return self._revlog.getstrippoint(minlink)
123 return self._revlog.getstrippoint(minlink)
122
124
123 def strip(self, minlink, transaction):
125 def strip(self, minlink, transaction):
124 return self._revlog.strip(minlink, transaction)
126 return self._revlog.strip(minlink, transaction)
125
127
126 def censorrevision(self, tr, node, tombstone=b''):
128 def censorrevision(self, tr, node, tombstone=b''):
127 return self._revlog.censorrevision(tr, node, tombstone=tombstone)
129 return self._revlog.censorrevision(tr, node, tombstone=tombstone)
128
130
129 def files(self):
131 def files(self):
130 return self._revlog.files()
132 return self._revlog.files()
131
133
132 def read(self, node):
134 def read(self, node):
133 return storageutil.filtermetadata(self.revision(node))
135 return storageutil.filtermetadata(self.revision(node))
134
136
135 def add(self, text, meta, transaction, link, p1=None, p2=None):
137 def add(self, text, meta, transaction, link, p1=None, p2=None):
136 if meta or text.startswith('\1\n'):
138 if meta or text.startswith('\1\n'):
137 text = storageutil.packmeta(meta, text)
139 text = storageutil.packmeta(meta, text)
138 return self.addrevision(text, transaction, link, p1, p2)
140 return self.addrevision(text, transaction, link, p1, p2)
139
141
140 def renamed(self, node):
142 def renamed(self, node):
141 return storageutil.filerevisioncopied(self, node)
143 return storageutil.filerevisioncopied(self, node)
142
144
143 def size(self, rev):
145 def size(self, rev):
144 """return the size of a given revision"""
146 """return the size of a given revision"""
145
147
146 # for revisions with renames, we have to go the slow way
148 # for revisions with renames, we have to go the slow way
147 node = self.node(rev)
149 node = self.node(rev)
148 if self.renamed(node):
150 if self.renamed(node):
149 return len(self.read(node))
151 return len(self.read(node))
150 if self.iscensored(rev):
152 if self.iscensored(rev):
151 return 0
153 return 0
152
154
153 # XXX if self.read(node).startswith("\1\n"), this returns (size+4)
155 # XXX if self.read(node).startswith("\1\n"), this returns (size+4)
154 return self._revlog.size(rev)
156 return self._revlog.size(rev)
155
157
156 def cmp(self, node, text):
158 def cmp(self, node, text):
157 """compare text with a given file revision
159 """compare text with a given file revision
158
160
159 returns True if text is different than what is stored.
161 returns True if text is different than what is stored.
160 """
162 """
161 return not storageutil.filedataequivalent(self, node, text)
163 return not storageutil.filedataequivalent(self, node, text)
162
164
163 def verifyintegrity(self, state):
165 def verifyintegrity(self, state):
164 return self._revlog.verifyintegrity(state)
166 return self._revlog.verifyintegrity(state)
165
167
166 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
168 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
167 revisionscount=False, trackedsize=False,
169 revisionscount=False, trackedsize=False,
168 storedsize=False):
170 storedsize=False):
169 return self._revlog.storageinfo(
171 return self._revlog.storageinfo(
170 exclusivefiles=exclusivefiles, sharedfiles=sharedfiles,
172 exclusivefiles=exclusivefiles, sharedfiles=sharedfiles,
171 revisionscount=revisionscount, trackedsize=trackedsize,
173 revisionscount=revisionscount, trackedsize=trackedsize,
172 storedsize=storedsize)
174 storedsize=storedsize)
173
175
174 # TODO these aren't part of the interface and aren't internal methods.
176 # TODO these aren't part of the interface and aren't internal methods.
175 # Callers should be fixed to not use them.
177 # Callers should be fixed to not use them.
176
178
177 # Used by bundlefilelog, unionfilelog.
179 # Used by bundlefilelog, unionfilelog.
178 @property
180 @property
179 def indexfile(self):
181 def indexfile(self):
180 return self._revlog.indexfile
182 return self._revlog.indexfile
181
183
182 @indexfile.setter
184 @indexfile.setter
183 def indexfile(self, value):
185 def indexfile(self, value):
184 self._revlog.indexfile = value
186 self._revlog.indexfile = value
185
187
186 # Used by repo upgrade.
188 # Used by repo upgrade.
187 def clone(self, tr, destrevlog, **kwargs):
189 def clone(self, tr, destrevlog, **kwargs):
188 if not isinstance(destrevlog, filelog):
190 if not isinstance(destrevlog, filelog):
189 raise error.ProgrammingError('expected filelog to clone()')
191 raise error.ProgrammingError('expected filelog to clone()')
190
192
191 return self._revlog.clone(tr, destrevlog._revlog, **kwargs)
193 return self._revlog.clone(tr, destrevlog._revlog, **kwargs)
192
194
193 class narrowfilelog(filelog):
195 class narrowfilelog(filelog):
194 """Filelog variation to be used with narrow stores."""
196 """Filelog variation to be used with narrow stores."""
195
197
196 def __init__(self, opener, path, narrowmatch):
198 def __init__(self, opener, path, narrowmatch):
197 super(narrowfilelog, self).__init__(opener, path)
199 super(narrowfilelog, self).__init__(opener, path)
198 self._narrowmatch = narrowmatch
200 self._narrowmatch = narrowmatch
199
201
200 def renamed(self, node):
202 def renamed(self, node):
201 res = super(narrowfilelog, self).renamed(node)
203 res = super(narrowfilelog, self).renamed(node)
202
204
203 # Renames that come from outside the narrowspec are problematic
205 # Renames that come from outside the narrowspec are problematic
204 # because we may lack the base text for the rename. This can result
206 # because we may lack the base text for the rename. This can result
205 # in code attempting to walk the ancestry or compute a diff
207 # in code attempting to walk the ancestry or compute a diff
206 # encountering a missing revision. We address this by silently
208 # encountering a missing revision. We address this by silently
207 # removing rename metadata if the source file is outside the
209 # removing rename metadata if the source file is outside the
208 # narrow spec.
210 # narrow spec.
209 #
211 #
210 # A better solution would be to see if the base revision is available,
212 # A better solution would be to see if the base revision is available,
211 # rather than assuming it isn't.
213 # rather than assuming it isn't.
212 #
214 #
213 # An even better solution would be to teach all consumers of rename
215 # An even better solution would be to teach all consumers of rename
214 # metadata that the base revision may not be available.
216 # metadata that the base revision may not be available.
215 #
217 #
216 # TODO consider better ways of doing this.
218 # TODO consider better ways of doing this.
217 if res and not self._narrowmatch(res[0]):
219 if res and not self._narrowmatch(res[0]):
218 return None
220 return None
219
221
220 return res
222 return res
221
223
222 def size(self, rev):
224 def size(self, rev):
223 # Because we have a custom renamed() that may lie, we need to call
225 # Because we have a custom renamed() that may lie, we need to call
224 # the base renamed() to report accurate results.
226 # the base renamed() to report accurate results.
225 node = self.node(rev)
227 node = self.node(rev)
226 if super(narrowfilelog, self).renamed(node):
228 if super(narrowfilelog, self).renamed(node):
227 return len(self.read(node))
229 return len(self.read(node))
228 else:
230 else:
229 return super(narrowfilelog, self).size(rev)
231 return super(narrowfilelog, self).size(rev)
230
232
231 def cmp(self, node, text):
233 def cmp(self, node, text):
232 different = super(narrowfilelog, self).cmp(node, text)
234 different = super(narrowfilelog, self).cmp(node, text)
233
235
234 # Because renamed() may lie, we may get false positives for
236 # Because renamed() may lie, we may get false positives for
235 # different content. Check for this by comparing against the original
237 # different content. Check for this by comparing against the original
236 # renamed() implementation.
238 # renamed() implementation.
237 if different:
239 if different:
238 if super(narrowfilelog, self).renamed(node):
240 if super(narrowfilelog, self).renamed(node):
239 t2 = self.read(node)
241 t2 = self.read(node)
240 return t2 != text
242 return t2 != text
241
243
242 return different
244 return different
@@ -1,1237 +1,1240
1 # hg.py - repository classes for mercurial
1 # hg.py - repository classes for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import shutil
14 import shutil
15 import stat
15 import stat
16
16
17 from .i18n import _
17 from .i18n import _
18 from .node import (
18 from .node import (
19 nullid,
19 nullid,
20 )
20 )
21
21
22 from . import (
22 from . import (
23 bookmarks,
23 bookmarks,
24 bundlerepo,
24 bundlerepo,
25 cacheutil,
25 cacheutil,
26 cmdutil,
26 cmdutil,
27 destutil,
27 destutil,
28 discovery,
28 discovery,
29 error,
29 error,
30 exchange,
30 exchange,
31 extensions,
31 extensions,
32 httppeer,
32 httppeer,
33 localrepo,
33 localrepo,
34 lock,
34 lock,
35 logcmdutil,
35 logcmdutil,
36 logexchange,
36 logexchange,
37 merge as mergemod,
37 merge as mergemod,
38 narrowspec,
38 narrowspec,
39 node,
39 node,
40 phases,
40 phases,
41 pycompat,
41 pycompat,
42 repository as repositorymod,
43 scmutil,
42 scmutil,
44 sshpeer,
43 sshpeer,
45 statichttprepo,
44 statichttprepo,
46 ui as uimod,
45 ui as uimod,
47 unionrepo,
46 unionrepo,
48 url,
47 url,
49 util,
48 util,
50 verify as verifymod,
49 verify as verifymod,
51 vfs as vfsmod,
50 vfs as vfsmod,
52 )
51 )
53
52
53 from .interfaces import (
54 repository as repositorymod,
55 )
56
54 release = lock.release
57 release = lock.release
55
58
56 # shared features
59 # shared features
57 sharedbookmarks = 'bookmarks'
60 sharedbookmarks = 'bookmarks'
58
61
59 def _local(path):
62 def _local(path):
60 path = util.expandpath(util.urllocalpath(path))
63 path = util.expandpath(util.urllocalpath(path))
61
64
62 try:
65 try:
63 isfile = os.path.isfile(path)
66 isfile = os.path.isfile(path)
64 # Python 2 raises TypeError, Python 3 ValueError.
67 # Python 2 raises TypeError, Python 3 ValueError.
65 except (TypeError, ValueError) as e:
68 except (TypeError, ValueError) as e:
66 raise error.Abort(_('invalid path %s: %s') % (
69 raise error.Abort(_('invalid path %s: %s') % (
67 path, pycompat.bytestr(e)))
70 path, pycompat.bytestr(e)))
68
71
69 return isfile and bundlerepo or localrepo
72 return isfile and bundlerepo or localrepo
70
73
71 def addbranchrevs(lrepo, other, branches, revs):
74 def addbranchrevs(lrepo, other, branches, revs):
72 peer = other.peer() # a courtesy to callers using a localrepo for other
75 peer = other.peer() # a courtesy to callers using a localrepo for other
73 hashbranch, branches = branches
76 hashbranch, branches = branches
74 if not hashbranch and not branches:
77 if not hashbranch and not branches:
75 x = revs or None
78 x = revs or None
76 if revs:
79 if revs:
77 y = revs[0]
80 y = revs[0]
78 else:
81 else:
79 y = None
82 y = None
80 return x, y
83 return x, y
81 if revs:
84 if revs:
82 revs = list(revs)
85 revs = list(revs)
83 else:
86 else:
84 revs = []
87 revs = []
85
88
86 if not peer.capable('branchmap'):
89 if not peer.capable('branchmap'):
87 if branches:
90 if branches:
88 raise error.Abort(_("remote branch lookup not supported"))
91 raise error.Abort(_("remote branch lookup not supported"))
89 revs.append(hashbranch)
92 revs.append(hashbranch)
90 return revs, revs[0]
93 return revs, revs[0]
91
94
92 with peer.commandexecutor() as e:
95 with peer.commandexecutor() as e:
93 branchmap = e.callcommand('branchmap', {}).result()
96 branchmap = e.callcommand('branchmap', {}).result()
94
97
95 def primary(branch):
98 def primary(branch):
96 if branch == '.':
99 if branch == '.':
97 if not lrepo:
100 if not lrepo:
98 raise error.Abort(_("dirstate branch not accessible"))
101 raise error.Abort(_("dirstate branch not accessible"))
99 branch = lrepo.dirstate.branch()
102 branch = lrepo.dirstate.branch()
100 if branch in branchmap:
103 if branch in branchmap:
101 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
104 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
102 return True
105 return True
103 else:
106 else:
104 return False
107 return False
105
108
106 for branch in branches:
109 for branch in branches:
107 if not primary(branch):
110 if not primary(branch):
108 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
111 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
109 if hashbranch:
112 if hashbranch:
110 if not primary(hashbranch):
113 if not primary(hashbranch):
111 revs.append(hashbranch)
114 revs.append(hashbranch)
112 return revs, revs[0]
115 return revs, revs[0]
113
116
114 def parseurl(path, branches=None):
117 def parseurl(path, branches=None):
115 '''parse url#branch, returning (url, (branch, branches))'''
118 '''parse url#branch, returning (url, (branch, branches))'''
116
119
117 u = util.url(path)
120 u = util.url(path)
118 branch = None
121 branch = None
119 if u.fragment:
122 if u.fragment:
120 branch = u.fragment
123 branch = u.fragment
121 u.fragment = None
124 u.fragment = None
122 return bytes(u), (branch, branches or [])
125 return bytes(u), (branch, branches or [])
123
126
124 schemes = {
127 schemes = {
125 'bundle': bundlerepo,
128 'bundle': bundlerepo,
126 'union': unionrepo,
129 'union': unionrepo,
127 'file': _local,
130 'file': _local,
128 'http': httppeer,
131 'http': httppeer,
129 'https': httppeer,
132 'https': httppeer,
130 'ssh': sshpeer,
133 'ssh': sshpeer,
131 'static-http': statichttprepo,
134 'static-http': statichttprepo,
132 }
135 }
133
136
134 def _peerlookup(path):
137 def _peerlookup(path):
135 u = util.url(path)
138 u = util.url(path)
136 scheme = u.scheme or 'file'
139 scheme = u.scheme or 'file'
137 thing = schemes.get(scheme) or schemes['file']
140 thing = schemes.get(scheme) or schemes['file']
138 try:
141 try:
139 return thing(path)
142 return thing(path)
140 except TypeError:
143 except TypeError:
141 # we can't test callable(thing) because 'thing' can be an unloaded
144 # we can't test callable(thing) because 'thing' can be an unloaded
142 # module that implements __call__
145 # module that implements __call__
143 if not util.safehasattr(thing, 'instance'):
146 if not util.safehasattr(thing, 'instance'):
144 raise
147 raise
145 return thing
148 return thing
146
149
147 def islocal(repo):
150 def islocal(repo):
148 '''return true if repo (or path pointing to repo) is local'''
151 '''return true if repo (or path pointing to repo) is local'''
149 if isinstance(repo, bytes):
152 if isinstance(repo, bytes):
150 try:
153 try:
151 return _peerlookup(repo).islocal(repo)
154 return _peerlookup(repo).islocal(repo)
152 except AttributeError:
155 except AttributeError:
153 return False
156 return False
154 return repo.local()
157 return repo.local()
155
158
156 def openpath(ui, path, sendaccept=True):
159 def openpath(ui, path, sendaccept=True):
157 '''open path with open if local, url.open if remote'''
160 '''open path with open if local, url.open if remote'''
158 pathurl = util.url(path, parsequery=False, parsefragment=False)
161 pathurl = util.url(path, parsequery=False, parsefragment=False)
159 if pathurl.islocal():
162 if pathurl.islocal():
160 return util.posixfile(pathurl.localpath(), 'rb')
163 return util.posixfile(pathurl.localpath(), 'rb')
161 else:
164 else:
162 return url.open(ui, path, sendaccept=sendaccept)
165 return url.open(ui, path, sendaccept=sendaccept)
163
166
164 # a list of (ui, repo) functions called for wire peer initialization
167 # a list of (ui, repo) functions called for wire peer initialization
165 wirepeersetupfuncs = []
168 wirepeersetupfuncs = []
166
169
167 def _peerorrepo(ui, path, create=False, presetupfuncs=None,
170 def _peerorrepo(ui, path, create=False, presetupfuncs=None,
168 intents=None, createopts=None):
171 intents=None, createopts=None):
169 """return a repository object for the specified path"""
172 """return a repository object for the specified path"""
170 obj = _peerlookup(path).instance(ui, path, create, intents=intents,
173 obj = _peerlookup(path).instance(ui, path, create, intents=intents,
171 createopts=createopts)
174 createopts=createopts)
172 ui = getattr(obj, "ui", ui)
175 ui = getattr(obj, "ui", ui)
173 for f in presetupfuncs or []:
176 for f in presetupfuncs or []:
174 f(ui, obj)
177 f(ui, obj)
175 ui.log(b'extension', b'- executing reposetup hooks\n')
178 ui.log(b'extension', b'- executing reposetup hooks\n')
176 with util.timedcm('all reposetup') as allreposetupstats:
179 with util.timedcm('all reposetup') as allreposetupstats:
177 for name, module in extensions.extensions(ui):
180 for name, module in extensions.extensions(ui):
178 ui.log(b'extension', b' - running reposetup for %s\n', name)
181 ui.log(b'extension', b' - running reposetup for %s\n', name)
179 hook = getattr(module, 'reposetup', None)
182 hook = getattr(module, 'reposetup', None)
180 if hook:
183 if hook:
181 with util.timedcm('reposetup %r', name) as stats:
184 with util.timedcm('reposetup %r', name) as stats:
182 hook(ui, obj)
185 hook(ui, obj)
183 ui.log(b'extension', b' > reposetup for %s took %s\n',
186 ui.log(b'extension', b' > reposetup for %s took %s\n',
184 name, stats)
187 name, stats)
185 ui.log(b'extension', b'> all reposetup took %s\n', allreposetupstats)
188 ui.log(b'extension', b'> all reposetup took %s\n', allreposetupstats)
186 if not obj.local():
189 if not obj.local():
187 for f in wirepeersetupfuncs:
190 for f in wirepeersetupfuncs:
188 f(ui, obj)
191 f(ui, obj)
189 return obj
192 return obj
190
193
191 def repository(ui, path='', create=False, presetupfuncs=None, intents=None,
194 def repository(ui, path='', create=False, presetupfuncs=None, intents=None,
192 createopts=None):
195 createopts=None):
193 """return a repository object for the specified path"""
196 """return a repository object for the specified path"""
194 peer = _peerorrepo(ui, path, create, presetupfuncs=presetupfuncs,
197 peer = _peerorrepo(ui, path, create, presetupfuncs=presetupfuncs,
195 intents=intents, createopts=createopts)
198 intents=intents, createopts=createopts)
196 repo = peer.local()
199 repo = peer.local()
197 if not repo:
200 if not repo:
198 raise error.Abort(_("repository '%s' is not local") %
201 raise error.Abort(_("repository '%s' is not local") %
199 (path or peer.url()))
202 (path or peer.url()))
200 return repo.filtered('visible')
203 return repo.filtered('visible')
201
204
202 def peer(uiorrepo, opts, path, create=False, intents=None, createopts=None):
205 def peer(uiorrepo, opts, path, create=False, intents=None, createopts=None):
203 '''return a repository peer for the specified path'''
206 '''return a repository peer for the specified path'''
204 rui = remoteui(uiorrepo, opts)
207 rui = remoteui(uiorrepo, opts)
205 return _peerorrepo(rui, path, create, intents=intents,
208 return _peerorrepo(rui, path, create, intents=intents,
206 createopts=createopts).peer()
209 createopts=createopts).peer()
207
210
208 def defaultdest(source):
211 def defaultdest(source):
209 '''return default destination of clone if none is given
212 '''return default destination of clone if none is given
210
213
211 >>> defaultdest(b'foo')
214 >>> defaultdest(b'foo')
212 'foo'
215 'foo'
213 >>> defaultdest(b'/foo/bar')
216 >>> defaultdest(b'/foo/bar')
214 'bar'
217 'bar'
215 >>> defaultdest(b'/')
218 >>> defaultdest(b'/')
216 ''
219 ''
217 >>> defaultdest(b'')
220 >>> defaultdest(b'')
218 ''
221 ''
219 >>> defaultdest(b'http://example.org/')
222 >>> defaultdest(b'http://example.org/')
220 ''
223 ''
221 >>> defaultdest(b'http://example.org/foo/')
224 >>> defaultdest(b'http://example.org/foo/')
222 'foo'
225 'foo'
223 '''
226 '''
224 path = util.url(source).path
227 path = util.url(source).path
225 if not path:
228 if not path:
226 return ''
229 return ''
227 return os.path.basename(os.path.normpath(path))
230 return os.path.basename(os.path.normpath(path))
228
231
229 def sharedreposource(repo):
232 def sharedreposource(repo):
230 """Returns repository object for source repository of a shared repo.
233 """Returns repository object for source repository of a shared repo.
231
234
232 If repo is not a shared repository, returns None.
235 If repo is not a shared repository, returns None.
233 """
236 """
234 if repo.sharedpath == repo.path:
237 if repo.sharedpath == repo.path:
235 return None
238 return None
236
239
237 if util.safehasattr(repo, 'srcrepo') and repo.srcrepo:
240 if util.safehasattr(repo, 'srcrepo') and repo.srcrepo:
238 return repo.srcrepo
241 return repo.srcrepo
239
242
240 # the sharedpath always ends in the .hg; we want the path to the repo
243 # the sharedpath always ends in the .hg; we want the path to the repo
241 source = repo.vfs.split(repo.sharedpath)[0]
244 source = repo.vfs.split(repo.sharedpath)[0]
242 srcurl, branches = parseurl(source)
245 srcurl, branches = parseurl(source)
243 srcrepo = repository(repo.ui, srcurl)
246 srcrepo = repository(repo.ui, srcurl)
244 repo.srcrepo = srcrepo
247 repo.srcrepo = srcrepo
245 return srcrepo
248 return srcrepo
246
249
247 def share(ui, source, dest=None, update=True, bookmarks=True, defaultpath=None,
250 def share(ui, source, dest=None, update=True, bookmarks=True, defaultpath=None,
248 relative=False):
251 relative=False):
249 '''create a shared repository'''
252 '''create a shared repository'''
250
253
251 if not islocal(source):
254 if not islocal(source):
252 raise error.Abort(_('can only share local repositories'))
255 raise error.Abort(_('can only share local repositories'))
253
256
254 if not dest:
257 if not dest:
255 dest = defaultdest(source)
258 dest = defaultdest(source)
256 else:
259 else:
257 dest = ui.expandpath(dest)
260 dest = ui.expandpath(dest)
258
261
259 if isinstance(source, bytes):
262 if isinstance(source, bytes):
260 origsource = ui.expandpath(source)
263 origsource = ui.expandpath(source)
261 source, branches = parseurl(origsource)
264 source, branches = parseurl(origsource)
262 srcrepo = repository(ui, source)
265 srcrepo = repository(ui, source)
263 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
266 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
264 else:
267 else:
265 srcrepo = source.local()
268 srcrepo = source.local()
266 checkout = None
269 checkout = None
267
270
268 shareditems = set()
271 shareditems = set()
269 if bookmarks:
272 if bookmarks:
270 shareditems.add(sharedbookmarks)
273 shareditems.add(sharedbookmarks)
271
274
272 r = repository(ui, dest, create=True, createopts={
275 r = repository(ui, dest, create=True, createopts={
273 'sharedrepo': srcrepo,
276 'sharedrepo': srcrepo,
274 'sharedrelative': relative,
277 'sharedrelative': relative,
275 'shareditems': shareditems,
278 'shareditems': shareditems,
276 })
279 })
277
280
278 postshare(srcrepo, r, defaultpath=defaultpath)
281 postshare(srcrepo, r, defaultpath=defaultpath)
279 r = repository(ui, dest)
282 r = repository(ui, dest)
280 _postshareupdate(r, update, checkout=checkout)
283 _postshareupdate(r, update, checkout=checkout)
281 return r
284 return r
282
285
283 def unshare(ui, repo):
286 def unshare(ui, repo):
284 """convert a shared repository to a normal one
287 """convert a shared repository to a normal one
285
288
286 Copy the store data to the repo and remove the sharedpath data.
289 Copy the store data to the repo and remove the sharedpath data.
287
290
288 Returns a new repository object representing the unshared repository.
291 Returns a new repository object representing the unshared repository.
289
292
290 The passed repository object is not usable after this function is
293 The passed repository object is not usable after this function is
291 called.
294 called.
292 """
295 """
293
296
294 with repo.lock():
297 with repo.lock():
295 # we use locks here because if we race with commit, we
298 # we use locks here because if we race with commit, we
296 # can end up with extra data in the cloned revlogs that's
299 # can end up with extra data in the cloned revlogs that's
297 # not pointed to by changesets, thus causing verify to
300 # not pointed to by changesets, thus causing verify to
298 # fail
301 # fail
299 destlock = copystore(ui, repo, repo.path)
302 destlock = copystore(ui, repo, repo.path)
300 with destlock or util.nullcontextmanager():
303 with destlock or util.nullcontextmanager():
301
304
302 sharefile = repo.vfs.join('sharedpath')
305 sharefile = repo.vfs.join('sharedpath')
303 util.rename(sharefile, sharefile + '.old')
306 util.rename(sharefile, sharefile + '.old')
304
307
305 repo.requirements.discard('shared')
308 repo.requirements.discard('shared')
306 repo.requirements.discard('relshared')
309 repo.requirements.discard('relshared')
307 repo._writerequirements()
310 repo._writerequirements()
308
311
309 # Removing share changes some fundamental properties of the repo instance.
312 # Removing share changes some fundamental properties of the repo instance.
310 # So we instantiate a new repo object and operate on it rather than
313 # So we instantiate a new repo object and operate on it rather than
311 # try to keep the existing repo usable.
314 # try to keep the existing repo usable.
312 newrepo = repository(repo.baseui, repo.root, create=False)
315 newrepo = repository(repo.baseui, repo.root, create=False)
313
316
314 # TODO: figure out how to access subrepos that exist, but were previously
317 # TODO: figure out how to access subrepos that exist, but were previously
315 # removed from .hgsub
318 # removed from .hgsub
316 c = newrepo['.']
319 c = newrepo['.']
317 subs = c.substate
320 subs = c.substate
318 for s in sorted(subs):
321 for s in sorted(subs):
319 c.sub(s).unshare()
322 c.sub(s).unshare()
320
323
321 localrepo.poisonrepository(repo)
324 localrepo.poisonrepository(repo)
322
325
323 return newrepo
326 return newrepo
324
327
325 def postshare(sourcerepo, destrepo, defaultpath=None):
328 def postshare(sourcerepo, destrepo, defaultpath=None):
326 """Called after a new shared repo is created.
329 """Called after a new shared repo is created.
327
330
328 The new repo only has a requirements file and pointer to the source.
331 The new repo only has a requirements file and pointer to the source.
329 This function configures additional shared data.
332 This function configures additional shared data.
330
333
331 Extensions can wrap this function and write additional entries to
334 Extensions can wrap this function and write additional entries to
332 destrepo/.hg/shared to indicate additional pieces of data to be shared.
335 destrepo/.hg/shared to indicate additional pieces of data to be shared.
333 """
336 """
334 default = defaultpath or sourcerepo.ui.config('paths', 'default')
337 default = defaultpath or sourcerepo.ui.config('paths', 'default')
335 if default:
338 if default:
336 template = ('[paths]\n'
339 template = ('[paths]\n'
337 'default = %s\n')
340 'default = %s\n')
338 destrepo.vfs.write('hgrc', util.tonativeeol(template % default))
341 destrepo.vfs.write('hgrc', util.tonativeeol(template % default))
339 if repositorymod.NARROW_REQUIREMENT in sourcerepo.requirements:
342 if repositorymod.NARROW_REQUIREMENT in sourcerepo.requirements:
340 with destrepo.wlock():
343 with destrepo.wlock():
341 narrowspec.copytoworkingcopy(destrepo)
344 narrowspec.copytoworkingcopy(destrepo)
342
345
343 def _postshareupdate(repo, update, checkout=None):
346 def _postshareupdate(repo, update, checkout=None):
344 """Maybe perform a working directory update after a shared repo is created.
347 """Maybe perform a working directory update after a shared repo is created.
345
348
346 ``update`` can be a boolean or a revision to update to.
349 ``update`` can be a boolean or a revision to update to.
347 """
350 """
348 if not update:
351 if not update:
349 return
352 return
350
353
351 repo.ui.status(_("updating working directory\n"))
354 repo.ui.status(_("updating working directory\n"))
352 if update is not True:
355 if update is not True:
353 checkout = update
356 checkout = update
354 for test in (checkout, 'default', 'tip'):
357 for test in (checkout, 'default', 'tip'):
355 if test is None:
358 if test is None:
356 continue
359 continue
357 try:
360 try:
358 uprev = repo.lookup(test)
361 uprev = repo.lookup(test)
359 break
362 break
360 except error.RepoLookupError:
363 except error.RepoLookupError:
361 continue
364 continue
362 _update(repo, uprev)
365 _update(repo, uprev)
363
366
364 def copystore(ui, srcrepo, destpath):
367 def copystore(ui, srcrepo, destpath):
365 '''copy files from store of srcrepo in destpath
368 '''copy files from store of srcrepo in destpath
366
369
367 returns destlock
370 returns destlock
368 '''
371 '''
369 destlock = None
372 destlock = None
370 try:
373 try:
371 hardlink = None
374 hardlink = None
372 topic = _('linking') if hardlink else _('copying')
375 topic = _('linking') if hardlink else _('copying')
373 with ui.makeprogress(topic, unit=_('files')) as progress:
376 with ui.makeprogress(topic, unit=_('files')) as progress:
374 num = 0
377 num = 0
375 srcpublishing = srcrepo.publishing()
378 srcpublishing = srcrepo.publishing()
376 srcvfs = vfsmod.vfs(srcrepo.sharedpath)
379 srcvfs = vfsmod.vfs(srcrepo.sharedpath)
377 dstvfs = vfsmod.vfs(destpath)
380 dstvfs = vfsmod.vfs(destpath)
378 for f in srcrepo.store.copylist():
381 for f in srcrepo.store.copylist():
379 if srcpublishing and f.endswith('phaseroots'):
382 if srcpublishing and f.endswith('phaseroots'):
380 continue
383 continue
381 dstbase = os.path.dirname(f)
384 dstbase = os.path.dirname(f)
382 if dstbase and not dstvfs.exists(dstbase):
385 if dstbase and not dstvfs.exists(dstbase):
383 dstvfs.mkdir(dstbase)
386 dstvfs.mkdir(dstbase)
384 if srcvfs.exists(f):
387 if srcvfs.exists(f):
385 if f.endswith('data'):
388 if f.endswith('data'):
386 # 'dstbase' may be empty (e.g. revlog format 0)
389 # 'dstbase' may be empty (e.g. revlog format 0)
387 lockfile = os.path.join(dstbase, "lock")
390 lockfile = os.path.join(dstbase, "lock")
388 # lock to avoid premature writing to the target
391 # lock to avoid premature writing to the target
389 destlock = lock.lock(dstvfs, lockfile)
392 destlock = lock.lock(dstvfs, lockfile)
390 hardlink, n = util.copyfiles(srcvfs.join(f), dstvfs.join(f),
393 hardlink, n = util.copyfiles(srcvfs.join(f), dstvfs.join(f),
391 hardlink, progress)
394 hardlink, progress)
392 num += n
395 num += n
393 if hardlink:
396 if hardlink:
394 ui.debug("linked %d files\n" % num)
397 ui.debug("linked %d files\n" % num)
395 else:
398 else:
396 ui.debug("copied %d files\n" % num)
399 ui.debug("copied %d files\n" % num)
397 return destlock
400 return destlock
398 except: # re-raises
401 except: # re-raises
399 release(destlock)
402 release(destlock)
400 raise
403 raise
401
404
402 def clonewithshare(ui, peeropts, sharepath, source, srcpeer, dest, pull=False,
405 def clonewithshare(ui, peeropts, sharepath, source, srcpeer, dest, pull=False,
403 rev=None, update=True, stream=False):
406 rev=None, update=True, stream=False):
404 """Perform a clone using a shared repo.
407 """Perform a clone using a shared repo.
405
408
406 The store for the repository will be located at <sharepath>/.hg. The
409 The store for the repository will be located at <sharepath>/.hg. The
407 specified revisions will be cloned or pulled from "source". A shared repo
410 specified revisions will be cloned or pulled from "source". A shared repo
408 will be created at "dest" and a working copy will be created if "update" is
411 will be created at "dest" and a working copy will be created if "update" is
409 True.
412 True.
410 """
413 """
411 revs = None
414 revs = None
412 if rev:
415 if rev:
413 if not srcpeer.capable('lookup'):
416 if not srcpeer.capable('lookup'):
414 raise error.Abort(_("src repository does not support "
417 raise error.Abort(_("src repository does not support "
415 "revision lookup and so doesn't "
418 "revision lookup and so doesn't "
416 "support clone by revision"))
419 "support clone by revision"))
417
420
418 # TODO this is batchable.
421 # TODO this is batchable.
419 remoterevs = []
422 remoterevs = []
420 for r in rev:
423 for r in rev:
421 with srcpeer.commandexecutor() as e:
424 with srcpeer.commandexecutor() as e:
422 remoterevs.append(e.callcommand('lookup', {
425 remoterevs.append(e.callcommand('lookup', {
423 'key': r,
426 'key': r,
424 }).result())
427 }).result())
425 revs = remoterevs
428 revs = remoterevs
426
429
427 # Obtain a lock before checking for or cloning the pooled repo otherwise
430 # Obtain a lock before checking for or cloning the pooled repo otherwise
428 # 2 clients may race creating or populating it.
431 # 2 clients may race creating or populating it.
429 pooldir = os.path.dirname(sharepath)
432 pooldir = os.path.dirname(sharepath)
430 # lock class requires the directory to exist.
433 # lock class requires the directory to exist.
431 try:
434 try:
432 util.makedir(pooldir, False)
435 util.makedir(pooldir, False)
433 except OSError as e:
436 except OSError as e:
434 if e.errno != errno.EEXIST:
437 if e.errno != errno.EEXIST:
435 raise
438 raise
436
439
437 poolvfs = vfsmod.vfs(pooldir)
440 poolvfs = vfsmod.vfs(pooldir)
438 basename = os.path.basename(sharepath)
441 basename = os.path.basename(sharepath)
439
442
440 with lock.lock(poolvfs, '%s.lock' % basename):
443 with lock.lock(poolvfs, '%s.lock' % basename):
441 if os.path.exists(sharepath):
444 if os.path.exists(sharepath):
442 ui.status(_('(sharing from existing pooled repository %s)\n') %
445 ui.status(_('(sharing from existing pooled repository %s)\n') %
443 basename)
446 basename)
444 else:
447 else:
445 ui.status(_('(sharing from new pooled repository %s)\n') % basename)
448 ui.status(_('(sharing from new pooled repository %s)\n') % basename)
446 # Always use pull mode because hardlinks in share mode don't work
449 # Always use pull mode because hardlinks in share mode don't work
447 # well. Never update because working copies aren't necessary in
450 # well. Never update because working copies aren't necessary in
448 # share mode.
451 # share mode.
449 clone(ui, peeropts, source, dest=sharepath, pull=True,
452 clone(ui, peeropts, source, dest=sharepath, pull=True,
450 revs=rev, update=False, stream=stream)
453 revs=rev, update=False, stream=stream)
451
454
452 # Resolve the value to put in [paths] section for the source.
455 # Resolve the value to put in [paths] section for the source.
453 if islocal(source):
456 if islocal(source):
454 defaultpath = os.path.abspath(util.urllocalpath(source))
457 defaultpath = os.path.abspath(util.urllocalpath(source))
455 else:
458 else:
456 defaultpath = source
459 defaultpath = source
457
460
458 sharerepo = repository(ui, path=sharepath)
461 sharerepo = repository(ui, path=sharepath)
459 destrepo = share(ui, sharerepo, dest=dest, update=False, bookmarks=False,
462 destrepo = share(ui, sharerepo, dest=dest, update=False, bookmarks=False,
460 defaultpath=defaultpath)
463 defaultpath=defaultpath)
461
464
462 # We need to perform a pull against the dest repo to fetch bookmarks
465 # We need to perform a pull against the dest repo to fetch bookmarks
463 # and other non-store data that isn't shared by default. In the case of
466 # and other non-store data that isn't shared by default. In the case of
464 # non-existing shared repo, this means we pull from the remote twice. This
467 # non-existing shared repo, this means we pull from the remote twice. This
465 # is a bit weird. But at the time it was implemented, there wasn't an easy
468 # is a bit weird. But at the time it was implemented, there wasn't an easy
466 # way to pull just non-changegroup data.
469 # way to pull just non-changegroup data.
467 exchange.pull(destrepo, srcpeer, heads=revs)
470 exchange.pull(destrepo, srcpeer, heads=revs)
468
471
469 _postshareupdate(destrepo, update)
472 _postshareupdate(destrepo, update)
470
473
471 return srcpeer, peer(ui, peeropts, dest)
474 return srcpeer, peer(ui, peeropts, dest)
472
475
473 # Recomputing branch cache might be slow on big repos,
476 # Recomputing branch cache might be slow on big repos,
474 # so just copy it
477 # so just copy it
475 def _copycache(srcrepo, dstcachedir, fname):
478 def _copycache(srcrepo, dstcachedir, fname):
476 """copy a cache from srcrepo to destcachedir (if it exists)"""
479 """copy a cache from srcrepo to destcachedir (if it exists)"""
477 srcbranchcache = srcrepo.vfs.join('cache/%s' % fname)
480 srcbranchcache = srcrepo.vfs.join('cache/%s' % fname)
478 dstbranchcache = os.path.join(dstcachedir, fname)
481 dstbranchcache = os.path.join(dstcachedir, fname)
479 if os.path.exists(srcbranchcache):
482 if os.path.exists(srcbranchcache):
480 if not os.path.exists(dstcachedir):
483 if not os.path.exists(dstcachedir):
481 os.mkdir(dstcachedir)
484 os.mkdir(dstcachedir)
482 util.copyfile(srcbranchcache, dstbranchcache)
485 util.copyfile(srcbranchcache, dstbranchcache)
483
486
484 def clone(ui, peeropts, source, dest=None, pull=False, revs=None,
487 def clone(ui, peeropts, source, dest=None, pull=False, revs=None,
485 update=True, stream=False, branch=None, shareopts=None,
488 update=True, stream=False, branch=None, shareopts=None,
486 storeincludepats=None, storeexcludepats=None, depth=None):
489 storeincludepats=None, storeexcludepats=None, depth=None):
487 """Make a copy of an existing repository.
490 """Make a copy of an existing repository.
488
491
489 Create a copy of an existing repository in a new directory. The
492 Create a copy of an existing repository in a new directory. The
490 source and destination are URLs, as passed to the repository
493 source and destination are URLs, as passed to the repository
491 function. Returns a pair of repository peers, the source and
494 function. Returns a pair of repository peers, the source and
492 newly created destination.
495 newly created destination.
493
496
494 The location of the source is added to the new repository's
497 The location of the source is added to the new repository's
495 .hg/hgrc file, as the default to be used for future pulls and
498 .hg/hgrc file, as the default to be used for future pulls and
496 pushes.
499 pushes.
497
500
498 If an exception is raised, the partly cloned/updated destination
501 If an exception is raised, the partly cloned/updated destination
499 repository will be deleted.
502 repository will be deleted.
500
503
501 Arguments:
504 Arguments:
502
505
503 source: repository object or URL
506 source: repository object or URL
504
507
505 dest: URL of destination repository to create (defaults to base
508 dest: URL of destination repository to create (defaults to base
506 name of source repository)
509 name of source repository)
507
510
508 pull: always pull from source repository, even in local case or if the
511 pull: always pull from source repository, even in local case or if the
509 server prefers streaming
512 server prefers streaming
510
513
511 stream: stream raw data uncompressed from repository (fast over
514 stream: stream raw data uncompressed from repository (fast over
512 LAN, slow over WAN)
515 LAN, slow over WAN)
513
516
514 revs: revision to clone up to (implies pull=True)
517 revs: revision to clone up to (implies pull=True)
515
518
516 update: update working directory after clone completes, if
519 update: update working directory after clone completes, if
517 destination is local repository (True means update to default rev,
520 destination is local repository (True means update to default rev,
518 anything else is treated as a revision)
521 anything else is treated as a revision)
519
522
520 branch: branches to clone
523 branch: branches to clone
521
524
522 shareopts: dict of options to control auto sharing behavior. The "pool" key
525 shareopts: dict of options to control auto sharing behavior. The "pool" key
523 activates auto sharing mode and defines the directory for stores. The
526 activates auto sharing mode and defines the directory for stores. The
524 "mode" key determines how to construct the directory name of the shared
527 "mode" key determines how to construct the directory name of the shared
525 repository. "identity" means the name is derived from the node of the first
528 repository. "identity" means the name is derived from the node of the first
526 changeset in the repository. "remote" means the name is derived from the
529 changeset in the repository. "remote" means the name is derived from the
527 remote's path/URL. Defaults to "identity."
530 remote's path/URL. Defaults to "identity."
528
531
529 storeincludepats and storeexcludepats: sets of file patterns to include and
532 storeincludepats and storeexcludepats: sets of file patterns to include and
530 exclude in the repository copy, respectively. If not defined, all files
533 exclude in the repository copy, respectively. If not defined, all files
531 will be included (a "full" clone). Otherwise a "narrow" clone containing
534 will be included (a "full" clone). Otherwise a "narrow" clone containing
532 only the requested files will be performed. If ``storeincludepats`` is not
535 only the requested files will be performed. If ``storeincludepats`` is not
533 defined but ``storeexcludepats`` is, ``storeincludepats`` is assumed to be
536 defined but ``storeexcludepats`` is, ``storeincludepats`` is assumed to be
534 ``path:.``. If both are empty sets, no files will be cloned.
537 ``path:.``. If both are empty sets, no files will be cloned.
535 """
538 """
536
539
537 if isinstance(source, bytes):
540 if isinstance(source, bytes):
538 origsource = ui.expandpath(source)
541 origsource = ui.expandpath(source)
539 source, branches = parseurl(origsource, branch)
542 source, branches = parseurl(origsource, branch)
540 srcpeer = peer(ui, peeropts, source)
543 srcpeer = peer(ui, peeropts, source)
541 else:
544 else:
542 srcpeer = source.peer() # in case we were called with a localrepo
545 srcpeer = source.peer() # in case we were called with a localrepo
543 branches = (None, branch or [])
546 branches = (None, branch or [])
544 origsource = source = srcpeer.url()
547 origsource = source = srcpeer.url()
545 revs, checkout = addbranchrevs(srcpeer, srcpeer, branches, revs)
548 revs, checkout = addbranchrevs(srcpeer, srcpeer, branches, revs)
546
549
547 if dest is None:
550 if dest is None:
548 dest = defaultdest(source)
551 dest = defaultdest(source)
549 if dest:
552 if dest:
550 ui.status(_("destination directory: %s\n") % dest)
553 ui.status(_("destination directory: %s\n") % dest)
551 else:
554 else:
552 dest = ui.expandpath(dest)
555 dest = ui.expandpath(dest)
553
556
554 dest = util.urllocalpath(dest)
557 dest = util.urllocalpath(dest)
555 source = util.urllocalpath(source)
558 source = util.urllocalpath(source)
556
559
557 if not dest:
560 if not dest:
558 raise error.Abort(_("empty destination path is not valid"))
561 raise error.Abort(_("empty destination path is not valid"))
559
562
560 destvfs = vfsmod.vfs(dest, expandpath=True)
563 destvfs = vfsmod.vfs(dest, expandpath=True)
561 if destvfs.lexists():
564 if destvfs.lexists():
562 if not destvfs.isdir():
565 if not destvfs.isdir():
563 raise error.Abort(_("destination '%s' already exists") % dest)
566 raise error.Abort(_("destination '%s' already exists") % dest)
564 elif destvfs.listdir():
567 elif destvfs.listdir():
565 raise error.Abort(_("destination '%s' is not empty") % dest)
568 raise error.Abort(_("destination '%s' is not empty") % dest)
566
569
567 createopts = {}
570 createopts = {}
568 narrow = False
571 narrow = False
569
572
570 if storeincludepats is not None:
573 if storeincludepats is not None:
571 narrowspec.validatepatterns(storeincludepats)
574 narrowspec.validatepatterns(storeincludepats)
572 narrow = True
575 narrow = True
573
576
574 if storeexcludepats is not None:
577 if storeexcludepats is not None:
575 narrowspec.validatepatterns(storeexcludepats)
578 narrowspec.validatepatterns(storeexcludepats)
576 narrow = True
579 narrow = True
577
580
578 if narrow:
581 if narrow:
579 # Include everything by default if only exclusion patterns defined.
582 # Include everything by default if only exclusion patterns defined.
580 if storeexcludepats and not storeincludepats:
583 if storeexcludepats and not storeincludepats:
581 storeincludepats = {'path:.'}
584 storeincludepats = {'path:.'}
582
585
583 createopts['narrowfiles'] = True
586 createopts['narrowfiles'] = True
584
587
585 if depth:
588 if depth:
586 createopts['shallowfilestore'] = True
589 createopts['shallowfilestore'] = True
587
590
588 if srcpeer.capable(b'lfs-serve'):
591 if srcpeer.capable(b'lfs-serve'):
589 # Repository creation honors the config if it disabled the extension, so
592 # Repository creation honors the config if it disabled the extension, so
590 # we can't just announce that lfs will be enabled. This check avoids
593 # we can't just announce that lfs will be enabled. This check avoids
591 # saying that lfs will be enabled, and then saying it's an unknown
594 # saying that lfs will be enabled, and then saying it's an unknown
592 # feature. The lfs creation option is set in either case so that a
595 # feature. The lfs creation option is set in either case so that a
593 # requirement is added. If the extension is explicitly disabled but the
596 # requirement is added. If the extension is explicitly disabled but the
594 # requirement is set, the clone aborts early, before transferring any
597 # requirement is set, the clone aborts early, before transferring any
595 # data.
598 # data.
596 createopts['lfs'] = True
599 createopts['lfs'] = True
597
600
598 if extensions.disabledext('lfs'):
601 if extensions.disabledext('lfs'):
599 ui.status(_('(remote is using large file support (lfs), but it is '
602 ui.status(_('(remote is using large file support (lfs), but it is '
600 'explicitly disabled in the local configuration)\n'))
603 'explicitly disabled in the local configuration)\n'))
601 else:
604 else:
602 ui.status(_('(remote is using large file support (lfs); lfs will '
605 ui.status(_('(remote is using large file support (lfs); lfs will '
603 'be enabled for this repository)\n'))
606 'be enabled for this repository)\n'))
604
607
605 shareopts = shareopts or {}
608 shareopts = shareopts or {}
606 sharepool = shareopts.get('pool')
609 sharepool = shareopts.get('pool')
607 sharenamemode = shareopts.get('mode')
610 sharenamemode = shareopts.get('mode')
608 if sharepool and islocal(dest):
611 if sharepool and islocal(dest):
609 sharepath = None
612 sharepath = None
610 if sharenamemode == 'identity':
613 if sharenamemode == 'identity':
611 # Resolve the name from the initial changeset in the remote
614 # Resolve the name from the initial changeset in the remote
612 # repository. This returns nullid when the remote is empty. It
615 # repository. This returns nullid when the remote is empty. It
613 # raises RepoLookupError if revision 0 is filtered or otherwise
616 # raises RepoLookupError if revision 0 is filtered or otherwise
614 # not available. If we fail to resolve, sharing is not enabled.
617 # not available. If we fail to resolve, sharing is not enabled.
615 try:
618 try:
616 with srcpeer.commandexecutor() as e:
619 with srcpeer.commandexecutor() as e:
617 rootnode = e.callcommand('lookup', {
620 rootnode = e.callcommand('lookup', {
618 'key': '0',
621 'key': '0',
619 }).result()
622 }).result()
620
623
621 if rootnode != node.nullid:
624 if rootnode != node.nullid:
622 sharepath = os.path.join(sharepool, node.hex(rootnode))
625 sharepath = os.path.join(sharepool, node.hex(rootnode))
623 else:
626 else:
624 ui.status(_('(not using pooled storage: '
627 ui.status(_('(not using pooled storage: '
625 'remote appears to be empty)\n'))
628 'remote appears to be empty)\n'))
626 except error.RepoLookupError:
629 except error.RepoLookupError:
627 ui.status(_('(not using pooled storage: '
630 ui.status(_('(not using pooled storage: '
628 'unable to resolve identity of remote)\n'))
631 'unable to resolve identity of remote)\n'))
629 elif sharenamemode == 'remote':
632 elif sharenamemode == 'remote':
630 sharepath = os.path.join(
633 sharepath = os.path.join(
631 sharepool, node.hex(hashlib.sha1(source).digest()))
634 sharepool, node.hex(hashlib.sha1(source).digest()))
632 else:
635 else:
633 raise error.Abort(_('unknown share naming mode: %s') %
636 raise error.Abort(_('unknown share naming mode: %s') %
634 sharenamemode)
637 sharenamemode)
635
638
636 # TODO this is a somewhat arbitrary restriction.
639 # TODO this is a somewhat arbitrary restriction.
637 if narrow:
640 if narrow:
638 ui.status(_('(pooled storage not supported for narrow clones)\n'))
641 ui.status(_('(pooled storage not supported for narrow clones)\n'))
639 sharepath = None
642 sharepath = None
640
643
641 if sharepath:
644 if sharepath:
642 return clonewithshare(ui, peeropts, sharepath, source, srcpeer,
645 return clonewithshare(ui, peeropts, sharepath, source, srcpeer,
643 dest, pull=pull, rev=revs, update=update,
646 dest, pull=pull, rev=revs, update=update,
644 stream=stream)
647 stream=stream)
645
648
646 srclock = destlock = cleandir = None
649 srclock = destlock = cleandir = None
647 srcrepo = srcpeer.local()
650 srcrepo = srcpeer.local()
648 try:
651 try:
649 abspath = origsource
652 abspath = origsource
650 if islocal(origsource):
653 if islocal(origsource):
651 abspath = os.path.abspath(util.urllocalpath(origsource))
654 abspath = os.path.abspath(util.urllocalpath(origsource))
652
655
653 if islocal(dest):
656 if islocal(dest):
654 cleandir = dest
657 cleandir = dest
655
658
656 copy = False
659 copy = False
657 if (srcrepo and srcrepo.cancopy() and islocal(dest)
660 if (srcrepo and srcrepo.cancopy() and islocal(dest)
658 and not phases.hassecret(srcrepo)):
661 and not phases.hassecret(srcrepo)):
659 copy = not pull and not revs
662 copy = not pull and not revs
660
663
661 # TODO this is a somewhat arbitrary restriction.
664 # TODO this is a somewhat arbitrary restriction.
662 if narrow:
665 if narrow:
663 copy = False
666 copy = False
664
667
665 if copy:
668 if copy:
666 try:
669 try:
667 # we use a lock here because if we race with commit, we
670 # we use a lock here because if we race with commit, we
668 # can end up with extra data in the cloned revlogs that's
671 # can end up with extra data in the cloned revlogs that's
669 # not pointed to by changesets, thus causing verify to
672 # not pointed to by changesets, thus causing verify to
670 # fail
673 # fail
671 srclock = srcrepo.lock(wait=False)
674 srclock = srcrepo.lock(wait=False)
672 except error.LockError:
675 except error.LockError:
673 copy = False
676 copy = False
674
677
675 if copy:
678 if copy:
676 srcrepo.hook('preoutgoing', throw=True, source='clone')
679 srcrepo.hook('preoutgoing', throw=True, source='clone')
677 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
680 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
678 if not os.path.exists(dest):
681 if not os.path.exists(dest):
679 util.makedirs(dest)
682 util.makedirs(dest)
680 else:
683 else:
681 # only clean up directories we create ourselves
684 # only clean up directories we create ourselves
682 cleandir = hgdir
685 cleandir = hgdir
683 try:
686 try:
684 destpath = hgdir
687 destpath = hgdir
685 util.makedir(destpath, notindexed=True)
688 util.makedir(destpath, notindexed=True)
686 except OSError as inst:
689 except OSError as inst:
687 if inst.errno == errno.EEXIST:
690 if inst.errno == errno.EEXIST:
688 cleandir = None
691 cleandir = None
689 raise error.Abort(_("destination '%s' already exists")
692 raise error.Abort(_("destination '%s' already exists")
690 % dest)
693 % dest)
691 raise
694 raise
692
695
693 destlock = copystore(ui, srcrepo, destpath)
696 destlock = copystore(ui, srcrepo, destpath)
694 # copy bookmarks over
697 # copy bookmarks over
695 srcbookmarks = srcrepo.vfs.join('bookmarks')
698 srcbookmarks = srcrepo.vfs.join('bookmarks')
696 dstbookmarks = os.path.join(destpath, 'bookmarks')
699 dstbookmarks = os.path.join(destpath, 'bookmarks')
697 if os.path.exists(srcbookmarks):
700 if os.path.exists(srcbookmarks):
698 util.copyfile(srcbookmarks, dstbookmarks)
701 util.copyfile(srcbookmarks, dstbookmarks)
699
702
700 dstcachedir = os.path.join(destpath, 'cache')
703 dstcachedir = os.path.join(destpath, 'cache')
701 for cache in cacheutil.cachetocopy(srcrepo):
704 for cache in cacheutil.cachetocopy(srcrepo):
702 _copycache(srcrepo, dstcachedir, cache)
705 _copycache(srcrepo, dstcachedir, cache)
703
706
704 # we need to re-init the repo after manually copying the data
707 # we need to re-init the repo after manually copying the data
705 # into it
708 # into it
706 destpeer = peer(srcrepo, peeropts, dest)
709 destpeer = peer(srcrepo, peeropts, dest)
707 srcrepo.hook('outgoing', source='clone',
710 srcrepo.hook('outgoing', source='clone',
708 node=node.hex(node.nullid))
711 node=node.hex(node.nullid))
709 else:
712 else:
710 try:
713 try:
711 # only pass ui when no srcrepo
714 # only pass ui when no srcrepo
712 destpeer = peer(srcrepo or ui, peeropts, dest, create=True,
715 destpeer = peer(srcrepo or ui, peeropts, dest, create=True,
713 createopts=createopts)
716 createopts=createopts)
714 except OSError as inst:
717 except OSError as inst:
715 if inst.errno == errno.EEXIST:
718 if inst.errno == errno.EEXIST:
716 cleandir = None
719 cleandir = None
717 raise error.Abort(_("destination '%s' already exists")
720 raise error.Abort(_("destination '%s' already exists")
718 % dest)
721 % dest)
719 raise
722 raise
720
723
721 if revs:
724 if revs:
722 if not srcpeer.capable('lookup'):
725 if not srcpeer.capable('lookup'):
723 raise error.Abort(_("src repository does not support "
726 raise error.Abort(_("src repository does not support "
724 "revision lookup and so doesn't "
727 "revision lookup and so doesn't "
725 "support clone by revision"))
728 "support clone by revision"))
726
729
727 # TODO this is batchable.
730 # TODO this is batchable.
728 remoterevs = []
731 remoterevs = []
729 for rev in revs:
732 for rev in revs:
730 with srcpeer.commandexecutor() as e:
733 with srcpeer.commandexecutor() as e:
731 remoterevs.append(e.callcommand('lookup', {
734 remoterevs.append(e.callcommand('lookup', {
732 'key': rev,
735 'key': rev,
733 }).result())
736 }).result())
734 revs = remoterevs
737 revs = remoterevs
735
738
736 checkout = revs[0]
739 checkout = revs[0]
737 else:
740 else:
738 revs = None
741 revs = None
739 local = destpeer.local()
742 local = destpeer.local()
740 if local:
743 if local:
741 if narrow:
744 if narrow:
742 with local.wlock(), local.lock():
745 with local.wlock(), local.lock():
743 local.setnarrowpats(storeincludepats, storeexcludepats)
746 local.setnarrowpats(storeincludepats, storeexcludepats)
744 narrowspec.copytoworkingcopy(local)
747 narrowspec.copytoworkingcopy(local)
745
748
746 u = util.url(abspath)
749 u = util.url(abspath)
747 defaulturl = bytes(u)
750 defaulturl = bytes(u)
748 local.ui.setconfig('paths', 'default', defaulturl, 'clone')
751 local.ui.setconfig('paths', 'default', defaulturl, 'clone')
749 if not stream:
752 if not stream:
750 if pull:
753 if pull:
751 stream = False
754 stream = False
752 else:
755 else:
753 stream = None
756 stream = None
754 # internal config: ui.quietbookmarkmove
757 # internal config: ui.quietbookmarkmove
755 overrides = {('ui', 'quietbookmarkmove'): True}
758 overrides = {('ui', 'quietbookmarkmove'): True}
756 with local.ui.configoverride(overrides, 'clone'):
759 with local.ui.configoverride(overrides, 'clone'):
757 exchange.pull(local, srcpeer, revs,
760 exchange.pull(local, srcpeer, revs,
758 streamclonerequested=stream,
761 streamclonerequested=stream,
759 includepats=storeincludepats,
762 includepats=storeincludepats,
760 excludepats=storeexcludepats,
763 excludepats=storeexcludepats,
761 depth=depth)
764 depth=depth)
762 elif srcrepo:
765 elif srcrepo:
763 # TODO lift restriction once exchange.push() accepts narrow
766 # TODO lift restriction once exchange.push() accepts narrow
764 # push.
767 # push.
765 if narrow:
768 if narrow:
766 raise error.Abort(_('narrow clone not available for '
769 raise error.Abort(_('narrow clone not available for '
767 'remote destinations'))
770 'remote destinations'))
768
771
769 exchange.push(srcrepo, destpeer, revs=revs,
772 exchange.push(srcrepo, destpeer, revs=revs,
770 bookmarks=srcrepo._bookmarks.keys())
773 bookmarks=srcrepo._bookmarks.keys())
771 else:
774 else:
772 raise error.Abort(_("clone from remote to remote not supported")
775 raise error.Abort(_("clone from remote to remote not supported")
773 )
776 )
774
777
775 cleandir = None
778 cleandir = None
776
779
777 destrepo = destpeer.local()
780 destrepo = destpeer.local()
778 if destrepo:
781 if destrepo:
779 template = uimod.samplehgrcs['cloned']
782 template = uimod.samplehgrcs['cloned']
780 u = util.url(abspath)
783 u = util.url(abspath)
781 u.passwd = None
784 u.passwd = None
782 defaulturl = bytes(u)
785 defaulturl = bytes(u)
783 destrepo.vfs.write('hgrc', util.tonativeeol(template % defaulturl))
786 destrepo.vfs.write('hgrc', util.tonativeeol(template % defaulturl))
784 destrepo.ui.setconfig('paths', 'default', defaulturl, 'clone')
787 destrepo.ui.setconfig('paths', 'default', defaulturl, 'clone')
785
788
786 if ui.configbool('experimental', 'remotenames'):
789 if ui.configbool('experimental', 'remotenames'):
787 logexchange.pullremotenames(destrepo, srcpeer)
790 logexchange.pullremotenames(destrepo, srcpeer)
788
791
789 if update:
792 if update:
790 if update is not True:
793 if update is not True:
791 with srcpeer.commandexecutor() as e:
794 with srcpeer.commandexecutor() as e:
792 checkout = e.callcommand('lookup', {
795 checkout = e.callcommand('lookup', {
793 'key': update,
796 'key': update,
794 }).result()
797 }).result()
795
798
796 uprev = None
799 uprev = None
797 status = None
800 status = None
798 if checkout is not None:
801 if checkout is not None:
799 # Some extensions (at least hg-git and hg-subversion) have
802 # Some extensions (at least hg-git and hg-subversion) have
800 # a peer.lookup() implementation that returns a name instead
803 # a peer.lookup() implementation that returns a name instead
801 # of a nodeid. We work around it here until we've figured
804 # of a nodeid. We work around it here until we've figured
802 # out a better solution.
805 # out a better solution.
803 if len(checkout) == 20 and checkout in destrepo:
806 if len(checkout) == 20 and checkout in destrepo:
804 uprev = checkout
807 uprev = checkout
805 elif scmutil.isrevsymbol(destrepo, checkout):
808 elif scmutil.isrevsymbol(destrepo, checkout):
806 uprev = scmutil.revsymbol(destrepo, checkout).node()
809 uprev = scmutil.revsymbol(destrepo, checkout).node()
807 else:
810 else:
808 if update is not True:
811 if update is not True:
809 try:
812 try:
810 uprev = destrepo.lookup(update)
813 uprev = destrepo.lookup(update)
811 except error.RepoLookupError:
814 except error.RepoLookupError:
812 pass
815 pass
813 if uprev is None:
816 if uprev is None:
814 try:
817 try:
815 uprev = destrepo._bookmarks['@']
818 uprev = destrepo._bookmarks['@']
816 update = '@'
819 update = '@'
817 bn = destrepo[uprev].branch()
820 bn = destrepo[uprev].branch()
818 if bn == 'default':
821 if bn == 'default':
819 status = _("updating to bookmark @\n")
822 status = _("updating to bookmark @\n")
820 else:
823 else:
821 status = (_("updating to bookmark @ on branch %s\n")
824 status = (_("updating to bookmark @ on branch %s\n")
822 % bn)
825 % bn)
823 except KeyError:
826 except KeyError:
824 try:
827 try:
825 uprev = destrepo.branchtip('default')
828 uprev = destrepo.branchtip('default')
826 except error.RepoLookupError:
829 except error.RepoLookupError:
827 uprev = destrepo.lookup('tip')
830 uprev = destrepo.lookup('tip')
828 if not status:
831 if not status:
829 bn = destrepo[uprev].branch()
832 bn = destrepo[uprev].branch()
830 status = _("updating to branch %s\n") % bn
833 status = _("updating to branch %s\n") % bn
831 destrepo.ui.status(status)
834 destrepo.ui.status(status)
832 _update(destrepo, uprev)
835 _update(destrepo, uprev)
833 if update in destrepo._bookmarks:
836 if update in destrepo._bookmarks:
834 bookmarks.activate(destrepo, update)
837 bookmarks.activate(destrepo, update)
835 finally:
838 finally:
836 release(srclock, destlock)
839 release(srclock, destlock)
837 if cleandir is not None:
840 if cleandir is not None:
838 shutil.rmtree(cleandir, True)
841 shutil.rmtree(cleandir, True)
839 if srcpeer is not None:
842 if srcpeer is not None:
840 srcpeer.close()
843 srcpeer.close()
841 return srcpeer, destpeer
844 return srcpeer, destpeer
842
845
843 def _showstats(repo, stats, quietempty=False):
846 def _showstats(repo, stats, quietempty=False):
844 if quietempty and stats.isempty():
847 if quietempty and stats.isempty():
845 return
848 return
846 repo.ui.status(_("%d files updated, %d files merged, "
849 repo.ui.status(_("%d files updated, %d files merged, "
847 "%d files removed, %d files unresolved\n") % (
850 "%d files removed, %d files unresolved\n") % (
848 stats.updatedcount, stats.mergedcount,
851 stats.updatedcount, stats.mergedcount,
849 stats.removedcount, stats.unresolvedcount))
852 stats.removedcount, stats.unresolvedcount))
850
853
851 def updaterepo(repo, node, overwrite, updatecheck=None):
854 def updaterepo(repo, node, overwrite, updatecheck=None):
852 """Update the working directory to node.
855 """Update the working directory to node.
853
856
854 When overwrite is set, changes are clobbered, merged else
857 When overwrite is set, changes are clobbered, merged else
855
858
856 returns stats (see pydoc mercurial.merge.applyupdates)"""
859 returns stats (see pydoc mercurial.merge.applyupdates)"""
857 return mergemod.update(repo, node, branchmerge=False, force=overwrite,
860 return mergemod.update(repo, node, branchmerge=False, force=overwrite,
858 labels=['working copy', 'destination'],
861 labels=['working copy', 'destination'],
859 updatecheck=updatecheck)
862 updatecheck=updatecheck)
860
863
861 def update(repo, node, quietempty=False, updatecheck=None):
864 def update(repo, node, quietempty=False, updatecheck=None):
862 """update the working directory to node"""
865 """update the working directory to node"""
863 stats = updaterepo(repo, node, False, updatecheck=updatecheck)
866 stats = updaterepo(repo, node, False, updatecheck=updatecheck)
864 _showstats(repo, stats, quietempty)
867 _showstats(repo, stats, quietempty)
865 if stats.unresolvedcount:
868 if stats.unresolvedcount:
866 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
869 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
867 return stats.unresolvedcount > 0
870 return stats.unresolvedcount > 0
868
871
869 # naming conflict in clone()
872 # naming conflict in clone()
870 _update = update
873 _update = update
871
874
872 def clean(repo, node, show_stats=True, quietempty=False):
875 def clean(repo, node, show_stats=True, quietempty=False):
873 """forcibly switch the working directory to node, clobbering changes"""
876 """forcibly switch the working directory to node, clobbering changes"""
874 stats = updaterepo(repo, node, True)
877 stats = updaterepo(repo, node, True)
875 repo.vfs.unlinkpath('graftstate', ignoremissing=True)
878 repo.vfs.unlinkpath('graftstate', ignoremissing=True)
876 if show_stats:
879 if show_stats:
877 _showstats(repo, stats, quietempty)
880 _showstats(repo, stats, quietempty)
878 return stats.unresolvedcount > 0
881 return stats.unresolvedcount > 0
879
882
880 # naming conflict in updatetotally()
883 # naming conflict in updatetotally()
881 _clean = clean
884 _clean = clean
882
885
883 def updatetotally(ui, repo, checkout, brev, clean=False, updatecheck=None):
886 def updatetotally(ui, repo, checkout, brev, clean=False, updatecheck=None):
884 """Update the working directory with extra care for non-file components
887 """Update the working directory with extra care for non-file components
885
888
886 This takes care of non-file components below:
889 This takes care of non-file components below:
887
890
888 :bookmark: might be advanced or (in)activated
891 :bookmark: might be advanced or (in)activated
889
892
890 This takes arguments below:
893 This takes arguments below:
891
894
892 :checkout: to which revision the working directory is updated
895 :checkout: to which revision the working directory is updated
893 :brev: a name, which might be a bookmark to be activated after updating
896 :brev: a name, which might be a bookmark to be activated after updating
894 :clean: whether changes in the working directory can be discarded
897 :clean: whether changes in the working directory can be discarded
895 :updatecheck: how to deal with a dirty working directory
898 :updatecheck: how to deal with a dirty working directory
896
899
897 Valid values for updatecheck are (None => linear):
900 Valid values for updatecheck are (None => linear):
898
901
899 * abort: abort if the working directory is dirty
902 * abort: abort if the working directory is dirty
900 * none: don't check (merge working directory changes into destination)
903 * none: don't check (merge working directory changes into destination)
901 * linear: check that update is linear before merging working directory
904 * linear: check that update is linear before merging working directory
902 changes into destination
905 changes into destination
903 * noconflict: check that the update does not result in file merges
906 * noconflict: check that the update does not result in file merges
904
907
905 This returns whether conflict is detected at updating or not.
908 This returns whether conflict is detected at updating or not.
906 """
909 """
907 if updatecheck is None:
910 if updatecheck is None:
908 updatecheck = ui.config('commands', 'update.check')
911 updatecheck = ui.config('commands', 'update.check')
909 if updatecheck not in ('abort', 'none', 'linear', 'noconflict'):
912 if updatecheck not in ('abort', 'none', 'linear', 'noconflict'):
910 # If not configured, or invalid value configured
913 # If not configured, or invalid value configured
911 updatecheck = 'linear'
914 updatecheck = 'linear'
912 with repo.wlock():
915 with repo.wlock():
913 movemarkfrom = None
916 movemarkfrom = None
914 warndest = False
917 warndest = False
915 if checkout is None:
918 if checkout is None:
916 updata = destutil.destupdate(repo, clean=clean)
919 updata = destutil.destupdate(repo, clean=clean)
917 checkout, movemarkfrom, brev = updata
920 checkout, movemarkfrom, brev = updata
918 warndest = True
921 warndest = True
919
922
920 if clean:
923 if clean:
921 ret = _clean(repo, checkout)
924 ret = _clean(repo, checkout)
922 else:
925 else:
923 if updatecheck == 'abort':
926 if updatecheck == 'abort':
924 cmdutil.bailifchanged(repo, merge=False)
927 cmdutil.bailifchanged(repo, merge=False)
925 updatecheck = 'none'
928 updatecheck = 'none'
926 ret = _update(repo, checkout, updatecheck=updatecheck)
929 ret = _update(repo, checkout, updatecheck=updatecheck)
927
930
928 if not ret and movemarkfrom:
931 if not ret and movemarkfrom:
929 if movemarkfrom == repo['.'].node():
932 if movemarkfrom == repo['.'].node():
930 pass # no-op update
933 pass # no-op update
931 elif bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
934 elif bookmarks.update(repo, [movemarkfrom], repo['.'].node()):
932 b = ui.label(repo._activebookmark, 'bookmarks.active')
935 b = ui.label(repo._activebookmark, 'bookmarks.active')
933 ui.status(_("updating bookmark %s\n") % b)
936 ui.status(_("updating bookmark %s\n") % b)
934 else:
937 else:
935 # this can happen with a non-linear update
938 # this can happen with a non-linear update
936 b = ui.label(repo._activebookmark, 'bookmarks')
939 b = ui.label(repo._activebookmark, 'bookmarks')
937 ui.status(_("(leaving bookmark %s)\n") % b)
940 ui.status(_("(leaving bookmark %s)\n") % b)
938 bookmarks.deactivate(repo)
941 bookmarks.deactivate(repo)
939 elif brev in repo._bookmarks:
942 elif brev in repo._bookmarks:
940 if brev != repo._activebookmark:
943 if brev != repo._activebookmark:
941 b = ui.label(brev, 'bookmarks.active')
944 b = ui.label(brev, 'bookmarks.active')
942 ui.status(_("(activating bookmark %s)\n") % b)
945 ui.status(_("(activating bookmark %s)\n") % b)
943 bookmarks.activate(repo, brev)
946 bookmarks.activate(repo, brev)
944 elif brev:
947 elif brev:
945 if repo._activebookmark:
948 if repo._activebookmark:
946 b = ui.label(repo._activebookmark, 'bookmarks')
949 b = ui.label(repo._activebookmark, 'bookmarks')
947 ui.status(_("(leaving bookmark %s)\n") % b)
950 ui.status(_("(leaving bookmark %s)\n") % b)
948 bookmarks.deactivate(repo)
951 bookmarks.deactivate(repo)
949
952
950 if warndest:
953 if warndest:
951 destutil.statusotherdests(ui, repo)
954 destutil.statusotherdests(ui, repo)
952
955
953 return ret
956 return ret
954
957
955 def merge(repo, node, force=None, remind=True, mergeforce=False, labels=None,
958 def merge(repo, node, force=None, remind=True, mergeforce=False, labels=None,
956 abort=False):
959 abort=False):
957 """Branch merge with node, resolving changes. Return true if any
960 """Branch merge with node, resolving changes. Return true if any
958 unresolved conflicts."""
961 unresolved conflicts."""
959 if abort:
962 if abort:
960 return abortmerge(repo.ui, repo)
963 return abortmerge(repo.ui, repo)
961
964
962 stats = mergemod.update(repo, node, branchmerge=True, force=force,
965 stats = mergemod.update(repo, node, branchmerge=True, force=force,
963 mergeforce=mergeforce, labels=labels)
966 mergeforce=mergeforce, labels=labels)
964 _showstats(repo, stats)
967 _showstats(repo, stats)
965 if stats.unresolvedcount:
968 if stats.unresolvedcount:
966 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
969 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
967 "or 'hg merge --abort' to abandon\n"))
970 "or 'hg merge --abort' to abandon\n"))
968 elif remind:
971 elif remind:
969 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
972 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
970 return stats.unresolvedcount > 0
973 return stats.unresolvedcount > 0
971
974
972 def abortmerge(ui, repo):
975 def abortmerge(ui, repo):
973 ms = mergemod.mergestate.read(repo)
976 ms = mergemod.mergestate.read(repo)
974 if ms.active():
977 if ms.active():
975 # there were conflicts
978 # there were conflicts
976 node = ms.localctx.hex()
979 node = ms.localctx.hex()
977 else:
980 else:
978 # there were no conficts, mergestate was not stored
981 # there were no conficts, mergestate was not stored
979 node = repo['.'].hex()
982 node = repo['.'].hex()
980
983
981 repo.ui.status(_("aborting the merge, updating back to"
984 repo.ui.status(_("aborting the merge, updating back to"
982 " %s\n") % node[:12])
985 " %s\n") % node[:12])
983 stats = mergemod.update(repo, node, branchmerge=False, force=True)
986 stats = mergemod.update(repo, node, branchmerge=False, force=True)
984 _showstats(repo, stats)
987 _showstats(repo, stats)
985 return stats.unresolvedcount > 0
988 return stats.unresolvedcount > 0
986
989
987 def _incoming(displaychlist, subreporecurse, ui, repo, source,
990 def _incoming(displaychlist, subreporecurse, ui, repo, source,
988 opts, buffered=False):
991 opts, buffered=False):
989 """
992 """
990 Helper for incoming / gincoming.
993 Helper for incoming / gincoming.
991 displaychlist gets called with
994 displaychlist gets called with
992 (remoterepo, incomingchangesetlist, displayer) parameters,
995 (remoterepo, incomingchangesetlist, displayer) parameters,
993 and is supposed to contain only code that can't be unified.
996 and is supposed to contain only code that can't be unified.
994 """
997 """
995 source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
998 source, branches = parseurl(ui.expandpath(source), opts.get('branch'))
996 other = peer(repo, opts, source)
999 other = peer(repo, opts, source)
997 ui.status(_('comparing with %s\n') % util.hidepassword(source))
1000 ui.status(_('comparing with %s\n') % util.hidepassword(source))
998 revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
1001 revs, checkout = addbranchrevs(repo, other, branches, opts.get('rev'))
999
1002
1000 if revs:
1003 if revs:
1001 revs = [other.lookup(rev) for rev in revs]
1004 revs = [other.lookup(rev) for rev in revs]
1002 other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
1005 other, chlist, cleanupfn = bundlerepo.getremotechanges(ui, repo, other,
1003 revs, opts["bundle"], opts["force"])
1006 revs, opts["bundle"], opts["force"])
1004 try:
1007 try:
1005 if not chlist:
1008 if not chlist:
1006 ui.status(_("no changes found\n"))
1009 ui.status(_("no changes found\n"))
1007 return subreporecurse()
1010 return subreporecurse()
1008 ui.pager('incoming')
1011 ui.pager('incoming')
1009 displayer = logcmdutil.changesetdisplayer(ui, other, opts,
1012 displayer = logcmdutil.changesetdisplayer(ui, other, opts,
1010 buffered=buffered)
1013 buffered=buffered)
1011 displaychlist(other, chlist, displayer)
1014 displaychlist(other, chlist, displayer)
1012 displayer.close()
1015 displayer.close()
1013 finally:
1016 finally:
1014 cleanupfn()
1017 cleanupfn()
1015 subreporecurse()
1018 subreporecurse()
1016 return 0 # exit code is zero since we found incoming changes
1019 return 0 # exit code is zero since we found incoming changes
1017
1020
1018 def incoming(ui, repo, source, opts):
1021 def incoming(ui, repo, source, opts):
1019 def subreporecurse():
1022 def subreporecurse():
1020 ret = 1
1023 ret = 1
1021 if opts.get('subrepos'):
1024 if opts.get('subrepos'):
1022 ctx = repo[None]
1025 ctx = repo[None]
1023 for subpath in sorted(ctx.substate):
1026 for subpath in sorted(ctx.substate):
1024 sub = ctx.sub(subpath)
1027 sub = ctx.sub(subpath)
1025 ret = min(ret, sub.incoming(ui, source, opts))
1028 ret = min(ret, sub.incoming(ui, source, opts))
1026 return ret
1029 return ret
1027
1030
1028 def display(other, chlist, displayer):
1031 def display(other, chlist, displayer):
1029 limit = logcmdutil.getlimit(opts)
1032 limit = logcmdutil.getlimit(opts)
1030 if opts.get('newest_first'):
1033 if opts.get('newest_first'):
1031 chlist.reverse()
1034 chlist.reverse()
1032 count = 0
1035 count = 0
1033 for n in chlist:
1036 for n in chlist:
1034 if limit is not None and count >= limit:
1037 if limit is not None and count >= limit:
1035 break
1038 break
1036 parents = [p for p in other.changelog.parents(n) if p != nullid]
1039 parents = [p for p in other.changelog.parents(n) if p != nullid]
1037 if opts.get('no_merges') and len(parents) == 2:
1040 if opts.get('no_merges') and len(parents) == 2:
1038 continue
1041 continue
1039 count += 1
1042 count += 1
1040 displayer.show(other[n])
1043 displayer.show(other[n])
1041 return _incoming(display, subreporecurse, ui, repo, source, opts)
1044 return _incoming(display, subreporecurse, ui, repo, source, opts)
1042
1045
1043 def _outgoing(ui, repo, dest, opts):
1046 def _outgoing(ui, repo, dest, opts):
1044 path = ui.paths.getpath(dest, default=('default-push', 'default'))
1047 path = ui.paths.getpath(dest, default=('default-push', 'default'))
1045 if not path:
1048 if not path:
1046 raise error.Abort(_('default repository not configured!'),
1049 raise error.Abort(_('default repository not configured!'),
1047 hint=_("see 'hg help config.paths'"))
1050 hint=_("see 'hg help config.paths'"))
1048 dest = path.pushloc or path.loc
1051 dest = path.pushloc or path.loc
1049 branches = path.branch, opts.get('branch') or []
1052 branches = path.branch, opts.get('branch') or []
1050
1053
1051 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
1054 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
1052 revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
1055 revs, checkout = addbranchrevs(repo, repo, branches, opts.get('rev'))
1053 if revs:
1056 if revs:
1054 revs = [repo[rev].node() for rev in scmutil.revrange(repo, revs)]
1057 revs = [repo[rev].node() for rev in scmutil.revrange(repo, revs)]
1055
1058
1056 other = peer(repo, opts, dest)
1059 other = peer(repo, opts, dest)
1057 outgoing = discovery.findcommonoutgoing(repo, other, revs,
1060 outgoing = discovery.findcommonoutgoing(repo, other, revs,
1058 force=opts.get('force'))
1061 force=opts.get('force'))
1059 o = outgoing.missing
1062 o = outgoing.missing
1060 if not o:
1063 if not o:
1061 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
1064 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
1062 return o, other
1065 return o, other
1063
1066
1064 def outgoing(ui, repo, dest, opts):
1067 def outgoing(ui, repo, dest, opts):
1065 def recurse():
1068 def recurse():
1066 ret = 1
1069 ret = 1
1067 if opts.get('subrepos'):
1070 if opts.get('subrepos'):
1068 ctx = repo[None]
1071 ctx = repo[None]
1069 for subpath in sorted(ctx.substate):
1072 for subpath in sorted(ctx.substate):
1070 sub = ctx.sub(subpath)
1073 sub = ctx.sub(subpath)
1071 ret = min(ret, sub.outgoing(ui, dest, opts))
1074 ret = min(ret, sub.outgoing(ui, dest, opts))
1072 return ret
1075 return ret
1073
1076
1074 limit = logcmdutil.getlimit(opts)
1077 limit = logcmdutil.getlimit(opts)
1075 o, other = _outgoing(ui, repo, dest, opts)
1078 o, other = _outgoing(ui, repo, dest, opts)
1076 if not o:
1079 if not o:
1077 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1080 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1078 return recurse()
1081 return recurse()
1079
1082
1080 if opts.get('newest_first'):
1083 if opts.get('newest_first'):
1081 o.reverse()
1084 o.reverse()
1082 ui.pager('outgoing')
1085 ui.pager('outgoing')
1083 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
1086 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
1084 count = 0
1087 count = 0
1085 for n in o:
1088 for n in o:
1086 if limit is not None and count >= limit:
1089 if limit is not None and count >= limit:
1087 break
1090 break
1088 parents = [p for p in repo.changelog.parents(n) if p != nullid]
1091 parents = [p for p in repo.changelog.parents(n) if p != nullid]
1089 if opts.get('no_merges') and len(parents) == 2:
1092 if opts.get('no_merges') and len(parents) == 2:
1090 continue
1093 continue
1091 count += 1
1094 count += 1
1092 displayer.show(repo[n])
1095 displayer.show(repo[n])
1093 displayer.close()
1096 displayer.close()
1094 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1097 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1095 recurse()
1098 recurse()
1096 return 0 # exit code is zero since we found outgoing changes
1099 return 0 # exit code is zero since we found outgoing changes
1097
1100
1098 def verify(repo, level=None):
1101 def verify(repo, level=None):
1099 """verify the consistency of a repository"""
1102 """verify the consistency of a repository"""
1100 ret = verifymod.verify(repo, level=level)
1103 ret = verifymod.verify(repo, level=level)
1101
1104
1102 # Broken subrepo references in hidden csets don't seem worth worrying about,
1105 # Broken subrepo references in hidden csets don't seem worth worrying about,
1103 # since they can't be pushed/pulled, and --hidden can be used if they are a
1106 # since they can't be pushed/pulled, and --hidden can be used if they are a
1104 # concern.
1107 # concern.
1105
1108
1106 # pathto() is needed for -R case
1109 # pathto() is needed for -R case
1107 revs = repo.revs("filelog(%s)",
1110 revs = repo.revs("filelog(%s)",
1108 util.pathto(repo.root, repo.getcwd(), '.hgsubstate'))
1111 util.pathto(repo.root, repo.getcwd(), '.hgsubstate'))
1109
1112
1110 if revs:
1113 if revs:
1111 repo.ui.status(_('checking subrepo links\n'))
1114 repo.ui.status(_('checking subrepo links\n'))
1112 for rev in revs:
1115 for rev in revs:
1113 ctx = repo[rev]
1116 ctx = repo[rev]
1114 try:
1117 try:
1115 for subpath in ctx.substate:
1118 for subpath in ctx.substate:
1116 try:
1119 try:
1117 ret = (ctx.sub(subpath, allowcreate=False).verify()
1120 ret = (ctx.sub(subpath, allowcreate=False).verify()
1118 or ret)
1121 or ret)
1119 except error.RepoError as e:
1122 except error.RepoError as e:
1120 repo.ui.warn(('%d: %s\n') % (rev, e))
1123 repo.ui.warn(('%d: %s\n') % (rev, e))
1121 except Exception:
1124 except Exception:
1122 repo.ui.warn(_('.hgsubstate is corrupt in revision %s\n') %
1125 repo.ui.warn(_('.hgsubstate is corrupt in revision %s\n') %
1123 node.short(ctx.node()))
1126 node.short(ctx.node()))
1124
1127
1125 return ret
1128 return ret
1126
1129
1127 def remoteui(src, opts):
1130 def remoteui(src, opts):
1128 'build a remote ui from ui or repo and opts'
1131 'build a remote ui from ui or repo and opts'
1129 if util.safehasattr(src, 'baseui'): # looks like a repository
1132 if util.safehasattr(src, 'baseui'): # looks like a repository
1130 dst = src.baseui.copy() # drop repo-specific config
1133 dst = src.baseui.copy() # drop repo-specific config
1131 src = src.ui # copy target options from repo
1134 src = src.ui # copy target options from repo
1132 else: # assume it's a global ui object
1135 else: # assume it's a global ui object
1133 dst = src.copy() # keep all global options
1136 dst = src.copy() # keep all global options
1134
1137
1135 # copy ssh-specific options
1138 # copy ssh-specific options
1136 for o in 'ssh', 'remotecmd':
1139 for o in 'ssh', 'remotecmd':
1137 v = opts.get(o) or src.config('ui', o)
1140 v = opts.get(o) or src.config('ui', o)
1138 if v:
1141 if v:
1139 dst.setconfig("ui", o, v, 'copied')
1142 dst.setconfig("ui", o, v, 'copied')
1140
1143
1141 # copy bundle-specific options
1144 # copy bundle-specific options
1142 r = src.config('bundle', 'mainreporoot')
1145 r = src.config('bundle', 'mainreporoot')
1143 if r:
1146 if r:
1144 dst.setconfig('bundle', 'mainreporoot', r, 'copied')
1147 dst.setconfig('bundle', 'mainreporoot', r, 'copied')
1145
1148
1146 # copy selected local settings to the remote ui
1149 # copy selected local settings to the remote ui
1147 for sect in ('auth', 'hostfingerprints', 'hostsecurity', 'http_proxy'):
1150 for sect in ('auth', 'hostfingerprints', 'hostsecurity', 'http_proxy'):
1148 for key, val in src.configitems(sect):
1151 for key, val in src.configitems(sect):
1149 dst.setconfig(sect, key, val, 'copied')
1152 dst.setconfig(sect, key, val, 'copied')
1150 v = src.config('web', 'cacerts')
1153 v = src.config('web', 'cacerts')
1151 if v:
1154 if v:
1152 dst.setconfig('web', 'cacerts', util.expandpath(v), 'copied')
1155 dst.setconfig('web', 'cacerts', util.expandpath(v), 'copied')
1153
1156
1154 return dst
1157 return dst
1155
1158
1156 # Files of interest
1159 # Files of interest
1157 # Used to check if the repository has changed looking at mtime and size of
1160 # Used to check if the repository has changed looking at mtime and size of
1158 # these files.
1161 # these files.
1159 foi = [('spath', '00changelog.i'),
1162 foi = [('spath', '00changelog.i'),
1160 ('spath', 'phaseroots'), # ! phase can change content at the same size
1163 ('spath', 'phaseroots'), # ! phase can change content at the same size
1161 ('spath', 'obsstore'),
1164 ('spath', 'obsstore'),
1162 ('path', 'bookmarks'), # ! bookmark can change content at the same size
1165 ('path', 'bookmarks'), # ! bookmark can change content at the same size
1163 ]
1166 ]
1164
1167
1165 class cachedlocalrepo(object):
1168 class cachedlocalrepo(object):
1166 """Holds a localrepository that can be cached and reused."""
1169 """Holds a localrepository that can be cached and reused."""
1167
1170
1168 def __init__(self, repo):
1171 def __init__(self, repo):
1169 """Create a new cached repo from an existing repo.
1172 """Create a new cached repo from an existing repo.
1170
1173
1171 We assume the passed in repo was recently created. If the
1174 We assume the passed in repo was recently created. If the
1172 repo has changed between when it was created and when it was
1175 repo has changed between when it was created and when it was
1173 turned into a cache, it may not refresh properly.
1176 turned into a cache, it may not refresh properly.
1174 """
1177 """
1175 assert isinstance(repo, localrepo.localrepository)
1178 assert isinstance(repo, localrepo.localrepository)
1176 self._repo = repo
1179 self._repo = repo
1177 self._state, self.mtime = self._repostate()
1180 self._state, self.mtime = self._repostate()
1178 self._filtername = repo.filtername
1181 self._filtername = repo.filtername
1179
1182
1180 def fetch(self):
1183 def fetch(self):
1181 """Refresh (if necessary) and return a repository.
1184 """Refresh (if necessary) and return a repository.
1182
1185
1183 If the cached instance is out of date, it will be recreated
1186 If the cached instance is out of date, it will be recreated
1184 automatically and returned.
1187 automatically and returned.
1185
1188
1186 Returns a tuple of the repo and a boolean indicating whether a new
1189 Returns a tuple of the repo and a boolean indicating whether a new
1187 repo instance was created.
1190 repo instance was created.
1188 """
1191 """
1189 # We compare the mtimes and sizes of some well-known files to
1192 # We compare the mtimes and sizes of some well-known files to
1190 # determine if the repo changed. This is not precise, as mtimes
1193 # determine if the repo changed. This is not precise, as mtimes
1191 # are susceptible to clock skew and imprecise filesystems and
1194 # are susceptible to clock skew and imprecise filesystems and
1192 # file content can change while maintaining the same size.
1195 # file content can change while maintaining the same size.
1193
1196
1194 state, mtime = self._repostate()
1197 state, mtime = self._repostate()
1195 if state == self._state:
1198 if state == self._state:
1196 return self._repo, False
1199 return self._repo, False
1197
1200
1198 repo = repository(self._repo.baseui, self._repo.url())
1201 repo = repository(self._repo.baseui, self._repo.url())
1199 if self._filtername:
1202 if self._filtername:
1200 self._repo = repo.filtered(self._filtername)
1203 self._repo = repo.filtered(self._filtername)
1201 else:
1204 else:
1202 self._repo = repo.unfiltered()
1205 self._repo = repo.unfiltered()
1203 self._state = state
1206 self._state = state
1204 self.mtime = mtime
1207 self.mtime = mtime
1205
1208
1206 return self._repo, True
1209 return self._repo, True
1207
1210
1208 def _repostate(self):
1211 def _repostate(self):
1209 state = []
1212 state = []
1210 maxmtime = -1
1213 maxmtime = -1
1211 for attr, fname in foi:
1214 for attr, fname in foi:
1212 prefix = getattr(self._repo, attr)
1215 prefix = getattr(self._repo, attr)
1213 p = os.path.join(prefix, fname)
1216 p = os.path.join(prefix, fname)
1214 try:
1217 try:
1215 st = os.stat(p)
1218 st = os.stat(p)
1216 except OSError:
1219 except OSError:
1217 st = os.stat(prefix)
1220 st = os.stat(prefix)
1218 state.append((st[stat.ST_MTIME], st.st_size))
1221 state.append((st[stat.ST_MTIME], st.st_size))
1219 maxmtime = max(maxmtime, st[stat.ST_MTIME])
1222 maxmtime = max(maxmtime, st[stat.ST_MTIME])
1220
1223
1221 return tuple(state), maxmtime
1224 return tuple(state), maxmtime
1222
1225
1223 def copy(self):
1226 def copy(self):
1224 """Obtain a copy of this class instance.
1227 """Obtain a copy of this class instance.
1225
1228
1226 A new localrepository instance is obtained. The new instance should be
1229 A new localrepository instance is obtained. The new instance should be
1227 completely independent of the original.
1230 completely independent of the original.
1228 """
1231 """
1229 repo = repository(self._repo.baseui, self._repo.origroot)
1232 repo = repository(self._repo.baseui, self._repo.origroot)
1230 if self._filtername:
1233 if self._filtername:
1231 repo = repo.filtered(self._filtername)
1234 repo = repo.filtered(self._filtername)
1232 else:
1235 else:
1233 repo = repo.unfiltered()
1236 repo = repo.unfiltered()
1234 c = cachedlocalrepo(repo)
1237 c = cachedlocalrepo(repo)
1235 c._state = self._state
1238 c._state = self._state
1236 c.mtime = self.mtime
1239 c.mtime = self.mtime
1237 return c
1240 return c
@@ -1,1010 +1,1012
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import io
12 import io
13 import os
13 import os
14 import socket
14 import socket
15 import struct
15 import struct
16 import weakref
16 import weakref
17
17
18 from .i18n import _
18 from .i18n import _
19 from .interfaces import (
20 repository,
21 )
19 from . import (
22 from . import (
20 bundle2,
23 bundle2,
21 error,
24 error,
22 httpconnection,
25 httpconnection,
23 pycompat,
26 pycompat,
24 repository,
25 statichttprepo,
27 statichttprepo,
26 url as urlmod,
28 url as urlmod,
27 util,
29 util,
28 wireprotoframing,
30 wireprotoframing,
29 wireprototypes,
31 wireprototypes,
30 wireprotov1peer,
32 wireprotov1peer,
31 wireprotov2peer,
33 wireprotov2peer,
32 wireprotov2server,
34 wireprotov2server,
33 )
35 )
34 from .utils import (
36 from .utils import (
35 cborutil,
37 cborutil,
36 interfaceutil,
38 interfaceutil,
37 stringutil,
39 stringutil,
38 )
40 )
39
41
40 httplib = util.httplib
42 httplib = util.httplib
41 urlerr = util.urlerr
43 urlerr = util.urlerr
42 urlreq = util.urlreq
44 urlreq = util.urlreq
43
45
44 def encodevalueinheaders(value, header, limit):
46 def encodevalueinheaders(value, header, limit):
45 """Encode a string value into multiple HTTP headers.
47 """Encode a string value into multiple HTTP headers.
46
48
47 ``value`` will be encoded into 1 or more HTTP headers with the names
49 ``value`` will be encoded into 1 or more HTTP headers with the names
48 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
50 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
49 name + value will be at most ``limit`` bytes long.
51 name + value will be at most ``limit`` bytes long.
50
52
51 Returns an iterable of 2-tuples consisting of header names and
53 Returns an iterable of 2-tuples consisting of header names and
52 values as native strings.
54 values as native strings.
53 """
55 """
54 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
56 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
55 # not bytes. This function always takes bytes in as arguments.
57 # not bytes. This function always takes bytes in as arguments.
56 fmt = pycompat.strurl(header) + r'-%s'
58 fmt = pycompat.strurl(header) + r'-%s'
57 # Note: it is *NOT* a bug that the last bit here is a bytestring
59 # Note: it is *NOT* a bug that the last bit here is a bytestring
58 # and not a unicode: we're just getting the encoded length anyway,
60 # and not a unicode: we're just getting the encoded length anyway,
59 # and using an r-string to make it portable between Python 2 and 3
61 # and using an r-string to make it portable between Python 2 and 3
60 # doesn't work because then the \r is a literal backslash-r
62 # doesn't work because then the \r is a literal backslash-r
61 # instead of a carriage return.
63 # instead of a carriage return.
62 valuelen = limit - len(fmt % r'000') - len(': \r\n')
64 valuelen = limit - len(fmt % r'000') - len(': \r\n')
63 result = []
65 result = []
64
66
65 n = 0
67 n = 0
66 for i in pycompat.xrange(0, len(value), valuelen):
68 for i in pycompat.xrange(0, len(value), valuelen):
67 n += 1
69 n += 1
68 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
70 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
69
71
70 return result
72 return result
71
73
72 class _multifile(object):
74 class _multifile(object):
73 def __init__(self, *fileobjs):
75 def __init__(self, *fileobjs):
74 for f in fileobjs:
76 for f in fileobjs:
75 if not util.safehasattr(f, 'length'):
77 if not util.safehasattr(f, 'length'):
76 raise ValueError(
78 raise ValueError(
77 '_multifile only supports file objects that '
79 '_multifile only supports file objects that '
78 'have a length but this one does not:', type(f), f)
80 'have a length but this one does not:', type(f), f)
79 self._fileobjs = fileobjs
81 self._fileobjs = fileobjs
80 self._index = 0
82 self._index = 0
81
83
82 @property
84 @property
83 def length(self):
85 def length(self):
84 return sum(f.length for f in self._fileobjs)
86 return sum(f.length for f in self._fileobjs)
85
87
86 def read(self, amt=None):
88 def read(self, amt=None):
87 if amt <= 0:
89 if amt <= 0:
88 return ''.join(f.read() for f in self._fileobjs)
90 return ''.join(f.read() for f in self._fileobjs)
89 parts = []
91 parts = []
90 while amt and self._index < len(self._fileobjs):
92 while amt and self._index < len(self._fileobjs):
91 parts.append(self._fileobjs[self._index].read(amt))
93 parts.append(self._fileobjs[self._index].read(amt))
92 got = len(parts[-1])
94 got = len(parts[-1])
93 if got < amt:
95 if got < amt:
94 self._index += 1
96 self._index += 1
95 amt -= got
97 amt -= got
96 return ''.join(parts)
98 return ''.join(parts)
97
99
98 def seek(self, offset, whence=os.SEEK_SET):
100 def seek(self, offset, whence=os.SEEK_SET):
99 if whence != os.SEEK_SET:
101 if whence != os.SEEK_SET:
100 raise NotImplementedError(
102 raise NotImplementedError(
101 '_multifile does not support anything other'
103 '_multifile does not support anything other'
102 ' than os.SEEK_SET for whence on seek()')
104 ' than os.SEEK_SET for whence on seek()')
103 if offset != 0:
105 if offset != 0:
104 raise NotImplementedError(
106 raise NotImplementedError(
105 '_multifile only supports seeking to start, but that '
107 '_multifile only supports seeking to start, but that '
106 'could be fixed if you need it')
108 'could be fixed if you need it')
107 for f in self._fileobjs:
109 for f in self._fileobjs:
108 f.seek(0)
110 f.seek(0)
109 self._index = 0
111 self._index = 0
110
112
111 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
113 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
112 repobaseurl, cmd, args):
114 repobaseurl, cmd, args):
113 """Make an HTTP request to run a command for a version 1 client.
115 """Make an HTTP request to run a command for a version 1 client.
114
116
115 ``caps`` is a set of known server capabilities. The value may be
117 ``caps`` is a set of known server capabilities. The value may be
116 None if capabilities are not yet known.
118 None if capabilities are not yet known.
117
119
118 ``capablefn`` is a function to evaluate a capability.
120 ``capablefn`` is a function to evaluate a capability.
119
121
120 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
122 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
121 raw data to pass to it.
123 raw data to pass to it.
122 """
124 """
123 if cmd == 'pushkey':
125 if cmd == 'pushkey':
124 args['data'] = ''
126 args['data'] = ''
125 data = args.pop('data', None)
127 data = args.pop('data', None)
126 headers = args.pop('headers', {})
128 headers = args.pop('headers', {})
127
129
128 ui.debug("sending %s command\n" % cmd)
130 ui.debug("sending %s command\n" % cmd)
129 q = [('cmd', cmd)]
131 q = [('cmd', cmd)]
130 headersize = 0
132 headersize = 0
131 # Important: don't use self.capable() here or else you end up
133 # Important: don't use self.capable() here or else you end up
132 # with infinite recursion when trying to look up capabilities
134 # with infinite recursion when trying to look up capabilities
133 # for the first time.
135 # for the first time.
134 postargsok = caps is not None and 'httppostargs' in caps
136 postargsok = caps is not None and 'httppostargs' in caps
135
137
136 # Send arguments via POST.
138 # Send arguments via POST.
137 if postargsok and args:
139 if postargsok and args:
138 strargs = urlreq.urlencode(sorted(args.items()))
140 strargs = urlreq.urlencode(sorted(args.items()))
139 if not data:
141 if not data:
140 data = strargs
142 data = strargs
141 else:
143 else:
142 if isinstance(data, bytes):
144 if isinstance(data, bytes):
143 i = io.BytesIO(data)
145 i = io.BytesIO(data)
144 i.length = len(data)
146 i.length = len(data)
145 data = i
147 data = i
146 argsio = io.BytesIO(strargs)
148 argsio = io.BytesIO(strargs)
147 argsio.length = len(strargs)
149 argsio.length = len(strargs)
148 data = _multifile(argsio, data)
150 data = _multifile(argsio, data)
149 headers[r'X-HgArgs-Post'] = len(strargs)
151 headers[r'X-HgArgs-Post'] = len(strargs)
150 elif args:
152 elif args:
151 # Calling self.capable() can infinite loop if we are calling
153 # Calling self.capable() can infinite loop if we are calling
152 # "capabilities". But that command should never accept wire
154 # "capabilities". But that command should never accept wire
153 # protocol arguments. So this should never happen.
155 # protocol arguments. So this should never happen.
154 assert cmd != 'capabilities'
156 assert cmd != 'capabilities'
155 httpheader = capablefn('httpheader')
157 httpheader = capablefn('httpheader')
156 if httpheader:
158 if httpheader:
157 headersize = int(httpheader.split(',', 1)[0])
159 headersize = int(httpheader.split(',', 1)[0])
158
160
159 # Send arguments via HTTP headers.
161 # Send arguments via HTTP headers.
160 if headersize > 0:
162 if headersize > 0:
161 # The headers can typically carry more data than the URL.
163 # The headers can typically carry more data than the URL.
162 encargs = urlreq.urlencode(sorted(args.items()))
164 encargs = urlreq.urlencode(sorted(args.items()))
163 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
165 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
164 headersize):
166 headersize):
165 headers[header] = value
167 headers[header] = value
166 # Send arguments via query string (Mercurial <1.9).
168 # Send arguments via query string (Mercurial <1.9).
167 else:
169 else:
168 q += sorted(args.items())
170 q += sorted(args.items())
169
171
170 qs = '?%s' % urlreq.urlencode(q)
172 qs = '?%s' % urlreq.urlencode(q)
171 cu = "%s%s" % (repobaseurl, qs)
173 cu = "%s%s" % (repobaseurl, qs)
172 size = 0
174 size = 0
173 if util.safehasattr(data, 'length'):
175 if util.safehasattr(data, 'length'):
174 size = data.length
176 size = data.length
175 elif data is not None:
177 elif data is not None:
176 size = len(data)
178 size = len(data)
177 if data is not None and r'Content-Type' not in headers:
179 if data is not None and r'Content-Type' not in headers:
178 headers[r'Content-Type'] = r'application/mercurial-0.1'
180 headers[r'Content-Type'] = r'application/mercurial-0.1'
179
181
180 # Tell the server we accept application/mercurial-0.2 and multiple
182 # Tell the server we accept application/mercurial-0.2 and multiple
181 # compression formats if the server is capable of emitting those
183 # compression formats if the server is capable of emitting those
182 # payloads.
184 # payloads.
183 # Note: Keep this set empty by default, as client advertisement of
185 # Note: Keep this set empty by default, as client advertisement of
184 # protocol parameters should only occur after the handshake.
186 # protocol parameters should only occur after the handshake.
185 protoparams = set()
187 protoparams = set()
186
188
187 mediatypes = set()
189 mediatypes = set()
188 if caps is not None:
190 if caps is not None:
189 mt = capablefn('httpmediatype')
191 mt = capablefn('httpmediatype')
190 if mt:
192 if mt:
191 protoparams.add('0.1')
193 protoparams.add('0.1')
192 mediatypes = set(mt.split(','))
194 mediatypes = set(mt.split(','))
193
195
194 protoparams.add('partial-pull')
196 protoparams.add('partial-pull')
195
197
196 if '0.2tx' in mediatypes:
198 if '0.2tx' in mediatypes:
197 protoparams.add('0.2')
199 protoparams.add('0.2')
198
200
199 if '0.2tx' in mediatypes and capablefn('compression'):
201 if '0.2tx' in mediatypes and capablefn('compression'):
200 # We /could/ compare supported compression formats and prune
202 # We /could/ compare supported compression formats and prune
201 # non-mutually supported or error if nothing is mutually supported.
203 # non-mutually supported or error if nothing is mutually supported.
202 # For now, send the full list to the server and have it error.
204 # For now, send the full list to the server and have it error.
203 comps = [e.wireprotosupport().name for e in
205 comps = [e.wireprotosupport().name for e in
204 util.compengines.supportedwireengines(util.CLIENTROLE)]
206 util.compengines.supportedwireengines(util.CLIENTROLE)]
205 protoparams.add('comp=%s' % ','.join(comps))
207 protoparams.add('comp=%s' % ','.join(comps))
206
208
207 if protoparams:
209 if protoparams:
208 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
210 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
209 'X-HgProto',
211 'X-HgProto',
210 headersize or 1024)
212 headersize or 1024)
211 for header, value in protoheaders:
213 for header, value in protoheaders:
212 headers[header] = value
214 headers[header] = value
213
215
214 varyheaders = []
216 varyheaders = []
215 for header in headers:
217 for header in headers:
216 if header.lower().startswith(r'x-hg'):
218 if header.lower().startswith(r'x-hg'):
217 varyheaders.append(header)
219 varyheaders.append(header)
218
220
219 if varyheaders:
221 if varyheaders:
220 headers[r'Vary'] = r','.join(sorted(varyheaders))
222 headers[r'Vary'] = r','.join(sorted(varyheaders))
221
223
222 req = requestbuilder(pycompat.strurl(cu), data, headers)
224 req = requestbuilder(pycompat.strurl(cu), data, headers)
223
225
224 if data is not None:
226 if data is not None:
225 ui.debug("sending %d bytes\n" % size)
227 ui.debug("sending %d bytes\n" % size)
226 req.add_unredirected_header(r'Content-Length', r'%d' % size)
228 req.add_unredirected_header(r'Content-Length', r'%d' % size)
227
229
228 return req, cu, qs
230 return req, cu, qs
229
231
230 def _reqdata(req):
232 def _reqdata(req):
231 """Get request data, if any. If no data, returns None."""
233 """Get request data, if any. If no data, returns None."""
232 if pycompat.ispy3:
234 if pycompat.ispy3:
233 return req.data
235 return req.data
234 if not req.has_data():
236 if not req.has_data():
235 return None
237 return None
236 return req.get_data()
238 return req.get_data()
237
239
238 def sendrequest(ui, opener, req):
240 def sendrequest(ui, opener, req):
239 """Send a prepared HTTP request.
241 """Send a prepared HTTP request.
240
242
241 Returns the response object.
243 Returns the response object.
242 """
244 """
243 dbg = ui.debug
245 dbg = ui.debug
244 if (ui.debugflag
246 if (ui.debugflag
245 and ui.configbool('devel', 'debug.peer-request')):
247 and ui.configbool('devel', 'debug.peer-request')):
246 line = 'devel-peer-request: %s\n'
248 line = 'devel-peer-request: %s\n'
247 dbg(line % '%s %s' % (pycompat.bytesurl(req.get_method()),
249 dbg(line % '%s %s' % (pycompat.bytesurl(req.get_method()),
248 pycompat.bytesurl(req.get_full_url())))
250 pycompat.bytesurl(req.get_full_url())))
249 hgargssize = None
251 hgargssize = None
250
252
251 for header, value in sorted(req.header_items()):
253 for header, value in sorted(req.header_items()):
252 header = pycompat.bytesurl(header)
254 header = pycompat.bytesurl(header)
253 value = pycompat.bytesurl(value)
255 value = pycompat.bytesurl(value)
254 if header.startswith('X-hgarg-'):
256 if header.startswith('X-hgarg-'):
255 if hgargssize is None:
257 if hgargssize is None:
256 hgargssize = 0
258 hgargssize = 0
257 hgargssize += len(value)
259 hgargssize += len(value)
258 else:
260 else:
259 dbg(line % ' %s %s' % (header, value))
261 dbg(line % ' %s %s' % (header, value))
260
262
261 if hgargssize is not None:
263 if hgargssize is not None:
262 dbg(line % ' %d bytes of commands arguments in headers'
264 dbg(line % ' %d bytes of commands arguments in headers'
263 % hgargssize)
265 % hgargssize)
264 data = _reqdata(req)
266 data = _reqdata(req)
265 if data is not None:
267 if data is not None:
266 length = getattr(data, 'length', None)
268 length = getattr(data, 'length', None)
267 if length is None:
269 if length is None:
268 length = len(data)
270 length = len(data)
269 dbg(line % ' %d bytes of data' % length)
271 dbg(line % ' %d bytes of data' % length)
270
272
271 start = util.timer()
273 start = util.timer()
272
274
273 res = None
275 res = None
274 try:
276 try:
275 res = opener.open(req)
277 res = opener.open(req)
276 except urlerr.httperror as inst:
278 except urlerr.httperror as inst:
277 if inst.code == 401:
279 if inst.code == 401:
278 raise error.Abort(_('authorization failed'))
280 raise error.Abort(_('authorization failed'))
279 raise
281 raise
280 except httplib.HTTPException as inst:
282 except httplib.HTTPException as inst:
281 ui.debug('http error requesting %s\n' %
283 ui.debug('http error requesting %s\n' %
282 util.hidepassword(req.get_full_url()))
284 util.hidepassword(req.get_full_url()))
283 ui.traceback()
285 ui.traceback()
284 raise IOError(None, inst)
286 raise IOError(None, inst)
285 finally:
287 finally:
286 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
288 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
287 code = res.code if res else -1
289 code = res.code if res else -1
288 dbg(line % ' finished in %.4f seconds (%d)'
290 dbg(line % ' finished in %.4f seconds (%d)'
289 % (util.timer() - start, code))
291 % (util.timer() - start, code))
290
292
291 # Insert error handlers for common I/O failures.
293 # Insert error handlers for common I/O failures.
292 urlmod.wrapresponse(res)
294 urlmod.wrapresponse(res)
293
295
294 return res
296 return res
295
297
296 class RedirectedRepoError(error.RepoError):
298 class RedirectedRepoError(error.RepoError):
297 def __init__(self, msg, respurl):
299 def __init__(self, msg, respurl):
298 super(RedirectedRepoError, self).__init__(msg)
300 super(RedirectedRepoError, self).__init__(msg)
299 self.respurl = respurl
301 self.respurl = respurl
300
302
301 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
303 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
302 allowcbor=False):
304 allowcbor=False):
303 # record the url we got redirected to
305 # record the url we got redirected to
304 redirected = False
306 redirected = False
305 respurl = pycompat.bytesurl(resp.geturl())
307 respurl = pycompat.bytesurl(resp.geturl())
306 if respurl.endswith(qs):
308 if respurl.endswith(qs):
307 respurl = respurl[:-len(qs)]
309 respurl = respurl[:-len(qs)]
308 qsdropped = False
310 qsdropped = False
309 else:
311 else:
310 qsdropped = True
312 qsdropped = True
311
313
312 if baseurl.rstrip('/') != respurl.rstrip('/'):
314 if baseurl.rstrip('/') != respurl.rstrip('/'):
313 redirected = True
315 redirected = True
314 if not ui.quiet:
316 if not ui.quiet:
315 ui.warn(_('real URL is %s\n') % respurl)
317 ui.warn(_('real URL is %s\n') % respurl)
316
318
317 try:
319 try:
318 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
320 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
319 except AttributeError:
321 except AttributeError:
320 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
322 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
321
323
322 safeurl = util.hidepassword(baseurl)
324 safeurl = util.hidepassword(baseurl)
323 if proto.startswith('application/hg-error'):
325 if proto.startswith('application/hg-error'):
324 raise error.OutOfBandError(resp.read())
326 raise error.OutOfBandError(resp.read())
325
327
326 # Pre 1.0 versions of Mercurial used text/plain and
328 # Pre 1.0 versions of Mercurial used text/plain and
327 # application/hg-changegroup. We don't support such old servers.
329 # application/hg-changegroup. We don't support such old servers.
328 if not proto.startswith('application/mercurial-'):
330 if not proto.startswith('application/mercurial-'):
329 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
331 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
330 msg = _("'%s' does not appear to be an hg repository:\n"
332 msg = _("'%s' does not appear to be an hg repository:\n"
331 "---%%<--- (%s)\n%s\n---%%<---\n") % (
333 "---%%<--- (%s)\n%s\n---%%<---\n") % (
332 safeurl, proto or 'no content-type', resp.read(1024))
334 safeurl, proto or 'no content-type', resp.read(1024))
333
335
334 # Some servers may strip the query string from the redirect. We
336 # Some servers may strip the query string from the redirect. We
335 # raise a special error type so callers can react to this specially.
337 # raise a special error type so callers can react to this specially.
336 if redirected and qsdropped:
338 if redirected and qsdropped:
337 raise RedirectedRepoError(msg, respurl)
339 raise RedirectedRepoError(msg, respurl)
338 else:
340 else:
339 raise error.RepoError(msg)
341 raise error.RepoError(msg)
340
342
341 try:
343 try:
342 subtype = proto.split('-', 1)[1]
344 subtype = proto.split('-', 1)[1]
343
345
344 # Unless we end up supporting CBOR in the legacy wire protocol,
346 # Unless we end up supporting CBOR in the legacy wire protocol,
345 # this should ONLY be encountered for the initial capabilities
347 # this should ONLY be encountered for the initial capabilities
346 # request during handshake.
348 # request during handshake.
347 if subtype == 'cbor':
349 if subtype == 'cbor':
348 if allowcbor:
350 if allowcbor:
349 return respurl, proto, resp
351 return respurl, proto, resp
350 else:
352 else:
351 raise error.RepoError(_('unexpected CBOR response from '
353 raise error.RepoError(_('unexpected CBOR response from '
352 'server'))
354 'server'))
353
355
354 version_info = tuple([int(n) for n in subtype.split('.')])
356 version_info = tuple([int(n) for n in subtype.split('.')])
355 except ValueError:
357 except ValueError:
356 raise error.RepoError(_("'%s' sent a broken Content-Type "
358 raise error.RepoError(_("'%s' sent a broken Content-Type "
357 "header (%s)") % (safeurl, proto))
359 "header (%s)") % (safeurl, proto))
358
360
359 # TODO consider switching to a decompression reader that uses
361 # TODO consider switching to a decompression reader that uses
360 # generators.
362 # generators.
361 if version_info == (0, 1):
363 if version_info == (0, 1):
362 if compressible:
364 if compressible:
363 resp = util.compengines['zlib'].decompressorreader(resp)
365 resp = util.compengines['zlib'].decompressorreader(resp)
364
366
365 elif version_info == (0, 2):
367 elif version_info == (0, 2):
366 # application/mercurial-0.2 always identifies the compression
368 # application/mercurial-0.2 always identifies the compression
367 # engine in the payload header.
369 # engine in the payload header.
368 elen = struct.unpack('B', util.readexactly(resp, 1))[0]
370 elen = struct.unpack('B', util.readexactly(resp, 1))[0]
369 ename = util.readexactly(resp, elen)
371 ename = util.readexactly(resp, elen)
370 engine = util.compengines.forwiretype(ename)
372 engine = util.compengines.forwiretype(ename)
371
373
372 resp = engine.decompressorreader(resp)
374 resp = engine.decompressorreader(resp)
373 else:
375 else:
374 raise error.RepoError(_("'%s' uses newer protocol %s") %
376 raise error.RepoError(_("'%s' uses newer protocol %s") %
375 (safeurl, subtype))
377 (safeurl, subtype))
376
378
377 return respurl, proto, resp
379 return respurl, proto, resp
378
380
379 class httppeer(wireprotov1peer.wirepeer):
381 class httppeer(wireprotov1peer.wirepeer):
380 def __init__(self, ui, path, url, opener, requestbuilder, caps):
382 def __init__(self, ui, path, url, opener, requestbuilder, caps):
381 self.ui = ui
383 self.ui = ui
382 self._path = path
384 self._path = path
383 self._url = url
385 self._url = url
384 self._caps = caps
386 self._caps = caps
385 self.limitedarguments = caps is not None and 'httppostargs' not in caps
387 self.limitedarguments = caps is not None and 'httppostargs' not in caps
386 self._urlopener = opener
388 self._urlopener = opener
387 self._requestbuilder = requestbuilder
389 self._requestbuilder = requestbuilder
388
390
389 def __del__(self):
391 def __del__(self):
390 for h in self._urlopener.handlers:
392 for h in self._urlopener.handlers:
391 h.close()
393 h.close()
392 getattr(h, "close_all", lambda: None)()
394 getattr(h, "close_all", lambda: None)()
393
395
394 # Begin of ipeerconnection interface.
396 # Begin of ipeerconnection interface.
395
397
396 def url(self):
398 def url(self):
397 return self._path
399 return self._path
398
400
399 def local(self):
401 def local(self):
400 return None
402 return None
401
403
402 def peer(self):
404 def peer(self):
403 return self
405 return self
404
406
405 def canpush(self):
407 def canpush(self):
406 return True
408 return True
407
409
408 def close(self):
410 def close(self):
409 try:
411 try:
410 reqs, sent, recv = (self._urlopener.requestscount,
412 reqs, sent, recv = (self._urlopener.requestscount,
411 self._urlopener.sentbytescount,
413 self._urlopener.sentbytescount,
412 self._urlopener.receivedbytescount)
414 self._urlopener.receivedbytescount)
413 except AttributeError:
415 except AttributeError:
414 return
416 return
415 self.ui.note(_('(sent %d HTTP requests and %d bytes; '
417 self.ui.note(_('(sent %d HTTP requests and %d bytes; '
416 'received %d bytes in responses)\n') %
418 'received %d bytes in responses)\n') %
417 (reqs, sent, recv))
419 (reqs, sent, recv))
418
420
419 # End of ipeerconnection interface.
421 # End of ipeerconnection interface.
420
422
421 # Begin of ipeercommands interface.
423 # Begin of ipeercommands interface.
422
424
423 def capabilities(self):
425 def capabilities(self):
424 return self._caps
426 return self._caps
425
427
426 # End of ipeercommands interface.
428 # End of ipeercommands interface.
427
429
428 def _callstream(self, cmd, _compressible=False, **args):
430 def _callstream(self, cmd, _compressible=False, **args):
429 args = pycompat.byteskwargs(args)
431 args = pycompat.byteskwargs(args)
430
432
431 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
433 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
432 self._caps, self.capable,
434 self._caps, self.capable,
433 self._url, cmd, args)
435 self._url, cmd, args)
434
436
435 resp = sendrequest(self.ui, self._urlopener, req)
437 resp = sendrequest(self.ui, self._urlopener, req)
436
438
437 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
439 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
438 resp, _compressible)
440 resp, _compressible)
439
441
440 return resp
442 return resp
441
443
442 def _call(self, cmd, **args):
444 def _call(self, cmd, **args):
443 fp = self._callstream(cmd, **args)
445 fp = self._callstream(cmd, **args)
444 try:
446 try:
445 return fp.read()
447 return fp.read()
446 finally:
448 finally:
447 # if using keepalive, allow connection to be reused
449 # if using keepalive, allow connection to be reused
448 fp.close()
450 fp.close()
449
451
450 def _callpush(self, cmd, cg, **args):
452 def _callpush(self, cmd, cg, **args):
451 # have to stream bundle to a temp file because we do not have
453 # have to stream bundle to a temp file because we do not have
452 # http 1.1 chunked transfer.
454 # http 1.1 chunked transfer.
453
455
454 types = self.capable('unbundle')
456 types = self.capable('unbundle')
455 try:
457 try:
456 types = types.split(',')
458 types = types.split(',')
457 except AttributeError:
459 except AttributeError:
458 # servers older than d1b16a746db6 will send 'unbundle' as a
460 # servers older than d1b16a746db6 will send 'unbundle' as a
459 # boolean capability. They only support headerless/uncompressed
461 # boolean capability. They only support headerless/uncompressed
460 # bundles.
462 # bundles.
461 types = [""]
463 types = [""]
462 for x in types:
464 for x in types:
463 if x in bundle2.bundletypes:
465 if x in bundle2.bundletypes:
464 type = x
466 type = x
465 break
467 break
466
468
467 tempname = bundle2.writebundle(self.ui, cg, None, type)
469 tempname = bundle2.writebundle(self.ui, cg, None, type)
468 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
470 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
469 headers = {r'Content-Type': r'application/mercurial-0.1'}
471 headers = {r'Content-Type': r'application/mercurial-0.1'}
470
472
471 try:
473 try:
472 r = self._call(cmd, data=fp, headers=headers, **args)
474 r = self._call(cmd, data=fp, headers=headers, **args)
473 vals = r.split('\n', 1)
475 vals = r.split('\n', 1)
474 if len(vals) < 2:
476 if len(vals) < 2:
475 raise error.ResponseError(_("unexpected response:"), r)
477 raise error.ResponseError(_("unexpected response:"), r)
476 return vals
478 return vals
477 except urlerr.httperror:
479 except urlerr.httperror:
478 # Catch and re-raise these so we don't try and treat them
480 # Catch and re-raise these so we don't try and treat them
479 # like generic socket errors. They lack any values in
481 # like generic socket errors. They lack any values in
480 # .args on Python 3 which breaks our socket.error block.
482 # .args on Python 3 which breaks our socket.error block.
481 raise
483 raise
482 except socket.error as err:
484 except socket.error as err:
483 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
485 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
484 raise error.Abort(_('push failed: %s') % err.args[1])
486 raise error.Abort(_('push failed: %s') % err.args[1])
485 raise error.Abort(err.args[1])
487 raise error.Abort(err.args[1])
486 finally:
488 finally:
487 fp.close()
489 fp.close()
488 os.unlink(tempname)
490 os.unlink(tempname)
489
491
490 def _calltwowaystream(self, cmd, fp, **args):
492 def _calltwowaystream(self, cmd, fp, **args):
491 fh = None
493 fh = None
492 fp_ = None
494 fp_ = None
493 filename = None
495 filename = None
494 try:
496 try:
495 # dump bundle to disk
497 # dump bundle to disk
496 fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg")
498 fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg")
497 fh = os.fdopen(fd, r"wb")
499 fh = os.fdopen(fd, r"wb")
498 d = fp.read(4096)
500 d = fp.read(4096)
499 while d:
501 while d:
500 fh.write(d)
502 fh.write(d)
501 d = fp.read(4096)
503 d = fp.read(4096)
502 fh.close()
504 fh.close()
503 # start http push
505 # start http push
504 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
506 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
505 headers = {r'Content-Type': r'application/mercurial-0.1'}
507 headers = {r'Content-Type': r'application/mercurial-0.1'}
506 return self._callstream(cmd, data=fp_, headers=headers, **args)
508 return self._callstream(cmd, data=fp_, headers=headers, **args)
507 finally:
509 finally:
508 if fp_ is not None:
510 if fp_ is not None:
509 fp_.close()
511 fp_.close()
510 if fh is not None:
512 if fh is not None:
511 fh.close()
513 fh.close()
512 os.unlink(filename)
514 os.unlink(filename)
513
515
514 def _callcompressable(self, cmd, **args):
516 def _callcompressable(self, cmd, **args):
515 return self._callstream(cmd, _compressible=True, **args)
517 return self._callstream(cmd, _compressible=True, **args)
516
518
517 def _abort(self, exception):
519 def _abort(self, exception):
518 raise exception
520 raise exception
519
521
520 def sendv2request(ui, opener, requestbuilder, apiurl, permission, requests,
522 def sendv2request(ui, opener, requestbuilder, apiurl, permission, requests,
521 redirect):
523 redirect):
522 wireprotoframing.populatestreamencoders()
524 wireprotoframing.populatestreamencoders()
523
525
524 uiencoders = ui.configlist(b'experimental', b'httppeer.v2-encoder-order')
526 uiencoders = ui.configlist(b'experimental', b'httppeer.v2-encoder-order')
525
527
526 if uiencoders:
528 if uiencoders:
527 encoders = []
529 encoders = []
528
530
529 for encoder in uiencoders:
531 for encoder in uiencoders:
530 if encoder not in wireprotoframing.STREAM_ENCODERS:
532 if encoder not in wireprotoframing.STREAM_ENCODERS:
531 ui.warn(_(b'wire protocol version 2 encoder referenced in '
533 ui.warn(_(b'wire protocol version 2 encoder referenced in '
532 b'config (%s) is not known; ignoring\n') % encoder)
534 b'config (%s) is not known; ignoring\n') % encoder)
533 else:
535 else:
534 encoders.append(encoder)
536 encoders.append(encoder)
535
537
536 else:
538 else:
537 encoders = wireprotoframing.STREAM_ENCODERS_ORDER
539 encoders = wireprotoframing.STREAM_ENCODERS_ORDER
538
540
539 reactor = wireprotoframing.clientreactor(ui,
541 reactor = wireprotoframing.clientreactor(ui,
540 hasmultiplesend=False,
542 hasmultiplesend=False,
541 buffersends=True,
543 buffersends=True,
542 clientcontentencoders=encoders)
544 clientcontentencoders=encoders)
543
545
544 handler = wireprotov2peer.clienthandler(ui, reactor,
546 handler = wireprotov2peer.clienthandler(ui, reactor,
545 opener=opener,
547 opener=opener,
546 requestbuilder=requestbuilder)
548 requestbuilder=requestbuilder)
547
549
548 url = '%s/%s' % (apiurl, permission)
550 url = '%s/%s' % (apiurl, permission)
549
551
550 if len(requests) > 1:
552 if len(requests) > 1:
551 url += '/multirequest'
553 url += '/multirequest'
552 else:
554 else:
553 url += '/%s' % requests[0][0]
555 url += '/%s' % requests[0][0]
554
556
555 ui.debug('sending %d commands\n' % len(requests))
557 ui.debug('sending %d commands\n' % len(requests))
556 for command, args, f in requests:
558 for command, args, f in requests:
557 ui.debug('sending command %s: %s\n' % (
559 ui.debug('sending command %s: %s\n' % (
558 command, stringutil.pprint(args, indent=2)))
560 command, stringutil.pprint(args, indent=2)))
559 assert not list(handler.callcommand(command, args, f,
561 assert not list(handler.callcommand(command, args, f,
560 redirect=redirect))
562 redirect=redirect))
561
563
562 # TODO stream this.
564 # TODO stream this.
563 body = b''.join(map(bytes, handler.flushcommands()))
565 body = b''.join(map(bytes, handler.flushcommands()))
564
566
565 # TODO modify user-agent to reflect v2
567 # TODO modify user-agent to reflect v2
566 headers = {
568 headers = {
567 r'Accept': wireprotov2server.FRAMINGTYPE,
569 r'Accept': wireprotov2server.FRAMINGTYPE,
568 r'Content-Type': wireprotov2server.FRAMINGTYPE,
570 r'Content-Type': wireprotov2server.FRAMINGTYPE,
569 }
571 }
570
572
571 req = requestbuilder(pycompat.strurl(url), body, headers)
573 req = requestbuilder(pycompat.strurl(url), body, headers)
572 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
574 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
573
575
574 try:
576 try:
575 res = opener.open(req)
577 res = opener.open(req)
576 except urlerr.httperror as e:
578 except urlerr.httperror as e:
577 if e.code == 401:
579 if e.code == 401:
578 raise error.Abort(_('authorization failed'))
580 raise error.Abort(_('authorization failed'))
579
581
580 raise
582 raise
581 except httplib.HTTPException as e:
583 except httplib.HTTPException as e:
582 ui.traceback()
584 ui.traceback()
583 raise IOError(None, e)
585 raise IOError(None, e)
584
586
585 return handler, res
587 return handler, res
586
588
587 class queuedcommandfuture(pycompat.futures.Future):
589 class queuedcommandfuture(pycompat.futures.Future):
588 """Wraps result() on command futures to trigger submission on call."""
590 """Wraps result() on command futures to trigger submission on call."""
589
591
590 def result(self, timeout=None):
592 def result(self, timeout=None):
591 if self.done():
593 if self.done():
592 return pycompat.futures.Future.result(self, timeout)
594 return pycompat.futures.Future.result(self, timeout)
593
595
594 self._peerexecutor.sendcommands()
596 self._peerexecutor.sendcommands()
595
597
596 # sendcommands() will restore the original __class__ and self.result
598 # sendcommands() will restore the original __class__ and self.result
597 # will resolve to Future.result.
599 # will resolve to Future.result.
598 return self.result(timeout)
600 return self.result(timeout)
599
601
600 @interfaceutil.implementer(repository.ipeercommandexecutor)
602 @interfaceutil.implementer(repository.ipeercommandexecutor)
601 class httpv2executor(object):
603 class httpv2executor(object):
602 def __init__(self, ui, opener, requestbuilder, apiurl, descriptor,
604 def __init__(self, ui, opener, requestbuilder, apiurl, descriptor,
603 redirect):
605 redirect):
604 self._ui = ui
606 self._ui = ui
605 self._opener = opener
607 self._opener = opener
606 self._requestbuilder = requestbuilder
608 self._requestbuilder = requestbuilder
607 self._apiurl = apiurl
609 self._apiurl = apiurl
608 self._descriptor = descriptor
610 self._descriptor = descriptor
609 self._redirect = redirect
611 self._redirect = redirect
610 self._sent = False
612 self._sent = False
611 self._closed = False
613 self._closed = False
612 self._neededpermissions = set()
614 self._neededpermissions = set()
613 self._calls = []
615 self._calls = []
614 self._futures = weakref.WeakSet()
616 self._futures = weakref.WeakSet()
615 self._responseexecutor = None
617 self._responseexecutor = None
616 self._responsef = None
618 self._responsef = None
617
619
618 def __enter__(self):
620 def __enter__(self):
619 return self
621 return self
620
622
621 def __exit__(self, exctype, excvalue, exctb):
623 def __exit__(self, exctype, excvalue, exctb):
622 self.close()
624 self.close()
623
625
624 def callcommand(self, command, args):
626 def callcommand(self, command, args):
625 if self._sent:
627 if self._sent:
626 raise error.ProgrammingError('callcommand() cannot be used after '
628 raise error.ProgrammingError('callcommand() cannot be used after '
627 'commands are sent')
629 'commands are sent')
628
630
629 if self._closed:
631 if self._closed:
630 raise error.ProgrammingError('callcommand() cannot be used after '
632 raise error.ProgrammingError('callcommand() cannot be used after '
631 'close()')
633 'close()')
632
634
633 # The service advertises which commands are available. So if we attempt
635 # The service advertises which commands are available. So if we attempt
634 # to call an unknown command or pass an unknown argument, we can screen
636 # to call an unknown command or pass an unknown argument, we can screen
635 # for this.
637 # for this.
636 if command not in self._descriptor['commands']:
638 if command not in self._descriptor['commands']:
637 raise error.ProgrammingError(
639 raise error.ProgrammingError(
638 'wire protocol command %s is not available' % command)
640 'wire protocol command %s is not available' % command)
639
641
640 cmdinfo = self._descriptor['commands'][command]
642 cmdinfo = self._descriptor['commands'][command]
641 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {}))
643 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {}))
642
644
643 if unknownargs:
645 if unknownargs:
644 raise error.ProgrammingError(
646 raise error.ProgrammingError(
645 'wire protocol command %s does not accept argument: %s' % (
647 'wire protocol command %s does not accept argument: %s' % (
646 command, ', '.join(sorted(unknownargs))))
648 command, ', '.join(sorted(unknownargs))))
647
649
648 self._neededpermissions |= set(cmdinfo['permissions'])
650 self._neededpermissions |= set(cmdinfo['permissions'])
649
651
650 # TODO we /could/ also validate types here, since the API descriptor
652 # TODO we /could/ also validate types here, since the API descriptor
651 # includes types...
653 # includes types...
652
654
653 f = pycompat.futures.Future()
655 f = pycompat.futures.Future()
654
656
655 # Monkeypatch it so result() triggers sendcommands(), otherwise result()
657 # Monkeypatch it so result() triggers sendcommands(), otherwise result()
656 # could deadlock.
658 # could deadlock.
657 f.__class__ = queuedcommandfuture
659 f.__class__ = queuedcommandfuture
658 f._peerexecutor = self
660 f._peerexecutor = self
659
661
660 self._futures.add(f)
662 self._futures.add(f)
661 self._calls.append((command, args, f))
663 self._calls.append((command, args, f))
662
664
663 return f
665 return f
664
666
665 def sendcommands(self):
667 def sendcommands(self):
666 if self._sent:
668 if self._sent:
667 return
669 return
668
670
669 if not self._calls:
671 if not self._calls:
670 return
672 return
671
673
672 self._sent = True
674 self._sent = True
673
675
674 # Unhack any future types so caller sees a clean type and so we
676 # Unhack any future types so caller sees a clean type and so we
675 # break reference cycle.
677 # break reference cycle.
676 for f in self._futures:
678 for f in self._futures:
677 if isinstance(f, queuedcommandfuture):
679 if isinstance(f, queuedcommandfuture):
678 f.__class__ = pycompat.futures.Future
680 f.__class__ = pycompat.futures.Future
679 f._peerexecutor = None
681 f._peerexecutor = None
680
682
681 # Mark the future as running and filter out cancelled futures.
683 # Mark the future as running and filter out cancelled futures.
682 calls = [(command, args, f)
684 calls = [(command, args, f)
683 for command, args, f in self._calls
685 for command, args, f in self._calls
684 if f.set_running_or_notify_cancel()]
686 if f.set_running_or_notify_cancel()]
685
687
686 # Clear out references, prevent improper object usage.
688 # Clear out references, prevent improper object usage.
687 self._calls = None
689 self._calls = None
688
690
689 if not calls:
691 if not calls:
690 return
692 return
691
693
692 permissions = set(self._neededpermissions)
694 permissions = set(self._neededpermissions)
693
695
694 if 'push' in permissions and 'pull' in permissions:
696 if 'push' in permissions and 'pull' in permissions:
695 permissions.remove('pull')
697 permissions.remove('pull')
696
698
697 if len(permissions) > 1:
699 if len(permissions) > 1:
698 raise error.RepoError(_('cannot make request requiring multiple '
700 raise error.RepoError(_('cannot make request requiring multiple '
699 'permissions: %s') %
701 'permissions: %s') %
700 _(', ').join(sorted(permissions)))
702 _(', ').join(sorted(permissions)))
701
703
702 permission = {
704 permission = {
703 'push': 'rw',
705 'push': 'rw',
704 'pull': 'ro',
706 'pull': 'ro',
705 }[permissions.pop()]
707 }[permissions.pop()]
706
708
707 handler, resp = sendv2request(
709 handler, resp = sendv2request(
708 self._ui, self._opener, self._requestbuilder, self._apiurl,
710 self._ui, self._opener, self._requestbuilder, self._apiurl,
709 permission, calls, self._redirect)
711 permission, calls, self._redirect)
710
712
711 # TODO we probably want to validate the HTTP code, media type, etc.
713 # TODO we probably want to validate the HTTP code, media type, etc.
712
714
713 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
715 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
714 self._responsef = self._responseexecutor.submit(self._handleresponse,
716 self._responsef = self._responseexecutor.submit(self._handleresponse,
715 handler, resp)
717 handler, resp)
716
718
717 def close(self):
719 def close(self):
718 if self._closed:
720 if self._closed:
719 return
721 return
720
722
721 self.sendcommands()
723 self.sendcommands()
722
724
723 self._closed = True
725 self._closed = True
724
726
725 if not self._responsef:
727 if not self._responsef:
726 return
728 return
727
729
728 # TODO ^C here may not result in immediate program termination.
730 # TODO ^C here may not result in immediate program termination.
729
731
730 try:
732 try:
731 self._responsef.result()
733 self._responsef.result()
732 finally:
734 finally:
733 self._responseexecutor.shutdown(wait=True)
735 self._responseexecutor.shutdown(wait=True)
734 self._responsef = None
736 self._responsef = None
735 self._responseexecutor = None
737 self._responseexecutor = None
736
738
737 # If any of our futures are still in progress, mark them as
739 # If any of our futures are still in progress, mark them as
738 # errored, otherwise a result() could wait indefinitely.
740 # errored, otherwise a result() could wait indefinitely.
739 for f in self._futures:
741 for f in self._futures:
740 if not f.done():
742 if not f.done():
741 f.set_exception(error.ResponseError(
743 f.set_exception(error.ResponseError(
742 _('unfulfilled command response')))
744 _('unfulfilled command response')))
743
745
744 self._futures = None
746 self._futures = None
745
747
746 def _handleresponse(self, handler, resp):
748 def _handleresponse(self, handler, resp):
747 # Called in a thread to read the response.
749 # Called in a thread to read the response.
748
750
749 while handler.readdata(resp):
751 while handler.readdata(resp):
750 pass
752 pass
751
753
752 @interfaceutil.implementer(repository.ipeerv2)
754 @interfaceutil.implementer(repository.ipeerv2)
753 class httpv2peer(object):
755 class httpv2peer(object):
754
756
755 limitedarguments = False
757 limitedarguments = False
756
758
757 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
759 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
758 apidescriptor):
760 apidescriptor):
759 self.ui = ui
761 self.ui = ui
760 self.apidescriptor = apidescriptor
762 self.apidescriptor = apidescriptor
761
763
762 if repourl.endswith('/'):
764 if repourl.endswith('/'):
763 repourl = repourl[:-1]
765 repourl = repourl[:-1]
764
766
765 self._url = repourl
767 self._url = repourl
766 self._apipath = apipath
768 self._apipath = apipath
767 self._apiurl = '%s/%s' % (repourl, apipath)
769 self._apiurl = '%s/%s' % (repourl, apipath)
768 self._opener = opener
770 self._opener = opener
769 self._requestbuilder = requestbuilder
771 self._requestbuilder = requestbuilder
770
772
771 self._redirect = wireprotov2peer.supportedredirects(ui, apidescriptor)
773 self._redirect = wireprotov2peer.supportedredirects(ui, apidescriptor)
772
774
773 # Start of ipeerconnection.
775 # Start of ipeerconnection.
774
776
775 def url(self):
777 def url(self):
776 return self._url
778 return self._url
777
779
778 def local(self):
780 def local(self):
779 return None
781 return None
780
782
781 def peer(self):
783 def peer(self):
782 return self
784 return self
783
785
784 def canpush(self):
786 def canpush(self):
785 # TODO change once implemented.
787 # TODO change once implemented.
786 return False
788 return False
787
789
788 def close(self):
790 def close(self):
789 self.ui.note(_('(sent %d HTTP requests and %d bytes; '
791 self.ui.note(_('(sent %d HTTP requests and %d bytes; '
790 'received %d bytes in responses)\n') %
792 'received %d bytes in responses)\n') %
791 (self._opener.requestscount,
793 (self._opener.requestscount,
792 self._opener.sentbytescount,
794 self._opener.sentbytescount,
793 self._opener.receivedbytescount))
795 self._opener.receivedbytescount))
794
796
795 # End of ipeerconnection.
797 # End of ipeerconnection.
796
798
797 # Start of ipeercapabilities.
799 # Start of ipeercapabilities.
798
800
799 def capable(self, name):
801 def capable(self, name):
800 # The capabilities used internally historically map to capabilities
802 # The capabilities used internally historically map to capabilities
801 # advertised from the "capabilities" wire protocol command. However,
803 # advertised from the "capabilities" wire protocol command. However,
802 # version 2 of that command works differently.
804 # version 2 of that command works differently.
803
805
804 # Maps to commands that are available.
806 # Maps to commands that are available.
805 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
807 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
806 return True
808 return True
807
809
808 # Other concepts.
810 # Other concepts.
809 if name in ('bundle2'):
811 if name in ('bundle2'):
810 return True
812 return True
811
813
812 # Alias command-* to presence of command of that name.
814 # Alias command-* to presence of command of that name.
813 if name.startswith('command-'):
815 if name.startswith('command-'):
814 return name[len('command-'):] in self.apidescriptor['commands']
816 return name[len('command-'):] in self.apidescriptor['commands']
815
817
816 return False
818 return False
817
819
818 def requirecap(self, name, purpose):
820 def requirecap(self, name, purpose):
819 if self.capable(name):
821 if self.capable(name):
820 return
822 return
821
823
822 raise error.CapabilityError(
824 raise error.CapabilityError(
823 _('cannot %s; client or remote repository does not support the '
825 _('cannot %s; client or remote repository does not support the '
824 '\'%s\' capability') % (purpose, name))
826 '\'%s\' capability') % (purpose, name))
825
827
826 # End of ipeercapabilities.
828 # End of ipeercapabilities.
827
829
828 def _call(self, name, **args):
830 def _call(self, name, **args):
829 with self.commandexecutor() as e:
831 with self.commandexecutor() as e:
830 return e.callcommand(name, args).result()
832 return e.callcommand(name, args).result()
831
833
832 def commandexecutor(self):
834 def commandexecutor(self):
833 return httpv2executor(self.ui, self._opener, self._requestbuilder,
835 return httpv2executor(self.ui, self._opener, self._requestbuilder,
834 self._apiurl, self.apidescriptor, self._redirect)
836 self._apiurl, self.apidescriptor, self._redirect)
835
837
836 # Registry of API service names to metadata about peers that handle it.
838 # Registry of API service names to metadata about peers that handle it.
837 #
839 #
838 # The following keys are meaningful:
840 # The following keys are meaningful:
839 #
841 #
840 # init
842 # init
841 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
843 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
842 # apidescriptor) to create a peer.
844 # apidescriptor) to create a peer.
843 #
845 #
844 # priority
846 # priority
845 # Integer priority for the service. If we could choose from multiple
847 # Integer priority for the service. If we could choose from multiple
846 # services, we choose the one with the highest priority.
848 # services, we choose the one with the highest priority.
847 API_PEERS = {
849 API_PEERS = {
848 wireprototypes.HTTP_WIREPROTO_V2: {
850 wireprototypes.HTTP_WIREPROTO_V2: {
849 'init': httpv2peer,
851 'init': httpv2peer,
850 'priority': 50,
852 'priority': 50,
851 },
853 },
852 }
854 }
853
855
854 def performhandshake(ui, url, opener, requestbuilder):
856 def performhandshake(ui, url, opener, requestbuilder):
855 # The handshake is a request to the capabilities command.
857 # The handshake is a request to the capabilities command.
856
858
857 caps = None
859 caps = None
858 def capable(x):
860 def capable(x):
859 raise error.ProgrammingError('should not be called')
861 raise error.ProgrammingError('should not be called')
860
862
861 args = {}
863 args = {}
862
864
863 # The client advertises support for newer protocols by adding an
865 # The client advertises support for newer protocols by adding an
864 # X-HgUpgrade-* header with a list of supported APIs and an
866 # X-HgUpgrade-* header with a list of supported APIs and an
865 # X-HgProto-* header advertising which serializing formats it supports.
867 # X-HgProto-* header advertising which serializing formats it supports.
866 # We only support the HTTP version 2 transport and CBOR responses for
868 # We only support the HTTP version 2 transport and CBOR responses for
867 # now.
869 # now.
868 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
870 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
869
871
870 if advertisev2:
872 if advertisev2:
871 args['headers'] = {
873 args['headers'] = {
872 r'X-HgProto-1': r'cbor',
874 r'X-HgProto-1': r'cbor',
873 }
875 }
874
876
875 args['headers'].update(
877 args['headers'].update(
876 encodevalueinheaders(' '.join(sorted(API_PEERS)),
878 encodevalueinheaders(' '.join(sorted(API_PEERS)),
877 'X-HgUpgrade',
879 'X-HgUpgrade',
878 # We don't know the header limit this early.
880 # We don't know the header limit this early.
879 # So make it small.
881 # So make it small.
880 1024))
882 1024))
881
883
882 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
884 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
883 capable, url, 'capabilities',
885 capable, url, 'capabilities',
884 args)
886 args)
885 resp = sendrequest(ui, opener, req)
887 resp = sendrequest(ui, opener, req)
886
888
887 # The server may redirect us to the repo root, stripping the
889 # The server may redirect us to the repo root, stripping the
888 # ?cmd=capabilities query string from the URL. The server would likely
890 # ?cmd=capabilities query string from the URL. The server would likely
889 # return HTML in this case and ``parsev1commandresponse()`` would raise.
891 # return HTML in this case and ``parsev1commandresponse()`` would raise.
890 # We catch this special case and re-issue the capabilities request against
892 # We catch this special case and re-issue the capabilities request against
891 # the new URL.
893 # the new URL.
892 #
894 #
893 # We should ideally not do this, as a redirect that drops the query
895 # We should ideally not do this, as a redirect that drops the query
894 # string from the URL is arguably a server bug. (Garbage in, garbage out).
896 # string from the URL is arguably a server bug. (Garbage in, garbage out).
895 # However, Mercurial clients for several years appeared to handle this
897 # However, Mercurial clients for several years appeared to handle this
896 # issue without behavior degradation. And according to issue 5860, it may
898 # issue without behavior degradation. And according to issue 5860, it may
897 # be a longstanding bug in some server implementations. So we allow a
899 # be a longstanding bug in some server implementations. So we allow a
898 # redirect that drops the query string to "just work."
900 # redirect that drops the query string to "just work."
899 try:
901 try:
900 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
902 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
901 compressible=False,
903 compressible=False,
902 allowcbor=advertisev2)
904 allowcbor=advertisev2)
903 except RedirectedRepoError as e:
905 except RedirectedRepoError as e:
904 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
906 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
905 capable, e.respurl,
907 capable, e.respurl,
906 'capabilities', args)
908 'capabilities', args)
907 resp = sendrequest(ui, opener, req)
909 resp = sendrequest(ui, opener, req)
908 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
910 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
909 compressible=False,
911 compressible=False,
910 allowcbor=advertisev2)
912 allowcbor=advertisev2)
911
913
912 try:
914 try:
913 rawdata = resp.read()
915 rawdata = resp.read()
914 finally:
916 finally:
915 resp.close()
917 resp.close()
916
918
917 if not ct.startswith('application/mercurial-'):
919 if not ct.startswith('application/mercurial-'):
918 raise error.ProgrammingError('unexpected content-type: %s' % ct)
920 raise error.ProgrammingError('unexpected content-type: %s' % ct)
919
921
920 if advertisev2:
922 if advertisev2:
921 if ct == 'application/mercurial-cbor':
923 if ct == 'application/mercurial-cbor':
922 try:
924 try:
923 info = cborutil.decodeall(rawdata)[0]
925 info = cborutil.decodeall(rawdata)[0]
924 except cborutil.CBORDecodeError:
926 except cborutil.CBORDecodeError:
925 raise error.Abort(_('error decoding CBOR from remote server'),
927 raise error.Abort(_('error decoding CBOR from remote server'),
926 hint=_('try again and consider contacting '
928 hint=_('try again and consider contacting '
927 'the server operator'))
929 'the server operator'))
928
930
929 # We got a legacy response. That's fine.
931 # We got a legacy response. That's fine.
930 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
932 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
931 info = {
933 info = {
932 'v1capabilities': set(rawdata.split())
934 'v1capabilities': set(rawdata.split())
933 }
935 }
934
936
935 else:
937 else:
936 raise error.RepoError(
938 raise error.RepoError(
937 _('unexpected response type from server: %s') % ct)
939 _('unexpected response type from server: %s') % ct)
938 else:
940 else:
939 info = {
941 info = {
940 'v1capabilities': set(rawdata.split())
942 'v1capabilities': set(rawdata.split())
941 }
943 }
942
944
943 return respurl, info
945 return respurl, info
944
946
945 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
947 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
946 """Construct an appropriate HTTP peer instance.
948 """Construct an appropriate HTTP peer instance.
947
949
948 ``opener`` is an ``url.opener`` that should be used to establish
950 ``opener`` is an ``url.opener`` that should be used to establish
949 connections, perform HTTP requests.
951 connections, perform HTTP requests.
950
952
951 ``requestbuilder`` is the type used for constructing HTTP requests.
953 ``requestbuilder`` is the type used for constructing HTTP requests.
952 It exists as an argument so extensions can override the default.
954 It exists as an argument so extensions can override the default.
953 """
955 """
954 u = util.url(path)
956 u = util.url(path)
955 if u.query or u.fragment:
957 if u.query or u.fragment:
956 raise error.Abort(_('unsupported URL component: "%s"') %
958 raise error.Abort(_('unsupported URL component: "%s"') %
957 (u.query or u.fragment))
959 (u.query or u.fragment))
958
960
959 # urllib cannot handle URLs with embedded user or passwd.
961 # urllib cannot handle URLs with embedded user or passwd.
960 url, authinfo = u.authinfo()
962 url, authinfo = u.authinfo()
961 ui.debug('using %s\n' % url)
963 ui.debug('using %s\n' % url)
962
964
963 opener = opener or urlmod.opener(ui, authinfo)
965 opener = opener or urlmod.opener(ui, authinfo)
964
966
965 respurl, info = performhandshake(ui, url, opener, requestbuilder)
967 respurl, info = performhandshake(ui, url, opener, requestbuilder)
966
968
967 # Given the intersection of APIs that both we and the server support,
969 # Given the intersection of APIs that both we and the server support,
968 # sort by their advertised priority and pick the first one.
970 # sort by their advertised priority and pick the first one.
969 #
971 #
970 # TODO consider making this request-based and interface driven. For
972 # TODO consider making this request-based and interface driven. For
971 # example, the caller could say "I want a peer that does X." It's quite
973 # example, the caller could say "I want a peer that does X." It's quite
972 # possible that not all peers would do that. Since we know the service
974 # possible that not all peers would do that. Since we know the service
973 # capabilities, we could filter out services not meeting the
975 # capabilities, we could filter out services not meeting the
974 # requirements. Possibly by consulting the interfaces defined by the
976 # requirements. Possibly by consulting the interfaces defined by the
975 # peer type.
977 # peer type.
976 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
978 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
977
979
978 preferredchoices = sorted(apipeerchoices,
980 preferredchoices = sorted(apipeerchoices,
979 key=lambda x: API_PEERS[x]['priority'],
981 key=lambda x: API_PEERS[x]['priority'],
980 reverse=True)
982 reverse=True)
981
983
982 for service in preferredchoices:
984 for service in preferredchoices:
983 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
985 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
984
986
985 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
987 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
986 requestbuilder,
988 requestbuilder,
987 info['apis'][service])
989 info['apis'][service])
988
990
989 # Failed to construct an API peer. Fall back to legacy.
991 # Failed to construct an API peer. Fall back to legacy.
990 return httppeer(ui, path, respurl, opener, requestbuilder,
992 return httppeer(ui, path, respurl, opener, requestbuilder,
991 info['v1capabilities'])
993 info['v1capabilities'])
992
994
993 def instance(ui, path, create, intents=None, createopts=None):
995 def instance(ui, path, create, intents=None, createopts=None):
994 if create:
996 if create:
995 raise error.Abort(_('cannot create new http repository'))
997 raise error.Abort(_('cannot create new http repository'))
996 try:
998 try:
997 if path.startswith('https:') and not urlmod.has_https:
999 if path.startswith('https:') and not urlmod.has_https:
998 raise error.Abort(_('Python support for SSL and HTTPS '
1000 raise error.Abort(_('Python support for SSL and HTTPS '
999 'is not installed'))
1001 'is not installed'))
1000
1002
1001 inst = makepeer(ui, path)
1003 inst = makepeer(ui, path)
1002
1004
1003 return inst
1005 return inst
1004 except error.RepoError as httpexception:
1006 except error.RepoError as httpexception:
1005 try:
1007 try:
1006 r = statichttprepo.instance(ui, "static-" + path, create)
1008 r = statichttprepo.instance(ui, "static-" + path, create)
1007 ui.note(_('(falling back to static-http)\n'))
1009 ui.note(_('(falling back to static-http)\n'))
1008 return r
1010 return r
1009 except error.RepoError:
1011 except error.RepoError:
1010 raise httpexception # use the original http RepoError instead
1012 raise httpexception # use the original http RepoError instead
@@ -1,1877 +1,1877
1 # repository.py - Interfaces and base classes for repositories and peers.
1 # repository.py - Interfaces and base classes for repositories and peers.
2 #
2 #
3 # Copyright 2017 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2017 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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from .i18n import _
10 from ..i18n import _
11 from . import (
11 from .. import (
12 error,
12 error,
13 )
13 )
14 from .utils import (
14 from ..utils import (
15 interfaceutil,
15 interfaceutil,
16 )
16 )
17
17
18 # When narrowing is finalized and no longer subject to format changes,
18 # When narrowing is finalized and no longer subject to format changes,
19 # we should move this to just "narrow" or similar.
19 # we should move this to just "narrow" or similar.
20 NARROW_REQUIREMENT = 'narrowhg-experimental'
20 NARROW_REQUIREMENT = 'narrowhg-experimental'
21
21
22 # Local repository feature string.
22 # Local repository feature string.
23
23
24 # Revlogs are being used for file storage.
24 # Revlogs are being used for file storage.
25 REPO_FEATURE_REVLOG_FILE_STORAGE = b'revlogfilestorage'
25 REPO_FEATURE_REVLOG_FILE_STORAGE = b'revlogfilestorage'
26 # The storage part of the repository is shared from an external source.
26 # The storage part of the repository is shared from an external source.
27 REPO_FEATURE_SHARED_STORAGE = b'sharedstore'
27 REPO_FEATURE_SHARED_STORAGE = b'sharedstore'
28 # LFS supported for backing file storage.
28 # LFS supported for backing file storage.
29 REPO_FEATURE_LFS = b'lfs'
29 REPO_FEATURE_LFS = b'lfs'
30 # Repository supports being stream cloned.
30 # Repository supports being stream cloned.
31 REPO_FEATURE_STREAM_CLONE = b'streamclone'
31 REPO_FEATURE_STREAM_CLONE = b'streamclone'
32 # Files storage may lack data for all ancestors.
32 # Files storage may lack data for all ancestors.
33 REPO_FEATURE_SHALLOW_FILE_STORAGE = b'shallowfilestorage'
33 REPO_FEATURE_SHALLOW_FILE_STORAGE = b'shallowfilestorage'
34
34
35 REVISION_FLAG_CENSORED = 1 << 15
35 REVISION_FLAG_CENSORED = 1 << 15
36 REVISION_FLAG_ELLIPSIS = 1 << 14
36 REVISION_FLAG_ELLIPSIS = 1 << 14
37 REVISION_FLAG_EXTSTORED = 1 << 13
37 REVISION_FLAG_EXTSTORED = 1 << 13
38
38
39 REVISION_FLAGS_KNOWN = (
39 REVISION_FLAGS_KNOWN = (
40 REVISION_FLAG_CENSORED | REVISION_FLAG_ELLIPSIS | REVISION_FLAG_EXTSTORED)
40 REVISION_FLAG_CENSORED | REVISION_FLAG_ELLIPSIS | REVISION_FLAG_EXTSTORED)
41
41
42 CG_DELTAMODE_STD = b'default'
42 CG_DELTAMODE_STD = b'default'
43 CG_DELTAMODE_PREV = b'previous'
43 CG_DELTAMODE_PREV = b'previous'
44 CG_DELTAMODE_FULL = b'fulltext'
44 CG_DELTAMODE_FULL = b'fulltext'
45 CG_DELTAMODE_P1 = b'p1'
45 CG_DELTAMODE_P1 = b'p1'
46
46
47 class ipeerconnection(interfaceutil.Interface):
47 class ipeerconnection(interfaceutil.Interface):
48 """Represents a "connection" to a repository.
48 """Represents a "connection" to a repository.
49
49
50 This is the base interface for representing a connection to a repository.
50 This is the base interface for representing a connection to a repository.
51 It holds basic properties and methods applicable to all peer types.
51 It holds basic properties and methods applicable to all peer types.
52
52
53 This is not a complete interface definition and should not be used
53 This is not a complete interface definition and should not be used
54 outside of this module.
54 outside of this module.
55 """
55 """
56 ui = interfaceutil.Attribute("""ui.ui instance""")
56 ui = interfaceutil.Attribute("""ui.ui instance""")
57
57
58 def url():
58 def url():
59 """Returns a URL string representing this peer.
59 """Returns a URL string representing this peer.
60
60
61 Currently, implementations expose the raw URL used to construct the
61 Currently, implementations expose the raw URL used to construct the
62 instance. It may contain credentials as part of the URL. The
62 instance. It may contain credentials as part of the URL. The
63 expectations of the value aren't well-defined and this could lead to
63 expectations of the value aren't well-defined and this could lead to
64 data leakage.
64 data leakage.
65
65
66 TODO audit/clean consumers and more clearly define the contents of this
66 TODO audit/clean consumers and more clearly define the contents of this
67 value.
67 value.
68 """
68 """
69
69
70 def local():
70 def local():
71 """Returns a local repository instance.
71 """Returns a local repository instance.
72
72
73 If the peer represents a local repository, returns an object that
73 If the peer represents a local repository, returns an object that
74 can be used to interface with it. Otherwise returns ``None``.
74 can be used to interface with it. Otherwise returns ``None``.
75 """
75 """
76
76
77 def peer():
77 def peer():
78 """Returns an object conforming to this interface.
78 """Returns an object conforming to this interface.
79
79
80 Most implementations will ``return self``.
80 Most implementations will ``return self``.
81 """
81 """
82
82
83 def canpush():
83 def canpush():
84 """Returns a boolean indicating if this peer can be pushed to."""
84 """Returns a boolean indicating if this peer can be pushed to."""
85
85
86 def close():
86 def close():
87 """Close the connection to this peer.
87 """Close the connection to this peer.
88
88
89 This is called when the peer will no longer be used. Resources
89 This is called when the peer will no longer be used. Resources
90 associated with the peer should be cleaned up.
90 associated with the peer should be cleaned up.
91 """
91 """
92
92
93 class ipeercapabilities(interfaceutil.Interface):
93 class ipeercapabilities(interfaceutil.Interface):
94 """Peer sub-interface related to capabilities."""
94 """Peer sub-interface related to capabilities."""
95
95
96 def capable(name):
96 def capable(name):
97 """Determine support for a named capability.
97 """Determine support for a named capability.
98
98
99 Returns ``False`` if capability not supported.
99 Returns ``False`` if capability not supported.
100
100
101 Returns ``True`` if boolean capability is supported. Returns a string
101 Returns ``True`` if boolean capability is supported. Returns a string
102 if capability support is non-boolean.
102 if capability support is non-boolean.
103
103
104 Capability strings may or may not map to wire protocol capabilities.
104 Capability strings may or may not map to wire protocol capabilities.
105 """
105 """
106
106
107 def requirecap(name, purpose):
107 def requirecap(name, purpose):
108 """Require a capability to be present.
108 """Require a capability to be present.
109
109
110 Raises a ``CapabilityError`` if the capability isn't present.
110 Raises a ``CapabilityError`` if the capability isn't present.
111 """
111 """
112
112
113 class ipeercommands(interfaceutil.Interface):
113 class ipeercommands(interfaceutil.Interface):
114 """Client-side interface for communicating over the wire protocol.
114 """Client-side interface for communicating over the wire protocol.
115
115
116 This interface is used as a gateway to the Mercurial wire protocol.
116 This interface is used as a gateway to the Mercurial wire protocol.
117 methods commonly call wire protocol commands of the same name.
117 methods commonly call wire protocol commands of the same name.
118 """
118 """
119
119
120 def branchmap():
120 def branchmap():
121 """Obtain heads in named branches.
121 """Obtain heads in named branches.
122
122
123 Returns a dict mapping branch name to an iterable of nodes that are
123 Returns a dict mapping branch name to an iterable of nodes that are
124 heads on that branch.
124 heads on that branch.
125 """
125 """
126
126
127 def capabilities():
127 def capabilities():
128 """Obtain capabilities of the peer.
128 """Obtain capabilities of the peer.
129
129
130 Returns a set of string capabilities.
130 Returns a set of string capabilities.
131 """
131 """
132
132
133 def clonebundles():
133 def clonebundles():
134 """Obtains the clone bundles manifest for the repo.
134 """Obtains the clone bundles manifest for the repo.
135
135
136 Returns the manifest as unparsed bytes.
136 Returns the manifest as unparsed bytes.
137 """
137 """
138
138
139 def debugwireargs(one, two, three=None, four=None, five=None):
139 def debugwireargs(one, two, three=None, four=None, five=None):
140 """Used to facilitate debugging of arguments passed over the wire."""
140 """Used to facilitate debugging of arguments passed over the wire."""
141
141
142 def getbundle(source, **kwargs):
142 def getbundle(source, **kwargs):
143 """Obtain remote repository data as a bundle.
143 """Obtain remote repository data as a bundle.
144
144
145 This command is how the bulk of repository data is transferred from
145 This command is how the bulk of repository data is transferred from
146 the peer to the local repository
146 the peer to the local repository
147
147
148 Returns a generator of bundle data.
148 Returns a generator of bundle data.
149 """
149 """
150
150
151 def heads():
151 def heads():
152 """Determine all known head revisions in the peer.
152 """Determine all known head revisions in the peer.
153
153
154 Returns an iterable of binary nodes.
154 Returns an iterable of binary nodes.
155 """
155 """
156
156
157 def known(nodes):
157 def known(nodes):
158 """Determine whether multiple nodes are known.
158 """Determine whether multiple nodes are known.
159
159
160 Accepts an iterable of nodes whose presence to check for.
160 Accepts an iterable of nodes whose presence to check for.
161
161
162 Returns an iterable of booleans indicating of the corresponding node
162 Returns an iterable of booleans indicating of the corresponding node
163 at that index is known to the peer.
163 at that index is known to the peer.
164 """
164 """
165
165
166 def listkeys(namespace):
166 def listkeys(namespace):
167 """Obtain all keys in a pushkey namespace.
167 """Obtain all keys in a pushkey namespace.
168
168
169 Returns an iterable of key names.
169 Returns an iterable of key names.
170 """
170 """
171
171
172 def lookup(key):
172 def lookup(key):
173 """Resolve a value to a known revision.
173 """Resolve a value to a known revision.
174
174
175 Returns a binary node of the resolved revision on success.
175 Returns a binary node of the resolved revision on success.
176 """
176 """
177
177
178 def pushkey(namespace, key, old, new):
178 def pushkey(namespace, key, old, new):
179 """Set a value using the ``pushkey`` protocol.
179 """Set a value using the ``pushkey`` protocol.
180
180
181 Arguments correspond to the pushkey namespace and key to operate on and
181 Arguments correspond to the pushkey namespace and key to operate on and
182 the old and new values for that key.
182 the old and new values for that key.
183
183
184 Returns a string with the peer result. The value inside varies by the
184 Returns a string with the peer result. The value inside varies by the
185 namespace.
185 namespace.
186 """
186 """
187
187
188 def stream_out():
188 def stream_out():
189 """Obtain streaming clone data.
189 """Obtain streaming clone data.
190
190
191 Successful result should be a generator of data chunks.
191 Successful result should be a generator of data chunks.
192 """
192 """
193
193
194 def unbundle(bundle, heads, url):
194 def unbundle(bundle, heads, url):
195 """Transfer repository data to the peer.
195 """Transfer repository data to the peer.
196
196
197 This is how the bulk of data during a push is transferred.
197 This is how the bulk of data during a push is transferred.
198
198
199 Returns the integer number of heads added to the peer.
199 Returns the integer number of heads added to the peer.
200 """
200 """
201
201
202 class ipeerlegacycommands(interfaceutil.Interface):
202 class ipeerlegacycommands(interfaceutil.Interface):
203 """Interface for implementing support for legacy wire protocol commands.
203 """Interface for implementing support for legacy wire protocol commands.
204
204
205 Wire protocol commands transition to legacy status when they are no longer
205 Wire protocol commands transition to legacy status when they are no longer
206 used by modern clients. To facilitate identifying which commands are
206 used by modern clients. To facilitate identifying which commands are
207 legacy, the interfaces are split.
207 legacy, the interfaces are split.
208 """
208 """
209
209
210 def between(pairs):
210 def between(pairs):
211 """Obtain nodes between pairs of nodes.
211 """Obtain nodes between pairs of nodes.
212
212
213 ``pairs`` is an iterable of node pairs.
213 ``pairs`` is an iterable of node pairs.
214
214
215 Returns an iterable of iterables of nodes corresponding to each
215 Returns an iterable of iterables of nodes corresponding to each
216 requested pair.
216 requested pair.
217 """
217 """
218
218
219 def branches(nodes):
219 def branches(nodes):
220 """Obtain ancestor changesets of specific nodes back to a branch point.
220 """Obtain ancestor changesets of specific nodes back to a branch point.
221
221
222 For each requested node, the peer finds the first ancestor node that is
222 For each requested node, the peer finds the first ancestor node that is
223 a DAG root or is a merge.
223 a DAG root or is a merge.
224
224
225 Returns an iterable of iterables with the resolved values for each node.
225 Returns an iterable of iterables with the resolved values for each node.
226 """
226 """
227
227
228 def changegroup(nodes, source):
228 def changegroup(nodes, source):
229 """Obtain a changegroup with data for descendants of specified nodes."""
229 """Obtain a changegroup with data for descendants of specified nodes."""
230
230
231 def changegroupsubset(bases, heads, source):
231 def changegroupsubset(bases, heads, source):
232 pass
232 pass
233
233
234 class ipeercommandexecutor(interfaceutil.Interface):
234 class ipeercommandexecutor(interfaceutil.Interface):
235 """Represents a mechanism to execute remote commands.
235 """Represents a mechanism to execute remote commands.
236
236
237 This is the primary interface for requesting that wire protocol commands
237 This is the primary interface for requesting that wire protocol commands
238 be executed. Instances of this interface are active in a context manager
238 be executed. Instances of this interface are active in a context manager
239 and have a well-defined lifetime. When the context manager exits, all
239 and have a well-defined lifetime. When the context manager exits, all
240 outstanding requests are waited on.
240 outstanding requests are waited on.
241 """
241 """
242
242
243 def callcommand(name, args):
243 def callcommand(name, args):
244 """Request that a named command be executed.
244 """Request that a named command be executed.
245
245
246 Receives the command name and a dictionary of command arguments.
246 Receives the command name and a dictionary of command arguments.
247
247
248 Returns a ``concurrent.futures.Future`` that will resolve to the
248 Returns a ``concurrent.futures.Future`` that will resolve to the
249 result of that command request. That exact value is left up to
249 result of that command request. That exact value is left up to
250 the implementation and possibly varies by command.
250 the implementation and possibly varies by command.
251
251
252 Not all commands can coexist with other commands in an executor
252 Not all commands can coexist with other commands in an executor
253 instance: it depends on the underlying wire protocol transport being
253 instance: it depends on the underlying wire protocol transport being
254 used and the command itself.
254 used and the command itself.
255
255
256 Implementations MAY call ``sendcommands()`` automatically if the
256 Implementations MAY call ``sendcommands()`` automatically if the
257 requested command can not coexist with other commands in this executor.
257 requested command can not coexist with other commands in this executor.
258
258
259 Implementations MAY call ``sendcommands()`` automatically when the
259 Implementations MAY call ``sendcommands()`` automatically when the
260 future's ``result()`` is called. So, consumers using multiple
260 future's ``result()`` is called. So, consumers using multiple
261 commands with an executor MUST ensure that ``result()`` is not called
261 commands with an executor MUST ensure that ``result()`` is not called
262 until all command requests have been issued.
262 until all command requests have been issued.
263 """
263 """
264
264
265 def sendcommands():
265 def sendcommands():
266 """Trigger submission of queued command requests.
266 """Trigger submission of queued command requests.
267
267
268 Not all transports submit commands as soon as they are requested to
268 Not all transports submit commands as soon as they are requested to
269 run. When called, this method forces queued command requests to be
269 run. When called, this method forces queued command requests to be
270 issued. It will no-op if all commands have already been sent.
270 issued. It will no-op if all commands have already been sent.
271
271
272 When called, no more new commands may be issued with this executor.
272 When called, no more new commands may be issued with this executor.
273 """
273 """
274
274
275 def close():
275 def close():
276 """Signal that this command request is finished.
276 """Signal that this command request is finished.
277
277
278 When called, no more new commands may be issued. All outstanding
278 When called, no more new commands may be issued. All outstanding
279 commands that have previously been issued are waited on before
279 commands that have previously been issued are waited on before
280 returning. This not only includes waiting for the futures to resolve,
280 returning. This not only includes waiting for the futures to resolve,
281 but also waiting for all response data to arrive. In other words,
281 but also waiting for all response data to arrive. In other words,
282 calling this waits for all on-wire state for issued command requests
282 calling this waits for all on-wire state for issued command requests
283 to finish.
283 to finish.
284
284
285 When used as a context manager, this method is called when exiting the
285 When used as a context manager, this method is called when exiting the
286 context manager.
286 context manager.
287
287
288 This method may call ``sendcommands()`` if there are buffered commands.
288 This method may call ``sendcommands()`` if there are buffered commands.
289 """
289 """
290
290
291 class ipeerrequests(interfaceutil.Interface):
291 class ipeerrequests(interfaceutil.Interface):
292 """Interface for executing commands on a peer."""
292 """Interface for executing commands on a peer."""
293
293
294 limitedarguments = interfaceutil.Attribute(
294 limitedarguments = interfaceutil.Attribute(
295 """True if the peer cannot receive large argument value for commands."""
295 """True if the peer cannot receive large argument value for commands."""
296 )
296 )
297
297
298 def commandexecutor():
298 def commandexecutor():
299 """A context manager that resolves to an ipeercommandexecutor.
299 """A context manager that resolves to an ipeercommandexecutor.
300
300
301 The object this resolves to can be used to issue command requests
301 The object this resolves to can be used to issue command requests
302 to the peer.
302 to the peer.
303
303
304 Callers should call its ``callcommand`` method to issue command
304 Callers should call its ``callcommand`` method to issue command
305 requests.
305 requests.
306
306
307 A new executor should be obtained for each distinct set of commands
307 A new executor should be obtained for each distinct set of commands
308 (possibly just a single command) that the consumer wants to execute
308 (possibly just a single command) that the consumer wants to execute
309 as part of a single operation or round trip. This is because some
309 as part of a single operation or round trip. This is because some
310 peers are half-duplex and/or don't support persistent connections.
310 peers are half-duplex and/or don't support persistent connections.
311 e.g. in the case of HTTP peers, commands sent to an executor represent
311 e.g. in the case of HTTP peers, commands sent to an executor represent
312 a single HTTP request. While some peers may support multiple command
312 a single HTTP request. While some peers may support multiple command
313 sends over the wire per executor, consumers need to code to the least
313 sends over the wire per executor, consumers need to code to the least
314 capable peer. So it should be assumed that command executors buffer
314 capable peer. So it should be assumed that command executors buffer
315 called commands until they are told to send them and that each
315 called commands until they are told to send them and that each
316 command executor could result in a new connection or wire-level request
316 command executor could result in a new connection or wire-level request
317 being issued.
317 being issued.
318 """
318 """
319
319
320 class ipeerbase(ipeerconnection, ipeercapabilities, ipeerrequests):
320 class ipeerbase(ipeerconnection, ipeercapabilities, ipeerrequests):
321 """Unified interface for peer repositories.
321 """Unified interface for peer repositories.
322
322
323 All peer instances must conform to this interface.
323 All peer instances must conform to this interface.
324 """
324 """
325
325
326 class ipeerv2(ipeerconnection, ipeercapabilities, ipeerrequests):
326 class ipeerv2(ipeerconnection, ipeercapabilities, ipeerrequests):
327 """Unified peer interface for wire protocol version 2 peers."""
327 """Unified peer interface for wire protocol version 2 peers."""
328
328
329 apidescriptor = interfaceutil.Attribute(
329 apidescriptor = interfaceutil.Attribute(
330 """Data structure holding description of server API.""")
330 """Data structure holding description of server API.""")
331
331
332 @interfaceutil.implementer(ipeerbase)
332 @interfaceutil.implementer(ipeerbase)
333 class peer(object):
333 class peer(object):
334 """Base class for peer repositories."""
334 """Base class for peer repositories."""
335
335
336 limitedarguments = False
336 limitedarguments = False
337
337
338 def capable(self, name):
338 def capable(self, name):
339 caps = self.capabilities()
339 caps = self.capabilities()
340 if name in caps:
340 if name in caps:
341 return True
341 return True
342
342
343 name = '%s=' % name
343 name = '%s=' % name
344 for cap in caps:
344 for cap in caps:
345 if cap.startswith(name):
345 if cap.startswith(name):
346 return cap[len(name):]
346 return cap[len(name):]
347
347
348 return False
348 return False
349
349
350 def requirecap(self, name, purpose):
350 def requirecap(self, name, purpose):
351 if self.capable(name):
351 if self.capable(name):
352 return
352 return
353
353
354 raise error.CapabilityError(
354 raise error.CapabilityError(
355 _('cannot %s; remote repository does not support the '
355 _('cannot %s; remote repository does not support the '
356 '\'%s\' capability') % (purpose, name))
356 '\'%s\' capability') % (purpose, name))
357
357
358 class iverifyproblem(interfaceutil.Interface):
358 class iverifyproblem(interfaceutil.Interface):
359 """Represents a problem with the integrity of the repository.
359 """Represents a problem with the integrity of the repository.
360
360
361 Instances of this interface are emitted to describe an integrity issue
361 Instances of this interface are emitted to describe an integrity issue
362 with a repository (e.g. corrupt storage, missing data, etc).
362 with a repository (e.g. corrupt storage, missing data, etc).
363
363
364 Instances are essentially messages associated with severity.
364 Instances are essentially messages associated with severity.
365 """
365 """
366 warning = interfaceutil.Attribute(
366 warning = interfaceutil.Attribute(
367 """Message indicating a non-fatal problem.""")
367 """Message indicating a non-fatal problem.""")
368
368
369 error = interfaceutil.Attribute(
369 error = interfaceutil.Attribute(
370 """Message indicating a fatal problem.""")
370 """Message indicating a fatal problem.""")
371
371
372 node = interfaceutil.Attribute(
372 node = interfaceutil.Attribute(
373 """Revision encountering the problem.
373 """Revision encountering the problem.
374
374
375 ``None`` means the problem doesn't apply to a single revision.
375 ``None`` means the problem doesn't apply to a single revision.
376 """)
376 """)
377
377
378 class irevisiondelta(interfaceutil.Interface):
378 class irevisiondelta(interfaceutil.Interface):
379 """Represents a delta between one revision and another.
379 """Represents a delta between one revision and another.
380
380
381 Instances convey enough information to allow a revision to be exchanged
381 Instances convey enough information to allow a revision to be exchanged
382 with another repository.
382 with another repository.
383
383
384 Instances represent the fulltext revision data or a delta against
384 Instances represent the fulltext revision data or a delta against
385 another revision. Therefore the ``revision`` and ``delta`` attributes
385 another revision. Therefore the ``revision`` and ``delta`` attributes
386 are mutually exclusive.
386 are mutually exclusive.
387
387
388 Typically used for changegroup generation.
388 Typically used for changegroup generation.
389 """
389 """
390
390
391 node = interfaceutil.Attribute(
391 node = interfaceutil.Attribute(
392 """20 byte node of this revision.""")
392 """20 byte node of this revision.""")
393
393
394 p1node = interfaceutil.Attribute(
394 p1node = interfaceutil.Attribute(
395 """20 byte node of 1st parent of this revision.""")
395 """20 byte node of 1st parent of this revision.""")
396
396
397 p2node = interfaceutil.Attribute(
397 p2node = interfaceutil.Attribute(
398 """20 byte node of 2nd parent of this revision.""")
398 """20 byte node of 2nd parent of this revision.""")
399
399
400 linknode = interfaceutil.Attribute(
400 linknode = interfaceutil.Attribute(
401 """20 byte node of the changelog revision this node is linked to.""")
401 """20 byte node of the changelog revision this node is linked to.""")
402
402
403 flags = interfaceutil.Attribute(
403 flags = interfaceutil.Attribute(
404 """2 bytes of integer flags that apply to this revision.
404 """2 bytes of integer flags that apply to this revision.
405
405
406 This is a bitwise composition of the ``REVISION_FLAG_*`` constants.
406 This is a bitwise composition of the ``REVISION_FLAG_*`` constants.
407 """)
407 """)
408
408
409 basenode = interfaceutil.Attribute(
409 basenode = interfaceutil.Attribute(
410 """20 byte node of the revision this data is a delta against.
410 """20 byte node of the revision this data is a delta against.
411
411
412 ``nullid`` indicates that the revision is a full revision and not
412 ``nullid`` indicates that the revision is a full revision and not
413 a delta.
413 a delta.
414 """)
414 """)
415
415
416 baserevisionsize = interfaceutil.Attribute(
416 baserevisionsize = interfaceutil.Attribute(
417 """Size of base revision this delta is against.
417 """Size of base revision this delta is against.
418
418
419 May be ``None`` if ``basenode`` is ``nullid``.
419 May be ``None`` if ``basenode`` is ``nullid``.
420 """)
420 """)
421
421
422 revision = interfaceutil.Attribute(
422 revision = interfaceutil.Attribute(
423 """Raw fulltext of revision data for this node.""")
423 """Raw fulltext of revision data for this node.""")
424
424
425 delta = interfaceutil.Attribute(
425 delta = interfaceutil.Attribute(
426 """Delta between ``basenode`` and ``node``.
426 """Delta between ``basenode`` and ``node``.
427
427
428 Stored in the bdiff delta format.
428 Stored in the bdiff delta format.
429 """)
429 """)
430
430
431 class ifilerevisionssequence(interfaceutil.Interface):
431 class ifilerevisionssequence(interfaceutil.Interface):
432 """Contains index data for all revisions of a file.
432 """Contains index data for all revisions of a file.
433
433
434 Types implementing this behave like lists of tuples. The index
434 Types implementing this behave like lists of tuples. The index
435 in the list corresponds to the revision number. The values contain
435 in the list corresponds to the revision number. The values contain
436 index metadata.
436 index metadata.
437
437
438 The *null* revision (revision number -1) is always the last item
438 The *null* revision (revision number -1) is always the last item
439 in the index.
439 in the index.
440 """
440 """
441
441
442 def __len__():
442 def __len__():
443 """The total number of revisions."""
443 """The total number of revisions."""
444
444
445 def __getitem__(rev):
445 def __getitem__(rev):
446 """Returns the object having a specific revision number.
446 """Returns the object having a specific revision number.
447
447
448 Returns an 8-tuple with the following fields:
448 Returns an 8-tuple with the following fields:
449
449
450 offset+flags
450 offset+flags
451 Contains the offset and flags for the revision. 64-bit unsigned
451 Contains the offset and flags for the revision. 64-bit unsigned
452 integer where first 6 bytes are the offset and the next 2 bytes
452 integer where first 6 bytes are the offset and the next 2 bytes
453 are flags. The offset can be 0 if it is not used by the store.
453 are flags. The offset can be 0 if it is not used by the store.
454 compressed size
454 compressed size
455 Size of the revision data in the store. It can be 0 if it isn't
455 Size of the revision data in the store. It can be 0 if it isn't
456 needed by the store.
456 needed by the store.
457 uncompressed size
457 uncompressed size
458 Fulltext size. It can be 0 if it isn't needed by the store.
458 Fulltext size. It can be 0 if it isn't needed by the store.
459 base revision
459 base revision
460 Revision number of revision the delta for storage is encoded
460 Revision number of revision the delta for storage is encoded
461 against. -1 indicates not encoded against a base revision.
461 against. -1 indicates not encoded against a base revision.
462 link revision
462 link revision
463 Revision number of changelog revision this entry is related to.
463 Revision number of changelog revision this entry is related to.
464 p1 revision
464 p1 revision
465 Revision number of 1st parent. -1 if no 1st parent.
465 Revision number of 1st parent. -1 if no 1st parent.
466 p2 revision
466 p2 revision
467 Revision number of 2nd parent. -1 if no 1st parent.
467 Revision number of 2nd parent. -1 if no 1st parent.
468 node
468 node
469 Binary node value for this revision number.
469 Binary node value for this revision number.
470
470
471 Negative values should index off the end of the sequence. ``-1``
471 Negative values should index off the end of the sequence. ``-1``
472 should return the null revision. ``-2`` should return the most
472 should return the null revision. ``-2`` should return the most
473 recent revision.
473 recent revision.
474 """
474 """
475
475
476 def __contains__(rev):
476 def __contains__(rev):
477 """Whether a revision number exists."""
477 """Whether a revision number exists."""
478
478
479 def insert(self, i, entry):
479 def insert(self, i, entry):
480 """Add an item to the index at specific revision."""
480 """Add an item to the index at specific revision."""
481
481
482 class ifileindex(interfaceutil.Interface):
482 class ifileindex(interfaceutil.Interface):
483 """Storage interface for index data of a single file.
483 """Storage interface for index data of a single file.
484
484
485 File storage data is divided into index metadata and data storage.
485 File storage data is divided into index metadata and data storage.
486 This interface defines the index portion of the interface.
486 This interface defines the index portion of the interface.
487
487
488 The index logically consists of:
488 The index logically consists of:
489
489
490 * A mapping between revision numbers and nodes.
490 * A mapping between revision numbers and nodes.
491 * DAG data (storing and querying the relationship between nodes).
491 * DAG data (storing and querying the relationship between nodes).
492 * Metadata to facilitate storage.
492 * Metadata to facilitate storage.
493 """
493 """
494 def __len__():
494 def __len__():
495 """Obtain the number of revisions stored for this file."""
495 """Obtain the number of revisions stored for this file."""
496
496
497 def __iter__():
497 def __iter__():
498 """Iterate over revision numbers for this file."""
498 """Iterate over revision numbers for this file."""
499
499
500 def hasnode(node):
500 def hasnode(node):
501 """Returns a bool indicating if a node is known to this store.
501 """Returns a bool indicating if a node is known to this store.
502
502
503 Implementations must only return True for full, binary node values:
503 Implementations must only return True for full, binary node values:
504 hex nodes, revision numbers, and partial node matches must be
504 hex nodes, revision numbers, and partial node matches must be
505 rejected.
505 rejected.
506
506
507 The null node is never present.
507 The null node is never present.
508 """
508 """
509
509
510 def revs(start=0, stop=None):
510 def revs(start=0, stop=None):
511 """Iterate over revision numbers for this file, with control."""
511 """Iterate over revision numbers for this file, with control."""
512
512
513 def parents(node):
513 def parents(node):
514 """Returns a 2-tuple of parent nodes for a revision.
514 """Returns a 2-tuple of parent nodes for a revision.
515
515
516 Values will be ``nullid`` if the parent is empty.
516 Values will be ``nullid`` if the parent is empty.
517 """
517 """
518
518
519 def parentrevs(rev):
519 def parentrevs(rev):
520 """Like parents() but operates on revision numbers."""
520 """Like parents() but operates on revision numbers."""
521
521
522 def rev(node):
522 def rev(node):
523 """Obtain the revision number given a node.
523 """Obtain the revision number given a node.
524
524
525 Raises ``error.LookupError`` if the node is not known.
525 Raises ``error.LookupError`` if the node is not known.
526 """
526 """
527
527
528 def node(rev):
528 def node(rev):
529 """Obtain the node value given a revision number.
529 """Obtain the node value given a revision number.
530
530
531 Raises ``IndexError`` if the node is not known.
531 Raises ``IndexError`` if the node is not known.
532 """
532 """
533
533
534 def lookup(node):
534 def lookup(node):
535 """Attempt to resolve a value to a node.
535 """Attempt to resolve a value to a node.
536
536
537 Value can be a binary node, hex node, revision number, or a string
537 Value can be a binary node, hex node, revision number, or a string
538 that can be converted to an integer.
538 that can be converted to an integer.
539
539
540 Raises ``error.LookupError`` if a node could not be resolved.
540 Raises ``error.LookupError`` if a node could not be resolved.
541 """
541 """
542
542
543 def linkrev(rev):
543 def linkrev(rev):
544 """Obtain the changeset revision number a revision is linked to."""
544 """Obtain the changeset revision number a revision is linked to."""
545
545
546 def iscensored(rev):
546 def iscensored(rev):
547 """Return whether a revision's content has been censored."""
547 """Return whether a revision's content has been censored."""
548
548
549 def commonancestorsheads(node1, node2):
549 def commonancestorsheads(node1, node2):
550 """Obtain an iterable of nodes containing heads of common ancestors.
550 """Obtain an iterable of nodes containing heads of common ancestors.
551
551
552 See ``ancestor.commonancestorsheads()``.
552 See ``ancestor.commonancestorsheads()``.
553 """
553 """
554
554
555 def descendants(revs):
555 def descendants(revs):
556 """Obtain descendant revision numbers for a set of revision numbers.
556 """Obtain descendant revision numbers for a set of revision numbers.
557
557
558 If ``nullrev`` is in the set, this is equivalent to ``revs()``.
558 If ``nullrev`` is in the set, this is equivalent to ``revs()``.
559 """
559 """
560
560
561 def heads(start=None, stop=None):
561 def heads(start=None, stop=None):
562 """Obtain a list of nodes that are DAG heads, with control.
562 """Obtain a list of nodes that are DAG heads, with control.
563
563
564 The set of revisions examined can be limited by specifying
564 The set of revisions examined can be limited by specifying
565 ``start`` and ``stop``. ``start`` is a node. ``stop`` is an
565 ``start`` and ``stop``. ``start`` is a node. ``stop`` is an
566 iterable of nodes. DAG traversal starts at earlier revision
566 iterable of nodes. DAG traversal starts at earlier revision
567 ``start`` and iterates forward until any node in ``stop`` is
567 ``start`` and iterates forward until any node in ``stop`` is
568 encountered.
568 encountered.
569 """
569 """
570
570
571 def children(node):
571 def children(node):
572 """Obtain nodes that are children of a node.
572 """Obtain nodes that are children of a node.
573
573
574 Returns a list of nodes.
574 Returns a list of nodes.
575 """
575 """
576
576
577 class ifiledata(interfaceutil.Interface):
577 class ifiledata(interfaceutil.Interface):
578 """Storage interface for data storage of a specific file.
578 """Storage interface for data storage of a specific file.
579
579
580 This complements ``ifileindex`` and provides an interface for accessing
580 This complements ``ifileindex`` and provides an interface for accessing
581 data for a tracked file.
581 data for a tracked file.
582 """
582 """
583 def size(rev):
583 def size(rev):
584 """Obtain the fulltext size of file data.
584 """Obtain the fulltext size of file data.
585
585
586 Any metadata is excluded from size measurements.
586 Any metadata is excluded from size measurements.
587 """
587 """
588
588
589 def revision(node, raw=False):
589 def revision(node, raw=False):
590 """"Obtain fulltext data for a node.
590 """"Obtain fulltext data for a node.
591
591
592 By default, any storage transformations are applied before the data
592 By default, any storage transformations are applied before the data
593 is returned. If ``raw`` is True, non-raw storage transformations
593 is returned. If ``raw`` is True, non-raw storage transformations
594 are not applied.
594 are not applied.
595
595
596 The fulltext data may contain a header containing metadata. Most
596 The fulltext data may contain a header containing metadata. Most
597 consumers should use ``read()`` to obtain the actual file data.
597 consumers should use ``read()`` to obtain the actual file data.
598 """
598 """
599
599
600 def rawdata(node):
600 def rawdata(node):
601 """Obtain raw data for a node.
601 """Obtain raw data for a node.
602 """
602 """
603
603
604 def read(node):
604 def read(node):
605 """Resolve file fulltext data.
605 """Resolve file fulltext data.
606
606
607 This is similar to ``revision()`` except any metadata in the data
607 This is similar to ``revision()`` except any metadata in the data
608 headers is stripped.
608 headers is stripped.
609 """
609 """
610
610
611 def renamed(node):
611 def renamed(node):
612 """Obtain copy metadata for a node.
612 """Obtain copy metadata for a node.
613
613
614 Returns ``False`` if no copy metadata is stored or a 2-tuple of
614 Returns ``False`` if no copy metadata is stored or a 2-tuple of
615 (path, node) from which this revision was copied.
615 (path, node) from which this revision was copied.
616 """
616 """
617
617
618 def cmp(node, fulltext):
618 def cmp(node, fulltext):
619 """Compare fulltext to another revision.
619 """Compare fulltext to another revision.
620
620
621 Returns True if the fulltext is different from what is stored.
621 Returns True if the fulltext is different from what is stored.
622
622
623 This takes copy metadata into account.
623 This takes copy metadata into account.
624
624
625 TODO better document the copy metadata and censoring logic.
625 TODO better document the copy metadata and censoring logic.
626 """
626 """
627
627
628 def emitrevisions(nodes,
628 def emitrevisions(nodes,
629 nodesorder=None,
629 nodesorder=None,
630 revisiondata=False,
630 revisiondata=False,
631 assumehaveparentrevisions=False,
631 assumehaveparentrevisions=False,
632 deltamode=CG_DELTAMODE_STD):
632 deltamode=CG_DELTAMODE_STD):
633 """Produce ``irevisiondelta`` for revisions.
633 """Produce ``irevisiondelta`` for revisions.
634
634
635 Given an iterable of nodes, emits objects conforming to the
635 Given an iterable of nodes, emits objects conforming to the
636 ``irevisiondelta`` interface that describe revisions in storage.
636 ``irevisiondelta`` interface that describe revisions in storage.
637
637
638 This method is a generator.
638 This method is a generator.
639
639
640 The input nodes may be unordered. Implementations must ensure that a
640 The input nodes may be unordered. Implementations must ensure that a
641 node's parents are emitted before the node itself. Transitively, this
641 node's parents are emitted before the node itself. Transitively, this
642 means that a node may only be emitted once all its ancestors in
642 means that a node may only be emitted once all its ancestors in
643 ``nodes`` have also been emitted.
643 ``nodes`` have also been emitted.
644
644
645 By default, emits "index" data (the ``node``, ``p1node``, and
645 By default, emits "index" data (the ``node``, ``p1node``, and
646 ``p2node`` attributes). If ``revisiondata`` is set, revision data
646 ``p2node`` attributes). If ``revisiondata`` is set, revision data
647 will also be present on the emitted objects.
647 will also be present on the emitted objects.
648
648
649 With default argument values, implementations can choose to emit
649 With default argument values, implementations can choose to emit
650 either fulltext revision data or a delta. When emitting deltas,
650 either fulltext revision data or a delta. When emitting deltas,
651 implementations must consider whether the delta's base revision
651 implementations must consider whether the delta's base revision
652 fulltext is available to the receiver.
652 fulltext is available to the receiver.
653
653
654 The base revision fulltext is guaranteed to be available if any of
654 The base revision fulltext is guaranteed to be available if any of
655 the following are met:
655 the following are met:
656
656
657 * Its fulltext revision was emitted by this method call.
657 * Its fulltext revision was emitted by this method call.
658 * A delta for that revision was emitted by this method call.
658 * A delta for that revision was emitted by this method call.
659 * ``assumehaveparentrevisions`` is True and the base revision is a
659 * ``assumehaveparentrevisions`` is True and the base revision is a
660 parent of the node.
660 parent of the node.
661
661
662 ``nodesorder`` can be used to control the order that revisions are
662 ``nodesorder`` can be used to control the order that revisions are
663 emitted. By default, revisions can be reordered as long as they are
663 emitted. By default, revisions can be reordered as long as they are
664 in DAG topological order (see above). If the value is ``nodes``,
664 in DAG topological order (see above). If the value is ``nodes``,
665 the iteration order from ``nodes`` should be used. If the value is
665 the iteration order from ``nodes`` should be used. If the value is
666 ``storage``, then the native order from the backing storage layer
666 ``storage``, then the native order from the backing storage layer
667 is used. (Not all storage layers will have strong ordering and behavior
667 is used. (Not all storage layers will have strong ordering and behavior
668 of this mode is storage-dependent.) ``nodes`` ordering can force
668 of this mode is storage-dependent.) ``nodes`` ordering can force
669 revisions to be emitted before their ancestors, so consumers should
669 revisions to be emitted before their ancestors, so consumers should
670 use it with care.
670 use it with care.
671
671
672 The ``linknode`` attribute on the returned ``irevisiondelta`` may not
672 The ``linknode`` attribute on the returned ``irevisiondelta`` may not
673 be set and it is the caller's responsibility to resolve it, if needed.
673 be set and it is the caller's responsibility to resolve it, if needed.
674
674
675 If ``deltamode`` is CG_DELTAMODE_PREV and revision data is requested,
675 If ``deltamode`` is CG_DELTAMODE_PREV and revision data is requested,
676 all revision data should be emitted as deltas against the revision
676 all revision data should be emitted as deltas against the revision
677 emitted just prior. The initial revision should be a delta against its
677 emitted just prior. The initial revision should be a delta against its
678 1st parent.
678 1st parent.
679 """
679 """
680
680
681 class ifilemutation(interfaceutil.Interface):
681 class ifilemutation(interfaceutil.Interface):
682 """Storage interface for mutation events of a tracked file."""
682 """Storage interface for mutation events of a tracked file."""
683
683
684 def add(filedata, meta, transaction, linkrev, p1, p2):
684 def add(filedata, meta, transaction, linkrev, p1, p2):
685 """Add a new revision to the store.
685 """Add a new revision to the store.
686
686
687 Takes file data, dictionary of metadata, a transaction, linkrev,
687 Takes file data, dictionary of metadata, a transaction, linkrev,
688 and parent nodes.
688 and parent nodes.
689
689
690 Returns the node that was added.
690 Returns the node that was added.
691
691
692 May no-op if a revision matching the supplied data is already stored.
692 May no-op if a revision matching the supplied data is already stored.
693 """
693 """
694
694
695 def addrevision(revisiondata, transaction, linkrev, p1, p2, node=None,
695 def addrevision(revisiondata, transaction, linkrev, p1, p2, node=None,
696 flags=0, cachedelta=None):
696 flags=0, cachedelta=None):
697 """Add a new revision to the store.
697 """Add a new revision to the store.
698
698
699 This is similar to ``add()`` except it operates at a lower level.
699 This is similar to ``add()`` except it operates at a lower level.
700
700
701 The data passed in already contains a metadata header, if any.
701 The data passed in already contains a metadata header, if any.
702
702
703 ``node`` and ``flags`` can be used to define the expected node and
703 ``node`` and ``flags`` can be used to define the expected node and
704 the flags to use with storage. ``flags`` is a bitwise value composed
704 the flags to use with storage. ``flags`` is a bitwise value composed
705 of the various ``REVISION_FLAG_*`` constants.
705 of the various ``REVISION_FLAG_*`` constants.
706
706
707 ``add()`` is usually called when adding files from e.g. the working
707 ``add()`` is usually called when adding files from e.g. the working
708 directory. ``addrevision()`` is often called by ``add()`` and for
708 directory. ``addrevision()`` is often called by ``add()`` and for
709 scenarios where revision data has already been computed, such as when
709 scenarios where revision data has already been computed, such as when
710 applying raw data from a peer repo.
710 applying raw data from a peer repo.
711 """
711 """
712
712
713 def addgroup(deltas, linkmapper, transaction, addrevisioncb=None,
713 def addgroup(deltas, linkmapper, transaction, addrevisioncb=None,
714 maybemissingparents=False):
714 maybemissingparents=False):
715 """Process a series of deltas for storage.
715 """Process a series of deltas for storage.
716
716
717 ``deltas`` is an iterable of 7-tuples of
717 ``deltas`` is an iterable of 7-tuples of
718 (node, p1, p2, linknode, deltabase, delta, flags) defining revisions
718 (node, p1, p2, linknode, deltabase, delta, flags) defining revisions
719 to add.
719 to add.
720
720
721 The ``delta`` field contains ``mpatch`` data to apply to a base
721 The ``delta`` field contains ``mpatch`` data to apply to a base
722 revision, identified by ``deltabase``. The base node can be
722 revision, identified by ``deltabase``. The base node can be
723 ``nullid``, in which case the header from the delta can be ignored
723 ``nullid``, in which case the header from the delta can be ignored
724 and the delta used as the fulltext.
724 and the delta used as the fulltext.
725
725
726 ``addrevisioncb`` should be called for each node as it is committed.
726 ``addrevisioncb`` should be called for each node as it is committed.
727
727
728 ``maybemissingparents`` is a bool indicating whether the incoming
728 ``maybemissingparents`` is a bool indicating whether the incoming
729 data may reference parents/ancestor revisions that aren't present.
729 data may reference parents/ancestor revisions that aren't present.
730 This flag is set when receiving data into a "shallow" store that
730 This flag is set when receiving data into a "shallow" store that
731 doesn't hold all history.
731 doesn't hold all history.
732
732
733 Returns a list of nodes that were processed. A node will be in the list
733 Returns a list of nodes that were processed. A node will be in the list
734 even if it existed in the store previously.
734 even if it existed in the store previously.
735 """
735 """
736
736
737 def censorrevision(tr, node, tombstone=b''):
737 def censorrevision(tr, node, tombstone=b''):
738 """Remove the content of a single revision.
738 """Remove the content of a single revision.
739
739
740 The specified ``node`` will have its content purged from storage.
740 The specified ``node`` will have its content purged from storage.
741 Future attempts to access the revision data for this node will
741 Future attempts to access the revision data for this node will
742 result in failure.
742 result in failure.
743
743
744 A ``tombstone`` message can optionally be stored. This message may be
744 A ``tombstone`` message can optionally be stored. This message may be
745 displayed to users when they attempt to access the missing revision
745 displayed to users when they attempt to access the missing revision
746 data.
746 data.
747
747
748 Storage backends may have stored deltas against the previous content
748 Storage backends may have stored deltas against the previous content
749 in this revision. As part of censoring a revision, these storage
749 in this revision. As part of censoring a revision, these storage
750 backends are expected to rewrite any internally stored deltas such
750 backends are expected to rewrite any internally stored deltas such
751 that they no longer reference the deleted content.
751 that they no longer reference the deleted content.
752 """
752 """
753
753
754 def getstrippoint(minlink):
754 def getstrippoint(minlink):
755 """Find the minimum revision that must be stripped to strip a linkrev.
755 """Find the minimum revision that must be stripped to strip a linkrev.
756
756
757 Returns a 2-tuple containing the minimum revision number and a set
757 Returns a 2-tuple containing the minimum revision number and a set
758 of all revisions numbers that would be broken by this strip.
758 of all revisions numbers that would be broken by this strip.
759
759
760 TODO this is highly revlog centric and should be abstracted into
760 TODO this is highly revlog centric and should be abstracted into
761 a higher-level deletion API. ``repair.strip()`` relies on this.
761 a higher-level deletion API. ``repair.strip()`` relies on this.
762 """
762 """
763
763
764 def strip(minlink, transaction):
764 def strip(minlink, transaction):
765 """Remove storage of items starting at a linkrev.
765 """Remove storage of items starting at a linkrev.
766
766
767 This uses ``getstrippoint()`` to determine the first node to remove.
767 This uses ``getstrippoint()`` to determine the first node to remove.
768 Then it effectively truncates storage for all revisions after that.
768 Then it effectively truncates storage for all revisions after that.
769
769
770 TODO this is highly revlog centric and should be abstracted into a
770 TODO this is highly revlog centric and should be abstracted into a
771 higher-level deletion API.
771 higher-level deletion API.
772 """
772 """
773
773
774 class ifilestorage(ifileindex, ifiledata, ifilemutation):
774 class ifilestorage(ifileindex, ifiledata, ifilemutation):
775 """Complete storage interface for a single tracked file."""
775 """Complete storage interface for a single tracked file."""
776
776
777 def files():
777 def files():
778 """Obtain paths that are backing storage for this file.
778 """Obtain paths that are backing storage for this file.
779
779
780 TODO this is used heavily by verify code and there should probably
780 TODO this is used heavily by verify code and there should probably
781 be a better API for that.
781 be a better API for that.
782 """
782 """
783
783
784 def storageinfo(exclusivefiles=False, sharedfiles=False,
784 def storageinfo(exclusivefiles=False, sharedfiles=False,
785 revisionscount=False, trackedsize=False,
785 revisionscount=False, trackedsize=False,
786 storedsize=False):
786 storedsize=False):
787 """Obtain information about storage for this file's data.
787 """Obtain information about storage for this file's data.
788
788
789 Returns a dict describing storage for this tracked path. The keys
789 Returns a dict describing storage for this tracked path. The keys
790 in the dict map to arguments of the same. The arguments are bools
790 in the dict map to arguments of the same. The arguments are bools
791 indicating whether to calculate and obtain that data.
791 indicating whether to calculate and obtain that data.
792
792
793 exclusivefiles
793 exclusivefiles
794 Iterable of (vfs, path) describing files that are exclusively
794 Iterable of (vfs, path) describing files that are exclusively
795 used to back storage for this tracked path.
795 used to back storage for this tracked path.
796
796
797 sharedfiles
797 sharedfiles
798 Iterable of (vfs, path) describing files that are used to back
798 Iterable of (vfs, path) describing files that are used to back
799 storage for this tracked path. Those files may also provide storage
799 storage for this tracked path. Those files may also provide storage
800 for other stored entities.
800 for other stored entities.
801
801
802 revisionscount
802 revisionscount
803 Number of revisions available for retrieval.
803 Number of revisions available for retrieval.
804
804
805 trackedsize
805 trackedsize
806 Total size in bytes of all tracked revisions. This is a sum of the
806 Total size in bytes of all tracked revisions. This is a sum of the
807 length of the fulltext of all revisions.
807 length of the fulltext of all revisions.
808
808
809 storedsize
809 storedsize
810 Total size in bytes used to store data for all tracked revisions.
810 Total size in bytes used to store data for all tracked revisions.
811 This is commonly less than ``trackedsize`` due to internal usage
811 This is commonly less than ``trackedsize`` due to internal usage
812 of deltas rather than fulltext revisions.
812 of deltas rather than fulltext revisions.
813
813
814 Not all storage backends may support all queries are have a reasonable
814 Not all storage backends may support all queries are have a reasonable
815 value to use. In that case, the value should be set to ``None`` and
815 value to use. In that case, the value should be set to ``None`` and
816 callers are expected to handle this special value.
816 callers are expected to handle this special value.
817 """
817 """
818
818
819 def verifyintegrity(state):
819 def verifyintegrity(state):
820 """Verifies the integrity of file storage.
820 """Verifies the integrity of file storage.
821
821
822 ``state`` is a dict holding state of the verifier process. It can be
822 ``state`` is a dict holding state of the verifier process. It can be
823 used to communicate data between invocations of multiple storage
823 used to communicate data between invocations of multiple storage
824 primitives.
824 primitives.
825
825
826 If individual revisions cannot have their revision content resolved,
826 If individual revisions cannot have their revision content resolved,
827 the method is expected to set the ``skipread`` key to a set of nodes
827 the method is expected to set the ``skipread`` key to a set of nodes
828 that encountered problems.
828 that encountered problems.
829
829
830 The method yields objects conforming to the ``iverifyproblem``
830 The method yields objects conforming to the ``iverifyproblem``
831 interface.
831 interface.
832 """
832 """
833
833
834 class idirs(interfaceutil.Interface):
834 class idirs(interfaceutil.Interface):
835 """Interface representing a collection of directories from paths.
835 """Interface representing a collection of directories from paths.
836
836
837 This interface is essentially a derived data structure representing
837 This interface is essentially a derived data structure representing
838 directories from a collection of paths.
838 directories from a collection of paths.
839 """
839 """
840
840
841 def addpath(path):
841 def addpath(path):
842 """Add a path to the collection.
842 """Add a path to the collection.
843
843
844 All directories in the path will be added to the collection.
844 All directories in the path will be added to the collection.
845 """
845 """
846
846
847 def delpath(path):
847 def delpath(path):
848 """Remove a path from the collection.
848 """Remove a path from the collection.
849
849
850 If the removal was the last path in a particular directory, the
850 If the removal was the last path in a particular directory, the
851 directory is removed from the collection.
851 directory is removed from the collection.
852 """
852 """
853
853
854 def __iter__():
854 def __iter__():
855 """Iterate over the directories in this collection of paths."""
855 """Iterate over the directories in this collection of paths."""
856
856
857 def __contains__(path):
857 def __contains__(path):
858 """Whether a specific directory is in this collection."""
858 """Whether a specific directory is in this collection."""
859
859
860 class imanifestdict(interfaceutil.Interface):
860 class imanifestdict(interfaceutil.Interface):
861 """Interface representing a manifest data structure.
861 """Interface representing a manifest data structure.
862
862
863 A manifest is effectively a dict mapping paths to entries. Each entry
863 A manifest is effectively a dict mapping paths to entries. Each entry
864 consists of a binary node and extra flags affecting that entry.
864 consists of a binary node and extra flags affecting that entry.
865 """
865 """
866
866
867 def __getitem__(path):
867 def __getitem__(path):
868 """Returns the binary node value for a path in the manifest.
868 """Returns the binary node value for a path in the manifest.
869
869
870 Raises ``KeyError`` if the path does not exist in the manifest.
870 Raises ``KeyError`` if the path does not exist in the manifest.
871
871
872 Equivalent to ``self.find(path)[0]``.
872 Equivalent to ``self.find(path)[0]``.
873 """
873 """
874
874
875 def find(path):
875 def find(path):
876 """Returns the entry for a path in the manifest.
876 """Returns the entry for a path in the manifest.
877
877
878 Returns a 2-tuple of (node, flags).
878 Returns a 2-tuple of (node, flags).
879
879
880 Raises ``KeyError`` if the path does not exist in the manifest.
880 Raises ``KeyError`` if the path does not exist in the manifest.
881 """
881 """
882
882
883 def __len__():
883 def __len__():
884 """Return the number of entries in the manifest."""
884 """Return the number of entries in the manifest."""
885
885
886 def __nonzero__():
886 def __nonzero__():
887 """Returns True if the manifest has entries, False otherwise."""
887 """Returns True if the manifest has entries, False otherwise."""
888
888
889 __bool__ = __nonzero__
889 __bool__ = __nonzero__
890
890
891 def __setitem__(path, node):
891 def __setitem__(path, node):
892 """Define the node value for a path in the manifest.
892 """Define the node value for a path in the manifest.
893
893
894 If the path is already in the manifest, its flags will be copied to
894 If the path is already in the manifest, its flags will be copied to
895 the new entry.
895 the new entry.
896 """
896 """
897
897
898 def __contains__(path):
898 def __contains__(path):
899 """Whether a path exists in the manifest."""
899 """Whether a path exists in the manifest."""
900
900
901 def __delitem__(path):
901 def __delitem__(path):
902 """Remove a path from the manifest.
902 """Remove a path from the manifest.
903
903
904 Raises ``KeyError`` if the path is not in the manifest.
904 Raises ``KeyError`` if the path is not in the manifest.
905 """
905 """
906
906
907 def __iter__():
907 def __iter__():
908 """Iterate over paths in the manifest."""
908 """Iterate over paths in the manifest."""
909
909
910 def iterkeys():
910 def iterkeys():
911 """Iterate over paths in the manifest."""
911 """Iterate over paths in the manifest."""
912
912
913 def keys():
913 def keys():
914 """Obtain a list of paths in the manifest."""
914 """Obtain a list of paths in the manifest."""
915
915
916 def filesnotin(other, match=None):
916 def filesnotin(other, match=None):
917 """Obtain the set of paths in this manifest but not in another.
917 """Obtain the set of paths in this manifest but not in another.
918
918
919 ``match`` is an optional matcher function to be applied to both
919 ``match`` is an optional matcher function to be applied to both
920 manifests.
920 manifests.
921
921
922 Returns a set of paths.
922 Returns a set of paths.
923 """
923 """
924
924
925 def dirs():
925 def dirs():
926 """Returns an object implementing the ``idirs`` interface."""
926 """Returns an object implementing the ``idirs`` interface."""
927
927
928 def hasdir(dir):
928 def hasdir(dir):
929 """Returns a bool indicating if a directory is in this manifest."""
929 """Returns a bool indicating if a directory is in this manifest."""
930
930
931 def matches(match):
931 def matches(match):
932 """Generate a new manifest filtered through a matcher.
932 """Generate a new manifest filtered through a matcher.
933
933
934 Returns an object conforming to the ``imanifestdict`` interface.
934 Returns an object conforming to the ``imanifestdict`` interface.
935 """
935 """
936
936
937 def walk(match):
937 def walk(match):
938 """Generator of paths in manifest satisfying a matcher.
938 """Generator of paths in manifest satisfying a matcher.
939
939
940 This is equivalent to ``self.matches(match).iterkeys()`` except a new
940 This is equivalent to ``self.matches(match).iterkeys()`` except a new
941 manifest object is not created.
941 manifest object is not created.
942
942
943 If the matcher has explicit files listed and they don't exist in
943 If the matcher has explicit files listed and they don't exist in
944 the manifest, ``match.bad()`` is called for each missing file.
944 the manifest, ``match.bad()`` is called for each missing file.
945 """
945 """
946
946
947 def diff(other, match=None, clean=False):
947 def diff(other, match=None, clean=False):
948 """Find differences between this manifest and another.
948 """Find differences between this manifest and another.
949
949
950 This manifest is compared to ``other``.
950 This manifest is compared to ``other``.
951
951
952 If ``match`` is provided, the two manifests are filtered against this
952 If ``match`` is provided, the two manifests are filtered against this
953 matcher and only entries satisfying the matcher are compared.
953 matcher and only entries satisfying the matcher are compared.
954
954
955 If ``clean`` is True, unchanged files are included in the returned
955 If ``clean`` is True, unchanged files are included in the returned
956 object.
956 object.
957
957
958 Returns a dict with paths as keys and values of 2-tuples of 2-tuples of
958 Returns a dict with paths as keys and values of 2-tuples of 2-tuples of
959 the form ``((node1, flag1), (node2, flag2))`` where ``(node1, flag1)``
959 the form ``((node1, flag1), (node2, flag2))`` where ``(node1, flag1)``
960 represents the node and flags for this manifest and ``(node2, flag2)``
960 represents the node and flags for this manifest and ``(node2, flag2)``
961 are the same for the other manifest.
961 are the same for the other manifest.
962 """
962 """
963
963
964 def setflag(path, flag):
964 def setflag(path, flag):
965 """Set the flag value for a given path.
965 """Set the flag value for a given path.
966
966
967 Raises ``KeyError`` if the path is not already in the manifest.
967 Raises ``KeyError`` if the path is not already in the manifest.
968 """
968 """
969
969
970 def get(path, default=None):
970 def get(path, default=None):
971 """Obtain the node value for a path or a default value if missing."""
971 """Obtain the node value for a path or a default value if missing."""
972
972
973 def flags(path, default=''):
973 def flags(path, default=''):
974 """Return the flags value for a path or a default value if missing."""
974 """Return the flags value for a path or a default value if missing."""
975
975
976 def copy():
976 def copy():
977 """Return a copy of this manifest."""
977 """Return a copy of this manifest."""
978
978
979 def items():
979 def items():
980 """Returns an iterable of (path, node) for items in this manifest."""
980 """Returns an iterable of (path, node) for items in this manifest."""
981
981
982 def iteritems():
982 def iteritems():
983 """Identical to items()."""
983 """Identical to items()."""
984
984
985 def iterentries():
985 def iterentries():
986 """Returns an iterable of (path, node, flags) for this manifest.
986 """Returns an iterable of (path, node, flags) for this manifest.
987
987
988 Similar to ``iteritems()`` except items are a 3-tuple and include
988 Similar to ``iteritems()`` except items are a 3-tuple and include
989 flags.
989 flags.
990 """
990 """
991
991
992 def text():
992 def text():
993 """Obtain the raw data representation for this manifest.
993 """Obtain the raw data representation for this manifest.
994
994
995 Result is used to create a manifest revision.
995 Result is used to create a manifest revision.
996 """
996 """
997
997
998 def fastdelta(base, changes):
998 def fastdelta(base, changes):
999 """Obtain a delta between this manifest and another given changes.
999 """Obtain a delta between this manifest and another given changes.
1000
1000
1001 ``base`` in the raw data representation for another manifest.
1001 ``base`` in the raw data representation for another manifest.
1002
1002
1003 ``changes`` is an iterable of ``(path, to_delete)``.
1003 ``changes`` is an iterable of ``(path, to_delete)``.
1004
1004
1005 Returns a 2-tuple containing ``bytearray(self.text())`` and the
1005 Returns a 2-tuple containing ``bytearray(self.text())`` and the
1006 delta between ``base`` and this manifest.
1006 delta between ``base`` and this manifest.
1007 """
1007 """
1008
1008
1009 class imanifestrevisionbase(interfaceutil.Interface):
1009 class imanifestrevisionbase(interfaceutil.Interface):
1010 """Base interface representing a single revision of a manifest.
1010 """Base interface representing a single revision of a manifest.
1011
1011
1012 Should not be used as a primary interface: should always be inherited
1012 Should not be used as a primary interface: should always be inherited
1013 as part of a larger interface.
1013 as part of a larger interface.
1014 """
1014 """
1015
1015
1016 def new():
1016 def new():
1017 """Obtain a new manifest instance.
1017 """Obtain a new manifest instance.
1018
1018
1019 Returns an object conforming to the ``imanifestrevisionwritable``
1019 Returns an object conforming to the ``imanifestrevisionwritable``
1020 interface. The instance will be associated with the same
1020 interface. The instance will be associated with the same
1021 ``imanifestlog`` collection as this instance.
1021 ``imanifestlog`` collection as this instance.
1022 """
1022 """
1023
1023
1024 def copy():
1024 def copy():
1025 """Obtain a copy of this manifest instance.
1025 """Obtain a copy of this manifest instance.
1026
1026
1027 Returns an object conforming to the ``imanifestrevisionwritable``
1027 Returns an object conforming to the ``imanifestrevisionwritable``
1028 interface. The instance will be associated with the same
1028 interface. The instance will be associated with the same
1029 ``imanifestlog`` collection as this instance.
1029 ``imanifestlog`` collection as this instance.
1030 """
1030 """
1031
1031
1032 def read():
1032 def read():
1033 """Obtain the parsed manifest data structure.
1033 """Obtain the parsed manifest data structure.
1034
1034
1035 The returned object conforms to the ``imanifestdict`` interface.
1035 The returned object conforms to the ``imanifestdict`` interface.
1036 """
1036 """
1037
1037
1038 class imanifestrevisionstored(imanifestrevisionbase):
1038 class imanifestrevisionstored(imanifestrevisionbase):
1039 """Interface representing a manifest revision committed to storage."""
1039 """Interface representing a manifest revision committed to storage."""
1040
1040
1041 def node():
1041 def node():
1042 """The binary node for this manifest."""
1042 """The binary node for this manifest."""
1043
1043
1044 parents = interfaceutil.Attribute(
1044 parents = interfaceutil.Attribute(
1045 """List of binary nodes that are parents for this manifest revision."""
1045 """List of binary nodes that are parents for this manifest revision."""
1046 )
1046 )
1047
1047
1048 def readdelta(shallow=False):
1048 def readdelta(shallow=False):
1049 """Obtain the manifest data structure representing changes from parent.
1049 """Obtain the manifest data structure representing changes from parent.
1050
1050
1051 This manifest is compared to its 1st parent. A new manifest representing
1051 This manifest is compared to its 1st parent. A new manifest representing
1052 those differences is constructed.
1052 those differences is constructed.
1053
1053
1054 The returned object conforms to the ``imanifestdict`` interface.
1054 The returned object conforms to the ``imanifestdict`` interface.
1055 """
1055 """
1056
1056
1057 def readfast(shallow=False):
1057 def readfast(shallow=False):
1058 """Calls either ``read()`` or ``readdelta()``.
1058 """Calls either ``read()`` or ``readdelta()``.
1059
1059
1060 The faster of the two options is called.
1060 The faster of the two options is called.
1061 """
1061 """
1062
1062
1063 def find(key):
1063 def find(key):
1064 """Calls self.read().find(key)``.
1064 """Calls self.read().find(key)``.
1065
1065
1066 Returns a 2-tuple of ``(node, flags)`` or raises ``KeyError``.
1066 Returns a 2-tuple of ``(node, flags)`` or raises ``KeyError``.
1067 """
1067 """
1068
1068
1069 class imanifestrevisionwritable(imanifestrevisionbase):
1069 class imanifestrevisionwritable(imanifestrevisionbase):
1070 """Interface representing a manifest revision that can be committed."""
1070 """Interface representing a manifest revision that can be committed."""
1071
1071
1072 def write(transaction, linkrev, p1node, p2node, added, removed, match=None):
1072 def write(transaction, linkrev, p1node, p2node, added, removed, match=None):
1073 """Add this revision to storage.
1073 """Add this revision to storage.
1074
1074
1075 Takes a transaction object, the changeset revision number it will
1075 Takes a transaction object, the changeset revision number it will
1076 be associated with, its parent nodes, and lists of added and
1076 be associated with, its parent nodes, and lists of added and
1077 removed paths.
1077 removed paths.
1078
1078
1079 If match is provided, storage can choose not to inspect or write out
1079 If match is provided, storage can choose not to inspect or write out
1080 items that do not match. Storage is still required to be able to provide
1080 items that do not match. Storage is still required to be able to provide
1081 the full manifest in the future for any directories written (these
1081 the full manifest in the future for any directories written (these
1082 manifests should not be "narrowed on disk").
1082 manifests should not be "narrowed on disk").
1083
1083
1084 Returns the binary node of the created revision.
1084 Returns the binary node of the created revision.
1085 """
1085 """
1086
1086
1087 class imanifeststorage(interfaceutil.Interface):
1087 class imanifeststorage(interfaceutil.Interface):
1088 """Storage interface for manifest data."""
1088 """Storage interface for manifest data."""
1089
1089
1090 tree = interfaceutil.Attribute(
1090 tree = interfaceutil.Attribute(
1091 """The path to the directory this manifest tracks.
1091 """The path to the directory this manifest tracks.
1092
1092
1093 The empty bytestring represents the root manifest.
1093 The empty bytestring represents the root manifest.
1094 """)
1094 """)
1095
1095
1096 index = interfaceutil.Attribute(
1096 index = interfaceutil.Attribute(
1097 """An ``ifilerevisionssequence`` instance.""")
1097 """An ``ifilerevisionssequence`` instance.""")
1098
1098
1099 indexfile = interfaceutil.Attribute(
1099 indexfile = interfaceutil.Attribute(
1100 """Path of revlog index file.
1100 """Path of revlog index file.
1101
1101
1102 TODO this is revlog specific and should not be exposed.
1102 TODO this is revlog specific and should not be exposed.
1103 """)
1103 """)
1104
1104
1105 opener = interfaceutil.Attribute(
1105 opener = interfaceutil.Attribute(
1106 """VFS opener to use to access underlying files used for storage.
1106 """VFS opener to use to access underlying files used for storage.
1107
1107
1108 TODO this is revlog specific and should not be exposed.
1108 TODO this is revlog specific and should not be exposed.
1109 """)
1109 """)
1110
1110
1111 version = interfaceutil.Attribute(
1111 version = interfaceutil.Attribute(
1112 """Revlog version number.
1112 """Revlog version number.
1113
1113
1114 TODO this is revlog specific and should not be exposed.
1114 TODO this is revlog specific and should not be exposed.
1115 """)
1115 """)
1116
1116
1117 _generaldelta = interfaceutil.Attribute(
1117 _generaldelta = interfaceutil.Attribute(
1118 """Whether generaldelta storage is being used.
1118 """Whether generaldelta storage is being used.
1119
1119
1120 TODO this is revlog specific and should not be exposed.
1120 TODO this is revlog specific and should not be exposed.
1121 """)
1121 """)
1122
1122
1123 fulltextcache = interfaceutil.Attribute(
1123 fulltextcache = interfaceutil.Attribute(
1124 """Dict with cache of fulltexts.
1124 """Dict with cache of fulltexts.
1125
1125
1126 TODO this doesn't feel appropriate for the storage interface.
1126 TODO this doesn't feel appropriate for the storage interface.
1127 """)
1127 """)
1128
1128
1129 def __len__():
1129 def __len__():
1130 """Obtain the number of revisions stored for this manifest."""
1130 """Obtain the number of revisions stored for this manifest."""
1131
1131
1132 def __iter__():
1132 def __iter__():
1133 """Iterate over revision numbers for this manifest."""
1133 """Iterate over revision numbers for this manifest."""
1134
1134
1135 def rev(node):
1135 def rev(node):
1136 """Obtain the revision number given a binary node.
1136 """Obtain the revision number given a binary node.
1137
1137
1138 Raises ``error.LookupError`` if the node is not known.
1138 Raises ``error.LookupError`` if the node is not known.
1139 """
1139 """
1140
1140
1141 def node(rev):
1141 def node(rev):
1142 """Obtain the node value given a revision number.
1142 """Obtain the node value given a revision number.
1143
1143
1144 Raises ``error.LookupError`` if the revision is not known.
1144 Raises ``error.LookupError`` if the revision is not known.
1145 """
1145 """
1146
1146
1147 def lookup(value):
1147 def lookup(value):
1148 """Attempt to resolve a value to a node.
1148 """Attempt to resolve a value to a node.
1149
1149
1150 Value can be a binary node, hex node, revision number, or a bytes
1150 Value can be a binary node, hex node, revision number, or a bytes
1151 that can be converted to an integer.
1151 that can be converted to an integer.
1152
1152
1153 Raises ``error.LookupError`` if a ndoe could not be resolved.
1153 Raises ``error.LookupError`` if a ndoe could not be resolved.
1154 """
1154 """
1155
1155
1156 def parents(node):
1156 def parents(node):
1157 """Returns a 2-tuple of parent nodes for a node.
1157 """Returns a 2-tuple of parent nodes for a node.
1158
1158
1159 Values will be ``nullid`` if the parent is empty.
1159 Values will be ``nullid`` if the parent is empty.
1160 """
1160 """
1161
1161
1162 def parentrevs(rev):
1162 def parentrevs(rev):
1163 """Like parents() but operates on revision numbers."""
1163 """Like parents() but operates on revision numbers."""
1164
1164
1165 def linkrev(rev):
1165 def linkrev(rev):
1166 """Obtain the changeset revision number a revision is linked to."""
1166 """Obtain the changeset revision number a revision is linked to."""
1167
1167
1168 def revision(node, _df=None, raw=False):
1168 def revision(node, _df=None, raw=False):
1169 """Obtain fulltext data for a node."""
1169 """Obtain fulltext data for a node."""
1170
1170
1171 def rawdata(node, _df=None):
1171 def rawdata(node, _df=None):
1172 """Obtain raw data for a node."""
1172 """Obtain raw data for a node."""
1173
1173
1174 def revdiff(rev1, rev2):
1174 def revdiff(rev1, rev2):
1175 """Obtain a delta between two revision numbers.
1175 """Obtain a delta between two revision numbers.
1176
1176
1177 The returned data is the result of ``bdiff.bdiff()`` on the raw
1177 The returned data is the result of ``bdiff.bdiff()`` on the raw
1178 revision data.
1178 revision data.
1179 """
1179 """
1180
1180
1181 def cmp(node, fulltext):
1181 def cmp(node, fulltext):
1182 """Compare fulltext to another revision.
1182 """Compare fulltext to another revision.
1183
1183
1184 Returns True if the fulltext is different from what is stored.
1184 Returns True if the fulltext is different from what is stored.
1185 """
1185 """
1186
1186
1187 def emitrevisions(nodes,
1187 def emitrevisions(nodes,
1188 nodesorder=None,
1188 nodesorder=None,
1189 revisiondata=False,
1189 revisiondata=False,
1190 assumehaveparentrevisions=False):
1190 assumehaveparentrevisions=False):
1191 """Produce ``irevisiondelta`` describing revisions.
1191 """Produce ``irevisiondelta`` describing revisions.
1192
1192
1193 See the documentation for ``ifiledata`` for more.
1193 See the documentation for ``ifiledata`` for more.
1194 """
1194 """
1195
1195
1196 def addgroup(deltas, linkmapper, transaction, addrevisioncb=None):
1196 def addgroup(deltas, linkmapper, transaction, addrevisioncb=None):
1197 """Process a series of deltas for storage.
1197 """Process a series of deltas for storage.
1198
1198
1199 See the documentation in ``ifilemutation`` for more.
1199 See the documentation in ``ifilemutation`` for more.
1200 """
1200 """
1201
1201
1202 def rawsize(rev):
1202 def rawsize(rev):
1203 """Obtain the size of tracked data.
1203 """Obtain the size of tracked data.
1204
1204
1205 Is equivalent to ``len(m.rawdata(node))``.
1205 Is equivalent to ``len(m.rawdata(node))``.
1206
1206
1207 TODO this method is only used by upgrade code and may be removed.
1207 TODO this method is only used by upgrade code and may be removed.
1208 """
1208 """
1209
1209
1210 def getstrippoint(minlink):
1210 def getstrippoint(minlink):
1211 """Find minimum revision that must be stripped to strip a linkrev.
1211 """Find minimum revision that must be stripped to strip a linkrev.
1212
1212
1213 See the documentation in ``ifilemutation`` for more.
1213 See the documentation in ``ifilemutation`` for more.
1214 """
1214 """
1215
1215
1216 def strip(minlink, transaction):
1216 def strip(minlink, transaction):
1217 """Remove storage of items starting at a linkrev.
1217 """Remove storage of items starting at a linkrev.
1218
1218
1219 See the documentation in ``ifilemutation`` for more.
1219 See the documentation in ``ifilemutation`` for more.
1220 """
1220 """
1221
1221
1222 def checksize():
1222 def checksize():
1223 """Obtain the expected sizes of backing files.
1223 """Obtain the expected sizes of backing files.
1224
1224
1225 TODO this is used by verify and it should not be part of the interface.
1225 TODO this is used by verify and it should not be part of the interface.
1226 """
1226 """
1227
1227
1228 def files():
1228 def files():
1229 """Obtain paths that are backing storage for this manifest.
1229 """Obtain paths that are backing storage for this manifest.
1230
1230
1231 TODO this is used by verify and there should probably be a better API
1231 TODO this is used by verify and there should probably be a better API
1232 for this functionality.
1232 for this functionality.
1233 """
1233 """
1234
1234
1235 def deltaparent(rev):
1235 def deltaparent(rev):
1236 """Obtain the revision that a revision is delta'd against.
1236 """Obtain the revision that a revision is delta'd against.
1237
1237
1238 TODO delta encoding is an implementation detail of storage and should
1238 TODO delta encoding is an implementation detail of storage and should
1239 not be exposed to the storage interface.
1239 not be exposed to the storage interface.
1240 """
1240 """
1241
1241
1242 def clone(tr, dest, **kwargs):
1242 def clone(tr, dest, **kwargs):
1243 """Clone this instance to another."""
1243 """Clone this instance to another."""
1244
1244
1245 def clearcaches(clear_persisted_data=False):
1245 def clearcaches(clear_persisted_data=False):
1246 """Clear any caches associated with this instance."""
1246 """Clear any caches associated with this instance."""
1247
1247
1248 def dirlog(d):
1248 def dirlog(d):
1249 """Obtain a manifest storage instance for a tree."""
1249 """Obtain a manifest storage instance for a tree."""
1250
1250
1251 def add(m, transaction, link, p1, p2, added, removed, readtree=None,
1251 def add(m, transaction, link, p1, p2, added, removed, readtree=None,
1252 match=None):
1252 match=None):
1253 """Add a revision to storage.
1253 """Add a revision to storage.
1254
1254
1255 ``m`` is an object conforming to ``imanifestdict``.
1255 ``m`` is an object conforming to ``imanifestdict``.
1256
1256
1257 ``link`` is the linkrev revision number.
1257 ``link`` is the linkrev revision number.
1258
1258
1259 ``p1`` and ``p2`` are the parent revision numbers.
1259 ``p1`` and ``p2`` are the parent revision numbers.
1260
1260
1261 ``added`` and ``removed`` are iterables of added and removed paths,
1261 ``added`` and ``removed`` are iterables of added and removed paths,
1262 respectively.
1262 respectively.
1263
1263
1264 ``readtree`` is a function that can be used to read the child tree(s)
1264 ``readtree`` is a function that can be used to read the child tree(s)
1265 when recursively writing the full tree structure when using
1265 when recursively writing the full tree structure when using
1266 treemanifets.
1266 treemanifets.
1267
1267
1268 ``match`` is a matcher that can be used to hint to storage that not all
1268 ``match`` is a matcher that can be used to hint to storage that not all
1269 paths must be inspected; this is an optimization and can be safely
1269 paths must be inspected; this is an optimization and can be safely
1270 ignored. Note that the storage must still be able to reproduce a full
1270 ignored. Note that the storage must still be able to reproduce a full
1271 manifest including files that did not match.
1271 manifest including files that did not match.
1272 """
1272 """
1273
1273
1274 def storageinfo(exclusivefiles=False, sharedfiles=False,
1274 def storageinfo(exclusivefiles=False, sharedfiles=False,
1275 revisionscount=False, trackedsize=False,
1275 revisionscount=False, trackedsize=False,
1276 storedsize=False):
1276 storedsize=False):
1277 """Obtain information about storage for this manifest's data.
1277 """Obtain information about storage for this manifest's data.
1278
1278
1279 See ``ifilestorage.storageinfo()`` for a description of this method.
1279 See ``ifilestorage.storageinfo()`` for a description of this method.
1280 This one behaves the same way, except for manifest data.
1280 This one behaves the same way, except for manifest data.
1281 """
1281 """
1282
1282
1283 class imanifestlog(interfaceutil.Interface):
1283 class imanifestlog(interfaceutil.Interface):
1284 """Interface representing a collection of manifest snapshots.
1284 """Interface representing a collection of manifest snapshots.
1285
1285
1286 Represents the root manifest in a repository.
1286 Represents the root manifest in a repository.
1287
1287
1288 Also serves as a means to access nested tree manifests and to cache
1288 Also serves as a means to access nested tree manifests and to cache
1289 tree manifests.
1289 tree manifests.
1290 """
1290 """
1291
1291
1292 def __getitem__(node):
1292 def __getitem__(node):
1293 """Obtain a manifest instance for a given binary node.
1293 """Obtain a manifest instance for a given binary node.
1294
1294
1295 Equivalent to calling ``self.get('', node)``.
1295 Equivalent to calling ``self.get('', node)``.
1296
1296
1297 The returned object conforms to the ``imanifestrevisionstored``
1297 The returned object conforms to the ``imanifestrevisionstored``
1298 interface.
1298 interface.
1299 """
1299 """
1300
1300
1301 def get(tree, node, verify=True):
1301 def get(tree, node, verify=True):
1302 """Retrieve the manifest instance for a given directory and binary node.
1302 """Retrieve the manifest instance for a given directory and binary node.
1303
1303
1304 ``node`` always refers to the node of the root manifest (which will be
1304 ``node`` always refers to the node of the root manifest (which will be
1305 the only manifest if flat manifests are being used).
1305 the only manifest if flat manifests are being used).
1306
1306
1307 If ``tree`` is the empty string, the root manifest is returned.
1307 If ``tree`` is the empty string, the root manifest is returned.
1308 Otherwise the manifest for the specified directory will be returned
1308 Otherwise the manifest for the specified directory will be returned
1309 (requires tree manifests).
1309 (requires tree manifests).
1310
1310
1311 If ``verify`` is True, ``LookupError`` is raised if the node is not
1311 If ``verify`` is True, ``LookupError`` is raised if the node is not
1312 known.
1312 known.
1313
1313
1314 The returned object conforms to the ``imanifestrevisionstored``
1314 The returned object conforms to the ``imanifestrevisionstored``
1315 interface.
1315 interface.
1316 """
1316 """
1317
1317
1318 def getstorage(tree):
1318 def getstorage(tree):
1319 """Retrieve an interface to storage for a particular tree.
1319 """Retrieve an interface to storage for a particular tree.
1320
1320
1321 If ``tree`` is the empty bytestring, storage for the root manifest will
1321 If ``tree`` is the empty bytestring, storage for the root manifest will
1322 be returned. Otherwise storage for a tree manifest is returned.
1322 be returned. Otherwise storage for a tree manifest is returned.
1323
1323
1324 TODO formalize interface for returned object.
1324 TODO formalize interface for returned object.
1325 """
1325 """
1326
1326
1327 def clearcaches():
1327 def clearcaches():
1328 """Clear caches associated with this collection."""
1328 """Clear caches associated with this collection."""
1329
1329
1330 def rev(node):
1330 def rev(node):
1331 """Obtain the revision number for a binary node.
1331 """Obtain the revision number for a binary node.
1332
1332
1333 Raises ``error.LookupError`` if the node is not known.
1333 Raises ``error.LookupError`` if the node is not known.
1334 """
1334 """
1335
1335
1336 class ilocalrepositoryfilestorage(interfaceutil.Interface):
1336 class ilocalrepositoryfilestorage(interfaceutil.Interface):
1337 """Local repository sub-interface providing access to tracked file storage.
1337 """Local repository sub-interface providing access to tracked file storage.
1338
1338
1339 This interface defines how a repository accesses storage for a single
1339 This interface defines how a repository accesses storage for a single
1340 tracked file path.
1340 tracked file path.
1341 """
1341 """
1342
1342
1343 def file(f):
1343 def file(f):
1344 """Obtain a filelog for a tracked path.
1344 """Obtain a filelog for a tracked path.
1345
1345
1346 The returned type conforms to the ``ifilestorage`` interface.
1346 The returned type conforms to the ``ifilestorage`` interface.
1347 """
1347 """
1348
1348
1349 class ilocalrepositorymain(interfaceutil.Interface):
1349 class ilocalrepositorymain(interfaceutil.Interface):
1350 """Main interface for local repositories.
1350 """Main interface for local repositories.
1351
1351
1352 This currently captures the reality of things - not how things should be.
1352 This currently captures the reality of things - not how things should be.
1353 """
1353 """
1354
1354
1355 supportedformats = interfaceutil.Attribute(
1355 supportedformats = interfaceutil.Attribute(
1356 """Set of requirements that apply to stream clone.
1356 """Set of requirements that apply to stream clone.
1357
1357
1358 This is actually a class attribute and is shared among all instances.
1358 This is actually a class attribute and is shared among all instances.
1359 """)
1359 """)
1360
1360
1361 supported = interfaceutil.Attribute(
1361 supported = interfaceutil.Attribute(
1362 """Set of requirements that this repo is capable of opening.""")
1362 """Set of requirements that this repo is capable of opening.""")
1363
1363
1364 requirements = interfaceutil.Attribute(
1364 requirements = interfaceutil.Attribute(
1365 """Set of requirements this repo uses.""")
1365 """Set of requirements this repo uses.""")
1366
1366
1367 features = interfaceutil.Attribute(
1367 features = interfaceutil.Attribute(
1368 """Set of "features" this repository supports.
1368 """Set of "features" this repository supports.
1369
1369
1370 A "feature" is a loosely-defined term. It can refer to a feature
1370 A "feature" is a loosely-defined term. It can refer to a feature
1371 in the classical sense or can describe an implementation detail
1371 in the classical sense or can describe an implementation detail
1372 of the repository. For example, a ``readonly`` feature may denote
1372 of the repository. For example, a ``readonly`` feature may denote
1373 the repository as read-only. Or a ``revlogfilestore`` feature may
1373 the repository as read-only. Or a ``revlogfilestore`` feature may
1374 denote that the repository is using revlogs for file storage.
1374 denote that the repository is using revlogs for file storage.
1375
1375
1376 The intent of features is to provide a machine-queryable mechanism
1376 The intent of features is to provide a machine-queryable mechanism
1377 for repo consumers to test for various repository characteristics.
1377 for repo consumers to test for various repository characteristics.
1378
1378
1379 Features are similar to ``requirements``. The main difference is that
1379 Features are similar to ``requirements``. The main difference is that
1380 requirements are stored on-disk and represent requirements to open the
1380 requirements are stored on-disk and represent requirements to open the
1381 repository. Features are more run-time capabilities of the repository
1381 repository. Features are more run-time capabilities of the repository
1382 and more granular capabilities (which may be derived from requirements).
1382 and more granular capabilities (which may be derived from requirements).
1383 """)
1383 """)
1384
1384
1385 filtername = interfaceutil.Attribute(
1385 filtername = interfaceutil.Attribute(
1386 """Name of the repoview that is active on this repo.""")
1386 """Name of the repoview that is active on this repo.""")
1387
1387
1388 wvfs = interfaceutil.Attribute(
1388 wvfs = interfaceutil.Attribute(
1389 """VFS used to access the working directory.""")
1389 """VFS used to access the working directory.""")
1390
1390
1391 vfs = interfaceutil.Attribute(
1391 vfs = interfaceutil.Attribute(
1392 """VFS rooted at the .hg directory.
1392 """VFS rooted at the .hg directory.
1393
1393
1394 Used to access repository data not in the store.
1394 Used to access repository data not in the store.
1395 """)
1395 """)
1396
1396
1397 svfs = interfaceutil.Attribute(
1397 svfs = interfaceutil.Attribute(
1398 """VFS rooted at the store.
1398 """VFS rooted at the store.
1399
1399
1400 Used to access repository data in the store. Typically .hg/store.
1400 Used to access repository data in the store. Typically .hg/store.
1401 But can point elsewhere if the store is shared.
1401 But can point elsewhere if the store is shared.
1402 """)
1402 """)
1403
1403
1404 root = interfaceutil.Attribute(
1404 root = interfaceutil.Attribute(
1405 """Path to the root of the working directory.""")
1405 """Path to the root of the working directory.""")
1406
1406
1407 path = interfaceutil.Attribute(
1407 path = interfaceutil.Attribute(
1408 """Path to the .hg directory.""")
1408 """Path to the .hg directory.""")
1409
1409
1410 origroot = interfaceutil.Attribute(
1410 origroot = interfaceutil.Attribute(
1411 """The filesystem path that was used to construct the repo.""")
1411 """The filesystem path that was used to construct the repo.""")
1412
1412
1413 auditor = interfaceutil.Attribute(
1413 auditor = interfaceutil.Attribute(
1414 """A pathauditor for the working directory.
1414 """A pathauditor for the working directory.
1415
1415
1416 This checks if a path refers to a nested repository.
1416 This checks if a path refers to a nested repository.
1417
1417
1418 Operates on the filesystem.
1418 Operates on the filesystem.
1419 """)
1419 """)
1420
1420
1421 nofsauditor = interfaceutil.Attribute(
1421 nofsauditor = interfaceutil.Attribute(
1422 """A pathauditor for the working directory.
1422 """A pathauditor for the working directory.
1423
1423
1424 This is like ``auditor`` except it doesn't do filesystem checks.
1424 This is like ``auditor`` except it doesn't do filesystem checks.
1425 """)
1425 """)
1426
1426
1427 baseui = interfaceutil.Attribute(
1427 baseui = interfaceutil.Attribute(
1428 """Original ui instance passed into constructor.""")
1428 """Original ui instance passed into constructor.""")
1429
1429
1430 ui = interfaceutil.Attribute(
1430 ui = interfaceutil.Attribute(
1431 """Main ui instance for this instance.""")
1431 """Main ui instance for this instance.""")
1432
1432
1433 sharedpath = interfaceutil.Attribute(
1433 sharedpath = interfaceutil.Attribute(
1434 """Path to the .hg directory of the repo this repo was shared from.""")
1434 """Path to the .hg directory of the repo this repo was shared from.""")
1435
1435
1436 store = interfaceutil.Attribute(
1436 store = interfaceutil.Attribute(
1437 """A store instance.""")
1437 """A store instance.""")
1438
1438
1439 spath = interfaceutil.Attribute(
1439 spath = interfaceutil.Attribute(
1440 """Path to the store.""")
1440 """Path to the store.""")
1441
1441
1442 sjoin = interfaceutil.Attribute(
1442 sjoin = interfaceutil.Attribute(
1443 """Alias to self.store.join.""")
1443 """Alias to self.store.join.""")
1444
1444
1445 cachevfs = interfaceutil.Attribute(
1445 cachevfs = interfaceutil.Attribute(
1446 """A VFS used to access the cache directory.
1446 """A VFS used to access the cache directory.
1447
1447
1448 Typically .hg/cache.
1448 Typically .hg/cache.
1449 """)
1449 """)
1450
1450
1451 wcachevfs = interfaceutil.Attribute(
1451 wcachevfs = interfaceutil.Attribute(
1452 """A VFS used to access the cache directory dedicated to working copy
1452 """A VFS used to access the cache directory dedicated to working copy
1453
1453
1454 Typically .hg/wcache.
1454 Typically .hg/wcache.
1455 """)
1455 """)
1456
1456
1457 filteredrevcache = interfaceutil.Attribute(
1457 filteredrevcache = interfaceutil.Attribute(
1458 """Holds sets of revisions to be filtered.""")
1458 """Holds sets of revisions to be filtered.""")
1459
1459
1460 names = interfaceutil.Attribute(
1460 names = interfaceutil.Attribute(
1461 """A ``namespaces`` instance.""")
1461 """A ``namespaces`` instance.""")
1462
1462
1463 def close():
1463 def close():
1464 """Close the handle on this repository."""
1464 """Close the handle on this repository."""
1465
1465
1466 def peer():
1466 def peer():
1467 """Obtain an object conforming to the ``peer`` interface."""
1467 """Obtain an object conforming to the ``peer`` interface."""
1468
1468
1469 def unfiltered():
1469 def unfiltered():
1470 """Obtain an unfiltered/raw view of this repo."""
1470 """Obtain an unfiltered/raw view of this repo."""
1471
1471
1472 def filtered(name, visibilityexceptions=None):
1472 def filtered(name, visibilityexceptions=None):
1473 """Obtain a named view of this repository."""
1473 """Obtain a named view of this repository."""
1474
1474
1475 obsstore = interfaceutil.Attribute(
1475 obsstore = interfaceutil.Attribute(
1476 """A store of obsolescence data.""")
1476 """A store of obsolescence data.""")
1477
1477
1478 changelog = interfaceutil.Attribute(
1478 changelog = interfaceutil.Attribute(
1479 """A handle on the changelog revlog.""")
1479 """A handle on the changelog revlog.""")
1480
1480
1481 manifestlog = interfaceutil.Attribute(
1481 manifestlog = interfaceutil.Attribute(
1482 """An instance conforming to the ``imanifestlog`` interface.
1482 """An instance conforming to the ``imanifestlog`` interface.
1483
1483
1484 Provides access to manifests for the repository.
1484 Provides access to manifests for the repository.
1485 """)
1485 """)
1486
1486
1487 dirstate = interfaceutil.Attribute(
1487 dirstate = interfaceutil.Attribute(
1488 """Working directory state.""")
1488 """Working directory state.""")
1489
1489
1490 narrowpats = interfaceutil.Attribute(
1490 narrowpats = interfaceutil.Attribute(
1491 """Matcher patterns for this repository's narrowspec.""")
1491 """Matcher patterns for this repository's narrowspec.""")
1492
1492
1493 def narrowmatch(match=None, includeexact=False):
1493 def narrowmatch(match=None, includeexact=False):
1494 """Obtain a matcher for the narrowspec."""
1494 """Obtain a matcher for the narrowspec."""
1495
1495
1496 def setnarrowpats(newincludes, newexcludes):
1496 def setnarrowpats(newincludes, newexcludes):
1497 """Define the narrowspec for this repository."""
1497 """Define the narrowspec for this repository."""
1498
1498
1499 def __getitem__(changeid):
1499 def __getitem__(changeid):
1500 """Try to resolve a changectx."""
1500 """Try to resolve a changectx."""
1501
1501
1502 def __contains__(changeid):
1502 def __contains__(changeid):
1503 """Whether a changeset exists."""
1503 """Whether a changeset exists."""
1504
1504
1505 def __nonzero__():
1505 def __nonzero__():
1506 """Always returns True."""
1506 """Always returns True."""
1507 return True
1507 return True
1508
1508
1509 __bool__ = __nonzero__
1509 __bool__ = __nonzero__
1510
1510
1511 def __len__():
1511 def __len__():
1512 """Returns the number of changesets in the repo."""
1512 """Returns the number of changesets in the repo."""
1513
1513
1514 def __iter__():
1514 def __iter__():
1515 """Iterate over revisions in the changelog."""
1515 """Iterate over revisions in the changelog."""
1516
1516
1517 def revs(expr, *args):
1517 def revs(expr, *args):
1518 """Evaluate a revset.
1518 """Evaluate a revset.
1519
1519
1520 Emits revisions.
1520 Emits revisions.
1521 """
1521 """
1522
1522
1523 def set(expr, *args):
1523 def set(expr, *args):
1524 """Evaluate a revset.
1524 """Evaluate a revset.
1525
1525
1526 Emits changectx instances.
1526 Emits changectx instances.
1527 """
1527 """
1528
1528
1529 def anyrevs(specs, user=False, localalias=None):
1529 def anyrevs(specs, user=False, localalias=None):
1530 """Find revisions matching one of the given revsets."""
1530 """Find revisions matching one of the given revsets."""
1531
1531
1532 def url():
1532 def url():
1533 """Returns a string representing the location of this repo."""
1533 """Returns a string representing the location of this repo."""
1534
1534
1535 def hook(name, throw=False, **args):
1535 def hook(name, throw=False, **args):
1536 """Call a hook."""
1536 """Call a hook."""
1537
1537
1538 def tags():
1538 def tags():
1539 """Return a mapping of tag to node."""
1539 """Return a mapping of tag to node."""
1540
1540
1541 def tagtype(tagname):
1541 def tagtype(tagname):
1542 """Return the type of a given tag."""
1542 """Return the type of a given tag."""
1543
1543
1544 def tagslist():
1544 def tagslist():
1545 """Return a list of tags ordered by revision."""
1545 """Return a list of tags ordered by revision."""
1546
1546
1547 def nodetags(node):
1547 def nodetags(node):
1548 """Return the tags associated with a node."""
1548 """Return the tags associated with a node."""
1549
1549
1550 def nodebookmarks(node):
1550 def nodebookmarks(node):
1551 """Return the list of bookmarks pointing to the specified node."""
1551 """Return the list of bookmarks pointing to the specified node."""
1552
1552
1553 def branchmap():
1553 def branchmap():
1554 """Return a mapping of branch to heads in that branch."""
1554 """Return a mapping of branch to heads in that branch."""
1555
1555
1556 def revbranchcache():
1556 def revbranchcache():
1557 pass
1557 pass
1558
1558
1559 def branchtip(branchtip, ignoremissing=False):
1559 def branchtip(branchtip, ignoremissing=False):
1560 """Return the tip node for a given branch."""
1560 """Return the tip node for a given branch."""
1561
1561
1562 def lookup(key):
1562 def lookup(key):
1563 """Resolve the node for a revision."""
1563 """Resolve the node for a revision."""
1564
1564
1565 def lookupbranch(key):
1565 def lookupbranch(key):
1566 """Look up the branch name of the given revision or branch name."""
1566 """Look up the branch name of the given revision or branch name."""
1567
1567
1568 def known(nodes):
1568 def known(nodes):
1569 """Determine whether a series of nodes is known.
1569 """Determine whether a series of nodes is known.
1570
1570
1571 Returns a list of bools.
1571 Returns a list of bools.
1572 """
1572 """
1573
1573
1574 def local():
1574 def local():
1575 """Whether the repository is local."""
1575 """Whether the repository is local."""
1576 return True
1576 return True
1577
1577
1578 def publishing():
1578 def publishing():
1579 """Whether the repository is a publishing repository."""
1579 """Whether the repository is a publishing repository."""
1580
1580
1581 def cancopy():
1581 def cancopy():
1582 pass
1582 pass
1583
1583
1584 def shared():
1584 def shared():
1585 """The type of shared repository or None."""
1585 """The type of shared repository or None."""
1586
1586
1587 def wjoin(f, *insidef):
1587 def wjoin(f, *insidef):
1588 """Calls self.vfs.reljoin(self.root, f, *insidef)"""
1588 """Calls self.vfs.reljoin(self.root, f, *insidef)"""
1589
1589
1590 def setparents(p1, p2):
1590 def setparents(p1, p2):
1591 """Set the parent nodes of the working directory."""
1591 """Set the parent nodes of the working directory."""
1592
1592
1593 def filectx(path, changeid=None, fileid=None):
1593 def filectx(path, changeid=None, fileid=None):
1594 """Obtain a filectx for the given file revision."""
1594 """Obtain a filectx for the given file revision."""
1595
1595
1596 def getcwd():
1596 def getcwd():
1597 """Obtain the current working directory from the dirstate."""
1597 """Obtain the current working directory from the dirstate."""
1598
1598
1599 def pathto(f, cwd=None):
1599 def pathto(f, cwd=None):
1600 """Obtain the relative path to a file."""
1600 """Obtain the relative path to a file."""
1601
1601
1602 def adddatafilter(name, fltr):
1602 def adddatafilter(name, fltr):
1603 pass
1603 pass
1604
1604
1605 def wread(filename):
1605 def wread(filename):
1606 """Read a file from wvfs, using data filters."""
1606 """Read a file from wvfs, using data filters."""
1607
1607
1608 def wwrite(filename, data, flags, backgroundclose=False, **kwargs):
1608 def wwrite(filename, data, flags, backgroundclose=False, **kwargs):
1609 """Write data to a file in the wvfs, using data filters."""
1609 """Write data to a file in the wvfs, using data filters."""
1610
1610
1611 def wwritedata(filename, data):
1611 def wwritedata(filename, data):
1612 """Resolve data for writing to the wvfs, using data filters."""
1612 """Resolve data for writing to the wvfs, using data filters."""
1613
1613
1614 def currenttransaction():
1614 def currenttransaction():
1615 """Obtain the current transaction instance or None."""
1615 """Obtain the current transaction instance or None."""
1616
1616
1617 def transaction(desc, report=None):
1617 def transaction(desc, report=None):
1618 """Open a new transaction to write to the repository."""
1618 """Open a new transaction to write to the repository."""
1619
1619
1620 def undofiles():
1620 def undofiles():
1621 """Returns a list of (vfs, path) for files to undo transactions."""
1621 """Returns a list of (vfs, path) for files to undo transactions."""
1622
1622
1623 def recover():
1623 def recover():
1624 """Roll back an interrupted transaction."""
1624 """Roll back an interrupted transaction."""
1625
1625
1626 def rollback(dryrun=False, force=False):
1626 def rollback(dryrun=False, force=False):
1627 """Undo the last transaction.
1627 """Undo the last transaction.
1628
1628
1629 DANGEROUS.
1629 DANGEROUS.
1630 """
1630 """
1631
1631
1632 def updatecaches(tr=None, full=False):
1632 def updatecaches(tr=None, full=False):
1633 """Warm repo caches."""
1633 """Warm repo caches."""
1634
1634
1635 def invalidatecaches():
1635 def invalidatecaches():
1636 """Invalidate cached data due to the repository mutating."""
1636 """Invalidate cached data due to the repository mutating."""
1637
1637
1638 def invalidatevolatilesets():
1638 def invalidatevolatilesets():
1639 pass
1639 pass
1640
1640
1641 def invalidatedirstate():
1641 def invalidatedirstate():
1642 """Invalidate the dirstate."""
1642 """Invalidate the dirstate."""
1643
1643
1644 def invalidate(clearfilecache=False):
1644 def invalidate(clearfilecache=False):
1645 pass
1645 pass
1646
1646
1647 def invalidateall():
1647 def invalidateall():
1648 pass
1648 pass
1649
1649
1650 def lock(wait=True):
1650 def lock(wait=True):
1651 """Lock the repository store and return a lock instance."""
1651 """Lock the repository store and return a lock instance."""
1652
1652
1653 def wlock(wait=True):
1653 def wlock(wait=True):
1654 """Lock the non-store parts of the repository."""
1654 """Lock the non-store parts of the repository."""
1655
1655
1656 def currentwlock():
1656 def currentwlock():
1657 """Return the wlock if it's held or None."""
1657 """Return the wlock if it's held or None."""
1658
1658
1659 def checkcommitpatterns(wctx, vdirs, match, status, fail):
1659 def checkcommitpatterns(wctx, vdirs, match, status, fail):
1660 pass
1660 pass
1661
1661
1662 def commit(text='', user=None, date=None, match=None, force=False,
1662 def commit(text='', user=None, date=None, match=None, force=False,
1663 editor=False, extra=None):
1663 editor=False, extra=None):
1664 """Add a new revision to the repository."""
1664 """Add a new revision to the repository."""
1665
1665
1666 def commitctx(ctx, error=False, origctx=None):
1666 def commitctx(ctx, error=False, origctx=None):
1667 """Commit a commitctx instance to the repository."""
1667 """Commit a commitctx instance to the repository."""
1668
1668
1669 def destroying():
1669 def destroying():
1670 """Inform the repository that nodes are about to be destroyed."""
1670 """Inform the repository that nodes are about to be destroyed."""
1671
1671
1672 def destroyed():
1672 def destroyed():
1673 """Inform the repository that nodes have been destroyed."""
1673 """Inform the repository that nodes have been destroyed."""
1674
1674
1675 def status(node1='.', node2=None, match=None, ignored=False,
1675 def status(node1='.', node2=None, match=None, ignored=False,
1676 clean=False, unknown=False, listsubrepos=False):
1676 clean=False, unknown=False, listsubrepos=False):
1677 """Convenience method to call repo[x].status()."""
1677 """Convenience method to call repo[x].status()."""
1678
1678
1679 def addpostdsstatus(ps):
1679 def addpostdsstatus(ps):
1680 pass
1680 pass
1681
1681
1682 def postdsstatus():
1682 def postdsstatus():
1683 pass
1683 pass
1684
1684
1685 def clearpostdsstatus():
1685 def clearpostdsstatus():
1686 pass
1686 pass
1687
1687
1688 def heads(start=None):
1688 def heads(start=None):
1689 """Obtain list of nodes that are DAG heads."""
1689 """Obtain list of nodes that are DAG heads."""
1690
1690
1691 def branchheads(branch=None, start=None, closed=False):
1691 def branchheads(branch=None, start=None, closed=False):
1692 pass
1692 pass
1693
1693
1694 def branches(nodes):
1694 def branches(nodes):
1695 pass
1695 pass
1696
1696
1697 def between(pairs):
1697 def between(pairs):
1698 pass
1698 pass
1699
1699
1700 def checkpush(pushop):
1700 def checkpush(pushop):
1701 pass
1701 pass
1702
1702
1703 prepushoutgoinghooks = interfaceutil.Attribute(
1703 prepushoutgoinghooks = interfaceutil.Attribute(
1704 """util.hooks instance.""")
1704 """util.hooks instance.""")
1705
1705
1706 def pushkey(namespace, key, old, new):
1706 def pushkey(namespace, key, old, new):
1707 pass
1707 pass
1708
1708
1709 def listkeys(namespace):
1709 def listkeys(namespace):
1710 pass
1710 pass
1711
1711
1712 def debugwireargs(one, two, three=None, four=None, five=None):
1712 def debugwireargs(one, two, three=None, four=None, five=None):
1713 pass
1713 pass
1714
1714
1715 def savecommitmessage(text):
1715 def savecommitmessage(text):
1716 pass
1716 pass
1717
1717
1718 class completelocalrepository(ilocalrepositorymain,
1718 class completelocalrepository(ilocalrepositorymain,
1719 ilocalrepositoryfilestorage):
1719 ilocalrepositoryfilestorage):
1720 """Complete interface for a local repository."""
1720 """Complete interface for a local repository."""
1721
1721
1722 class iwireprotocolcommandcacher(interfaceutil.Interface):
1722 class iwireprotocolcommandcacher(interfaceutil.Interface):
1723 """Represents a caching backend for wire protocol commands.
1723 """Represents a caching backend for wire protocol commands.
1724
1724
1725 Wire protocol version 2 supports transparent caching of many commands.
1725 Wire protocol version 2 supports transparent caching of many commands.
1726 To leverage this caching, servers can activate objects that cache
1726 To leverage this caching, servers can activate objects that cache
1727 command responses. Objects handle both cache writing and reading.
1727 command responses. Objects handle both cache writing and reading.
1728 This interface defines how that response caching mechanism works.
1728 This interface defines how that response caching mechanism works.
1729
1729
1730 Wire protocol version 2 commands emit a series of objects that are
1730 Wire protocol version 2 commands emit a series of objects that are
1731 serialized and sent to the client. The caching layer exists between
1731 serialized and sent to the client. The caching layer exists between
1732 the invocation of the command function and the sending of its output
1732 the invocation of the command function and the sending of its output
1733 objects to an output layer.
1733 objects to an output layer.
1734
1734
1735 Instances of this interface represent a binding to a cache that
1735 Instances of this interface represent a binding to a cache that
1736 can serve a response (in place of calling a command function) and/or
1736 can serve a response (in place of calling a command function) and/or
1737 write responses to a cache for subsequent use.
1737 write responses to a cache for subsequent use.
1738
1738
1739 When a command request arrives, the following happens with regards
1739 When a command request arrives, the following happens with regards
1740 to this interface:
1740 to this interface:
1741
1741
1742 1. The server determines whether the command request is cacheable.
1742 1. The server determines whether the command request is cacheable.
1743 2. If it is, an instance of this interface is spawned.
1743 2. If it is, an instance of this interface is spawned.
1744 3. The cacher is activated in a context manager (``__enter__`` is called).
1744 3. The cacher is activated in a context manager (``__enter__`` is called).
1745 4. A cache *key* for that request is derived. This will call the
1745 4. A cache *key* for that request is derived. This will call the
1746 instance's ``adjustcachekeystate()`` method so the derivation
1746 instance's ``adjustcachekeystate()`` method so the derivation
1747 can be influenced.
1747 can be influenced.
1748 5. The cacher is informed of the derived cache key via a call to
1748 5. The cacher is informed of the derived cache key via a call to
1749 ``setcachekey()``.
1749 ``setcachekey()``.
1750 6. The cacher's ``lookup()`` method is called to test for presence of
1750 6. The cacher's ``lookup()`` method is called to test for presence of
1751 the derived key in the cache.
1751 the derived key in the cache.
1752 7. If ``lookup()`` returns a hit, that cached result is used in place
1752 7. If ``lookup()`` returns a hit, that cached result is used in place
1753 of invoking the command function. ``__exit__`` is called and the instance
1753 of invoking the command function. ``__exit__`` is called and the instance
1754 is discarded.
1754 is discarded.
1755 8. The command function is invoked.
1755 8. The command function is invoked.
1756 9. ``onobject()`` is called for each object emitted by the command
1756 9. ``onobject()`` is called for each object emitted by the command
1757 function.
1757 function.
1758 10. After the final object is seen, ``onfinished()`` is called.
1758 10. After the final object is seen, ``onfinished()`` is called.
1759 11. ``__exit__`` is called to signal the end of use of the instance.
1759 11. ``__exit__`` is called to signal the end of use of the instance.
1760
1760
1761 Cache *key* derivation can be influenced by the instance.
1761 Cache *key* derivation can be influenced by the instance.
1762
1762
1763 Cache keys are initially derived by a deterministic representation of
1763 Cache keys are initially derived by a deterministic representation of
1764 the command request. This includes the command name, arguments, protocol
1764 the command request. This includes the command name, arguments, protocol
1765 version, etc. This initial key derivation is performed by CBOR-encoding a
1765 version, etc. This initial key derivation is performed by CBOR-encoding a
1766 data structure and feeding that output into a hasher.
1766 data structure and feeding that output into a hasher.
1767
1767
1768 Instances of this interface can influence this initial key derivation
1768 Instances of this interface can influence this initial key derivation
1769 via ``adjustcachekeystate()``.
1769 via ``adjustcachekeystate()``.
1770
1770
1771 The instance is informed of the derived cache key via a call to
1771 The instance is informed of the derived cache key via a call to
1772 ``setcachekey()``. The instance must store the key locally so it can
1772 ``setcachekey()``. The instance must store the key locally so it can
1773 be consulted on subsequent operations that may require it.
1773 be consulted on subsequent operations that may require it.
1774
1774
1775 When constructed, the instance has access to a callable that can be used
1775 When constructed, the instance has access to a callable that can be used
1776 for encoding response objects. This callable receives as its single
1776 for encoding response objects. This callable receives as its single
1777 argument an object emitted by a command function. It returns an iterable
1777 argument an object emitted by a command function. It returns an iterable
1778 of bytes chunks representing the encoded object. Unless the cacher is
1778 of bytes chunks representing the encoded object. Unless the cacher is
1779 caching native Python objects in memory or has a way of reconstructing
1779 caching native Python objects in memory or has a way of reconstructing
1780 the original Python objects, implementations typically call this function
1780 the original Python objects, implementations typically call this function
1781 to produce bytes from the output objects and then store those bytes in
1781 to produce bytes from the output objects and then store those bytes in
1782 the cache. When it comes time to re-emit those bytes, they are wrapped
1782 the cache. When it comes time to re-emit those bytes, they are wrapped
1783 in a ``wireprototypes.encodedresponse`` instance to tell the output
1783 in a ``wireprototypes.encodedresponse`` instance to tell the output
1784 layer that they are pre-encoded.
1784 layer that they are pre-encoded.
1785
1785
1786 When receiving the objects emitted by the command function, instances
1786 When receiving the objects emitted by the command function, instances
1787 can choose what to do with those objects. The simplest thing to do is
1787 can choose what to do with those objects. The simplest thing to do is
1788 re-emit the original objects. They will be forwarded to the output
1788 re-emit the original objects. They will be forwarded to the output
1789 layer and will be processed as if the cacher did not exist.
1789 layer and will be processed as if the cacher did not exist.
1790
1790
1791 Implementations could also choose to not emit objects - instead locally
1791 Implementations could also choose to not emit objects - instead locally
1792 buffering objects or their encoded representation. They could then emit
1792 buffering objects or their encoded representation. They could then emit
1793 a single "coalesced" object when ``onfinished()`` is called. In
1793 a single "coalesced" object when ``onfinished()`` is called. In
1794 this way, the implementation would function as a filtering layer of
1794 this way, the implementation would function as a filtering layer of
1795 sorts.
1795 sorts.
1796
1796
1797 When caching objects, typically the encoded form of the object will
1797 When caching objects, typically the encoded form of the object will
1798 be stored. Keep in mind that if the original object is forwarded to
1798 be stored. Keep in mind that if the original object is forwarded to
1799 the output layer, it will need to be encoded there as well. For large
1799 the output layer, it will need to be encoded there as well. For large
1800 output, this redundant encoding could add overhead. Implementations
1800 output, this redundant encoding could add overhead. Implementations
1801 could wrap the encoded object data in ``wireprototypes.encodedresponse``
1801 could wrap the encoded object data in ``wireprototypes.encodedresponse``
1802 instances to avoid this overhead.
1802 instances to avoid this overhead.
1803 """
1803 """
1804 def __enter__():
1804 def __enter__():
1805 """Marks the instance as active.
1805 """Marks the instance as active.
1806
1806
1807 Should return self.
1807 Should return self.
1808 """
1808 """
1809
1809
1810 def __exit__(exctype, excvalue, exctb):
1810 def __exit__(exctype, excvalue, exctb):
1811 """Called when cacher is no longer used.
1811 """Called when cacher is no longer used.
1812
1812
1813 This can be used by implementations to perform cleanup actions (e.g.
1813 This can be used by implementations to perform cleanup actions (e.g.
1814 disconnecting network sockets, aborting a partially cached response.
1814 disconnecting network sockets, aborting a partially cached response.
1815 """
1815 """
1816
1816
1817 def adjustcachekeystate(state):
1817 def adjustcachekeystate(state):
1818 """Influences cache key derivation by adjusting state to derive key.
1818 """Influences cache key derivation by adjusting state to derive key.
1819
1819
1820 A dict defining the state used to derive the cache key is passed.
1820 A dict defining the state used to derive the cache key is passed.
1821
1821
1822 Implementations can modify this dict to record additional state that
1822 Implementations can modify this dict to record additional state that
1823 is wanted to influence key derivation.
1823 is wanted to influence key derivation.
1824
1824
1825 Implementations are *highly* encouraged to not modify or delete
1825 Implementations are *highly* encouraged to not modify or delete
1826 existing keys.
1826 existing keys.
1827 """
1827 """
1828
1828
1829 def setcachekey(key):
1829 def setcachekey(key):
1830 """Record the derived cache key for this request.
1830 """Record the derived cache key for this request.
1831
1831
1832 Instances may mutate the key for internal usage, as desired. e.g.
1832 Instances may mutate the key for internal usage, as desired. e.g.
1833 instances may wish to prepend the repo name, introduce path
1833 instances may wish to prepend the repo name, introduce path
1834 components for filesystem or URL addressing, etc. Behavior is up to
1834 components for filesystem or URL addressing, etc. Behavior is up to
1835 the cache.
1835 the cache.
1836
1836
1837 Returns a bool indicating if the request is cacheable by this
1837 Returns a bool indicating if the request is cacheable by this
1838 instance.
1838 instance.
1839 """
1839 """
1840
1840
1841 def lookup():
1841 def lookup():
1842 """Attempt to resolve an entry in the cache.
1842 """Attempt to resolve an entry in the cache.
1843
1843
1844 The instance is instructed to look for the cache key that it was
1844 The instance is instructed to look for the cache key that it was
1845 informed about via the call to ``setcachekey()``.
1845 informed about via the call to ``setcachekey()``.
1846
1846
1847 If there's no cache hit or the cacher doesn't wish to use the cached
1847 If there's no cache hit or the cacher doesn't wish to use the cached
1848 entry, ``None`` should be returned.
1848 entry, ``None`` should be returned.
1849
1849
1850 Else, a dict defining the cached result should be returned. The
1850 Else, a dict defining the cached result should be returned. The
1851 dict may have the following keys:
1851 dict may have the following keys:
1852
1852
1853 objs
1853 objs
1854 An iterable of objects that should be sent to the client. That
1854 An iterable of objects that should be sent to the client. That
1855 iterable of objects is expected to be what the command function
1855 iterable of objects is expected to be what the command function
1856 would return if invoked or an equivalent representation thereof.
1856 would return if invoked or an equivalent representation thereof.
1857 """
1857 """
1858
1858
1859 def onobject(obj):
1859 def onobject(obj):
1860 """Called when a new object is emitted from the command function.
1860 """Called when a new object is emitted from the command function.
1861
1861
1862 Receives as its argument the object that was emitted from the
1862 Receives as its argument the object that was emitted from the
1863 command function.
1863 command function.
1864
1864
1865 This method returns an iterator of objects to forward to the output
1865 This method returns an iterator of objects to forward to the output
1866 layer. The easiest implementation is a generator that just
1866 layer. The easiest implementation is a generator that just
1867 ``yield obj``.
1867 ``yield obj``.
1868 """
1868 """
1869
1869
1870 def onfinished():
1870 def onfinished():
1871 """Called after all objects have been emitted from the command function.
1871 """Called after all objects have been emitted from the command function.
1872
1872
1873 Implementations should return an iterator of objects to forward to
1873 Implementations should return an iterator of objects to forward to
1874 the output layer.
1874 the output layer.
1875
1875
1876 This method can be a generator.
1876 This method can be a generator.
1877 """
1877 """
@@ -1,3312 +1,3316
1 # localrepo.py - read/write repository class for mercurial
1 # localrepo.py - read/write repository class for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import hashlib
11 import hashlib
12 import os
12 import os
13 import random
13 import random
14 import sys
14 import sys
15 import time
15 import time
16 import weakref
16 import weakref
17
17
18 from .i18n import _
18 from .i18n import _
19 from .node import (
19 from .node import (
20 bin,
20 bin,
21 hex,
21 hex,
22 nullid,
22 nullid,
23 nullrev,
23 nullrev,
24 short,
24 short,
25 )
25 )
26 from . import (
26 from . import (
27 bookmarks,
27 bookmarks,
28 branchmap,
28 branchmap,
29 bundle2,
29 bundle2,
30 changegroup,
30 changegroup,
31 changelog,
31 changelog,
32 color,
32 color,
33 context,
33 context,
34 dirstate,
34 dirstate,
35 dirstateguard,
35 dirstateguard,
36 discovery,
36 discovery,
37 encoding,
37 encoding,
38 error,
38 error,
39 exchange,
39 exchange,
40 extensions,
40 extensions,
41 filelog,
41 filelog,
42 hook,
42 hook,
43 lock as lockmod,
43 lock as lockmod,
44 manifest,
44 manifest,
45 match as matchmod,
45 match as matchmod,
46 merge as mergemod,
46 merge as mergemod,
47 mergeutil,
47 mergeutil,
48 namespaces,
48 namespaces,
49 narrowspec,
49 narrowspec,
50 obsolete,
50 obsolete,
51 pathutil,
51 pathutil,
52 phases,
52 phases,
53 pushkey,
53 pushkey,
54 pycompat,
54 pycompat,
55 repository,
56 repoview,
55 repoview,
57 revset,
56 revset,
58 revsetlang,
57 revsetlang,
59 scmutil,
58 scmutil,
60 sparse,
59 sparse,
61 store as storemod,
60 store as storemod,
62 subrepoutil,
61 subrepoutil,
63 tags as tagsmod,
62 tags as tagsmod,
64 transaction,
63 transaction,
65 txnutil,
64 txnutil,
66 util,
65 util,
67 vfs as vfsmod,
66 vfs as vfsmod,
68 )
67 )
68
69 from .interfaces import (
70 repository,
71 )
72
69 from .utils import (
73 from .utils import (
70 interfaceutil,
74 interfaceutil,
71 procutil,
75 procutil,
72 stringutil,
76 stringutil,
73 )
77 )
74
78
75 from .revlogutils import (
79 from .revlogutils import (
76 constants as revlogconst,
80 constants as revlogconst,
77 )
81 )
78
82
79 release = lockmod.release
83 release = lockmod.release
80 urlerr = util.urlerr
84 urlerr = util.urlerr
81 urlreq = util.urlreq
85 urlreq = util.urlreq
82
86
83 # set of (path, vfs-location) tuples. vfs-location is:
87 # set of (path, vfs-location) tuples. vfs-location is:
84 # - 'plain for vfs relative paths
88 # - 'plain for vfs relative paths
85 # - '' for svfs relative paths
89 # - '' for svfs relative paths
86 _cachedfiles = set()
90 _cachedfiles = set()
87
91
88 class _basefilecache(scmutil.filecache):
92 class _basefilecache(scmutil.filecache):
89 """All filecache usage on repo are done for logic that should be unfiltered
93 """All filecache usage on repo are done for logic that should be unfiltered
90 """
94 """
91 def __get__(self, repo, type=None):
95 def __get__(self, repo, type=None):
92 if repo is None:
96 if repo is None:
93 return self
97 return self
94 # proxy to unfiltered __dict__ since filtered repo has no entry
98 # proxy to unfiltered __dict__ since filtered repo has no entry
95 unfi = repo.unfiltered()
99 unfi = repo.unfiltered()
96 try:
100 try:
97 return unfi.__dict__[self.sname]
101 return unfi.__dict__[self.sname]
98 except KeyError:
102 except KeyError:
99 pass
103 pass
100 return super(_basefilecache, self).__get__(unfi, type)
104 return super(_basefilecache, self).__get__(unfi, type)
101
105
102 def set(self, repo, value):
106 def set(self, repo, value):
103 return super(_basefilecache, self).set(repo.unfiltered(), value)
107 return super(_basefilecache, self).set(repo.unfiltered(), value)
104
108
105 class repofilecache(_basefilecache):
109 class repofilecache(_basefilecache):
106 """filecache for files in .hg but outside of .hg/store"""
110 """filecache for files in .hg but outside of .hg/store"""
107 def __init__(self, *paths):
111 def __init__(self, *paths):
108 super(repofilecache, self).__init__(*paths)
112 super(repofilecache, self).__init__(*paths)
109 for path in paths:
113 for path in paths:
110 _cachedfiles.add((path, 'plain'))
114 _cachedfiles.add((path, 'plain'))
111
115
112 def join(self, obj, fname):
116 def join(self, obj, fname):
113 return obj.vfs.join(fname)
117 return obj.vfs.join(fname)
114
118
115 class storecache(_basefilecache):
119 class storecache(_basefilecache):
116 """filecache for files in the store"""
120 """filecache for files in the store"""
117 def __init__(self, *paths):
121 def __init__(self, *paths):
118 super(storecache, self).__init__(*paths)
122 super(storecache, self).__init__(*paths)
119 for path in paths:
123 for path in paths:
120 _cachedfiles.add((path, ''))
124 _cachedfiles.add((path, ''))
121
125
122 def join(self, obj, fname):
126 def join(self, obj, fname):
123 return obj.sjoin(fname)
127 return obj.sjoin(fname)
124
128
125 class mixedrepostorecache(_basefilecache):
129 class mixedrepostorecache(_basefilecache):
126 """filecache for a mix files in .hg/store and outside"""
130 """filecache for a mix files in .hg/store and outside"""
127 def __init__(self, *pathsandlocations):
131 def __init__(self, *pathsandlocations):
128 # scmutil.filecache only uses the path for passing back into our
132 # scmutil.filecache only uses the path for passing back into our
129 # join(), so we can safely pass a list of paths and locations
133 # join(), so we can safely pass a list of paths and locations
130 super(mixedrepostorecache, self).__init__(*pathsandlocations)
134 super(mixedrepostorecache, self).__init__(*pathsandlocations)
131 _cachedfiles.update(pathsandlocations)
135 _cachedfiles.update(pathsandlocations)
132
136
133 def join(self, obj, fnameandlocation):
137 def join(self, obj, fnameandlocation):
134 fname, location = fnameandlocation
138 fname, location = fnameandlocation
135 if location == 'plain':
139 if location == 'plain':
136 return obj.vfs.join(fname)
140 return obj.vfs.join(fname)
137 else:
141 else:
138 if location != '':
142 if location != '':
139 raise error.ProgrammingError('unexpected location: %s' %
143 raise error.ProgrammingError('unexpected location: %s' %
140 location)
144 location)
141 return obj.sjoin(fname)
145 return obj.sjoin(fname)
142
146
143 def isfilecached(repo, name):
147 def isfilecached(repo, name):
144 """check if a repo has already cached "name" filecache-ed property
148 """check if a repo has already cached "name" filecache-ed property
145
149
146 This returns (cachedobj-or-None, iscached) tuple.
150 This returns (cachedobj-or-None, iscached) tuple.
147 """
151 """
148 cacheentry = repo.unfiltered()._filecache.get(name, None)
152 cacheentry = repo.unfiltered()._filecache.get(name, None)
149 if not cacheentry:
153 if not cacheentry:
150 return None, False
154 return None, False
151 return cacheentry.obj, True
155 return cacheentry.obj, True
152
156
153 class unfilteredpropertycache(util.propertycache):
157 class unfilteredpropertycache(util.propertycache):
154 """propertycache that apply to unfiltered repo only"""
158 """propertycache that apply to unfiltered repo only"""
155
159
156 def __get__(self, repo, type=None):
160 def __get__(self, repo, type=None):
157 unfi = repo.unfiltered()
161 unfi = repo.unfiltered()
158 if unfi is repo:
162 if unfi is repo:
159 return super(unfilteredpropertycache, self).__get__(unfi)
163 return super(unfilteredpropertycache, self).__get__(unfi)
160 return getattr(unfi, self.name)
164 return getattr(unfi, self.name)
161
165
162 class filteredpropertycache(util.propertycache):
166 class filteredpropertycache(util.propertycache):
163 """propertycache that must take filtering in account"""
167 """propertycache that must take filtering in account"""
164
168
165 def cachevalue(self, obj, value):
169 def cachevalue(self, obj, value):
166 object.__setattr__(obj, self.name, value)
170 object.__setattr__(obj, self.name, value)
167
171
168
172
169 def hasunfilteredcache(repo, name):
173 def hasunfilteredcache(repo, name):
170 """check if a repo has an unfilteredpropertycache value for <name>"""
174 """check if a repo has an unfilteredpropertycache value for <name>"""
171 return name in vars(repo.unfiltered())
175 return name in vars(repo.unfiltered())
172
176
173 def unfilteredmethod(orig):
177 def unfilteredmethod(orig):
174 """decorate method that always need to be run on unfiltered version"""
178 """decorate method that always need to be run on unfiltered version"""
175 def wrapper(repo, *args, **kwargs):
179 def wrapper(repo, *args, **kwargs):
176 return orig(repo.unfiltered(), *args, **kwargs)
180 return orig(repo.unfiltered(), *args, **kwargs)
177 return wrapper
181 return wrapper
178
182
179 moderncaps = {'lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
183 moderncaps = {'lookup', 'branchmap', 'pushkey', 'known', 'getbundle',
180 'unbundle'}
184 'unbundle'}
181 legacycaps = moderncaps.union({'changegroupsubset'})
185 legacycaps = moderncaps.union({'changegroupsubset'})
182
186
183 @interfaceutil.implementer(repository.ipeercommandexecutor)
187 @interfaceutil.implementer(repository.ipeercommandexecutor)
184 class localcommandexecutor(object):
188 class localcommandexecutor(object):
185 def __init__(self, peer):
189 def __init__(self, peer):
186 self._peer = peer
190 self._peer = peer
187 self._sent = False
191 self._sent = False
188 self._closed = False
192 self._closed = False
189
193
190 def __enter__(self):
194 def __enter__(self):
191 return self
195 return self
192
196
193 def __exit__(self, exctype, excvalue, exctb):
197 def __exit__(self, exctype, excvalue, exctb):
194 self.close()
198 self.close()
195
199
196 def callcommand(self, command, args):
200 def callcommand(self, command, args):
197 if self._sent:
201 if self._sent:
198 raise error.ProgrammingError('callcommand() cannot be used after '
202 raise error.ProgrammingError('callcommand() cannot be used after '
199 'sendcommands()')
203 'sendcommands()')
200
204
201 if self._closed:
205 if self._closed:
202 raise error.ProgrammingError('callcommand() cannot be used after '
206 raise error.ProgrammingError('callcommand() cannot be used after '
203 'close()')
207 'close()')
204
208
205 # We don't need to support anything fancy. Just call the named
209 # We don't need to support anything fancy. Just call the named
206 # method on the peer and return a resolved future.
210 # method on the peer and return a resolved future.
207 fn = getattr(self._peer, pycompat.sysstr(command))
211 fn = getattr(self._peer, pycompat.sysstr(command))
208
212
209 f = pycompat.futures.Future()
213 f = pycompat.futures.Future()
210
214
211 try:
215 try:
212 result = fn(**pycompat.strkwargs(args))
216 result = fn(**pycompat.strkwargs(args))
213 except Exception:
217 except Exception:
214 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
218 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
215 else:
219 else:
216 f.set_result(result)
220 f.set_result(result)
217
221
218 return f
222 return f
219
223
220 def sendcommands(self):
224 def sendcommands(self):
221 self._sent = True
225 self._sent = True
222
226
223 def close(self):
227 def close(self):
224 self._closed = True
228 self._closed = True
225
229
226 @interfaceutil.implementer(repository.ipeercommands)
230 @interfaceutil.implementer(repository.ipeercommands)
227 class localpeer(repository.peer):
231 class localpeer(repository.peer):
228 '''peer for a local repo; reflects only the most recent API'''
232 '''peer for a local repo; reflects only the most recent API'''
229
233
230 def __init__(self, repo, caps=None):
234 def __init__(self, repo, caps=None):
231 super(localpeer, self).__init__()
235 super(localpeer, self).__init__()
232
236
233 if caps is None:
237 if caps is None:
234 caps = moderncaps.copy()
238 caps = moderncaps.copy()
235 self._repo = repo.filtered('served')
239 self._repo = repo.filtered('served')
236 self.ui = repo.ui
240 self.ui = repo.ui
237 self._caps = repo._restrictcapabilities(caps)
241 self._caps = repo._restrictcapabilities(caps)
238
242
239 # Begin of _basepeer interface.
243 # Begin of _basepeer interface.
240
244
241 def url(self):
245 def url(self):
242 return self._repo.url()
246 return self._repo.url()
243
247
244 def local(self):
248 def local(self):
245 return self._repo
249 return self._repo
246
250
247 def peer(self):
251 def peer(self):
248 return self
252 return self
249
253
250 def canpush(self):
254 def canpush(self):
251 return True
255 return True
252
256
253 def close(self):
257 def close(self):
254 self._repo.close()
258 self._repo.close()
255
259
256 # End of _basepeer interface.
260 # End of _basepeer interface.
257
261
258 # Begin of _basewirecommands interface.
262 # Begin of _basewirecommands interface.
259
263
260 def branchmap(self):
264 def branchmap(self):
261 return self._repo.branchmap()
265 return self._repo.branchmap()
262
266
263 def capabilities(self):
267 def capabilities(self):
264 return self._caps
268 return self._caps
265
269
266 def clonebundles(self):
270 def clonebundles(self):
267 return self._repo.tryread('clonebundles.manifest')
271 return self._repo.tryread('clonebundles.manifest')
268
272
269 def debugwireargs(self, one, two, three=None, four=None, five=None):
273 def debugwireargs(self, one, two, three=None, four=None, five=None):
270 """Used to test argument passing over the wire"""
274 """Used to test argument passing over the wire"""
271 return "%s %s %s %s %s" % (one, two, pycompat.bytestr(three),
275 return "%s %s %s %s %s" % (one, two, pycompat.bytestr(three),
272 pycompat.bytestr(four),
276 pycompat.bytestr(four),
273 pycompat.bytestr(five))
277 pycompat.bytestr(five))
274
278
275 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
279 def getbundle(self, source, heads=None, common=None, bundlecaps=None,
276 **kwargs):
280 **kwargs):
277 chunks = exchange.getbundlechunks(self._repo, source, heads=heads,
281 chunks = exchange.getbundlechunks(self._repo, source, heads=heads,
278 common=common, bundlecaps=bundlecaps,
282 common=common, bundlecaps=bundlecaps,
279 **kwargs)[1]
283 **kwargs)[1]
280 cb = util.chunkbuffer(chunks)
284 cb = util.chunkbuffer(chunks)
281
285
282 if exchange.bundle2requested(bundlecaps):
286 if exchange.bundle2requested(bundlecaps):
283 # When requesting a bundle2, getbundle returns a stream to make the
287 # When requesting a bundle2, getbundle returns a stream to make the
284 # wire level function happier. We need to build a proper object
288 # wire level function happier. We need to build a proper object
285 # from it in local peer.
289 # from it in local peer.
286 return bundle2.getunbundler(self.ui, cb)
290 return bundle2.getunbundler(self.ui, cb)
287 else:
291 else:
288 return changegroup.getunbundler('01', cb, None)
292 return changegroup.getunbundler('01', cb, None)
289
293
290 def heads(self):
294 def heads(self):
291 return self._repo.heads()
295 return self._repo.heads()
292
296
293 def known(self, nodes):
297 def known(self, nodes):
294 return self._repo.known(nodes)
298 return self._repo.known(nodes)
295
299
296 def listkeys(self, namespace):
300 def listkeys(self, namespace):
297 return self._repo.listkeys(namespace)
301 return self._repo.listkeys(namespace)
298
302
299 def lookup(self, key):
303 def lookup(self, key):
300 return self._repo.lookup(key)
304 return self._repo.lookup(key)
301
305
302 def pushkey(self, namespace, key, old, new):
306 def pushkey(self, namespace, key, old, new):
303 return self._repo.pushkey(namespace, key, old, new)
307 return self._repo.pushkey(namespace, key, old, new)
304
308
305 def stream_out(self):
309 def stream_out(self):
306 raise error.Abort(_('cannot perform stream clone against local '
310 raise error.Abort(_('cannot perform stream clone against local '
307 'peer'))
311 'peer'))
308
312
309 def unbundle(self, bundle, heads, url):
313 def unbundle(self, bundle, heads, url):
310 """apply a bundle on a repo
314 """apply a bundle on a repo
311
315
312 This function handles the repo locking itself."""
316 This function handles the repo locking itself."""
313 try:
317 try:
314 try:
318 try:
315 bundle = exchange.readbundle(self.ui, bundle, None)
319 bundle = exchange.readbundle(self.ui, bundle, None)
316 ret = exchange.unbundle(self._repo, bundle, heads, 'push', url)
320 ret = exchange.unbundle(self._repo, bundle, heads, 'push', url)
317 if util.safehasattr(ret, 'getchunks'):
321 if util.safehasattr(ret, 'getchunks'):
318 # This is a bundle20 object, turn it into an unbundler.
322 # This is a bundle20 object, turn it into an unbundler.
319 # This little dance should be dropped eventually when the
323 # This little dance should be dropped eventually when the
320 # API is finally improved.
324 # API is finally improved.
321 stream = util.chunkbuffer(ret.getchunks())
325 stream = util.chunkbuffer(ret.getchunks())
322 ret = bundle2.getunbundler(self.ui, stream)
326 ret = bundle2.getunbundler(self.ui, stream)
323 return ret
327 return ret
324 except Exception as exc:
328 except Exception as exc:
325 # If the exception contains output salvaged from a bundle2
329 # If the exception contains output salvaged from a bundle2
326 # reply, we need to make sure it is printed before continuing
330 # reply, we need to make sure it is printed before continuing
327 # to fail. So we build a bundle2 with such output and consume
331 # to fail. So we build a bundle2 with such output and consume
328 # it directly.
332 # it directly.
329 #
333 #
330 # This is not very elegant but allows a "simple" solution for
334 # This is not very elegant but allows a "simple" solution for
331 # issue4594
335 # issue4594
332 output = getattr(exc, '_bundle2salvagedoutput', ())
336 output = getattr(exc, '_bundle2salvagedoutput', ())
333 if output:
337 if output:
334 bundler = bundle2.bundle20(self._repo.ui)
338 bundler = bundle2.bundle20(self._repo.ui)
335 for out in output:
339 for out in output:
336 bundler.addpart(out)
340 bundler.addpart(out)
337 stream = util.chunkbuffer(bundler.getchunks())
341 stream = util.chunkbuffer(bundler.getchunks())
338 b = bundle2.getunbundler(self.ui, stream)
342 b = bundle2.getunbundler(self.ui, stream)
339 bundle2.processbundle(self._repo, b)
343 bundle2.processbundle(self._repo, b)
340 raise
344 raise
341 except error.PushRaced as exc:
345 except error.PushRaced as exc:
342 raise error.ResponseError(_('push failed:'),
346 raise error.ResponseError(_('push failed:'),
343 stringutil.forcebytestr(exc))
347 stringutil.forcebytestr(exc))
344
348
345 # End of _basewirecommands interface.
349 # End of _basewirecommands interface.
346
350
347 # Begin of peer interface.
351 # Begin of peer interface.
348
352
349 def commandexecutor(self):
353 def commandexecutor(self):
350 return localcommandexecutor(self)
354 return localcommandexecutor(self)
351
355
352 # End of peer interface.
356 # End of peer interface.
353
357
354 @interfaceutil.implementer(repository.ipeerlegacycommands)
358 @interfaceutil.implementer(repository.ipeerlegacycommands)
355 class locallegacypeer(localpeer):
359 class locallegacypeer(localpeer):
356 '''peer extension which implements legacy methods too; used for tests with
360 '''peer extension which implements legacy methods too; used for tests with
357 restricted capabilities'''
361 restricted capabilities'''
358
362
359 def __init__(self, repo):
363 def __init__(self, repo):
360 super(locallegacypeer, self).__init__(repo, caps=legacycaps)
364 super(locallegacypeer, self).__init__(repo, caps=legacycaps)
361
365
362 # Begin of baselegacywirecommands interface.
366 # Begin of baselegacywirecommands interface.
363
367
364 def between(self, pairs):
368 def between(self, pairs):
365 return self._repo.between(pairs)
369 return self._repo.between(pairs)
366
370
367 def branches(self, nodes):
371 def branches(self, nodes):
368 return self._repo.branches(nodes)
372 return self._repo.branches(nodes)
369
373
370 def changegroup(self, nodes, source):
374 def changegroup(self, nodes, source):
371 outgoing = discovery.outgoing(self._repo, missingroots=nodes,
375 outgoing = discovery.outgoing(self._repo, missingroots=nodes,
372 missingheads=self._repo.heads())
376 missingheads=self._repo.heads())
373 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
377 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
374
378
375 def changegroupsubset(self, bases, heads, source):
379 def changegroupsubset(self, bases, heads, source):
376 outgoing = discovery.outgoing(self._repo, missingroots=bases,
380 outgoing = discovery.outgoing(self._repo, missingroots=bases,
377 missingheads=heads)
381 missingheads=heads)
378 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
382 return changegroup.makechangegroup(self._repo, outgoing, '01', source)
379
383
380 # End of baselegacywirecommands interface.
384 # End of baselegacywirecommands interface.
381
385
382 # Increment the sub-version when the revlog v2 format changes to lock out old
386 # Increment the sub-version when the revlog v2 format changes to lock out old
383 # clients.
387 # clients.
384 REVLOGV2_REQUIREMENT = 'exp-revlogv2.1'
388 REVLOGV2_REQUIREMENT = 'exp-revlogv2.1'
385
389
386 # A repository with the sparserevlog feature will have delta chains that
390 # A repository with the sparserevlog feature will have delta chains that
387 # can spread over a larger span. Sparse reading cuts these large spans into
391 # can spread over a larger span. Sparse reading cuts these large spans into
388 # pieces, so that each piece isn't too big.
392 # pieces, so that each piece isn't too big.
389 # Without the sparserevlog capability, reading from the repository could use
393 # Without the sparserevlog capability, reading from the repository could use
390 # huge amounts of memory, because the whole span would be read at once,
394 # huge amounts of memory, because the whole span would be read at once,
391 # including all the intermediate revisions that aren't pertinent for the chain.
395 # including all the intermediate revisions that aren't pertinent for the chain.
392 # This is why once a repository has enabled sparse-read, it becomes required.
396 # This is why once a repository has enabled sparse-read, it becomes required.
393 SPARSEREVLOG_REQUIREMENT = 'sparserevlog'
397 SPARSEREVLOG_REQUIREMENT = 'sparserevlog'
394
398
395 # Functions receiving (ui, features) that extensions can register to impact
399 # Functions receiving (ui, features) that extensions can register to impact
396 # the ability to load repositories with custom requirements. Only
400 # the ability to load repositories with custom requirements. Only
397 # functions defined in loaded extensions are called.
401 # functions defined in loaded extensions are called.
398 #
402 #
399 # The function receives a set of requirement strings that the repository
403 # The function receives a set of requirement strings that the repository
400 # is capable of opening. Functions will typically add elements to the
404 # is capable of opening. Functions will typically add elements to the
401 # set to reflect that the extension knows how to handle that requirements.
405 # set to reflect that the extension knows how to handle that requirements.
402 featuresetupfuncs = set()
406 featuresetupfuncs = set()
403
407
404 def makelocalrepository(baseui, path, intents=None):
408 def makelocalrepository(baseui, path, intents=None):
405 """Create a local repository object.
409 """Create a local repository object.
406
410
407 Given arguments needed to construct a local repository, this function
411 Given arguments needed to construct a local repository, this function
408 performs various early repository loading functionality (such as
412 performs various early repository loading functionality (such as
409 reading the ``.hg/requires`` and ``.hg/hgrc`` files), validates that
413 reading the ``.hg/requires`` and ``.hg/hgrc`` files), validates that
410 the repository can be opened, derives a type suitable for representing
414 the repository can be opened, derives a type suitable for representing
411 that repository, and returns an instance of it.
415 that repository, and returns an instance of it.
412
416
413 The returned object conforms to the ``repository.completelocalrepository``
417 The returned object conforms to the ``repository.completelocalrepository``
414 interface.
418 interface.
415
419
416 The repository type is derived by calling a series of factory functions
420 The repository type is derived by calling a series of factory functions
417 for each aspect/interface of the final repository. These are defined by
421 for each aspect/interface of the final repository. These are defined by
418 ``REPO_INTERFACES``.
422 ``REPO_INTERFACES``.
419
423
420 Each factory function is called to produce a type implementing a specific
424 Each factory function is called to produce a type implementing a specific
421 interface. The cumulative list of returned types will be combined into a
425 interface. The cumulative list of returned types will be combined into a
422 new type and that type will be instantiated to represent the local
426 new type and that type will be instantiated to represent the local
423 repository.
427 repository.
424
428
425 The factory functions each receive various state that may be consulted
429 The factory functions each receive various state that may be consulted
426 as part of deriving a type.
430 as part of deriving a type.
427
431
428 Extensions should wrap these factory functions to customize repository type
432 Extensions should wrap these factory functions to customize repository type
429 creation. Note that an extension's wrapped function may be called even if
433 creation. Note that an extension's wrapped function may be called even if
430 that extension is not loaded for the repo being constructed. Extensions
434 that extension is not loaded for the repo being constructed. Extensions
431 should check if their ``__name__`` appears in the
435 should check if their ``__name__`` appears in the
432 ``extensionmodulenames`` set passed to the factory function and no-op if
436 ``extensionmodulenames`` set passed to the factory function and no-op if
433 not.
437 not.
434 """
438 """
435 ui = baseui.copy()
439 ui = baseui.copy()
436 # Prevent copying repo configuration.
440 # Prevent copying repo configuration.
437 ui.copy = baseui.copy
441 ui.copy = baseui.copy
438
442
439 # Working directory VFS rooted at repository root.
443 # Working directory VFS rooted at repository root.
440 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
444 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
441
445
442 # Main VFS for .hg/ directory.
446 # Main VFS for .hg/ directory.
443 hgpath = wdirvfs.join(b'.hg')
447 hgpath = wdirvfs.join(b'.hg')
444 hgvfs = vfsmod.vfs(hgpath, cacheaudited=True)
448 hgvfs = vfsmod.vfs(hgpath, cacheaudited=True)
445
449
446 # The .hg/ path should exist and should be a directory. All other
450 # The .hg/ path should exist and should be a directory. All other
447 # cases are errors.
451 # cases are errors.
448 if not hgvfs.isdir():
452 if not hgvfs.isdir():
449 try:
453 try:
450 hgvfs.stat()
454 hgvfs.stat()
451 except OSError as e:
455 except OSError as e:
452 if e.errno != errno.ENOENT:
456 if e.errno != errno.ENOENT:
453 raise
457 raise
454
458
455 raise error.RepoError(_(b'repository %s not found') % path)
459 raise error.RepoError(_(b'repository %s not found') % path)
456
460
457 # .hg/requires file contains a newline-delimited list of
461 # .hg/requires file contains a newline-delimited list of
458 # features/capabilities the opener (us) must have in order to use
462 # features/capabilities the opener (us) must have in order to use
459 # the repository. This file was introduced in Mercurial 0.9.2,
463 # the repository. This file was introduced in Mercurial 0.9.2,
460 # which means very old repositories may not have one. We assume
464 # which means very old repositories may not have one. We assume
461 # a missing file translates to no requirements.
465 # a missing file translates to no requirements.
462 try:
466 try:
463 requirements = set(hgvfs.read(b'requires').splitlines())
467 requirements = set(hgvfs.read(b'requires').splitlines())
464 except IOError as e:
468 except IOError as e:
465 if e.errno != errno.ENOENT:
469 if e.errno != errno.ENOENT:
466 raise
470 raise
467 requirements = set()
471 requirements = set()
468
472
469 # The .hg/hgrc file may load extensions or contain config options
473 # The .hg/hgrc file may load extensions or contain config options
470 # that influence repository construction. Attempt to load it and
474 # that influence repository construction. Attempt to load it and
471 # process any new extensions that it may have pulled in.
475 # process any new extensions that it may have pulled in.
472 if loadhgrc(ui, wdirvfs, hgvfs, requirements):
476 if loadhgrc(ui, wdirvfs, hgvfs, requirements):
473 afterhgrcload(ui, wdirvfs, hgvfs, requirements)
477 afterhgrcload(ui, wdirvfs, hgvfs, requirements)
474 extensions.loadall(ui)
478 extensions.loadall(ui)
475 extensions.populateui(ui)
479 extensions.populateui(ui)
476
480
477 # Set of module names of extensions loaded for this repository.
481 # Set of module names of extensions loaded for this repository.
478 extensionmodulenames = {m.__name__ for n, m in extensions.extensions(ui)}
482 extensionmodulenames = {m.__name__ for n, m in extensions.extensions(ui)}
479
483
480 supportedrequirements = gathersupportedrequirements(ui)
484 supportedrequirements = gathersupportedrequirements(ui)
481
485
482 # We first validate the requirements are known.
486 # We first validate the requirements are known.
483 ensurerequirementsrecognized(requirements, supportedrequirements)
487 ensurerequirementsrecognized(requirements, supportedrequirements)
484
488
485 # Then we validate that the known set is reasonable to use together.
489 # Then we validate that the known set is reasonable to use together.
486 ensurerequirementscompatible(ui, requirements)
490 ensurerequirementscompatible(ui, requirements)
487
491
488 # TODO there are unhandled edge cases related to opening repositories with
492 # TODO there are unhandled edge cases related to opening repositories with
489 # shared storage. If storage is shared, we should also test for requirements
493 # shared storage. If storage is shared, we should also test for requirements
490 # compatibility in the pointed-to repo. This entails loading the .hg/hgrc in
494 # compatibility in the pointed-to repo. This entails loading the .hg/hgrc in
491 # that repo, as that repo may load extensions needed to open it. This is a
495 # that repo, as that repo may load extensions needed to open it. This is a
492 # bit complicated because we don't want the other hgrc to overwrite settings
496 # bit complicated because we don't want the other hgrc to overwrite settings
493 # in this hgrc.
497 # in this hgrc.
494 #
498 #
495 # This bug is somewhat mitigated by the fact that we copy the .hg/requires
499 # This bug is somewhat mitigated by the fact that we copy the .hg/requires
496 # file when sharing repos. But if a requirement is added after the share is
500 # file when sharing repos. But if a requirement is added after the share is
497 # performed, thereby introducing a new requirement for the opener, we may
501 # performed, thereby introducing a new requirement for the opener, we may
498 # will not see that and could encounter a run-time error interacting with
502 # will not see that and could encounter a run-time error interacting with
499 # that shared store since it has an unknown-to-us requirement.
503 # that shared store since it has an unknown-to-us requirement.
500
504
501 # At this point, we know we should be capable of opening the repository.
505 # At this point, we know we should be capable of opening the repository.
502 # Now get on with doing that.
506 # Now get on with doing that.
503
507
504 features = set()
508 features = set()
505
509
506 # The "store" part of the repository holds versioned data. How it is
510 # The "store" part of the repository holds versioned data. How it is
507 # accessed is determined by various requirements. The ``shared`` or
511 # accessed is determined by various requirements. The ``shared`` or
508 # ``relshared`` requirements indicate the store lives in the path contained
512 # ``relshared`` requirements indicate the store lives in the path contained
509 # in the ``.hg/sharedpath`` file. This is an absolute path for
513 # in the ``.hg/sharedpath`` file. This is an absolute path for
510 # ``shared`` and relative to ``.hg/`` for ``relshared``.
514 # ``shared`` and relative to ``.hg/`` for ``relshared``.
511 if b'shared' in requirements or b'relshared' in requirements:
515 if b'shared' in requirements or b'relshared' in requirements:
512 sharedpath = hgvfs.read(b'sharedpath').rstrip(b'\n')
516 sharedpath = hgvfs.read(b'sharedpath').rstrip(b'\n')
513 if b'relshared' in requirements:
517 if b'relshared' in requirements:
514 sharedpath = hgvfs.join(sharedpath)
518 sharedpath = hgvfs.join(sharedpath)
515
519
516 sharedvfs = vfsmod.vfs(sharedpath, realpath=True)
520 sharedvfs = vfsmod.vfs(sharedpath, realpath=True)
517
521
518 if not sharedvfs.exists():
522 if not sharedvfs.exists():
519 raise error.RepoError(_(b'.hg/sharedpath points to nonexistent '
523 raise error.RepoError(_(b'.hg/sharedpath points to nonexistent '
520 b'directory %s') % sharedvfs.base)
524 b'directory %s') % sharedvfs.base)
521
525
522 features.add(repository.REPO_FEATURE_SHARED_STORAGE)
526 features.add(repository.REPO_FEATURE_SHARED_STORAGE)
523
527
524 storebasepath = sharedvfs.base
528 storebasepath = sharedvfs.base
525 cachepath = sharedvfs.join(b'cache')
529 cachepath = sharedvfs.join(b'cache')
526 else:
530 else:
527 storebasepath = hgvfs.base
531 storebasepath = hgvfs.base
528 cachepath = hgvfs.join(b'cache')
532 cachepath = hgvfs.join(b'cache')
529 wcachepath = hgvfs.join(b'wcache')
533 wcachepath = hgvfs.join(b'wcache')
530
534
531
535
532 # The store has changed over time and the exact layout is dictated by
536 # The store has changed over time and the exact layout is dictated by
533 # requirements. The store interface abstracts differences across all
537 # requirements. The store interface abstracts differences across all
534 # of them.
538 # of them.
535 store = makestore(requirements, storebasepath,
539 store = makestore(requirements, storebasepath,
536 lambda base: vfsmod.vfs(base, cacheaudited=True))
540 lambda base: vfsmod.vfs(base, cacheaudited=True))
537 hgvfs.createmode = store.createmode
541 hgvfs.createmode = store.createmode
538
542
539 storevfs = store.vfs
543 storevfs = store.vfs
540 storevfs.options = resolvestorevfsoptions(ui, requirements, features)
544 storevfs.options = resolvestorevfsoptions(ui, requirements, features)
541
545
542 # The cache vfs is used to manage cache files.
546 # The cache vfs is used to manage cache files.
543 cachevfs = vfsmod.vfs(cachepath, cacheaudited=True)
547 cachevfs = vfsmod.vfs(cachepath, cacheaudited=True)
544 cachevfs.createmode = store.createmode
548 cachevfs.createmode = store.createmode
545 # The cache vfs is used to manage cache files related to the working copy
549 # The cache vfs is used to manage cache files related to the working copy
546 wcachevfs = vfsmod.vfs(wcachepath, cacheaudited=True)
550 wcachevfs = vfsmod.vfs(wcachepath, cacheaudited=True)
547 wcachevfs.createmode = store.createmode
551 wcachevfs.createmode = store.createmode
548
552
549 # Now resolve the type for the repository object. We do this by repeatedly
553 # Now resolve the type for the repository object. We do this by repeatedly
550 # calling a factory function to produces types for specific aspects of the
554 # calling a factory function to produces types for specific aspects of the
551 # repo's operation. The aggregate returned types are used as base classes
555 # repo's operation. The aggregate returned types are used as base classes
552 # for a dynamically-derived type, which will represent our new repository.
556 # for a dynamically-derived type, which will represent our new repository.
553
557
554 bases = []
558 bases = []
555 extrastate = {}
559 extrastate = {}
556
560
557 for iface, fn in REPO_INTERFACES:
561 for iface, fn in REPO_INTERFACES:
558 # We pass all potentially useful state to give extensions tons of
562 # We pass all potentially useful state to give extensions tons of
559 # flexibility.
563 # flexibility.
560 typ = fn()(ui=ui,
564 typ = fn()(ui=ui,
561 intents=intents,
565 intents=intents,
562 requirements=requirements,
566 requirements=requirements,
563 features=features,
567 features=features,
564 wdirvfs=wdirvfs,
568 wdirvfs=wdirvfs,
565 hgvfs=hgvfs,
569 hgvfs=hgvfs,
566 store=store,
570 store=store,
567 storevfs=storevfs,
571 storevfs=storevfs,
568 storeoptions=storevfs.options,
572 storeoptions=storevfs.options,
569 cachevfs=cachevfs,
573 cachevfs=cachevfs,
570 wcachevfs=wcachevfs,
574 wcachevfs=wcachevfs,
571 extensionmodulenames=extensionmodulenames,
575 extensionmodulenames=extensionmodulenames,
572 extrastate=extrastate,
576 extrastate=extrastate,
573 baseclasses=bases)
577 baseclasses=bases)
574
578
575 if not isinstance(typ, type):
579 if not isinstance(typ, type):
576 raise error.ProgrammingError('unable to construct type for %s' %
580 raise error.ProgrammingError('unable to construct type for %s' %
577 iface)
581 iface)
578
582
579 bases.append(typ)
583 bases.append(typ)
580
584
581 # type() allows you to use characters in type names that wouldn't be
585 # type() allows you to use characters in type names that wouldn't be
582 # recognized as Python symbols in source code. We abuse that to add
586 # recognized as Python symbols in source code. We abuse that to add
583 # rich information about our constructed repo.
587 # rich information about our constructed repo.
584 name = pycompat.sysstr(b'derivedrepo:%s<%s>' % (
588 name = pycompat.sysstr(b'derivedrepo:%s<%s>' % (
585 wdirvfs.base,
589 wdirvfs.base,
586 b','.join(sorted(requirements))))
590 b','.join(sorted(requirements))))
587
591
588 cls = type(name, tuple(bases), {})
592 cls = type(name, tuple(bases), {})
589
593
590 return cls(
594 return cls(
591 baseui=baseui,
595 baseui=baseui,
592 ui=ui,
596 ui=ui,
593 origroot=path,
597 origroot=path,
594 wdirvfs=wdirvfs,
598 wdirvfs=wdirvfs,
595 hgvfs=hgvfs,
599 hgvfs=hgvfs,
596 requirements=requirements,
600 requirements=requirements,
597 supportedrequirements=supportedrequirements,
601 supportedrequirements=supportedrequirements,
598 sharedpath=storebasepath,
602 sharedpath=storebasepath,
599 store=store,
603 store=store,
600 cachevfs=cachevfs,
604 cachevfs=cachevfs,
601 wcachevfs=wcachevfs,
605 wcachevfs=wcachevfs,
602 features=features,
606 features=features,
603 intents=intents)
607 intents=intents)
604
608
605 def loadhgrc(ui, wdirvfs, hgvfs, requirements):
609 def loadhgrc(ui, wdirvfs, hgvfs, requirements):
606 """Load hgrc files/content into a ui instance.
610 """Load hgrc files/content into a ui instance.
607
611
608 This is called during repository opening to load any additional
612 This is called during repository opening to load any additional
609 config files or settings relevant to the current repository.
613 config files or settings relevant to the current repository.
610
614
611 Returns a bool indicating whether any additional configs were loaded.
615 Returns a bool indicating whether any additional configs were loaded.
612
616
613 Extensions should monkeypatch this function to modify how per-repo
617 Extensions should monkeypatch this function to modify how per-repo
614 configs are loaded. For example, an extension may wish to pull in
618 configs are loaded. For example, an extension may wish to pull in
615 configs from alternate files or sources.
619 configs from alternate files or sources.
616 """
620 """
617 try:
621 try:
618 ui.readconfig(hgvfs.join(b'hgrc'), root=wdirvfs.base)
622 ui.readconfig(hgvfs.join(b'hgrc'), root=wdirvfs.base)
619 return True
623 return True
620 except IOError:
624 except IOError:
621 return False
625 return False
622
626
623 def afterhgrcload(ui, wdirvfs, hgvfs, requirements):
627 def afterhgrcload(ui, wdirvfs, hgvfs, requirements):
624 """Perform additional actions after .hg/hgrc is loaded.
628 """Perform additional actions after .hg/hgrc is loaded.
625
629
626 This function is called during repository loading immediately after
630 This function is called during repository loading immediately after
627 the .hg/hgrc file is loaded and before per-repo extensions are loaded.
631 the .hg/hgrc file is loaded and before per-repo extensions are loaded.
628
632
629 The function can be used to validate configs, automatically add
633 The function can be used to validate configs, automatically add
630 options (including extensions) based on requirements, etc.
634 options (including extensions) based on requirements, etc.
631 """
635 """
632
636
633 # Map of requirements to list of extensions to load automatically when
637 # Map of requirements to list of extensions to load automatically when
634 # requirement is present.
638 # requirement is present.
635 autoextensions = {
639 autoextensions = {
636 b'largefiles': [b'largefiles'],
640 b'largefiles': [b'largefiles'],
637 b'lfs': [b'lfs'],
641 b'lfs': [b'lfs'],
638 }
642 }
639
643
640 for requirement, names in sorted(autoextensions.items()):
644 for requirement, names in sorted(autoextensions.items()):
641 if requirement not in requirements:
645 if requirement not in requirements:
642 continue
646 continue
643
647
644 for name in names:
648 for name in names:
645 if not ui.hasconfig(b'extensions', name):
649 if not ui.hasconfig(b'extensions', name):
646 ui.setconfig(b'extensions', name, b'', source='autoload')
650 ui.setconfig(b'extensions', name, b'', source='autoload')
647
651
648 def gathersupportedrequirements(ui):
652 def gathersupportedrequirements(ui):
649 """Determine the complete set of recognized requirements."""
653 """Determine the complete set of recognized requirements."""
650 # Start with all requirements supported by this file.
654 # Start with all requirements supported by this file.
651 supported = set(localrepository._basesupported)
655 supported = set(localrepository._basesupported)
652
656
653 # Execute ``featuresetupfuncs`` entries if they belong to an extension
657 # Execute ``featuresetupfuncs`` entries if they belong to an extension
654 # relevant to this ui instance.
658 # relevant to this ui instance.
655 modules = {m.__name__ for n, m in extensions.extensions(ui)}
659 modules = {m.__name__ for n, m in extensions.extensions(ui)}
656
660
657 for fn in featuresetupfuncs:
661 for fn in featuresetupfuncs:
658 if fn.__module__ in modules:
662 if fn.__module__ in modules:
659 fn(ui, supported)
663 fn(ui, supported)
660
664
661 # Add derived requirements from registered compression engines.
665 # Add derived requirements from registered compression engines.
662 for name in util.compengines:
666 for name in util.compengines:
663 engine = util.compengines[name]
667 engine = util.compengines[name]
664 if engine.available() and engine.revlogheader():
668 if engine.available() and engine.revlogheader():
665 supported.add(b'exp-compression-%s' % name)
669 supported.add(b'exp-compression-%s' % name)
666 if engine.name() == 'zstd':
670 if engine.name() == 'zstd':
667 supported.add(b'revlog-compression-zstd')
671 supported.add(b'revlog-compression-zstd')
668
672
669 return supported
673 return supported
670
674
671 def ensurerequirementsrecognized(requirements, supported):
675 def ensurerequirementsrecognized(requirements, supported):
672 """Validate that a set of local requirements is recognized.
676 """Validate that a set of local requirements is recognized.
673
677
674 Receives a set of requirements. Raises an ``error.RepoError`` if there
678 Receives a set of requirements. Raises an ``error.RepoError`` if there
675 exists any requirement in that set that currently loaded code doesn't
679 exists any requirement in that set that currently loaded code doesn't
676 recognize.
680 recognize.
677
681
678 Returns a set of supported requirements.
682 Returns a set of supported requirements.
679 """
683 """
680 missing = set()
684 missing = set()
681
685
682 for requirement in requirements:
686 for requirement in requirements:
683 if requirement in supported:
687 if requirement in supported:
684 continue
688 continue
685
689
686 if not requirement or not requirement[0:1].isalnum():
690 if not requirement or not requirement[0:1].isalnum():
687 raise error.RequirementError(_(b'.hg/requires file is corrupt'))
691 raise error.RequirementError(_(b'.hg/requires file is corrupt'))
688
692
689 missing.add(requirement)
693 missing.add(requirement)
690
694
691 if missing:
695 if missing:
692 raise error.RequirementError(
696 raise error.RequirementError(
693 _(b'repository requires features unknown to this Mercurial: %s') %
697 _(b'repository requires features unknown to this Mercurial: %s') %
694 b' '.join(sorted(missing)),
698 b' '.join(sorted(missing)),
695 hint=_(b'see https://mercurial-scm.org/wiki/MissingRequirement '
699 hint=_(b'see https://mercurial-scm.org/wiki/MissingRequirement '
696 b'for more information'))
700 b'for more information'))
697
701
698 def ensurerequirementscompatible(ui, requirements):
702 def ensurerequirementscompatible(ui, requirements):
699 """Validates that a set of recognized requirements is mutually compatible.
703 """Validates that a set of recognized requirements is mutually compatible.
700
704
701 Some requirements may not be compatible with others or require
705 Some requirements may not be compatible with others or require
702 config options that aren't enabled. This function is called during
706 config options that aren't enabled. This function is called during
703 repository opening to ensure that the set of requirements needed
707 repository opening to ensure that the set of requirements needed
704 to open a repository is sane and compatible with config options.
708 to open a repository is sane and compatible with config options.
705
709
706 Extensions can monkeypatch this function to perform additional
710 Extensions can monkeypatch this function to perform additional
707 checking.
711 checking.
708
712
709 ``error.RepoError`` should be raised on failure.
713 ``error.RepoError`` should be raised on failure.
710 """
714 """
711 if b'exp-sparse' in requirements and not sparse.enabled:
715 if b'exp-sparse' in requirements and not sparse.enabled:
712 raise error.RepoError(_(b'repository is using sparse feature but '
716 raise error.RepoError(_(b'repository is using sparse feature but '
713 b'sparse is not enabled; enable the '
717 b'sparse is not enabled; enable the '
714 b'"sparse" extensions to access'))
718 b'"sparse" extensions to access'))
715
719
716 def makestore(requirements, path, vfstype):
720 def makestore(requirements, path, vfstype):
717 """Construct a storage object for a repository."""
721 """Construct a storage object for a repository."""
718 if b'store' in requirements:
722 if b'store' in requirements:
719 if b'fncache' in requirements:
723 if b'fncache' in requirements:
720 return storemod.fncachestore(path, vfstype,
724 return storemod.fncachestore(path, vfstype,
721 b'dotencode' in requirements)
725 b'dotencode' in requirements)
722
726
723 return storemod.encodedstore(path, vfstype)
727 return storemod.encodedstore(path, vfstype)
724
728
725 return storemod.basicstore(path, vfstype)
729 return storemod.basicstore(path, vfstype)
726
730
727 def resolvestorevfsoptions(ui, requirements, features):
731 def resolvestorevfsoptions(ui, requirements, features):
728 """Resolve the options to pass to the store vfs opener.
732 """Resolve the options to pass to the store vfs opener.
729
733
730 The returned dict is used to influence behavior of the storage layer.
734 The returned dict is used to influence behavior of the storage layer.
731 """
735 """
732 options = {}
736 options = {}
733
737
734 if b'treemanifest' in requirements:
738 if b'treemanifest' in requirements:
735 options[b'treemanifest'] = True
739 options[b'treemanifest'] = True
736
740
737 # experimental config: format.manifestcachesize
741 # experimental config: format.manifestcachesize
738 manifestcachesize = ui.configint(b'format', b'manifestcachesize')
742 manifestcachesize = ui.configint(b'format', b'manifestcachesize')
739 if manifestcachesize is not None:
743 if manifestcachesize is not None:
740 options[b'manifestcachesize'] = manifestcachesize
744 options[b'manifestcachesize'] = manifestcachesize
741
745
742 # In the absence of another requirement superseding a revlog-related
746 # In the absence of another requirement superseding a revlog-related
743 # requirement, we have to assume the repo is using revlog version 0.
747 # requirement, we have to assume the repo is using revlog version 0.
744 # This revlog format is super old and we don't bother trying to parse
748 # This revlog format is super old and we don't bother trying to parse
745 # opener options for it because those options wouldn't do anything
749 # opener options for it because those options wouldn't do anything
746 # meaningful on such old repos.
750 # meaningful on such old repos.
747 if b'revlogv1' in requirements or REVLOGV2_REQUIREMENT in requirements:
751 if b'revlogv1' in requirements or REVLOGV2_REQUIREMENT in requirements:
748 options.update(resolverevlogstorevfsoptions(ui, requirements, features))
752 options.update(resolverevlogstorevfsoptions(ui, requirements, features))
749
753
750 return options
754 return options
751
755
752 def resolverevlogstorevfsoptions(ui, requirements, features):
756 def resolverevlogstorevfsoptions(ui, requirements, features):
753 """Resolve opener options specific to revlogs."""
757 """Resolve opener options specific to revlogs."""
754
758
755 options = {}
759 options = {}
756 options[b'flagprocessors'] = {}
760 options[b'flagprocessors'] = {}
757
761
758 if b'revlogv1' in requirements:
762 if b'revlogv1' in requirements:
759 options[b'revlogv1'] = True
763 options[b'revlogv1'] = True
760 if REVLOGV2_REQUIREMENT in requirements:
764 if REVLOGV2_REQUIREMENT in requirements:
761 options[b'revlogv2'] = True
765 options[b'revlogv2'] = True
762
766
763 if b'generaldelta' in requirements:
767 if b'generaldelta' in requirements:
764 options[b'generaldelta'] = True
768 options[b'generaldelta'] = True
765
769
766 # experimental config: format.chunkcachesize
770 # experimental config: format.chunkcachesize
767 chunkcachesize = ui.configint(b'format', b'chunkcachesize')
771 chunkcachesize = ui.configint(b'format', b'chunkcachesize')
768 if chunkcachesize is not None:
772 if chunkcachesize is not None:
769 options[b'chunkcachesize'] = chunkcachesize
773 options[b'chunkcachesize'] = chunkcachesize
770
774
771 deltabothparents = ui.configbool(b'storage',
775 deltabothparents = ui.configbool(b'storage',
772 b'revlog.optimize-delta-parent-choice')
776 b'revlog.optimize-delta-parent-choice')
773 options[b'deltabothparents'] = deltabothparents
777 options[b'deltabothparents'] = deltabothparents
774
778
775 lazydelta = ui.configbool(b'storage', b'revlog.reuse-external-delta')
779 lazydelta = ui.configbool(b'storage', b'revlog.reuse-external-delta')
776 lazydeltabase = False
780 lazydeltabase = False
777 if lazydelta:
781 if lazydelta:
778 lazydeltabase = ui.configbool(b'storage',
782 lazydeltabase = ui.configbool(b'storage',
779 b'revlog.reuse-external-delta-parent')
783 b'revlog.reuse-external-delta-parent')
780 if lazydeltabase is None:
784 if lazydeltabase is None:
781 lazydeltabase = not scmutil.gddeltaconfig(ui)
785 lazydeltabase = not scmutil.gddeltaconfig(ui)
782 options[b'lazydelta'] = lazydelta
786 options[b'lazydelta'] = lazydelta
783 options[b'lazydeltabase'] = lazydeltabase
787 options[b'lazydeltabase'] = lazydeltabase
784
788
785 chainspan = ui.configbytes(b'experimental', b'maxdeltachainspan')
789 chainspan = ui.configbytes(b'experimental', b'maxdeltachainspan')
786 if 0 <= chainspan:
790 if 0 <= chainspan:
787 options[b'maxdeltachainspan'] = chainspan
791 options[b'maxdeltachainspan'] = chainspan
788
792
789 mmapindexthreshold = ui.configbytes(b'experimental',
793 mmapindexthreshold = ui.configbytes(b'experimental',
790 b'mmapindexthreshold')
794 b'mmapindexthreshold')
791 if mmapindexthreshold is not None:
795 if mmapindexthreshold is not None:
792 options[b'mmapindexthreshold'] = mmapindexthreshold
796 options[b'mmapindexthreshold'] = mmapindexthreshold
793
797
794 withsparseread = ui.configbool(b'experimental', b'sparse-read')
798 withsparseread = ui.configbool(b'experimental', b'sparse-read')
795 srdensitythres = float(ui.config(b'experimental',
799 srdensitythres = float(ui.config(b'experimental',
796 b'sparse-read.density-threshold'))
800 b'sparse-read.density-threshold'))
797 srmingapsize = ui.configbytes(b'experimental',
801 srmingapsize = ui.configbytes(b'experimental',
798 b'sparse-read.min-gap-size')
802 b'sparse-read.min-gap-size')
799 options[b'with-sparse-read'] = withsparseread
803 options[b'with-sparse-read'] = withsparseread
800 options[b'sparse-read-density-threshold'] = srdensitythres
804 options[b'sparse-read-density-threshold'] = srdensitythres
801 options[b'sparse-read-min-gap-size'] = srmingapsize
805 options[b'sparse-read-min-gap-size'] = srmingapsize
802
806
803 sparserevlog = SPARSEREVLOG_REQUIREMENT in requirements
807 sparserevlog = SPARSEREVLOG_REQUIREMENT in requirements
804 options[b'sparse-revlog'] = sparserevlog
808 options[b'sparse-revlog'] = sparserevlog
805 if sparserevlog:
809 if sparserevlog:
806 options[b'generaldelta'] = True
810 options[b'generaldelta'] = True
807
811
808 maxchainlen = None
812 maxchainlen = None
809 if sparserevlog:
813 if sparserevlog:
810 maxchainlen = revlogconst.SPARSE_REVLOG_MAX_CHAIN_LENGTH
814 maxchainlen = revlogconst.SPARSE_REVLOG_MAX_CHAIN_LENGTH
811 # experimental config: format.maxchainlen
815 # experimental config: format.maxchainlen
812 maxchainlen = ui.configint(b'format', b'maxchainlen', maxchainlen)
816 maxchainlen = ui.configint(b'format', b'maxchainlen', maxchainlen)
813 if maxchainlen is not None:
817 if maxchainlen is not None:
814 options[b'maxchainlen'] = maxchainlen
818 options[b'maxchainlen'] = maxchainlen
815
819
816 for r in requirements:
820 for r in requirements:
817 # we allow multiple compression engine requirement to co-exist because
821 # we allow multiple compression engine requirement to co-exist because
818 # strickly speaking, revlog seems to support mixed compression style.
822 # strickly speaking, revlog seems to support mixed compression style.
819 #
823 #
820 # The compression used for new entries will be "the last one"
824 # The compression used for new entries will be "the last one"
821 prefix = r.startswith
825 prefix = r.startswith
822 if prefix('revlog-compression-') or prefix('exp-compression-'):
826 if prefix('revlog-compression-') or prefix('exp-compression-'):
823 options[b'compengine'] = r.split('-', 2)[2]
827 options[b'compengine'] = r.split('-', 2)[2]
824
828
825 options[b'zlib.level'] = ui.configint(b'storage', b'revlog.zlib.level')
829 options[b'zlib.level'] = ui.configint(b'storage', b'revlog.zlib.level')
826 if options[b'zlib.level'] is not None:
830 if options[b'zlib.level'] is not None:
827 if not (0 <= options[b'zlib.level'] <= 9):
831 if not (0 <= options[b'zlib.level'] <= 9):
828 msg = _('invalid value for `storage.revlog.zlib.level` config: %d')
832 msg = _('invalid value for `storage.revlog.zlib.level` config: %d')
829 raise error.Abort(msg % options[b'zlib.level'])
833 raise error.Abort(msg % options[b'zlib.level'])
830 options[b'zstd.level'] = ui.configint(b'storage', b'revlog.zstd.level')
834 options[b'zstd.level'] = ui.configint(b'storage', b'revlog.zstd.level')
831 if options[b'zstd.level'] is not None:
835 if options[b'zstd.level'] is not None:
832 if not (0 <= options[b'zstd.level'] <= 22):
836 if not (0 <= options[b'zstd.level'] <= 22):
833 msg = _('invalid value for `storage.revlog.zstd.level` config: %d')
837 msg = _('invalid value for `storage.revlog.zstd.level` config: %d')
834 raise error.Abort(msg % options[b'zstd.level'])
838 raise error.Abort(msg % options[b'zstd.level'])
835
839
836 if repository.NARROW_REQUIREMENT in requirements:
840 if repository.NARROW_REQUIREMENT in requirements:
837 options[b'enableellipsis'] = True
841 options[b'enableellipsis'] = True
838
842
839 return options
843 return options
840
844
841 def makemain(**kwargs):
845 def makemain(**kwargs):
842 """Produce a type conforming to ``ilocalrepositorymain``."""
846 """Produce a type conforming to ``ilocalrepositorymain``."""
843 return localrepository
847 return localrepository
844
848
845 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
849 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
846 class revlogfilestorage(object):
850 class revlogfilestorage(object):
847 """File storage when using revlogs."""
851 """File storage when using revlogs."""
848
852
849 def file(self, path):
853 def file(self, path):
850 if path[0] == b'/':
854 if path[0] == b'/':
851 path = path[1:]
855 path = path[1:]
852
856
853 return filelog.filelog(self.svfs, path)
857 return filelog.filelog(self.svfs, path)
854
858
855 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
859 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
856 class revlognarrowfilestorage(object):
860 class revlognarrowfilestorage(object):
857 """File storage when using revlogs and narrow files."""
861 """File storage when using revlogs and narrow files."""
858
862
859 def file(self, path):
863 def file(self, path):
860 if path[0] == b'/':
864 if path[0] == b'/':
861 path = path[1:]
865 path = path[1:]
862
866
863 return filelog.narrowfilelog(self.svfs, path, self._storenarrowmatch)
867 return filelog.narrowfilelog(self.svfs, path, self._storenarrowmatch)
864
868
865 def makefilestorage(requirements, features, **kwargs):
869 def makefilestorage(requirements, features, **kwargs):
866 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
870 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
867 features.add(repository.REPO_FEATURE_REVLOG_FILE_STORAGE)
871 features.add(repository.REPO_FEATURE_REVLOG_FILE_STORAGE)
868 features.add(repository.REPO_FEATURE_STREAM_CLONE)
872 features.add(repository.REPO_FEATURE_STREAM_CLONE)
869
873
870 if repository.NARROW_REQUIREMENT in requirements:
874 if repository.NARROW_REQUIREMENT in requirements:
871 return revlognarrowfilestorage
875 return revlognarrowfilestorage
872 else:
876 else:
873 return revlogfilestorage
877 return revlogfilestorage
874
878
875 # List of repository interfaces and factory functions for them. Each
879 # List of repository interfaces and factory functions for them. Each
876 # will be called in order during ``makelocalrepository()`` to iteratively
880 # will be called in order during ``makelocalrepository()`` to iteratively
877 # derive the final type for a local repository instance. We capture the
881 # derive the final type for a local repository instance. We capture the
878 # function as a lambda so we don't hold a reference and the module-level
882 # function as a lambda so we don't hold a reference and the module-level
879 # functions can be wrapped.
883 # functions can be wrapped.
880 REPO_INTERFACES = [
884 REPO_INTERFACES = [
881 (repository.ilocalrepositorymain, lambda: makemain),
885 (repository.ilocalrepositorymain, lambda: makemain),
882 (repository.ilocalrepositoryfilestorage, lambda: makefilestorage),
886 (repository.ilocalrepositoryfilestorage, lambda: makefilestorage),
883 ]
887 ]
884
888
885 @interfaceutil.implementer(repository.ilocalrepositorymain)
889 @interfaceutil.implementer(repository.ilocalrepositorymain)
886 class localrepository(object):
890 class localrepository(object):
887 """Main class for representing local repositories.
891 """Main class for representing local repositories.
888
892
889 All local repositories are instances of this class.
893 All local repositories are instances of this class.
890
894
891 Constructed on its own, instances of this class are not usable as
895 Constructed on its own, instances of this class are not usable as
892 repository objects. To obtain a usable repository object, call
896 repository objects. To obtain a usable repository object, call
893 ``hg.repository()``, ``localrepo.instance()``, or
897 ``hg.repository()``, ``localrepo.instance()``, or
894 ``localrepo.makelocalrepository()``. The latter is the lowest-level.
898 ``localrepo.makelocalrepository()``. The latter is the lowest-level.
895 ``instance()`` adds support for creating new repositories.
899 ``instance()`` adds support for creating new repositories.
896 ``hg.repository()`` adds more extension integration, including calling
900 ``hg.repository()`` adds more extension integration, including calling
897 ``reposetup()``. Generally speaking, ``hg.repository()`` should be
901 ``reposetup()``. Generally speaking, ``hg.repository()`` should be
898 used.
902 used.
899 """
903 """
900
904
901 # obsolete experimental requirements:
905 # obsolete experimental requirements:
902 # - manifestv2: An experimental new manifest format that allowed
906 # - manifestv2: An experimental new manifest format that allowed
903 # for stem compression of long paths. Experiment ended up not
907 # for stem compression of long paths. Experiment ended up not
904 # being successful (repository sizes went up due to worse delta
908 # being successful (repository sizes went up due to worse delta
905 # chains), and the code was deleted in 4.6.
909 # chains), and the code was deleted in 4.6.
906 supportedformats = {
910 supportedformats = {
907 'revlogv1',
911 'revlogv1',
908 'generaldelta',
912 'generaldelta',
909 'treemanifest',
913 'treemanifest',
910 REVLOGV2_REQUIREMENT,
914 REVLOGV2_REQUIREMENT,
911 SPARSEREVLOG_REQUIREMENT,
915 SPARSEREVLOG_REQUIREMENT,
912 bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT,
916 bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT,
913 }
917 }
914 _basesupported = supportedformats | {
918 _basesupported = supportedformats | {
915 'store',
919 'store',
916 'fncache',
920 'fncache',
917 'shared',
921 'shared',
918 'relshared',
922 'relshared',
919 'dotencode',
923 'dotencode',
920 'exp-sparse',
924 'exp-sparse',
921 'internal-phase'
925 'internal-phase'
922 }
926 }
923
927
924 # list of prefix for file which can be written without 'wlock'
928 # list of prefix for file which can be written without 'wlock'
925 # Extensions should extend this list when needed
929 # Extensions should extend this list when needed
926 _wlockfreeprefix = {
930 _wlockfreeprefix = {
927 # We migh consider requiring 'wlock' for the next
931 # We migh consider requiring 'wlock' for the next
928 # two, but pretty much all the existing code assume
932 # two, but pretty much all the existing code assume
929 # wlock is not needed so we keep them excluded for
933 # wlock is not needed so we keep them excluded for
930 # now.
934 # now.
931 'hgrc',
935 'hgrc',
932 'requires',
936 'requires',
933 # XXX cache is a complicatged business someone
937 # XXX cache is a complicatged business someone
934 # should investigate this in depth at some point
938 # should investigate this in depth at some point
935 'cache/',
939 'cache/',
936 # XXX shouldn't be dirstate covered by the wlock?
940 # XXX shouldn't be dirstate covered by the wlock?
937 'dirstate',
941 'dirstate',
938 # XXX bisect was still a bit too messy at the time
942 # XXX bisect was still a bit too messy at the time
939 # this changeset was introduced. Someone should fix
943 # this changeset was introduced. Someone should fix
940 # the remainig bit and drop this line
944 # the remainig bit and drop this line
941 'bisect.state',
945 'bisect.state',
942 }
946 }
943
947
944 def __init__(self, baseui, ui, origroot, wdirvfs, hgvfs, requirements,
948 def __init__(self, baseui, ui, origroot, wdirvfs, hgvfs, requirements,
945 supportedrequirements, sharedpath, store, cachevfs, wcachevfs,
949 supportedrequirements, sharedpath, store, cachevfs, wcachevfs,
946 features, intents=None):
950 features, intents=None):
947 """Create a new local repository instance.
951 """Create a new local repository instance.
948
952
949 Most callers should use ``hg.repository()``, ``localrepo.instance()``,
953 Most callers should use ``hg.repository()``, ``localrepo.instance()``,
950 or ``localrepo.makelocalrepository()`` for obtaining a new repository
954 or ``localrepo.makelocalrepository()`` for obtaining a new repository
951 object.
955 object.
952
956
953 Arguments:
957 Arguments:
954
958
955 baseui
959 baseui
956 ``ui.ui`` instance that ``ui`` argument was based off of.
960 ``ui.ui`` instance that ``ui`` argument was based off of.
957
961
958 ui
962 ui
959 ``ui.ui`` instance for use by the repository.
963 ``ui.ui`` instance for use by the repository.
960
964
961 origroot
965 origroot
962 ``bytes`` path to working directory root of this repository.
966 ``bytes`` path to working directory root of this repository.
963
967
964 wdirvfs
968 wdirvfs
965 ``vfs.vfs`` rooted at the working directory.
969 ``vfs.vfs`` rooted at the working directory.
966
970
967 hgvfs
971 hgvfs
968 ``vfs.vfs`` rooted at .hg/
972 ``vfs.vfs`` rooted at .hg/
969
973
970 requirements
974 requirements
971 ``set`` of bytestrings representing repository opening requirements.
975 ``set`` of bytestrings representing repository opening requirements.
972
976
973 supportedrequirements
977 supportedrequirements
974 ``set`` of bytestrings representing repository requirements that we
978 ``set`` of bytestrings representing repository requirements that we
975 know how to open. May be a supetset of ``requirements``.
979 know how to open. May be a supetset of ``requirements``.
976
980
977 sharedpath
981 sharedpath
978 ``bytes`` Defining path to storage base directory. Points to a
982 ``bytes`` Defining path to storage base directory. Points to a
979 ``.hg/`` directory somewhere.
983 ``.hg/`` directory somewhere.
980
984
981 store
985 store
982 ``store.basicstore`` (or derived) instance providing access to
986 ``store.basicstore`` (or derived) instance providing access to
983 versioned storage.
987 versioned storage.
984
988
985 cachevfs
989 cachevfs
986 ``vfs.vfs`` used for cache files.
990 ``vfs.vfs`` used for cache files.
987
991
988 wcachevfs
992 wcachevfs
989 ``vfs.vfs`` used for cache files related to the working copy.
993 ``vfs.vfs`` used for cache files related to the working copy.
990
994
991 features
995 features
992 ``set`` of bytestrings defining features/capabilities of this
996 ``set`` of bytestrings defining features/capabilities of this
993 instance.
997 instance.
994
998
995 intents
999 intents
996 ``set`` of system strings indicating what this repo will be used
1000 ``set`` of system strings indicating what this repo will be used
997 for.
1001 for.
998 """
1002 """
999 self.baseui = baseui
1003 self.baseui = baseui
1000 self.ui = ui
1004 self.ui = ui
1001 self.origroot = origroot
1005 self.origroot = origroot
1002 # vfs rooted at working directory.
1006 # vfs rooted at working directory.
1003 self.wvfs = wdirvfs
1007 self.wvfs = wdirvfs
1004 self.root = wdirvfs.base
1008 self.root = wdirvfs.base
1005 # vfs rooted at .hg/. Used to access most non-store paths.
1009 # vfs rooted at .hg/. Used to access most non-store paths.
1006 self.vfs = hgvfs
1010 self.vfs = hgvfs
1007 self.path = hgvfs.base
1011 self.path = hgvfs.base
1008 self.requirements = requirements
1012 self.requirements = requirements
1009 self.supported = supportedrequirements
1013 self.supported = supportedrequirements
1010 self.sharedpath = sharedpath
1014 self.sharedpath = sharedpath
1011 self.store = store
1015 self.store = store
1012 self.cachevfs = cachevfs
1016 self.cachevfs = cachevfs
1013 self.wcachevfs = wcachevfs
1017 self.wcachevfs = wcachevfs
1014 self.features = features
1018 self.features = features
1015
1019
1016 self.filtername = None
1020 self.filtername = None
1017
1021
1018 if (self.ui.configbool('devel', 'all-warnings') or
1022 if (self.ui.configbool('devel', 'all-warnings') or
1019 self.ui.configbool('devel', 'check-locks')):
1023 self.ui.configbool('devel', 'check-locks')):
1020 self.vfs.audit = self._getvfsward(self.vfs.audit)
1024 self.vfs.audit = self._getvfsward(self.vfs.audit)
1021 # A list of callback to shape the phase if no data were found.
1025 # A list of callback to shape the phase if no data were found.
1022 # Callback are in the form: func(repo, roots) --> processed root.
1026 # Callback are in the form: func(repo, roots) --> processed root.
1023 # This list it to be filled by extension during repo setup
1027 # This list it to be filled by extension during repo setup
1024 self._phasedefaults = []
1028 self._phasedefaults = []
1025
1029
1026 color.setup(self.ui)
1030 color.setup(self.ui)
1027
1031
1028 self.spath = self.store.path
1032 self.spath = self.store.path
1029 self.svfs = self.store.vfs
1033 self.svfs = self.store.vfs
1030 self.sjoin = self.store.join
1034 self.sjoin = self.store.join
1031 if (self.ui.configbool('devel', 'all-warnings') or
1035 if (self.ui.configbool('devel', 'all-warnings') or
1032 self.ui.configbool('devel', 'check-locks')):
1036 self.ui.configbool('devel', 'check-locks')):
1033 if util.safehasattr(self.svfs, 'vfs'): # this is filtervfs
1037 if util.safehasattr(self.svfs, 'vfs'): # this is filtervfs
1034 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
1038 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
1035 else: # standard vfs
1039 else: # standard vfs
1036 self.svfs.audit = self._getsvfsward(self.svfs.audit)
1040 self.svfs.audit = self._getsvfsward(self.svfs.audit)
1037
1041
1038 self._dirstatevalidatewarned = False
1042 self._dirstatevalidatewarned = False
1039
1043
1040 self._branchcaches = branchmap.BranchMapCache()
1044 self._branchcaches = branchmap.BranchMapCache()
1041 self._revbranchcache = None
1045 self._revbranchcache = None
1042 self._filterpats = {}
1046 self._filterpats = {}
1043 self._datafilters = {}
1047 self._datafilters = {}
1044 self._transref = self._lockref = self._wlockref = None
1048 self._transref = self._lockref = self._wlockref = None
1045
1049
1046 # A cache for various files under .hg/ that tracks file changes,
1050 # A cache for various files under .hg/ that tracks file changes,
1047 # (used by the filecache decorator)
1051 # (used by the filecache decorator)
1048 #
1052 #
1049 # Maps a property name to its util.filecacheentry
1053 # Maps a property name to its util.filecacheentry
1050 self._filecache = {}
1054 self._filecache = {}
1051
1055
1052 # hold sets of revision to be filtered
1056 # hold sets of revision to be filtered
1053 # should be cleared when something might have changed the filter value:
1057 # should be cleared when something might have changed the filter value:
1054 # - new changesets,
1058 # - new changesets,
1055 # - phase change,
1059 # - phase change,
1056 # - new obsolescence marker,
1060 # - new obsolescence marker,
1057 # - working directory parent change,
1061 # - working directory parent change,
1058 # - bookmark changes
1062 # - bookmark changes
1059 self.filteredrevcache = {}
1063 self.filteredrevcache = {}
1060
1064
1061 # post-dirstate-status hooks
1065 # post-dirstate-status hooks
1062 self._postdsstatus = []
1066 self._postdsstatus = []
1063
1067
1064 # generic mapping between names and nodes
1068 # generic mapping between names and nodes
1065 self.names = namespaces.namespaces()
1069 self.names = namespaces.namespaces()
1066
1070
1067 # Key to signature value.
1071 # Key to signature value.
1068 self._sparsesignaturecache = {}
1072 self._sparsesignaturecache = {}
1069 # Signature to cached matcher instance.
1073 # Signature to cached matcher instance.
1070 self._sparsematchercache = {}
1074 self._sparsematchercache = {}
1071
1075
1072 self._extrafilterid = repoview.extrafilter(ui)
1076 self._extrafilterid = repoview.extrafilter(ui)
1073
1077
1074 def _getvfsward(self, origfunc):
1078 def _getvfsward(self, origfunc):
1075 """build a ward for self.vfs"""
1079 """build a ward for self.vfs"""
1076 rref = weakref.ref(self)
1080 rref = weakref.ref(self)
1077 def checkvfs(path, mode=None):
1081 def checkvfs(path, mode=None):
1078 ret = origfunc(path, mode=mode)
1082 ret = origfunc(path, mode=mode)
1079 repo = rref()
1083 repo = rref()
1080 if (repo is None
1084 if (repo is None
1081 or not util.safehasattr(repo, '_wlockref')
1085 or not util.safehasattr(repo, '_wlockref')
1082 or not util.safehasattr(repo, '_lockref')):
1086 or not util.safehasattr(repo, '_lockref')):
1083 return
1087 return
1084 if mode in (None, 'r', 'rb'):
1088 if mode in (None, 'r', 'rb'):
1085 return
1089 return
1086 if path.startswith(repo.path):
1090 if path.startswith(repo.path):
1087 # truncate name relative to the repository (.hg)
1091 # truncate name relative to the repository (.hg)
1088 path = path[len(repo.path) + 1:]
1092 path = path[len(repo.path) + 1:]
1089 if path.startswith('cache/'):
1093 if path.startswith('cache/'):
1090 msg = 'accessing cache with vfs instead of cachevfs: "%s"'
1094 msg = 'accessing cache with vfs instead of cachevfs: "%s"'
1091 repo.ui.develwarn(msg % path, stacklevel=3, config="cache-vfs")
1095 repo.ui.develwarn(msg % path, stacklevel=3, config="cache-vfs")
1092 if path.startswith('journal.') or path.startswith('undo.'):
1096 if path.startswith('journal.') or path.startswith('undo.'):
1093 # journal is covered by 'lock'
1097 # journal is covered by 'lock'
1094 if repo._currentlock(repo._lockref) is None:
1098 if repo._currentlock(repo._lockref) is None:
1095 repo.ui.develwarn('write with no lock: "%s"' % path,
1099 repo.ui.develwarn('write with no lock: "%s"' % path,
1096 stacklevel=3, config='check-locks')
1100 stacklevel=3, config='check-locks')
1097 elif repo._currentlock(repo._wlockref) is None:
1101 elif repo._currentlock(repo._wlockref) is None:
1098 # rest of vfs files are covered by 'wlock'
1102 # rest of vfs files are covered by 'wlock'
1099 #
1103 #
1100 # exclude special files
1104 # exclude special files
1101 for prefix in self._wlockfreeprefix:
1105 for prefix in self._wlockfreeprefix:
1102 if path.startswith(prefix):
1106 if path.startswith(prefix):
1103 return
1107 return
1104 repo.ui.develwarn('write with no wlock: "%s"' % path,
1108 repo.ui.develwarn('write with no wlock: "%s"' % path,
1105 stacklevel=3, config='check-locks')
1109 stacklevel=3, config='check-locks')
1106 return ret
1110 return ret
1107 return checkvfs
1111 return checkvfs
1108
1112
1109 def _getsvfsward(self, origfunc):
1113 def _getsvfsward(self, origfunc):
1110 """build a ward for self.svfs"""
1114 """build a ward for self.svfs"""
1111 rref = weakref.ref(self)
1115 rref = weakref.ref(self)
1112 def checksvfs(path, mode=None):
1116 def checksvfs(path, mode=None):
1113 ret = origfunc(path, mode=mode)
1117 ret = origfunc(path, mode=mode)
1114 repo = rref()
1118 repo = rref()
1115 if repo is None or not util.safehasattr(repo, '_lockref'):
1119 if repo is None or not util.safehasattr(repo, '_lockref'):
1116 return
1120 return
1117 if mode in (None, 'r', 'rb'):
1121 if mode in (None, 'r', 'rb'):
1118 return
1122 return
1119 if path.startswith(repo.sharedpath):
1123 if path.startswith(repo.sharedpath):
1120 # truncate name relative to the repository (.hg)
1124 # truncate name relative to the repository (.hg)
1121 path = path[len(repo.sharedpath) + 1:]
1125 path = path[len(repo.sharedpath) + 1:]
1122 if repo._currentlock(repo._lockref) is None:
1126 if repo._currentlock(repo._lockref) is None:
1123 repo.ui.develwarn('write with no lock: "%s"' % path,
1127 repo.ui.develwarn('write with no lock: "%s"' % path,
1124 stacklevel=4)
1128 stacklevel=4)
1125 return ret
1129 return ret
1126 return checksvfs
1130 return checksvfs
1127
1131
1128 def close(self):
1132 def close(self):
1129 self._writecaches()
1133 self._writecaches()
1130
1134
1131 def _writecaches(self):
1135 def _writecaches(self):
1132 if self._revbranchcache:
1136 if self._revbranchcache:
1133 self._revbranchcache.write()
1137 self._revbranchcache.write()
1134
1138
1135 def _restrictcapabilities(self, caps):
1139 def _restrictcapabilities(self, caps):
1136 if self.ui.configbool('experimental', 'bundle2-advertise'):
1140 if self.ui.configbool('experimental', 'bundle2-advertise'):
1137 caps = set(caps)
1141 caps = set(caps)
1138 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self,
1142 capsblob = bundle2.encodecaps(bundle2.getrepocaps(self,
1139 role='client'))
1143 role='client'))
1140 caps.add('bundle2=' + urlreq.quote(capsblob))
1144 caps.add('bundle2=' + urlreq.quote(capsblob))
1141 return caps
1145 return caps
1142
1146
1143 def _writerequirements(self):
1147 def _writerequirements(self):
1144 scmutil.writerequires(self.vfs, self.requirements)
1148 scmutil.writerequires(self.vfs, self.requirements)
1145
1149
1146 # Don't cache auditor/nofsauditor, or you'll end up with reference cycle:
1150 # Don't cache auditor/nofsauditor, or you'll end up with reference cycle:
1147 # self -> auditor -> self._checknested -> self
1151 # self -> auditor -> self._checknested -> self
1148
1152
1149 @property
1153 @property
1150 def auditor(self):
1154 def auditor(self):
1151 # This is only used by context.workingctx.match in order to
1155 # This is only used by context.workingctx.match in order to
1152 # detect files in subrepos.
1156 # detect files in subrepos.
1153 return pathutil.pathauditor(self.root, callback=self._checknested)
1157 return pathutil.pathauditor(self.root, callback=self._checknested)
1154
1158
1155 @property
1159 @property
1156 def nofsauditor(self):
1160 def nofsauditor(self):
1157 # This is only used by context.basectx.match in order to detect
1161 # This is only used by context.basectx.match in order to detect
1158 # files in subrepos.
1162 # files in subrepos.
1159 return pathutil.pathauditor(self.root, callback=self._checknested,
1163 return pathutil.pathauditor(self.root, callback=self._checknested,
1160 realfs=False, cached=True)
1164 realfs=False, cached=True)
1161
1165
1162 def _checknested(self, path):
1166 def _checknested(self, path):
1163 """Determine if path is a legal nested repository."""
1167 """Determine if path is a legal nested repository."""
1164 if not path.startswith(self.root):
1168 if not path.startswith(self.root):
1165 return False
1169 return False
1166 subpath = path[len(self.root) + 1:]
1170 subpath = path[len(self.root) + 1:]
1167 normsubpath = util.pconvert(subpath)
1171 normsubpath = util.pconvert(subpath)
1168
1172
1169 # XXX: Checking against the current working copy is wrong in
1173 # XXX: Checking against the current working copy is wrong in
1170 # the sense that it can reject things like
1174 # the sense that it can reject things like
1171 #
1175 #
1172 # $ hg cat -r 10 sub/x.txt
1176 # $ hg cat -r 10 sub/x.txt
1173 #
1177 #
1174 # if sub/ is no longer a subrepository in the working copy
1178 # if sub/ is no longer a subrepository in the working copy
1175 # parent revision.
1179 # parent revision.
1176 #
1180 #
1177 # However, it can of course also allow things that would have
1181 # However, it can of course also allow things that would have
1178 # been rejected before, such as the above cat command if sub/
1182 # been rejected before, such as the above cat command if sub/
1179 # is a subrepository now, but was a normal directory before.
1183 # is a subrepository now, but was a normal directory before.
1180 # The old path auditor would have rejected by mistake since it
1184 # The old path auditor would have rejected by mistake since it
1181 # panics when it sees sub/.hg/.
1185 # panics when it sees sub/.hg/.
1182 #
1186 #
1183 # All in all, checking against the working copy seems sensible
1187 # All in all, checking against the working copy seems sensible
1184 # since we want to prevent access to nested repositories on
1188 # since we want to prevent access to nested repositories on
1185 # the filesystem *now*.
1189 # the filesystem *now*.
1186 ctx = self[None]
1190 ctx = self[None]
1187 parts = util.splitpath(subpath)
1191 parts = util.splitpath(subpath)
1188 while parts:
1192 while parts:
1189 prefix = '/'.join(parts)
1193 prefix = '/'.join(parts)
1190 if prefix in ctx.substate:
1194 if prefix in ctx.substate:
1191 if prefix == normsubpath:
1195 if prefix == normsubpath:
1192 return True
1196 return True
1193 else:
1197 else:
1194 sub = ctx.sub(prefix)
1198 sub = ctx.sub(prefix)
1195 return sub.checknested(subpath[len(prefix) + 1:])
1199 return sub.checknested(subpath[len(prefix) + 1:])
1196 else:
1200 else:
1197 parts.pop()
1201 parts.pop()
1198 return False
1202 return False
1199
1203
1200 def peer(self):
1204 def peer(self):
1201 return localpeer(self) # not cached to avoid reference cycle
1205 return localpeer(self) # not cached to avoid reference cycle
1202
1206
1203 def unfiltered(self):
1207 def unfiltered(self):
1204 """Return unfiltered version of the repository
1208 """Return unfiltered version of the repository
1205
1209
1206 Intended to be overwritten by filtered repo."""
1210 Intended to be overwritten by filtered repo."""
1207 return self
1211 return self
1208
1212
1209 def filtered(self, name, visibilityexceptions=None):
1213 def filtered(self, name, visibilityexceptions=None):
1210 """Return a filtered version of a repository
1214 """Return a filtered version of a repository
1211
1215
1212 The `name` parameter is the identifier of the requested view. This
1216 The `name` parameter is the identifier of the requested view. This
1213 will return a repoview object set "exactly" to the specified view.
1217 will return a repoview object set "exactly" to the specified view.
1214
1218
1215 This function does not apply recursive filtering to a repository. For
1219 This function does not apply recursive filtering to a repository. For
1216 example calling `repo.filtered("served")` will return a repoview using
1220 example calling `repo.filtered("served")` will return a repoview using
1217 the "served" view, regardless of the initial view used by `repo`.
1221 the "served" view, regardless of the initial view used by `repo`.
1218
1222
1219 In other word, there is always only one level of `repoview` "filtering".
1223 In other word, there is always only one level of `repoview` "filtering".
1220 """
1224 """
1221 if self._extrafilterid is not None and '%' not in name:
1225 if self._extrafilterid is not None and '%' not in name:
1222 name = name + '%' + self._extrafilterid
1226 name = name + '%' + self._extrafilterid
1223
1227
1224 cls = repoview.newtype(self.unfiltered().__class__)
1228 cls = repoview.newtype(self.unfiltered().__class__)
1225 return cls(self, name, visibilityexceptions)
1229 return cls(self, name, visibilityexceptions)
1226
1230
1227 @mixedrepostorecache(('bookmarks', 'plain'), ('bookmarks.current', 'plain'),
1231 @mixedrepostorecache(('bookmarks', 'plain'), ('bookmarks.current', 'plain'),
1228 ('bookmarks', ''), ('00changelog.i', ''))
1232 ('bookmarks', ''), ('00changelog.i', ''))
1229 def _bookmarks(self):
1233 def _bookmarks(self):
1230 # Since the multiple files involved in the transaction cannot be
1234 # Since the multiple files involved in the transaction cannot be
1231 # written atomically (with current repository format), there is a race
1235 # written atomically (with current repository format), there is a race
1232 # condition here.
1236 # condition here.
1233 #
1237 #
1234 # 1) changelog content A is read
1238 # 1) changelog content A is read
1235 # 2) outside transaction update changelog to content B
1239 # 2) outside transaction update changelog to content B
1236 # 3) outside transaction update bookmark file referring to content B
1240 # 3) outside transaction update bookmark file referring to content B
1237 # 4) bookmarks file content is read and filtered against changelog-A
1241 # 4) bookmarks file content is read and filtered against changelog-A
1238 #
1242 #
1239 # When this happens, bookmarks against nodes missing from A are dropped.
1243 # When this happens, bookmarks against nodes missing from A are dropped.
1240 #
1244 #
1241 # Having this happening during read is not great, but it become worse
1245 # Having this happening during read is not great, but it become worse
1242 # when this happen during write because the bookmarks to the "unknown"
1246 # when this happen during write because the bookmarks to the "unknown"
1243 # nodes will be dropped for good. However, writes happen within locks.
1247 # nodes will be dropped for good. However, writes happen within locks.
1244 # This locking makes it possible to have a race free consistent read.
1248 # This locking makes it possible to have a race free consistent read.
1245 # For this purpose data read from disc before locking are
1249 # For this purpose data read from disc before locking are
1246 # "invalidated" right after the locks are taken. This invalidations are
1250 # "invalidated" right after the locks are taken. This invalidations are
1247 # "light", the `filecache` mechanism keep the data in memory and will
1251 # "light", the `filecache` mechanism keep the data in memory and will
1248 # reuse them if the underlying files did not changed. Not parsing the
1252 # reuse them if the underlying files did not changed. Not parsing the
1249 # same data multiple times helps performances.
1253 # same data multiple times helps performances.
1250 #
1254 #
1251 # Unfortunately in the case describe above, the files tracked by the
1255 # Unfortunately in the case describe above, the files tracked by the
1252 # bookmarks file cache might not have changed, but the in-memory
1256 # bookmarks file cache might not have changed, but the in-memory
1253 # content is still "wrong" because we used an older changelog content
1257 # content is still "wrong" because we used an older changelog content
1254 # to process the on-disk data. So after locking, the changelog would be
1258 # to process the on-disk data. So after locking, the changelog would be
1255 # refreshed but `_bookmarks` would be preserved.
1259 # refreshed but `_bookmarks` would be preserved.
1256 # Adding `00changelog.i` to the list of tracked file is not
1260 # Adding `00changelog.i` to the list of tracked file is not
1257 # enough, because at the time we build the content for `_bookmarks` in
1261 # enough, because at the time we build the content for `_bookmarks` in
1258 # (4), the changelog file has already diverged from the content used
1262 # (4), the changelog file has already diverged from the content used
1259 # for loading `changelog` in (1)
1263 # for loading `changelog` in (1)
1260 #
1264 #
1261 # To prevent the issue, we force the changelog to be explicitly
1265 # To prevent the issue, we force the changelog to be explicitly
1262 # reloaded while computing `_bookmarks`. The data race can still happen
1266 # reloaded while computing `_bookmarks`. The data race can still happen
1263 # without the lock (with a narrower window), but it would no longer go
1267 # without the lock (with a narrower window), but it would no longer go
1264 # undetected during the lock time refresh.
1268 # undetected during the lock time refresh.
1265 #
1269 #
1266 # The new schedule is as follow
1270 # The new schedule is as follow
1267 #
1271 #
1268 # 1) filecache logic detect that `_bookmarks` needs to be computed
1272 # 1) filecache logic detect that `_bookmarks` needs to be computed
1269 # 2) cachestat for `bookmarks` and `changelog` are captured (for book)
1273 # 2) cachestat for `bookmarks` and `changelog` are captured (for book)
1270 # 3) We force `changelog` filecache to be tested
1274 # 3) We force `changelog` filecache to be tested
1271 # 4) cachestat for `changelog` are captured (for changelog)
1275 # 4) cachestat for `changelog` are captured (for changelog)
1272 # 5) `_bookmarks` is computed and cached
1276 # 5) `_bookmarks` is computed and cached
1273 #
1277 #
1274 # The step in (3) ensure we have a changelog at least as recent as the
1278 # The step in (3) ensure we have a changelog at least as recent as the
1275 # cache stat computed in (1). As a result at locking time:
1279 # cache stat computed in (1). As a result at locking time:
1276 # * if the changelog did not changed since (1) -> we can reuse the data
1280 # * if the changelog did not changed since (1) -> we can reuse the data
1277 # * otherwise -> the bookmarks get refreshed.
1281 # * otherwise -> the bookmarks get refreshed.
1278 self._refreshchangelog()
1282 self._refreshchangelog()
1279 return bookmarks.bmstore(self)
1283 return bookmarks.bmstore(self)
1280
1284
1281 def _refreshchangelog(self):
1285 def _refreshchangelog(self):
1282 """make sure the in memory changelog match the on-disk one"""
1286 """make sure the in memory changelog match the on-disk one"""
1283 if ('changelog' in vars(self) and self.currenttransaction() is None):
1287 if ('changelog' in vars(self) and self.currenttransaction() is None):
1284 del self.changelog
1288 del self.changelog
1285
1289
1286 @property
1290 @property
1287 def _activebookmark(self):
1291 def _activebookmark(self):
1288 return self._bookmarks.active
1292 return self._bookmarks.active
1289
1293
1290 # _phasesets depend on changelog. what we need is to call
1294 # _phasesets depend on changelog. what we need is to call
1291 # _phasecache.invalidate() if '00changelog.i' was changed, but it
1295 # _phasecache.invalidate() if '00changelog.i' was changed, but it
1292 # can't be easily expressed in filecache mechanism.
1296 # can't be easily expressed in filecache mechanism.
1293 @storecache('phaseroots', '00changelog.i')
1297 @storecache('phaseroots', '00changelog.i')
1294 def _phasecache(self):
1298 def _phasecache(self):
1295 return phases.phasecache(self, self._phasedefaults)
1299 return phases.phasecache(self, self._phasedefaults)
1296
1300
1297 @storecache('obsstore')
1301 @storecache('obsstore')
1298 def obsstore(self):
1302 def obsstore(self):
1299 return obsolete.makestore(self.ui, self)
1303 return obsolete.makestore(self.ui, self)
1300
1304
1301 @storecache('00changelog.i')
1305 @storecache('00changelog.i')
1302 def changelog(self):
1306 def changelog(self):
1303 return changelog.changelog(self.svfs,
1307 return changelog.changelog(self.svfs,
1304 trypending=txnutil.mayhavepending(self.root))
1308 trypending=txnutil.mayhavepending(self.root))
1305
1309
1306 @storecache('00manifest.i')
1310 @storecache('00manifest.i')
1307 def manifestlog(self):
1311 def manifestlog(self):
1308 rootstore = manifest.manifestrevlog(self.svfs)
1312 rootstore = manifest.manifestrevlog(self.svfs)
1309 return manifest.manifestlog(self.svfs, self, rootstore,
1313 return manifest.manifestlog(self.svfs, self, rootstore,
1310 self._storenarrowmatch)
1314 self._storenarrowmatch)
1311
1315
1312 @repofilecache('dirstate')
1316 @repofilecache('dirstate')
1313 def dirstate(self):
1317 def dirstate(self):
1314 return self._makedirstate()
1318 return self._makedirstate()
1315
1319
1316 def _makedirstate(self):
1320 def _makedirstate(self):
1317 """Extension point for wrapping the dirstate per-repo."""
1321 """Extension point for wrapping the dirstate per-repo."""
1318 sparsematchfn = lambda: sparse.matcher(self)
1322 sparsematchfn = lambda: sparse.matcher(self)
1319
1323
1320 return dirstate.dirstate(self.vfs, self.ui, self.root,
1324 return dirstate.dirstate(self.vfs, self.ui, self.root,
1321 self._dirstatevalidate, sparsematchfn)
1325 self._dirstatevalidate, sparsematchfn)
1322
1326
1323 def _dirstatevalidate(self, node):
1327 def _dirstatevalidate(self, node):
1324 try:
1328 try:
1325 self.changelog.rev(node)
1329 self.changelog.rev(node)
1326 return node
1330 return node
1327 except error.LookupError:
1331 except error.LookupError:
1328 if not self._dirstatevalidatewarned:
1332 if not self._dirstatevalidatewarned:
1329 self._dirstatevalidatewarned = True
1333 self._dirstatevalidatewarned = True
1330 self.ui.warn(_("warning: ignoring unknown"
1334 self.ui.warn(_("warning: ignoring unknown"
1331 " working parent %s!\n") % short(node))
1335 " working parent %s!\n") % short(node))
1332 return nullid
1336 return nullid
1333
1337
1334 @storecache(narrowspec.FILENAME)
1338 @storecache(narrowspec.FILENAME)
1335 def narrowpats(self):
1339 def narrowpats(self):
1336 """matcher patterns for this repository's narrowspec
1340 """matcher patterns for this repository's narrowspec
1337
1341
1338 A tuple of (includes, excludes).
1342 A tuple of (includes, excludes).
1339 """
1343 """
1340 return narrowspec.load(self)
1344 return narrowspec.load(self)
1341
1345
1342 @storecache(narrowspec.FILENAME)
1346 @storecache(narrowspec.FILENAME)
1343 def _storenarrowmatch(self):
1347 def _storenarrowmatch(self):
1344 if repository.NARROW_REQUIREMENT not in self.requirements:
1348 if repository.NARROW_REQUIREMENT not in self.requirements:
1345 return matchmod.always()
1349 return matchmod.always()
1346 include, exclude = self.narrowpats
1350 include, exclude = self.narrowpats
1347 return narrowspec.match(self.root, include=include, exclude=exclude)
1351 return narrowspec.match(self.root, include=include, exclude=exclude)
1348
1352
1349 @storecache(narrowspec.FILENAME)
1353 @storecache(narrowspec.FILENAME)
1350 def _narrowmatch(self):
1354 def _narrowmatch(self):
1351 if repository.NARROW_REQUIREMENT not in self.requirements:
1355 if repository.NARROW_REQUIREMENT not in self.requirements:
1352 return matchmod.always()
1356 return matchmod.always()
1353 narrowspec.checkworkingcopynarrowspec(self)
1357 narrowspec.checkworkingcopynarrowspec(self)
1354 include, exclude = self.narrowpats
1358 include, exclude = self.narrowpats
1355 return narrowspec.match(self.root, include=include, exclude=exclude)
1359 return narrowspec.match(self.root, include=include, exclude=exclude)
1356
1360
1357 def narrowmatch(self, match=None, includeexact=False):
1361 def narrowmatch(self, match=None, includeexact=False):
1358 """matcher corresponding the the repo's narrowspec
1362 """matcher corresponding the the repo's narrowspec
1359
1363
1360 If `match` is given, then that will be intersected with the narrow
1364 If `match` is given, then that will be intersected with the narrow
1361 matcher.
1365 matcher.
1362
1366
1363 If `includeexact` is True, then any exact matches from `match` will
1367 If `includeexact` is True, then any exact matches from `match` will
1364 be included even if they're outside the narrowspec.
1368 be included even if they're outside the narrowspec.
1365 """
1369 """
1366 if match:
1370 if match:
1367 if includeexact and not self._narrowmatch.always():
1371 if includeexact and not self._narrowmatch.always():
1368 # do not exclude explicitly-specified paths so that they can
1372 # do not exclude explicitly-specified paths so that they can
1369 # be warned later on
1373 # be warned later on
1370 em = matchmod.exact(match.files())
1374 em = matchmod.exact(match.files())
1371 nm = matchmod.unionmatcher([self._narrowmatch, em])
1375 nm = matchmod.unionmatcher([self._narrowmatch, em])
1372 return matchmod.intersectmatchers(match, nm)
1376 return matchmod.intersectmatchers(match, nm)
1373 return matchmod.intersectmatchers(match, self._narrowmatch)
1377 return matchmod.intersectmatchers(match, self._narrowmatch)
1374 return self._narrowmatch
1378 return self._narrowmatch
1375
1379
1376 def setnarrowpats(self, newincludes, newexcludes):
1380 def setnarrowpats(self, newincludes, newexcludes):
1377 narrowspec.save(self, newincludes, newexcludes)
1381 narrowspec.save(self, newincludes, newexcludes)
1378 self.invalidate(clearfilecache=True)
1382 self.invalidate(clearfilecache=True)
1379
1383
1380 def __getitem__(self, changeid):
1384 def __getitem__(self, changeid):
1381 if changeid is None:
1385 if changeid is None:
1382 return context.workingctx(self)
1386 return context.workingctx(self)
1383 if isinstance(changeid, context.basectx):
1387 if isinstance(changeid, context.basectx):
1384 return changeid
1388 return changeid
1385 if isinstance(changeid, slice):
1389 if isinstance(changeid, slice):
1386 # wdirrev isn't contiguous so the slice shouldn't include it
1390 # wdirrev isn't contiguous so the slice shouldn't include it
1387 return [self[i]
1391 return [self[i]
1388 for i in pycompat.xrange(*changeid.indices(len(self)))
1392 for i in pycompat.xrange(*changeid.indices(len(self)))
1389 if i not in self.changelog.filteredrevs]
1393 if i not in self.changelog.filteredrevs]
1390 try:
1394 try:
1391 if isinstance(changeid, int):
1395 if isinstance(changeid, int):
1392 node = self.changelog.node(changeid)
1396 node = self.changelog.node(changeid)
1393 rev = changeid
1397 rev = changeid
1394 elif changeid == 'null':
1398 elif changeid == 'null':
1395 node = nullid
1399 node = nullid
1396 rev = nullrev
1400 rev = nullrev
1397 elif changeid == 'tip':
1401 elif changeid == 'tip':
1398 node = self.changelog.tip()
1402 node = self.changelog.tip()
1399 rev = self.changelog.rev(node)
1403 rev = self.changelog.rev(node)
1400 elif changeid == '.':
1404 elif changeid == '.':
1401 # this is a hack to delay/avoid loading obsmarkers
1405 # this is a hack to delay/avoid loading obsmarkers
1402 # when we know that '.' won't be hidden
1406 # when we know that '.' won't be hidden
1403 node = self.dirstate.p1()
1407 node = self.dirstate.p1()
1404 rev = self.unfiltered().changelog.rev(node)
1408 rev = self.unfiltered().changelog.rev(node)
1405 elif len(changeid) == 20:
1409 elif len(changeid) == 20:
1406 try:
1410 try:
1407 node = changeid
1411 node = changeid
1408 rev = self.changelog.rev(changeid)
1412 rev = self.changelog.rev(changeid)
1409 except error.FilteredLookupError:
1413 except error.FilteredLookupError:
1410 changeid = hex(changeid) # for the error message
1414 changeid = hex(changeid) # for the error message
1411 raise
1415 raise
1412 except LookupError:
1416 except LookupError:
1413 # check if it might have come from damaged dirstate
1417 # check if it might have come from damaged dirstate
1414 #
1418 #
1415 # XXX we could avoid the unfiltered if we had a recognizable
1419 # XXX we could avoid the unfiltered if we had a recognizable
1416 # exception for filtered changeset access
1420 # exception for filtered changeset access
1417 if (self.local()
1421 if (self.local()
1418 and changeid in self.unfiltered().dirstate.parents()):
1422 and changeid in self.unfiltered().dirstate.parents()):
1419 msg = _("working directory has unknown parent '%s'!")
1423 msg = _("working directory has unknown parent '%s'!")
1420 raise error.Abort(msg % short(changeid))
1424 raise error.Abort(msg % short(changeid))
1421 changeid = hex(changeid) # for the error message
1425 changeid = hex(changeid) # for the error message
1422 raise
1426 raise
1423
1427
1424 elif len(changeid) == 40:
1428 elif len(changeid) == 40:
1425 node = bin(changeid)
1429 node = bin(changeid)
1426 rev = self.changelog.rev(node)
1430 rev = self.changelog.rev(node)
1427 else:
1431 else:
1428 raise error.ProgrammingError(
1432 raise error.ProgrammingError(
1429 "unsupported changeid '%s' of type %s" %
1433 "unsupported changeid '%s' of type %s" %
1430 (changeid, type(changeid)))
1434 (changeid, type(changeid)))
1431
1435
1432 return context.changectx(self, rev, node)
1436 return context.changectx(self, rev, node)
1433
1437
1434 except (error.FilteredIndexError, error.FilteredLookupError):
1438 except (error.FilteredIndexError, error.FilteredLookupError):
1435 raise error.FilteredRepoLookupError(_("filtered revision '%s'")
1439 raise error.FilteredRepoLookupError(_("filtered revision '%s'")
1436 % pycompat.bytestr(changeid))
1440 % pycompat.bytestr(changeid))
1437 except (IndexError, LookupError):
1441 except (IndexError, LookupError):
1438 raise error.RepoLookupError(
1442 raise error.RepoLookupError(
1439 _("unknown revision '%s'") % pycompat.bytestr(changeid))
1443 _("unknown revision '%s'") % pycompat.bytestr(changeid))
1440 except error.WdirUnsupported:
1444 except error.WdirUnsupported:
1441 return context.workingctx(self)
1445 return context.workingctx(self)
1442
1446
1443 def __contains__(self, changeid):
1447 def __contains__(self, changeid):
1444 """True if the given changeid exists
1448 """True if the given changeid exists
1445
1449
1446 error.AmbiguousPrefixLookupError is raised if an ambiguous node
1450 error.AmbiguousPrefixLookupError is raised if an ambiguous node
1447 specified.
1451 specified.
1448 """
1452 """
1449 try:
1453 try:
1450 self[changeid]
1454 self[changeid]
1451 return True
1455 return True
1452 except error.RepoLookupError:
1456 except error.RepoLookupError:
1453 return False
1457 return False
1454
1458
1455 def __nonzero__(self):
1459 def __nonzero__(self):
1456 return True
1460 return True
1457
1461
1458 __bool__ = __nonzero__
1462 __bool__ = __nonzero__
1459
1463
1460 def __len__(self):
1464 def __len__(self):
1461 # no need to pay the cost of repoview.changelog
1465 # no need to pay the cost of repoview.changelog
1462 unfi = self.unfiltered()
1466 unfi = self.unfiltered()
1463 return len(unfi.changelog)
1467 return len(unfi.changelog)
1464
1468
1465 def __iter__(self):
1469 def __iter__(self):
1466 return iter(self.changelog)
1470 return iter(self.changelog)
1467
1471
1468 def revs(self, expr, *args):
1472 def revs(self, expr, *args):
1469 '''Find revisions matching a revset.
1473 '''Find revisions matching a revset.
1470
1474
1471 The revset is specified as a string ``expr`` that may contain
1475 The revset is specified as a string ``expr`` that may contain
1472 %-formatting to escape certain types. See ``revsetlang.formatspec``.
1476 %-formatting to escape certain types. See ``revsetlang.formatspec``.
1473
1477
1474 Revset aliases from the configuration are not expanded. To expand
1478 Revset aliases from the configuration are not expanded. To expand
1475 user aliases, consider calling ``scmutil.revrange()`` or
1479 user aliases, consider calling ``scmutil.revrange()`` or
1476 ``repo.anyrevs([expr], user=True)``.
1480 ``repo.anyrevs([expr], user=True)``.
1477
1481
1478 Returns a revset.abstractsmartset, which is a list-like interface
1482 Returns a revset.abstractsmartset, which is a list-like interface
1479 that contains integer revisions.
1483 that contains integer revisions.
1480 '''
1484 '''
1481 tree = revsetlang.spectree(expr, *args)
1485 tree = revsetlang.spectree(expr, *args)
1482 return revset.makematcher(tree)(self)
1486 return revset.makematcher(tree)(self)
1483
1487
1484 def set(self, expr, *args):
1488 def set(self, expr, *args):
1485 '''Find revisions matching a revset and emit changectx instances.
1489 '''Find revisions matching a revset and emit changectx instances.
1486
1490
1487 This is a convenience wrapper around ``revs()`` that iterates the
1491 This is a convenience wrapper around ``revs()`` that iterates the
1488 result and is a generator of changectx instances.
1492 result and is a generator of changectx instances.
1489
1493
1490 Revset aliases from the configuration are not expanded. To expand
1494 Revset aliases from the configuration are not expanded. To expand
1491 user aliases, consider calling ``scmutil.revrange()``.
1495 user aliases, consider calling ``scmutil.revrange()``.
1492 '''
1496 '''
1493 for r in self.revs(expr, *args):
1497 for r in self.revs(expr, *args):
1494 yield self[r]
1498 yield self[r]
1495
1499
1496 def anyrevs(self, specs, user=False, localalias=None):
1500 def anyrevs(self, specs, user=False, localalias=None):
1497 '''Find revisions matching one of the given revsets.
1501 '''Find revisions matching one of the given revsets.
1498
1502
1499 Revset aliases from the configuration are not expanded by default. To
1503 Revset aliases from the configuration are not expanded by default. To
1500 expand user aliases, specify ``user=True``. To provide some local
1504 expand user aliases, specify ``user=True``. To provide some local
1501 definitions overriding user aliases, set ``localalias`` to
1505 definitions overriding user aliases, set ``localalias`` to
1502 ``{name: definitionstring}``.
1506 ``{name: definitionstring}``.
1503 '''
1507 '''
1504 if user:
1508 if user:
1505 m = revset.matchany(self.ui, specs,
1509 m = revset.matchany(self.ui, specs,
1506 lookup=revset.lookupfn(self),
1510 lookup=revset.lookupfn(self),
1507 localalias=localalias)
1511 localalias=localalias)
1508 else:
1512 else:
1509 m = revset.matchany(None, specs, localalias=localalias)
1513 m = revset.matchany(None, specs, localalias=localalias)
1510 return m(self)
1514 return m(self)
1511
1515
1512 def url(self):
1516 def url(self):
1513 return 'file:' + self.root
1517 return 'file:' + self.root
1514
1518
1515 def hook(self, name, throw=False, **args):
1519 def hook(self, name, throw=False, **args):
1516 """Call a hook, passing this repo instance.
1520 """Call a hook, passing this repo instance.
1517
1521
1518 This a convenience method to aid invoking hooks. Extensions likely
1522 This a convenience method to aid invoking hooks. Extensions likely
1519 won't call this unless they have registered a custom hook or are
1523 won't call this unless they have registered a custom hook or are
1520 replacing code that is expected to call a hook.
1524 replacing code that is expected to call a hook.
1521 """
1525 """
1522 return hook.hook(self.ui, self, name, throw, **args)
1526 return hook.hook(self.ui, self, name, throw, **args)
1523
1527
1524 @filteredpropertycache
1528 @filteredpropertycache
1525 def _tagscache(self):
1529 def _tagscache(self):
1526 '''Returns a tagscache object that contains various tags related
1530 '''Returns a tagscache object that contains various tags related
1527 caches.'''
1531 caches.'''
1528
1532
1529 # This simplifies its cache management by having one decorated
1533 # This simplifies its cache management by having one decorated
1530 # function (this one) and the rest simply fetch things from it.
1534 # function (this one) and the rest simply fetch things from it.
1531 class tagscache(object):
1535 class tagscache(object):
1532 def __init__(self):
1536 def __init__(self):
1533 # These two define the set of tags for this repository. tags
1537 # These two define the set of tags for this repository. tags
1534 # maps tag name to node; tagtypes maps tag name to 'global' or
1538 # maps tag name to node; tagtypes maps tag name to 'global' or
1535 # 'local'. (Global tags are defined by .hgtags across all
1539 # 'local'. (Global tags are defined by .hgtags across all
1536 # heads, and local tags are defined in .hg/localtags.)
1540 # heads, and local tags are defined in .hg/localtags.)
1537 # They constitute the in-memory cache of tags.
1541 # They constitute the in-memory cache of tags.
1538 self.tags = self.tagtypes = None
1542 self.tags = self.tagtypes = None
1539
1543
1540 self.nodetagscache = self.tagslist = None
1544 self.nodetagscache = self.tagslist = None
1541
1545
1542 cache = tagscache()
1546 cache = tagscache()
1543 cache.tags, cache.tagtypes = self._findtags()
1547 cache.tags, cache.tagtypes = self._findtags()
1544
1548
1545 return cache
1549 return cache
1546
1550
1547 def tags(self):
1551 def tags(self):
1548 '''return a mapping of tag to node'''
1552 '''return a mapping of tag to node'''
1549 t = {}
1553 t = {}
1550 if self.changelog.filteredrevs:
1554 if self.changelog.filteredrevs:
1551 tags, tt = self._findtags()
1555 tags, tt = self._findtags()
1552 else:
1556 else:
1553 tags = self._tagscache.tags
1557 tags = self._tagscache.tags
1554 rev = self.changelog.rev
1558 rev = self.changelog.rev
1555 for k, v in tags.iteritems():
1559 for k, v in tags.iteritems():
1556 try:
1560 try:
1557 # ignore tags to unknown nodes
1561 # ignore tags to unknown nodes
1558 rev(v)
1562 rev(v)
1559 t[k] = v
1563 t[k] = v
1560 except (error.LookupError, ValueError):
1564 except (error.LookupError, ValueError):
1561 pass
1565 pass
1562 return t
1566 return t
1563
1567
1564 def _findtags(self):
1568 def _findtags(self):
1565 '''Do the hard work of finding tags. Return a pair of dicts
1569 '''Do the hard work of finding tags. Return a pair of dicts
1566 (tags, tagtypes) where tags maps tag name to node, and tagtypes
1570 (tags, tagtypes) where tags maps tag name to node, and tagtypes
1567 maps tag name to a string like \'global\' or \'local\'.
1571 maps tag name to a string like \'global\' or \'local\'.
1568 Subclasses or extensions are free to add their own tags, but
1572 Subclasses or extensions are free to add their own tags, but
1569 should be aware that the returned dicts will be retained for the
1573 should be aware that the returned dicts will be retained for the
1570 duration of the localrepo object.'''
1574 duration of the localrepo object.'''
1571
1575
1572 # XXX what tagtype should subclasses/extensions use? Currently
1576 # XXX what tagtype should subclasses/extensions use? Currently
1573 # mq and bookmarks add tags, but do not set the tagtype at all.
1577 # mq and bookmarks add tags, but do not set the tagtype at all.
1574 # Should each extension invent its own tag type? Should there
1578 # Should each extension invent its own tag type? Should there
1575 # be one tagtype for all such "virtual" tags? Or is the status
1579 # be one tagtype for all such "virtual" tags? Or is the status
1576 # quo fine?
1580 # quo fine?
1577
1581
1578
1582
1579 # map tag name to (node, hist)
1583 # map tag name to (node, hist)
1580 alltags = tagsmod.findglobaltags(self.ui, self)
1584 alltags = tagsmod.findglobaltags(self.ui, self)
1581 # map tag name to tag type
1585 # map tag name to tag type
1582 tagtypes = dict((tag, 'global') for tag in alltags)
1586 tagtypes = dict((tag, 'global') for tag in alltags)
1583
1587
1584 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
1588 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
1585
1589
1586 # Build the return dicts. Have to re-encode tag names because
1590 # Build the return dicts. Have to re-encode tag names because
1587 # the tags module always uses UTF-8 (in order not to lose info
1591 # the tags module always uses UTF-8 (in order not to lose info
1588 # writing to the cache), but the rest of Mercurial wants them in
1592 # writing to the cache), but the rest of Mercurial wants them in
1589 # local encoding.
1593 # local encoding.
1590 tags = {}
1594 tags = {}
1591 for (name, (node, hist)) in alltags.iteritems():
1595 for (name, (node, hist)) in alltags.iteritems():
1592 if node != nullid:
1596 if node != nullid:
1593 tags[encoding.tolocal(name)] = node
1597 tags[encoding.tolocal(name)] = node
1594 tags['tip'] = self.changelog.tip()
1598 tags['tip'] = self.changelog.tip()
1595 tagtypes = dict([(encoding.tolocal(name), value)
1599 tagtypes = dict([(encoding.tolocal(name), value)
1596 for (name, value) in tagtypes.iteritems()])
1600 for (name, value) in tagtypes.iteritems()])
1597 return (tags, tagtypes)
1601 return (tags, tagtypes)
1598
1602
1599 def tagtype(self, tagname):
1603 def tagtype(self, tagname):
1600 '''
1604 '''
1601 return the type of the given tag. result can be:
1605 return the type of the given tag. result can be:
1602
1606
1603 'local' : a local tag
1607 'local' : a local tag
1604 'global' : a global tag
1608 'global' : a global tag
1605 None : tag does not exist
1609 None : tag does not exist
1606 '''
1610 '''
1607
1611
1608 return self._tagscache.tagtypes.get(tagname)
1612 return self._tagscache.tagtypes.get(tagname)
1609
1613
1610 def tagslist(self):
1614 def tagslist(self):
1611 '''return a list of tags ordered by revision'''
1615 '''return a list of tags ordered by revision'''
1612 if not self._tagscache.tagslist:
1616 if not self._tagscache.tagslist:
1613 l = []
1617 l = []
1614 for t, n in self.tags().iteritems():
1618 for t, n in self.tags().iteritems():
1615 l.append((self.changelog.rev(n), t, n))
1619 l.append((self.changelog.rev(n), t, n))
1616 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
1620 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
1617
1621
1618 return self._tagscache.tagslist
1622 return self._tagscache.tagslist
1619
1623
1620 def nodetags(self, node):
1624 def nodetags(self, node):
1621 '''return the tags associated with a node'''
1625 '''return the tags associated with a node'''
1622 if not self._tagscache.nodetagscache:
1626 if not self._tagscache.nodetagscache:
1623 nodetagscache = {}
1627 nodetagscache = {}
1624 for t, n in self._tagscache.tags.iteritems():
1628 for t, n in self._tagscache.tags.iteritems():
1625 nodetagscache.setdefault(n, []).append(t)
1629 nodetagscache.setdefault(n, []).append(t)
1626 for tags in nodetagscache.itervalues():
1630 for tags in nodetagscache.itervalues():
1627 tags.sort()
1631 tags.sort()
1628 self._tagscache.nodetagscache = nodetagscache
1632 self._tagscache.nodetagscache = nodetagscache
1629 return self._tagscache.nodetagscache.get(node, [])
1633 return self._tagscache.nodetagscache.get(node, [])
1630
1634
1631 def nodebookmarks(self, node):
1635 def nodebookmarks(self, node):
1632 """return the list of bookmarks pointing to the specified node"""
1636 """return the list of bookmarks pointing to the specified node"""
1633 return self._bookmarks.names(node)
1637 return self._bookmarks.names(node)
1634
1638
1635 def branchmap(self):
1639 def branchmap(self):
1636 '''returns a dictionary {branch: [branchheads]} with branchheads
1640 '''returns a dictionary {branch: [branchheads]} with branchheads
1637 ordered by increasing revision number'''
1641 ordered by increasing revision number'''
1638 return self._branchcaches[self]
1642 return self._branchcaches[self]
1639
1643
1640 @unfilteredmethod
1644 @unfilteredmethod
1641 def revbranchcache(self):
1645 def revbranchcache(self):
1642 if not self._revbranchcache:
1646 if not self._revbranchcache:
1643 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
1647 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
1644 return self._revbranchcache
1648 return self._revbranchcache
1645
1649
1646 def branchtip(self, branch, ignoremissing=False):
1650 def branchtip(self, branch, ignoremissing=False):
1647 '''return the tip node for a given branch
1651 '''return the tip node for a given branch
1648
1652
1649 If ignoremissing is True, then this method will not raise an error.
1653 If ignoremissing is True, then this method will not raise an error.
1650 This is helpful for callers that only expect None for a missing branch
1654 This is helpful for callers that only expect None for a missing branch
1651 (e.g. namespace).
1655 (e.g. namespace).
1652
1656
1653 '''
1657 '''
1654 try:
1658 try:
1655 return self.branchmap().branchtip(branch)
1659 return self.branchmap().branchtip(branch)
1656 except KeyError:
1660 except KeyError:
1657 if not ignoremissing:
1661 if not ignoremissing:
1658 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
1662 raise error.RepoLookupError(_("unknown branch '%s'") % branch)
1659 else:
1663 else:
1660 pass
1664 pass
1661
1665
1662 def lookup(self, key):
1666 def lookup(self, key):
1663 node = scmutil.revsymbol(self, key).node()
1667 node = scmutil.revsymbol(self, key).node()
1664 if node is None:
1668 if node is None:
1665 raise error.RepoLookupError(_("unknown revision '%s'") % key)
1669 raise error.RepoLookupError(_("unknown revision '%s'") % key)
1666 return node
1670 return node
1667
1671
1668 def lookupbranch(self, key):
1672 def lookupbranch(self, key):
1669 if self.branchmap().hasbranch(key):
1673 if self.branchmap().hasbranch(key):
1670 return key
1674 return key
1671
1675
1672 return scmutil.revsymbol(self, key).branch()
1676 return scmutil.revsymbol(self, key).branch()
1673
1677
1674 def known(self, nodes):
1678 def known(self, nodes):
1675 cl = self.changelog
1679 cl = self.changelog
1676 nm = cl.nodemap
1680 nm = cl.nodemap
1677 filtered = cl.filteredrevs
1681 filtered = cl.filteredrevs
1678 result = []
1682 result = []
1679 for n in nodes:
1683 for n in nodes:
1680 r = nm.get(n)
1684 r = nm.get(n)
1681 resp = not (r is None or r in filtered)
1685 resp = not (r is None or r in filtered)
1682 result.append(resp)
1686 result.append(resp)
1683 return result
1687 return result
1684
1688
1685 def local(self):
1689 def local(self):
1686 return self
1690 return self
1687
1691
1688 def publishing(self):
1692 def publishing(self):
1689 # it's safe (and desirable) to trust the publish flag unconditionally
1693 # it's safe (and desirable) to trust the publish flag unconditionally
1690 # so that we don't finalize changes shared between users via ssh or nfs
1694 # so that we don't finalize changes shared between users via ssh or nfs
1691 return self.ui.configbool('phases', 'publish', untrusted=True)
1695 return self.ui.configbool('phases', 'publish', untrusted=True)
1692
1696
1693 def cancopy(self):
1697 def cancopy(self):
1694 # so statichttprepo's override of local() works
1698 # so statichttprepo's override of local() works
1695 if not self.local():
1699 if not self.local():
1696 return False
1700 return False
1697 if not self.publishing():
1701 if not self.publishing():
1698 return True
1702 return True
1699 # if publishing we can't copy if there is filtered content
1703 # if publishing we can't copy if there is filtered content
1700 return not self.filtered('visible').changelog.filteredrevs
1704 return not self.filtered('visible').changelog.filteredrevs
1701
1705
1702 def shared(self):
1706 def shared(self):
1703 '''the type of shared repository (None if not shared)'''
1707 '''the type of shared repository (None if not shared)'''
1704 if self.sharedpath != self.path:
1708 if self.sharedpath != self.path:
1705 return 'store'
1709 return 'store'
1706 return None
1710 return None
1707
1711
1708 def wjoin(self, f, *insidef):
1712 def wjoin(self, f, *insidef):
1709 return self.vfs.reljoin(self.root, f, *insidef)
1713 return self.vfs.reljoin(self.root, f, *insidef)
1710
1714
1711 def setparents(self, p1, p2=nullid):
1715 def setparents(self, p1, p2=nullid):
1712 with self.dirstate.parentchange():
1716 with self.dirstate.parentchange():
1713 copies = self.dirstate.setparents(p1, p2)
1717 copies = self.dirstate.setparents(p1, p2)
1714 pctx = self[p1]
1718 pctx = self[p1]
1715 if copies:
1719 if copies:
1716 # Adjust copy records, the dirstate cannot do it, it
1720 # Adjust copy records, the dirstate cannot do it, it
1717 # requires access to parents manifests. Preserve them
1721 # requires access to parents manifests. Preserve them
1718 # only for entries added to first parent.
1722 # only for entries added to first parent.
1719 for f in copies:
1723 for f in copies:
1720 if f not in pctx and copies[f] in pctx:
1724 if f not in pctx and copies[f] in pctx:
1721 self.dirstate.copy(copies[f], f)
1725 self.dirstate.copy(copies[f], f)
1722 if p2 == nullid:
1726 if p2 == nullid:
1723 for f, s in sorted(self.dirstate.copies().items()):
1727 for f, s in sorted(self.dirstate.copies().items()):
1724 if f not in pctx and s not in pctx:
1728 if f not in pctx and s not in pctx:
1725 self.dirstate.copy(None, f)
1729 self.dirstate.copy(None, f)
1726
1730
1727 def filectx(self, path, changeid=None, fileid=None, changectx=None):
1731 def filectx(self, path, changeid=None, fileid=None, changectx=None):
1728 """changeid must be a changeset revision, if specified.
1732 """changeid must be a changeset revision, if specified.
1729 fileid can be a file revision or node."""
1733 fileid can be a file revision or node."""
1730 return context.filectx(self, path, changeid, fileid,
1734 return context.filectx(self, path, changeid, fileid,
1731 changectx=changectx)
1735 changectx=changectx)
1732
1736
1733 def getcwd(self):
1737 def getcwd(self):
1734 return self.dirstate.getcwd()
1738 return self.dirstate.getcwd()
1735
1739
1736 def pathto(self, f, cwd=None):
1740 def pathto(self, f, cwd=None):
1737 return self.dirstate.pathto(f, cwd)
1741 return self.dirstate.pathto(f, cwd)
1738
1742
1739 def _loadfilter(self, filter):
1743 def _loadfilter(self, filter):
1740 if filter not in self._filterpats:
1744 if filter not in self._filterpats:
1741 l = []
1745 l = []
1742 for pat, cmd in self.ui.configitems(filter):
1746 for pat, cmd in self.ui.configitems(filter):
1743 if cmd == '!':
1747 if cmd == '!':
1744 continue
1748 continue
1745 mf = matchmod.match(self.root, '', [pat])
1749 mf = matchmod.match(self.root, '', [pat])
1746 fn = None
1750 fn = None
1747 params = cmd
1751 params = cmd
1748 for name, filterfn in self._datafilters.iteritems():
1752 for name, filterfn in self._datafilters.iteritems():
1749 if cmd.startswith(name):
1753 if cmd.startswith(name):
1750 fn = filterfn
1754 fn = filterfn
1751 params = cmd[len(name):].lstrip()
1755 params = cmd[len(name):].lstrip()
1752 break
1756 break
1753 if not fn:
1757 if not fn:
1754 fn = lambda s, c, **kwargs: procutil.filter(s, c)
1758 fn = lambda s, c, **kwargs: procutil.filter(s, c)
1755 # Wrap old filters not supporting keyword arguments
1759 # Wrap old filters not supporting keyword arguments
1756 if not pycompat.getargspec(fn)[2]:
1760 if not pycompat.getargspec(fn)[2]:
1757 oldfn = fn
1761 oldfn = fn
1758 fn = lambda s, c, **kwargs: oldfn(s, c)
1762 fn = lambda s, c, **kwargs: oldfn(s, c)
1759 l.append((mf, fn, params))
1763 l.append((mf, fn, params))
1760 self._filterpats[filter] = l
1764 self._filterpats[filter] = l
1761 return self._filterpats[filter]
1765 return self._filterpats[filter]
1762
1766
1763 def _filter(self, filterpats, filename, data):
1767 def _filter(self, filterpats, filename, data):
1764 for mf, fn, cmd in filterpats:
1768 for mf, fn, cmd in filterpats:
1765 if mf(filename):
1769 if mf(filename):
1766 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
1770 self.ui.debug("filtering %s through %s\n" % (filename, cmd))
1767 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
1771 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
1768 break
1772 break
1769
1773
1770 return data
1774 return data
1771
1775
1772 @unfilteredpropertycache
1776 @unfilteredpropertycache
1773 def _encodefilterpats(self):
1777 def _encodefilterpats(self):
1774 return self._loadfilter('encode')
1778 return self._loadfilter('encode')
1775
1779
1776 @unfilteredpropertycache
1780 @unfilteredpropertycache
1777 def _decodefilterpats(self):
1781 def _decodefilterpats(self):
1778 return self._loadfilter('decode')
1782 return self._loadfilter('decode')
1779
1783
1780 def adddatafilter(self, name, filter):
1784 def adddatafilter(self, name, filter):
1781 self._datafilters[name] = filter
1785 self._datafilters[name] = filter
1782
1786
1783 def wread(self, filename):
1787 def wread(self, filename):
1784 if self.wvfs.islink(filename):
1788 if self.wvfs.islink(filename):
1785 data = self.wvfs.readlink(filename)
1789 data = self.wvfs.readlink(filename)
1786 else:
1790 else:
1787 data = self.wvfs.read(filename)
1791 data = self.wvfs.read(filename)
1788 return self._filter(self._encodefilterpats, filename, data)
1792 return self._filter(self._encodefilterpats, filename, data)
1789
1793
1790 def wwrite(self, filename, data, flags, backgroundclose=False, **kwargs):
1794 def wwrite(self, filename, data, flags, backgroundclose=False, **kwargs):
1791 """write ``data`` into ``filename`` in the working directory
1795 """write ``data`` into ``filename`` in the working directory
1792
1796
1793 This returns length of written (maybe decoded) data.
1797 This returns length of written (maybe decoded) data.
1794 """
1798 """
1795 data = self._filter(self._decodefilterpats, filename, data)
1799 data = self._filter(self._decodefilterpats, filename, data)
1796 if 'l' in flags:
1800 if 'l' in flags:
1797 self.wvfs.symlink(data, filename)
1801 self.wvfs.symlink(data, filename)
1798 else:
1802 else:
1799 self.wvfs.write(filename, data, backgroundclose=backgroundclose,
1803 self.wvfs.write(filename, data, backgroundclose=backgroundclose,
1800 **kwargs)
1804 **kwargs)
1801 if 'x' in flags:
1805 if 'x' in flags:
1802 self.wvfs.setflags(filename, False, True)
1806 self.wvfs.setflags(filename, False, True)
1803 else:
1807 else:
1804 self.wvfs.setflags(filename, False, False)
1808 self.wvfs.setflags(filename, False, False)
1805 return len(data)
1809 return len(data)
1806
1810
1807 def wwritedata(self, filename, data):
1811 def wwritedata(self, filename, data):
1808 return self._filter(self._decodefilterpats, filename, data)
1812 return self._filter(self._decodefilterpats, filename, data)
1809
1813
1810 def currenttransaction(self):
1814 def currenttransaction(self):
1811 """return the current transaction or None if non exists"""
1815 """return the current transaction or None if non exists"""
1812 if self._transref:
1816 if self._transref:
1813 tr = self._transref()
1817 tr = self._transref()
1814 else:
1818 else:
1815 tr = None
1819 tr = None
1816
1820
1817 if tr and tr.running():
1821 if tr and tr.running():
1818 return tr
1822 return tr
1819 return None
1823 return None
1820
1824
1821 def transaction(self, desc, report=None):
1825 def transaction(self, desc, report=None):
1822 if (self.ui.configbool('devel', 'all-warnings')
1826 if (self.ui.configbool('devel', 'all-warnings')
1823 or self.ui.configbool('devel', 'check-locks')):
1827 or self.ui.configbool('devel', 'check-locks')):
1824 if self._currentlock(self._lockref) is None:
1828 if self._currentlock(self._lockref) is None:
1825 raise error.ProgrammingError('transaction requires locking')
1829 raise error.ProgrammingError('transaction requires locking')
1826 tr = self.currenttransaction()
1830 tr = self.currenttransaction()
1827 if tr is not None:
1831 if tr is not None:
1828 return tr.nest(name=desc)
1832 return tr.nest(name=desc)
1829
1833
1830 # abort here if the journal already exists
1834 # abort here if the journal already exists
1831 if self.svfs.exists("journal"):
1835 if self.svfs.exists("journal"):
1832 raise error.RepoError(
1836 raise error.RepoError(
1833 _("abandoned transaction found"),
1837 _("abandoned transaction found"),
1834 hint=_("run 'hg recover' to clean up transaction"))
1838 hint=_("run 'hg recover' to clean up transaction"))
1835
1839
1836 idbase = "%.40f#%f" % (random.random(), time.time())
1840 idbase = "%.40f#%f" % (random.random(), time.time())
1837 ha = hex(hashlib.sha1(idbase).digest())
1841 ha = hex(hashlib.sha1(idbase).digest())
1838 txnid = 'TXN:' + ha
1842 txnid = 'TXN:' + ha
1839 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1843 self.hook('pretxnopen', throw=True, txnname=desc, txnid=txnid)
1840
1844
1841 self._writejournal(desc)
1845 self._writejournal(desc)
1842 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1846 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
1843 if report:
1847 if report:
1844 rp = report
1848 rp = report
1845 else:
1849 else:
1846 rp = self.ui.warn
1850 rp = self.ui.warn
1847 vfsmap = {'plain': self.vfs, 'store': self.svfs} # root of .hg/
1851 vfsmap = {'plain': self.vfs, 'store': self.svfs} # root of .hg/
1848 # we must avoid cyclic reference between repo and transaction.
1852 # we must avoid cyclic reference between repo and transaction.
1849 reporef = weakref.ref(self)
1853 reporef = weakref.ref(self)
1850 # Code to track tag movement
1854 # Code to track tag movement
1851 #
1855 #
1852 # Since tags are all handled as file content, it is actually quite hard
1856 # Since tags are all handled as file content, it is actually quite hard
1853 # to track these movement from a code perspective. So we fallback to a
1857 # to track these movement from a code perspective. So we fallback to a
1854 # tracking at the repository level. One could envision to track changes
1858 # tracking at the repository level. One could envision to track changes
1855 # to the '.hgtags' file through changegroup apply but that fails to
1859 # to the '.hgtags' file through changegroup apply but that fails to
1856 # cope with case where transaction expose new heads without changegroup
1860 # cope with case where transaction expose new heads without changegroup
1857 # being involved (eg: phase movement).
1861 # being involved (eg: phase movement).
1858 #
1862 #
1859 # For now, We gate the feature behind a flag since this likely comes
1863 # For now, We gate the feature behind a flag since this likely comes
1860 # with performance impacts. The current code run more often than needed
1864 # with performance impacts. The current code run more often than needed
1861 # and do not use caches as much as it could. The current focus is on
1865 # and do not use caches as much as it could. The current focus is on
1862 # the behavior of the feature so we disable it by default. The flag
1866 # the behavior of the feature so we disable it by default. The flag
1863 # will be removed when we are happy with the performance impact.
1867 # will be removed when we are happy with the performance impact.
1864 #
1868 #
1865 # Once this feature is no longer experimental move the following
1869 # Once this feature is no longer experimental move the following
1866 # documentation to the appropriate help section:
1870 # documentation to the appropriate help section:
1867 #
1871 #
1868 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
1872 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
1869 # tags (new or changed or deleted tags). In addition the details of
1873 # tags (new or changed or deleted tags). In addition the details of
1870 # these changes are made available in a file at:
1874 # these changes are made available in a file at:
1871 # ``REPOROOT/.hg/changes/tags.changes``.
1875 # ``REPOROOT/.hg/changes/tags.changes``.
1872 # Make sure you check for HG_TAG_MOVED before reading that file as it
1876 # Make sure you check for HG_TAG_MOVED before reading that file as it
1873 # might exist from a previous transaction even if no tag were touched
1877 # might exist from a previous transaction even if no tag were touched
1874 # in this one. Changes are recorded in a line base format::
1878 # in this one. Changes are recorded in a line base format::
1875 #
1879 #
1876 # <action> <hex-node> <tag-name>\n
1880 # <action> <hex-node> <tag-name>\n
1877 #
1881 #
1878 # Actions are defined as follow:
1882 # Actions are defined as follow:
1879 # "-R": tag is removed,
1883 # "-R": tag is removed,
1880 # "+A": tag is added,
1884 # "+A": tag is added,
1881 # "-M": tag is moved (old value),
1885 # "-M": tag is moved (old value),
1882 # "+M": tag is moved (new value),
1886 # "+M": tag is moved (new value),
1883 tracktags = lambda x: None
1887 tracktags = lambda x: None
1884 # experimental config: experimental.hook-track-tags
1888 # experimental config: experimental.hook-track-tags
1885 shouldtracktags = self.ui.configbool('experimental', 'hook-track-tags')
1889 shouldtracktags = self.ui.configbool('experimental', 'hook-track-tags')
1886 if desc != 'strip' and shouldtracktags:
1890 if desc != 'strip' and shouldtracktags:
1887 oldheads = self.changelog.headrevs()
1891 oldheads = self.changelog.headrevs()
1888 def tracktags(tr2):
1892 def tracktags(tr2):
1889 repo = reporef()
1893 repo = reporef()
1890 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
1894 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
1891 newheads = repo.changelog.headrevs()
1895 newheads = repo.changelog.headrevs()
1892 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
1896 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
1893 # notes: we compare lists here.
1897 # notes: we compare lists here.
1894 # As we do it only once buiding set would not be cheaper
1898 # As we do it only once buiding set would not be cheaper
1895 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
1899 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
1896 if changes:
1900 if changes:
1897 tr2.hookargs['tag_moved'] = '1'
1901 tr2.hookargs['tag_moved'] = '1'
1898 with repo.vfs('changes/tags.changes', 'w',
1902 with repo.vfs('changes/tags.changes', 'w',
1899 atomictemp=True) as changesfile:
1903 atomictemp=True) as changesfile:
1900 # note: we do not register the file to the transaction
1904 # note: we do not register the file to the transaction
1901 # because we needs it to still exist on the transaction
1905 # because we needs it to still exist on the transaction
1902 # is close (for txnclose hooks)
1906 # is close (for txnclose hooks)
1903 tagsmod.writediff(changesfile, changes)
1907 tagsmod.writediff(changesfile, changes)
1904 def validate(tr2):
1908 def validate(tr2):
1905 """will run pre-closing hooks"""
1909 """will run pre-closing hooks"""
1906 # XXX the transaction API is a bit lacking here so we take a hacky
1910 # XXX the transaction API is a bit lacking here so we take a hacky
1907 # path for now
1911 # path for now
1908 #
1912 #
1909 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
1913 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
1910 # dict is copied before these run. In addition we needs the data
1914 # dict is copied before these run. In addition we needs the data
1911 # available to in memory hooks too.
1915 # available to in memory hooks too.
1912 #
1916 #
1913 # Moreover, we also need to make sure this runs before txnclose
1917 # Moreover, we also need to make sure this runs before txnclose
1914 # hooks and there is no "pending" mechanism that would execute
1918 # hooks and there is no "pending" mechanism that would execute
1915 # logic only if hooks are about to run.
1919 # logic only if hooks are about to run.
1916 #
1920 #
1917 # Fixing this limitation of the transaction is also needed to track
1921 # Fixing this limitation of the transaction is also needed to track
1918 # other families of changes (bookmarks, phases, obsolescence).
1922 # other families of changes (bookmarks, phases, obsolescence).
1919 #
1923 #
1920 # This will have to be fixed before we remove the experimental
1924 # This will have to be fixed before we remove the experimental
1921 # gating.
1925 # gating.
1922 tracktags(tr2)
1926 tracktags(tr2)
1923 repo = reporef()
1927 repo = reporef()
1924 if repo.ui.configbool('experimental', 'single-head-per-branch'):
1928 if repo.ui.configbool('experimental', 'single-head-per-branch'):
1925 scmutil.enforcesinglehead(repo, tr2, desc)
1929 scmutil.enforcesinglehead(repo, tr2, desc)
1926 if hook.hashook(repo.ui, 'pretxnclose-bookmark'):
1930 if hook.hashook(repo.ui, 'pretxnclose-bookmark'):
1927 for name, (old, new) in sorted(tr.changes['bookmarks'].items()):
1931 for name, (old, new) in sorted(tr.changes['bookmarks'].items()):
1928 args = tr.hookargs.copy()
1932 args = tr.hookargs.copy()
1929 args.update(bookmarks.preparehookargs(name, old, new))
1933 args.update(bookmarks.preparehookargs(name, old, new))
1930 repo.hook('pretxnclose-bookmark', throw=True,
1934 repo.hook('pretxnclose-bookmark', throw=True,
1931 **pycompat.strkwargs(args))
1935 **pycompat.strkwargs(args))
1932 if hook.hashook(repo.ui, 'pretxnclose-phase'):
1936 if hook.hashook(repo.ui, 'pretxnclose-phase'):
1933 cl = repo.unfiltered().changelog
1937 cl = repo.unfiltered().changelog
1934 for rev, (old, new) in tr.changes['phases'].items():
1938 for rev, (old, new) in tr.changes['phases'].items():
1935 args = tr.hookargs.copy()
1939 args = tr.hookargs.copy()
1936 node = hex(cl.node(rev))
1940 node = hex(cl.node(rev))
1937 args.update(phases.preparehookargs(node, old, new))
1941 args.update(phases.preparehookargs(node, old, new))
1938 repo.hook('pretxnclose-phase', throw=True,
1942 repo.hook('pretxnclose-phase', throw=True,
1939 **pycompat.strkwargs(args))
1943 **pycompat.strkwargs(args))
1940
1944
1941 repo.hook('pretxnclose', throw=True,
1945 repo.hook('pretxnclose', throw=True,
1942 **pycompat.strkwargs(tr.hookargs))
1946 **pycompat.strkwargs(tr.hookargs))
1943 def releasefn(tr, success):
1947 def releasefn(tr, success):
1944 repo = reporef()
1948 repo = reporef()
1945 if repo is None:
1949 if repo is None:
1946 # If the repo has been GC'd (and this release function is being
1950 # If the repo has been GC'd (and this release function is being
1947 # called from transaction.__del__), there's not much we can do,
1951 # called from transaction.__del__), there's not much we can do,
1948 # so just leave the unfinished transaction there and let the
1952 # so just leave the unfinished transaction there and let the
1949 # user run `hg recover`.
1953 # user run `hg recover`.
1950 return
1954 return
1951 if success:
1955 if success:
1952 # this should be explicitly invoked here, because
1956 # this should be explicitly invoked here, because
1953 # in-memory changes aren't written out at closing
1957 # in-memory changes aren't written out at closing
1954 # transaction, if tr.addfilegenerator (via
1958 # transaction, if tr.addfilegenerator (via
1955 # dirstate.write or so) isn't invoked while
1959 # dirstate.write or so) isn't invoked while
1956 # transaction running
1960 # transaction running
1957 repo.dirstate.write(None)
1961 repo.dirstate.write(None)
1958 else:
1962 else:
1959 # discard all changes (including ones already written
1963 # discard all changes (including ones already written
1960 # out) in this transaction
1964 # out) in this transaction
1961 narrowspec.restorebackup(self, 'journal.narrowspec')
1965 narrowspec.restorebackup(self, 'journal.narrowspec')
1962 narrowspec.restorewcbackup(self, 'journal.narrowspec.dirstate')
1966 narrowspec.restorewcbackup(self, 'journal.narrowspec.dirstate')
1963 repo.dirstate.restorebackup(None, 'journal.dirstate')
1967 repo.dirstate.restorebackup(None, 'journal.dirstate')
1964
1968
1965 repo.invalidate(clearfilecache=True)
1969 repo.invalidate(clearfilecache=True)
1966
1970
1967 tr = transaction.transaction(rp, self.svfs, vfsmap,
1971 tr = transaction.transaction(rp, self.svfs, vfsmap,
1968 "journal",
1972 "journal",
1969 "undo",
1973 "undo",
1970 aftertrans(renames),
1974 aftertrans(renames),
1971 self.store.createmode,
1975 self.store.createmode,
1972 validator=validate,
1976 validator=validate,
1973 releasefn=releasefn,
1977 releasefn=releasefn,
1974 checkambigfiles=_cachedfiles,
1978 checkambigfiles=_cachedfiles,
1975 name=desc)
1979 name=desc)
1976 tr.changes['origrepolen'] = len(self)
1980 tr.changes['origrepolen'] = len(self)
1977 tr.changes['obsmarkers'] = set()
1981 tr.changes['obsmarkers'] = set()
1978 tr.changes['phases'] = {}
1982 tr.changes['phases'] = {}
1979 tr.changes['bookmarks'] = {}
1983 tr.changes['bookmarks'] = {}
1980
1984
1981 tr.hookargs['txnid'] = txnid
1985 tr.hookargs['txnid'] = txnid
1982 tr.hookargs['txnname'] = desc
1986 tr.hookargs['txnname'] = desc
1983 # note: writing the fncache only during finalize mean that the file is
1987 # note: writing the fncache only during finalize mean that the file is
1984 # outdated when running hooks. As fncache is used for streaming clone,
1988 # outdated when running hooks. As fncache is used for streaming clone,
1985 # this is not expected to break anything that happen during the hooks.
1989 # this is not expected to break anything that happen during the hooks.
1986 tr.addfinalize('flush-fncache', self.store.write)
1990 tr.addfinalize('flush-fncache', self.store.write)
1987 def txnclosehook(tr2):
1991 def txnclosehook(tr2):
1988 """To be run if transaction is successful, will schedule a hook run
1992 """To be run if transaction is successful, will schedule a hook run
1989 """
1993 """
1990 # Don't reference tr2 in hook() so we don't hold a reference.
1994 # Don't reference tr2 in hook() so we don't hold a reference.
1991 # This reduces memory consumption when there are multiple
1995 # This reduces memory consumption when there are multiple
1992 # transactions per lock. This can likely go away if issue5045
1996 # transactions per lock. This can likely go away if issue5045
1993 # fixes the function accumulation.
1997 # fixes the function accumulation.
1994 hookargs = tr2.hookargs
1998 hookargs = tr2.hookargs
1995
1999
1996 def hookfunc():
2000 def hookfunc():
1997 repo = reporef()
2001 repo = reporef()
1998 if hook.hashook(repo.ui, 'txnclose-bookmark'):
2002 if hook.hashook(repo.ui, 'txnclose-bookmark'):
1999 bmchanges = sorted(tr.changes['bookmarks'].items())
2003 bmchanges = sorted(tr.changes['bookmarks'].items())
2000 for name, (old, new) in bmchanges:
2004 for name, (old, new) in bmchanges:
2001 args = tr.hookargs.copy()
2005 args = tr.hookargs.copy()
2002 args.update(bookmarks.preparehookargs(name, old, new))
2006 args.update(bookmarks.preparehookargs(name, old, new))
2003 repo.hook('txnclose-bookmark', throw=False,
2007 repo.hook('txnclose-bookmark', throw=False,
2004 **pycompat.strkwargs(args))
2008 **pycompat.strkwargs(args))
2005
2009
2006 if hook.hashook(repo.ui, 'txnclose-phase'):
2010 if hook.hashook(repo.ui, 'txnclose-phase'):
2007 cl = repo.unfiltered().changelog
2011 cl = repo.unfiltered().changelog
2008 phasemv = sorted(tr.changes['phases'].items())
2012 phasemv = sorted(tr.changes['phases'].items())
2009 for rev, (old, new) in phasemv:
2013 for rev, (old, new) in phasemv:
2010 args = tr.hookargs.copy()
2014 args = tr.hookargs.copy()
2011 node = hex(cl.node(rev))
2015 node = hex(cl.node(rev))
2012 args.update(phases.preparehookargs(node, old, new))
2016 args.update(phases.preparehookargs(node, old, new))
2013 repo.hook('txnclose-phase', throw=False,
2017 repo.hook('txnclose-phase', throw=False,
2014 **pycompat.strkwargs(args))
2018 **pycompat.strkwargs(args))
2015
2019
2016 repo.hook('txnclose', throw=False,
2020 repo.hook('txnclose', throw=False,
2017 **pycompat.strkwargs(hookargs))
2021 **pycompat.strkwargs(hookargs))
2018 reporef()._afterlock(hookfunc)
2022 reporef()._afterlock(hookfunc)
2019 tr.addfinalize('txnclose-hook', txnclosehook)
2023 tr.addfinalize('txnclose-hook', txnclosehook)
2020 # Include a leading "-" to make it happen before the transaction summary
2024 # Include a leading "-" to make it happen before the transaction summary
2021 # reports registered via scmutil.registersummarycallback() whose names
2025 # reports registered via scmutil.registersummarycallback() whose names
2022 # are 00-txnreport etc. That way, the caches will be warm when the
2026 # are 00-txnreport etc. That way, the caches will be warm when the
2023 # callbacks run.
2027 # callbacks run.
2024 tr.addpostclose('-warm-cache', self._buildcacheupdater(tr))
2028 tr.addpostclose('-warm-cache', self._buildcacheupdater(tr))
2025 def txnaborthook(tr2):
2029 def txnaborthook(tr2):
2026 """To be run if transaction is aborted
2030 """To be run if transaction is aborted
2027 """
2031 """
2028 reporef().hook('txnabort', throw=False,
2032 reporef().hook('txnabort', throw=False,
2029 **pycompat.strkwargs(tr2.hookargs))
2033 **pycompat.strkwargs(tr2.hookargs))
2030 tr.addabort('txnabort-hook', txnaborthook)
2034 tr.addabort('txnabort-hook', txnaborthook)
2031 # avoid eager cache invalidation. in-memory data should be identical
2035 # avoid eager cache invalidation. in-memory data should be identical
2032 # to stored data if transaction has no error.
2036 # to stored data if transaction has no error.
2033 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
2037 tr.addpostclose('refresh-filecachestats', self._refreshfilecachestats)
2034 self._transref = weakref.ref(tr)
2038 self._transref = weakref.ref(tr)
2035 scmutil.registersummarycallback(self, tr, desc)
2039 scmutil.registersummarycallback(self, tr, desc)
2036 return tr
2040 return tr
2037
2041
2038 def _journalfiles(self):
2042 def _journalfiles(self):
2039 return ((self.svfs, 'journal'),
2043 return ((self.svfs, 'journal'),
2040 (self.svfs, 'journal.narrowspec'),
2044 (self.svfs, 'journal.narrowspec'),
2041 (self.vfs, 'journal.narrowspec.dirstate'),
2045 (self.vfs, 'journal.narrowspec.dirstate'),
2042 (self.vfs, 'journal.dirstate'),
2046 (self.vfs, 'journal.dirstate'),
2043 (self.vfs, 'journal.branch'),
2047 (self.vfs, 'journal.branch'),
2044 (self.vfs, 'journal.desc'),
2048 (self.vfs, 'journal.desc'),
2045 (bookmarks.bookmarksvfs(self), 'journal.bookmarks'),
2049 (bookmarks.bookmarksvfs(self), 'journal.bookmarks'),
2046 (self.svfs, 'journal.phaseroots'))
2050 (self.svfs, 'journal.phaseroots'))
2047
2051
2048 def undofiles(self):
2052 def undofiles(self):
2049 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
2053 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
2050
2054
2051 @unfilteredmethod
2055 @unfilteredmethod
2052 def _writejournal(self, desc):
2056 def _writejournal(self, desc):
2053 self.dirstate.savebackup(None, 'journal.dirstate')
2057 self.dirstate.savebackup(None, 'journal.dirstate')
2054 narrowspec.savewcbackup(self, 'journal.narrowspec.dirstate')
2058 narrowspec.savewcbackup(self, 'journal.narrowspec.dirstate')
2055 narrowspec.savebackup(self, 'journal.narrowspec')
2059 narrowspec.savebackup(self, 'journal.narrowspec')
2056 self.vfs.write("journal.branch",
2060 self.vfs.write("journal.branch",
2057 encoding.fromlocal(self.dirstate.branch()))
2061 encoding.fromlocal(self.dirstate.branch()))
2058 self.vfs.write("journal.desc",
2062 self.vfs.write("journal.desc",
2059 "%d\n%s\n" % (len(self), desc))
2063 "%d\n%s\n" % (len(self), desc))
2060 bookmarksvfs = bookmarks.bookmarksvfs(self)
2064 bookmarksvfs = bookmarks.bookmarksvfs(self)
2061 bookmarksvfs.write("journal.bookmarks",
2065 bookmarksvfs.write("journal.bookmarks",
2062 bookmarksvfs.tryread("bookmarks"))
2066 bookmarksvfs.tryread("bookmarks"))
2063 self.svfs.write("journal.phaseroots",
2067 self.svfs.write("journal.phaseroots",
2064 self.svfs.tryread("phaseroots"))
2068 self.svfs.tryread("phaseroots"))
2065
2069
2066 def recover(self):
2070 def recover(self):
2067 with self.lock():
2071 with self.lock():
2068 if self.svfs.exists("journal"):
2072 if self.svfs.exists("journal"):
2069 self.ui.status(_("rolling back interrupted transaction\n"))
2073 self.ui.status(_("rolling back interrupted transaction\n"))
2070 vfsmap = {'': self.svfs,
2074 vfsmap = {'': self.svfs,
2071 'plain': self.vfs,}
2075 'plain': self.vfs,}
2072 transaction.rollback(self.svfs, vfsmap, "journal",
2076 transaction.rollback(self.svfs, vfsmap, "journal",
2073 self.ui.warn,
2077 self.ui.warn,
2074 checkambigfiles=_cachedfiles)
2078 checkambigfiles=_cachedfiles)
2075 self.invalidate()
2079 self.invalidate()
2076 return True
2080 return True
2077 else:
2081 else:
2078 self.ui.warn(_("no interrupted transaction available\n"))
2082 self.ui.warn(_("no interrupted transaction available\n"))
2079 return False
2083 return False
2080
2084
2081 def rollback(self, dryrun=False, force=False):
2085 def rollback(self, dryrun=False, force=False):
2082 wlock = lock = dsguard = None
2086 wlock = lock = dsguard = None
2083 try:
2087 try:
2084 wlock = self.wlock()
2088 wlock = self.wlock()
2085 lock = self.lock()
2089 lock = self.lock()
2086 if self.svfs.exists("undo"):
2090 if self.svfs.exists("undo"):
2087 dsguard = dirstateguard.dirstateguard(self, 'rollback')
2091 dsguard = dirstateguard.dirstateguard(self, 'rollback')
2088
2092
2089 return self._rollback(dryrun, force, dsguard)
2093 return self._rollback(dryrun, force, dsguard)
2090 else:
2094 else:
2091 self.ui.warn(_("no rollback information available\n"))
2095 self.ui.warn(_("no rollback information available\n"))
2092 return 1
2096 return 1
2093 finally:
2097 finally:
2094 release(dsguard, lock, wlock)
2098 release(dsguard, lock, wlock)
2095
2099
2096 @unfilteredmethod # Until we get smarter cache management
2100 @unfilteredmethod # Until we get smarter cache management
2097 def _rollback(self, dryrun, force, dsguard):
2101 def _rollback(self, dryrun, force, dsguard):
2098 ui = self.ui
2102 ui = self.ui
2099 try:
2103 try:
2100 args = self.vfs.read('undo.desc').splitlines()
2104 args = self.vfs.read('undo.desc').splitlines()
2101 (oldlen, desc, detail) = (int(args[0]), args[1], None)
2105 (oldlen, desc, detail) = (int(args[0]), args[1], None)
2102 if len(args) >= 3:
2106 if len(args) >= 3:
2103 detail = args[2]
2107 detail = args[2]
2104 oldtip = oldlen - 1
2108 oldtip = oldlen - 1
2105
2109
2106 if detail and ui.verbose:
2110 if detail and ui.verbose:
2107 msg = (_('repository tip rolled back to revision %d'
2111 msg = (_('repository tip rolled back to revision %d'
2108 ' (undo %s: %s)\n')
2112 ' (undo %s: %s)\n')
2109 % (oldtip, desc, detail))
2113 % (oldtip, desc, detail))
2110 else:
2114 else:
2111 msg = (_('repository tip rolled back to revision %d'
2115 msg = (_('repository tip rolled back to revision %d'
2112 ' (undo %s)\n')
2116 ' (undo %s)\n')
2113 % (oldtip, desc))
2117 % (oldtip, desc))
2114 except IOError:
2118 except IOError:
2115 msg = _('rolling back unknown transaction\n')
2119 msg = _('rolling back unknown transaction\n')
2116 desc = None
2120 desc = None
2117
2121
2118 if not force and self['.'] != self['tip'] and desc == 'commit':
2122 if not force and self['.'] != self['tip'] and desc == 'commit':
2119 raise error.Abort(
2123 raise error.Abort(
2120 _('rollback of last commit while not checked out '
2124 _('rollback of last commit while not checked out '
2121 'may lose data'), hint=_('use -f to force'))
2125 'may lose data'), hint=_('use -f to force'))
2122
2126
2123 ui.status(msg)
2127 ui.status(msg)
2124 if dryrun:
2128 if dryrun:
2125 return 0
2129 return 0
2126
2130
2127 parents = self.dirstate.parents()
2131 parents = self.dirstate.parents()
2128 self.destroying()
2132 self.destroying()
2129 vfsmap = {'plain': self.vfs, '': self.svfs}
2133 vfsmap = {'plain': self.vfs, '': self.svfs}
2130 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn,
2134 transaction.rollback(self.svfs, vfsmap, 'undo', ui.warn,
2131 checkambigfiles=_cachedfiles)
2135 checkambigfiles=_cachedfiles)
2132 bookmarksvfs = bookmarks.bookmarksvfs(self)
2136 bookmarksvfs = bookmarks.bookmarksvfs(self)
2133 if bookmarksvfs.exists('undo.bookmarks'):
2137 if bookmarksvfs.exists('undo.bookmarks'):
2134 bookmarksvfs.rename('undo.bookmarks', 'bookmarks', checkambig=True)
2138 bookmarksvfs.rename('undo.bookmarks', 'bookmarks', checkambig=True)
2135 if self.svfs.exists('undo.phaseroots'):
2139 if self.svfs.exists('undo.phaseroots'):
2136 self.svfs.rename('undo.phaseroots', 'phaseroots', checkambig=True)
2140 self.svfs.rename('undo.phaseroots', 'phaseroots', checkambig=True)
2137 self.invalidate()
2141 self.invalidate()
2138
2142
2139 parentgone = any(p not in self.changelog.nodemap for p in parents)
2143 parentgone = any(p not in self.changelog.nodemap for p in parents)
2140 if parentgone:
2144 if parentgone:
2141 # prevent dirstateguard from overwriting already restored one
2145 # prevent dirstateguard from overwriting already restored one
2142 dsguard.close()
2146 dsguard.close()
2143
2147
2144 narrowspec.restorebackup(self, 'undo.narrowspec')
2148 narrowspec.restorebackup(self, 'undo.narrowspec')
2145 narrowspec.restorewcbackup(self, 'undo.narrowspec.dirstate')
2149 narrowspec.restorewcbackup(self, 'undo.narrowspec.dirstate')
2146 self.dirstate.restorebackup(None, 'undo.dirstate')
2150 self.dirstate.restorebackup(None, 'undo.dirstate')
2147 try:
2151 try:
2148 branch = self.vfs.read('undo.branch')
2152 branch = self.vfs.read('undo.branch')
2149 self.dirstate.setbranch(encoding.tolocal(branch))
2153 self.dirstate.setbranch(encoding.tolocal(branch))
2150 except IOError:
2154 except IOError:
2151 ui.warn(_('named branch could not be reset: '
2155 ui.warn(_('named branch could not be reset: '
2152 'current branch is still \'%s\'\n')
2156 'current branch is still \'%s\'\n')
2153 % self.dirstate.branch())
2157 % self.dirstate.branch())
2154
2158
2155 parents = tuple([p.rev() for p in self[None].parents()])
2159 parents = tuple([p.rev() for p in self[None].parents()])
2156 if len(parents) > 1:
2160 if len(parents) > 1:
2157 ui.status(_('working directory now based on '
2161 ui.status(_('working directory now based on '
2158 'revisions %d and %d\n') % parents)
2162 'revisions %d and %d\n') % parents)
2159 else:
2163 else:
2160 ui.status(_('working directory now based on '
2164 ui.status(_('working directory now based on '
2161 'revision %d\n') % parents)
2165 'revision %d\n') % parents)
2162 mergemod.mergestate.clean(self, self['.'].node())
2166 mergemod.mergestate.clean(self, self['.'].node())
2163
2167
2164 # TODO: if we know which new heads may result from this rollback, pass
2168 # TODO: if we know which new heads may result from this rollback, pass
2165 # them to destroy(), which will prevent the branchhead cache from being
2169 # them to destroy(), which will prevent the branchhead cache from being
2166 # invalidated.
2170 # invalidated.
2167 self.destroyed()
2171 self.destroyed()
2168 return 0
2172 return 0
2169
2173
2170 def _buildcacheupdater(self, newtransaction):
2174 def _buildcacheupdater(self, newtransaction):
2171 """called during transaction to build the callback updating cache
2175 """called during transaction to build the callback updating cache
2172
2176
2173 Lives on the repository to help extension who might want to augment
2177 Lives on the repository to help extension who might want to augment
2174 this logic. For this purpose, the created transaction is passed to the
2178 this logic. For this purpose, the created transaction is passed to the
2175 method.
2179 method.
2176 """
2180 """
2177 # we must avoid cyclic reference between repo and transaction.
2181 # we must avoid cyclic reference between repo and transaction.
2178 reporef = weakref.ref(self)
2182 reporef = weakref.ref(self)
2179 def updater(tr):
2183 def updater(tr):
2180 repo = reporef()
2184 repo = reporef()
2181 repo.updatecaches(tr)
2185 repo.updatecaches(tr)
2182 return updater
2186 return updater
2183
2187
2184 @unfilteredmethod
2188 @unfilteredmethod
2185 def updatecaches(self, tr=None, full=False):
2189 def updatecaches(self, tr=None, full=False):
2186 """warm appropriate caches
2190 """warm appropriate caches
2187
2191
2188 If this function is called after a transaction closed. The transaction
2192 If this function is called after a transaction closed. The transaction
2189 will be available in the 'tr' argument. This can be used to selectively
2193 will be available in the 'tr' argument. This can be used to selectively
2190 update caches relevant to the changes in that transaction.
2194 update caches relevant to the changes in that transaction.
2191
2195
2192 If 'full' is set, make sure all caches the function knows about have
2196 If 'full' is set, make sure all caches the function knows about have
2193 up-to-date data. Even the ones usually loaded more lazily.
2197 up-to-date data. Even the ones usually loaded more lazily.
2194 """
2198 """
2195 if tr is not None and tr.hookargs.get('source') == 'strip':
2199 if tr is not None and tr.hookargs.get('source') == 'strip':
2196 # During strip, many caches are invalid but
2200 # During strip, many caches are invalid but
2197 # later call to `destroyed` will refresh them.
2201 # later call to `destroyed` will refresh them.
2198 return
2202 return
2199
2203
2200 if tr is None or tr.changes['origrepolen'] < len(self):
2204 if tr is None or tr.changes['origrepolen'] < len(self):
2201 # accessing the 'ser ved' branchmap should refresh all the others,
2205 # accessing the 'ser ved' branchmap should refresh all the others,
2202 self.ui.debug('updating the branch cache\n')
2206 self.ui.debug('updating the branch cache\n')
2203 self.filtered('served').branchmap()
2207 self.filtered('served').branchmap()
2204 self.filtered('served.hidden').branchmap()
2208 self.filtered('served.hidden').branchmap()
2205
2209
2206 if full:
2210 if full:
2207 unfi = self.unfiltered()
2211 unfi = self.unfiltered()
2208 rbc = unfi.revbranchcache()
2212 rbc = unfi.revbranchcache()
2209 for r in unfi.changelog:
2213 for r in unfi.changelog:
2210 rbc.branchinfo(r)
2214 rbc.branchinfo(r)
2211 rbc.write()
2215 rbc.write()
2212
2216
2213 # ensure the working copy parents are in the manifestfulltextcache
2217 # ensure the working copy parents are in the manifestfulltextcache
2214 for ctx in self['.'].parents():
2218 for ctx in self['.'].parents():
2215 ctx.manifest() # accessing the manifest is enough
2219 ctx.manifest() # accessing the manifest is enough
2216
2220
2217 # accessing fnode cache warms the cache
2221 # accessing fnode cache warms the cache
2218 tagsmod.fnoderevs(self.ui, unfi, unfi.changelog.revs())
2222 tagsmod.fnoderevs(self.ui, unfi, unfi.changelog.revs())
2219 # accessing tags warm the cache
2223 # accessing tags warm the cache
2220 self.tags()
2224 self.tags()
2221 self.filtered('served').tags()
2225 self.filtered('served').tags()
2222
2226
2223 # The `full` arg is documented as updating even the lazily-loaded
2227 # The `full` arg is documented as updating even the lazily-loaded
2224 # caches immediately, so we're forcing a write to cause these caches
2228 # caches immediately, so we're forcing a write to cause these caches
2225 # to be warmed up even if they haven't explicitly been requested
2229 # to be warmed up even if they haven't explicitly been requested
2226 # yet (if they've never been used by hg, they won't ever have been
2230 # yet (if they've never been used by hg, they won't ever have been
2227 # written, even if they're a subset of another kind of cache that
2231 # written, even if they're a subset of another kind of cache that
2228 # *has* been used).
2232 # *has* been used).
2229 for filt in repoview.filtertable.keys():
2233 for filt in repoview.filtertable.keys():
2230 filtered = self.filtered(filt)
2234 filtered = self.filtered(filt)
2231 filtered.branchmap().write(filtered)
2235 filtered.branchmap().write(filtered)
2232
2236
2233 def invalidatecaches(self):
2237 def invalidatecaches(self):
2234
2238
2235 if r'_tagscache' in vars(self):
2239 if r'_tagscache' in vars(self):
2236 # can't use delattr on proxy
2240 # can't use delattr on proxy
2237 del self.__dict__[r'_tagscache']
2241 del self.__dict__[r'_tagscache']
2238
2242
2239 self._branchcaches.clear()
2243 self._branchcaches.clear()
2240 self.invalidatevolatilesets()
2244 self.invalidatevolatilesets()
2241 self._sparsesignaturecache.clear()
2245 self._sparsesignaturecache.clear()
2242
2246
2243 def invalidatevolatilesets(self):
2247 def invalidatevolatilesets(self):
2244 self.filteredrevcache.clear()
2248 self.filteredrevcache.clear()
2245 obsolete.clearobscaches(self)
2249 obsolete.clearobscaches(self)
2246
2250
2247 def invalidatedirstate(self):
2251 def invalidatedirstate(self):
2248 '''Invalidates the dirstate, causing the next call to dirstate
2252 '''Invalidates the dirstate, causing the next call to dirstate
2249 to check if it was modified since the last time it was read,
2253 to check if it was modified since the last time it was read,
2250 rereading it if it has.
2254 rereading it if it has.
2251
2255
2252 This is different to dirstate.invalidate() that it doesn't always
2256 This is different to dirstate.invalidate() that it doesn't always
2253 rereads the dirstate. Use dirstate.invalidate() if you want to
2257 rereads the dirstate. Use dirstate.invalidate() if you want to
2254 explicitly read the dirstate again (i.e. restoring it to a previous
2258 explicitly read the dirstate again (i.e. restoring it to a previous
2255 known good state).'''
2259 known good state).'''
2256 if hasunfilteredcache(self, r'dirstate'):
2260 if hasunfilteredcache(self, r'dirstate'):
2257 for k in self.dirstate._filecache:
2261 for k in self.dirstate._filecache:
2258 try:
2262 try:
2259 delattr(self.dirstate, k)
2263 delattr(self.dirstate, k)
2260 except AttributeError:
2264 except AttributeError:
2261 pass
2265 pass
2262 delattr(self.unfiltered(), r'dirstate')
2266 delattr(self.unfiltered(), r'dirstate')
2263
2267
2264 def invalidate(self, clearfilecache=False):
2268 def invalidate(self, clearfilecache=False):
2265 '''Invalidates both store and non-store parts other than dirstate
2269 '''Invalidates both store and non-store parts other than dirstate
2266
2270
2267 If a transaction is running, invalidation of store is omitted,
2271 If a transaction is running, invalidation of store is omitted,
2268 because discarding in-memory changes might cause inconsistency
2272 because discarding in-memory changes might cause inconsistency
2269 (e.g. incomplete fncache causes unintentional failure, but
2273 (e.g. incomplete fncache causes unintentional failure, but
2270 redundant one doesn't).
2274 redundant one doesn't).
2271 '''
2275 '''
2272 unfiltered = self.unfiltered() # all file caches are stored unfiltered
2276 unfiltered = self.unfiltered() # all file caches are stored unfiltered
2273 for k in list(self._filecache.keys()):
2277 for k in list(self._filecache.keys()):
2274 # dirstate is invalidated separately in invalidatedirstate()
2278 # dirstate is invalidated separately in invalidatedirstate()
2275 if k == 'dirstate':
2279 if k == 'dirstate':
2276 continue
2280 continue
2277 if (k == 'changelog' and
2281 if (k == 'changelog' and
2278 self.currenttransaction() and
2282 self.currenttransaction() and
2279 self.changelog._delayed):
2283 self.changelog._delayed):
2280 # The changelog object may store unwritten revisions. We don't
2284 # The changelog object may store unwritten revisions. We don't
2281 # want to lose them.
2285 # want to lose them.
2282 # TODO: Solve the problem instead of working around it.
2286 # TODO: Solve the problem instead of working around it.
2283 continue
2287 continue
2284
2288
2285 if clearfilecache:
2289 if clearfilecache:
2286 del self._filecache[k]
2290 del self._filecache[k]
2287 try:
2291 try:
2288 delattr(unfiltered, k)
2292 delattr(unfiltered, k)
2289 except AttributeError:
2293 except AttributeError:
2290 pass
2294 pass
2291 self.invalidatecaches()
2295 self.invalidatecaches()
2292 if not self.currenttransaction():
2296 if not self.currenttransaction():
2293 # TODO: Changing contents of store outside transaction
2297 # TODO: Changing contents of store outside transaction
2294 # causes inconsistency. We should make in-memory store
2298 # causes inconsistency. We should make in-memory store
2295 # changes detectable, and abort if changed.
2299 # changes detectable, and abort if changed.
2296 self.store.invalidatecaches()
2300 self.store.invalidatecaches()
2297
2301
2298 def invalidateall(self):
2302 def invalidateall(self):
2299 '''Fully invalidates both store and non-store parts, causing the
2303 '''Fully invalidates both store and non-store parts, causing the
2300 subsequent operation to reread any outside changes.'''
2304 subsequent operation to reread any outside changes.'''
2301 # extension should hook this to invalidate its caches
2305 # extension should hook this to invalidate its caches
2302 self.invalidate()
2306 self.invalidate()
2303 self.invalidatedirstate()
2307 self.invalidatedirstate()
2304
2308
2305 @unfilteredmethod
2309 @unfilteredmethod
2306 def _refreshfilecachestats(self, tr):
2310 def _refreshfilecachestats(self, tr):
2307 """Reload stats of cached files so that they are flagged as valid"""
2311 """Reload stats of cached files so that they are flagged as valid"""
2308 for k, ce in self._filecache.items():
2312 for k, ce in self._filecache.items():
2309 k = pycompat.sysstr(k)
2313 k = pycompat.sysstr(k)
2310 if k == r'dirstate' or k not in self.__dict__:
2314 if k == r'dirstate' or k not in self.__dict__:
2311 continue
2315 continue
2312 ce.refresh()
2316 ce.refresh()
2313
2317
2314 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
2318 def _lock(self, vfs, lockname, wait, releasefn, acquirefn, desc,
2315 inheritchecker=None, parentenvvar=None):
2319 inheritchecker=None, parentenvvar=None):
2316 parentlock = None
2320 parentlock = None
2317 # the contents of parentenvvar are used by the underlying lock to
2321 # the contents of parentenvvar are used by the underlying lock to
2318 # determine whether it can be inherited
2322 # determine whether it can be inherited
2319 if parentenvvar is not None:
2323 if parentenvvar is not None:
2320 parentlock = encoding.environ.get(parentenvvar)
2324 parentlock = encoding.environ.get(parentenvvar)
2321
2325
2322 timeout = 0
2326 timeout = 0
2323 warntimeout = 0
2327 warntimeout = 0
2324 if wait:
2328 if wait:
2325 timeout = self.ui.configint("ui", "timeout")
2329 timeout = self.ui.configint("ui", "timeout")
2326 warntimeout = self.ui.configint("ui", "timeout.warn")
2330 warntimeout = self.ui.configint("ui", "timeout.warn")
2327 # internal config: ui.signal-safe-lock
2331 # internal config: ui.signal-safe-lock
2328 signalsafe = self.ui.configbool('ui', 'signal-safe-lock')
2332 signalsafe = self.ui.configbool('ui', 'signal-safe-lock')
2329
2333
2330 l = lockmod.trylock(self.ui, vfs, lockname, timeout, warntimeout,
2334 l = lockmod.trylock(self.ui, vfs, lockname, timeout, warntimeout,
2331 releasefn=releasefn,
2335 releasefn=releasefn,
2332 acquirefn=acquirefn, desc=desc,
2336 acquirefn=acquirefn, desc=desc,
2333 inheritchecker=inheritchecker,
2337 inheritchecker=inheritchecker,
2334 parentlock=parentlock,
2338 parentlock=parentlock,
2335 signalsafe=signalsafe)
2339 signalsafe=signalsafe)
2336 return l
2340 return l
2337
2341
2338 def _afterlock(self, callback):
2342 def _afterlock(self, callback):
2339 """add a callback to be run when the repository is fully unlocked
2343 """add a callback to be run when the repository is fully unlocked
2340
2344
2341 The callback will be executed when the outermost lock is released
2345 The callback will be executed when the outermost lock is released
2342 (with wlock being higher level than 'lock')."""
2346 (with wlock being higher level than 'lock')."""
2343 for ref in (self._wlockref, self._lockref):
2347 for ref in (self._wlockref, self._lockref):
2344 l = ref and ref()
2348 l = ref and ref()
2345 if l and l.held:
2349 if l and l.held:
2346 l.postrelease.append(callback)
2350 l.postrelease.append(callback)
2347 break
2351 break
2348 else: # no lock have been found.
2352 else: # no lock have been found.
2349 callback()
2353 callback()
2350
2354
2351 def lock(self, wait=True):
2355 def lock(self, wait=True):
2352 '''Lock the repository store (.hg/store) and return a weak reference
2356 '''Lock the repository store (.hg/store) and return a weak reference
2353 to the lock. Use this before modifying the store (e.g. committing or
2357 to the lock. Use this before modifying the store (e.g. committing or
2354 stripping). If you are opening a transaction, get a lock as well.)
2358 stripping). If you are opening a transaction, get a lock as well.)
2355
2359
2356 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2360 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2357 'wlock' first to avoid a dead-lock hazard.'''
2361 'wlock' first to avoid a dead-lock hazard.'''
2358 l = self._currentlock(self._lockref)
2362 l = self._currentlock(self._lockref)
2359 if l is not None:
2363 if l is not None:
2360 l.lock()
2364 l.lock()
2361 return l
2365 return l
2362
2366
2363 l = self._lock(vfs=self.svfs,
2367 l = self._lock(vfs=self.svfs,
2364 lockname="lock",
2368 lockname="lock",
2365 wait=wait,
2369 wait=wait,
2366 releasefn=None,
2370 releasefn=None,
2367 acquirefn=self.invalidate,
2371 acquirefn=self.invalidate,
2368 desc=_('repository %s') % self.origroot)
2372 desc=_('repository %s') % self.origroot)
2369 self._lockref = weakref.ref(l)
2373 self._lockref = weakref.ref(l)
2370 return l
2374 return l
2371
2375
2372 def _wlockchecktransaction(self):
2376 def _wlockchecktransaction(self):
2373 if self.currenttransaction() is not None:
2377 if self.currenttransaction() is not None:
2374 raise error.LockInheritanceContractViolation(
2378 raise error.LockInheritanceContractViolation(
2375 'wlock cannot be inherited in the middle of a transaction')
2379 'wlock cannot be inherited in the middle of a transaction')
2376
2380
2377 def wlock(self, wait=True):
2381 def wlock(self, wait=True):
2378 '''Lock the non-store parts of the repository (everything under
2382 '''Lock the non-store parts of the repository (everything under
2379 .hg except .hg/store) and return a weak reference to the lock.
2383 .hg except .hg/store) and return a weak reference to the lock.
2380
2384
2381 Use this before modifying files in .hg.
2385 Use this before modifying files in .hg.
2382
2386
2383 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2387 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2384 'wlock' first to avoid a dead-lock hazard.'''
2388 'wlock' first to avoid a dead-lock hazard.'''
2385 l = self._wlockref and self._wlockref()
2389 l = self._wlockref and self._wlockref()
2386 if l is not None and l.held:
2390 if l is not None and l.held:
2387 l.lock()
2391 l.lock()
2388 return l
2392 return l
2389
2393
2390 # We do not need to check for non-waiting lock acquisition. Such
2394 # We do not need to check for non-waiting lock acquisition. Such
2391 # acquisition would not cause dead-lock as they would just fail.
2395 # acquisition would not cause dead-lock as they would just fail.
2392 if wait and (self.ui.configbool('devel', 'all-warnings')
2396 if wait and (self.ui.configbool('devel', 'all-warnings')
2393 or self.ui.configbool('devel', 'check-locks')):
2397 or self.ui.configbool('devel', 'check-locks')):
2394 if self._currentlock(self._lockref) is not None:
2398 if self._currentlock(self._lockref) is not None:
2395 self.ui.develwarn('"wlock" acquired after "lock"')
2399 self.ui.develwarn('"wlock" acquired after "lock"')
2396
2400
2397 def unlock():
2401 def unlock():
2398 if self.dirstate.pendingparentchange():
2402 if self.dirstate.pendingparentchange():
2399 self.dirstate.invalidate()
2403 self.dirstate.invalidate()
2400 else:
2404 else:
2401 self.dirstate.write(None)
2405 self.dirstate.write(None)
2402
2406
2403 self._filecache['dirstate'].refresh()
2407 self._filecache['dirstate'].refresh()
2404
2408
2405 l = self._lock(self.vfs, "wlock", wait, unlock,
2409 l = self._lock(self.vfs, "wlock", wait, unlock,
2406 self.invalidatedirstate, _('working directory of %s') %
2410 self.invalidatedirstate, _('working directory of %s') %
2407 self.origroot,
2411 self.origroot,
2408 inheritchecker=self._wlockchecktransaction,
2412 inheritchecker=self._wlockchecktransaction,
2409 parentenvvar='HG_WLOCK_LOCKER')
2413 parentenvvar='HG_WLOCK_LOCKER')
2410 self._wlockref = weakref.ref(l)
2414 self._wlockref = weakref.ref(l)
2411 return l
2415 return l
2412
2416
2413 def _currentlock(self, lockref):
2417 def _currentlock(self, lockref):
2414 """Returns the lock if it's held, or None if it's not."""
2418 """Returns the lock if it's held, or None if it's not."""
2415 if lockref is None:
2419 if lockref is None:
2416 return None
2420 return None
2417 l = lockref()
2421 l = lockref()
2418 if l is None or not l.held:
2422 if l is None or not l.held:
2419 return None
2423 return None
2420 return l
2424 return l
2421
2425
2422 def currentwlock(self):
2426 def currentwlock(self):
2423 """Returns the wlock if it's held, or None if it's not."""
2427 """Returns the wlock if it's held, or None if it's not."""
2424 return self._currentlock(self._wlockref)
2428 return self._currentlock(self._wlockref)
2425
2429
2426 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist,
2430 def _filecommit(self, fctx, manifest1, manifest2, linkrev, tr, changelist,
2427 includecopymeta):
2431 includecopymeta):
2428 """
2432 """
2429 commit an individual file as part of a larger transaction
2433 commit an individual file as part of a larger transaction
2430 """
2434 """
2431
2435
2432 fname = fctx.path()
2436 fname = fctx.path()
2433 fparent1 = manifest1.get(fname, nullid)
2437 fparent1 = manifest1.get(fname, nullid)
2434 fparent2 = manifest2.get(fname, nullid)
2438 fparent2 = manifest2.get(fname, nullid)
2435 if isinstance(fctx, context.filectx):
2439 if isinstance(fctx, context.filectx):
2436 node = fctx.filenode()
2440 node = fctx.filenode()
2437 if node in [fparent1, fparent2]:
2441 if node in [fparent1, fparent2]:
2438 self.ui.debug('reusing %s filelog entry\n' % fname)
2442 self.ui.debug('reusing %s filelog entry\n' % fname)
2439 if ((fparent1 != nullid and
2443 if ((fparent1 != nullid and
2440 manifest1.flags(fname) != fctx.flags()) or
2444 manifest1.flags(fname) != fctx.flags()) or
2441 (fparent2 != nullid and
2445 (fparent2 != nullid and
2442 manifest2.flags(fname) != fctx.flags())):
2446 manifest2.flags(fname) != fctx.flags())):
2443 changelist.append(fname)
2447 changelist.append(fname)
2444 return node
2448 return node
2445
2449
2446 flog = self.file(fname)
2450 flog = self.file(fname)
2447 meta = {}
2451 meta = {}
2448 cfname = fctx.copysource()
2452 cfname = fctx.copysource()
2449 if cfname and cfname != fname:
2453 if cfname and cfname != fname:
2450 # Mark the new revision of this file as a copy of another
2454 # Mark the new revision of this file as a copy of another
2451 # file. This copy data will effectively act as a parent
2455 # file. This copy data will effectively act as a parent
2452 # of this new revision. If this is a merge, the first
2456 # of this new revision. If this is a merge, the first
2453 # parent will be the nullid (meaning "look up the copy data")
2457 # parent will be the nullid (meaning "look up the copy data")
2454 # and the second one will be the other parent. For example:
2458 # and the second one will be the other parent. For example:
2455 #
2459 #
2456 # 0 --- 1 --- 3 rev1 changes file foo
2460 # 0 --- 1 --- 3 rev1 changes file foo
2457 # \ / rev2 renames foo to bar and changes it
2461 # \ / rev2 renames foo to bar and changes it
2458 # \- 2 -/ rev3 should have bar with all changes and
2462 # \- 2 -/ rev3 should have bar with all changes and
2459 # should record that bar descends from
2463 # should record that bar descends from
2460 # bar in rev2 and foo in rev1
2464 # bar in rev2 and foo in rev1
2461 #
2465 #
2462 # this allows this merge to succeed:
2466 # this allows this merge to succeed:
2463 #
2467 #
2464 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
2468 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
2465 # \ / merging rev3 and rev4 should use bar@rev2
2469 # \ / merging rev3 and rev4 should use bar@rev2
2466 # \- 2 --- 4 as the merge base
2470 # \- 2 --- 4 as the merge base
2467 #
2471 #
2468
2472
2469 cnode = manifest1.get(cfname)
2473 cnode = manifest1.get(cfname)
2470 newfparent = fparent2
2474 newfparent = fparent2
2471
2475
2472 if manifest2: # branch merge
2476 if manifest2: # branch merge
2473 if fparent2 == nullid or cnode is None: # copied on remote side
2477 if fparent2 == nullid or cnode is None: # copied on remote side
2474 if cfname in manifest2:
2478 if cfname in manifest2:
2475 cnode = manifest2[cfname]
2479 cnode = manifest2[cfname]
2476 newfparent = fparent1
2480 newfparent = fparent1
2477
2481
2478 # Here, we used to search backwards through history to try to find
2482 # Here, we used to search backwards through history to try to find
2479 # where the file copy came from if the source of a copy was not in
2483 # where the file copy came from if the source of a copy was not in
2480 # the parent directory. However, this doesn't actually make sense to
2484 # the parent directory. However, this doesn't actually make sense to
2481 # do (what does a copy from something not in your working copy even
2485 # do (what does a copy from something not in your working copy even
2482 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
2486 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
2483 # the user that copy information was dropped, so if they didn't
2487 # the user that copy information was dropped, so if they didn't
2484 # expect this outcome it can be fixed, but this is the correct
2488 # expect this outcome it can be fixed, but this is the correct
2485 # behavior in this circumstance.
2489 # behavior in this circumstance.
2486
2490
2487 if cnode:
2491 if cnode:
2488 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(cnode)))
2492 self.ui.debug(" %s: copy %s:%s\n" % (fname, cfname, hex(cnode)))
2489 if includecopymeta:
2493 if includecopymeta:
2490 meta["copy"] = cfname
2494 meta["copy"] = cfname
2491 meta["copyrev"] = hex(cnode)
2495 meta["copyrev"] = hex(cnode)
2492 fparent1, fparent2 = nullid, newfparent
2496 fparent1, fparent2 = nullid, newfparent
2493 else:
2497 else:
2494 self.ui.warn(_("warning: can't find ancestor for '%s' "
2498 self.ui.warn(_("warning: can't find ancestor for '%s' "
2495 "copied from '%s'!\n") % (fname, cfname))
2499 "copied from '%s'!\n") % (fname, cfname))
2496
2500
2497 elif fparent1 == nullid:
2501 elif fparent1 == nullid:
2498 fparent1, fparent2 = fparent2, nullid
2502 fparent1, fparent2 = fparent2, nullid
2499 elif fparent2 != nullid:
2503 elif fparent2 != nullid:
2500 # is one parent an ancestor of the other?
2504 # is one parent an ancestor of the other?
2501 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
2505 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
2502 if fparent1 in fparentancestors:
2506 if fparent1 in fparentancestors:
2503 fparent1, fparent2 = fparent2, nullid
2507 fparent1, fparent2 = fparent2, nullid
2504 elif fparent2 in fparentancestors:
2508 elif fparent2 in fparentancestors:
2505 fparent2 = nullid
2509 fparent2 = nullid
2506
2510
2507 # is the file changed?
2511 # is the file changed?
2508 text = fctx.data()
2512 text = fctx.data()
2509 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
2513 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
2510 changelist.append(fname)
2514 changelist.append(fname)
2511 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
2515 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
2512 # are just the flags changed during merge?
2516 # are just the flags changed during merge?
2513 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
2517 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
2514 changelist.append(fname)
2518 changelist.append(fname)
2515
2519
2516 return fparent1
2520 return fparent1
2517
2521
2518 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
2522 def checkcommitpatterns(self, wctx, vdirs, match, status, fail):
2519 """check for commit arguments that aren't committable"""
2523 """check for commit arguments that aren't committable"""
2520 if match.isexact() or match.prefix():
2524 if match.isexact() or match.prefix():
2521 matched = set(status.modified + status.added + status.removed)
2525 matched = set(status.modified + status.added + status.removed)
2522
2526
2523 for f in match.files():
2527 for f in match.files():
2524 f = self.dirstate.normalize(f)
2528 f = self.dirstate.normalize(f)
2525 if f == '.' or f in matched or f in wctx.substate:
2529 if f == '.' or f in matched or f in wctx.substate:
2526 continue
2530 continue
2527 if f in status.deleted:
2531 if f in status.deleted:
2528 fail(f, _('file not found!'))
2532 fail(f, _('file not found!'))
2529 if f in vdirs: # visited directory
2533 if f in vdirs: # visited directory
2530 d = f + '/'
2534 d = f + '/'
2531 for mf in matched:
2535 for mf in matched:
2532 if mf.startswith(d):
2536 if mf.startswith(d):
2533 break
2537 break
2534 else:
2538 else:
2535 fail(f, _("no match under directory!"))
2539 fail(f, _("no match under directory!"))
2536 elif f not in self.dirstate:
2540 elif f not in self.dirstate:
2537 fail(f, _("file not tracked!"))
2541 fail(f, _("file not tracked!"))
2538
2542
2539 @unfilteredmethod
2543 @unfilteredmethod
2540 def commit(self, text="", user=None, date=None, match=None, force=False,
2544 def commit(self, text="", user=None, date=None, match=None, force=False,
2541 editor=False, extra=None):
2545 editor=False, extra=None):
2542 """Add a new revision to current repository.
2546 """Add a new revision to current repository.
2543
2547
2544 Revision information is gathered from the working directory,
2548 Revision information is gathered from the working directory,
2545 match can be used to filter the committed files. If editor is
2549 match can be used to filter the committed files. If editor is
2546 supplied, it is called to get a commit message.
2550 supplied, it is called to get a commit message.
2547 """
2551 """
2548 if extra is None:
2552 if extra is None:
2549 extra = {}
2553 extra = {}
2550
2554
2551 def fail(f, msg):
2555 def fail(f, msg):
2552 raise error.Abort('%s: %s' % (f, msg))
2556 raise error.Abort('%s: %s' % (f, msg))
2553
2557
2554 if not match:
2558 if not match:
2555 match = matchmod.always()
2559 match = matchmod.always()
2556
2560
2557 if not force:
2561 if not force:
2558 vdirs = []
2562 vdirs = []
2559 match.explicitdir = vdirs.append
2563 match.explicitdir = vdirs.append
2560 match.bad = fail
2564 match.bad = fail
2561
2565
2562 # lock() for recent changelog (see issue4368)
2566 # lock() for recent changelog (see issue4368)
2563 with self.wlock(), self.lock():
2567 with self.wlock(), self.lock():
2564 wctx = self[None]
2568 wctx = self[None]
2565 merge = len(wctx.parents()) > 1
2569 merge = len(wctx.parents()) > 1
2566
2570
2567 if not force and merge and not match.always():
2571 if not force and merge and not match.always():
2568 raise error.Abort(_('cannot partially commit a merge '
2572 raise error.Abort(_('cannot partially commit a merge '
2569 '(do not specify files or patterns)'))
2573 '(do not specify files or patterns)'))
2570
2574
2571 status = self.status(match=match, clean=force)
2575 status = self.status(match=match, clean=force)
2572 if force:
2576 if force:
2573 status.modified.extend(status.clean) # mq may commit clean files
2577 status.modified.extend(status.clean) # mq may commit clean files
2574
2578
2575 # check subrepos
2579 # check subrepos
2576 subs, commitsubs, newstate = subrepoutil.precommit(
2580 subs, commitsubs, newstate = subrepoutil.precommit(
2577 self.ui, wctx, status, match, force=force)
2581 self.ui, wctx, status, match, force=force)
2578
2582
2579 # make sure all explicit patterns are matched
2583 # make sure all explicit patterns are matched
2580 if not force:
2584 if not force:
2581 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
2585 self.checkcommitpatterns(wctx, vdirs, match, status, fail)
2582
2586
2583 cctx = context.workingcommitctx(self, status,
2587 cctx = context.workingcommitctx(self, status,
2584 text, user, date, extra)
2588 text, user, date, extra)
2585
2589
2586 # internal config: ui.allowemptycommit
2590 # internal config: ui.allowemptycommit
2587 allowemptycommit = (wctx.branch() != wctx.p1().branch()
2591 allowemptycommit = (wctx.branch() != wctx.p1().branch()
2588 or extra.get('close') or merge or cctx.files()
2592 or extra.get('close') or merge or cctx.files()
2589 or self.ui.configbool('ui', 'allowemptycommit'))
2593 or self.ui.configbool('ui', 'allowemptycommit'))
2590 if not allowemptycommit:
2594 if not allowemptycommit:
2591 return None
2595 return None
2592
2596
2593 if merge and cctx.deleted():
2597 if merge and cctx.deleted():
2594 raise error.Abort(_("cannot commit merge with missing files"))
2598 raise error.Abort(_("cannot commit merge with missing files"))
2595
2599
2596 ms = mergemod.mergestate.read(self)
2600 ms = mergemod.mergestate.read(self)
2597 mergeutil.checkunresolved(ms)
2601 mergeutil.checkunresolved(ms)
2598
2602
2599 if editor:
2603 if editor:
2600 cctx._text = editor(self, cctx, subs)
2604 cctx._text = editor(self, cctx, subs)
2601 edited = (text != cctx._text)
2605 edited = (text != cctx._text)
2602
2606
2603 # Save commit message in case this transaction gets rolled back
2607 # Save commit message in case this transaction gets rolled back
2604 # (e.g. by a pretxncommit hook). Leave the content alone on
2608 # (e.g. by a pretxncommit hook). Leave the content alone on
2605 # the assumption that the user will use the same editor again.
2609 # the assumption that the user will use the same editor again.
2606 msgfn = self.savecommitmessage(cctx._text)
2610 msgfn = self.savecommitmessage(cctx._text)
2607
2611
2608 # commit subs and write new state
2612 # commit subs and write new state
2609 if subs:
2613 if subs:
2610 uipathfn = scmutil.getuipathfn(self)
2614 uipathfn = scmutil.getuipathfn(self)
2611 for s in sorted(commitsubs):
2615 for s in sorted(commitsubs):
2612 sub = wctx.sub(s)
2616 sub = wctx.sub(s)
2613 self.ui.status(_('committing subrepository %s\n') %
2617 self.ui.status(_('committing subrepository %s\n') %
2614 uipathfn(subrepoutil.subrelpath(sub)))
2618 uipathfn(subrepoutil.subrelpath(sub)))
2615 sr = sub.commit(cctx._text, user, date)
2619 sr = sub.commit(cctx._text, user, date)
2616 newstate[s] = (newstate[s][0], sr)
2620 newstate[s] = (newstate[s][0], sr)
2617 subrepoutil.writestate(self, newstate)
2621 subrepoutil.writestate(self, newstate)
2618
2622
2619 p1, p2 = self.dirstate.parents()
2623 p1, p2 = self.dirstate.parents()
2620 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
2624 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
2621 try:
2625 try:
2622 self.hook("precommit", throw=True, parent1=hookp1,
2626 self.hook("precommit", throw=True, parent1=hookp1,
2623 parent2=hookp2)
2627 parent2=hookp2)
2624 with self.transaction('commit'):
2628 with self.transaction('commit'):
2625 ret = self.commitctx(cctx, True)
2629 ret = self.commitctx(cctx, True)
2626 # update bookmarks, dirstate and mergestate
2630 # update bookmarks, dirstate and mergestate
2627 bookmarks.update(self, [p1, p2], ret)
2631 bookmarks.update(self, [p1, p2], ret)
2628 cctx.markcommitted(ret)
2632 cctx.markcommitted(ret)
2629 ms.reset()
2633 ms.reset()
2630 except: # re-raises
2634 except: # re-raises
2631 if edited:
2635 if edited:
2632 self.ui.write(
2636 self.ui.write(
2633 _('note: commit message saved in %s\n') % msgfn)
2637 _('note: commit message saved in %s\n') % msgfn)
2634 raise
2638 raise
2635
2639
2636 def commithook():
2640 def commithook():
2637 # hack for command that use a temporary commit (eg: histedit)
2641 # hack for command that use a temporary commit (eg: histedit)
2638 # temporary commit got stripped before hook release
2642 # temporary commit got stripped before hook release
2639 if self.changelog.hasnode(ret):
2643 if self.changelog.hasnode(ret):
2640 self.hook("commit", node=hex(ret), parent1=hookp1,
2644 self.hook("commit", node=hex(ret), parent1=hookp1,
2641 parent2=hookp2)
2645 parent2=hookp2)
2642 self._afterlock(commithook)
2646 self._afterlock(commithook)
2643 return ret
2647 return ret
2644
2648
2645 @unfilteredmethod
2649 @unfilteredmethod
2646 def commitctx(self, ctx, error=False, origctx=None):
2650 def commitctx(self, ctx, error=False, origctx=None):
2647 """Add a new revision to current repository.
2651 """Add a new revision to current repository.
2648 Revision information is passed via the context argument.
2652 Revision information is passed via the context argument.
2649
2653
2650 ctx.files() should list all files involved in this commit, i.e.
2654 ctx.files() should list all files involved in this commit, i.e.
2651 modified/added/removed files. On merge, it may be wider than the
2655 modified/added/removed files. On merge, it may be wider than the
2652 ctx.files() to be committed, since any file nodes derived directly
2656 ctx.files() to be committed, since any file nodes derived directly
2653 from p1 or p2 are excluded from the committed ctx.files().
2657 from p1 or p2 are excluded from the committed ctx.files().
2654
2658
2655 origctx is for convert to work around the problem that bug
2659 origctx is for convert to work around the problem that bug
2656 fixes to the files list in changesets change hashes. For
2660 fixes to the files list in changesets change hashes. For
2657 convert to be the identity, it can pass an origctx and this
2661 convert to be the identity, it can pass an origctx and this
2658 function will use the same files list when it makes sense to
2662 function will use the same files list when it makes sense to
2659 do so.
2663 do so.
2660 """
2664 """
2661
2665
2662 p1, p2 = ctx.p1(), ctx.p2()
2666 p1, p2 = ctx.p1(), ctx.p2()
2663 user = ctx.user()
2667 user = ctx.user()
2664
2668
2665 writecopiesto = self.ui.config('experimental', 'copies.write-to')
2669 writecopiesto = self.ui.config('experimental', 'copies.write-to')
2666 writefilecopymeta = writecopiesto != 'changeset-only'
2670 writefilecopymeta = writecopiesto != 'changeset-only'
2667 writechangesetcopy = (writecopiesto in
2671 writechangesetcopy = (writecopiesto in
2668 ('changeset-only', 'compatibility'))
2672 ('changeset-only', 'compatibility'))
2669 p1copies, p2copies = None, None
2673 p1copies, p2copies = None, None
2670 if writechangesetcopy:
2674 if writechangesetcopy:
2671 p1copies = ctx.p1copies()
2675 p1copies = ctx.p1copies()
2672 p2copies = ctx.p2copies()
2676 p2copies = ctx.p2copies()
2673 filesadded, filesremoved = None, None
2677 filesadded, filesremoved = None, None
2674 with self.lock(), self.transaction("commit") as tr:
2678 with self.lock(), self.transaction("commit") as tr:
2675 trp = weakref.proxy(tr)
2679 trp = weakref.proxy(tr)
2676
2680
2677 if ctx.manifestnode():
2681 if ctx.manifestnode():
2678 # reuse an existing manifest revision
2682 # reuse an existing manifest revision
2679 self.ui.debug('reusing known manifest\n')
2683 self.ui.debug('reusing known manifest\n')
2680 mn = ctx.manifestnode()
2684 mn = ctx.manifestnode()
2681 files = ctx.files()
2685 files = ctx.files()
2682 if writechangesetcopy:
2686 if writechangesetcopy:
2683 filesadded = ctx.filesadded()
2687 filesadded = ctx.filesadded()
2684 filesremoved = ctx.filesremoved()
2688 filesremoved = ctx.filesremoved()
2685 elif ctx.files():
2689 elif ctx.files():
2686 m1ctx = p1.manifestctx()
2690 m1ctx = p1.manifestctx()
2687 m2ctx = p2.manifestctx()
2691 m2ctx = p2.manifestctx()
2688 mctx = m1ctx.copy()
2692 mctx = m1ctx.copy()
2689
2693
2690 m = mctx.read()
2694 m = mctx.read()
2691 m1 = m1ctx.read()
2695 m1 = m1ctx.read()
2692 m2 = m2ctx.read()
2696 m2 = m2ctx.read()
2693
2697
2694 # check in files
2698 # check in files
2695 added = []
2699 added = []
2696 changed = []
2700 changed = []
2697 removed = list(ctx.removed())
2701 removed = list(ctx.removed())
2698 linkrev = len(self)
2702 linkrev = len(self)
2699 self.ui.note(_("committing files:\n"))
2703 self.ui.note(_("committing files:\n"))
2700 uipathfn = scmutil.getuipathfn(self)
2704 uipathfn = scmutil.getuipathfn(self)
2701 for f in sorted(ctx.modified() + ctx.added()):
2705 for f in sorted(ctx.modified() + ctx.added()):
2702 self.ui.note(uipathfn(f) + "\n")
2706 self.ui.note(uipathfn(f) + "\n")
2703 try:
2707 try:
2704 fctx = ctx[f]
2708 fctx = ctx[f]
2705 if fctx is None:
2709 if fctx is None:
2706 removed.append(f)
2710 removed.append(f)
2707 else:
2711 else:
2708 added.append(f)
2712 added.append(f)
2709 m[f] = self._filecommit(fctx, m1, m2, linkrev,
2713 m[f] = self._filecommit(fctx, m1, m2, linkrev,
2710 trp, changed,
2714 trp, changed,
2711 writefilecopymeta)
2715 writefilecopymeta)
2712 m.setflag(f, fctx.flags())
2716 m.setflag(f, fctx.flags())
2713 except OSError:
2717 except OSError:
2714 self.ui.warn(_("trouble committing %s!\n") %
2718 self.ui.warn(_("trouble committing %s!\n") %
2715 uipathfn(f))
2719 uipathfn(f))
2716 raise
2720 raise
2717 except IOError as inst:
2721 except IOError as inst:
2718 errcode = getattr(inst, 'errno', errno.ENOENT)
2722 errcode = getattr(inst, 'errno', errno.ENOENT)
2719 if error or errcode and errcode != errno.ENOENT:
2723 if error or errcode and errcode != errno.ENOENT:
2720 self.ui.warn(_("trouble committing %s!\n") %
2724 self.ui.warn(_("trouble committing %s!\n") %
2721 uipathfn(f))
2725 uipathfn(f))
2722 raise
2726 raise
2723
2727
2724 # update manifest
2728 # update manifest
2725 removed = [f for f in removed if f in m1 or f in m2]
2729 removed = [f for f in removed if f in m1 or f in m2]
2726 drop = sorted([f for f in removed if f in m])
2730 drop = sorted([f for f in removed if f in m])
2727 for f in drop:
2731 for f in drop:
2728 del m[f]
2732 del m[f]
2729 if p2.rev() != nullrev:
2733 if p2.rev() != nullrev:
2730 @util.cachefunc
2734 @util.cachefunc
2731 def mas():
2735 def mas():
2732 p1n = p1.node()
2736 p1n = p1.node()
2733 p2n = p2.node()
2737 p2n = p2.node()
2734 cahs = self.changelog.commonancestorsheads(p1n, p2n)
2738 cahs = self.changelog.commonancestorsheads(p1n, p2n)
2735 if not cahs:
2739 if not cahs:
2736 cahs = [nullrev]
2740 cahs = [nullrev]
2737 return [self[r].manifest() for r in cahs]
2741 return [self[r].manifest() for r in cahs]
2738 def deletionfromparent(f):
2742 def deletionfromparent(f):
2739 # When a file is removed relative to p1 in a merge, this
2743 # When a file is removed relative to p1 in a merge, this
2740 # function determines whether the absence is due to a
2744 # function determines whether the absence is due to a
2741 # deletion from a parent, or whether the merge commit
2745 # deletion from a parent, or whether the merge commit
2742 # itself deletes the file. We decide this by doing a
2746 # itself deletes the file. We decide this by doing a
2743 # simplified three way merge of the manifest entry for
2747 # simplified three way merge of the manifest entry for
2744 # the file. There are two ways we decide the merge
2748 # the file. There are two ways we decide the merge
2745 # itself didn't delete a file:
2749 # itself didn't delete a file:
2746 # - neither parent (nor the merge) contain the file
2750 # - neither parent (nor the merge) contain the file
2747 # - exactly one parent contains the file, and that
2751 # - exactly one parent contains the file, and that
2748 # parent has the same filelog entry as the merge
2752 # parent has the same filelog entry as the merge
2749 # ancestor (or all of them if there two). In other
2753 # ancestor (or all of them if there two). In other
2750 # words, that parent left the file unchanged while the
2754 # words, that parent left the file unchanged while the
2751 # other one deleted it.
2755 # other one deleted it.
2752 # One way to think about this is that deleting a file is
2756 # One way to think about this is that deleting a file is
2753 # similar to emptying it, so the list of changed files
2757 # similar to emptying it, so the list of changed files
2754 # should be similar either way. The computation
2758 # should be similar either way. The computation
2755 # described above is not done directly in _filecommit
2759 # described above is not done directly in _filecommit
2756 # when creating the list of changed files, however
2760 # when creating the list of changed files, however
2757 # it does something very similar by comparing filelog
2761 # it does something very similar by comparing filelog
2758 # nodes.
2762 # nodes.
2759 if f in m1:
2763 if f in m1:
2760 return (f not in m2
2764 return (f not in m2
2761 and all(f in ma and ma.find(f) == m1.find(f)
2765 and all(f in ma and ma.find(f) == m1.find(f)
2762 for ma in mas()))
2766 for ma in mas()))
2763 elif f in m2:
2767 elif f in m2:
2764 return all(f in ma and ma.find(f) == m2.find(f)
2768 return all(f in ma and ma.find(f) == m2.find(f)
2765 for ma in mas())
2769 for ma in mas())
2766 else:
2770 else:
2767 return True
2771 return True
2768 removed = [f for f in removed if not deletionfromparent(f)]
2772 removed = [f for f in removed if not deletionfromparent(f)]
2769
2773
2770 files = changed + removed
2774 files = changed + removed
2771 md = None
2775 md = None
2772 if not files:
2776 if not files:
2773 # if no "files" actually changed in terms of the changelog,
2777 # if no "files" actually changed in terms of the changelog,
2774 # try hard to detect unmodified manifest entry so that the
2778 # try hard to detect unmodified manifest entry so that the
2775 # exact same commit can be reproduced later on convert.
2779 # exact same commit can be reproduced later on convert.
2776 md = m1.diff(m, scmutil.matchfiles(self, ctx.files()))
2780 md = m1.diff(m, scmutil.matchfiles(self, ctx.files()))
2777 if not files and md:
2781 if not files and md:
2778 self.ui.debug('not reusing manifest (no file change in '
2782 self.ui.debug('not reusing manifest (no file change in '
2779 'changelog, but manifest differs)\n')
2783 'changelog, but manifest differs)\n')
2780 if files or md:
2784 if files or md:
2781 self.ui.note(_("committing manifest\n"))
2785 self.ui.note(_("committing manifest\n"))
2782 # we're using narrowmatch here since it's already applied at
2786 # we're using narrowmatch here since it's already applied at
2783 # other stages (such as dirstate.walk), so we're already
2787 # other stages (such as dirstate.walk), so we're already
2784 # ignoring things outside of narrowspec in most cases. The
2788 # ignoring things outside of narrowspec in most cases. The
2785 # one case where we might have files outside the narrowspec
2789 # one case where we might have files outside the narrowspec
2786 # at this point is merges, and we already error out in the
2790 # at this point is merges, and we already error out in the
2787 # case where the merge has files outside of the narrowspec,
2791 # case where the merge has files outside of the narrowspec,
2788 # so this is safe.
2792 # so this is safe.
2789 mn = mctx.write(trp, linkrev,
2793 mn = mctx.write(trp, linkrev,
2790 p1.manifestnode(), p2.manifestnode(),
2794 p1.manifestnode(), p2.manifestnode(),
2791 added, drop, match=self.narrowmatch())
2795 added, drop, match=self.narrowmatch())
2792
2796
2793 if writechangesetcopy:
2797 if writechangesetcopy:
2794 filesadded = [f for f in changed
2798 filesadded = [f for f in changed
2795 if not (f in m1 or f in m2)]
2799 if not (f in m1 or f in m2)]
2796 filesremoved = removed
2800 filesremoved = removed
2797 else:
2801 else:
2798 self.ui.debug('reusing manifest from p1 (listed files '
2802 self.ui.debug('reusing manifest from p1 (listed files '
2799 'actually unchanged)\n')
2803 'actually unchanged)\n')
2800 mn = p1.manifestnode()
2804 mn = p1.manifestnode()
2801 else:
2805 else:
2802 self.ui.debug('reusing manifest from p1 (no file change)\n')
2806 self.ui.debug('reusing manifest from p1 (no file change)\n')
2803 mn = p1.manifestnode()
2807 mn = p1.manifestnode()
2804 files = []
2808 files = []
2805
2809
2806 if writecopiesto == 'changeset-only':
2810 if writecopiesto == 'changeset-only':
2807 # If writing only to changeset extras, use None to indicate that
2811 # If writing only to changeset extras, use None to indicate that
2808 # no entry should be written. If writing to both, write an empty
2812 # no entry should be written. If writing to both, write an empty
2809 # entry to prevent the reader from falling back to reading
2813 # entry to prevent the reader from falling back to reading
2810 # filelogs.
2814 # filelogs.
2811 p1copies = p1copies or None
2815 p1copies = p1copies or None
2812 p2copies = p2copies or None
2816 p2copies = p2copies or None
2813 filesadded = filesadded or None
2817 filesadded = filesadded or None
2814 filesremoved = filesremoved or None
2818 filesremoved = filesremoved or None
2815
2819
2816 if origctx and origctx.manifestnode() == mn:
2820 if origctx and origctx.manifestnode() == mn:
2817 files = origctx.files()
2821 files = origctx.files()
2818
2822
2819 # update changelog
2823 # update changelog
2820 self.ui.note(_("committing changelog\n"))
2824 self.ui.note(_("committing changelog\n"))
2821 self.changelog.delayupdate(tr)
2825 self.changelog.delayupdate(tr)
2822 n = self.changelog.add(mn, files, ctx.description(),
2826 n = self.changelog.add(mn, files, ctx.description(),
2823 trp, p1.node(), p2.node(),
2827 trp, p1.node(), p2.node(),
2824 user, ctx.date(), ctx.extra().copy(),
2828 user, ctx.date(), ctx.extra().copy(),
2825 p1copies, p2copies, filesadded, filesremoved)
2829 p1copies, p2copies, filesadded, filesremoved)
2826 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
2830 xp1, xp2 = p1.hex(), p2 and p2.hex() or ''
2827 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
2831 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
2828 parent2=xp2)
2832 parent2=xp2)
2829 # set the new commit is proper phase
2833 # set the new commit is proper phase
2830 targetphase = subrepoutil.newcommitphase(self.ui, ctx)
2834 targetphase = subrepoutil.newcommitphase(self.ui, ctx)
2831 if targetphase:
2835 if targetphase:
2832 # retract boundary do not alter parent changeset.
2836 # retract boundary do not alter parent changeset.
2833 # if a parent have higher the resulting phase will
2837 # if a parent have higher the resulting phase will
2834 # be compliant anyway
2838 # be compliant anyway
2835 #
2839 #
2836 # if minimal phase was 0 we don't need to retract anything
2840 # if minimal phase was 0 we don't need to retract anything
2837 phases.registernew(self, tr, targetphase, [n])
2841 phases.registernew(self, tr, targetphase, [n])
2838 return n
2842 return n
2839
2843
2840 @unfilteredmethod
2844 @unfilteredmethod
2841 def destroying(self):
2845 def destroying(self):
2842 '''Inform the repository that nodes are about to be destroyed.
2846 '''Inform the repository that nodes are about to be destroyed.
2843 Intended for use by strip and rollback, so there's a common
2847 Intended for use by strip and rollback, so there's a common
2844 place for anything that has to be done before destroying history.
2848 place for anything that has to be done before destroying history.
2845
2849
2846 This is mostly useful for saving state that is in memory and waiting
2850 This is mostly useful for saving state that is in memory and waiting
2847 to be flushed when the current lock is released. Because a call to
2851 to be flushed when the current lock is released. Because a call to
2848 destroyed is imminent, the repo will be invalidated causing those
2852 destroyed is imminent, the repo will be invalidated causing those
2849 changes to stay in memory (waiting for the next unlock), or vanish
2853 changes to stay in memory (waiting for the next unlock), or vanish
2850 completely.
2854 completely.
2851 '''
2855 '''
2852 # When using the same lock to commit and strip, the phasecache is left
2856 # When using the same lock to commit and strip, the phasecache is left
2853 # dirty after committing. Then when we strip, the repo is invalidated,
2857 # dirty after committing. Then when we strip, the repo is invalidated,
2854 # causing those changes to disappear.
2858 # causing those changes to disappear.
2855 if '_phasecache' in vars(self):
2859 if '_phasecache' in vars(self):
2856 self._phasecache.write()
2860 self._phasecache.write()
2857
2861
2858 @unfilteredmethod
2862 @unfilteredmethod
2859 def destroyed(self):
2863 def destroyed(self):
2860 '''Inform the repository that nodes have been destroyed.
2864 '''Inform the repository that nodes have been destroyed.
2861 Intended for use by strip and rollback, so there's a common
2865 Intended for use by strip and rollback, so there's a common
2862 place for anything that has to be done after destroying history.
2866 place for anything that has to be done after destroying history.
2863 '''
2867 '''
2864 # When one tries to:
2868 # When one tries to:
2865 # 1) destroy nodes thus calling this method (e.g. strip)
2869 # 1) destroy nodes thus calling this method (e.g. strip)
2866 # 2) use phasecache somewhere (e.g. commit)
2870 # 2) use phasecache somewhere (e.g. commit)
2867 #
2871 #
2868 # then 2) will fail because the phasecache contains nodes that were
2872 # then 2) will fail because the phasecache contains nodes that were
2869 # removed. We can either remove phasecache from the filecache,
2873 # removed. We can either remove phasecache from the filecache,
2870 # causing it to reload next time it is accessed, or simply filter
2874 # causing it to reload next time it is accessed, or simply filter
2871 # the removed nodes now and write the updated cache.
2875 # the removed nodes now and write the updated cache.
2872 self._phasecache.filterunknown(self)
2876 self._phasecache.filterunknown(self)
2873 self._phasecache.write()
2877 self._phasecache.write()
2874
2878
2875 # refresh all repository caches
2879 # refresh all repository caches
2876 self.updatecaches()
2880 self.updatecaches()
2877
2881
2878 # Ensure the persistent tag cache is updated. Doing it now
2882 # Ensure the persistent tag cache is updated. Doing it now
2879 # means that the tag cache only has to worry about destroyed
2883 # means that the tag cache only has to worry about destroyed
2880 # heads immediately after a strip/rollback. That in turn
2884 # heads immediately after a strip/rollback. That in turn
2881 # guarantees that "cachetip == currenttip" (comparing both rev
2885 # guarantees that "cachetip == currenttip" (comparing both rev
2882 # and node) always means no nodes have been added or destroyed.
2886 # and node) always means no nodes have been added or destroyed.
2883
2887
2884 # XXX this is suboptimal when qrefresh'ing: we strip the current
2888 # XXX this is suboptimal when qrefresh'ing: we strip the current
2885 # head, refresh the tag cache, then immediately add a new head.
2889 # head, refresh the tag cache, then immediately add a new head.
2886 # But I think doing it this way is necessary for the "instant
2890 # But I think doing it this way is necessary for the "instant
2887 # tag cache retrieval" case to work.
2891 # tag cache retrieval" case to work.
2888 self.invalidate()
2892 self.invalidate()
2889
2893
2890 def status(self, node1='.', node2=None, match=None,
2894 def status(self, node1='.', node2=None, match=None,
2891 ignored=False, clean=False, unknown=False,
2895 ignored=False, clean=False, unknown=False,
2892 listsubrepos=False):
2896 listsubrepos=False):
2893 '''a convenience method that calls node1.status(node2)'''
2897 '''a convenience method that calls node1.status(node2)'''
2894 return self[node1].status(node2, match, ignored, clean, unknown,
2898 return self[node1].status(node2, match, ignored, clean, unknown,
2895 listsubrepos)
2899 listsubrepos)
2896
2900
2897 def addpostdsstatus(self, ps):
2901 def addpostdsstatus(self, ps):
2898 """Add a callback to run within the wlock, at the point at which status
2902 """Add a callback to run within the wlock, at the point at which status
2899 fixups happen.
2903 fixups happen.
2900
2904
2901 On status completion, callback(wctx, status) will be called with the
2905 On status completion, callback(wctx, status) will be called with the
2902 wlock held, unless the dirstate has changed from underneath or the wlock
2906 wlock held, unless the dirstate has changed from underneath or the wlock
2903 couldn't be grabbed.
2907 couldn't be grabbed.
2904
2908
2905 Callbacks should not capture and use a cached copy of the dirstate --
2909 Callbacks should not capture and use a cached copy of the dirstate --
2906 it might change in the meanwhile. Instead, they should access the
2910 it might change in the meanwhile. Instead, they should access the
2907 dirstate via wctx.repo().dirstate.
2911 dirstate via wctx.repo().dirstate.
2908
2912
2909 This list is emptied out after each status run -- extensions should
2913 This list is emptied out after each status run -- extensions should
2910 make sure it adds to this list each time dirstate.status is called.
2914 make sure it adds to this list each time dirstate.status is called.
2911 Extensions should also make sure they don't call this for statuses
2915 Extensions should also make sure they don't call this for statuses
2912 that don't involve the dirstate.
2916 that don't involve the dirstate.
2913 """
2917 """
2914
2918
2915 # The list is located here for uniqueness reasons -- it is actually
2919 # The list is located here for uniqueness reasons -- it is actually
2916 # managed by the workingctx, but that isn't unique per-repo.
2920 # managed by the workingctx, but that isn't unique per-repo.
2917 self._postdsstatus.append(ps)
2921 self._postdsstatus.append(ps)
2918
2922
2919 def postdsstatus(self):
2923 def postdsstatus(self):
2920 """Used by workingctx to get the list of post-dirstate-status hooks."""
2924 """Used by workingctx to get the list of post-dirstate-status hooks."""
2921 return self._postdsstatus
2925 return self._postdsstatus
2922
2926
2923 def clearpostdsstatus(self):
2927 def clearpostdsstatus(self):
2924 """Used by workingctx to clear post-dirstate-status hooks."""
2928 """Used by workingctx to clear post-dirstate-status hooks."""
2925 del self._postdsstatus[:]
2929 del self._postdsstatus[:]
2926
2930
2927 def heads(self, start=None):
2931 def heads(self, start=None):
2928 if start is None:
2932 if start is None:
2929 cl = self.changelog
2933 cl = self.changelog
2930 headrevs = reversed(cl.headrevs())
2934 headrevs = reversed(cl.headrevs())
2931 return [cl.node(rev) for rev in headrevs]
2935 return [cl.node(rev) for rev in headrevs]
2932
2936
2933 heads = self.changelog.heads(start)
2937 heads = self.changelog.heads(start)
2934 # sort the output in rev descending order
2938 # sort the output in rev descending order
2935 return sorted(heads, key=self.changelog.rev, reverse=True)
2939 return sorted(heads, key=self.changelog.rev, reverse=True)
2936
2940
2937 def branchheads(self, branch=None, start=None, closed=False):
2941 def branchheads(self, branch=None, start=None, closed=False):
2938 '''return a (possibly filtered) list of heads for the given branch
2942 '''return a (possibly filtered) list of heads for the given branch
2939
2943
2940 Heads are returned in topological order, from newest to oldest.
2944 Heads are returned in topological order, from newest to oldest.
2941 If branch is None, use the dirstate branch.
2945 If branch is None, use the dirstate branch.
2942 If start is not None, return only heads reachable from start.
2946 If start is not None, return only heads reachable from start.
2943 If closed is True, return heads that are marked as closed as well.
2947 If closed is True, return heads that are marked as closed as well.
2944 '''
2948 '''
2945 if branch is None:
2949 if branch is None:
2946 branch = self[None].branch()
2950 branch = self[None].branch()
2947 branches = self.branchmap()
2951 branches = self.branchmap()
2948 if not branches.hasbranch(branch):
2952 if not branches.hasbranch(branch):
2949 return []
2953 return []
2950 # the cache returns heads ordered lowest to highest
2954 # the cache returns heads ordered lowest to highest
2951 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
2955 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
2952 if start is not None:
2956 if start is not None:
2953 # filter out the heads that cannot be reached from startrev
2957 # filter out the heads that cannot be reached from startrev
2954 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
2958 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
2955 bheads = [h for h in bheads if h in fbheads]
2959 bheads = [h for h in bheads if h in fbheads]
2956 return bheads
2960 return bheads
2957
2961
2958 def branches(self, nodes):
2962 def branches(self, nodes):
2959 if not nodes:
2963 if not nodes:
2960 nodes = [self.changelog.tip()]
2964 nodes = [self.changelog.tip()]
2961 b = []
2965 b = []
2962 for n in nodes:
2966 for n in nodes:
2963 t = n
2967 t = n
2964 while True:
2968 while True:
2965 p = self.changelog.parents(n)
2969 p = self.changelog.parents(n)
2966 if p[1] != nullid or p[0] == nullid:
2970 if p[1] != nullid or p[0] == nullid:
2967 b.append((t, n, p[0], p[1]))
2971 b.append((t, n, p[0], p[1]))
2968 break
2972 break
2969 n = p[0]
2973 n = p[0]
2970 return b
2974 return b
2971
2975
2972 def between(self, pairs):
2976 def between(self, pairs):
2973 r = []
2977 r = []
2974
2978
2975 for top, bottom in pairs:
2979 for top, bottom in pairs:
2976 n, l, i = top, [], 0
2980 n, l, i = top, [], 0
2977 f = 1
2981 f = 1
2978
2982
2979 while n != bottom and n != nullid:
2983 while n != bottom and n != nullid:
2980 p = self.changelog.parents(n)[0]
2984 p = self.changelog.parents(n)[0]
2981 if i == f:
2985 if i == f:
2982 l.append(n)
2986 l.append(n)
2983 f = f * 2
2987 f = f * 2
2984 n = p
2988 n = p
2985 i += 1
2989 i += 1
2986
2990
2987 r.append(l)
2991 r.append(l)
2988
2992
2989 return r
2993 return r
2990
2994
2991 def checkpush(self, pushop):
2995 def checkpush(self, pushop):
2992 """Extensions can override this function if additional checks have
2996 """Extensions can override this function if additional checks have
2993 to be performed before pushing, or call it if they override push
2997 to be performed before pushing, or call it if they override push
2994 command.
2998 command.
2995 """
2999 """
2996
3000
2997 @unfilteredpropertycache
3001 @unfilteredpropertycache
2998 def prepushoutgoinghooks(self):
3002 def prepushoutgoinghooks(self):
2999 """Return util.hooks consists of a pushop with repo, remote, outgoing
3003 """Return util.hooks consists of a pushop with repo, remote, outgoing
3000 methods, which are called before pushing changesets.
3004 methods, which are called before pushing changesets.
3001 """
3005 """
3002 return util.hooks()
3006 return util.hooks()
3003
3007
3004 def pushkey(self, namespace, key, old, new):
3008 def pushkey(self, namespace, key, old, new):
3005 try:
3009 try:
3006 tr = self.currenttransaction()
3010 tr = self.currenttransaction()
3007 hookargs = {}
3011 hookargs = {}
3008 if tr is not None:
3012 if tr is not None:
3009 hookargs.update(tr.hookargs)
3013 hookargs.update(tr.hookargs)
3010 hookargs = pycompat.strkwargs(hookargs)
3014 hookargs = pycompat.strkwargs(hookargs)
3011 hookargs[r'namespace'] = namespace
3015 hookargs[r'namespace'] = namespace
3012 hookargs[r'key'] = key
3016 hookargs[r'key'] = key
3013 hookargs[r'old'] = old
3017 hookargs[r'old'] = old
3014 hookargs[r'new'] = new
3018 hookargs[r'new'] = new
3015 self.hook('prepushkey', throw=True, **hookargs)
3019 self.hook('prepushkey', throw=True, **hookargs)
3016 except error.HookAbort as exc:
3020 except error.HookAbort as exc:
3017 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
3021 self.ui.write_err(_("pushkey-abort: %s\n") % exc)
3018 if exc.hint:
3022 if exc.hint:
3019 self.ui.write_err(_("(%s)\n") % exc.hint)
3023 self.ui.write_err(_("(%s)\n") % exc.hint)
3020 return False
3024 return False
3021 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
3025 self.ui.debug('pushing key for "%s:%s"\n' % (namespace, key))
3022 ret = pushkey.push(self, namespace, key, old, new)
3026 ret = pushkey.push(self, namespace, key, old, new)
3023 def runhook():
3027 def runhook():
3024 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
3028 self.hook('pushkey', namespace=namespace, key=key, old=old, new=new,
3025 ret=ret)
3029 ret=ret)
3026 self._afterlock(runhook)
3030 self._afterlock(runhook)
3027 return ret
3031 return ret
3028
3032
3029 def listkeys(self, namespace):
3033 def listkeys(self, namespace):
3030 self.hook('prelistkeys', throw=True, namespace=namespace)
3034 self.hook('prelistkeys', throw=True, namespace=namespace)
3031 self.ui.debug('listing keys for "%s"\n' % namespace)
3035 self.ui.debug('listing keys for "%s"\n' % namespace)
3032 values = pushkey.list(self, namespace)
3036 values = pushkey.list(self, namespace)
3033 self.hook('listkeys', namespace=namespace, values=values)
3037 self.hook('listkeys', namespace=namespace, values=values)
3034 return values
3038 return values
3035
3039
3036 def debugwireargs(self, one, two, three=None, four=None, five=None):
3040 def debugwireargs(self, one, two, three=None, four=None, five=None):
3037 '''used to test argument passing over the wire'''
3041 '''used to test argument passing over the wire'''
3038 return "%s %s %s %s %s" % (one, two, pycompat.bytestr(three),
3042 return "%s %s %s %s %s" % (one, two, pycompat.bytestr(three),
3039 pycompat.bytestr(four),
3043 pycompat.bytestr(four),
3040 pycompat.bytestr(five))
3044 pycompat.bytestr(five))
3041
3045
3042 def savecommitmessage(self, text):
3046 def savecommitmessage(self, text):
3043 fp = self.vfs('last-message.txt', 'wb')
3047 fp = self.vfs('last-message.txt', 'wb')
3044 try:
3048 try:
3045 fp.write(text)
3049 fp.write(text)
3046 finally:
3050 finally:
3047 fp.close()
3051 fp.close()
3048 return self.pathto(fp.name[len(self.root) + 1:])
3052 return self.pathto(fp.name[len(self.root) + 1:])
3049
3053
3050 # used to avoid circular references so destructors work
3054 # used to avoid circular references so destructors work
3051 def aftertrans(files):
3055 def aftertrans(files):
3052 renamefiles = [tuple(t) for t in files]
3056 renamefiles = [tuple(t) for t in files]
3053 def a():
3057 def a():
3054 for vfs, src, dest in renamefiles:
3058 for vfs, src, dest in renamefiles:
3055 # if src and dest refer to a same file, vfs.rename is a no-op,
3059 # if src and dest refer to a same file, vfs.rename is a no-op,
3056 # leaving both src and dest on disk. delete dest to make sure
3060 # leaving both src and dest on disk. delete dest to make sure
3057 # the rename couldn't be such a no-op.
3061 # the rename couldn't be such a no-op.
3058 vfs.tryunlink(dest)
3062 vfs.tryunlink(dest)
3059 try:
3063 try:
3060 vfs.rename(src, dest)
3064 vfs.rename(src, dest)
3061 except OSError: # journal file does not yet exist
3065 except OSError: # journal file does not yet exist
3062 pass
3066 pass
3063 return a
3067 return a
3064
3068
3065 def undoname(fn):
3069 def undoname(fn):
3066 base, name = os.path.split(fn)
3070 base, name = os.path.split(fn)
3067 assert name.startswith('journal')
3071 assert name.startswith('journal')
3068 return os.path.join(base, name.replace('journal', 'undo', 1))
3072 return os.path.join(base, name.replace('journal', 'undo', 1))
3069
3073
3070 def instance(ui, path, create, intents=None, createopts=None):
3074 def instance(ui, path, create, intents=None, createopts=None):
3071 localpath = util.urllocalpath(path)
3075 localpath = util.urllocalpath(path)
3072 if create:
3076 if create:
3073 createrepository(ui, localpath, createopts=createopts)
3077 createrepository(ui, localpath, createopts=createopts)
3074
3078
3075 return makelocalrepository(ui, localpath, intents=intents)
3079 return makelocalrepository(ui, localpath, intents=intents)
3076
3080
3077 def islocal(path):
3081 def islocal(path):
3078 return True
3082 return True
3079
3083
3080 def defaultcreateopts(ui, createopts=None):
3084 def defaultcreateopts(ui, createopts=None):
3081 """Populate the default creation options for a repository.
3085 """Populate the default creation options for a repository.
3082
3086
3083 A dictionary of explicitly requested creation options can be passed
3087 A dictionary of explicitly requested creation options can be passed
3084 in. Missing keys will be populated.
3088 in. Missing keys will be populated.
3085 """
3089 """
3086 createopts = dict(createopts or {})
3090 createopts = dict(createopts or {})
3087
3091
3088 if 'backend' not in createopts:
3092 if 'backend' not in createopts:
3089 # experimental config: storage.new-repo-backend
3093 # experimental config: storage.new-repo-backend
3090 createopts['backend'] = ui.config('storage', 'new-repo-backend')
3094 createopts['backend'] = ui.config('storage', 'new-repo-backend')
3091
3095
3092 return createopts
3096 return createopts
3093
3097
3094 def newreporequirements(ui, createopts):
3098 def newreporequirements(ui, createopts):
3095 """Determine the set of requirements for a new local repository.
3099 """Determine the set of requirements for a new local repository.
3096
3100
3097 Extensions can wrap this function to specify custom requirements for
3101 Extensions can wrap this function to specify custom requirements for
3098 new repositories.
3102 new repositories.
3099 """
3103 """
3100 # If the repo is being created from a shared repository, we copy
3104 # If the repo is being created from a shared repository, we copy
3101 # its requirements.
3105 # its requirements.
3102 if 'sharedrepo' in createopts:
3106 if 'sharedrepo' in createopts:
3103 requirements = set(createopts['sharedrepo'].requirements)
3107 requirements = set(createopts['sharedrepo'].requirements)
3104 if createopts.get('sharedrelative'):
3108 if createopts.get('sharedrelative'):
3105 requirements.add('relshared')
3109 requirements.add('relshared')
3106 else:
3110 else:
3107 requirements.add('shared')
3111 requirements.add('shared')
3108
3112
3109 return requirements
3113 return requirements
3110
3114
3111 if 'backend' not in createopts:
3115 if 'backend' not in createopts:
3112 raise error.ProgrammingError('backend key not present in createopts; '
3116 raise error.ProgrammingError('backend key not present in createopts; '
3113 'was defaultcreateopts() called?')
3117 'was defaultcreateopts() called?')
3114
3118
3115 if createopts['backend'] != 'revlogv1':
3119 if createopts['backend'] != 'revlogv1':
3116 raise error.Abort(_('unable to determine repository requirements for '
3120 raise error.Abort(_('unable to determine repository requirements for '
3117 'storage backend: %s') % createopts['backend'])
3121 'storage backend: %s') % createopts['backend'])
3118
3122
3119 requirements = {'revlogv1'}
3123 requirements = {'revlogv1'}
3120 if ui.configbool('format', 'usestore'):
3124 if ui.configbool('format', 'usestore'):
3121 requirements.add('store')
3125 requirements.add('store')
3122 if ui.configbool('format', 'usefncache'):
3126 if ui.configbool('format', 'usefncache'):
3123 requirements.add('fncache')
3127 requirements.add('fncache')
3124 if ui.configbool('format', 'dotencode'):
3128 if ui.configbool('format', 'dotencode'):
3125 requirements.add('dotencode')
3129 requirements.add('dotencode')
3126
3130
3127 compengine = ui.config('format', 'revlog-compression')
3131 compengine = ui.config('format', 'revlog-compression')
3128 if compengine not in util.compengines:
3132 if compengine not in util.compengines:
3129 raise error.Abort(_('compression engine %s defined by '
3133 raise error.Abort(_('compression engine %s defined by '
3130 'format.revlog-compression not available') %
3134 'format.revlog-compression not available') %
3131 compengine,
3135 compengine,
3132 hint=_('run "hg debuginstall" to list available '
3136 hint=_('run "hg debuginstall" to list available '
3133 'compression engines'))
3137 'compression engines'))
3134
3138
3135 # zlib is the historical default and doesn't need an explicit requirement.
3139 # zlib is the historical default and doesn't need an explicit requirement.
3136 elif compengine == 'zstd':
3140 elif compengine == 'zstd':
3137 requirements.add('revlog-compression-zstd')
3141 requirements.add('revlog-compression-zstd')
3138 elif compengine != 'zlib':
3142 elif compengine != 'zlib':
3139 requirements.add('exp-compression-%s' % compengine)
3143 requirements.add('exp-compression-%s' % compengine)
3140
3144
3141 if scmutil.gdinitconfig(ui):
3145 if scmutil.gdinitconfig(ui):
3142 requirements.add('generaldelta')
3146 requirements.add('generaldelta')
3143 if ui.configbool('format', 'sparse-revlog'):
3147 if ui.configbool('format', 'sparse-revlog'):
3144 requirements.add(SPARSEREVLOG_REQUIREMENT)
3148 requirements.add(SPARSEREVLOG_REQUIREMENT)
3145 if ui.configbool('experimental', 'treemanifest'):
3149 if ui.configbool('experimental', 'treemanifest'):
3146 requirements.add('treemanifest')
3150 requirements.add('treemanifest')
3147
3151
3148 revlogv2 = ui.config('experimental', 'revlogv2')
3152 revlogv2 = ui.config('experimental', 'revlogv2')
3149 if revlogv2 == 'enable-unstable-format-and-corrupt-my-data':
3153 if revlogv2 == 'enable-unstable-format-and-corrupt-my-data':
3150 requirements.remove('revlogv1')
3154 requirements.remove('revlogv1')
3151 # generaldelta is implied by revlogv2.
3155 # generaldelta is implied by revlogv2.
3152 requirements.discard('generaldelta')
3156 requirements.discard('generaldelta')
3153 requirements.add(REVLOGV2_REQUIREMENT)
3157 requirements.add(REVLOGV2_REQUIREMENT)
3154 # experimental config: format.internal-phase
3158 # experimental config: format.internal-phase
3155 if ui.configbool('format', 'internal-phase'):
3159 if ui.configbool('format', 'internal-phase'):
3156 requirements.add('internal-phase')
3160 requirements.add('internal-phase')
3157
3161
3158 if createopts.get('narrowfiles'):
3162 if createopts.get('narrowfiles'):
3159 requirements.add(repository.NARROW_REQUIREMENT)
3163 requirements.add(repository.NARROW_REQUIREMENT)
3160
3164
3161 if createopts.get('lfs'):
3165 if createopts.get('lfs'):
3162 requirements.add('lfs')
3166 requirements.add('lfs')
3163
3167
3164 if ui.configbool('format', 'bookmarks-in-store'):
3168 if ui.configbool('format', 'bookmarks-in-store'):
3165 requirements.add(bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT)
3169 requirements.add(bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT)
3166
3170
3167 return requirements
3171 return requirements
3168
3172
3169 def filterknowncreateopts(ui, createopts):
3173 def filterknowncreateopts(ui, createopts):
3170 """Filters a dict of repo creation options against options that are known.
3174 """Filters a dict of repo creation options against options that are known.
3171
3175
3172 Receives a dict of repo creation options and returns a dict of those
3176 Receives a dict of repo creation options and returns a dict of those
3173 options that we don't know how to handle.
3177 options that we don't know how to handle.
3174
3178
3175 This function is called as part of repository creation. If the
3179 This function is called as part of repository creation. If the
3176 returned dict contains any items, repository creation will not
3180 returned dict contains any items, repository creation will not
3177 be allowed, as it means there was a request to create a repository
3181 be allowed, as it means there was a request to create a repository
3178 with options not recognized by loaded code.
3182 with options not recognized by loaded code.
3179
3183
3180 Extensions can wrap this function to filter out creation options
3184 Extensions can wrap this function to filter out creation options
3181 they know how to handle.
3185 they know how to handle.
3182 """
3186 """
3183 known = {
3187 known = {
3184 'backend',
3188 'backend',
3185 'lfs',
3189 'lfs',
3186 'narrowfiles',
3190 'narrowfiles',
3187 'sharedrepo',
3191 'sharedrepo',
3188 'sharedrelative',
3192 'sharedrelative',
3189 'shareditems',
3193 'shareditems',
3190 'shallowfilestore',
3194 'shallowfilestore',
3191 }
3195 }
3192
3196
3193 return {k: v for k, v in createopts.items() if k not in known}
3197 return {k: v for k, v in createopts.items() if k not in known}
3194
3198
3195 def createrepository(ui, path, createopts=None):
3199 def createrepository(ui, path, createopts=None):
3196 """Create a new repository in a vfs.
3200 """Create a new repository in a vfs.
3197
3201
3198 ``path`` path to the new repo's working directory.
3202 ``path`` path to the new repo's working directory.
3199 ``createopts`` options for the new repository.
3203 ``createopts`` options for the new repository.
3200
3204
3201 The following keys for ``createopts`` are recognized:
3205 The following keys for ``createopts`` are recognized:
3202
3206
3203 backend
3207 backend
3204 The storage backend to use.
3208 The storage backend to use.
3205 lfs
3209 lfs
3206 Repository will be created with ``lfs`` requirement. The lfs extension
3210 Repository will be created with ``lfs`` requirement. The lfs extension
3207 will automatically be loaded when the repository is accessed.
3211 will automatically be loaded when the repository is accessed.
3208 narrowfiles
3212 narrowfiles
3209 Set up repository to support narrow file storage.
3213 Set up repository to support narrow file storage.
3210 sharedrepo
3214 sharedrepo
3211 Repository object from which storage should be shared.
3215 Repository object from which storage should be shared.
3212 sharedrelative
3216 sharedrelative
3213 Boolean indicating if the path to the shared repo should be
3217 Boolean indicating if the path to the shared repo should be
3214 stored as relative. By default, the pointer to the "parent" repo
3218 stored as relative. By default, the pointer to the "parent" repo
3215 is stored as an absolute path.
3219 is stored as an absolute path.
3216 shareditems
3220 shareditems
3217 Set of items to share to the new repository (in addition to storage).
3221 Set of items to share to the new repository (in addition to storage).
3218 shallowfilestore
3222 shallowfilestore
3219 Indicates that storage for files should be shallow (not all ancestor
3223 Indicates that storage for files should be shallow (not all ancestor
3220 revisions are known).
3224 revisions are known).
3221 """
3225 """
3222 createopts = defaultcreateopts(ui, createopts=createopts)
3226 createopts = defaultcreateopts(ui, createopts=createopts)
3223
3227
3224 unknownopts = filterknowncreateopts(ui, createopts)
3228 unknownopts = filterknowncreateopts(ui, createopts)
3225
3229
3226 if not isinstance(unknownopts, dict):
3230 if not isinstance(unknownopts, dict):
3227 raise error.ProgrammingError('filterknowncreateopts() did not return '
3231 raise error.ProgrammingError('filterknowncreateopts() did not return '
3228 'a dict')
3232 'a dict')
3229
3233
3230 if unknownopts:
3234 if unknownopts:
3231 raise error.Abort(_('unable to create repository because of unknown '
3235 raise error.Abort(_('unable to create repository because of unknown '
3232 'creation option: %s') %
3236 'creation option: %s') %
3233 ', '.join(sorted(unknownopts)),
3237 ', '.join(sorted(unknownopts)),
3234 hint=_('is a required extension not loaded?'))
3238 hint=_('is a required extension not loaded?'))
3235
3239
3236 requirements = newreporequirements(ui, createopts=createopts)
3240 requirements = newreporequirements(ui, createopts=createopts)
3237
3241
3238 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
3242 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
3239
3243
3240 hgvfs = vfsmod.vfs(wdirvfs.join(b'.hg'))
3244 hgvfs = vfsmod.vfs(wdirvfs.join(b'.hg'))
3241 if hgvfs.exists():
3245 if hgvfs.exists():
3242 raise error.RepoError(_('repository %s already exists') % path)
3246 raise error.RepoError(_('repository %s already exists') % path)
3243
3247
3244 if 'sharedrepo' in createopts:
3248 if 'sharedrepo' in createopts:
3245 sharedpath = createopts['sharedrepo'].sharedpath
3249 sharedpath = createopts['sharedrepo'].sharedpath
3246
3250
3247 if createopts.get('sharedrelative'):
3251 if createopts.get('sharedrelative'):
3248 try:
3252 try:
3249 sharedpath = os.path.relpath(sharedpath, hgvfs.base)
3253 sharedpath = os.path.relpath(sharedpath, hgvfs.base)
3250 except (IOError, ValueError) as e:
3254 except (IOError, ValueError) as e:
3251 # ValueError is raised on Windows if the drive letters differ
3255 # ValueError is raised on Windows if the drive letters differ
3252 # on each path.
3256 # on each path.
3253 raise error.Abort(_('cannot calculate relative path'),
3257 raise error.Abort(_('cannot calculate relative path'),
3254 hint=stringutil.forcebytestr(e))
3258 hint=stringutil.forcebytestr(e))
3255
3259
3256 if not wdirvfs.exists():
3260 if not wdirvfs.exists():
3257 wdirvfs.makedirs()
3261 wdirvfs.makedirs()
3258
3262
3259 hgvfs.makedir(notindexed=True)
3263 hgvfs.makedir(notindexed=True)
3260 if 'sharedrepo' not in createopts:
3264 if 'sharedrepo' not in createopts:
3261 hgvfs.mkdir(b'cache')
3265 hgvfs.mkdir(b'cache')
3262 hgvfs.mkdir(b'wcache')
3266 hgvfs.mkdir(b'wcache')
3263
3267
3264 if b'store' in requirements and 'sharedrepo' not in createopts:
3268 if b'store' in requirements and 'sharedrepo' not in createopts:
3265 hgvfs.mkdir(b'store')
3269 hgvfs.mkdir(b'store')
3266
3270
3267 # We create an invalid changelog outside the store so very old
3271 # We create an invalid changelog outside the store so very old
3268 # Mercurial versions (which didn't know about the requirements
3272 # Mercurial versions (which didn't know about the requirements
3269 # file) encounter an error on reading the changelog. This
3273 # file) encounter an error on reading the changelog. This
3270 # effectively locks out old clients and prevents them from
3274 # effectively locks out old clients and prevents them from
3271 # mucking with a repo in an unknown format.
3275 # mucking with a repo in an unknown format.
3272 #
3276 #
3273 # The revlog header has version 2, which won't be recognized by
3277 # The revlog header has version 2, which won't be recognized by
3274 # such old clients.
3278 # such old clients.
3275 hgvfs.append(b'00changelog.i',
3279 hgvfs.append(b'00changelog.i',
3276 b'\0\0\0\2 dummy changelog to prevent using the old repo '
3280 b'\0\0\0\2 dummy changelog to prevent using the old repo '
3277 b'layout')
3281 b'layout')
3278
3282
3279 scmutil.writerequires(hgvfs, requirements)
3283 scmutil.writerequires(hgvfs, requirements)
3280
3284
3281 # Write out file telling readers where to find the shared store.
3285 # Write out file telling readers where to find the shared store.
3282 if 'sharedrepo' in createopts:
3286 if 'sharedrepo' in createopts:
3283 hgvfs.write(b'sharedpath', sharedpath)
3287 hgvfs.write(b'sharedpath', sharedpath)
3284
3288
3285 if createopts.get('shareditems'):
3289 if createopts.get('shareditems'):
3286 shared = b'\n'.join(sorted(createopts['shareditems'])) + b'\n'
3290 shared = b'\n'.join(sorted(createopts['shareditems'])) + b'\n'
3287 hgvfs.write(b'shared', shared)
3291 hgvfs.write(b'shared', shared)
3288
3292
3289 def poisonrepository(repo):
3293 def poisonrepository(repo):
3290 """Poison a repository instance so it can no longer be used."""
3294 """Poison a repository instance so it can no longer be used."""
3291 # Perform any cleanup on the instance.
3295 # Perform any cleanup on the instance.
3292 repo.close()
3296 repo.close()
3293
3297
3294 # Our strategy is to replace the type of the object with one that
3298 # Our strategy is to replace the type of the object with one that
3295 # has all attribute lookups result in error.
3299 # has all attribute lookups result in error.
3296 #
3300 #
3297 # But we have to allow the close() method because some constructors
3301 # But we have to allow the close() method because some constructors
3298 # of repos call close() on repo references.
3302 # of repos call close() on repo references.
3299 class poisonedrepository(object):
3303 class poisonedrepository(object):
3300 def __getattribute__(self, item):
3304 def __getattribute__(self, item):
3301 if item == r'close':
3305 if item == r'close':
3302 return object.__getattribute__(self, item)
3306 return object.__getattribute__(self, item)
3303
3307
3304 raise error.ProgrammingError('repo instances should not be used '
3308 raise error.ProgrammingError('repo instances should not be used '
3305 'after unshare')
3309 'after unshare')
3306
3310
3307 def close(self):
3311 def close(self):
3308 pass
3312 pass
3309
3313
3310 # We may have a repoview, which intercepts __setattr__. So be sure
3314 # We may have a repoview, which intercepts __setattr__. So be sure
3311 # we operate at the lowest level possible.
3315 # we operate at the lowest level possible.
3312 object.__setattr__(repo, r'__class__', poisonedrepository)
3316 object.__setattr__(repo, r'__class__', poisonedrepository)
@@ -1,2103 +1,2105
1 # manifest.py - manifest revision class for mercurial
1 # manifest.py - manifest revision class for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import heapq
10 import heapq
11 import itertools
11 import itertools
12 import struct
12 import struct
13 import weakref
13 import weakref
14
14
15 from .i18n import _
15 from .i18n import _
16 from .node import (
16 from .node import (
17 bin,
17 bin,
18 hex,
18 hex,
19 nullid,
19 nullid,
20 nullrev,
20 nullrev,
21 )
21 )
22 from . import (
22 from . import (
23 error,
23 error,
24 mdiff,
24 mdiff,
25 policy,
25 policy,
26 pycompat,
26 pycompat,
27 repository,
28 revlog,
27 revlog,
29 util,
28 util,
30 )
29 )
30 from .interfaces import (
31 repository,
32 )
31 from .utils import (
33 from .utils import (
32 interfaceutil,
34 interfaceutil,
33 )
35 )
34
36
35 parsers = policy.importmod(r'parsers')
37 parsers = policy.importmod(r'parsers')
36 propertycache = util.propertycache
38 propertycache = util.propertycache
37
39
38 # Allow tests to more easily test the alternate path in manifestdict.fastdelta()
40 # Allow tests to more easily test the alternate path in manifestdict.fastdelta()
39 FASTDELTA_TEXTDIFF_THRESHOLD = 1000
41 FASTDELTA_TEXTDIFF_THRESHOLD = 1000
40
42
41 def _parse(data):
43 def _parse(data):
42 # This method does a little bit of excessive-looking
44 # This method does a little bit of excessive-looking
43 # precondition checking. This is so that the behavior of this
45 # precondition checking. This is so that the behavior of this
44 # class exactly matches its C counterpart to try and help
46 # class exactly matches its C counterpart to try and help
45 # prevent surprise breakage for anyone that develops against
47 # prevent surprise breakage for anyone that develops against
46 # the pure version.
48 # the pure version.
47 if data and data[-1:] != '\n':
49 if data and data[-1:] != '\n':
48 raise ValueError('Manifest did not end in a newline.')
50 raise ValueError('Manifest did not end in a newline.')
49 prev = None
51 prev = None
50 for l in data.splitlines():
52 for l in data.splitlines():
51 if prev is not None and prev > l:
53 if prev is not None and prev > l:
52 raise ValueError('Manifest lines not in sorted order.')
54 raise ValueError('Manifest lines not in sorted order.')
53 prev = l
55 prev = l
54 f, n = l.split('\0')
56 f, n = l.split('\0')
55 if len(n) > 40:
57 if len(n) > 40:
56 yield f, bin(n[:40]), n[40:]
58 yield f, bin(n[:40]), n[40:]
57 else:
59 else:
58 yield f, bin(n), ''
60 yield f, bin(n), ''
59
61
60 def _text(it):
62 def _text(it):
61 files = []
63 files = []
62 lines = []
64 lines = []
63 for f, n, fl in it:
65 for f, n, fl in it:
64 files.append(f)
66 files.append(f)
65 # if this is changed to support newlines in filenames,
67 # if this is changed to support newlines in filenames,
66 # be sure to check the templates/ dir again (especially *-raw.tmpl)
68 # be sure to check the templates/ dir again (especially *-raw.tmpl)
67 lines.append("%s\0%s%s\n" % (f, hex(n), fl))
69 lines.append("%s\0%s%s\n" % (f, hex(n), fl))
68
70
69 _checkforbidden(files)
71 _checkforbidden(files)
70 return ''.join(lines)
72 return ''.join(lines)
71
73
72 class lazymanifestiter(object):
74 class lazymanifestiter(object):
73 def __init__(self, lm):
75 def __init__(self, lm):
74 self.pos = 0
76 self.pos = 0
75 self.lm = lm
77 self.lm = lm
76
78
77 def __iter__(self):
79 def __iter__(self):
78 return self
80 return self
79
81
80 def next(self):
82 def next(self):
81 try:
83 try:
82 data, pos = self.lm._get(self.pos)
84 data, pos = self.lm._get(self.pos)
83 except IndexError:
85 except IndexError:
84 raise StopIteration
86 raise StopIteration
85 if pos == -1:
87 if pos == -1:
86 self.pos += 1
88 self.pos += 1
87 return data[0]
89 return data[0]
88 self.pos += 1
90 self.pos += 1
89 zeropos = data.find('\x00', pos)
91 zeropos = data.find('\x00', pos)
90 return data[pos:zeropos]
92 return data[pos:zeropos]
91
93
92 __next__ = next
94 __next__ = next
93
95
94 class lazymanifestiterentries(object):
96 class lazymanifestiterentries(object):
95 def __init__(self, lm):
97 def __init__(self, lm):
96 self.lm = lm
98 self.lm = lm
97 self.pos = 0
99 self.pos = 0
98
100
99 def __iter__(self):
101 def __iter__(self):
100 return self
102 return self
101
103
102 def next(self):
104 def next(self):
103 try:
105 try:
104 data, pos = self.lm._get(self.pos)
106 data, pos = self.lm._get(self.pos)
105 except IndexError:
107 except IndexError:
106 raise StopIteration
108 raise StopIteration
107 if pos == -1:
109 if pos == -1:
108 self.pos += 1
110 self.pos += 1
109 return data
111 return data
110 zeropos = data.find('\x00', pos)
112 zeropos = data.find('\x00', pos)
111 hashval = unhexlify(data, self.lm.extrainfo[self.pos],
113 hashval = unhexlify(data, self.lm.extrainfo[self.pos],
112 zeropos + 1, 40)
114 zeropos + 1, 40)
113 flags = self.lm._getflags(data, self.pos, zeropos)
115 flags = self.lm._getflags(data, self.pos, zeropos)
114 self.pos += 1
116 self.pos += 1
115 return (data[pos:zeropos], hashval, flags)
117 return (data[pos:zeropos], hashval, flags)
116
118
117 __next__ = next
119 __next__ = next
118
120
119 def unhexlify(data, extra, pos, length):
121 def unhexlify(data, extra, pos, length):
120 s = bin(data[pos:pos + length])
122 s = bin(data[pos:pos + length])
121 if extra:
123 if extra:
122 s += chr(extra & 0xff)
124 s += chr(extra & 0xff)
123 return s
125 return s
124
126
125 def _cmp(a, b):
127 def _cmp(a, b):
126 return (a > b) - (a < b)
128 return (a > b) - (a < b)
127
129
128 class _lazymanifest(object):
130 class _lazymanifest(object):
129 """A pure python manifest backed by a byte string. It is supplimented with
131 """A pure python manifest backed by a byte string. It is supplimented with
130 internal lists as it is modified, until it is compacted back to a pure byte
132 internal lists as it is modified, until it is compacted back to a pure byte
131 string.
133 string.
132
134
133 ``data`` is the initial manifest data.
135 ``data`` is the initial manifest data.
134
136
135 ``positions`` is a list of offsets, one per manifest entry. Positive
137 ``positions`` is a list of offsets, one per manifest entry. Positive
136 values are offsets into ``data``, negative values are offsets into the
138 values are offsets into ``data``, negative values are offsets into the
137 ``extradata`` list. When an entry is removed, its entry is dropped from
139 ``extradata`` list. When an entry is removed, its entry is dropped from
138 ``positions``. The values are encoded such that when walking the list and
140 ``positions``. The values are encoded such that when walking the list and
139 indexing into ``data`` or ``extradata`` as appropriate, the entries are
141 indexing into ``data`` or ``extradata`` as appropriate, the entries are
140 sorted by filename.
142 sorted by filename.
141
143
142 ``extradata`` is a list of (key, hash, flags) for entries that were added or
144 ``extradata`` is a list of (key, hash, flags) for entries that were added or
143 modified since the manifest was created or compacted.
145 modified since the manifest was created or compacted.
144 """
146 """
145 def __init__(self, data, positions=None, extrainfo=None, extradata=None,
147 def __init__(self, data, positions=None, extrainfo=None, extradata=None,
146 hasremovals=False):
148 hasremovals=False):
147 if positions is None:
149 if positions is None:
148 self.positions = self.findlines(data)
150 self.positions = self.findlines(data)
149 self.extrainfo = [0] * len(self.positions)
151 self.extrainfo = [0] * len(self.positions)
150 self.data = data
152 self.data = data
151 self.extradata = []
153 self.extradata = []
152 self.hasremovals = False
154 self.hasremovals = False
153 else:
155 else:
154 self.positions = positions[:]
156 self.positions = positions[:]
155 self.extrainfo = extrainfo[:]
157 self.extrainfo = extrainfo[:]
156 self.extradata = extradata[:]
158 self.extradata = extradata[:]
157 self.data = data
159 self.data = data
158 self.hasremovals = hasremovals
160 self.hasremovals = hasremovals
159
161
160 def findlines(self, data):
162 def findlines(self, data):
161 if not data:
163 if not data:
162 return []
164 return []
163 pos = data.find("\n")
165 pos = data.find("\n")
164 if pos == -1 or data[-1:] != '\n':
166 if pos == -1 or data[-1:] != '\n':
165 raise ValueError("Manifest did not end in a newline.")
167 raise ValueError("Manifest did not end in a newline.")
166 positions = [0]
168 positions = [0]
167 prev = data[:data.find('\x00')]
169 prev = data[:data.find('\x00')]
168 while pos < len(data) - 1 and pos != -1:
170 while pos < len(data) - 1 and pos != -1:
169 positions.append(pos + 1)
171 positions.append(pos + 1)
170 nexts = data[pos + 1:data.find('\x00', pos + 1)]
172 nexts = data[pos + 1:data.find('\x00', pos + 1)]
171 if nexts < prev:
173 if nexts < prev:
172 raise ValueError("Manifest lines not in sorted order.")
174 raise ValueError("Manifest lines not in sorted order.")
173 prev = nexts
175 prev = nexts
174 pos = data.find("\n", pos + 1)
176 pos = data.find("\n", pos + 1)
175 return positions
177 return positions
176
178
177 def _get(self, index):
179 def _get(self, index):
178 # get the position encoded in pos:
180 # get the position encoded in pos:
179 # positive number is an index in 'data'
181 # positive number is an index in 'data'
180 # negative number is in extrapieces
182 # negative number is in extrapieces
181 pos = self.positions[index]
183 pos = self.positions[index]
182 if pos >= 0:
184 if pos >= 0:
183 return self.data, pos
185 return self.data, pos
184 return self.extradata[-pos - 1], -1
186 return self.extradata[-pos - 1], -1
185
187
186 def _getkey(self, pos):
188 def _getkey(self, pos):
187 if pos >= 0:
189 if pos >= 0:
188 return self.data[pos:self.data.find('\x00', pos + 1)]
190 return self.data[pos:self.data.find('\x00', pos + 1)]
189 return self.extradata[-pos - 1][0]
191 return self.extradata[-pos - 1][0]
190
192
191 def bsearch(self, key):
193 def bsearch(self, key):
192 first = 0
194 first = 0
193 last = len(self.positions) - 1
195 last = len(self.positions) - 1
194
196
195 while first <= last:
197 while first <= last:
196 midpoint = (first + last)//2
198 midpoint = (first + last)//2
197 nextpos = self.positions[midpoint]
199 nextpos = self.positions[midpoint]
198 candidate = self._getkey(nextpos)
200 candidate = self._getkey(nextpos)
199 r = _cmp(key, candidate)
201 r = _cmp(key, candidate)
200 if r == 0:
202 if r == 0:
201 return midpoint
203 return midpoint
202 else:
204 else:
203 if r < 0:
205 if r < 0:
204 last = midpoint - 1
206 last = midpoint - 1
205 else:
207 else:
206 first = midpoint + 1
208 first = midpoint + 1
207 return -1
209 return -1
208
210
209 def bsearch2(self, key):
211 def bsearch2(self, key):
210 # same as the above, but will always return the position
212 # same as the above, but will always return the position
211 # done for performance reasons
213 # done for performance reasons
212 first = 0
214 first = 0
213 last = len(self.positions) - 1
215 last = len(self.positions) - 1
214
216
215 while first <= last:
217 while first <= last:
216 midpoint = (first + last)//2
218 midpoint = (first + last)//2
217 nextpos = self.positions[midpoint]
219 nextpos = self.positions[midpoint]
218 candidate = self._getkey(nextpos)
220 candidate = self._getkey(nextpos)
219 r = _cmp(key, candidate)
221 r = _cmp(key, candidate)
220 if r == 0:
222 if r == 0:
221 return (midpoint, True)
223 return (midpoint, True)
222 else:
224 else:
223 if r < 0:
225 if r < 0:
224 last = midpoint - 1
226 last = midpoint - 1
225 else:
227 else:
226 first = midpoint + 1
228 first = midpoint + 1
227 return (first, False)
229 return (first, False)
228
230
229 def __contains__(self, key):
231 def __contains__(self, key):
230 return self.bsearch(key) != -1
232 return self.bsearch(key) != -1
231
233
232 def _getflags(self, data, needle, pos):
234 def _getflags(self, data, needle, pos):
233 start = pos + 41
235 start = pos + 41
234 end = data.find("\n", start)
236 end = data.find("\n", start)
235 if end == -1:
237 if end == -1:
236 end = len(data) - 1
238 end = len(data) - 1
237 if start == end:
239 if start == end:
238 return ''
240 return ''
239 return self.data[start:end]
241 return self.data[start:end]
240
242
241 def __getitem__(self, key):
243 def __getitem__(self, key):
242 if not isinstance(key, bytes):
244 if not isinstance(key, bytes):
243 raise TypeError("getitem: manifest keys must be a bytes.")
245 raise TypeError("getitem: manifest keys must be a bytes.")
244 needle = self.bsearch(key)
246 needle = self.bsearch(key)
245 if needle == -1:
247 if needle == -1:
246 raise KeyError
248 raise KeyError
247 data, pos = self._get(needle)
249 data, pos = self._get(needle)
248 if pos == -1:
250 if pos == -1:
249 return (data[1], data[2])
251 return (data[1], data[2])
250 zeropos = data.find('\x00', pos)
252 zeropos = data.find('\x00', pos)
251 assert 0 <= needle <= len(self.positions)
253 assert 0 <= needle <= len(self.positions)
252 assert len(self.extrainfo) == len(self.positions)
254 assert len(self.extrainfo) == len(self.positions)
253 hashval = unhexlify(data, self.extrainfo[needle], zeropos + 1, 40)
255 hashval = unhexlify(data, self.extrainfo[needle], zeropos + 1, 40)
254 flags = self._getflags(data, needle, zeropos)
256 flags = self._getflags(data, needle, zeropos)
255 return (hashval, flags)
257 return (hashval, flags)
256
258
257 def __delitem__(self, key):
259 def __delitem__(self, key):
258 needle, found = self.bsearch2(key)
260 needle, found = self.bsearch2(key)
259 if not found:
261 if not found:
260 raise KeyError
262 raise KeyError
261 cur = self.positions[needle]
263 cur = self.positions[needle]
262 self.positions = self.positions[:needle] + self.positions[needle + 1:]
264 self.positions = self.positions[:needle] + self.positions[needle + 1:]
263 self.extrainfo = self.extrainfo[:needle] + self.extrainfo[needle + 1:]
265 self.extrainfo = self.extrainfo[:needle] + self.extrainfo[needle + 1:]
264 if cur >= 0:
266 if cur >= 0:
265 # This does NOT unsort the list as far as the search functions are
267 # This does NOT unsort the list as far as the search functions are
266 # concerned, as they only examine lines mapped by self.positions.
268 # concerned, as they only examine lines mapped by self.positions.
267 self.data = self.data[:cur] + '\x00' + self.data[cur + 1:]
269 self.data = self.data[:cur] + '\x00' + self.data[cur + 1:]
268 self.hasremovals = True
270 self.hasremovals = True
269
271
270 def __setitem__(self, key, value):
272 def __setitem__(self, key, value):
271 if not isinstance(key, bytes):
273 if not isinstance(key, bytes):
272 raise TypeError("setitem: manifest keys must be a byte string.")
274 raise TypeError("setitem: manifest keys must be a byte string.")
273 if not isinstance(value, tuple) or len(value) != 2:
275 if not isinstance(value, tuple) or len(value) != 2:
274 raise TypeError("Manifest values must be a tuple of (node, flags).")
276 raise TypeError("Manifest values must be a tuple of (node, flags).")
275 hashval = value[0]
277 hashval = value[0]
276 if not isinstance(hashval, bytes) or not 20 <= len(hashval) <= 22:
278 if not isinstance(hashval, bytes) or not 20 <= len(hashval) <= 22:
277 raise TypeError("node must be a 20-byte byte string")
279 raise TypeError("node must be a 20-byte byte string")
278 flags = value[1]
280 flags = value[1]
279 if len(hashval) == 22:
281 if len(hashval) == 22:
280 hashval = hashval[:-1]
282 hashval = hashval[:-1]
281 if not isinstance(flags, bytes) or len(flags) > 1:
283 if not isinstance(flags, bytes) or len(flags) > 1:
282 raise TypeError("flags must a 0 or 1 byte string, got %r", flags)
284 raise TypeError("flags must a 0 or 1 byte string, got %r", flags)
283 needle, found = self.bsearch2(key)
285 needle, found = self.bsearch2(key)
284 if found:
286 if found:
285 # put the item
287 # put the item
286 pos = self.positions[needle]
288 pos = self.positions[needle]
287 if pos < 0:
289 if pos < 0:
288 self.extradata[-pos - 1] = (key, hashval, value[1])
290 self.extradata[-pos - 1] = (key, hashval, value[1])
289 else:
291 else:
290 # just don't bother
292 # just don't bother
291 self.extradata.append((key, hashval, value[1]))
293 self.extradata.append((key, hashval, value[1]))
292 self.positions[needle] = -len(self.extradata)
294 self.positions[needle] = -len(self.extradata)
293 else:
295 else:
294 # not found, put it in with extra positions
296 # not found, put it in with extra positions
295 self.extradata.append((key, hashval, value[1]))
297 self.extradata.append((key, hashval, value[1]))
296 self.positions = (self.positions[:needle] + [-len(self.extradata)]
298 self.positions = (self.positions[:needle] + [-len(self.extradata)]
297 + self.positions[needle:])
299 + self.positions[needle:])
298 self.extrainfo = (self.extrainfo[:needle] + [0] +
300 self.extrainfo = (self.extrainfo[:needle] + [0] +
299 self.extrainfo[needle:])
301 self.extrainfo[needle:])
300
302
301 def copy(self):
303 def copy(self):
302 # XXX call _compact like in C?
304 # XXX call _compact like in C?
303 return _lazymanifest(self.data, self.positions, self.extrainfo,
305 return _lazymanifest(self.data, self.positions, self.extrainfo,
304 self.extradata, self.hasremovals)
306 self.extradata, self.hasremovals)
305
307
306 def _compact(self):
308 def _compact(self):
307 # hopefully not called TOO often
309 # hopefully not called TOO often
308 if len(self.extradata) == 0 and not self.hasremovals:
310 if len(self.extradata) == 0 and not self.hasremovals:
309 return
311 return
310 l = []
312 l = []
311 i = 0
313 i = 0
312 offset = 0
314 offset = 0
313 self.extrainfo = [0] * len(self.positions)
315 self.extrainfo = [0] * len(self.positions)
314 while i < len(self.positions):
316 while i < len(self.positions):
315 if self.positions[i] >= 0:
317 if self.positions[i] >= 0:
316 cur = self.positions[i]
318 cur = self.positions[i]
317 last_cut = cur
319 last_cut = cur
318
320
319 # Collect all contiguous entries in the buffer at the current
321 # Collect all contiguous entries in the buffer at the current
320 # offset, breaking out only for added/modified items held in
322 # offset, breaking out only for added/modified items held in
321 # extradata, or a deleted line prior to the next position.
323 # extradata, or a deleted line prior to the next position.
322 while True:
324 while True:
323 self.positions[i] = offset
325 self.positions[i] = offset
324 i += 1
326 i += 1
325 if i == len(self.positions) or self.positions[i] < 0:
327 if i == len(self.positions) or self.positions[i] < 0:
326 break
328 break
327
329
328 # A removed file has no positions[] entry, but does have an
330 # A removed file has no positions[] entry, but does have an
329 # overwritten first byte. Break out and find the end of the
331 # overwritten first byte. Break out and find the end of the
330 # current good entry/entries if there is a removed file
332 # current good entry/entries if there is a removed file
331 # before the next position.
333 # before the next position.
332 if (self.hasremovals
334 if (self.hasremovals
333 and self.data.find('\n\x00', cur,
335 and self.data.find('\n\x00', cur,
334 self.positions[i]) != -1):
336 self.positions[i]) != -1):
335 break
337 break
336
338
337 offset += self.positions[i] - cur
339 offset += self.positions[i] - cur
338 cur = self.positions[i]
340 cur = self.positions[i]
339 end_cut = self.data.find('\n', cur)
341 end_cut = self.data.find('\n', cur)
340 if end_cut != -1:
342 if end_cut != -1:
341 end_cut += 1
343 end_cut += 1
342 offset += end_cut - cur
344 offset += end_cut - cur
343 l.append(self.data[last_cut:end_cut])
345 l.append(self.data[last_cut:end_cut])
344 else:
346 else:
345 while i < len(self.positions) and self.positions[i] < 0:
347 while i < len(self.positions) and self.positions[i] < 0:
346 cur = self.positions[i]
348 cur = self.positions[i]
347 t = self.extradata[-cur - 1]
349 t = self.extradata[-cur - 1]
348 l.append(self._pack(t))
350 l.append(self._pack(t))
349 self.positions[i] = offset
351 self.positions[i] = offset
350 if len(t[1]) > 20:
352 if len(t[1]) > 20:
351 self.extrainfo[i] = ord(t[1][21])
353 self.extrainfo[i] = ord(t[1][21])
352 offset += len(l[-1])
354 offset += len(l[-1])
353 i += 1
355 i += 1
354 self.data = ''.join(l)
356 self.data = ''.join(l)
355 self.hasremovals = False
357 self.hasremovals = False
356 self.extradata = []
358 self.extradata = []
357
359
358 def _pack(self, d):
360 def _pack(self, d):
359 return d[0] + '\x00' + hex(d[1][:20]) + d[2] + '\n'
361 return d[0] + '\x00' + hex(d[1][:20]) + d[2] + '\n'
360
362
361 def text(self):
363 def text(self):
362 self._compact()
364 self._compact()
363 return self.data
365 return self.data
364
366
365 def diff(self, m2, clean=False):
367 def diff(self, m2, clean=False):
366 '''Finds changes between the current manifest and m2.'''
368 '''Finds changes between the current manifest and m2.'''
367 # XXX think whether efficiency matters here
369 # XXX think whether efficiency matters here
368 diff = {}
370 diff = {}
369
371
370 for fn, e1, flags in self.iterentries():
372 for fn, e1, flags in self.iterentries():
371 if fn not in m2:
373 if fn not in m2:
372 diff[fn] = (e1, flags), (None, '')
374 diff[fn] = (e1, flags), (None, '')
373 else:
375 else:
374 e2 = m2[fn]
376 e2 = m2[fn]
375 if (e1, flags) != e2:
377 if (e1, flags) != e2:
376 diff[fn] = (e1, flags), e2
378 diff[fn] = (e1, flags), e2
377 elif clean:
379 elif clean:
378 diff[fn] = None
380 diff[fn] = None
379
381
380 for fn, e2, flags in m2.iterentries():
382 for fn, e2, flags in m2.iterentries():
381 if fn not in self:
383 if fn not in self:
382 diff[fn] = (None, ''), (e2, flags)
384 diff[fn] = (None, ''), (e2, flags)
383
385
384 return diff
386 return diff
385
387
386 def iterentries(self):
388 def iterentries(self):
387 return lazymanifestiterentries(self)
389 return lazymanifestiterentries(self)
388
390
389 def iterkeys(self):
391 def iterkeys(self):
390 return lazymanifestiter(self)
392 return lazymanifestiter(self)
391
393
392 def __iter__(self):
394 def __iter__(self):
393 return lazymanifestiter(self)
395 return lazymanifestiter(self)
394
396
395 def __len__(self):
397 def __len__(self):
396 return len(self.positions)
398 return len(self.positions)
397
399
398 def filtercopy(self, filterfn):
400 def filtercopy(self, filterfn):
399 # XXX should be optimized
401 # XXX should be optimized
400 c = _lazymanifest('')
402 c = _lazymanifest('')
401 for f, n, fl in self.iterentries():
403 for f, n, fl in self.iterentries():
402 if filterfn(f):
404 if filterfn(f):
403 c[f] = n, fl
405 c[f] = n, fl
404 return c
406 return c
405
407
406 try:
408 try:
407 _lazymanifest = parsers.lazymanifest
409 _lazymanifest = parsers.lazymanifest
408 except AttributeError:
410 except AttributeError:
409 pass
411 pass
410
412
411 @interfaceutil.implementer(repository.imanifestdict)
413 @interfaceutil.implementer(repository.imanifestdict)
412 class manifestdict(object):
414 class manifestdict(object):
413 def __init__(self, data=''):
415 def __init__(self, data=''):
414 self._lm = _lazymanifest(data)
416 self._lm = _lazymanifest(data)
415
417
416 def __getitem__(self, key):
418 def __getitem__(self, key):
417 return self._lm[key][0]
419 return self._lm[key][0]
418
420
419 def find(self, key):
421 def find(self, key):
420 return self._lm[key]
422 return self._lm[key]
421
423
422 def __len__(self):
424 def __len__(self):
423 return len(self._lm)
425 return len(self._lm)
424
426
425 def __nonzero__(self):
427 def __nonzero__(self):
426 # nonzero is covered by the __len__ function, but implementing it here
428 # nonzero is covered by the __len__ function, but implementing it here
427 # makes it easier for extensions to override.
429 # makes it easier for extensions to override.
428 return len(self._lm) != 0
430 return len(self._lm) != 0
429
431
430 __bool__ = __nonzero__
432 __bool__ = __nonzero__
431
433
432 def __setitem__(self, key, node):
434 def __setitem__(self, key, node):
433 self._lm[key] = node, self.flags(key, '')
435 self._lm[key] = node, self.flags(key, '')
434
436
435 def __contains__(self, key):
437 def __contains__(self, key):
436 if key is None:
438 if key is None:
437 return False
439 return False
438 return key in self._lm
440 return key in self._lm
439
441
440 def __delitem__(self, key):
442 def __delitem__(self, key):
441 del self._lm[key]
443 del self._lm[key]
442
444
443 def __iter__(self):
445 def __iter__(self):
444 return self._lm.__iter__()
446 return self._lm.__iter__()
445
447
446 def iterkeys(self):
448 def iterkeys(self):
447 return self._lm.iterkeys()
449 return self._lm.iterkeys()
448
450
449 def keys(self):
451 def keys(self):
450 return list(self.iterkeys())
452 return list(self.iterkeys())
451
453
452 def filesnotin(self, m2, match=None):
454 def filesnotin(self, m2, match=None):
453 '''Set of files in this manifest that are not in the other'''
455 '''Set of files in this manifest that are not in the other'''
454 if match:
456 if match:
455 m1 = self.matches(match)
457 m1 = self.matches(match)
456 m2 = m2.matches(match)
458 m2 = m2.matches(match)
457 return m1.filesnotin(m2)
459 return m1.filesnotin(m2)
458 diff = self.diff(m2)
460 diff = self.diff(m2)
459 files = set(filepath
461 files = set(filepath
460 for filepath, hashflags in diff.iteritems()
462 for filepath, hashflags in diff.iteritems()
461 if hashflags[1][0] is None)
463 if hashflags[1][0] is None)
462 return files
464 return files
463
465
464 @propertycache
466 @propertycache
465 def _dirs(self):
467 def _dirs(self):
466 return util.dirs(self)
468 return util.dirs(self)
467
469
468 def dirs(self):
470 def dirs(self):
469 return self._dirs
471 return self._dirs
470
472
471 def hasdir(self, dir):
473 def hasdir(self, dir):
472 return dir in self._dirs
474 return dir in self._dirs
473
475
474 def _filesfastpath(self, match):
476 def _filesfastpath(self, match):
475 '''Checks whether we can correctly and quickly iterate over matcher
477 '''Checks whether we can correctly and quickly iterate over matcher
476 files instead of over manifest files.'''
478 files instead of over manifest files.'''
477 files = match.files()
479 files = match.files()
478 return (len(files) < 100 and (match.isexact() or
480 return (len(files) < 100 and (match.isexact() or
479 (match.prefix() and all(fn in self for fn in files))))
481 (match.prefix() and all(fn in self for fn in files))))
480
482
481 def walk(self, match):
483 def walk(self, match):
482 '''Generates matching file names.
484 '''Generates matching file names.
483
485
484 Equivalent to manifest.matches(match).iterkeys(), but without creating
486 Equivalent to manifest.matches(match).iterkeys(), but without creating
485 an entirely new manifest.
487 an entirely new manifest.
486
488
487 It also reports nonexistent files by marking them bad with match.bad().
489 It also reports nonexistent files by marking them bad with match.bad().
488 '''
490 '''
489 if match.always():
491 if match.always():
490 for f in iter(self):
492 for f in iter(self):
491 yield f
493 yield f
492 return
494 return
493
495
494 fset = set(match.files())
496 fset = set(match.files())
495
497
496 # avoid the entire walk if we're only looking for specific files
498 # avoid the entire walk if we're only looking for specific files
497 if self._filesfastpath(match):
499 if self._filesfastpath(match):
498 for fn in sorted(fset):
500 for fn in sorted(fset):
499 yield fn
501 yield fn
500 return
502 return
501
503
502 for fn in self:
504 for fn in self:
503 if fn in fset:
505 if fn in fset:
504 # specified pattern is the exact name
506 # specified pattern is the exact name
505 fset.remove(fn)
507 fset.remove(fn)
506 if match(fn):
508 if match(fn):
507 yield fn
509 yield fn
508
510
509 # for dirstate.walk, files=[''] means "walk the whole tree".
511 # for dirstate.walk, files=[''] means "walk the whole tree".
510 # follow that here, too
512 # follow that here, too
511 fset.discard('')
513 fset.discard('')
512
514
513 for fn in sorted(fset):
515 for fn in sorted(fset):
514 if not self.hasdir(fn):
516 if not self.hasdir(fn):
515 match.bad(fn, None)
517 match.bad(fn, None)
516
518
517 def matches(self, match):
519 def matches(self, match):
518 '''generate a new manifest filtered by the match argument'''
520 '''generate a new manifest filtered by the match argument'''
519 if match.always():
521 if match.always():
520 return self.copy()
522 return self.copy()
521
523
522 if self._filesfastpath(match):
524 if self._filesfastpath(match):
523 m = manifestdict()
525 m = manifestdict()
524 lm = self._lm
526 lm = self._lm
525 for fn in match.files():
527 for fn in match.files():
526 if fn in lm:
528 if fn in lm:
527 m._lm[fn] = lm[fn]
529 m._lm[fn] = lm[fn]
528 return m
530 return m
529
531
530 m = manifestdict()
532 m = manifestdict()
531 m._lm = self._lm.filtercopy(match)
533 m._lm = self._lm.filtercopy(match)
532 return m
534 return m
533
535
534 def diff(self, m2, match=None, clean=False):
536 def diff(self, m2, match=None, clean=False):
535 '''Finds changes between the current manifest and m2.
537 '''Finds changes between the current manifest and m2.
536
538
537 Args:
539 Args:
538 m2: the manifest to which this manifest should be compared.
540 m2: the manifest to which this manifest should be compared.
539 clean: if true, include files unchanged between these manifests
541 clean: if true, include files unchanged between these manifests
540 with a None value in the returned dictionary.
542 with a None value in the returned dictionary.
541
543
542 The result is returned as a dict with filename as key and
544 The result is returned as a dict with filename as key and
543 values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
545 values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
544 nodeid in the current/other manifest and fl1/fl2 is the flag
546 nodeid in the current/other manifest and fl1/fl2 is the flag
545 in the current/other manifest. Where the file does not exist,
547 in the current/other manifest. Where the file does not exist,
546 the nodeid will be None and the flags will be the empty
548 the nodeid will be None and the flags will be the empty
547 string.
549 string.
548 '''
550 '''
549 if match:
551 if match:
550 m1 = self.matches(match)
552 m1 = self.matches(match)
551 m2 = m2.matches(match)
553 m2 = m2.matches(match)
552 return m1.diff(m2, clean=clean)
554 return m1.diff(m2, clean=clean)
553 return self._lm.diff(m2._lm, clean)
555 return self._lm.diff(m2._lm, clean)
554
556
555 def setflag(self, key, flag):
557 def setflag(self, key, flag):
556 self._lm[key] = self[key], flag
558 self._lm[key] = self[key], flag
557
559
558 def get(self, key, default=None):
560 def get(self, key, default=None):
559 try:
561 try:
560 return self._lm[key][0]
562 return self._lm[key][0]
561 except KeyError:
563 except KeyError:
562 return default
564 return default
563
565
564 def flags(self, key, default=''):
566 def flags(self, key, default=''):
565 try:
567 try:
566 return self._lm[key][1]
568 return self._lm[key][1]
567 except KeyError:
569 except KeyError:
568 return default
570 return default
569
571
570 def copy(self):
572 def copy(self):
571 c = manifestdict()
573 c = manifestdict()
572 c._lm = self._lm.copy()
574 c._lm = self._lm.copy()
573 return c
575 return c
574
576
575 def items(self):
577 def items(self):
576 return (x[:2] for x in self._lm.iterentries())
578 return (x[:2] for x in self._lm.iterentries())
577
579
578 def iteritems(self):
580 def iteritems(self):
579 return (x[:2] for x in self._lm.iterentries())
581 return (x[:2] for x in self._lm.iterentries())
580
582
581 def iterentries(self):
583 def iterentries(self):
582 return self._lm.iterentries()
584 return self._lm.iterentries()
583
585
584 def text(self):
586 def text(self):
585 # most likely uses native version
587 # most likely uses native version
586 return self._lm.text()
588 return self._lm.text()
587
589
588 def fastdelta(self, base, changes):
590 def fastdelta(self, base, changes):
589 """Given a base manifest text as a bytearray and a list of changes
591 """Given a base manifest text as a bytearray and a list of changes
590 relative to that text, compute a delta that can be used by revlog.
592 relative to that text, compute a delta that can be used by revlog.
591 """
593 """
592 delta = []
594 delta = []
593 dstart = None
595 dstart = None
594 dend = None
596 dend = None
595 dline = [""]
597 dline = [""]
596 start = 0
598 start = 0
597 # zero copy representation of base as a buffer
599 # zero copy representation of base as a buffer
598 addbuf = util.buffer(base)
600 addbuf = util.buffer(base)
599
601
600 changes = list(changes)
602 changes = list(changes)
601 if len(changes) < FASTDELTA_TEXTDIFF_THRESHOLD:
603 if len(changes) < FASTDELTA_TEXTDIFF_THRESHOLD:
602 # start with a readonly loop that finds the offset of
604 # start with a readonly loop that finds the offset of
603 # each line and creates the deltas
605 # each line and creates the deltas
604 for f, todelete in changes:
606 for f, todelete in changes:
605 # bs will either be the index of the item or the insert point
607 # bs will either be the index of the item or the insert point
606 start, end = _msearch(addbuf, f, start)
608 start, end = _msearch(addbuf, f, start)
607 if not todelete:
609 if not todelete:
608 h, fl = self._lm[f]
610 h, fl = self._lm[f]
609 l = "%s\0%s%s\n" % (f, hex(h), fl)
611 l = "%s\0%s%s\n" % (f, hex(h), fl)
610 else:
612 else:
611 if start == end:
613 if start == end:
612 # item we want to delete was not found, error out
614 # item we want to delete was not found, error out
613 raise AssertionError(
615 raise AssertionError(
614 _("failed to remove %s from manifest") % f)
616 _("failed to remove %s from manifest") % f)
615 l = ""
617 l = ""
616 if dstart is not None and dstart <= start and dend >= start:
618 if dstart is not None and dstart <= start and dend >= start:
617 if dend < end:
619 if dend < end:
618 dend = end
620 dend = end
619 if l:
621 if l:
620 dline.append(l)
622 dline.append(l)
621 else:
623 else:
622 if dstart is not None:
624 if dstart is not None:
623 delta.append([dstart, dend, "".join(dline)])
625 delta.append([dstart, dend, "".join(dline)])
624 dstart = start
626 dstart = start
625 dend = end
627 dend = end
626 dline = [l]
628 dline = [l]
627
629
628 if dstart is not None:
630 if dstart is not None:
629 delta.append([dstart, dend, "".join(dline)])
631 delta.append([dstart, dend, "".join(dline)])
630 # apply the delta to the base, and get a delta for addrevision
632 # apply the delta to the base, and get a delta for addrevision
631 deltatext, arraytext = _addlistdelta(base, delta)
633 deltatext, arraytext = _addlistdelta(base, delta)
632 else:
634 else:
633 # For large changes, it's much cheaper to just build the text and
635 # For large changes, it's much cheaper to just build the text and
634 # diff it.
636 # diff it.
635 arraytext = bytearray(self.text())
637 arraytext = bytearray(self.text())
636 deltatext = mdiff.textdiff(
638 deltatext = mdiff.textdiff(
637 util.buffer(base), util.buffer(arraytext))
639 util.buffer(base), util.buffer(arraytext))
638
640
639 return arraytext, deltatext
641 return arraytext, deltatext
640
642
641 def _msearch(m, s, lo=0, hi=None):
643 def _msearch(m, s, lo=0, hi=None):
642 '''return a tuple (start, end) that says where to find s within m.
644 '''return a tuple (start, end) that says where to find s within m.
643
645
644 If the string is found m[start:end] are the line containing
646 If the string is found m[start:end] are the line containing
645 that string. If start == end the string was not found and
647 that string. If start == end the string was not found and
646 they indicate the proper sorted insertion point.
648 they indicate the proper sorted insertion point.
647
649
648 m should be a buffer, a memoryview or a byte string.
650 m should be a buffer, a memoryview or a byte string.
649 s is a byte string'''
651 s is a byte string'''
650 def advance(i, c):
652 def advance(i, c):
651 while i < lenm and m[i:i + 1] != c:
653 while i < lenm and m[i:i + 1] != c:
652 i += 1
654 i += 1
653 return i
655 return i
654 if not s:
656 if not s:
655 return (lo, lo)
657 return (lo, lo)
656 lenm = len(m)
658 lenm = len(m)
657 if not hi:
659 if not hi:
658 hi = lenm
660 hi = lenm
659 while lo < hi:
661 while lo < hi:
660 mid = (lo + hi) // 2
662 mid = (lo + hi) // 2
661 start = mid
663 start = mid
662 while start > 0 and m[start - 1:start] != '\n':
664 while start > 0 and m[start - 1:start] != '\n':
663 start -= 1
665 start -= 1
664 end = advance(start, '\0')
666 end = advance(start, '\0')
665 if bytes(m[start:end]) < s:
667 if bytes(m[start:end]) < s:
666 # we know that after the null there are 40 bytes of sha1
668 # we know that after the null there are 40 bytes of sha1
667 # this translates to the bisect lo = mid + 1
669 # this translates to the bisect lo = mid + 1
668 lo = advance(end + 40, '\n') + 1
670 lo = advance(end + 40, '\n') + 1
669 else:
671 else:
670 # this translates to the bisect hi = mid
672 # this translates to the bisect hi = mid
671 hi = start
673 hi = start
672 end = advance(lo, '\0')
674 end = advance(lo, '\0')
673 found = m[lo:end]
675 found = m[lo:end]
674 if s == found:
676 if s == found:
675 # we know that after the null there are 40 bytes of sha1
677 # we know that after the null there are 40 bytes of sha1
676 end = advance(end + 40, '\n')
678 end = advance(end + 40, '\n')
677 return (lo, end + 1)
679 return (lo, end + 1)
678 else:
680 else:
679 return (lo, lo)
681 return (lo, lo)
680
682
681 def _checkforbidden(l):
683 def _checkforbidden(l):
682 """Check filenames for illegal characters."""
684 """Check filenames for illegal characters."""
683 for f in l:
685 for f in l:
684 if '\n' in f or '\r' in f:
686 if '\n' in f or '\r' in f:
685 raise error.StorageError(
687 raise error.StorageError(
686 _("'\\n' and '\\r' disallowed in filenames: %r")
688 _("'\\n' and '\\r' disallowed in filenames: %r")
687 % pycompat.bytestr(f))
689 % pycompat.bytestr(f))
688
690
689
691
690 # apply the changes collected during the bisect loop to our addlist
692 # apply the changes collected during the bisect loop to our addlist
691 # return a delta suitable for addrevision
693 # return a delta suitable for addrevision
692 def _addlistdelta(addlist, x):
694 def _addlistdelta(addlist, x):
693 # for large addlist arrays, building a new array is cheaper
695 # for large addlist arrays, building a new array is cheaper
694 # than repeatedly modifying the existing one
696 # than repeatedly modifying the existing one
695 currentposition = 0
697 currentposition = 0
696 newaddlist = bytearray()
698 newaddlist = bytearray()
697
699
698 for start, end, content in x:
700 for start, end, content in x:
699 newaddlist += addlist[currentposition:start]
701 newaddlist += addlist[currentposition:start]
700 if content:
702 if content:
701 newaddlist += bytearray(content)
703 newaddlist += bytearray(content)
702
704
703 currentposition = end
705 currentposition = end
704
706
705 newaddlist += addlist[currentposition:]
707 newaddlist += addlist[currentposition:]
706
708
707 deltatext = "".join(struct.pack(">lll", start, end, len(content))
709 deltatext = "".join(struct.pack(">lll", start, end, len(content))
708 + content for start, end, content in x)
710 + content for start, end, content in x)
709 return deltatext, newaddlist
711 return deltatext, newaddlist
710
712
711 def _splittopdir(f):
713 def _splittopdir(f):
712 if '/' in f:
714 if '/' in f:
713 dir, subpath = f.split('/', 1)
715 dir, subpath = f.split('/', 1)
714 return dir + '/', subpath
716 return dir + '/', subpath
715 else:
717 else:
716 return '', f
718 return '', f
717
719
718 _noop = lambda s: None
720 _noop = lambda s: None
719
721
720 class treemanifest(object):
722 class treemanifest(object):
721 def __init__(self, dir='', text=''):
723 def __init__(self, dir='', text=''):
722 self._dir = dir
724 self._dir = dir
723 self._node = nullid
725 self._node = nullid
724 self._loadfunc = _noop
726 self._loadfunc = _noop
725 self._copyfunc = _noop
727 self._copyfunc = _noop
726 self._dirty = False
728 self._dirty = False
727 self._dirs = {}
729 self._dirs = {}
728 self._lazydirs = {}
730 self._lazydirs = {}
729 # Using _lazymanifest here is a little slower than plain old dicts
731 # Using _lazymanifest here is a little slower than plain old dicts
730 self._files = {}
732 self._files = {}
731 self._flags = {}
733 self._flags = {}
732 if text:
734 if text:
733 def readsubtree(subdir, subm):
735 def readsubtree(subdir, subm):
734 raise AssertionError('treemanifest constructor only accepts '
736 raise AssertionError('treemanifest constructor only accepts '
735 'flat manifests')
737 'flat manifests')
736 self.parse(text, readsubtree)
738 self.parse(text, readsubtree)
737 self._dirty = True # Mark flat manifest dirty after parsing
739 self._dirty = True # Mark flat manifest dirty after parsing
738
740
739 def _subpath(self, path):
741 def _subpath(self, path):
740 return self._dir + path
742 return self._dir + path
741
743
742 def _loadalllazy(self):
744 def _loadalllazy(self):
743 selfdirs = self._dirs
745 selfdirs = self._dirs
744 for d, (path, node, readsubtree, docopy) in self._lazydirs.iteritems():
746 for d, (path, node, readsubtree, docopy) in self._lazydirs.iteritems():
745 if docopy:
747 if docopy:
746 selfdirs[d] = readsubtree(path, node).copy()
748 selfdirs[d] = readsubtree(path, node).copy()
747 else:
749 else:
748 selfdirs[d] = readsubtree(path, node)
750 selfdirs[d] = readsubtree(path, node)
749 self._lazydirs = {}
751 self._lazydirs = {}
750
752
751 def _loadlazy(self, d):
753 def _loadlazy(self, d):
752 v = self._lazydirs.get(d)
754 v = self._lazydirs.get(d)
753 if v:
755 if v:
754 path, node, readsubtree, docopy = v
756 path, node, readsubtree, docopy = v
755 if docopy:
757 if docopy:
756 self._dirs[d] = readsubtree(path, node).copy()
758 self._dirs[d] = readsubtree(path, node).copy()
757 else:
759 else:
758 self._dirs[d] = readsubtree(path, node)
760 self._dirs[d] = readsubtree(path, node)
759 del self._lazydirs[d]
761 del self._lazydirs[d]
760
762
761 def _loadchildrensetlazy(self, visit):
763 def _loadchildrensetlazy(self, visit):
762 if not visit:
764 if not visit:
763 return None
765 return None
764 if visit == 'all' or visit == 'this':
766 if visit == 'all' or visit == 'this':
765 self._loadalllazy()
767 self._loadalllazy()
766 return None
768 return None
767
769
768 loadlazy = self._loadlazy
770 loadlazy = self._loadlazy
769 for k in visit:
771 for k in visit:
770 loadlazy(k + '/')
772 loadlazy(k + '/')
771 return visit
773 return visit
772
774
773 def _loaddifflazy(self, t1, t2):
775 def _loaddifflazy(self, t1, t2):
774 """load items in t1 and t2 if they're needed for diffing.
776 """load items in t1 and t2 if they're needed for diffing.
775
777
776 The criteria currently is:
778 The criteria currently is:
777 - if it's not present in _lazydirs in either t1 or t2, load it in the
779 - if it's not present in _lazydirs in either t1 or t2, load it in the
778 other (it may already be loaded or it may not exist, doesn't matter)
780 other (it may already be loaded or it may not exist, doesn't matter)
779 - if it's present in _lazydirs in both, compare the nodeid; if it
781 - if it's present in _lazydirs in both, compare the nodeid; if it
780 differs, load it in both
782 differs, load it in both
781 """
783 """
782 toloadlazy = []
784 toloadlazy = []
783 for d, v1 in t1._lazydirs.iteritems():
785 for d, v1 in t1._lazydirs.iteritems():
784 v2 = t2._lazydirs.get(d)
786 v2 = t2._lazydirs.get(d)
785 if not v2 or v2[1] != v1[1]:
787 if not v2 or v2[1] != v1[1]:
786 toloadlazy.append(d)
788 toloadlazy.append(d)
787 for d, v1 in t2._lazydirs.iteritems():
789 for d, v1 in t2._lazydirs.iteritems():
788 if d not in t1._lazydirs:
790 if d not in t1._lazydirs:
789 toloadlazy.append(d)
791 toloadlazy.append(d)
790
792
791 for d in toloadlazy:
793 for d in toloadlazy:
792 t1._loadlazy(d)
794 t1._loadlazy(d)
793 t2._loadlazy(d)
795 t2._loadlazy(d)
794
796
795 def __len__(self):
797 def __len__(self):
796 self._load()
798 self._load()
797 size = len(self._files)
799 size = len(self._files)
798 self._loadalllazy()
800 self._loadalllazy()
799 for m in self._dirs.values():
801 for m in self._dirs.values():
800 size += m.__len__()
802 size += m.__len__()
801 return size
803 return size
802
804
803 def __nonzero__(self):
805 def __nonzero__(self):
804 # Faster than "__len() != 0" since it avoids loading sub-manifests
806 # Faster than "__len() != 0" since it avoids loading sub-manifests
805 return not self._isempty()
807 return not self._isempty()
806
808
807 __bool__ = __nonzero__
809 __bool__ = __nonzero__
808
810
809 def _isempty(self):
811 def _isempty(self):
810 self._load() # for consistency; already loaded by all callers
812 self._load() # for consistency; already loaded by all callers
811 # See if we can skip loading everything.
813 # See if we can skip loading everything.
812 if self._files or (self._dirs and
814 if self._files or (self._dirs and
813 any(not m._isempty() for m in self._dirs.values())):
815 any(not m._isempty() for m in self._dirs.values())):
814 return False
816 return False
815 self._loadalllazy()
817 self._loadalllazy()
816 return (not self._dirs or
818 return (not self._dirs or
817 all(m._isempty() for m in self._dirs.values()))
819 all(m._isempty() for m in self._dirs.values()))
818
820
819 def __repr__(self):
821 def __repr__(self):
820 return ('<treemanifest dir=%s, node=%s, loaded=%s, dirty=%s at 0x%x>' %
822 return ('<treemanifest dir=%s, node=%s, loaded=%s, dirty=%s at 0x%x>' %
821 (self._dir, hex(self._node),
823 (self._dir, hex(self._node),
822 bool(self._loadfunc is _noop),
824 bool(self._loadfunc is _noop),
823 self._dirty, id(self)))
825 self._dirty, id(self)))
824
826
825 def dir(self):
827 def dir(self):
826 '''The directory that this tree manifest represents, including a
828 '''The directory that this tree manifest represents, including a
827 trailing '/'. Empty string for the repo root directory.'''
829 trailing '/'. Empty string for the repo root directory.'''
828 return self._dir
830 return self._dir
829
831
830 def node(self):
832 def node(self):
831 '''This node of this instance. nullid for unsaved instances. Should
833 '''This node of this instance. nullid for unsaved instances. Should
832 be updated when the instance is read or written from a revlog.
834 be updated when the instance is read or written from a revlog.
833 '''
835 '''
834 assert not self._dirty
836 assert not self._dirty
835 return self._node
837 return self._node
836
838
837 def setnode(self, node):
839 def setnode(self, node):
838 self._node = node
840 self._node = node
839 self._dirty = False
841 self._dirty = False
840
842
841 def iterentries(self):
843 def iterentries(self):
842 self._load()
844 self._load()
843 self._loadalllazy()
845 self._loadalllazy()
844 for p, n in sorted(itertools.chain(self._dirs.items(),
846 for p, n in sorted(itertools.chain(self._dirs.items(),
845 self._files.items())):
847 self._files.items())):
846 if p in self._files:
848 if p in self._files:
847 yield self._subpath(p), n, self._flags.get(p, '')
849 yield self._subpath(p), n, self._flags.get(p, '')
848 else:
850 else:
849 for x in n.iterentries():
851 for x in n.iterentries():
850 yield x
852 yield x
851
853
852 def items(self):
854 def items(self):
853 self._load()
855 self._load()
854 self._loadalllazy()
856 self._loadalllazy()
855 for p, n in sorted(itertools.chain(self._dirs.items(),
857 for p, n in sorted(itertools.chain(self._dirs.items(),
856 self._files.items())):
858 self._files.items())):
857 if p in self._files:
859 if p in self._files:
858 yield self._subpath(p), n
860 yield self._subpath(p), n
859 else:
861 else:
860 for f, sn in n.iteritems():
862 for f, sn in n.iteritems():
861 yield f, sn
863 yield f, sn
862
864
863 iteritems = items
865 iteritems = items
864
866
865 def iterkeys(self):
867 def iterkeys(self):
866 self._load()
868 self._load()
867 self._loadalllazy()
869 self._loadalllazy()
868 for p in sorted(itertools.chain(self._dirs, self._files)):
870 for p in sorted(itertools.chain(self._dirs, self._files)):
869 if p in self._files:
871 if p in self._files:
870 yield self._subpath(p)
872 yield self._subpath(p)
871 else:
873 else:
872 for f in self._dirs[p]:
874 for f in self._dirs[p]:
873 yield f
875 yield f
874
876
875 def keys(self):
877 def keys(self):
876 return list(self.iterkeys())
878 return list(self.iterkeys())
877
879
878 def __iter__(self):
880 def __iter__(self):
879 return self.iterkeys()
881 return self.iterkeys()
880
882
881 def __contains__(self, f):
883 def __contains__(self, f):
882 if f is None:
884 if f is None:
883 return False
885 return False
884 self._load()
886 self._load()
885 dir, subpath = _splittopdir(f)
887 dir, subpath = _splittopdir(f)
886 if dir:
888 if dir:
887 self._loadlazy(dir)
889 self._loadlazy(dir)
888
890
889 if dir not in self._dirs:
891 if dir not in self._dirs:
890 return False
892 return False
891
893
892 return self._dirs[dir].__contains__(subpath)
894 return self._dirs[dir].__contains__(subpath)
893 else:
895 else:
894 return f in self._files
896 return f in self._files
895
897
896 def get(self, f, default=None):
898 def get(self, f, default=None):
897 self._load()
899 self._load()
898 dir, subpath = _splittopdir(f)
900 dir, subpath = _splittopdir(f)
899 if dir:
901 if dir:
900 self._loadlazy(dir)
902 self._loadlazy(dir)
901
903
902 if dir not in self._dirs:
904 if dir not in self._dirs:
903 return default
905 return default
904 return self._dirs[dir].get(subpath, default)
906 return self._dirs[dir].get(subpath, default)
905 else:
907 else:
906 return self._files.get(f, default)
908 return self._files.get(f, default)
907
909
908 def __getitem__(self, f):
910 def __getitem__(self, f):
909 self._load()
911 self._load()
910 dir, subpath = _splittopdir(f)
912 dir, subpath = _splittopdir(f)
911 if dir:
913 if dir:
912 self._loadlazy(dir)
914 self._loadlazy(dir)
913
915
914 return self._dirs[dir].__getitem__(subpath)
916 return self._dirs[dir].__getitem__(subpath)
915 else:
917 else:
916 return self._files[f]
918 return self._files[f]
917
919
918 def flags(self, f):
920 def flags(self, f):
919 self._load()
921 self._load()
920 dir, subpath = _splittopdir(f)
922 dir, subpath = _splittopdir(f)
921 if dir:
923 if dir:
922 self._loadlazy(dir)
924 self._loadlazy(dir)
923
925
924 if dir not in self._dirs:
926 if dir not in self._dirs:
925 return ''
927 return ''
926 return self._dirs[dir].flags(subpath)
928 return self._dirs[dir].flags(subpath)
927 else:
929 else:
928 if f in self._lazydirs or f in self._dirs:
930 if f in self._lazydirs or f in self._dirs:
929 return ''
931 return ''
930 return self._flags.get(f, '')
932 return self._flags.get(f, '')
931
933
932 def find(self, f):
934 def find(self, f):
933 self._load()
935 self._load()
934 dir, subpath = _splittopdir(f)
936 dir, subpath = _splittopdir(f)
935 if dir:
937 if dir:
936 self._loadlazy(dir)
938 self._loadlazy(dir)
937
939
938 return self._dirs[dir].find(subpath)
940 return self._dirs[dir].find(subpath)
939 else:
941 else:
940 return self._files[f], self._flags.get(f, '')
942 return self._files[f], self._flags.get(f, '')
941
943
942 def __delitem__(self, f):
944 def __delitem__(self, f):
943 self._load()
945 self._load()
944 dir, subpath = _splittopdir(f)
946 dir, subpath = _splittopdir(f)
945 if dir:
947 if dir:
946 self._loadlazy(dir)
948 self._loadlazy(dir)
947
949
948 self._dirs[dir].__delitem__(subpath)
950 self._dirs[dir].__delitem__(subpath)
949 # If the directory is now empty, remove it
951 # If the directory is now empty, remove it
950 if self._dirs[dir]._isempty():
952 if self._dirs[dir]._isempty():
951 del self._dirs[dir]
953 del self._dirs[dir]
952 else:
954 else:
953 del self._files[f]
955 del self._files[f]
954 if f in self._flags:
956 if f in self._flags:
955 del self._flags[f]
957 del self._flags[f]
956 self._dirty = True
958 self._dirty = True
957
959
958 def __setitem__(self, f, n):
960 def __setitem__(self, f, n):
959 assert n is not None
961 assert n is not None
960 self._load()
962 self._load()
961 dir, subpath = _splittopdir(f)
963 dir, subpath = _splittopdir(f)
962 if dir:
964 if dir:
963 self._loadlazy(dir)
965 self._loadlazy(dir)
964 if dir not in self._dirs:
966 if dir not in self._dirs:
965 self._dirs[dir] = treemanifest(self._subpath(dir))
967 self._dirs[dir] = treemanifest(self._subpath(dir))
966 self._dirs[dir].__setitem__(subpath, n)
968 self._dirs[dir].__setitem__(subpath, n)
967 else:
969 else:
968 self._files[f] = n[:21] # to match manifestdict's behavior
970 self._files[f] = n[:21] # to match manifestdict's behavior
969 self._dirty = True
971 self._dirty = True
970
972
971 def _load(self):
973 def _load(self):
972 if self._loadfunc is not _noop:
974 if self._loadfunc is not _noop:
973 lf, self._loadfunc = self._loadfunc, _noop
975 lf, self._loadfunc = self._loadfunc, _noop
974 lf(self)
976 lf(self)
975 elif self._copyfunc is not _noop:
977 elif self._copyfunc is not _noop:
976 cf, self._copyfunc = self._copyfunc, _noop
978 cf, self._copyfunc = self._copyfunc, _noop
977 cf(self)
979 cf(self)
978
980
979 def setflag(self, f, flags):
981 def setflag(self, f, flags):
980 """Set the flags (symlink, executable) for path f."""
982 """Set the flags (symlink, executable) for path f."""
981 self._load()
983 self._load()
982 dir, subpath = _splittopdir(f)
984 dir, subpath = _splittopdir(f)
983 if dir:
985 if dir:
984 self._loadlazy(dir)
986 self._loadlazy(dir)
985 if dir not in self._dirs:
987 if dir not in self._dirs:
986 self._dirs[dir] = treemanifest(self._subpath(dir))
988 self._dirs[dir] = treemanifest(self._subpath(dir))
987 self._dirs[dir].setflag(subpath, flags)
989 self._dirs[dir].setflag(subpath, flags)
988 else:
990 else:
989 self._flags[f] = flags
991 self._flags[f] = flags
990 self._dirty = True
992 self._dirty = True
991
993
992 def copy(self):
994 def copy(self):
993 copy = treemanifest(self._dir)
995 copy = treemanifest(self._dir)
994 copy._node = self._node
996 copy._node = self._node
995 copy._dirty = self._dirty
997 copy._dirty = self._dirty
996 if self._copyfunc is _noop:
998 if self._copyfunc is _noop:
997 def _copyfunc(s):
999 def _copyfunc(s):
998 self._load()
1000 self._load()
999 s._lazydirs = {d: (p, n, r, True) for
1001 s._lazydirs = {d: (p, n, r, True) for
1000 d, (p, n, r, c) in self._lazydirs.iteritems()}
1002 d, (p, n, r, c) in self._lazydirs.iteritems()}
1001 sdirs = s._dirs
1003 sdirs = s._dirs
1002 for d, v in self._dirs.iteritems():
1004 for d, v in self._dirs.iteritems():
1003 sdirs[d] = v.copy()
1005 sdirs[d] = v.copy()
1004 s._files = dict.copy(self._files)
1006 s._files = dict.copy(self._files)
1005 s._flags = dict.copy(self._flags)
1007 s._flags = dict.copy(self._flags)
1006 if self._loadfunc is _noop:
1008 if self._loadfunc is _noop:
1007 _copyfunc(copy)
1009 _copyfunc(copy)
1008 else:
1010 else:
1009 copy._copyfunc = _copyfunc
1011 copy._copyfunc = _copyfunc
1010 else:
1012 else:
1011 copy._copyfunc = self._copyfunc
1013 copy._copyfunc = self._copyfunc
1012 return copy
1014 return copy
1013
1015
1014 def filesnotin(self, m2, match=None):
1016 def filesnotin(self, m2, match=None):
1015 '''Set of files in this manifest that are not in the other'''
1017 '''Set of files in this manifest that are not in the other'''
1016 if match and not match.always():
1018 if match and not match.always():
1017 m1 = self.matches(match)
1019 m1 = self.matches(match)
1018 m2 = m2.matches(match)
1020 m2 = m2.matches(match)
1019 return m1.filesnotin(m2)
1021 return m1.filesnotin(m2)
1020
1022
1021 files = set()
1023 files = set()
1022 def _filesnotin(t1, t2):
1024 def _filesnotin(t1, t2):
1023 if t1._node == t2._node and not t1._dirty and not t2._dirty:
1025 if t1._node == t2._node and not t1._dirty and not t2._dirty:
1024 return
1026 return
1025 t1._load()
1027 t1._load()
1026 t2._load()
1028 t2._load()
1027 self._loaddifflazy(t1, t2)
1029 self._loaddifflazy(t1, t2)
1028 for d, m1 in t1._dirs.iteritems():
1030 for d, m1 in t1._dirs.iteritems():
1029 if d in t2._dirs:
1031 if d in t2._dirs:
1030 m2 = t2._dirs[d]
1032 m2 = t2._dirs[d]
1031 _filesnotin(m1, m2)
1033 _filesnotin(m1, m2)
1032 else:
1034 else:
1033 files.update(m1.iterkeys())
1035 files.update(m1.iterkeys())
1034
1036
1035 for fn in t1._files:
1037 for fn in t1._files:
1036 if fn not in t2._files:
1038 if fn not in t2._files:
1037 files.add(t1._subpath(fn))
1039 files.add(t1._subpath(fn))
1038
1040
1039 _filesnotin(self, m2)
1041 _filesnotin(self, m2)
1040 return files
1042 return files
1041
1043
1042 @propertycache
1044 @propertycache
1043 def _alldirs(self):
1045 def _alldirs(self):
1044 return util.dirs(self)
1046 return util.dirs(self)
1045
1047
1046 def dirs(self):
1048 def dirs(self):
1047 return self._alldirs
1049 return self._alldirs
1048
1050
1049 def hasdir(self, dir):
1051 def hasdir(self, dir):
1050 self._load()
1052 self._load()
1051 topdir, subdir = _splittopdir(dir)
1053 topdir, subdir = _splittopdir(dir)
1052 if topdir:
1054 if topdir:
1053 self._loadlazy(topdir)
1055 self._loadlazy(topdir)
1054 if topdir in self._dirs:
1056 if topdir in self._dirs:
1055 return self._dirs[topdir].hasdir(subdir)
1057 return self._dirs[topdir].hasdir(subdir)
1056 return False
1058 return False
1057 dirslash = dir + '/'
1059 dirslash = dir + '/'
1058 return dirslash in self._dirs or dirslash in self._lazydirs
1060 return dirslash in self._dirs or dirslash in self._lazydirs
1059
1061
1060 def walk(self, match):
1062 def walk(self, match):
1061 '''Generates matching file names.
1063 '''Generates matching file names.
1062
1064
1063 Equivalent to manifest.matches(match).iterkeys(), but without creating
1065 Equivalent to manifest.matches(match).iterkeys(), but without creating
1064 an entirely new manifest.
1066 an entirely new manifest.
1065
1067
1066 It also reports nonexistent files by marking them bad with match.bad().
1068 It also reports nonexistent files by marking them bad with match.bad().
1067 '''
1069 '''
1068 if match.always():
1070 if match.always():
1069 for f in iter(self):
1071 for f in iter(self):
1070 yield f
1072 yield f
1071 return
1073 return
1072
1074
1073 fset = set(match.files())
1075 fset = set(match.files())
1074
1076
1075 for fn in self._walk(match):
1077 for fn in self._walk(match):
1076 if fn in fset:
1078 if fn in fset:
1077 # specified pattern is the exact name
1079 # specified pattern is the exact name
1078 fset.remove(fn)
1080 fset.remove(fn)
1079 yield fn
1081 yield fn
1080
1082
1081 # for dirstate.walk, files=[''] means "walk the whole tree".
1083 # for dirstate.walk, files=[''] means "walk the whole tree".
1082 # follow that here, too
1084 # follow that here, too
1083 fset.discard('')
1085 fset.discard('')
1084
1086
1085 for fn in sorted(fset):
1087 for fn in sorted(fset):
1086 if not self.hasdir(fn):
1088 if not self.hasdir(fn):
1087 match.bad(fn, None)
1089 match.bad(fn, None)
1088
1090
1089 def _walk(self, match):
1091 def _walk(self, match):
1090 '''Recursively generates matching file names for walk().'''
1092 '''Recursively generates matching file names for walk().'''
1091 visit = match.visitchildrenset(self._dir[:-1])
1093 visit = match.visitchildrenset(self._dir[:-1])
1092 if not visit:
1094 if not visit:
1093 return
1095 return
1094
1096
1095 # yield this dir's files and walk its submanifests
1097 # yield this dir's files and walk its submanifests
1096 self._load()
1098 self._load()
1097 visit = self._loadchildrensetlazy(visit)
1099 visit = self._loadchildrensetlazy(visit)
1098 for p in sorted(list(self._dirs) + list(self._files)):
1100 for p in sorted(list(self._dirs) + list(self._files)):
1099 if p in self._files:
1101 if p in self._files:
1100 fullp = self._subpath(p)
1102 fullp = self._subpath(p)
1101 if match(fullp):
1103 if match(fullp):
1102 yield fullp
1104 yield fullp
1103 else:
1105 else:
1104 if not visit or p[:-1] in visit:
1106 if not visit or p[:-1] in visit:
1105 for f in self._dirs[p]._walk(match):
1107 for f in self._dirs[p]._walk(match):
1106 yield f
1108 yield f
1107
1109
1108 def matches(self, match):
1110 def matches(self, match):
1109 '''generate a new manifest filtered by the match argument'''
1111 '''generate a new manifest filtered by the match argument'''
1110 if match.always():
1112 if match.always():
1111 return self.copy()
1113 return self.copy()
1112
1114
1113 return self._matches(match)
1115 return self._matches(match)
1114
1116
1115 def _matches(self, match):
1117 def _matches(self, match):
1116 '''recursively generate a new manifest filtered by the match argument.
1118 '''recursively generate a new manifest filtered by the match argument.
1117 '''
1119 '''
1118
1120
1119 visit = match.visitchildrenset(self._dir[:-1])
1121 visit = match.visitchildrenset(self._dir[:-1])
1120 if visit == 'all':
1122 if visit == 'all':
1121 return self.copy()
1123 return self.copy()
1122 ret = treemanifest(self._dir)
1124 ret = treemanifest(self._dir)
1123 if not visit:
1125 if not visit:
1124 return ret
1126 return ret
1125
1127
1126 self._load()
1128 self._load()
1127 for fn in self._files:
1129 for fn in self._files:
1128 # While visitchildrenset *usually* lists only subdirs, this is
1130 # While visitchildrenset *usually* lists only subdirs, this is
1129 # actually up to the matcher and may have some files in the set().
1131 # actually up to the matcher and may have some files in the set().
1130 # If visit == 'this', we should obviously look at the files in this
1132 # If visit == 'this', we should obviously look at the files in this
1131 # directory; if visit is a set, and fn is in it, we should inspect
1133 # directory; if visit is a set, and fn is in it, we should inspect
1132 # fn (but no need to inspect things not in the set).
1134 # fn (but no need to inspect things not in the set).
1133 if visit != 'this' and fn not in visit:
1135 if visit != 'this' and fn not in visit:
1134 continue
1136 continue
1135 fullp = self._subpath(fn)
1137 fullp = self._subpath(fn)
1136 # visitchildrenset isn't perfect, we still need to call the regular
1138 # visitchildrenset isn't perfect, we still need to call the regular
1137 # matcher code to further filter results.
1139 # matcher code to further filter results.
1138 if not match(fullp):
1140 if not match(fullp):
1139 continue
1141 continue
1140 ret._files[fn] = self._files[fn]
1142 ret._files[fn] = self._files[fn]
1141 if fn in self._flags:
1143 if fn in self._flags:
1142 ret._flags[fn] = self._flags[fn]
1144 ret._flags[fn] = self._flags[fn]
1143
1145
1144 visit = self._loadchildrensetlazy(visit)
1146 visit = self._loadchildrensetlazy(visit)
1145 for dir, subm in self._dirs.iteritems():
1147 for dir, subm in self._dirs.iteritems():
1146 if visit and dir[:-1] not in visit:
1148 if visit and dir[:-1] not in visit:
1147 continue
1149 continue
1148 m = subm._matches(match)
1150 m = subm._matches(match)
1149 if not m._isempty():
1151 if not m._isempty():
1150 ret._dirs[dir] = m
1152 ret._dirs[dir] = m
1151
1153
1152 if not ret._isempty():
1154 if not ret._isempty():
1153 ret._dirty = True
1155 ret._dirty = True
1154 return ret
1156 return ret
1155
1157
1156 def diff(self, m2, match=None, clean=False):
1158 def diff(self, m2, match=None, clean=False):
1157 '''Finds changes between the current manifest and m2.
1159 '''Finds changes between the current manifest and m2.
1158
1160
1159 Args:
1161 Args:
1160 m2: the manifest to which this manifest should be compared.
1162 m2: the manifest to which this manifest should be compared.
1161 clean: if true, include files unchanged between these manifests
1163 clean: if true, include files unchanged between these manifests
1162 with a None value in the returned dictionary.
1164 with a None value in the returned dictionary.
1163
1165
1164 The result is returned as a dict with filename as key and
1166 The result is returned as a dict with filename as key and
1165 values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
1167 values of the form ((n1,fl1),(n2,fl2)), where n1/n2 is the
1166 nodeid in the current/other manifest and fl1/fl2 is the flag
1168 nodeid in the current/other manifest and fl1/fl2 is the flag
1167 in the current/other manifest. Where the file does not exist,
1169 in the current/other manifest. Where the file does not exist,
1168 the nodeid will be None and the flags will be the empty
1170 the nodeid will be None and the flags will be the empty
1169 string.
1171 string.
1170 '''
1172 '''
1171 if match and not match.always():
1173 if match and not match.always():
1172 m1 = self.matches(match)
1174 m1 = self.matches(match)
1173 m2 = m2.matches(match)
1175 m2 = m2.matches(match)
1174 return m1.diff(m2, clean=clean)
1176 return m1.diff(m2, clean=clean)
1175 result = {}
1177 result = {}
1176 emptytree = treemanifest()
1178 emptytree = treemanifest()
1177
1179
1178 def _iterativediff(t1, t2, stack):
1180 def _iterativediff(t1, t2, stack):
1179 """compares two tree manifests and append new tree-manifests which
1181 """compares two tree manifests and append new tree-manifests which
1180 needs to be compared to stack"""
1182 needs to be compared to stack"""
1181 if t1._node == t2._node and not t1._dirty and not t2._dirty:
1183 if t1._node == t2._node and not t1._dirty and not t2._dirty:
1182 return
1184 return
1183 t1._load()
1185 t1._load()
1184 t2._load()
1186 t2._load()
1185 self._loaddifflazy(t1, t2)
1187 self._loaddifflazy(t1, t2)
1186
1188
1187 for d, m1 in t1._dirs.iteritems():
1189 for d, m1 in t1._dirs.iteritems():
1188 m2 = t2._dirs.get(d, emptytree)
1190 m2 = t2._dirs.get(d, emptytree)
1189 stack.append((m1, m2))
1191 stack.append((m1, m2))
1190
1192
1191 for d, m2 in t2._dirs.iteritems():
1193 for d, m2 in t2._dirs.iteritems():
1192 if d not in t1._dirs:
1194 if d not in t1._dirs:
1193 stack.append((emptytree, m2))
1195 stack.append((emptytree, m2))
1194
1196
1195 for fn, n1 in t1._files.iteritems():
1197 for fn, n1 in t1._files.iteritems():
1196 fl1 = t1._flags.get(fn, '')
1198 fl1 = t1._flags.get(fn, '')
1197 n2 = t2._files.get(fn, None)
1199 n2 = t2._files.get(fn, None)
1198 fl2 = t2._flags.get(fn, '')
1200 fl2 = t2._flags.get(fn, '')
1199 if n1 != n2 or fl1 != fl2:
1201 if n1 != n2 or fl1 != fl2:
1200 result[t1._subpath(fn)] = ((n1, fl1), (n2, fl2))
1202 result[t1._subpath(fn)] = ((n1, fl1), (n2, fl2))
1201 elif clean:
1203 elif clean:
1202 result[t1._subpath(fn)] = None
1204 result[t1._subpath(fn)] = None
1203
1205
1204 for fn, n2 in t2._files.iteritems():
1206 for fn, n2 in t2._files.iteritems():
1205 if fn not in t1._files:
1207 if fn not in t1._files:
1206 fl2 = t2._flags.get(fn, '')
1208 fl2 = t2._flags.get(fn, '')
1207 result[t2._subpath(fn)] = ((None, ''), (n2, fl2))
1209 result[t2._subpath(fn)] = ((None, ''), (n2, fl2))
1208
1210
1209 stackls = []
1211 stackls = []
1210 _iterativediff(self, m2, stackls)
1212 _iterativediff(self, m2, stackls)
1211 while stackls:
1213 while stackls:
1212 t1, t2 = stackls.pop()
1214 t1, t2 = stackls.pop()
1213 # stackls is populated in the function call
1215 # stackls is populated in the function call
1214 _iterativediff(t1, t2, stackls)
1216 _iterativediff(t1, t2, stackls)
1215 return result
1217 return result
1216
1218
1217 def unmodifiedsince(self, m2):
1219 def unmodifiedsince(self, m2):
1218 return not self._dirty and not m2._dirty and self._node == m2._node
1220 return not self._dirty and not m2._dirty and self._node == m2._node
1219
1221
1220 def parse(self, text, readsubtree):
1222 def parse(self, text, readsubtree):
1221 selflazy = self._lazydirs
1223 selflazy = self._lazydirs
1222 subpath = self._subpath
1224 subpath = self._subpath
1223 for f, n, fl in _parse(text):
1225 for f, n, fl in _parse(text):
1224 if fl == 't':
1226 if fl == 't':
1225 f = f + '/'
1227 f = f + '/'
1226 # False below means "doesn't need to be copied" and can use the
1228 # False below means "doesn't need to be copied" and can use the
1227 # cached value from readsubtree directly.
1229 # cached value from readsubtree directly.
1228 selflazy[f] = (subpath(f), n, readsubtree, False)
1230 selflazy[f] = (subpath(f), n, readsubtree, False)
1229 elif '/' in f:
1231 elif '/' in f:
1230 # This is a flat manifest, so use __setitem__ and setflag rather
1232 # This is a flat manifest, so use __setitem__ and setflag rather
1231 # than assigning directly to _files and _flags, so we can
1233 # than assigning directly to _files and _flags, so we can
1232 # assign a path in a subdirectory, and to mark dirty (compared
1234 # assign a path in a subdirectory, and to mark dirty (compared
1233 # to nullid).
1235 # to nullid).
1234 self[f] = n
1236 self[f] = n
1235 if fl:
1237 if fl:
1236 self.setflag(f, fl)
1238 self.setflag(f, fl)
1237 else:
1239 else:
1238 # Assigning to _files and _flags avoids marking as dirty,
1240 # Assigning to _files and _flags avoids marking as dirty,
1239 # and should be a little faster.
1241 # and should be a little faster.
1240 self._files[f] = n
1242 self._files[f] = n
1241 if fl:
1243 if fl:
1242 self._flags[f] = fl
1244 self._flags[f] = fl
1243
1245
1244 def text(self):
1246 def text(self):
1245 """Get the full data of this manifest as a bytestring."""
1247 """Get the full data of this manifest as a bytestring."""
1246 self._load()
1248 self._load()
1247 return _text(self.iterentries())
1249 return _text(self.iterentries())
1248
1250
1249 def dirtext(self):
1251 def dirtext(self):
1250 """Get the full data of this directory as a bytestring. Make sure that
1252 """Get the full data of this directory as a bytestring. Make sure that
1251 any submanifests have been written first, so their nodeids are correct.
1253 any submanifests have been written first, so their nodeids are correct.
1252 """
1254 """
1253 self._load()
1255 self._load()
1254 flags = self.flags
1256 flags = self.flags
1255 lazydirs = [(d[:-1], v[1], 't') for d, v in self._lazydirs.iteritems()]
1257 lazydirs = [(d[:-1], v[1], 't') for d, v in self._lazydirs.iteritems()]
1256 dirs = [(d[:-1], self._dirs[d]._node, 't') for d in self._dirs]
1258 dirs = [(d[:-1], self._dirs[d]._node, 't') for d in self._dirs]
1257 files = [(f, self._files[f], flags(f)) for f in self._files]
1259 files = [(f, self._files[f], flags(f)) for f in self._files]
1258 return _text(sorted(dirs + files + lazydirs))
1260 return _text(sorted(dirs + files + lazydirs))
1259
1261
1260 def read(self, gettext, readsubtree):
1262 def read(self, gettext, readsubtree):
1261 def _load_for_read(s):
1263 def _load_for_read(s):
1262 s.parse(gettext(), readsubtree)
1264 s.parse(gettext(), readsubtree)
1263 s._dirty = False
1265 s._dirty = False
1264 self._loadfunc = _load_for_read
1266 self._loadfunc = _load_for_read
1265
1267
1266 def writesubtrees(self, m1, m2, writesubtree, match):
1268 def writesubtrees(self, m1, m2, writesubtree, match):
1267 self._load() # for consistency; should never have any effect here
1269 self._load() # for consistency; should never have any effect here
1268 m1._load()
1270 m1._load()
1269 m2._load()
1271 m2._load()
1270 emptytree = treemanifest()
1272 emptytree = treemanifest()
1271 def getnode(m, d):
1273 def getnode(m, d):
1272 ld = m._lazydirs.get(d)
1274 ld = m._lazydirs.get(d)
1273 if ld:
1275 if ld:
1274 return ld[1]
1276 return ld[1]
1275 return m._dirs.get(d, emptytree)._node
1277 return m._dirs.get(d, emptytree)._node
1276
1278
1277 # let's skip investigating things that `match` says we do not need.
1279 # let's skip investigating things that `match` says we do not need.
1278 visit = match.visitchildrenset(self._dir[:-1])
1280 visit = match.visitchildrenset(self._dir[:-1])
1279 visit = self._loadchildrensetlazy(visit)
1281 visit = self._loadchildrensetlazy(visit)
1280 if visit == 'this' or visit == 'all':
1282 if visit == 'this' or visit == 'all':
1281 visit = None
1283 visit = None
1282 for d, subm in self._dirs.iteritems():
1284 for d, subm in self._dirs.iteritems():
1283 if visit and d[:-1] not in visit:
1285 if visit and d[:-1] not in visit:
1284 continue
1286 continue
1285 subp1 = getnode(m1, d)
1287 subp1 = getnode(m1, d)
1286 subp2 = getnode(m2, d)
1288 subp2 = getnode(m2, d)
1287 if subp1 == nullid:
1289 if subp1 == nullid:
1288 subp1, subp2 = subp2, subp1
1290 subp1, subp2 = subp2, subp1
1289 writesubtree(subm, subp1, subp2, match)
1291 writesubtree(subm, subp1, subp2, match)
1290
1292
1291 def walksubtrees(self, matcher=None):
1293 def walksubtrees(self, matcher=None):
1292 """Returns an iterator of the subtrees of this manifest, including this
1294 """Returns an iterator of the subtrees of this manifest, including this
1293 manifest itself.
1295 manifest itself.
1294
1296
1295 If `matcher` is provided, it only returns subtrees that match.
1297 If `matcher` is provided, it only returns subtrees that match.
1296 """
1298 """
1297 if matcher and not matcher.visitdir(self._dir[:-1]):
1299 if matcher and not matcher.visitdir(self._dir[:-1]):
1298 return
1300 return
1299 if not matcher or matcher(self._dir[:-1]):
1301 if not matcher or matcher(self._dir[:-1]):
1300 yield self
1302 yield self
1301
1303
1302 self._load()
1304 self._load()
1303 # OPT: use visitchildrenset to avoid loading everything.
1305 # OPT: use visitchildrenset to avoid loading everything.
1304 self._loadalllazy()
1306 self._loadalllazy()
1305 for d, subm in self._dirs.iteritems():
1307 for d, subm in self._dirs.iteritems():
1306 for subtree in subm.walksubtrees(matcher=matcher):
1308 for subtree in subm.walksubtrees(matcher=matcher):
1307 yield subtree
1309 yield subtree
1308
1310
1309 class manifestfulltextcache(util.lrucachedict):
1311 class manifestfulltextcache(util.lrucachedict):
1310 """File-backed LRU cache for the manifest cache
1312 """File-backed LRU cache for the manifest cache
1311
1313
1312 File consists of entries, up to EOF:
1314 File consists of entries, up to EOF:
1313
1315
1314 - 20 bytes node, 4 bytes length, <length> manifest data
1316 - 20 bytes node, 4 bytes length, <length> manifest data
1315
1317
1316 These are written in reverse cache order (oldest to newest).
1318 These are written in reverse cache order (oldest to newest).
1317
1319
1318 """
1320 """
1319
1321
1320 _file = 'manifestfulltextcache'
1322 _file = 'manifestfulltextcache'
1321
1323
1322 def __init__(self, max):
1324 def __init__(self, max):
1323 super(manifestfulltextcache, self).__init__(max)
1325 super(manifestfulltextcache, self).__init__(max)
1324 self._dirty = False
1326 self._dirty = False
1325 self._read = False
1327 self._read = False
1326 self._opener = None
1328 self._opener = None
1327
1329
1328 def read(self):
1330 def read(self):
1329 if self._read or self._opener is None:
1331 if self._read or self._opener is None:
1330 return
1332 return
1331
1333
1332 try:
1334 try:
1333 with self._opener(self._file) as fp:
1335 with self._opener(self._file) as fp:
1334 set = super(manifestfulltextcache, self).__setitem__
1336 set = super(manifestfulltextcache, self).__setitem__
1335 # ignore trailing data, this is a cache, corruption is skipped
1337 # ignore trailing data, this is a cache, corruption is skipped
1336 while True:
1338 while True:
1337 node = fp.read(20)
1339 node = fp.read(20)
1338 if len(node) < 20:
1340 if len(node) < 20:
1339 break
1341 break
1340 try:
1342 try:
1341 size = struct.unpack('>L', fp.read(4))[0]
1343 size = struct.unpack('>L', fp.read(4))[0]
1342 except struct.error:
1344 except struct.error:
1343 break
1345 break
1344 value = bytearray(fp.read(size))
1346 value = bytearray(fp.read(size))
1345 if len(value) != size:
1347 if len(value) != size:
1346 break
1348 break
1347 set(node, value)
1349 set(node, value)
1348 except IOError:
1350 except IOError:
1349 # the file is allowed to be missing
1351 # the file is allowed to be missing
1350 pass
1352 pass
1351
1353
1352 self._read = True
1354 self._read = True
1353 self._dirty = False
1355 self._dirty = False
1354
1356
1355 def write(self):
1357 def write(self):
1356 if not self._dirty or self._opener is None:
1358 if not self._dirty or self._opener is None:
1357 return
1359 return
1358 # rotate backwards to the first used node
1360 # rotate backwards to the first used node
1359 with self._opener(self._file, 'w', atomictemp=True, checkambig=True
1361 with self._opener(self._file, 'w', atomictemp=True, checkambig=True
1360 ) as fp:
1362 ) as fp:
1361 node = self._head.prev
1363 node = self._head.prev
1362 while True:
1364 while True:
1363 if node.key in self._cache:
1365 if node.key in self._cache:
1364 fp.write(node.key)
1366 fp.write(node.key)
1365 fp.write(struct.pack('>L', len(node.value)))
1367 fp.write(struct.pack('>L', len(node.value)))
1366 fp.write(node.value)
1368 fp.write(node.value)
1367 if node is self._head:
1369 if node is self._head:
1368 break
1370 break
1369 node = node.prev
1371 node = node.prev
1370
1372
1371 def __len__(self):
1373 def __len__(self):
1372 if not self._read:
1374 if not self._read:
1373 self.read()
1375 self.read()
1374 return super(manifestfulltextcache, self).__len__()
1376 return super(manifestfulltextcache, self).__len__()
1375
1377
1376 def __contains__(self, k):
1378 def __contains__(self, k):
1377 if not self._read:
1379 if not self._read:
1378 self.read()
1380 self.read()
1379 return super(manifestfulltextcache, self).__contains__(k)
1381 return super(manifestfulltextcache, self).__contains__(k)
1380
1382
1381 def __iter__(self):
1383 def __iter__(self):
1382 if not self._read:
1384 if not self._read:
1383 self.read()
1385 self.read()
1384 return super(manifestfulltextcache, self).__iter__()
1386 return super(manifestfulltextcache, self).__iter__()
1385
1387
1386 def __getitem__(self, k):
1388 def __getitem__(self, k):
1387 if not self._read:
1389 if not self._read:
1388 self.read()
1390 self.read()
1389 # the cache lru order can change on read
1391 # the cache lru order can change on read
1390 setdirty = self._cache.get(k) is not self._head
1392 setdirty = self._cache.get(k) is not self._head
1391 value = super(manifestfulltextcache, self).__getitem__(k)
1393 value = super(manifestfulltextcache, self).__getitem__(k)
1392 if setdirty:
1394 if setdirty:
1393 self._dirty = True
1395 self._dirty = True
1394 return value
1396 return value
1395
1397
1396 def __setitem__(self, k, v):
1398 def __setitem__(self, k, v):
1397 if not self._read:
1399 if not self._read:
1398 self.read()
1400 self.read()
1399 super(manifestfulltextcache, self).__setitem__(k, v)
1401 super(manifestfulltextcache, self).__setitem__(k, v)
1400 self._dirty = True
1402 self._dirty = True
1401
1403
1402 def __delitem__(self, k):
1404 def __delitem__(self, k):
1403 if not self._read:
1405 if not self._read:
1404 self.read()
1406 self.read()
1405 super(manifestfulltextcache, self).__delitem__(k)
1407 super(manifestfulltextcache, self).__delitem__(k)
1406 self._dirty = True
1408 self._dirty = True
1407
1409
1408 def get(self, k, default=None):
1410 def get(self, k, default=None):
1409 if not self._read:
1411 if not self._read:
1410 self.read()
1412 self.read()
1411 return super(manifestfulltextcache, self).get(k, default=default)
1413 return super(manifestfulltextcache, self).get(k, default=default)
1412
1414
1413 def clear(self, clear_persisted_data=False):
1415 def clear(self, clear_persisted_data=False):
1414 super(manifestfulltextcache, self).clear()
1416 super(manifestfulltextcache, self).clear()
1415 if clear_persisted_data:
1417 if clear_persisted_data:
1416 self._dirty = True
1418 self._dirty = True
1417 self.write()
1419 self.write()
1418 self._read = False
1420 self._read = False
1419
1421
1420 # and upper bound of what we expect from compression
1422 # and upper bound of what we expect from compression
1421 # (real live value seems to be "3")
1423 # (real live value seems to be "3")
1422 MAXCOMPRESSION = 3
1424 MAXCOMPRESSION = 3
1423
1425
1424 @interfaceutil.implementer(repository.imanifeststorage)
1426 @interfaceutil.implementer(repository.imanifeststorage)
1425 class manifestrevlog(object):
1427 class manifestrevlog(object):
1426 '''A revlog that stores manifest texts. This is responsible for caching the
1428 '''A revlog that stores manifest texts. This is responsible for caching the
1427 full-text manifest contents.
1429 full-text manifest contents.
1428 '''
1430 '''
1429 def __init__(self, opener, tree='', dirlogcache=None, indexfile=None,
1431 def __init__(self, opener, tree='', dirlogcache=None, indexfile=None,
1430 treemanifest=False):
1432 treemanifest=False):
1431 """Constructs a new manifest revlog
1433 """Constructs a new manifest revlog
1432
1434
1433 `indexfile` - used by extensions to have two manifests at once, like
1435 `indexfile` - used by extensions to have two manifests at once, like
1434 when transitioning between flatmanifeset and treemanifests.
1436 when transitioning between flatmanifeset and treemanifests.
1435
1437
1436 `treemanifest` - used to indicate this is a tree manifest revlog. Opener
1438 `treemanifest` - used to indicate this is a tree manifest revlog. Opener
1437 options can also be used to make this a tree manifest revlog. The opener
1439 options can also be used to make this a tree manifest revlog. The opener
1438 option takes precedence, so if it is set to True, we ignore whatever
1440 option takes precedence, so if it is set to True, we ignore whatever
1439 value is passed in to the constructor.
1441 value is passed in to the constructor.
1440 """
1442 """
1441 # During normal operations, we expect to deal with not more than four
1443 # During normal operations, we expect to deal with not more than four
1442 # revs at a time (such as during commit --amend). When rebasing large
1444 # revs at a time (such as during commit --amend). When rebasing large
1443 # stacks of commits, the number can go up, hence the config knob below.
1445 # stacks of commits, the number can go up, hence the config knob below.
1444 cachesize = 4
1446 cachesize = 4
1445 optiontreemanifest = False
1447 optiontreemanifest = False
1446 opts = getattr(opener, 'options', None)
1448 opts = getattr(opener, 'options', None)
1447 if opts is not None:
1449 if opts is not None:
1448 cachesize = opts.get('manifestcachesize', cachesize)
1450 cachesize = opts.get('manifestcachesize', cachesize)
1449 optiontreemanifest = opts.get('treemanifest', False)
1451 optiontreemanifest = opts.get('treemanifest', False)
1450
1452
1451 self._treeondisk = optiontreemanifest or treemanifest
1453 self._treeondisk = optiontreemanifest or treemanifest
1452
1454
1453 self._fulltextcache = manifestfulltextcache(cachesize)
1455 self._fulltextcache = manifestfulltextcache(cachesize)
1454
1456
1455 if tree:
1457 if tree:
1456 assert self._treeondisk, 'opts is %r' % opts
1458 assert self._treeondisk, 'opts is %r' % opts
1457
1459
1458 if indexfile is None:
1460 if indexfile is None:
1459 indexfile = '00manifest.i'
1461 indexfile = '00manifest.i'
1460 if tree:
1462 if tree:
1461 indexfile = "meta/" + tree + indexfile
1463 indexfile = "meta/" + tree + indexfile
1462
1464
1463 self.tree = tree
1465 self.tree = tree
1464
1466
1465 # The dirlogcache is kept on the root manifest log
1467 # The dirlogcache is kept on the root manifest log
1466 if tree:
1468 if tree:
1467 self._dirlogcache = dirlogcache
1469 self._dirlogcache = dirlogcache
1468 else:
1470 else:
1469 self._dirlogcache = {'': self}
1471 self._dirlogcache = {'': self}
1470
1472
1471 self._revlog = revlog.revlog(opener, indexfile,
1473 self._revlog = revlog.revlog(opener, indexfile,
1472 # only root indexfile is cached
1474 # only root indexfile is cached
1473 checkambig=not bool(tree),
1475 checkambig=not bool(tree),
1474 mmaplargeindex=True,
1476 mmaplargeindex=True,
1475 upperboundcomp=MAXCOMPRESSION)
1477 upperboundcomp=MAXCOMPRESSION)
1476
1478
1477 self.index = self._revlog.index
1479 self.index = self._revlog.index
1478 self.version = self._revlog.version
1480 self.version = self._revlog.version
1479 self._generaldelta = self._revlog._generaldelta
1481 self._generaldelta = self._revlog._generaldelta
1480
1482
1481 def _setupmanifestcachehooks(self, repo):
1483 def _setupmanifestcachehooks(self, repo):
1482 """Persist the manifestfulltextcache on lock release"""
1484 """Persist the manifestfulltextcache on lock release"""
1483 if not util.safehasattr(repo, '_wlockref'):
1485 if not util.safehasattr(repo, '_wlockref'):
1484 return
1486 return
1485
1487
1486 self._fulltextcache._opener = repo.wcachevfs
1488 self._fulltextcache._opener = repo.wcachevfs
1487 if repo._currentlock(repo._wlockref) is None:
1489 if repo._currentlock(repo._wlockref) is None:
1488 return
1490 return
1489
1491
1490 reporef = weakref.ref(repo)
1492 reporef = weakref.ref(repo)
1491 manifestrevlogref = weakref.ref(self)
1493 manifestrevlogref = weakref.ref(self)
1492
1494
1493 def persistmanifestcache():
1495 def persistmanifestcache():
1494 repo = reporef()
1496 repo = reporef()
1495 self = manifestrevlogref()
1497 self = manifestrevlogref()
1496 if repo is None or self is None:
1498 if repo is None or self is None:
1497 return
1499 return
1498 if repo.manifestlog.getstorage(b'') is not self:
1500 if repo.manifestlog.getstorage(b'') is not self:
1499 # there's a different manifest in play now, abort
1501 # there's a different manifest in play now, abort
1500 return
1502 return
1501 self._fulltextcache.write()
1503 self._fulltextcache.write()
1502
1504
1503 repo._afterlock(persistmanifestcache)
1505 repo._afterlock(persistmanifestcache)
1504
1506
1505 @property
1507 @property
1506 def fulltextcache(self):
1508 def fulltextcache(self):
1507 return self._fulltextcache
1509 return self._fulltextcache
1508
1510
1509 def clearcaches(self, clear_persisted_data=False):
1511 def clearcaches(self, clear_persisted_data=False):
1510 self._revlog.clearcaches()
1512 self._revlog.clearcaches()
1511 self._fulltextcache.clear(clear_persisted_data=clear_persisted_data)
1513 self._fulltextcache.clear(clear_persisted_data=clear_persisted_data)
1512 self._dirlogcache = {self.tree: self}
1514 self._dirlogcache = {self.tree: self}
1513
1515
1514 def dirlog(self, d):
1516 def dirlog(self, d):
1515 if d:
1517 if d:
1516 assert self._treeondisk
1518 assert self._treeondisk
1517 if d not in self._dirlogcache:
1519 if d not in self._dirlogcache:
1518 mfrevlog = manifestrevlog(self.opener, d,
1520 mfrevlog = manifestrevlog(self.opener, d,
1519 self._dirlogcache,
1521 self._dirlogcache,
1520 treemanifest=self._treeondisk)
1522 treemanifest=self._treeondisk)
1521 self._dirlogcache[d] = mfrevlog
1523 self._dirlogcache[d] = mfrevlog
1522 return self._dirlogcache[d]
1524 return self._dirlogcache[d]
1523
1525
1524 def add(self, m, transaction, link, p1, p2, added, removed, readtree=None,
1526 def add(self, m, transaction, link, p1, p2, added, removed, readtree=None,
1525 match=None):
1527 match=None):
1526 if p1 in self.fulltextcache and util.safehasattr(m, 'fastdelta'):
1528 if p1 in self.fulltextcache and util.safehasattr(m, 'fastdelta'):
1527 # If our first parent is in the manifest cache, we can
1529 # If our first parent is in the manifest cache, we can
1528 # compute a delta here using properties we know about the
1530 # compute a delta here using properties we know about the
1529 # manifest up-front, which may save time later for the
1531 # manifest up-front, which may save time later for the
1530 # revlog layer.
1532 # revlog layer.
1531
1533
1532 _checkforbidden(added)
1534 _checkforbidden(added)
1533 # combine the changed lists into one sorted iterator
1535 # combine the changed lists into one sorted iterator
1534 work = heapq.merge([(x, False) for x in sorted(added)],
1536 work = heapq.merge([(x, False) for x in sorted(added)],
1535 [(x, True) for x in sorted(removed)])
1537 [(x, True) for x in sorted(removed)])
1536
1538
1537 arraytext, deltatext = m.fastdelta(self.fulltextcache[p1], work)
1539 arraytext, deltatext = m.fastdelta(self.fulltextcache[p1], work)
1538 cachedelta = self._revlog.rev(p1), deltatext
1540 cachedelta = self._revlog.rev(p1), deltatext
1539 text = util.buffer(arraytext)
1541 text = util.buffer(arraytext)
1540 n = self._revlog.addrevision(text, transaction, link, p1, p2,
1542 n = self._revlog.addrevision(text, transaction, link, p1, p2,
1541 cachedelta)
1543 cachedelta)
1542 else:
1544 else:
1543 # The first parent manifest isn't already loaded, so we'll
1545 # The first parent manifest isn't already loaded, so we'll
1544 # just encode a fulltext of the manifest and pass that
1546 # just encode a fulltext of the manifest and pass that
1545 # through to the revlog layer, and let it handle the delta
1547 # through to the revlog layer, and let it handle the delta
1546 # process.
1548 # process.
1547 if self._treeondisk:
1549 if self._treeondisk:
1548 assert readtree, "readtree must be set for treemanifest writes"
1550 assert readtree, "readtree must be set for treemanifest writes"
1549 assert match, "match must be specified for treemanifest writes"
1551 assert match, "match must be specified for treemanifest writes"
1550 m1 = readtree(self.tree, p1)
1552 m1 = readtree(self.tree, p1)
1551 m2 = readtree(self.tree, p2)
1553 m2 = readtree(self.tree, p2)
1552 n = self._addtree(m, transaction, link, m1, m2, readtree,
1554 n = self._addtree(m, transaction, link, m1, m2, readtree,
1553 match=match)
1555 match=match)
1554 arraytext = None
1556 arraytext = None
1555 else:
1557 else:
1556 text = m.text()
1558 text = m.text()
1557 n = self._revlog.addrevision(text, transaction, link, p1, p2)
1559 n = self._revlog.addrevision(text, transaction, link, p1, p2)
1558 arraytext = bytearray(text)
1560 arraytext = bytearray(text)
1559
1561
1560 if arraytext is not None:
1562 if arraytext is not None:
1561 self.fulltextcache[n] = arraytext
1563 self.fulltextcache[n] = arraytext
1562
1564
1563 return n
1565 return n
1564
1566
1565 def _addtree(self, m, transaction, link, m1, m2, readtree, match):
1567 def _addtree(self, m, transaction, link, m1, m2, readtree, match):
1566 # If the manifest is unchanged compared to one parent,
1568 # If the manifest is unchanged compared to one parent,
1567 # don't write a new revision
1569 # don't write a new revision
1568 if self.tree != '' and (m.unmodifiedsince(m1) or m.unmodifiedsince(
1570 if self.tree != '' and (m.unmodifiedsince(m1) or m.unmodifiedsince(
1569 m2)):
1571 m2)):
1570 return m.node()
1572 return m.node()
1571 def writesubtree(subm, subp1, subp2, match):
1573 def writesubtree(subm, subp1, subp2, match):
1572 sublog = self.dirlog(subm.dir())
1574 sublog = self.dirlog(subm.dir())
1573 sublog.add(subm, transaction, link, subp1, subp2, None, None,
1575 sublog.add(subm, transaction, link, subp1, subp2, None, None,
1574 readtree=readtree, match=match)
1576 readtree=readtree, match=match)
1575 m.writesubtrees(m1, m2, writesubtree, match)
1577 m.writesubtrees(m1, m2, writesubtree, match)
1576 text = m.dirtext()
1578 text = m.dirtext()
1577 n = None
1579 n = None
1578 if self.tree != '':
1580 if self.tree != '':
1579 # Double-check whether contents are unchanged to one parent
1581 # Double-check whether contents are unchanged to one parent
1580 if text == m1.dirtext():
1582 if text == m1.dirtext():
1581 n = m1.node()
1583 n = m1.node()
1582 elif text == m2.dirtext():
1584 elif text == m2.dirtext():
1583 n = m2.node()
1585 n = m2.node()
1584
1586
1585 if not n:
1587 if not n:
1586 n = self._revlog.addrevision(text, transaction, link, m1.node(),
1588 n = self._revlog.addrevision(text, transaction, link, m1.node(),
1587 m2.node())
1589 m2.node())
1588
1590
1589 # Save nodeid so parent manifest can calculate its nodeid
1591 # Save nodeid so parent manifest can calculate its nodeid
1590 m.setnode(n)
1592 m.setnode(n)
1591 return n
1593 return n
1592
1594
1593 def __len__(self):
1595 def __len__(self):
1594 return len(self._revlog)
1596 return len(self._revlog)
1595
1597
1596 def __iter__(self):
1598 def __iter__(self):
1597 return self._revlog.__iter__()
1599 return self._revlog.__iter__()
1598
1600
1599 def rev(self, node):
1601 def rev(self, node):
1600 return self._revlog.rev(node)
1602 return self._revlog.rev(node)
1601
1603
1602 def node(self, rev):
1604 def node(self, rev):
1603 return self._revlog.node(rev)
1605 return self._revlog.node(rev)
1604
1606
1605 def lookup(self, value):
1607 def lookup(self, value):
1606 return self._revlog.lookup(value)
1608 return self._revlog.lookup(value)
1607
1609
1608 def parentrevs(self, rev):
1610 def parentrevs(self, rev):
1609 return self._revlog.parentrevs(rev)
1611 return self._revlog.parentrevs(rev)
1610
1612
1611 def parents(self, node):
1613 def parents(self, node):
1612 return self._revlog.parents(node)
1614 return self._revlog.parents(node)
1613
1615
1614 def linkrev(self, rev):
1616 def linkrev(self, rev):
1615 return self._revlog.linkrev(rev)
1617 return self._revlog.linkrev(rev)
1616
1618
1617 def checksize(self):
1619 def checksize(self):
1618 return self._revlog.checksize()
1620 return self._revlog.checksize()
1619
1621
1620 def revision(self, node, _df=None, raw=False):
1622 def revision(self, node, _df=None, raw=False):
1621 return self._revlog.revision(node, _df=_df, raw=raw)
1623 return self._revlog.revision(node, _df=_df, raw=raw)
1622
1624
1623 def rawdata(self, node, _df=None):
1625 def rawdata(self, node, _df=None):
1624 return self._revlog.rawdata(node, _df=_df)
1626 return self._revlog.rawdata(node, _df=_df)
1625
1627
1626 def revdiff(self, rev1, rev2):
1628 def revdiff(self, rev1, rev2):
1627 return self._revlog.revdiff(rev1, rev2)
1629 return self._revlog.revdiff(rev1, rev2)
1628
1630
1629 def cmp(self, node, text):
1631 def cmp(self, node, text):
1630 return self._revlog.cmp(node, text)
1632 return self._revlog.cmp(node, text)
1631
1633
1632 def deltaparent(self, rev):
1634 def deltaparent(self, rev):
1633 return self._revlog.deltaparent(rev)
1635 return self._revlog.deltaparent(rev)
1634
1636
1635 def emitrevisions(self, nodes, nodesorder=None,
1637 def emitrevisions(self, nodes, nodesorder=None,
1636 revisiondata=False, assumehaveparentrevisions=False,
1638 revisiondata=False, assumehaveparentrevisions=False,
1637 deltamode=repository.CG_DELTAMODE_STD):
1639 deltamode=repository.CG_DELTAMODE_STD):
1638 return self._revlog.emitrevisions(
1640 return self._revlog.emitrevisions(
1639 nodes, nodesorder=nodesorder, revisiondata=revisiondata,
1641 nodes, nodesorder=nodesorder, revisiondata=revisiondata,
1640 assumehaveparentrevisions=assumehaveparentrevisions,
1642 assumehaveparentrevisions=assumehaveparentrevisions,
1641 deltamode=deltamode)
1643 deltamode=deltamode)
1642
1644
1643 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None):
1645 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None):
1644 return self._revlog.addgroup(deltas, linkmapper, transaction,
1646 return self._revlog.addgroup(deltas, linkmapper, transaction,
1645 addrevisioncb=addrevisioncb)
1647 addrevisioncb=addrevisioncb)
1646
1648
1647 def rawsize(self, rev):
1649 def rawsize(self, rev):
1648 return self._revlog.rawsize(rev)
1650 return self._revlog.rawsize(rev)
1649
1651
1650 def getstrippoint(self, minlink):
1652 def getstrippoint(self, minlink):
1651 return self._revlog.getstrippoint(minlink)
1653 return self._revlog.getstrippoint(minlink)
1652
1654
1653 def strip(self, minlink, transaction):
1655 def strip(self, minlink, transaction):
1654 return self._revlog.strip(minlink, transaction)
1656 return self._revlog.strip(minlink, transaction)
1655
1657
1656 def files(self):
1658 def files(self):
1657 return self._revlog.files()
1659 return self._revlog.files()
1658
1660
1659 def clone(self, tr, destrevlog, **kwargs):
1661 def clone(self, tr, destrevlog, **kwargs):
1660 if not isinstance(destrevlog, manifestrevlog):
1662 if not isinstance(destrevlog, manifestrevlog):
1661 raise error.ProgrammingError('expected manifestrevlog to clone()')
1663 raise error.ProgrammingError('expected manifestrevlog to clone()')
1662
1664
1663 return self._revlog.clone(tr, destrevlog._revlog, **kwargs)
1665 return self._revlog.clone(tr, destrevlog._revlog, **kwargs)
1664
1666
1665 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
1667 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
1666 revisionscount=False, trackedsize=False,
1668 revisionscount=False, trackedsize=False,
1667 storedsize=False):
1669 storedsize=False):
1668 return self._revlog.storageinfo(
1670 return self._revlog.storageinfo(
1669 exclusivefiles=exclusivefiles, sharedfiles=sharedfiles,
1671 exclusivefiles=exclusivefiles, sharedfiles=sharedfiles,
1670 revisionscount=revisionscount, trackedsize=trackedsize,
1672 revisionscount=revisionscount, trackedsize=trackedsize,
1671 storedsize=storedsize)
1673 storedsize=storedsize)
1672
1674
1673 @property
1675 @property
1674 def indexfile(self):
1676 def indexfile(self):
1675 return self._revlog.indexfile
1677 return self._revlog.indexfile
1676
1678
1677 @indexfile.setter
1679 @indexfile.setter
1678 def indexfile(self, value):
1680 def indexfile(self, value):
1679 self._revlog.indexfile = value
1681 self._revlog.indexfile = value
1680
1682
1681 @property
1683 @property
1682 def opener(self):
1684 def opener(self):
1683 return self._revlog.opener
1685 return self._revlog.opener
1684
1686
1685 @opener.setter
1687 @opener.setter
1686 def opener(self, value):
1688 def opener(self, value):
1687 self._revlog.opener = value
1689 self._revlog.opener = value
1688
1690
1689 @interfaceutil.implementer(repository.imanifestlog)
1691 @interfaceutil.implementer(repository.imanifestlog)
1690 class manifestlog(object):
1692 class manifestlog(object):
1691 """A collection class representing the collection of manifest snapshots
1693 """A collection class representing the collection of manifest snapshots
1692 referenced by commits in the repository.
1694 referenced by commits in the repository.
1693
1695
1694 In this situation, 'manifest' refers to the abstract concept of a snapshot
1696 In this situation, 'manifest' refers to the abstract concept of a snapshot
1695 of the list of files in the given commit. Consumers of the output of this
1697 of the list of files in the given commit. Consumers of the output of this
1696 class do not care about the implementation details of the actual manifests
1698 class do not care about the implementation details of the actual manifests
1697 they receive (i.e. tree or flat or lazily loaded, etc)."""
1699 they receive (i.e. tree or flat or lazily loaded, etc)."""
1698 def __init__(self, opener, repo, rootstore, narrowmatch):
1700 def __init__(self, opener, repo, rootstore, narrowmatch):
1699 usetreemanifest = False
1701 usetreemanifest = False
1700 cachesize = 4
1702 cachesize = 4
1701
1703
1702 opts = getattr(opener, 'options', None)
1704 opts = getattr(opener, 'options', None)
1703 if opts is not None:
1705 if opts is not None:
1704 usetreemanifest = opts.get('treemanifest', usetreemanifest)
1706 usetreemanifest = opts.get('treemanifest', usetreemanifest)
1705 cachesize = opts.get('manifestcachesize', cachesize)
1707 cachesize = opts.get('manifestcachesize', cachesize)
1706
1708
1707 self._treemanifests = usetreemanifest
1709 self._treemanifests = usetreemanifest
1708
1710
1709 self._rootstore = rootstore
1711 self._rootstore = rootstore
1710 self._rootstore._setupmanifestcachehooks(repo)
1712 self._rootstore._setupmanifestcachehooks(repo)
1711 self._narrowmatch = narrowmatch
1713 self._narrowmatch = narrowmatch
1712
1714
1713 # A cache of the manifestctx or treemanifestctx for each directory
1715 # A cache of the manifestctx or treemanifestctx for each directory
1714 self._dirmancache = {}
1716 self._dirmancache = {}
1715 self._dirmancache[''] = util.lrucachedict(cachesize)
1717 self._dirmancache[''] = util.lrucachedict(cachesize)
1716
1718
1717 self._cachesize = cachesize
1719 self._cachesize = cachesize
1718
1720
1719 def __getitem__(self, node):
1721 def __getitem__(self, node):
1720 """Retrieves the manifest instance for the given node. Throws a
1722 """Retrieves the manifest instance for the given node. Throws a
1721 LookupError if not found.
1723 LookupError if not found.
1722 """
1724 """
1723 return self.get('', node)
1725 return self.get('', node)
1724
1726
1725 def get(self, tree, node, verify=True):
1727 def get(self, tree, node, verify=True):
1726 """Retrieves the manifest instance for the given node. Throws a
1728 """Retrieves the manifest instance for the given node. Throws a
1727 LookupError if not found.
1729 LookupError if not found.
1728
1730
1729 `verify` - if True an exception will be thrown if the node is not in
1731 `verify` - if True an exception will be thrown if the node is not in
1730 the revlog
1732 the revlog
1731 """
1733 """
1732 if node in self._dirmancache.get(tree, ()):
1734 if node in self._dirmancache.get(tree, ()):
1733 return self._dirmancache[tree][node]
1735 return self._dirmancache[tree][node]
1734
1736
1735 if not self._narrowmatch.always():
1737 if not self._narrowmatch.always():
1736 if not self._narrowmatch.visitdir(tree[:-1]):
1738 if not self._narrowmatch.visitdir(tree[:-1]):
1737 return excludeddirmanifestctx(tree, node)
1739 return excludeddirmanifestctx(tree, node)
1738 if tree:
1740 if tree:
1739 if self._rootstore._treeondisk:
1741 if self._rootstore._treeondisk:
1740 if verify:
1742 if verify:
1741 # Side-effect is LookupError is raised if node doesn't
1743 # Side-effect is LookupError is raised if node doesn't
1742 # exist.
1744 # exist.
1743 self.getstorage(tree).rev(node)
1745 self.getstorage(tree).rev(node)
1744
1746
1745 m = treemanifestctx(self, tree, node)
1747 m = treemanifestctx(self, tree, node)
1746 else:
1748 else:
1747 raise error.Abort(
1749 raise error.Abort(
1748 _("cannot ask for manifest directory '%s' in a flat "
1750 _("cannot ask for manifest directory '%s' in a flat "
1749 "manifest") % tree)
1751 "manifest") % tree)
1750 else:
1752 else:
1751 if verify:
1753 if verify:
1752 # Side-effect is LookupError is raised if node doesn't exist.
1754 # Side-effect is LookupError is raised if node doesn't exist.
1753 self._rootstore.rev(node)
1755 self._rootstore.rev(node)
1754
1756
1755 if self._treemanifests:
1757 if self._treemanifests:
1756 m = treemanifestctx(self, '', node)
1758 m = treemanifestctx(self, '', node)
1757 else:
1759 else:
1758 m = manifestctx(self, node)
1760 m = manifestctx(self, node)
1759
1761
1760 if node != nullid:
1762 if node != nullid:
1761 mancache = self._dirmancache.get(tree)
1763 mancache = self._dirmancache.get(tree)
1762 if not mancache:
1764 if not mancache:
1763 mancache = util.lrucachedict(self._cachesize)
1765 mancache = util.lrucachedict(self._cachesize)
1764 self._dirmancache[tree] = mancache
1766 self._dirmancache[tree] = mancache
1765 mancache[node] = m
1767 mancache[node] = m
1766 return m
1768 return m
1767
1769
1768 def getstorage(self, tree):
1770 def getstorage(self, tree):
1769 return self._rootstore.dirlog(tree)
1771 return self._rootstore.dirlog(tree)
1770
1772
1771 def clearcaches(self, clear_persisted_data=False):
1773 def clearcaches(self, clear_persisted_data=False):
1772 self._dirmancache.clear()
1774 self._dirmancache.clear()
1773 self._rootstore.clearcaches(clear_persisted_data=clear_persisted_data)
1775 self._rootstore.clearcaches(clear_persisted_data=clear_persisted_data)
1774
1776
1775 def rev(self, node):
1777 def rev(self, node):
1776 return self._rootstore.rev(node)
1778 return self._rootstore.rev(node)
1777
1779
1778 @interfaceutil.implementer(repository.imanifestrevisionwritable)
1780 @interfaceutil.implementer(repository.imanifestrevisionwritable)
1779 class memmanifestctx(object):
1781 class memmanifestctx(object):
1780 def __init__(self, manifestlog):
1782 def __init__(self, manifestlog):
1781 self._manifestlog = manifestlog
1783 self._manifestlog = manifestlog
1782 self._manifestdict = manifestdict()
1784 self._manifestdict = manifestdict()
1783
1785
1784 def _storage(self):
1786 def _storage(self):
1785 return self._manifestlog.getstorage(b'')
1787 return self._manifestlog.getstorage(b'')
1786
1788
1787 def new(self):
1789 def new(self):
1788 return memmanifestctx(self._manifestlog)
1790 return memmanifestctx(self._manifestlog)
1789
1791
1790 def copy(self):
1792 def copy(self):
1791 memmf = memmanifestctx(self._manifestlog)
1793 memmf = memmanifestctx(self._manifestlog)
1792 memmf._manifestdict = self.read().copy()
1794 memmf._manifestdict = self.read().copy()
1793 return memmf
1795 return memmf
1794
1796
1795 def read(self):
1797 def read(self):
1796 return self._manifestdict
1798 return self._manifestdict
1797
1799
1798 def write(self, transaction, link, p1, p2, added, removed, match=None):
1800 def write(self, transaction, link, p1, p2, added, removed, match=None):
1799 return self._storage().add(self._manifestdict, transaction, link,
1801 return self._storage().add(self._manifestdict, transaction, link,
1800 p1, p2, added, removed, match=match)
1802 p1, p2, added, removed, match=match)
1801
1803
1802 @interfaceutil.implementer(repository.imanifestrevisionstored)
1804 @interfaceutil.implementer(repository.imanifestrevisionstored)
1803 class manifestctx(object):
1805 class manifestctx(object):
1804 """A class representing a single revision of a manifest, including its
1806 """A class representing a single revision of a manifest, including its
1805 contents, its parent revs, and its linkrev.
1807 contents, its parent revs, and its linkrev.
1806 """
1808 """
1807 def __init__(self, manifestlog, node):
1809 def __init__(self, manifestlog, node):
1808 self._manifestlog = manifestlog
1810 self._manifestlog = manifestlog
1809 self._data = None
1811 self._data = None
1810
1812
1811 self._node = node
1813 self._node = node
1812
1814
1813 # TODO: We eventually want p1, p2, and linkrev exposed on this class,
1815 # TODO: We eventually want p1, p2, and linkrev exposed on this class,
1814 # but let's add it later when something needs it and we can load it
1816 # but let's add it later when something needs it and we can load it
1815 # lazily.
1817 # lazily.
1816 #self.p1, self.p2 = store.parents(node)
1818 #self.p1, self.p2 = store.parents(node)
1817 #rev = store.rev(node)
1819 #rev = store.rev(node)
1818 #self.linkrev = store.linkrev(rev)
1820 #self.linkrev = store.linkrev(rev)
1819
1821
1820 def _storage(self):
1822 def _storage(self):
1821 return self._manifestlog.getstorage(b'')
1823 return self._manifestlog.getstorage(b'')
1822
1824
1823 def node(self):
1825 def node(self):
1824 return self._node
1826 return self._node
1825
1827
1826 def new(self):
1828 def new(self):
1827 return memmanifestctx(self._manifestlog)
1829 return memmanifestctx(self._manifestlog)
1828
1830
1829 def copy(self):
1831 def copy(self):
1830 memmf = memmanifestctx(self._manifestlog)
1832 memmf = memmanifestctx(self._manifestlog)
1831 memmf._manifestdict = self.read().copy()
1833 memmf._manifestdict = self.read().copy()
1832 return memmf
1834 return memmf
1833
1835
1834 @propertycache
1836 @propertycache
1835 def parents(self):
1837 def parents(self):
1836 return self._storage().parents(self._node)
1838 return self._storage().parents(self._node)
1837
1839
1838 def read(self):
1840 def read(self):
1839 if self._data is None:
1841 if self._data is None:
1840 if self._node == nullid:
1842 if self._node == nullid:
1841 self._data = manifestdict()
1843 self._data = manifestdict()
1842 else:
1844 else:
1843 store = self._storage()
1845 store = self._storage()
1844 if self._node in store.fulltextcache:
1846 if self._node in store.fulltextcache:
1845 text = pycompat.bytestr(store.fulltextcache[self._node])
1847 text = pycompat.bytestr(store.fulltextcache[self._node])
1846 else:
1848 else:
1847 text = store.revision(self._node)
1849 text = store.revision(self._node)
1848 arraytext = bytearray(text)
1850 arraytext = bytearray(text)
1849 store.fulltextcache[self._node] = arraytext
1851 store.fulltextcache[self._node] = arraytext
1850 self._data = manifestdict(text)
1852 self._data = manifestdict(text)
1851 return self._data
1853 return self._data
1852
1854
1853 def readfast(self, shallow=False):
1855 def readfast(self, shallow=False):
1854 '''Calls either readdelta or read, based on which would be less work.
1856 '''Calls either readdelta or read, based on which would be less work.
1855 readdelta is called if the delta is against the p1, and therefore can be
1857 readdelta is called if the delta is against the p1, and therefore can be
1856 read quickly.
1858 read quickly.
1857
1859
1858 If `shallow` is True, nothing changes since this is a flat manifest.
1860 If `shallow` is True, nothing changes since this is a flat manifest.
1859 '''
1861 '''
1860 store = self._storage()
1862 store = self._storage()
1861 r = store.rev(self._node)
1863 r = store.rev(self._node)
1862 deltaparent = store.deltaparent(r)
1864 deltaparent = store.deltaparent(r)
1863 if deltaparent != nullrev and deltaparent in store.parentrevs(r):
1865 if deltaparent != nullrev and deltaparent in store.parentrevs(r):
1864 return self.readdelta()
1866 return self.readdelta()
1865 return self.read()
1867 return self.read()
1866
1868
1867 def readdelta(self, shallow=False):
1869 def readdelta(self, shallow=False):
1868 '''Returns a manifest containing just the entries that are present
1870 '''Returns a manifest containing just the entries that are present
1869 in this manifest, but not in its p1 manifest. This is efficient to read
1871 in this manifest, but not in its p1 manifest. This is efficient to read
1870 if the revlog delta is already p1.
1872 if the revlog delta is already p1.
1871
1873
1872 Changing the value of `shallow` has no effect on flat manifests.
1874 Changing the value of `shallow` has no effect on flat manifests.
1873 '''
1875 '''
1874 store = self._storage()
1876 store = self._storage()
1875 r = store.rev(self._node)
1877 r = store.rev(self._node)
1876 d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
1878 d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
1877 return manifestdict(d)
1879 return manifestdict(d)
1878
1880
1879 def find(self, key):
1881 def find(self, key):
1880 return self.read().find(key)
1882 return self.read().find(key)
1881
1883
1882 @interfaceutil.implementer(repository.imanifestrevisionwritable)
1884 @interfaceutil.implementer(repository.imanifestrevisionwritable)
1883 class memtreemanifestctx(object):
1885 class memtreemanifestctx(object):
1884 def __init__(self, manifestlog, dir=''):
1886 def __init__(self, manifestlog, dir=''):
1885 self._manifestlog = manifestlog
1887 self._manifestlog = manifestlog
1886 self._dir = dir
1888 self._dir = dir
1887 self._treemanifest = treemanifest()
1889 self._treemanifest = treemanifest()
1888
1890
1889 def _storage(self):
1891 def _storage(self):
1890 return self._manifestlog.getstorage(b'')
1892 return self._manifestlog.getstorage(b'')
1891
1893
1892 def new(self, dir=''):
1894 def new(self, dir=''):
1893 return memtreemanifestctx(self._manifestlog, dir=dir)
1895 return memtreemanifestctx(self._manifestlog, dir=dir)
1894
1896
1895 def copy(self):
1897 def copy(self):
1896 memmf = memtreemanifestctx(self._manifestlog, dir=self._dir)
1898 memmf = memtreemanifestctx(self._manifestlog, dir=self._dir)
1897 memmf._treemanifest = self._treemanifest.copy()
1899 memmf._treemanifest = self._treemanifest.copy()
1898 return memmf
1900 return memmf
1899
1901
1900 def read(self):
1902 def read(self):
1901 return self._treemanifest
1903 return self._treemanifest
1902
1904
1903 def write(self, transaction, link, p1, p2, added, removed, match=None):
1905 def write(self, transaction, link, p1, p2, added, removed, match=None):
1904 def readtree(dir, node):
1906 def readtree(dir, node):
1905 return self._manifestlog.get(dir, node).read()
1907 return self._manifestlog.get(dir, node).read()
1906 return self._storage().add(self._treemanifest, transaction, link,
1908 return self._storage().add(self._treemanifest, transaction, link,
1907 p1, p2, added, removed, readtree=readtree,
1909 p1, p2, added, removed, readtree=readtree,
1908 match=match)
1910 match=match)
1909
1911
1910 @interfaceutil.implementer(repository.imanifestrevisionstored)
1912 @interfaceutil.implementer(repository.imanifestrevisionstored)
1911 class treemanifestctx(object):
1913 class treemanifestctx(object):
1912 def __init__(self, manifestlog, dir, node):
1914 def __init__(self, manifestlog, dir, node):
1913 self._manifestlog = manifestlog
1915 self._manifestlog = manifestlog
1914 self._dir = dir
1916 self._dir = dir
1915 self._data = None
1917 self._data = None
1916
1918
1917 self._node = node
1919 self._node = node
1918
1920
1919 # TODO: Load p1/p2/linkrev lazily. They need to be lazily loaded so that
1921 # TODO: Load p1/p2/linkrev lazily. They need to be lazily loaded so that
1920 # we can instantiate treemanifestctx objects for directories we don't
1922 # we can instantiate treemanifestctx objects for directories we don't
1921 # have on disk.
1923 # have on disk.
1922 #self.p1, self.p2 = store.parents(node)
1924 #self.p1, self.p2 = store.parents(node)
1923 #rev = store.rev(node)
1925 #rev = store.rev(node)
1924 #self.linkrev = store.linkrev(rev)
1926 #self.linkrev = store.linkrev(rev)
1925
1927
1926 def _storage(self):
1928 def _storage(self):
1927 narrowmatch = self._manifestlog._narrowmatch
1929 narrowmatch = self._manifestlog._narrowmatch
1928 if not narrowmatch.always():
1930 if not narrowmatch.always():
1929 if not narrowmatch.visitdir(self._dir[:-1]):
1931 if not narrowmatch.visitdir(self._dir[:-1]):
1930 return excludedmanifestrevlog(self._dir)
1932 return excludedmanifestrevlog(self._dir)
1931 return self._manifestlog.getstorage(self._dir)
1933 return self._manifestlog.getstorage(self._dir)
1932
1934
1933 def read(self):
1935 def read(self):
1934 if self._data is None:
1936 if self._data is None:
1935 store = self._storage()
1937 store = self._storage()
1936 if self._node == nullid:
1938 if self._node == nullid:
1937 self._data = treemanifest()
1939 self._data = treemanifest()
1938 # TODO accessing non-public API
1940 # TODO accessing non-public API
1939 elif store._treeondisk:
1941 elif store._treeondisk:
1940 m = treemanifest(dir=self._dir)
1942 m = treemanifest(dir=self._dir)
1941 def gettext():
1943 def gettext():
1942 return store.revision(self._node)
1944 return store.revision(self._node)
1943 def readsubtree(dir, subm):
1945 def readsubtree(dir, subm):
1944 # Set verify to False since we need to be able to create
1946 # Set verify to False since we need to be able to create
1945 # subtrees for trees that don't exist on disk.
1947 # subtrees for trees that don't exist on disk.
1946 return self._manifestlog.get(dir, subm, verify=False).read()
1948 return self._manifestlog.get(dir, subm, verify=False).read()
1947 m.read(gettext, readsubtree)
1949 m.read(gettext, readsubtree)
1948 m.setnode(self._node)
1950 m.setnode(self._node)
1949 self._data = m
1951 self._data = m
1950 else:
1952 else:
1951 if self._node in store.fulltextcache:
1953 if self._node in store.fulltextcache:
1952 text = pycompat.bytestr(store.fulltextcache[self._node])
1954 text = pycompat.bytestr(store.fulltextcache[self._node])
1953 else:
1955 else:
1954 text = store.revision(self._node)
1956 text = store.revision(self._node)
1955 arraytext = bytearray(text)
1957 arraytext = bytearray(text)
1956 store.fulltextcache[self._node] = arraytext
1958 store.fulltextcache[self._node] = arraytext
1957 self._data = treemanifest(dir=self._dir, text=text)
1959 self._data = treemanifest(dir=self._dir, text=text)
1958
1960
1959 return self._data
1961 return self._data
1960
1962
1961 def node(self):
1963 def node(self):
1962 return self._node
1964 return self._node
1963
1965
1964 def new(self, dir=''):
1966 def new(self, dir=''):
1965 return memtreemanifestctx(self._manifestlog, dir=dir)
1967 return memtreemanifestctx(self._manifestlog, dir=dir)
1966
1968
1967 def copy(self):
1969 def copy(self):
1968 memmf = memtreemanifestctx(self._manifestlog, dir=self._dir)
1970 memmf = memtreemanifestctx(self._manifestlog, dir=self._dir)
1969 memmf._treemanifest = self.read().copy()
1971 memmf._treemanifest = self.read().copy()
1970 return memmf
1972 return memmf
1971
1973
1972 @propertycache
1974 @propertycache
1973 def parents(self):
1975 def parents(self):
1974 return self._storage().parents(self._node)
1976 return self._storage().parents(self._node)
1975
1977
1976 def readdelta(self, shallow=False):
1978 def readdelta(self, shallow=False):
1977 '''Returns a manifest containing just the entries that are present
1979 '''Returns a manifest containing just the entries that are present
1978 in this manifest, but not in its p1 manifest. This is efficient to read
1980 in this manifest, but not in its p1 manifest. This is efficient to read
1979 if the revlog delta is already p1.
1981 if the revlog delta is already p1.
1980
1982
1981 If `shallow` is True, this will read the delta for this directory,
1983 If `shallow` is True, this will read the delta for this directory,
1982 without recursively reading subdirectory manifests. Instead, any
1984 without recursively reading subdirectory manifests. Instead, any
1983 subdirectory entry will be reported as it appears in the manifest, i.e.
1985 subdirectory entry will be reported as it appears in the manifest, i.e.
1984 the subdirectory will be reported among files and distinguished only by
1986 the subdirectory will be reported among files and distinguished only by
1985 its 't' flag.
1987 its 't' flag.
1986 '''
1988 '''
1987 store = self._storage()
1989 store = self._storage()
1988 if shallow:
1990 if shallow:
1989 r = store.rev(self._node)
1991 r = store.rev(self._node)
1990 d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
1992 d = mdiff.patchtext(store.revdiff(store.deltaparent(r), r))
1991 return manifestdict(d)
1993 return manifestdict(d)
1992 else:
1994 else:
1993 # Need to perform a slow delta
1995 # Need to perform a slow delta
1994 r0 = store.deltaparent(store.rev(self._node))
1996 r0 = store.deltaparent(store.rev(self._node))
1995 m0 = self._manifestlog.get(self._dir, store.node(r0)).read()
1997 m0 = self._manifestlog.get(self._dir, store.node(r0)).read()
1996 m1 = self.read()
1998 m1 = self.read()
1997 md = treemanifest(dir=self._dir)
1999 md = treemanifest(dir=self._dir)
1998 for f, ((n0, fl0), (n1, fl1)) in m0.diff(m1).iteritems():
2000 for f, ((n0, fl0), (n1, fl1)) in m0.diff(m1).iteritems():
1999 if n1:
2001 if n1:
2000 md[f] = n1
2002 md[f] = n1
2001 if fl1:
2003 if fl1:
2002 md.setflag(f, fl1)
2004 md.setflag(f, fl1)
2003 return md
2005 return md
2004
2006
2005 def readfast(self, shallow=False):
2007 def readfast(self, shallow=False):
2006 '''Calls either readdelta or read, based on which would be less work.
2008 '''Calls either readdelta or read, based on which would be less work.
2007 readdelta is called if the delta is against the p1, and therefore can be
2009 readdelta is called if the delta is against the p1, and therefore can be
2008 read quickly.
2010 read quickly.
2009
2011
2010 If `shallow` is True, it only returns the entries from this manifest,
2012 If `shallow` is True, it only returns the entries from this manifest,
2011 and not any submanifests.
2013 and not any submanifests.
2012 '''
2014 '''
2013 store = self._storage()
2015 store = self._storage()
2014 r = store.rev(self._node)
2016 r = store.rev(self._node)
2015 deltaparent = store.deltaparent(r)
2017 deltaparent = store.deltaparent(r)
2016 if (deltaparent != nullrev and
2018 if (deltaparent != nullrev and
2017 deltaparent in store.parentrevs(r)):
2019 deltaparent in store.parentrevs(r)):
2018 return self.readdelta(shallow=shallow)
2020 return self.readdelta(shallow=shallow)
2019
2021
2020 if shallow:
2022 if shallow:
2021 return manifestdict(store.revision(self._node))
2023 return manifestdict(store.revision(self._node))
2022 else:
2024 else:
2023 return self.read()
2025 return self.read()
2024
2026
2025 def find(self, key):
2027 def find(self, key):
2026 return self.read().find(key)
2028 return self.read().find(key)
2027
2029
2028 class excludeddir(treemanifest):
2030 class excludeddir(treemanifest):
2029 """Stand-in for a directory that is excluded from the repository.
2031 """Stand-in for a directory that is excluded from the repository.
2030
2032
2031 With narrowing active on a repository that uses treemanifests,
2033 With narrowing active on a repository that uses treemanifests,
2032 some of the directory revlogs will be excluded from the resulting
2034 some of the directory revlogs will be excluded from the resulting
2033 clone. This is a huge storage win for clients, but means we need
2035 clone. This is a huge storage win for clients, but means we need
2034 some sort of pseudo-manifest to surface to internals so we can
2036 some sort of pseudo-manifest to surface to internals so we can
2035 detect a merge conflict outside the narrowspec. That's what this
2037 detect a merge conflict outside the narrowspec. That's what this
2036 class is: it stands in for a directory whose node is known, but
2038 class is: it stands in for a directory whose node is known, but
2037 whose contents are unknown.
2039 whose contents are unknown.
2038 """
2040 """
2039 def __init__(self, dir, node):
2041 def __init__(self, dir, node):
2040 super(excludeddir, self).__init__(dir)
2042 super(excludeddir, self).__init__(dir)
2041 self._node = node
2043 self._node = node
2042 # Add an empty file, which will be included by iterators and such,
2044 # Add an empty file, which will be included by iterators and such,
2043 # appearing as the directory itself (i.e. something like "dir/")
2045 # appearing as the directory itself (i.e. something like "dir/")
2044 self._files[''] = node
2046 self._files[''] = node
2045 self._flags[''] = 't'
2047 self._flags[''] = 't'
2046
2048
2047 # Manifests outside the narrowspec should never be modified, so avoid
2049 # Manifests outside the narrowspec should never be modified, so avoid
2048 # copying. This makes a noticeable difference when there are very many
2050 # copying. This makes a noticeable difference when there are very many
2049 # directories outside the narrowspec. Also, it makes sense for the copy to
2051 # directories outside the narrowspec. Also, it makes sense for the copy to
2050 # be of the same type as the original, which would not happen with the
2052 # be of the same type as the original, which would not happen with the
2051 # super type's copy().
2053 # super type's copy().
2052 def copy(self):
2054 def copy(self):
2053 return self
2055 return self
2054
2056
2055 class excludeddirmanifestctx(treemanifestctx):
2057 class excludeddirmanifestctx(treemanifestctx):
2056 """context wrapper for excludeddir - see that docstring for rationale"""
2058 """context wrapper for excludeddir - see that docstring for rationale"""
2057 def __init__(self, dir, node):
2059 def __init__(self, dir, node):
2058 self._dir = dir
2060 self._dir = dir
2059 self._node = node
2061 self._node = node
2060
2062
2061 def read(self):
2063 def read(self):
2062 return excludeddir(self._dir, self._node)
2064 return excludeddir(self._dir, self._node)
2063
2065
2064 def write(self, *args):
2066 def write(self, *args):
2065 raise error.ProgrammingError(
2067 raise error.ProgrammingError(
2066 'attempt to write manifest from excluded dir %s' % self._dir)
2068 'attempt to write manifest from excluded dir %s' % self._dir)
2067
2069
2068 class excludedmanifestrevlog(manifestrevlog):
2070 class excludedmanifestrevlog(manifestrevlog):
2069 """Stand-in for excluded treemanifest revlogs.
2071 """Stand-in for excluded treemanifest revlogs.
2070
2072
2071 When narrowing is active on a treemanifest repository, we'll have
2073 When narrowing is active on a treemanifest repository, we'll have
2072 references to directories we can't see due to the revlog being
2074 references to directories we can't see due to the revlog being
2073 skipped. This class exists to conform to the manifestrevlog
2075 skipped. This class exists to conform to the manifestrevlog
2074 interface for those directories and proactively prevent writes to
2076 interface for those directories and proactively prevent writes to
2075 outside the narrowspec.
2077 outside the narrowspec.
2076 """
2078 """
2077
2079
2078 def __init__(self, dir):
2080 def __init__(self, dir):
2079 self._dir = dir
2081 self._dir = dir
2080
2082
2081 def __len__(self):
2083 def __len__(self):
2082 raise error.ProgrammingError(
2084 raise error.ProgrammingError(
2083 'attempt to get length of excluded dir %s' % self._dir)
2085 'attempt to get length of excluded dir %s' % self._dir)
2084
2086
2085 def rev(self, node):
2087 def rev(self, node):
2086 raise error.ProgrammingError(
2088 raise error.ProgrammingError(
2087 'attempt to get rev from excluded dir %s' % self._dir)
2089 'attempt to get rev from excluded dir %s' % self._dir)
2088
2090
2089 def linkrev(self, node):
2091 def linkrev(self, node):
2090 raise error.ProgrammingError(
2092 raise error.ProgrammingError(
2091 'attempt to get linkrev from excluded dir %s' % self._dir)
2093 'attempt to get linkrev from excluded dir %s' % self._dir)
2092
2094
2093 def node(self, rev):
2095 def node(self, rev):
2094 raise error.ProgrammingError(
2096 raise error.ProgrammingError(
2095 'attempt to get node from excluded dir %s' % self._dir)
2097 'attempt to get node from excluded dir %s' % self._dir)
2096
2098
2097 def add(self, *args, **kwargs):
2099 def add(self, *args, **kwargs):
2098 # We should never write entries in dirlogs outside the narrow clone.
2100 # We should never write entries in dirlogs outside the narrow clone.
2099 # However, the method still gets called from writesubtree() in
2101 # However, the method still gets called from writesubtree() in
2100 # _addtree(), so we need to handle it. We should possibly make that
2102 # _addtree(), so we need to handle it. We should possibly make that
2101 # avoid calling add() with a clean manifest (_dirty is always False
2103 # avoid calling add() with a clean manifest (_dirty is always False
2102 # in excludeddir instances).
2104 # in excludeddir instances).
2103 pass
2105 pass
@@ -1,317 +1,319
1 # narrowspec.py - methods for working with a narrow view of a repository
1 # narrowspec.py - methods for working with a narrow view of a repository
2 #
2 #
3 # Copyright 2017 Google, Inc.
3 # Copyright 2017 Google, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from .i18n import _
10 from .i18n import _
11 from .interfaces import (
12 repository,
13 )
11 from . import (
14 from . import (
12 error,
15 error,
13 match as matchmod,
16 match as matchmod,
14 merge,
17 merge,
15 repository,
16 scmutil,
18 scmutil,
17 sparse,
19 sparse,
18 util,
20 util,
19 )
21 )
20
22
21 # The file in .hg/store/ that indicates which paths exit in the store
23 # The file in .hg/store/ that indicates which paths exit in the store
22 FILENAME = 'narrowspec'
24 FILENAME = 'narrowspec'
23 # The file in .hg/ that indicates which paths exit in the dirstate
25 # The file in .hg/ that indicates which paths exit in the dirstate
24 DIRSTATE_FILENAME = 'narrowspec.dirstate'
26 DIRSTATE_FILENAME = 'narrowspec.dirstate'
25
27
26 # Pattern prefixes that are allowed in narrow patterns. This list MUST
28 # Pattern prefixes that are allowed in narrow patterns. This list MUST
27 # only contain patterns that are fast and safe to evaluate. Keep in mind
29 # only contain patterns that are fast and safe to evaluate. Keep in mind
28 # that patterns are supplied by clients and executed on remote servers
30 # that patterns are supplied by clients and executed on remote servers
29 # as part of wire protocol commands. That means that changes to this
31 # as part of wire protocol commands. That means that changes to this
30 # data structure influence the wire protocol and should not be taken
32 # data structure influence the wire protocol and should not be taken
31 # lightly - especially removals.
33 # lightly - especially removals.
32 VALID_PREFIXES = (
34 VALID_PREFIXES = (
33 b'path:',
35 b'path:',
34 b'rootfilesin:',
36 b'rootfilesin:',
35 )
37 )
36
38
37 def normalizesplitpattern(kind, pat):
39 def normalizesplitpattern(kind, pat):
38 """Returns the normalized version of a pattern and kind.
40 """Returns the normalized version of a pattern and kind.
39
41
40 Returns a tuple with the normalized kind and normalized pattern.
42 Returns a tuple with the normalized kind and normalized pattern.
41 """
43 """
42 pat = pat.rstrip('/')
44 pat = pat.rstrip('/')
43 _validatepattern(pat)
45 _validatepattern(pat)
44 return kind, pat
46 return kind, pat
45
47
46 def _numlines(s):
48 def _numlines(s):
47 """Returns the number of lines in s, including ending empty lines."""
49 """Returns the number of lines in s, including ending empty lines."""
48 # We use splitlines because it is Unicode-friendly and thus Python 3
50 # We use splitlines because it is Unicode-friendly and thus Python 3
49 # compatible. However, it does not count empty lines at the end, so trick
51 # compatible. However, it does not count empty lines at the end, so trick
50 # it by adding a character at the end.
52 # it by adding a character at the end.
51 return len((s + 'x').splitlines())
53 return len((s + 'x').splitlines())
52
54
53 def _validatepattern(pat):
55 def _validatepattern(pat):
54 """Validates the pattern and aborts if it is invalid.
56 """Validates the pattern and aborts if it is invalid.
55
57
56 Patterns are stored in the narrowspec as newline-separated
58 Patterns are stored in the narrowspec as newline-separated
57 POSIX-style bytestring paths. There's no escaping.
59 POSIX-style bytestring paths. There's no escaping.
58 """
60 """
59
61
60 # We use newlines as separators in the narrowspec file, so don't allow them
62 # We use newlines as separators in the narrowspec file, so don't allow them
61 # in patterns.
63 # in patterns.
62 if _numlines(pat) > 1:
64 if _numlines(pat) > 1:
63 raise error.Abort(_('newlines are not allowed in narrowspec paths'))
65 raise error.Abort(_('newlines are not allowed in narrowspec paths'))
64
66
65 components = pat.split('/')
67 components = pat.split('/')
66 if '.' in components or '..' in components:
68 if '.' in components or '..' in components:
67 raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))
69 raise error.Abort(_('"." and ".." are not allowed in narrowspec paths'))
68
70
69 def normalizepattern(pattern, defaultkind='path'):
71 def normalizepattern(pattern, defaultkind='path'):
70 """Returns the normalized version of a text-format pattern.
72 """Returns the normalized version of a text-format pattern.
71
73
72 If the pattern has no kind, the default will be added.
74 If the pattern has no kind, the default will be added.
73 """
75 """
74 kind, pat = matchmod._patsplit(pattern, defaultkind)
76 kind, pat = matchmod._patsplit(pattern, defaultkind)
75 return '%s:%s' % normalizesplitpattern(kind, pat)
77 return '%s:%s' % normalizesplitpattern(kind, pat)
76
78
77 def parsepatterns(pats):
79 def parsepatterns(pats):
78 """Parses an iterable of patterns into a typed pattern set.
80 """Parses an iterable of patterns into a typed pattern set.
79
81
80 Patterns are assumed to be ``path:`` if no prefix is present.
82 Patterns are assumed to be ``path:`` if no prefix is present.
81 For safety and performance reasons, only some prefixes are allowed.
83 For safety and performance reasons, only some prefixes are allowed.
82 See ``validatepatterns()``.
84 See ``validatepatterns()``.
83
85
84 This function should be used on patterns that come from the user to
86 This function should be used on patterns that come from the user to
85 normalize and validate them to the internal data structure used for
87 normalize and validate them to the internal data structure used for
86 representing patterns.
88 representing patterns.
87 """
89 """
88 res = {normalizepattern(orig) for orig in pats}
90 res = {normalizepattern(orig) for orig in pats}
89 validatepatterns(res)
91 validatepatterns(res)
90 return res
92 return res
91
93
92 def validatepatterns(pats):
94 def validatepatterns(pats):
93 """Validate that patterns are in the expected data structure and format.
95 """Validate that patterns are in the expected data structure and format.
94
96
95 And that is a set of normalized patterns beginning with ``path:`` or
97 And that is a set of normalized patterns beginning with ``path:`` or
96 ``rootfilesin:``.
98 ``rootfilesin:``.
97
99
98 This function should be used to validate internal data structures
100 This function should be used to validate internal data structures
99 and patterns that are loaded from sources that use the internal,
101 and patterns that are loaded from sources that use the internal,
100 prefixed pattern representation (but can't necessarily be fully trusted).
102 prefixed pattern representation (but can't necessarily be fully trusted).
101 """
103 """
102 if not isinstance(pats, set):
104 if not isinstance(pats, set):
103 raise error.ProgrammingError('narrow patterns should be a set; '
105 raise error.ProgrammingError('narrow patterns should be a set; '
104 'got %r' % pats)
106 'got %r' % pats)
105
107
106 for pat in pats:
108 for pat in pats:
107 if not pat.startswith(VALID_PREFIXES):
109 if not pat.startswith(VALID_PREFIXES):
108 # Use a Mercurial exception because this can happen due to user
110 # Use a Mercurial exception because this can happen due to user
109 # bugs (e.g. manually updating spec file).
111 # bugs (e.g. manually updating spec file).
110 raise error.Abort(_('invalid prefix on narrow pattern: %s') % pat,
112 raise error.Abort(_('invalid prefix on narrow pattern: %s') % pat,
111 hint=_('narrow patterns must begin with one of '
113 hint=_('narrow patterns must begin with one of '
112 'the following: %s') %
114 'the following: %s') %
113 ', '.join(VALID_PREFIXES))
115 ', '.join(VALID_PREFIXES))
114
116
115 def format(includes, excludes):
117 def format(includes, excludes):
116 output = '[include]\n'
118 output = '[include]\n'
117 for i in sorted(includes - excludes):
119 for i in sorted(includes - excludes):
118 output += i + '\n'
120 output += i + '\n'
119 output += '[exclude]\n'
121 output += '[exclude]\n'
120 for e in sorted(excludes):
122 for e in sorted(excludes):
121 output += e + '\n'
123 output += e + '\n'
122 return output
124 return output
123
125
124 def match(root, include=None, exclude=None):
126 def match(root, include=None, exclude=None):
125 if not include:
127 if not include:
126 # Passing empty include and empty exclude to matchmod.match()
128 # Passing empty include and empty exclude to matchmod.match()
127 # gives a matcher that matches everything, so explicitly use
129 # gives a matcher that matches everything, so explicitly use
128 # the nevermatcher.
130 # the nevermatcher.
129 return matchmod.never()
131 return matchmod.never()
130 return matchmod.match(root, '', [], include=include or [],
132 return matchmod.match(root, '', [], include=include or [],
131 exclude=exclude or [])
133 exclude=exclude or [])
132
134
133 def parseconfig(ui, spec):
135 def parseconfig(ui, spec):
134 # maybe we should care about the profiles returned too
136 # maybe we should care about the profiles returned too
135 includepats, excludepats, profiles = sparse.parseconfig(ui, spec, 'narrow')
137 includepats, excludepats, profiles = sparse.parseconfig(ui, spec, 'narrow')
136 if profiles:
138 if profiles:
137 raise error.Abort(_("including other spec files using '%include' is not"
139 raise error.Abort(_("including other spec files using '%include' is not"
138 " supported in narrowspec"))
140 " supported in narrowspec"))
139
141
140 validatepatterns(includepats)
142 validatepatterns(includepats)
141 validatepatterns(excludepats)
143 validatepatterns(excludepats)
142
144
143 return includepats, excludepats
145 return includepats, excludepats
144
146
145 def load(repo):
147 def load(repo):
146 # Treat "narrowspec does not exist" the same as "narrowspec file exists
148 # Treat "narrowspec does not exist" the same as "narrowspec file exists
147 # and is empty".
149 # and is empty".
148 spec = repo.svfs.tryread(FILENAME)
150 spec = repo.svfs.tryread(FILENAME)
149 return parseconfig(repo.ui, spec)
151 return parseconfig(repo.ui, spec)
150
152
151 def save(repo, includepats, excludepats):
153 def save(repo, includepats, excludepats):
152 validatepatterns(includepats)
154 validatepatterns(includepats)
153 validatepatterns(excludepats)
155 validatepatterns(excludepats)
154 spec = format(includepats, excludepats)
156 spec = format(includepats, excludepats)
155 repo.svfs.write(FILENAME, spec)
157 repo.svfs.write(FILENAME, spec)
156
158
157 def copytoworkingcopy(repo):
159 def copytoworkingcopy(repo):
158 spec = repo.svfs.read(FILENAME)
160 spec = repo.svfs.read(FILENAME)
159 repo.vfs.write(DIRSTATE_FILENAME, spec)
161 repo.vfs.write(DIRSTATE_FILENAME, spec)
160
162
161 def savebackup(repo, backupname):
163 def savebackup(repo, backupname):
162 if repository.NARROW_REQUIREMENT not in repo.requirements:
164 if repository.NARROW_REQUIREMENT not in repo.requirements:
163 return
165 return
164 svfs = repo.svfs
166 svfs = repo.svfs
165 svfs.tryunlink(backupname)
167 svfs.tryunlink(backupname)
166 util.copyfile(svfs.join(FILENAME), svfs.join(backupname), hardlink=True)
168 util.copyfile(svfs.join(FILENAME), svfs.join(backupname), hardlink=True)
167
169
168 def restorebackup(repo, backupname):
170 def restorebackup(repo, backupname):
169 if repository.NARROW_REQUIREMENT not in repo.requirements:
171 if repository.NARROW_REQUIREMENT not in repo.requirements:
170 return
172 return
171 util.rename(repo.svfs.join(backupname), repo.svfs.join(FILENAME))
173 util.rename(repo.svfs.join(backupname), repo.svfs.join(FILENAME))
172
174
173 def savewcbackup(repo, backupname):
175 def savewcbackup(repo, backupname):
174 if repository.NARROW_REQUIREMENT not in repo.requirements:
176 if repository.NARROW_REQUIREMENT not in repo.requirements:
175 return
177 return
176 vfs = repo.vfs
178 vfs = repo.vfs
177 vfs.tryunlink(backupname)
179 vfs.tryunlink(backupname)
178 # It may not exist in old repos
180 # It may not exist in old repos
179 if vfs.exists(DIRSTATE_FILENAME):
181 if vfs.exists(DIRSTATE_FILENAME):
180 util.copyfile(vfs.join(DIRSTATE_FILENAME), vfs.join(backupname),
182 util.copyfile(vfs.join(DIRSTATE_FILENAME), vfs.join(backupname),
181 hardlink=True)
183 hardlink=True)
182
184
183 def restorewcbackup(repo, backupname):
185 def restorewcbackup(repo, backupname):
184 if repository.NARROW_REQUIREMENT not in repo.requirements:
186 if repository.NARROW_REQUIREMENT not in repo.requirements:
185 return
187 return
186 # It may not exist in old repos
188 # It may not exist in old repos
187 if repo.vfs.exists(backupname):
189 if repo.vfs.exists(backupname):
188 util.rename(repo.vfs.join(backupname), repo.vfs.join(DIRSTATE_FILENAME))
190 util.rename(repo.vfs.join(backupname), repo.vfs.join(DIRSTATE_FILENAME))
189
191
190 def clearwcbackup(repo, backupname):
192 def clearwcbackup(repo, backupname):
191 if repository.NARROW_REQUIREMENT not in repo.requirements:
193 if repository.NARROW_REQUIREMENT not in repo.requirements:
192 return
194 return
193 repo.vfs.tryunlink(backupname)
195 repo.vfs.tryunlink(backupname)
194
196
195 def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
197 def restrictpatterns(req_includes, req_excludes, repo_includes, repo_excludes):
196 r""" Restricts the patterns according to repo settings,
198 r""" Restricts the patterns according to repo settings,
197 results in a logical AND operation
199 results in a logical AND operation
198
200
199 :param req_includes: requested includes
201 :param req_includes: requested includes
200 :param req_excludes: requested excludes
202 :param req_excludes: requested excludes
201 :param repo_includes: repo includes
203 :param repo_includes: repo includes
202 :param repo_excludes: repo excludes
204 :param repo_excludes: repo excludes
203 :return: include patterns, exclude patterns, and invalid include patterns.
205 :return: include patterns, exclude patterns, and invalid include patterns.
204
206
205 >>> restrictpatterns({'f1','f2'}, {}, ['f1'], [])
207 >>> restrictpatterns({'f1','f2'}, {}, ['f1'], [])
206 (set(['f1']), {}, [])
208 (set(['f1']), {}, [])
207 >>> restrictpatterns({'f1'}, {}, ['f1','f2'], [])
209 >>> restrictpatterns({'f1'}, {}, ['f1','f2'], [])
208 (set(['f1']), {}, [])
210 (set(['f1']), {}, [])
209 >>> restrictpatterns({'f1/fc1', 'f3/fc3'}, {}, ['f1','f2'], [])
211 >>> restrictpatterns({'f1/fc1', 'f3/fc3'}, {}, ['f1','f2'], [])
210 (set(['f1/fc1']), {}, [])
212 (set(['f1/fc1']), {}, [])
211 >>> restrictpatterns({'f1_fc1'}, {}, ['f1','f2'], [])
213 >>> restrictpatterns({'f1_fc1'}, {}, ['f1','f2'], [])
212 ([], set(['path:.']), [])
214 ([], set(['path:.']), [])
213 >>> restrictpatterns({'f1/../f2/fc2'}, {}, ['f1','f2'], [])
215 >>> restrictpatterns({'f1/../f2/fc2'}, {}, ['f1','f2'], [])
214 (set(['f2/fc2']), {}, [])
216 (set(['f2/fc2']), {}, [])
215 >>> restrictpatterns({'f1/../f3/fc3'}, {}, ['f1','f2'], [])
217 >>> restrictpatterns({'f1/../f3/fc3'}, {}, ['f1','f2'], [])
216 ([], set(['path:.']), [])
218 ([], set(['path:.']), [])
217 >>> restrictpatterns({'f1/$non_exitent_var'}, {}, ['f1','f2'], [])
219 >>> restrictpatterns({'f1/$non_exitent_var'}, {}, ['f1','f2'], [])
218 (set(['f1/$non_exitent_var']), {}, [])
220 (set(['f1/$non_exitent_var']), {}, [])
219 """
221 """
220 res_excludes = set(req_excludes)
222 res_excludes = set(req_excludes)
221 res_excludes.update(repo_excludes)
223 res_excludes.update(repo_excludes)
222 invalid_includes = []
224 invalid_includes = []
223 if not req_includes:
225 if not req_includes:
224 res_includes = set(repo_includes)
226 res_includes = set(repo_includes)
225 elif 'path:.' not in repo_includes:
227 elif 'path:.' not in repo_includes:
226 res_includes = []
228 res_includes = []
227 for req_include in req_includes:
229 for req_include in req_includes:
228 req_include = util.expandpath(util.normpath(req_include))
230 req_include = util.expandpath(util.normpath(req_include))
229 if req_include in repo_includes:
231 if req_include in repo_includes:
230 res_includes.append(req_include)
232 res_includes.append(req_include)
231 continue
233 continue
232 valid = False
234 valid = False
233 for repo_include in repo_includes:
235 for repo_include in repo_includes:
234 if req_include.startswith(repo_include + '/'):
236 if req_include.startswith(repo_include + '/'):
235 valid = True
237 valid = True
236 res_includes.append(req_include)
238 res_includes.append(req_include)
237 break
239 break
238 if not valid:
240 if not valid:
239 invalid_includes.append(req_include)
241 invalid_includes.append(req_include)
240 if len(res_includes) == 0:
242 if len(res_includes) == 0:
241 res_excludes = {'path:.'}
243 res_excludes = {'path:.'}
242 else:
244 else:
243 res_includes = set(res_includes)
245 res_includes = set(res_includes)
244 else:
246 else:
245 res_includes = set(req_includes)
247 res_includes = set(req_includes)
246 return res_includes, res_excludes, invalid_includes
248 return res_includes, res_excludes, invalid_includes
247
249
248 # These two are extracted for extensions (specifically for Google's CitC file
250 # These two are extracted for extensions (specifically for Google's CitC file
249 # system)
251 # system)
250 def _deletecleanfiles(repo, files):
252 def _deletecleanfiles(repo, files):
251 for f in files:
253 for f in files:
252 repo.wvfs.unlinkpath(f)
254 repo.wvfs.unlinkpath(f)
253
255
254 def _writeaddedfiles(repo, pctx, files):
256 def _writeaddedfiles(repo, pctx, files):
255 actions = merge.emptyactions()
257 actions = merge.emptyactions()
256 addgaction = actions[merge.ACTION_GET].append
258 addgaction = actions[merge.ACTION_GET].append
257 mf = repo['.'].manifest()
259 mf = repo['.'].manifest()
258 for f in files:
260 for f in files:
259 if not repo.wvfs.exists(f):
261 if not repo.wvfs.exists(f):
260 addgaction((f, (mf.flags(f), False), "narrowspec updated"))
262 addgaction((f, (mf.flags(f), False), "narrowspec updated"))
261 merge.applyupdates(repo, actions, wctx=repo[None],
263 merge.applyupdates(repo, actions, wctx=repo[None],
262 mctx=repo['.'], overwrite=False, wantfiledata=False)
264 mctx=repo['.'], overwrite=False, wantfiledata=False)
263
265
264 def checkworkingcopynarrowspec(repo):
266 def checkworkingcopynarrowspec(repo):
265 # Avoid infinite recursion when updating the working copy
267 # Avoid infinite recursion when updating the working copy
266 if getattr(repo, '_updatingnarrowspec', False):
268 if getattr(repo, '_updatingnarrowspec', False):
267 return
269 return
268 storespec = repo.svfs.tryread(FILENAME)
270 storespec = repo.svfs.tryread(FILENAME)
269 wcspec = repo.vfs.tryread(DIRSTATE_FILENAME)
271 wcspec = repo.vfs.tryread(DIRSTATE_FILENAME)
270 if wcspec != storespec:
272 if wcspec != storespec:
271 raise error.Abort(_("working copy's narrowspec is stale"),
273 raise error.Abort(_("working copy's narrowspec is stale"),
272 hint=_("run 'hg tracked --update-working-copy'"))
274 hint=_("run 'hg tracked --update-working-copy'"))
273
275
274 def updateworkingcopy(repo, assumeclean=False):
276 def updateworkingcopy(repo, assumeclean=False):
275 """updates the working copy and dirstate from the store narrowspec
277 """updates the working copy and dirstate from the store narrowspec
276
278
277 When assumeclean=True, files that are not known to be clean will also
279 When assumeclean=True, files that are not known to be clean will also
278 be deleted. It is then up to the caller to make sure they are clean.
280 be deleted. It is then up to the caller to make sure they are clean.
279 """
281 """
280 oldspec = repo.vfs.tryread(DIRSTATE_FILENAME)
282 oldspec = repo.vfs.tryread(DIRSTATE_FILENAME)
281 newspec = repo.svfs.tryread(FILENAME)
283 newspec = repo.svfs.tryread(FILENAME)
282 repo._updatingnarrowspec = True
284 repo._updatingnarrowspec = True
283
285
284 oldincludes, oldexcludes = parseconfig(repo.ui, oldspec)
286 oldincludes, oldexcludes = parseconfig(repo.ui, oldspec)
285 newincludes, newexcludes = parseconfig(repo.ui, newspec)
287 newincludes, newexcludes = parseconfig(repo.ui, newspec)
286 oldmatch = match(repo.root, include=oldincludes, exclude=oldexcludes)
288 oldmatch = match(repo.root, include=oldincludes, exclude=oldexcludes)
287 newmatch = match(repo.root, include=newincludes, exclude=newexcludes)
289 newmatch = match(repo.root, include=newincludes, exclude=newexcludes)
288 addedmatch = matchmod.differencematcher(newmatch, oldmatch)
290 addedmatch = matchmod.differencematcher(newmatch, oldmatch)
289 removedmatch = matchmod.differencematcher(oldmatch, newmatch)
291 removedmatch = matchmod.differencematcher(oldmatch, newmatch)
290
292
291 ds = repo.dirstate
293 ds = repo.dirstate
292 lookup, status = ds.status(removedmatch, subrepos=[], ignored=True,
294 lookup, status = ds.status(removedmatch, subrepos=[], ignored=True,
293 clean=True, unknown=True)
295 clean=True, unknown=True)
294 trackeddirty = status.modified + status.added
296 trackeddirty = status.modified + status.added
295 clean = status.clean
297 clean = status.clean
296 if assumeclean:
298 if assumeclean:
297 assert not trackeddirty
299 assert not trackeddirty
298 clean.extend(lookup)
300 clean.extend(lookup)
299 else:
301 else:
300 trackeddirty.extend(lookup)
302 trackeddirty.extend(lookup)
301 _deletecleanfiles(repo, clean)
303 _deletecleanfiles(repo, clean)
302 uipathfn = scmutil.getuipathfn(repo)
304 uipathfn = scmutil.getuipathfn(repo)
303 for f in sorted(trackeddirty):
305 for f in sorted(trackeddirty):
304 repo.ui.status(_('not deleting possibly dirty file %s\n') % uipathfn(f))
306 repo.ui.status(_('not deleting possibly dirty file %s\n') % uipathfn(f))
305 for f in sorted(status.unknown):
307 for f in sorted(status.unknown):
306 repo.ui.status(_('not deleting unknown file %s\n') % uipathfn(f))
308 repo.ui.status(_('not deleting unknown file %s\n') % uipathfn(f))
307 for f in sorted(status.ignored):
309 for f in sorted(status.ignored):
308 repo.ui.status(_('not deleting ignored file %s\n') % uipathfn(f))
310 repo.ui.status(_('not deleting ignored file %s\n') % uipathfn(f))
309 for f in clean + trackeddirty:
311 for f in clean + trackeddirty:
310 ds.drop(f)
312 ds.drop(f)
311
313
312 pctx = repo['.']
314 pctx = repo['.']
313 newfiles = [f for f in pctx.manifest().walk(addedmatch) if f not in ds]
315 newfiles = [f for f in pctx.manifest().walk(addedmatch) if f not in ds]
314 for f in newfiles:
316 for f in newfiles:
315 ds.normallookup(f)
317 ds.normallookup(f)
316 _writeaddedfiles(repo, pctx, newfiles)
318 _writeaddedfiles(repo, pctx, newfiles)
317 repo._updatingnarrowspec = False
319 repo._updatingnarrowspec = False
@@ -1,2676 +1,2678
1 # revlog.py - storage back-end for mercurial
1 # revlog.py - storage back-end for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@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 """Storage back-end for Mercurial.
8 """Storage back-end for Mercurial.
9
9
10 This provides efficient delta storage with O(1) retrieve and append
10 This provides efficient delta storage with O(1) retrieve and append
11 and O(changes) merge between branches.
11 and O(changes) merge between branches.
12 """
12 """
13
13
14 from __future__ import absolute_import
14 from __future__ import absolute_import
15
15
16 import collections
16 import collections
17 import contextlib
17 import contextlib
18 import errno
18 import errno
19 import io
19 import io
20 import os
20 import os
21 import struct
21 import struct
22 import zlib
22 import zlib
23
23
24 # import stuff from node for others to import from revlog
24 # import stuff from node for others to import from revlog
25 from .node import (
25 from .node import (
26 bin,
26 bin,
27 hex,
27 hex,
28 nullhex,
28 nullhex,
29 nullid,
29 nullid,
30 nullrev,
30 nullrev,
31 short,
31 short,
32 wdirfilenodeids,
32 wdirfilenodeids,
33 wdirhex,
33 wdirhex,
34 wdirid,
34 wdirid,
35 wdirrev,
35 wdirrev,
36 )
36 )
37 from .i18n import _
37 from .i18n import _
38 from .revlogutils.constants import (
38 from .revlogutils.constants import (
39 FLAG_GENERALDELTA,
39 FLAG_GENERALDELTA,
40 FLAG_INLINE_DATA,
40 FLAG_INLINE_DATA,
41 REVLOGV0,
41 REVLOGV0,
42 REVLOGV1,
42 REVLOGV1,
43 REVLOGV1_FLAGS,
43 REVLOGV1_FLAGS,
44 REVLOGV2,
44 REVLOGV2,
45 REVLOGV2_FLAGS,
45 REVLOGV2_FLAGS,
46 REVLOG_DEFAULT_FLAGS,
46 REVLOG_DEFAULT_FLAGS,
47 REVLOG_DEFAULT_FORMAT,
47 REVLOG_DEFAULT_FORMAT,
48 REVLOG_DEFAULT_VERSION,
48 REVLOG_DEFAULT_VERSION,
49 )
49 )
50 from .revlogutils.flagutil import (
50 from .revlogutils.flagutil import (
51 REVIDX_DEFAULT_FLAGS,
51 REVIDX_DEFAULT_FLAGS,
52 REVIDX_ELLIPSIS,
52 REVIDX_ELLIPSIS,
53 REVIDX_EXTSTORED,
53 REVIDX_EXTSTORED,
54 REVIDX_FLAGS_ORDER,
54 REVIDX_FLAGS_ORDER,
55 REVIDX_ISCENSORED,
55 REVIDX_ISCENSORED,
56 REVIDX_RAWTEXT_CHANGING_FLAGS,
56 REVIDX_RAWTEXT_CHANGING_FLAGS,
57 )
57 )
58 from .thirdparty import (
58 from .thirdparty import (
59 attr,
59 attr,
60 )
60 )
61 from . import (
61 from . import (
62 ancestor,
62 ancestor,
63 dagop,
63 dagop,
64 error,
64 error,
65 mdiff,
65 mdiff,
66 policy,
66 policy,
67 pycompat,
67 pycompat,
68 repository,
69 templatefilters,
68 templatefilters,
70 util,
69 util,
71 )
70 )
71 from .interfaces import (
72 repository,
73 )
72 from .revlogutils import (
74 from .revlogutils import (
73 deltas as deltautil,
75 deltas as deltautil,
74 flagutil,
76 flagutil,
75 )
77 )
76 from .utils import (
78 from .utils import (
77 interfaceutil,
79 interfaceutil,
78 storageutil,
80 storageutil,
79 stringutil,
81 stringutil,
80 )
82 )
81
83
82 # blanked usage of all the name to prevent pyflakes constraints
84 # blanked usage of all the name to prevent pyflakes constraints
83 # We need these name available in the module for extensions.
85 # We need these name available in the module for extensions.
84 REVLOGV0
86 REVLOGV0
85 REVLOGV1
87 REVLOGV1
86 REVLOGV2
88 REVLOGV2
87 FLAG_INLINE_DATA
89 FLAG_INLINE_DATA
88 FLAG_GENERALDELTA
90 FLAG_GENERALDELTA
89 REVLOG_DEFAULT_FLAGS
91 REVLOG_DEFAULT_FLAGS
90 REVLOG_DEFAULT_FORMAT
92 REVLOG_DEFAULT_FORMAT
91 REVLOG_DEFAULT_VERSION
93 REVLOG_DEFAULT_VERSION
92 REVLOGV1_FLAGS
94 REVLOGV1_FLAGS
93 REVLOGV2_FLAGS
95 REVLOGV2_FLAGS
94 REVIDX_ISCENSORED
96 REVIDX_ISCENSORED
95 REVIDX_ELLIPSIS
97 REVIDX_ELLIPSIS
96 REVIDX_EXTSTORED
98 REVIDX_EXTSTORED
97 REVIDX_DEFAULT_FLAGS
99 REVIDX_DEFAULT_FLAGS
98 REVIDX_FLAGS_ORDER
100 REVIDX_FLAGS_ORDER
99 REVIDX_RAWTEXT_CHANGING_FLAGS
101 REVIDX_RAWTEXT_CHANGING_FLAGS
100
102
101 parsers = policy.importmod(r'parsers')
103 parsers = policy.importmod(r'parsers')
102 rustancestor = policy.importrust(r'ancestor')
104 rustancestor = policy.importrust(r'ancestor')
103 rustdagop = policy.importrust(r'dagop')
105 rustdagop = policy.importrust(r'dagop')
104
106
105 # Aliased for performance.
107 # Aliased for performance.
106 _zlibdecompress = zlib.decompress
108 _zlibdecompress = zlib.decompress
107
109
108 # max size of revlog with inline data
110 # max size of revlog with inline data
109 _maxinline = 131072
111 _maxinline = 131072
110 _chunksize = 1048576
112 _chunksize = 1048576
111
113
112 # Flag processors for REVIDX_ELLIPSIS.
114 # Flag processors for REVIDX_ELLIPSIS.
113 def ellipsisreadprocessor(rl, text):
115 def ellipsisreadprocessor(rl, text):
114 return text, False
116 return text, False
115
117
116 def ellipsiswriteprocessor(rl, text):
118 def ellipsiswriteprocessor(rl, text):
117 return text, False
119 return text, False
118
120
119 def ellipsisrawprocessor(rl, text):
121 def ellipsisrawprocessor(rl, text):
120 return False
122 return False
121
123
122 ellipsisprocessor = (
124 ellipsisprocessor = (
123 ellipsisreadprocessor,
125 ellipsisreadprocessor,
124 ellipsiswriteprocessor,
126 ellipsiswriteprocessor,
125 ellipsisrawprocessor,
127 ellipsisrawprocessor,
126 )
128 )
127
129
128 def getoffset(q):
130 def getoffset(q):
129 return int(q >> 16)
131 return int(q >> 16)
130
132
131 def gettype(q):
133 def gettype(q):
132 return int(q & 0xFFFF)
134 return int(q & 0xFFFF)
133
135
134 def offset_type(offset, type):
136 def offset_type(offset, type):
135 if (type & ~flagutil.REVIDX_KNOWN_FLAGS) != 0:
137 if (type & ~flagutil.REVIDX_KNOWN_FLAGS) != 0:
136 raise ValueError('unknown revlog index flags')
138 raise ValueError('unknown revlog index flags')
137 return int(int(offset) << 16 | type)
139 return int(int(offset) << 16 | type)
138
140
139 @attr.s(slots=True, frozen=True)
141 @attr.s(slots=True, frozen=True)
140 class _revisioninfo(object):
142 class _revisioninfo(object):
141 """Information about a revision that allows building its fulltext
143 """Information about a revision that allows building its fulltext
142 node: expected hash of the revision
144 node: expected hash of the revision
143 p1, p2: parent revs of the revision
145 p1, p2: parent revs of the revision
144 btext: built text cache consisting of a one-element list
146 btext: built text cache consisting of a one-element list
145 cachedelta: (baserev, uncompressed_delta) or None
147 cachedelta: (baserev, uncompressed_delta) or None
146 flags: flags associated to the revision storage
148 flags: flags associated to the revision storage
147
149
148 One of btext[0] or cachedelta must be set.
150 One of btext[0] or cachedelta must be set.
149 """
151 """
150 node = attr.ib()
152 node = attr.ib()
151 p1 = attr.ib()
153 p1 = attr.ib()
152 p2 = attr.ib()
154 p2 = attr.ib()
153 btext = attr.ib()
155 btext = attr.ib()
154 textlen = attr.ib()
156 textlen = attr.ib()
155 cachedelta = attr.ib()
157 cachedelta = attr.ib()
156 flags = attr.ib()
158 flags = attr.ib()
157
159
158 @interfaceutil.implementer(repository.irevisiondelta)
160 @interfaceutil.implementer(repository.irevisiondelta)
159 @attr.s(slots=True)
161 @attr.s(slots=True)
160 class revlogrevisiondelta(object):
162 class revlogrevisiondelta(object):
161 node = attr.ib()
163 node = attr.ib()
162 p1node = attr.ib()
164 p1node = attr.ib()
163 p2node = attr.ib()
165 p2node = attr.ib()
164 basenode = attr.ib()
166 basenode = attr.ib()
165 flags = attr.ib()
167 flags = attr.ib()
166 baserevisionsize = attr.ib()
168 baserevisionsize = attr.ib()
167 revision = attr.ib()
169 revision = attr.ib()
168 delta = attr.ib()
170 delta = attr.ib()
169 linknode = attr.ib(default=None)
171 linknode = attr.ib(default=None)
170
172
171 @interfaceutil.implementer(repository.iverifyproblem)
173 @interfaceutil.implementer(repository.iverifyproblem)
172 @attr.s(frozen=True)
174 @attr.s(frozen=True)
173 class revlogproblem(object):
175 class revlogproblem(object):
174 warning = attr.ib(default=None)
176 warning = attr.ib(default=None)
175 error = attr.ib(default=None)
177 error = attr.ib(default=None)
176 node = attr.ib(default=None)
178 node = attr.ib(default=None)
177
179
178 # index v0:
180 # index v0:
179 # 4 bytes: offset
181 # 4 bytes: offset
180 # 4 bytes: compressed length
182 # 4 bytes: compressed length
181 # 4 bytes: base rev
183 # 4 bytes: base rev
182 # 4 bytes: link rev
184 # 4 bytes: link rev
183 # 20 bytes: parent 1 nodeid
185 # 20 bytes: parent 1 nodeid
184 # 20 bytes: parent 2 nodeid
186 # 20 bytes: parent 2 nodeid
185 # 20 bytes: nodeid
187 # 20 bytes: nodeid
186 indexformatv0 = struct.Struct(">4l20s20s20s")
188 indexformatv0 = struct.Struct(">4l20s20s20s")
187 indexformatv0_pack = indexformatv0.pack
189 indexformatv0_pack = indexformatv0.pack
188 indexformatv0_unpack = indexformatv0.unpack
190 indexformatv0_unpack = indexformatv0.unpack
189
191
190 class revlogoldindex(list):
192 class revlogoldindex(list):
191 def __getitem__(self, i):
193 def __getitem__(self, i):
192 if i == -1:
194 if i == -1:
193 return (0, 0, 0, -1, -1, -1, -1, nullid)
195 return (0, 0, 0, -1, -1, -1, -1, nullid)
194 return list.__getitem__(self, i)
196 return list.__getitem__(self, i)
195
197
196 class revlogoldio(object):
198 class revlogoldio(object):
197 def __init__(self):
199 def __init__(self):
198 self.size = indexformatv0.size
200 self.size = indexformatv0.size
199
201
200 def parseindex(self, data, inline):
202 def parseindex(self, data, inline):
201 s = self.size
203 s = self.size
202 index = []
204 index = []
203 nodemap = {nullid: nullrev}
205 nodemap = {nullid: nullrev}
204 n = off = 0
206 n = off = 0
205 l = len(data)
207 l = len(data)
206 while off + s <= l:
208 while off + s <= l:
207 cur = data[off:off + s]
209 cur = data[off:off + s]
208 off += s
210 off += s
209 e = indexformatv0_unpack(cur)
211 e = indexformatv0_unpack(cur)
210 # transform to revlogv1 format
212 # transform to revlogv1 format
211 e2 = (offset_type(e[0], 0), e[1], -1, e[2], e[3],
213 e2 = (offset_type(e[0], 0), e[1], -1, e[2], e[3],
212 nodemap.get(e[4], nullrev), nodemap.get(e[5], nullrev), e[6])
214 nodemap.get(e[4], nullrev), nodemap.get(e[5], nullrev), e[6])
213 index.append(e2)
215 index.append(e2)
214 nodemap[e[6]] = n
216 nodemap[e[6]] = n
215 n += 1
217 n += 1
216
218
217 return revlogoldindex(index), nodemap, None
219 return revlogoldindex(index), nodemap, None
218
220
219 def packentry(self, entry, node, version, rev):
221 def packentry(self, entry, node, version, rev):
220 if gettype(entry[0]):
222 if gettype(entry[0]):
221 raise error.RevlogError(_('index entry flags need revlog '
223 raise error.RevlogError(_('index entry flags need revlog '
222 'version 1'))
224 'version 1'))
223 e2 = (getoffset(entry[0]), entry[1], entry[3], entry[4],
225 e2 = (getoffset(entry[0]), entry[1], entry[3], entry[4],
224 node(entry[5]), node(entry[6]), entry[7])
226 node(entry[5]), node(entry[6]), entry[7])
225 return indexformatv0_pack(*e2)
227 return indexformatv0_pack(*e2)
226
228
227 # index ng:
229 # index ng:
228 # 6 bytes: offset
230 # 6 bytes: offset
229 # 2 bytes: flags
231 # 2 bytes: flags
230 # 4 bytes: compressed length
232 # 4 bytes: compressed length
231 # 4 bytes: uncompressed length
233 # 4 bytes: uncompressed length
232 # 4 bytes: base rev
234 # 4 bytes: base rev
233 # 4 bytes: link rev
235 # 4 bytes: link rev
234 # 4 bytes: parent 1 rev
236 # 4 bytes: parent 1 rev
235 # 4 bytes: parent 2 rev
237 # 4 bytes: parent 2 rev
236 # 32 bytes: nodeid
238 # 32 bytes: nodeid
237 indexformatng = struct.Struct(">Qiiiiii20s12x")
239 indexformatng = struct.Struct(">Qiiiiii20s12x")
238 indexformatng_pack = indexformatng.pack
240 indexformatng_pack = indexformatng.pack
239 versionformat = struct.Struct(">I")
241 versionformat = struct.Struct(">I")
240 versionformat_pack = versionformat.pack
242 versionformat_pack = versionformat.pack
241 versionformat_unpack = versionformat.unpack
243 versionformat_unpack = versionformat.unpack
242
244
243 # corresponds to uncompressed length of indexformatng (2 gigs, 4-byte
245 # corresponds to uncompressed length of indexformatng (2 gigs, 4-byte
244 # signed integer)
246 # signed integer)
245 _maxentrysize = 0x7fffffff
247 _maxentrysize = 0x7fffffff
246
248
247 class revlogio(object):
249 class revlogio(object):
248 def __init__(self):
250 def __init__(self):
249 self.size = indexformatng.size
251 self.size = indexformatng.size
250
252
251 def parseindex(self, data, inline):
253 def parseindex(self, data, inline):
252 # call the C implementation to parse the index data
254 # call the C implementation to parse the index data
253 index, cache = parsers.parse_index2(data, inline)
255 index, cache = parsers.parse_index2(data, inline)
254 return index, getattr(index, 'nodemap', None), cache
256 return index, getattr(index, 'nodemap', None), cache
255
257
256 def packentry(self, entry, node, version, rev):
258 def packentry(self, entry, node, version, rev):
257 p = indexformatng_pack(*entry)
259 p = indexformatng_pack(*entry)
258 if rev == 0:
260 if rev == 0:
259 p = versionformat_pack(version) + p[4:]
261 p = versionformat_pack(version) + p[4:]
260 return p
262 return p
261
263
262 class revlog(object):
264 class revlog(object):
263 """
265 """
264 the underlying revision storage object
266 the underlying revision storage object
265
267
266 A revlog consists of two parts, an index and the revision data.
268 A revlog consists of two parts, an index and the revision data.
267
269
268 The index is a file with a fixed record size containing
270 The index is a file with a fixed record size containing
269 information on each revision, including its nodeid (hash), the
271 information on each revision, including its nodeid (hash), the
270 nodeids of its parents, the position and offset of its data within
272 nodeids of its parents, the position and offset of its data within
271 the data file, and the revision it's based on. Finally, each entry
273 the data file, and the revision it's based on. Finally, each entry
272 contains a linkrev entry that can serve as a pointer to external
274 contains a linkrev entry that can serve as a pointer to external
273 data.
275 data.
274
276
275 The revision data itself is a linear collection of data chunks.
277 The revision data itself is a linear collection of data chunks.
276 Each chunk represents a revision and is usually represented as a
278 Each chunk represents a revision and is usually represented as a
277 delta against the previous chunk. To bound lookup time, runs of
279 delta against the previous chunk. To bound lookup time, runs of
278 deltas are limited to about 2 times the length of the original
280 deltas are limited to about 2 times the length of the original
279 version data. This makes retrieval of a version proportional to
281 version data. This makes retrieval of a version proportional to
280 its size, or O(1) relative to the number of revisions.
282 its size, or O(1) relative to the number of revisions.
281
283
282 Both pieces of the revlog are written to in an append-only
284 Both pieces of the revlog are written to in an append-only
283 fashion, which means we never need to rewrite a file to insert or
285 fashion, which means we never need to rewrite a file to insert or
284 remove data, and can use some simple techniques to avoid the need
286 remove data, and can use some simple techniques to avoid the need
285 for locking while reading.
287 for locking while reading.
286
288
287 If checkambig, indexfile is opened with checkambig=True at
289 If checkambig, indexfile is opened with checkambig=True at
288 writing, to avoid file stat ambiguity.
290 writing, to avoid file stat ambiguity.
289
291
290 If mmaplargeindex is True, and an mmapindexthreshold is set, the
292 If mmaplargeindex is True, and an mmapindexthreshold is set, the
291 index will be mmapped rather than read if it is larger than the
293 index will be mmapped rather than read if it is larger than the
292 configured threshold.
294 configured threshold.
293
295
294 If censorable is True, the revlog can have censored revisions.
296 If censorable is True, the revlog can have censored revisions.
295
297
296 If `upperboundcomp` is not None, this is the expected maximal gain from
298 If `upperboundcomp` is not None, this is the expected maximal gain from
297 compression for the data content.
299 compression for the data content.
298 """
300 """
299 def __init__(self, opener, indexfile, datafile=None, checkambig=False,
301 def __init__(self, opener, indexfile, datafile=None, checkambig=False,
300 mmaplargeindex=False, censorable=False,
302 mmaplargeindex=False, censorable=False,
301 upperboundcomp=None):
303 upperboundcomp=None):
302 """
304 """
303 create a revlog object
305 create a revlog object
304
306
305 opener is a function that abstracts the file opening operation
307 opener is a function that abstracts the file opening operation
306 and can be used to implement COW semantics or the like.
308 and can be used to implement COW semantics or the like.
307
309
308 """
310 """
309 self.upperboundcomp = upperboundcomp
311 self.upperboundcomp = upperboundcomp
310 self.indexfile = indexfile
312 self.indexfile = indexfile
311 self.datafile = datafile or (indexfile[:-2] + ".d")
313 self.datafile = datafile or (indexfile[:-2] + ".d")
312 self.opener = opener
314 self.opener = opener
313 # When True, indexfile is opened with checkambig=True at writing, to
315 # When True, indexfile is opened with checkambig=True at writing, to
314 # avoid file stat ambiguity.
316 # avoid file stat ambiguity.
315 self._checkambig = checkambig
317 self._checkambig = checkambig
316 self._mmaplargeindex = mmaplargeindex
318 self._mmaplargeindex = mmaplargeindex
317 self._censorable = censorable
319 self._censorable = censorable
318 # 3-tuple of (node, rev, text) for a raw revision.
320 # 3-tuple of (node, rev, text) for a raw revision.
319 self._revisioncache = None
321 self._revisioncache = None
320 # Maps rev to chain base rev.
322 # Maps rev to chain base rev.
321 self._chainbasecache = util.lrucachedict(100)
323 self._chainbasecache = util.lrucachedict(100)
322 # 2-tuple of (offset, data) of raw data from the revlog at an offset.
324 # 2-tuple of (offset, data) of raw data from the revlog at an offset.
323 self._chunkcache = (0, '')
325 self._chunkcache = (0, '')
324 # How much data to read and cache into the raw revlog data cache.
326 # How much data to read and cache into the raw revlog data cache.
325 self._chunkcachesize = 65536
327 self._chunkcachesize = 65536
326 self._maxchainlen = None
328 self._maxchainlen = None
327 self._deltabothparents = True
329 self._deltabothparents = True
328 self.index = []
330 self.index = []
329 # Mapping of partial identifiers to full nodes.
331 # Mapping of partial identifiers to full nodes.
330 self._pcache = {}
332 self._pcache = {}
331 # Mapping of revision integer to full node.
333 # Mapping of revision integer to full node.
332 self._nodecache = {nullid: nullrev}
334 self._nodecache = {nullid: nullrev}
333 self._nodepos = None
335 self._nodepos = None
334 self._compengine = 'zlib'
336 self._compengine = 'zlib'
335 self._compengineopts = {}
337 self._compengineopts = {}
336 self._maxdeltachainspan = -1
338 self._maxdeltachainspan = -1
337 self._withsparseread = False
339 self._withsparseread = False
338 self._sparserevlog = False
340 self._sparserevlog = False
339 self._srdensitythreshold = 0.50
341 self._srdensitythreshold = 0.50
340 self._srmingapsize = 262144
342 self._srmingapsize = 262144
341
343
342 # Make copy of flag processors so each revlog instance can support
344 # Make copy of flag processors so each revlog instance can support
343 # custom flags.
345 # custom flags.
344 self._flagprocessors = dict(flagutil.flagprocessors)
346 self._flagprocessors = dict(flagutil.flagprocessors)
345
347
346 # 2-tuple of file handles being used for active writing.
348 # 2-tuple of file handles being used for active writing.
347 self._writinghandles = None
349 self._writinghandles = None
348
350
349 self._loadindex()
351 self._loadindex()
350
352
351 def _loadindex(self):
353 def _loadindex(self):
352 mmapindexthreshold = None
354 mmapindexthreshold = None
353 opts = getattr(self.opener, 'options', {}) or {}
355 opts = getattr(self.opener, 'options', {}) or {}
354
356
355 if 'revlogv2' in opts:
357 if 'revlogv2' in opts:
356 newversionflags = REVLOGV2 | FLAG_INLINE_DATA
358 newversionflags = REVLOGV2 | FLAG_INLINE_DATA
357 elif 'revlogv1' in opts:
359 elif 'revlogv1' in opts:
358 newversionflags = REVLOGV1 | FLAG_INLINE_DATA
360 newversionflags = REVLOGV1 | FLAG_INLINE_DATA
359 if 'generaldelta' in opts:
361 if 'generaldelta' in opts:
360 newversionflags |= FLAG_GENERALDELTA
362 newversionflags |= FLAG_GENERALDELTA
361 elif getattr(self.opener, 'options', None) is not None:
363 elif getattr(self.opener, 'options', None) is not None:
362 # If options provided but no 'revlog*' found, the repository
364 # If options provided but no 'revlog*' found, the repository
363 # would have no 'requires' file in it, which means we have to
365 # would have no 'requires' file in it, which means we have to
364 # stick to the old format.
366 # stick to the old format.
365 newversionflags = REVLOGV0
367 newversionflags = REVLOGV0
366 else:
368 else:
367 newversionflags = REVLOG_DEFAULT_VERSION
369 newversionflags = REVLOG_DEFAULT_VERSION
368
370
369 if 'chunkcachesize' in opts:
371 if 'chunkcachesize' in opts:
370 self._chunkcachesize = opts['chunkcachesize']
372 self._chunkcachesize = opts['chunkcachesize']
371 if 'maxchainlen' in opts:
373 if 'maxchainlen' in opts:
372 self._maxchainlen = opts['maxchainlen']
374 self._maxchainlen = opts['maxchainlen']
373 if 'deltabothparents' in opts:
375 if 'deltabothparents' in opts:
374 self._deltabothparents = opts['deltabothparents']
376 self._deltabothparents = opts['deltabothparents']
375 self._lazydelta = bool(opts.get('lazydelta', True))
377 self._lazydelta = bool(opts.get('lazydelta', True))
376 self._lazydeltabase = False
378 self._lazydeltabase = False
377 if self._lazydelta:
379 if self._lazydelta:
378 self._lazydeltabase = bool(opts.get('lazydeltabase', False))
380 self._lazydeltabase = bool(opts.get('lazydeltabase', False))
379 if 'compengine' in opts:
381 if 'compengine' in opts:
380 self._compengine = opts['compengine']
382 self._compengine = opts['compengine']
381 if 'zlib.level' in opts:
383 if 'zlib.level' in opts:
382 self._compengineopts['zlib.level'] = opts['zlib.level']
384 self._compengineopts['zlib.level'] = opts['zlib.level']
383 if 'zstd.level' in opts:
385 if 'zstd.level' in opts:
384 self._compengineopts['zstd.level'] = opts['zstd.level']
386 self._compengineopts['zstd.level'] = opts['zstd.level']
385 if 'maxdeltachainspan' in opts:
387 if 'maxdeltachainspan' in opts:
386 self._maxdeltachainspan = opts['maxdeltachainspan']
388 self._maxdeltachainspan = opts['maxdeltachainspan']
387 if self._mmaplargeindex and 'mmapindexthreshold' in opts:
389 if self._mmaplargeindex and 'mmapindexthreshold' in opts:
388 mmapindexthreshold = opts['mmapindexthreshold']
390 mmapindexthreshold = opts['mmapindexthreshold']
389 self._sparserevlog = bool(opts.get('sparse-revlog', False))
391 self._sparserevlog = bool(opts.get('sparse-revlog', False))
390 withsparseread = bool(opts.get('with-sparse-read', False))
392 withsparseread = bool(opts.get('with-sparse-read', False))
391 # sparse-revlog forces sparse-read
393 # sparse-revlog forces sparse-read
392 self._withsparseread = self._sparserevlog or withsparseread
394 self._withsparseread = self._sparserevlog or withsparseread
393 if 'sparse-read-density-threshold' in opts:
395 if 'sparse-read-density-threshold' in opts:
394 self._srdensitythreshold = opts['sparse-read-density-threshold']
396 self._srdensitythreshold = opts['sparse-read-density-threshold']
395 if 'sparse-read-min-gap-size' in opts:
397 if 'sparse-read-min-gap-size' in opts:
396 self._srmingapsize = opts['sparse-read-min-gap-size']
398 self._srmingapsize = opts['sparse-read-min-gap-size']
397 if opts.get('enableellipsis'):
399 if opts.get('enableellipsis'):
398 self._flagprocessors[REVIDX_ELLIPSIS] = ellipsisprocessor
400 self._flagprocessors[REVIDX_ELLIPSIS] = ellipsisprocessor
399
401
400 # revlog v0 doesn't have flag processors
402 # revlog v0 doesn't have flag processors
401 for flag, processor in opts.get(b'flagprocessors', {}).iteritems():
403 for flag, processor in opts.get(b'flagprocessors', {}).iteritems():
402 flagutil.insertflagprocessor(flag, processor, self._flagprocessors)
404 flagutil.insertflagprocessor(flag, processor, self._flagprocessors)
403
405
404 if self._chunkcachesize <= 0:
406 if self._chunkcachesize <= 0:
405 raise error.RevlogError(_('revlog chunk cache size %r is not '
407 raise error.RevlogError(_('revlog chunk cache size %r is not '
406 'greater than 0') % self._chunkcachesize)
408 'greater than 0') % self._chunkcachesize)
407 elif self._chunkcachesize & (self._chunkcachesize - 1):
409 elif self._chunkcachesize & (self._chunkcachesize - 1):
408 raise error.RevlogError(_('revlog chunk cache size %r is not a '
410 raise error.RevlogError(_('revlog chunk cache size %r is not a '
409 'power of 2') % self._chunkcachesize)
411 'power of 2') % self._chunkcachesize)
410
412
411 indexdata = ''
413 indexdata = ''
412 self._initempty = True
414 self._initempty = True
413 try:
415 try:
414 with self._indexfp() as f:
416 with self._indexfp() as f:
415 if (mmapindexthreshold is not None and
417 if (mmapindexthreshold is not None and
416 self.opener.fstat(f).st_size >= mmapindexthreshold):
418 self.opener.fstat(f).st_size >= mmapindexthreshold):
417 # TODO: should .close() to release resources without
419 # TODO: should .close() to release resources without
418 # relying on Python GC
420 # relying on Python GC
419 indexdata = util.buffer(util.mmapread(f))
421 indexdata = util.buffer(util.mmapread(f))
420 else:
422 else:
421 indexdata = f.read()
423 indexdata = f.read()
422 if len(indexdata) > 0:
424 if len(indexdata) > 0:
423 versionflags = versionformat_unpack(indexdata[:4])[0]
425 versionflags = versionformat_unpack(indexdata[:4])[0]
424 self._initempty = False
426 self._initempty = False
425 else:
427 else:
426 versionflags = newversionflags
428 versionflags = newversionflags
427 except IOError as inst:
429 except IOError as inst:
428 if inst.errno != errno.ENOENT:
430 if inst.errno != errno.ENOENT:
429 raise
431 raise
430
432
431 versionflags = newversionflags
433 versionflags = newversionflags
432
434
433 self.version = versionflags
435 self.version = versionflags
434
436
435 flags = versionflags & ~0xFFFF
437 flags = versionflags & ~0xFFFF
436 fmt = versionflags & 0xFFFF
438 fmt = versionflags & 0xFFFF
437
439
438 if fmt == REVLOGV0:
440 if fmt == REVLOGV0:
439 if flags:
441 if flags:
440 raise error.RevlogError(_('unknown flags (%#04x) in version %d '
442 raise error.RevlogError(_('unknown flags (%#04x) in version %d '
441 'revlog %s') %
443 'revlog %s') %
442 (flags >> 16, fmt, self.indexfile))
444 (flags >> 16, fmt, self.indexfile))
443
445
444 self._inline = False
446 self._inline = False
445 self._generaldelta = False
447 self._generaldelta = False
446
448
447 elif fmt == REVLOGV1:
449 elif fmt == REVLOGV1:
448 if flags & ~REVLOGV1_FLAGS:
450 if flags & ~REVLOGV1_FLAGS:
449 raise error.RevlogError(_('unknown flags (%#04x) in version %d '
451 raise error.RevlogError(_('unknown flags (%#04x) in version %d '
450 'revlog %s') %
452 'revlog %s') %
451 (flags >> 16, fmt, self.indexfile))
453 (flags >> 16, fmt, self.indexfile))
452
454
453 self._inline = versionflags & FLAG_INLINE_DATA
455 self._inline = versionflags & FLAG_INLINE_DATA
454 self._generaldelta = versionflags & FLAG_GENERALDELTA
456 self._generaldelta = versionflags & FLAG_GENERALDELTA
455
457
456 elif fmt == REVLOGV2:
458 elif fmt == REVLOGV2:
457 if flags & ~REVLOGV2_FLAGS:
459 if flags & ~REVLOGV2_FLAGS:
458 raise error.RevlogError(_('unknown flags (%#04x) in version %d '
460 raise error.RevlogError(_('unknown flags (%#04x) in version %d '
459 'revlog %s') %
461 'revlog %s') %
460 (flags >> 16, fmt, self.indexfile))
462 (flags >> 16, fmt, self.indexfile))
461
463
462 self._inline = versionflags & FLAG_INLINE_DATA
464 self._inline = versionflags & FLAG_INLINE_DATA
463 # generaldelta implied by version 2 revlogs.
465 # generaldelta implied by version 2 revlogs.
464 self._generaldelta = True
466 self._generaldelta = True
465
467
466 else:
468 else:
467 raise error.RevlogError(_('unknown version (%d) in revlog %s') %
469 raise error.RevlogError(_('unknown version (%d) in revlog %s') %
468 (fmt, self.indexfile))
470 (fmt, self.indexfile))
469 # sparse-revlog can't be on without general-delta (issue6056)
471 # sparse-revlog can't be on without general-delta (issue6056)
470 if not self._generaldelta:
472 if not self._generaldelta:
471 self._sparserevlog = False
473 self._sparserevlog = False
472
474
473 self._storedeltachains = True
475 self._storedeltachains = True
474
476
475 self._io = revlogio()
477 self._io = revlogio()
476 if self.version == REVLOGV0:
478 if self.version == REVLOGV0:
477 self._io = revlogoldio()
479 self._io = revlogoldio()
478 try:
480 try:
479 d = self._io.parseindex(indexdata, self._inline)
481 d = self._io.parseindex(indexdata, self._inline)
480 except (ValueError, IndexError):
482 except (ValueError, IndexError):
481 raise error.RevlogError(_("index %s is corrupted") %
483 raise error.RevlogError(_("index %s is corrupted") %
482 self.indexfile)
484 self.indexfile)
483 self.index, nodemap, self._chunkcache = d
485 self.index, nodemap, self._chunkcache = d
484 if nodemap is not None:
486 if nodemap is not None:
485 self.nodemap = self._nodecache = nodemap
487 self.nodemap = self._nodecache = nodemap
486 if not self._chunkcache:
488 if not self._chunkcache:
487 self._chunkclear()
489 self._chunkclear()
488 # revnum -> (chain-length, sum-delta-length)
490 # revnum -> (chain-length, sum-delta-length)
489 self._chaininfocache = {}
491 self._chaininfocache = {}
490 # revlog header -> revlog compressor
492 # revlog header -> revlog compressor
491 self._decompressors = {}
493 self._decompressors = {}
492
494
493 @util.propertycache
495 @util.propertycache
494 def _compressor(self):
496 def _compressor(self):
495 engine = util.compengines[self._compengine]
497 engine = util.compengines[self._compengine]
496 return engine.revlogcompressor(self._compengineopts)
498 return engine.revlogcompressor(self._compengineopts)
497
499
498 def _indexfp(self, mode='r'):
500 def _indexfp(self, mode='r'):
499 """file object for the revlog's index file"""
501 """file object for the revlog's index file"""
500 args = {r'mode': mode}
502 args = {r'mode': mode}
501 if mode != 'r':
503 if mode != 'r':
502 args[r'checkambig'] = self._checkambig
504 args[r'checkambig'] = self._checkambig
503 if mode == 'w':
505 if mode == 'w':
504 args[r'atomictemp'] = True
506 args[r'atomictemp'] = True
505 return self.opener(self.indexfile, **args)
507 return self.opener(self.indexfile, **args)
506
508
507 def _datafp(self, mode='r'):
509 def _datafp(self, mode='r'):
508 """file object for the revlog's data file"""
510 """file object for the revlog's data file"""
509 return self.opener(self.datafile, mode=mode)
511 return self.opener(self.datafile, mode=mode)
510
512
511 @contextlib.contextmanager
513 @contextlib.contextmanager
512 def _datareadfp(self, existingfp=None):
514 def _datareadfp(self, existingfp=None):
513 """file object suitable to read data"""
515 """file object suitable to read data"""
514 # Use explicit file handle, if given.
516 # Use explicit file handle, if given.
515 if existingfp is not None:
517 if existingfp is not None:
516 yield existingfp
518 yield existingfp
517
519
518 # Use a file handle being actively used for writes, if available.
520 # Use a file handle being actively used for writes, if available.
519 # There is some danger to doing this because reads will seek the
521 # There is some danger to doing this because reads will seek the
520 # file. However, _writeentry() performs a SEEK_END before all writes,
522 # file. However, _writeentry() performs a SEEK_END before all writes,
521 # so we should be safe.
523 # so we should be safe.
522 elif self._writinghandles:
524 elif self._writinghandles:
523 if self._inline:
525 if self._inline:
524 yield self._writinghandles[0]
526 yield self._writinghandles[0]
525 else:
527 else:
526 yield self._writinghandles[1]
528 yield self._writinghandles[1]
527
529
528 # Otherwise open a new file handle.
530 # Otherwise open a new file handle.
529 else:
531 else:
530 if self._inline:
532 if self._inline:
531 func = self._indexfp
533 func = self._indexfp
532 else:
534 else:
533 func = self._datafp
535 func = self._datafp
534 with func() as fp:
536 with func() as fp:
535 yield fp
537 yield fp
536
538
537 def tip(self):
539 def tip(self):
538 return self.node(len(self.index) - 1)
540 return self.node(len(self.index) - 1)
539 def __contains__(self, rev):
541 def __contains__(self, rev):
540 return 0 <= rev < len(self)
542 return 0 <= rev < len(self)
541 def __len__(self):
543 def __len__(self):
542 return len(self.index)
544 return len(self.index)
543 def __iter__(self):
545 def __iter__(self):
544 return iter(pycompat.xrange(len(self)))
546 return iter(pycompat.xrange(len(self)))
545 def revs(self, start=0, stop=None):
547 def revs(self, start=0, stop=None):
546 """iterate over all rev in this revlog (from start to stop)"""
548 """iterate over all rev in this revlog (from start to stop)"""
547 return storageutil.iterrevs(len(self), start=start, stop=stop)
549 return storageutil.iterrevs(len(self), start=start, stop=stop)
548
550
549 @util.propertycache
551 @util.propertycache
550 def nodemap(self):
552 def nodemap(self):
551 if self.index:
553 if self.index:
552 # populate mapping down to the initial node
554 # populate mapping down to the initial node
553 node0 = self.index[0][7] # get around changelog filtering
555 node0 = self.index[0][7] # get around changelog filtering
554 self.rev(node0)
556 self.rev(node0)
555 return self._nodecache
557 return self._nodecache
556
558
557 def hasnode(self, node):
559 def hasnode(self, node):
558 try:
560 try:
559 self.rev(node)
561 self.rev(node)
560 return True
562 return True
561 except KeyError:
563 except KeyError:
562 return False
564 return False
563
565
564 def candelta(self, baserev, rev):
566 def candelta(self, baserev, rev):
565 """whether two revisions (baserev, rev) can be delta-ed or not"""
567 """whether two revisions (baserev, rev) can be delta-ed or not"""
566 # Disable delta if either rev requires a content-changing flag
568 # Disable delta if either rev requires a content-changing flag
567 # processor (ex. LFS). This is because such flag processor can alter
569 # processor (ex. LFS). This is because such flag processor can alter
568 # the rawtext content that the delta will be based on, and two clients
570 # the rawtext content that the delta will be based on, and two clients
569 # could have a same revlog node with different flags (i.e. different
571 # could have a same revlog node with different flags (i.e. different
570 # rawtext contents) and the delta could be incompatible.
572 # rawtext contents) and the delta could be incompatible.
571 if ((self.flags(baserev) & REVIDX_RAWTEXT_CHANGING_FLAGS)
573 if ((self.flags(baserev) & REVIDX_RAWTEXT_CHANGING_FLAGS)
572 or (self.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS)):
574 or (self.flags(rev) & REVIDX_RAWTEXT_CHANGING_FLAGS)):
573 return False
575 return False
574 return True
576 return True
575
577
576 def clearcaches(self):
578 def clearcaches(self):
577 self._revisioncache = None
579 self._revisioncache = None
578 self._chainbasecache.clear()
580 self._chainbasecache.clear()
579 self._chunkcache = (0, '')
581 self._chunkcache = (0, '')
580 self._pcache = {}
582 self._pcache = {}
581
583
582 try:
584 try:
583 # If we are using the native C version, you are in a fun case
585 # If we are using the native C version, you are in a fun case
584 # where self.index, self.nodemap and self._nodecaches is the same
586 # where self.index, self.nodemap and self._nodecaches is the same
585 # object.
587 # object.
586 self._nodecache.clearcaches()
588 self._nodecache.clearcaches()
587 except AttributeError:
589 except AttributeError:
588 self._nodecache = {nullid: nullrev}
590 self._nodecache = {nullid: nullrev}
589 self._nodepos = None
591 self._nodepos = None
590
592
591 def rev(self, node):
593 def rev(self, node):
592 try:
594 try:
593 return self._nodecache[node]
595 return self._nodecache[node]
594 except TypeError:
596 except TypeError:
595 raise
597 raise
596 except error.RevlogError:
598 except error.RevlogError:
597 # parsers.c radix tree lookup failed
599 # parsers.c radix tree lookup failed
598 if node == wdirid or node in wdirfilenodeids:
600 if node == wdirid or node in wdirfilenodeids:
599 raise error.WdirUnsupported
601 raise error.WdirUnsupported
600 raise error.LookupError(node, self.indexfile, _('no node'))
602 raise error.LookupError(node, self.indexfile, _('no node'))
601 except KeyError:
603 except KeyError:
602 # pure python cache lookup failed
604 # pure python cache lookup failed
603 n = self._nodecache
605 n = self._nodecache
604 i = self.index
606 i = self.index
605 p = self._nodepos
607 p = self._nodepos
606 if p is None:
608 if p is None:
607 p = len(i) - 1
609 p = len(i) - 1
608 else:
610 else:
609 assert p < len(i)
611 assert p < len(i)
610 for r in pycompat.xrange(p, -1, -1):
612 for r in pycompat.xrange(p, -1, -1):
611 v = i[r][7]
613 v = i[r][7]
612 n[v] = r
614 n[v] = r
613 if v == node:
615 if v == node:
614 self._nodepos = r - 1
616 self._nodepos = r - 1
615 return r
617 return r
616 if node == wdirid or node in wdirfilenodeids:
618 if node == wdirid or node in wdirfilenodeids:
617 raise error.WdirUnsupported
619 raise error.WdirUnsupported
618 raise error.LookupError(node, self.indexfile, _('no node'))
620 raise error.LookupError(node, self.indexfile, _('no node'))
619
621
620 # Accessors for index entries.
622 # Accessors for index entries.
621
623
622 # First tuple entry is 8 bytes. First 6 bytes are offset. Last 2 bytes
624 # First tuple entry is 8 bytes. First 6 bytes are offset. Last 2 bytes
623 # are flags.
625 # are flags.
624 def start(self, rev):
626 def start(self, rev):
625 return int(self.index[rev][0] >> 16)
627 return int(self.index[rev][0] >> 16)
626
628
627 def flags(self, rev):
629 def flags(self, rev):
628 return self.index[rev][0] & 0xFFFF
630 return self.index[rev][0] & 0xFFFF
629
631
630 def length(self, rev):
632 def length(self, rev):
631 return self.index[rev][1]
633 return self.index[rev][1]
632
634
633 def rawsize(self, rev):
635 def rawsize(self, rev):
634 """return the length of the uncompressed text for a given revision"""
636 """return the length of the uncompressed text for a given revision"""
635 l = self.index[rev][2]
637 l = self.index[rev][2]
636 if l >= 0:
638 if l >= 0:
637 return l
639 return l
638
640
639 t = self.rawdata(rev)
641 t = self.rawdata(rev)
640 return len(t)
642 return len(t)
641
643
642 def size(self, rev):
644 def size(self, rev):
643 """length of non-raw text (processed by a "read" flag processor)"""
645 """length of non-raw text (processed by a "read" flag processor)"""
644 # fast path: if no "read" flag processor could change the content,
646 # fast path: if no "read" flag processor could change the content,
645 # size is rawsize. note: ELLIPSIS is known to not change the content.
647 # size is rawsize. note: ELLIPSIS is known to not change the content.
646 flags = self.flags(rev)
648 flags = self.flags(rev)
647 if flags & (flagutil.REVIDX_KNOWN_FLAGS ^ REVIDX_ELLIPSIS) == 0:
649 if flags & (flagutil.REVIDX_KNOWN_FLAGS ^ REVIDX_ELLIPSIS) == 0:
648 return self.rawsize(rev)
650 return self.rawsize(rev)
649
651
650 return len(self.revision(rev, raw=False))
652 return len(self.revision(rev, raw=False))
651
653
652 def chainbase(self, rev):
654 def chainbase(self, rev):
653 base = self._chainbasecache.get(rev)
655 base = self._chainbasecache.get(rev)
654 if base is not None:
656 if base is not None:
655 return base
657 return base
656
658
657 index = self.index
659 index = self.index
658 iterrev = rev
660 iterrev = rev
659 base = index[iterrev][3]
661 base = index[iterrev][3]
660 while base != iterrev:
662 while base != iterrev:
661 iterrev = base
663 iterrev = base
662 base = index[iterrev][3]
664 base = index[iterrev][3]
663
665
664 self._chainbasecache[rev] = base
666 self._chainbasecache[rev] = base
665 return base
667 return base
666
668
667 def linkrev(self, rev):
669 def linkrev(self, rev):
668 return self.index[rev][4]
670 return self.index[rev][4]
669
671
670 def parentrevs(self, rev):
672 def parentrevs(self, rev):
671 try:
673 try:
672 entry = self.index[rev]
674 entry = self.index[rev]
673 except IndexError:
675 except IndexError:
674 if rev == wdirrev:
676 if rev == wdirrev:
675 raise error.WdirUnsupported
677 raise error.WdirUnsupported
676 raise
678 raise
677
679
678 return entry[5], entry[6]
680 return entry[5], entry[6]
679
681
680 # fast parentrevs(rev) where rev isn't filtered
682 # fast parentrevs(rev) where rev isn't filtered
681 _uncheckedparentrevs = parentrevs
683 _uncheckedparentrevs = parentrevs
682
684
683 def node(self, rev):
685 def node(self, rev):
684 try:
686 try:
685 return self.index[rev][7]
687 return self.index[rev][7]
686 except IndexError:
688 except IndexError:
687 if rev == wdirrev:
689 if rev == wdirrev:
688 raise error.WdirUnsupported
690 raise error.WdirUnsupported
689 raise
691 raise
690
692
691 # Derived from index values.
693 # Derived from index values.
692
694
693 def end(self, rev):
695 def end(self, rev):
694 return self.start(rev) + self.length(rev)
696 return self.start(rev) + self.length(rev)
695
697
696 def parents(self, node):
698 def parents(self, node):
697 i = self.index
699 i = self.index
698 d = i[self.rev(node)]
700 d = i[self.rev(node)]
699 return i[d[5]][7], i[d[6]][7] # map revisions to nodes inline
701 return i[d[5]][7], i[d[6]][7] # map revisions to nodes inline
700
702
701 def chainlen(self, rev):
703 def chainlen(self, rev):
702 return self._chaininfo(rev)[0]
704 return self._chaininfo(rev)[0]
703
705
704 def _chaininfo(self, rev):
706 def _chaininfo(self, rev):
705 chaininfocache = self._chaininfocache
707 chaininfocache = self._chaininfocache
706 if rev in chaininfocache:
708 if rev in chaininfocache:
707 return chaininfocache[rev]
709 return chaininfocache[rev]
708 index = self.index
710 index = self.index
709 generaldelta = self._generaldelta
711 generaldelta = self._generaldelta
710 iterrev = rev
712 iterrev = rev
711 e = index[iterrev]
713 e = index[iterrev]
712 clen = 0
714 clen = 0
713 compresseddeltalen = 0
715 compresseddeltalen = 0
714 while iterrev != e[3]:
716 while iterrev != e[3]:
715 clen += 1
717 clen += 1
716 compresseddeltalen += e[1]
718 compresseddeltalen += e[1]
717 if generaldelta:
719 if generaldelta:
718 iterrev = e[3]
720 iterrev = e[3]
719 else:
721 else:
720 iterrev -= 1
722 iterrev -= 1
721 if iterrev in chaininfocache:
723 if iterrev in chaininfocache:
722 t = chaininfocache[iterrev]
724 t = chaininfocache[iterrev]
723 clen += t[0]
725 clen += t[0]
724 compresseddeltalen += t[1]
726 compresseddeltalen += t[1]
725 break
727 break
726 e = index[iterrev]
728 e = index[iterrev]
727 else:
729 else:
728 # Add text length of base since decompressing that also takes
730 # Add text length of base since decompressing that also takes
729 # work. For cache hits the length is already included.
731 # work. For cache hits the length is already included.
730 compresseddeltalen += e[1]
732 compresseddeltalen += e[1]
731 r = (clen, compresseddeltalen)
733 r = (clen, compresseddeltalen)
732 chaininfocache[rev] = r
734 chaininfocache[rev] = r
733 return r
735 return r
734
736
735 def _deltachain(self, rev, stoprev=None):
737 def _deltachain(self, rev, stoprev=None):
736 """Obtain the delta chain for a revision.
738 """Obtain the delta chain for a revision.
737
739
738 ``stoprev`` specifies a revision to stop at. If not specified, we
740 ``stoprev`` specifies a revision to stop at. If not specified, we
739 stop at the base of the chain.
741 stop at the base of the chain.
740
742
741 Returns a 2-tuple of (chain, stopped) where ``chain`` is a list of
743 Returns a 2-tuple of (chain, stopped) where ``chain`` is a list of
742 revs in ascending order and ``stopped`` is a bool indicating whether
744 revs in ascending order and ``stopped`` is a bool indicating whether
743 ``stoprev`` was hit.
745 ``stoprev`` was hit.
744 """
746 """
745 # Try C implementation.
747 # Try C implementation.
746 try:
748 try:
747 return self.index.deltachain(rev, stoprev, self._generaldelta)
749 return self.index.deltachain(rev, stoprev, self._generaldelta)
748 except AttributeError:
750 except AttributeError:
749 pass
751 pass
750
752
751 chain = []
753 chain = []
752
754
753 # Alias to prevent attribute lookup in tight loop.
755 # Alias to prevent attribute lookup in tight loop.
754 index = self.index
756 index = self.index
755 generaldelta = self._generaldelta
757 generaldelta = self._generaldelta
756
758
757 iterrev = rev
759 iterrev = rev
758 e = index[iterrev]
760 e = index[iterrev]
759 while iterrev != e[3] and iterrev != stoprev:
761 while iterrev != e[3] and iterrev != stoprev:
760 chain.append(iterrev)
762 chain.append(iterrev)
761 if generaldelta:
763 if generaldelta:
762 iterrev = e[3]
764 iterrev = e[3]
763 else:
765 else:
764 iterrev -= 1
766 iterrev -= 1
765 e = index[iterrev]
767 e = index[iterrev]
766
768
767 if iterrev == stoprev:
769 if iterrev == stoprev:
768 stopped = True
770 stopped = True
769 else:
771 else:
770 chain.append(iterrev)
772 chain.append(iterrev)
771 stopped = False
773 stopped = False
772
774
773 chain.reverse()
775 chain.reverse()
774 return chain, stopped
776 return chain, stopped
775
777
776 def ancestors(self, revs, stoprev=0, inclusive=False):
778 def ancestors(self, revs, stoprev=0, inclusive=False):
777 """Generate the ancestors of 'revs' in reverse revision order.
779 """Generate the ancestors of 'revs' in reverse revision order.
778 Does not generate revs lower than stoprev.
780 Does not generate revs lower than stoprev.
779
781
780 See the documentation for ancestor.lazyancestors for more details."""
782 See the documentation for ancestor.lazyancestors for more details."""
781
783
782 # first, make sure start revisions aren't filtered
784 # first, make sure start revisions aren't filtered
783 revs = list(revs)
785 revs = list(revs)
784 checkrev = self.node
786 checkrev = self.node
785 for r in revs:
787 for r in revs:
786 checkrev(r)
788 checkrev(r)
787 # and we're sure ancestors aren't filtered as well
789 # and we're sure ancestors aren't filtered as well
788
790
789 if rustancestor is not None:
791 if rustancestor is not None:
790 lazyancestors = rustancestor.LazyAncestors
792 lazyancestors = rustancestor.LazyAncestors
791 arg = self.index
793 arg = self.index
792 elif util.safehasattr(parsers, 'rustlazyancestors'):
794 elif util.safehasattr(parsers, 'rustlazyancestors'):
793 lazyancestors = ancestor.rustlazyancestors
795 lazyancestors = ancestor.rustlazyancestors
794 arg = self.index
796 arg = self.index
795 else:
797 else:
796 lazyancestors = ancestor.lazyancestors
798 lazyancestors = ancestor.lazyancestors
797 arg = self._uncheckedparentrevs
799 arg = self._uncheckedparentrevs
798 return lazyancestors(arg, revs, stoprev=stoprev, inclusive=inclusive)
800 return lazyancestors(arg, revs, stoprev=stoprev, inclusive=inclusive)
799
801
800 def descendants(self, revs):
802 def descendants(self, revs):
801 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
803 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
802
804
803 def findcommonmissing(self, common=None, heads=None):
805 def findcommonmissing(self, common=None, heads=None):
804 """Return a tuple of the ancestors of common and the ancestors of heads
806 """Return a tuple of the ancestors of common and the ancestors of heads
805 that are not ancestors of common. In revset terminology, we return the
807 that are not ancestors of common. In revset terminology, we return the
806 tuple:
808 tuple:
807
809
808 ::common, (::heads) - (::common)
810 ::common, (::heads) - (::common)
809
811
810 The list is sorted by revision number, meaning it is
812 The list is sorted by revision number, meaning it is
811 topologically sorted.
813 topologically sorted.
812
814
813 'heads' and 'common' are both lists of node IDs. If heads is
815 'heads' and 'common' are both lists of node IDs. If heads is
814 not supplied, uses all of the revlog's heads. If common is not
816 not supplied, uses all of the revlog's heads. If common is not
815 supplied, uses nullid."""
817 supplied, uses nullid."""
816 if common is None:
818 if common is None:
817 common = [nullid]
819 common = [nullid]
818 if heads is None:
820 if heads is None:
819 heads = self.heads()
821 heads = self.heads()
820
822
821 common = [self.rev(n) for n in common]
823 common = [self.rev(n) for n in common]
822 heads = [self.rev(n) for n in heads]
824 heads = [self.rev(n) for n in heads]
823
825
824 # we want the ancestors, but inclusive
826 # we want the ancestors, but inclusive
825 class lazyset(object):
827 class lazyset(object):
826 def __init__(self, lazyvalues):
828 def __init__(self, lazyvalues):
827 self.addedvalues = set()
829 self.addedvalues = set()
828 self.lazyvalues = lazyvalues
830 self.lazyvalues = lazyvalues
829
831
830 def __contains__(self, value):
832 def __contains__(self, value):
831 return value in self.addedvalues or value in self.lazyvalues
833 return value in self.addedvalues or value in self.lazyvalues
832
834
833 def __iter__(self):
835 def __iter__(self):
834 added = self.addedvalues
836 added = self.addedvalues
835 for r in added:
837 for r in added:
836 yield r
838 yield r
837 for r in self.lazyvalues:
839 for r in self.lazyvalues:
838 if not r in added:
840 if not r in added:
839 yield r
841 yield r
840
842
841 def add(self, value):
843 def add(self, value):
842 self.addedvalues.add(value)
844 self.addedvalues.add(value)
843
845
844 def update(self, values):
846 def update(self, values):
845 self.addedvalues.update(values)
847 self.addedvalues.update(values)
846
848
847 has = lazyset(self.ancestors(common))
849 has = lazyset(self.ancestors(common))
848 has.add(nullrev)
850 has.add(nullrev)
849 has.update(common)
851 has.update(common)
850
852
851 # take all ancestors from heads that aren't in has
853 # take all ancestors from heads that aren't in has
852 missing = set()
854 missing = set()
853 visit = collections.deque(r for r in heads if r not in has)
855 visit = collections.deque(r for r in heads if r not in has)
854 while visit:
856 while visit:
855 r = visit.popleft()
857 r = visit.popleft()
856 if r in missing:
858 if r in missing:
857 continue
859 continue
858 else:
860 else:
859 missing.add(r)
861 missing.add(r)
860 for p in self.parentrevs(r):
862 for p in self.parentrevs(r):
861 if p not in has:
863 if p not in has:
862 visit.append(p)
864 visit.append(p)
863 missing = list(missing)
865 missing = list(missing)
864 missing.sort()
866 missing.sort()
865 return has, [self.node(miss) for miss in missing]
867 return has, [self.node(miss) for miss in missing]
866
868
867 def incrementalmissingrevs(self, common=None):
869 def incrementalmissingrevs(self, common=None):
868 """Return an object that can be used to incrementally compute the
870 """Return an object that can be used to incrementally compute the
869 revision numbers of the ancestors of arbitrary sets that are not
871 revision numbers of the ancestors of arbitrary sets that are not
870 ancestors of common. This is an ancestor.incrementalmissingancestors
872 ancestors of common. This is an ancestor.incrementalmissingancestors
871 object.
873 object.
872
874
873 'common' is a list of revision numbers. If common is not supplied, uses
875 'common' is a list of revision numbers. If common is not supplied, uses
874 nullrev.
876 nullrev.
875 """
877 """
876 if common is None:
878 if common is None:
877 common = [nullrev]
879 common = [nullrev]
878
880
879 if rustancestor is not None:
881 if rustancestor is not None:
880 return rustancestor.MissingAncestors(self.index, common)
882 return rustancestor.MissingAncestors(self.index, common)
881 return ancestor.incrementalmissingancestors(self.parentrevs, common)
883 return ancestor.incrementalmissingancestors(self.parentrevs, common)
882
884
883 def findmissingrevs(self, common=None, heads=None):
885 def findmissingrevs(self, common=None, heads=None):
884 """Return the revision numbers of the ancestors of heads that
886 """Return the revision numbers of the ancestors of heads that
885 are not ancestors of common.
887 are not ancestors of common.
886
888
887 More specifically, return a list of revision numbers corresponding to
889 More specifically, return a list of revision numbers corresponding to
888 nodes N such that every N satisfies the following constraints:
890 nodes N such that every N satisfies the following constraints:
889
891
890 1. N is an ancestor of some node in 'heads'
892 1. N is an ancestor of some node in 'heads'
891 2. N is not an ancestor of any node in 'common'
893 2. N is not an ancestor of any node in 'common'
892
894
893 The list is sorted by revision number, meaning it is
895 The list is sorted by revision number, meaning it is
894 topologically sorted.
896 topologically sorted.
895
897
896 'heads' and 'common' are both lists of revision numbers. If heads is
898 'heads' and 'common' are both lists of revision numbers. If heads is
897 not supplied, uses all of the revlog's heads. If common is not
899 not supplied, uses all of the revlog's heads. If common is not
898 supplied, uses nullid."""
900 supplied, uses nullid."""
899 if common is None:
901 if common is None:
900 common = [nullrev]
902 common = [nullrev]
901 if heads is None:
903 if heads is None:
902 heads = self.headrevs()
904 heads = self.headrevs()
903
905
904 inc = self.incrementalmissingrevs(common=common)
906 inc = self.incrementalmissingrevs(common=common)
905 return inc.missingancestors(heads)
907 return inc.missingancestors(heads)
906
908
907 def findmissing(self, common=None, heads=None):
909 def findmissing(self, common=None, heads=None):
908 """Return the ancestors of heads that are not ancestors of common.
910 """Return the ancestors of heads that are not ancestors of common.
909
911
910 More specifically, return a list of nodes N such that every N
912 More specifically, return a list of nodes N such that every N
911 satisfies the following constraints:
913 satisfies the following constraints:
912
914
913 1. N is an ancestor of some node in 'heads'
915 1. N is an ancestor of some node in 'heads'
914 2. N is not an ancestor of any node in 'common'
916 2. N is not an ancestor of any node in 'common'
915
917
916 The list is sorted by revision number, meaning it is
918 The list is sorted by revision number, meaning it is
917 topologically sorted.
919 topologically sorted.
918
920
919 'heads' and 'common' are both lists of node IDs. If heads is
921 'heads' and 'common' are both lists of node IDs. If heads is
920 not supplied, uses all of the revlog's heads. If common is not
922 not supplied, uses all of the revlog's heads. If common is not
921 supplied, uses nullid."""
923 supplied, uses nullid."""
922 if common is None:
924 if common is None:
923 common = [nullid]
925 common = [nullid]
924 if heads is None:
926 if heads is None:
925 heads = self.heads()
927 heads = self.heads()
926
928
927 common = [self.rev(n) for n in common]
929 common = [self.rev(n) for n in common]
928 heads = [self.rev(n) for n in heads]
930 heads = [self.rev(n) for n in heads]
929
931
930 inc = self.incrementalmissingrevs(common=common)
932 inc = self.incrementalmissingrevs(common=common)
931 return [self.node(r) for r in inc.missingancestors(heads)]
933 return [self.node(r) for r in inc.missingancestors(heads)]
932
934
933 def nodesbetween(self, roots=None, heads=None):
935 def nodesbetween(self, roots=None, heads=None):
934 """Return a topological path from 'roots' to 'heads'.
936 """Return a topological path from 'roots' to 'heads'.
935
937
936 Return a tuple (nodes, outroots, outheads) where 'nodes' is a
938 Return a tuple (nodes, outroots, outheads) where 'nodes' is a
937 topologically sorted list of all nodes N that satisfy both of
939 topologically sorted list of all nodes N that satisfy both of
938 these constraints:
940 these constraints:
939
941
940 1. N is a descendant of some node in 'roots'
942 1. N is a descendant of some node in 'roots'
941 2. N is an ancestor of some node in 'heads'
943 2. N is an ancestor of some node in 'heads'
942
944
943 Every node is considered to be both a descendant and an ancestor
945 Every node is considered to be both a descendant and an ancestor
944 of itself, so every reachable node in 'roots' and 'heads' will be
946 of itself, so every reachable node in 'roots' and 'heads' will be
945 included in 'nodes'.
947 included in 'nodes'.
946
948
947 'outroots' is the list of reachable nodes in 'roots', i.e., the
949 'outroots' is the list of reachable nodes in 'roots', i.e., the
948 subset of 'roots' that is returned in 'nodes'. Likewise,
950 subset of 'roots' that is returned in 'nodes'. Likewise,
949 'outheads' is the subset of 'heads' that is also in 'nodes'.
951 'outheads' is the subset of 'heads' that is also in 'nodes'.
950
952
951 'roots' and 'heads' are both lists of node IDs. If 'roots' is
953 'roots' and 'heads' are both lists of node IDs. If 'roots' is
952 unspecified, uses nullid as the only root. If 'heads' is
954 unspecified, uses nullid as the only root. If 'heads' is
953 unspecified, uses list of all of the revlog's heads."""
955 unspecified, uses list of all of the revlog's heads."""
954 nonodes = ([], [], [])
956 nonodes = ([], [], [])
955 if roots is not None:
957 if roots is not None:
956 roots = list(roots)
958 roots = list(roots)
957 if not roots:
959 if not roots:
958 return nonodes
960 return nonodes
959 lowestrev = min([self.rev(n) for n in roots])
961 lowestrev = min([self.rev(n) for n in roots])
960 else:
962 else:
961 roots = [nullid] # Everybody's a descendant of nullid
963 roots = [nullid] # Everybody's a descendant of nullid
962 lowestrev = nullrev
964 lowestrev = nullrev
963 if (lowestrev == nullrev) and (heads is None):
965 if (lowestrev == nullrev) and (heads is None):
964 # We want _all_ the nodes!
966 # We want _all_ the nodes!
965 return ([self.node(r) for r in self], [nullid], list(self.heads()))
967 return ([self.node(r) for r in self], [nullid], list(self.heads()))
966 if heads is None:
968 if heads is None:
967 # All nodes are ancestors, so the latest ancestor is the last
969 # All nodes are ancestors, so the latest ancestor is the last
968 # node.
970 # node.
969 highestrev = len(self) - 1
971 highestrev = len(self) - 1
970 # Set ancestors to None to signal that every node is an ancestor.
972 # Set ancestors to None to signal that every node is an ancestor.
971 ancestors = None
973 ancestors = None
972 # Set heads to an empty dictionary for later discovery of heads
974 # Set heads to an empty dictionary for later discovery of heads
973 heads = {}
975 heads = {}
974 else:
976 else:
975 heads = list(heads)
977 heads = list(heads)
976 if not heads:
978 if not heads:
977 return nonodes
979 return nonodes
978 ancestors = set()
980 ancestors = set()
979 # Turn heads into a dictionary so we can remove 'fake' heads.
981 # Turn heads into a dictionary so we can remove 'fake' heads.
980 # Also, later we will be using it to filter out the heads we can't
982 # Also, later we will be using it to filter out the heads we can't
981 # find from roots.
983 # find from roots.
982 heads = dict.fromkeys(heads, False)
984 heads = dict.fromkeys(heads, False)
983 # Start at the top and keep marking parents until we're done.
985 # Start at the top and keep marking parents until we're done.
984 nodestotag = set(heads)
986 nodestotag = set(heads)
985 # Remember where the top was so we can use it as a limit later.
987 # Remember where the top was so we can use it as a limit later.
986 highestrev = max([self.rev(n) for n in nodestotag])
988 highestrev = max([self.rev(n) for n in nodestotag])
987 while nodestotag:
989 while nodestotag:
988 # grab a node to tag
990 # grab a node to tag
989 n = nodestotag.pop()
991 n = nodestotag.pop()
990 # Never tag nullid
992 # Never tag nullid
991 if n == nullid:
993 if n == nullid:
992 continue
994 continue
993 # A node's revision number represents its place in a
995 # A node's revision number represents its place in a
994 # topologically sorted list of nodes.
996 # topologically sorted list of nodes.
995 r = self.rev(n)
997 r = self.rev(n)
996 if r >= lowestrev:
998 if r >= lowestrev:
997 if n not in ancestors:
999 if n not in ancestors:
998 # If we are possibly a descendant of one of the roots
1000 # If we are possibly a descendant of one of the roots
999 # and we haven't already been marked as an ancestor
1001 # and we haven't already been marked as an ancestor
1000 ancestors.add(n) # Mark as ancestor
1002 ancestors.add(n) # Mark as ancestor
1001 # Add non-nullid parents to list of nodes to tag.
1003 # Add non-nullid parents to list of nodes to tag.
1002 nodestotag.update([p for p in self.parents(n) if
1004 nodestotag.update([p for p in self.parents(n) if
1003 p != nullid])
1005 p != nullid])
1004 elif n in heads: # We've seen it before, is it a fake head?
1006 elif n in heads: # We've seen it before, is it a fake head?
1005 # So it is, real heads should not be the ancestors of
1007 # So it is, real heads should not be the ancestors of
1006 # any other heads.
1008 # any other heads.
1007 heads.pop(n)
1009 heads.pop(n)
1008 if not ancestors:
1010 if not ancestors:
1009 return nonodes
1011 return nonodes
1010 # Now that we have our set of ancestors, we want to remove any
1012 # Now that we have our set of ancestors, we want to remove any
1011 # roots that are not ancestors.
1013 # roots that are not ancestors.
1012
1014
1013 # If one of the roots was nullid, everything is included anyway.
1015 # If one of the roots was nullid, everything is included anyway.
1014 if lowestrev > nullrev:
1016 if lowestrev > nullrev:
1015 # But, since we weren't, let's recompute the lowest rev to not
1017 # But, since we weren't, let's recompute the lowest rev to not
1016 # include roots that aren't ancestors.
1018 # include roots that aren't ancestors.
1017
1019
1018 # Filter out roots that aren't ancestors of heads
1020 # Filter out roots that aren't ancestors of heads
1019 roots = [root for root in roots if root in ancestors]
1021 roots = [root for root in roots if root in ancestors]
1020 # Recompute the lowest revision
1022 # Recompute the lowest revision
1021 if roots:
1023 if roots:
1022 lowestrev = min([self.rev(root) for root in roots])
1024 lowestrev = min([self.rev(root) for root in roots])
1023 else:
1025 else:
1024 # No more roots? Return empty list
1026 # No more roots? Return empty list
1025 return nonodes
1027 return nonodes
1026 else:
1028 else:
1027 # We are descending from nullid, and don't need to care about
1029 # We are descending from nullid, and don't need to care about
1028 # any other roots.
1030 # any other roots.
1029 lowestrev = nullrev
1031 lowestrev = nullrev
1030 roots = [nullid]
1032 roots = [nullid]
1031 # Transform our roots list into a set.
1033 # Transform our roots list into a set.
1032 descendants = set(roots)
1034 descendants = set(roots)
1033 # Also, keep the original roots so we can filter out roots that aren't
1035 # Also, keep the original roots so we can filter out roots that aren't
1034 # 'real' roots (i.e. are descended from other roots).
1036 # 'real' roots (i.e. are descended from other roots).
1035 roots = descendants.copy()
1037 roots = descendants.copy()
1036 # Our topologically sorted list of output nodes.
1038 # Our topologically sorted list of output nodes.
1037 orderedout = []
1039 orderedout = []
1038 # Don't start at nullid since we don't want nullid in our output list,
1040 # Don't start at nullid since we don't want nullid in our output list,
1039 # and if nullid shows up in descendants, empty parents will look like
1041 # and if nullid shows up in descendants, empty parents will look like
1040 # they're descendants.
1042 # they're descendants.
1041 for r in self.revs(start=max(lowestrev, 0), stop=highestrev + 1):
1043 for r in self.revs(start=max(lowestrev, 0), stop=highestrev + 1):
1042 n = self.node(r)
1044 n = self.node(r)
1043 isdescendant = False
1045 isdescendant = False
1044 if lowestrev == nullrev: # Everybody is a descendant of nullid
1046 if lowestrev == nullrev: # Everybody is a descendant of nullid
1045 isdescendant = True
1047 isdescendant = True
1046 elif n in descendants:
1048 elif n in descendants:
1047 # n is already a descendant
1049 # n is already a descendant
1048 isdescendant = True
1050 isdescendant = True
1049 # This check only needs to be done here because all the roots
1051 # This check only needs to be done here because all the roots
1050 # will start being marked is descendants before the loop.
1052 # will start being marked is descendants before the loop.
1051 if n in roots:
1053 if n in roots:
1052 # If n was a root, check if it's a 'real' root.
1054 # If n was a root, check if it's a 'real' root.
1053 p = tuple(self.parents(n))
1055 p = tuple(self.parents(n))
1054 # If any of its parents are descendants, it's not a root.
1056 # If any of its parents are descendants, it's not a root.
1055 if (p[0] in descendants) or (p[1] in descendants):
1057 if (p[0] in descendants) or (p[1] in descendants):
1056 roots.remove(n)
1058 roots.remove(n)
1057 else:
1059 else:
1058 p = tuple(self.parents(n))
1060 p = tuple(self.parents(n))
1059 # A node is a descendant if either of its parents are
1061 # A node is a descendant if either of its parents are
1060 # descendants. (We seeded the dependents list with the roots
1062 # descendants. (We seeded the dependents list with the roots
1061 # up there, remember?)
1063 # up there, remember?)
1062 if (p[0] in descendants) or (p[1] in descendants):
1064 if (p[0] in descendants) or (p[1] in descendants):
1063 descendants.add(n)
1065 descendants.add(n)
1064 isdescendant = True
1066 isdescendant = True
1065 if isdescendant and ((ancestors is None) or (n in ancestors)):
1067 if isdescendant and ((ancestors is None) or (n in ancestors)):
1066 # Only include nodes that are both descendants and ancestors.
1068 # Only include nodes that are both descendants and ancestors.
1067 orderedout.append(n)
1069 orderedout.append(n)
1068 if (ancestors is not None) and (n in heads):
1070 if (ancestors is not None) and (n in heads):
1069 # We're trying to figure out which heads are reachable
1071 # We're trying to figure out which heads are reachable
1070 # from roots.
1072 # from roots.
1071 # Mark this head as having been reached
1073 # Mark this head as having been reached
1072 heads[n] = True
1074 heads[n] = True
1073 elif ancestors is None:
1075 elif ancestors is None:
1074 # Otherwise, we're trying to discover the heads.
1076 # Otherwise, we're trying to discover the heads.
1075 # Assume this is a head because if it isn't, the next step
1077 # Assume this is a head because if it isn't, the next step
1076 # will eventually remove it.
1078 # will eventually remove it.
1077 heads[n] = True
1079 heads[n] = True
1078 # But, obviously its parents aren't.
1080 # But, obviously its parents aren't.
1079 for p in self.parents(n):
1081 for p in self.parents(n):
1080 heads.pop(p, None)
1082 heads.pop(p, None)
1081 heads = [head for head, flag in heads.iteritems() if flag]
1083 heads = [head for head, flag in heads.iteritems() if flag]
1082 roots = list(roots)
1084 roots = list(roots)
1083 assert orderedout
1085 assert orderedout
1084 assert roots
1086 assert roots
1085 assert heads
1087 assert heads
1086 return (orderedout, roots, heads)
1088 return (orderedout, roots, heads)
1087
1089
1088 def headrevs(self, revs=None):
1090 def headrevs(self, revs=None):
1089 if revs is None:
1091 if revs is None:
1090 try:
1092 try:
1091 return self.index.headrevs()
1093 return self.index.headrevs()
1092 except AttributeError:
1094 except AttributeError:
1093 return self._headrevs()
1095 return self._headrevs()
1094 if rustdagop is not None:
1096 if rustdagop is not None:
1095 return rustdagop.headrevs(self.index, revs)
1097 return rustdagop.headrevs(self.index, revs)
1096 return dagop.headrevs(revs, self._uncheckedparentrevs)
1098 return dagop.headrevs(revs, self._uncheckedparentrevs)
1097
1099
1098 def computephases(self, roots):
1100 def computephases(self, roots):
1099 return self.index.computephasesmapsets(roots)
1101 return self.index.computephasesmapsets(roots)
1100
1102
1101 def _headrevs(self):
1103 def _headrevs(self):
1102 count = len(self)
1104 count = len(self)
1103 if not count:
1105 if not count:
1104 return [nullrev]
1106 return [nullrev]
1105 # we won't iter over filtered rev so nobody is a head at start
1107 # we won't iter over filtered rev so nobody is a head at start
1106 ishead = [0] * (count + 1)
1108 ishead = [0] * (count + 1)
1107 index = self.index
1109 index = self.index
1108 for r in self:
1110 for r in self:
1109 ishead[r] = 1 # I may be an head
1111 ishead[r] = 1 # I may be an head
1110 e = index[r]
1112 e = index[r]
1111 ishead[e[5]] = ishead[e[6]] = 0 # my parent are not
1113 ishead[e[5]] = ishead[e[6]] = 0 # my parent are not
1112 return [r for r, val in enumerate(ishead) if val]
1114 return [r for r, val in enumerate(ishead) if val]
1113
1115
1114 def heads(self, start=None, stop=None):
1116 def heads(self, start=None, stop=None):
1115 """return the list of all nodes that have no children
1117 """return the list of all nodes that have no children
1116
1118
1117 if start is specified, only heads that are descendants of
1119 if start is specified, only heads that are descendants of
1118 start will be returned
1120 start will be returned
1119 if stop is specified, it will consider all the revs from stop
1121 if stop is specified, it will consider all the revs from stop
1120 as if they had no children
1122 as if they had no children
1121 """
1123 """
1122 if start is None and stop is None:
1124 if start is None and stop is None:
1123 if not len(self):
1125 if not len(self):
1124 return [nullid]
1126 return [nullid]
1125 return [self.node(r) for r in self.headrevs()]
1127 return [self.node(r) for r in self.headrevs()]
1126
1128
1127 if start is None:
1129 if start is None:
1128 start = nullrev
1130 start = nullrev
1129 else:
1131 else:
1130 start = self.rev(start)
1132 start = self.rev(start)
1131
1133
1132 stoprevs = set(self.rev(n) for n in stop or [])
1134 stoprevs = set(self.rev(n) for n in stop or [])
1133
1135
1134 revs = dagop.headrevssubset(self.revs, self.parentrevs, startrev=start,
1136 revs = dagop.headrevssubset(self.revs, self.parentrevs, startrev=start,
1135 stoprevs=stoprevs)
1137 stoprevs=stoprevs)
1136
1138
1137 return [self.node(rev) for rev in revs]
1139 return [self.node(rev) for rev in revs]
1138
1140
1139 def children(self, node):
1141 def children(self, node):
1140 """find the children of a given node"""
1142 """find the children of a given node"""
1141 c = []
1143 c = []
1142 p = self.rev(node)
1144 p = self.rev(node)
1143 for r in self.revs(start=p + 1):
1145 for r in self.revs(start=p + 1):
1144 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
1146 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
1145 if prevs:
1147 if prevs:
1146 for pr in prevs:
1148 for pr in prevs:
1147 if pr == p:
1149 if pr == p:
1148 c.append(self.node(r))
1150 c.append(self.node(r))
1149 elif p == nullrev:
1151 elif p == nullrev:
1150 c.append(self.node(r))
1152 c.append(self.node(r))
1151 return c
1153 return c
1152
1154
1153 def commonancestorsheads(self, a, b):
1155 def commonancestorsheads(self, a, b):
1154 """calculate all the heads of the common ancestors of nodes a and b"""
1156 """calculate all the heads of the common ancestors of nodes a and b"""
1155 a, b = self.rev(a), self.rev(b)
1157 a, b = self.rev(a), self.rev(b)
1156 ancs = self._commonancestorsheads(a, b)
1158 ancs = self._commonancestorsheads(a, b)
1157 return pycompat.maplist(self.node, ancs)
1159 return pycompat.maplist(self.node, ancs)
1158
1160
1159 def _commonancestorsheads(self, *revs):
1161 def _commonancestorsheads(self, *revs):
1160 """calculate all the heads of the common ancestors of revs"""
1162 """calculate all the heads of the common ancestors of revs"""
1161 try:
1163 try:
1162 ancs = self.index.commonancestorsheads(*revs)
1164 ancs = self.index.commonancestorsheads(*revs)
1163 except (AttributeError, OverflowError): # C implementation failed
1165 except (AttributeError, OverflowError): # C implementation failed
1164 ancs = ancestor.commonancestorsheads(self.parentrevs, *revs)
1166 ancs = ancestor.commonancestorsheads(self.parentrevs, *revs)
1165 return ancs
1167 return ancs
1166
1168
1167 def isancestor(self, a, b):
1169 def isancestor(self, a, b):
1168 """return True if node a is an ancestor of node b
1170 """return True if node a is an ancestor of node b
1169
1171
1170 A revision is considered an ancestor of itself."""
1172 A revision is considered an ancestor of itself."""
1171 a, b = self.rev(a), self.rev(b)
1173 a, b = self.rev(a), self.rev(b)
1172 return self.isancestorrev(a, b)
1174 return self.isancestorrev(a, b)
1173
1175
1174 def isancestorrev(self, a, b):
1176 def isancestorrev(self, a, b):
1175 """return True if revision a is an ancestor of revision b
1177 """return True if revision a is an ancestor of revision b
1176
1178
1177 A revision is considered an ancestor of itself.
1179 A revision is considered an ancestor of itself.
1178
1180
1179 The implementation of this is trivial but the use of
1181 The implementation of this is trivial but the use of
1180 reachableroots is not."""
1182 reachableroots is not."""
1181 if a == nullrev:
1183 if a == nullrev:
1182 return True
1184 return True
1183 elif a == b:
1185 elif a == b:
1184 return True
1186 return True
1185 elif a > b:
1187 elif a > b:
1186 return False
1188 return False
1187 return bool(self.reachableroots(a, [b], [a], includepath=False))
1189 return bool(self.reachableroots(a, [b], [a], includepath=False))
1188
1190
1189 def reachableroots(self, minroot, heads, roots, includepath=False):
1191 def reachableroots(self, minroot, heads, roots, includepath=False):
1190 """return (heads(::<roots> and <roots>::<heads>))
1192 """return (heads(::<roots> and <roots>::<heads>))
1191
1193
1192 If includepath is True, return (<roots>::<heads>)."""
1194 If includepath is True, return (<roots>::<heads>)."""
1193 try:
1195 try:
1194 return self.index.reachableroots2(minroot, heads, roots,
1196 return self.index.reachableroots2(minroot, heads, roots,
1195 includepath)
1197 includepath)
1196 except AttributeError:
1198 except AttributeError:
1197 return dagop._reachablerootspure(self.parentrevs,
1199 return dagop._reachablerootspure(self.parentrevs,
1198 minroot, roots, heads, includepath)
1200 minroot, roots, heads, includepath)
1199
1201
1200 def ancestor(self, a, b):
1202 def ancestor(self, a, b):
1201 """calculate the "best" common ancestor of nodes a and b"""
1203 """calculate the "best" common ancestor of nodes a and b"""
1202
1204
1203 a, b = self.rev(a), self.rev(b)
1205 a, b = self.rev(a), self.rev(b)
1204 try:
1206 try:
1205 ancs = self.index.ancestors(a, b)
1207 ancs = self.index.ancestors(a, b)
1206 except (AttributeError, OverflowError):
1208 except (AttributeError, OverflowError):
1207 ancs = ancestor.ancestors(self.parentrevs, a, b)
1209 ancs = ancestor.ancestors(self.parentrevs, a, b)
1208 if ancs:
1210 if ancs:
1209 # choose a consistent winner when there's a tie
1211 # choose a consistent winner when there's a tie
1210 return min(map(self.node, ancs))
1212 return min(map(self.node, ancs))
1211 return nullid
1213 return nullid
1212
1214
1213 def _match(self, id):
1215 def _match(self, id):
1214 if isinstance(id, int):
1216 if isinstance(id, int):
1215 # rev
1217 # rev
1216 return self.node(id)
1218 return self.node(id)
1217 if len(id) == 20:
1219 if len(id) == 20:
1218 # possibly a binary node
1220 # possibly a binary node
1219 # odds of a binary node being all hex in ASCII are 1 in 10**25
1221 # odds of a binary node being all hex in ASCII are 1 in 10**25
1220 try:
1222 try:
1221 node = id
1223 node = id
1222 self.rev(node) # quick search the index
1224 self.rev(node) # quick search the index
1223 return node
1225 return node
1224 except error.LookupError:
1226 except error.LookupError:
1225 pass # may be partial hex id
1227 pass # may be partial hex id
1226 try:
1228 try:
1227 # str(rev)
1229 # str(rev)
1228 rev = int(id)
1230 rev = int(id)
1229 if "%d" % rev != id:
1231 if "%d" % rev != id:
1230 raise ValueError
1232 raise ValueError
1231 if rev < 0:
1233 if rev < 0:
1232 rev = len(self) + rev
1234 rev = len(self) + rev
1233 if rev < 0 or rev >= len(self):
1235 if rev < 0 or rev >= len(self):
1234 raise ValueError
1236 raise ValueError
1235 return self.node(rev)
1237 return self.node(rev)
1236 except (ValueError, OverflowError):
1238 except (ValueError, OverflowError):
1237 pass
1239 pass
1238 if len(id) == 40:
1240 if len(id) == 40:
1239 try:
1241 try:
1240 # a full hex nodeid?
1242 # a full hex nodeid?
1241 node = bin(id)
1243 node = bin(id)
1242 self.rev(node)
1244 self.rev(node)
1243 return node
1245 return node
1244 except (TypeError, error.LookupError):
1246 except (TypeError, error.LookupError):
1245 pass
1247 pass
1246
1248
1247 def _partialmatch(self, id):
1249 def _partialmatch(self, id):
1248 # we don't care wdirfilenodeids as they should be always full hash
1250 # we don't care wdirfilenodeids as they should be always full hash
1249 maybewdir = wdirhex.startswith(id)
1251 maybewdir = wdirhex.startswith(id)
1250 try:
1252 try:
1251 partial = self.index.partialmatch(id)
1253 partial = self.index.partialmatch(id)
1252 if partial and self.hasnode(partial):
1254 if partial and self.hasnode(partial):
1253 if maybewdir:
1255 if maybewdir:
1254 # single 'ff...' match in radix tree, ambiguous with wdir
1256 # single 'ff...' match in radix tree, ambiguous with wdir
1255 raise error.RevlogError
1257 raise error.RevlogError
1256 return partial
1258 return partial
1257 if maybewdir:
1259 if maybewdir:
1258 # no 'ff...' match in radix tree, wdir identified
1260 # no 'ff...' match in radix tree, wdir identified
1259 raise error.WdirUnsupported
1261 raise error.WdirUnsupported
1260 return None
1262 return None
1261 except error.RevlogError:
1263 except error.RevlogError:
1262 # parsers.c radix tree lookup gave multiple matches
1264 # parsers.c radix tree lookup gave multiple matches
1263 # fast path: for unfiltered changelog, radix tree is accurate
1265 # fast path: for unfiltered changelog, radix tree is accurate
1264 if not getattr(self, 'filteredrevs', None):
1266 if not getattr(self, 'filteredrevs', None):
1265 raise error.AmbiguousPrefixLookupError(
1267 raise error.AmbiguousPrefixLookupError(
1266 id, self.indexfile, _('ambiguous identifier'))
1268 id, self.indexfile, _('ambiguous identifier'))
1267 # fall through to slow path that filters hidden revisions
1269 # fall through to slow path that filters hidden revisions
1268 except (AttributeError, ValueError):
1270 except (AttributeError, ValueError):
1269 # we are pure python, or key was too short to search radix tree
1271 # we are pure python, or key was too short to search radix tree
1270 pass
1272 pass
1271
1273
1272 if id in self._pcache:
1274 if id in self._pcache:
1273 return self._pcache[id]
1275 return self._pcache[id]
1274
1276
1275 if len(id) <= 40:
1277 if len(id) <= 40:
1276 try:
1278 try:
1277 # hex(node)[:...]
1279 # hex(node)[:...]
1278 l = len(id) // 2 # grab an even number of digits
1280 l = len(id) // 2 # grab an even number of digits
1279 prefix = bin(id[:l * 2])
1281 prefix = bin(id[:l * 2])
1280 nl = [e[7] for e in self.index if e[7].startswith(prefix)]
1282 nl = [e[7] for e in self.index if e[7].startswith(prefix)]
1281 nl = [n for n in nl if hex(n).startswith(id) and
1283 nl = [n for n in nl if hex(n).startswith(id) and
1282 self.hasnode(n)]
1284 self.hasnode(n)]
1283 if nullhex.startswith(id):
1285 if nullhex.startswith(id):
1284 nl.append(nullid)
1286 nl.append(nullid)
1285 if len(nl) > 0:
1287 if len(nl) > 0:
1286 if len(nl) == 1 and not maybewdir:
1288 if len(nl) == 1 and not maybewdir:
1287 self._pcache[id] = nl[0]
1289 self._pcache[id] = nl[0]
1288 return nl[0]
1290 return nl[0]
1289 raise error.AmbiguousPrefixLookupError(
1291 raise error.AmbiguousPrefixLookupError(
1290 id, self.indexfile, _('ambiguous identifier'))
1292 id, self.indexfile, _('ambiguous identifier'))
1291 if maybewdir:
1293 if maybewdir:
1292 raise error.WdirUnsupported
1294 raise error.WdirUnsupported
1293 return None
1295 return None
1294 except TypeError:
1296 except TypeError:
1295 pass
1297 pass
1296
1298
1297 def lookup(self, id):
1299 def lookup(self, id):
1298 """locate a node based on:
1300 """locate a node based on:
1299 - revision number or str(revision number)
1301 - revision number or str(revision number)
1300 - nodeid or subset of hex nodeid
1302 - nodeid or subset of hex nodeid
1301 """
1303 """
1302 n = self._match(id)
1304 n = self._match(id)
1303 if n is not None:
1305 if n is not None:
1304 return n
1306 return n
1305 n = self._partialmatch(id)
1307 n = self._partialmatch(id)
1306 if n:
1308 if n:
1307 return n
1309 return n
1308
1310
1309 raise error.LookupError(id, self.indexfile, _('no match found'))
1311 raise error.LookupError(id, self.indexfile, _('no match found'))
1310
1312
1311 def shortest(self, node, minlength=1):
1313 def shortest(self, node, minlength=1):
1312 """Find the shortest unambiguous prefix that matches node."""
1314 """Find the shortest unambiguous prefix that matches node."""
1313 def isvalid(prefix):
1315 def isvalid(prefix):
1314 try:
1316 try:
1315 matchednode = self._partialmatch(prefix)
1317 matchednode = self._partialmatch(prefix)
1316 except error.AmbiguousPrefixLookupError:
1318 except error.AmbiguousPrefixLookupError:
1317 return False
1319 return False
1318 except error.WdirUnsupported:
1320 except error.WdirUnsupported:
1319 # single 'ff...' match
1321 # single 'ff...' match
1320 return True
1322 return True
1321 if matchednode is None:
1323 if matchednode is None:
1322 raise error.LookupError(node, self.indexfile, _('no node'))
1324 raise error.LookupError(node, self.indexfile, _('no node'))
1323 return True
1325 return True
1324
1326
1325 def maybewdir(prefix):
1327 def maybewdir(prefix):
1326 return all(c == 'f' for c in pycompat.iterbytestr(prefix))
1328 return all(c == 'f' for c in pycompat.iterbytestr(prefix))
1327
1329
1328 hexnode = hex(node)
1330 hexnode = hex(node)
1329
1331
1330 def disambiguate(hexnode, minlength):
1332 def disambiguate(hexnode, minlength):
1331 """Disambiguate against wdirid."""
1333 """Disambiguate against wdirid."""
1332 for length in range(minlength, 41):
1334 for length in range(minlength, 41):
1333 prefix = hexnode[:length]
1335 prefix = hexnode[:length]
1334 if not maybewdir(prefix):
1336 if not maybewdir(prefix):
1335 return prefix
1337 return prefix
1336
1338
1337 if not getattr(self, 'filteredrevs', None):
1339 if not getattr(self, 'filteredrevs', None):
1338 try:
1340 try:
1339 length = max(self.index.shortest(node), minlength)
1341 length = max(self.index.shortest(node), minlength)
1340 return disambiguate(hexnode, length)
1342 return disambiguate(hexnode, length)
1341 except error.RevlogError:
1343 except error.RevlogError:
1342 if node != wdirid:
1344 if node != wdirid:
1343 raise error.LookupError(node, self.indexfile, _('no node'))
1345 raise error.LookupError(node, self.indexfile, _('no node'))
1344 except AttributeError:
1346 except AttributeError:
1345 # Fall through to pure code
1347 # Fall through to pure code
1346 pass
1348 pass
1347
1349
1348 if node == wdirid:
1350 if node == wdirid:
1349 for length in range(minlength, 41):
1351 for length in range(minlength, 41):
1350 prefix = hexnode[:length]
1352 prefix = hexnode[:length]
1351 if isvalid(prefix):
1353 if isvalid(prefix):
1352 return prefix
1354 return prefix
1353
1355
1354 for length in range(minlength, 41):
1356 for length in range(minlength, 41):
1355 prefix = hexnode[:length]
1357 prefix = hexnode[:length]
1356 if isvalid(prefix):
1358 if isvalid(prefix):
1357 return disambiguate(hexnode, length)
1359 return disambiguate(hexnode, length)
1358
1360
1359 def cmp(self, node, text):
1361 def cmp(self, node, text):
1360 """compare text with a given file revision
1362 """compare text with a given file revision
1361
1363
1362 returns True if text is different than what is stored.
1364 returns True if text is different than what is stored.
1363 """
1365 """
1364 p1, p2 = self.parents(node)
1366 p1, p2 = self.parents(node)
1365 return storageutil.hashrevisionsha1(text, p1, p2) != node
1367 return storageutil.hashrevisionsha1(text, p1, p2) != node
1366
1368
1367 def _cachesegment(self, offset, data):
1369 def _cachesegment(self, offset, data):
1368 """Add a segment to the revlog cache.
1370 """Add a segment to the revlog cache.
1369
1371
1370 Accepts an absolute offset and the data that is at that location.
1372 Accepts an absolute offset and the data that is at that location.
1371 """
1373 """
1372 o, d = self._chunkcache
1374 o, d = self._chunkcache
1373 # try to add to existing cache
1375 # try to add to existing cache
1374 if o + len(d) == offset and len(d) + len(data) < _chunksize:
1376 if o + len(d) == offset and len(d) + len(data) < _chunksize:
1375 self._chunkcache = o, d + data
1377 self._chunkcache = o, d + data
1376 else:
1378 else:
1377 self._chunkcache = offset, data
1379 self._chunkcache = offset, data
1378
1380
1379 def _readsegment(self, offset, length, df=None):
1381 def _readsegment(self, offset, length, df=None):
1380 """Load a segment of raw data from the revlog.
1382 """Load a segment of raw data from the revlog.
1381
1383
1382 Accepts an absolute offset, length to read, and an optional existing
1384 Accepts an absolute offset, length to read, and an optional existing
1383 file handle to read from.
1385 file handle to read from.
1384
1386
1385 If an existing file handle is passed, it will be seeked and the
1387 If an existing file handle is passed, it will be seeked and the
1386 original seek position will NOT be restored.
1388 original seek position will NOT be restored.
1387
1389
1388 Returns a str or buffer of raw byte data.
1390 Returns a str or buffer of raw byte data.
1389
1391
1390 Raises if the requested number of bytes could not be read.
1392 Raises if the requested number of bytes could not be read.
1391 """
1393 """
1392 # Cache data both forward and backward around the requested
1394 # Cache data both forward and backward around the requested
1393 # data, in a fixed size window. This helps speed up operations
1395 # data, in a fixed size window. This helps speed up operations
1394 # involving reading the revlog backwards.
1396 # involving reading the revlog backwards.
1395 cachesize = self._chunkcachesize
1397 cachesize = self._chunkcachesize
1396 realoffset = offset & ~(cachesize - 1)
1398 realoffset = offset & ~(cachesize - 1)
1397 reallength = (((offset + length + cachesize) & ~(cachesize - 1))
1399 reallength = (((offset + length + cachesize) & ~(cachesize - 1))
1398 - realoffset)
1400 - realoffset)
1399 with self._datareadfp(df) as df:
1401 with self._datareadfp(df) as df:
1400 df.seek(realoffset)
1402 df.seek(realoffset)
1401 d = df.read(reallength)
1403 d = df.read(reallength)
1402
1404
1403 self._cachesegment(realoffset, d)
1405 self._cachesegment(realoffset, d)
1404 if offset != realoffset or reallength != length:
1406 if offset != realoffset or reallength != length:
1405 startoffset = offset - realoffset
1407 startoffset = offset - realoffset
1406 if len(d) - startoffset < length:
1408 if len(d) - startoffset < length:
1407 raise error.RevlogError(
1409 raise error.RevlogError(
1408 _('partial read of revlog %s; expected %d bytes from '
1410 _('partial read of revlog %s; expected %d bytes from '
1409 'offset %d, got %d') %
1411 'offset %d, got %d') %
1410 (self.indexfile if self._inline else self.datafile,
1412 (self.indexfile if self._inline else self.datafile,
1411 length, realoffset, len(d) - startoffset))
1413 length, realoffset, len(d) - startoffset))
1412
1414
1413 return util.buffer(d, startoffset, length)
1415 return util.buffer(d, startoffset, length)
1414
1416
1415 if len(d) < length:
1417 if len(d) < length:
1416 raise error.RevlogError(
1418 raise error.RevlogError(
1417 _('partial read of revlog %s; expected %d bytes from offset '
1419 _('partial read of revlog %s; expected %d bytes from offset '
1418 '%d, got %d') %
1420 '%d, got %d') %
1419 (self.indexfile if self._inline else self.datafile,
1421 (self.indexfile if self._inline else self.datafile,
1420 length, offset, len(d)))
1422 length, offset, len(d)))
1421
1423
1422 return d
1424 return d
1423
1425
1424 def _getsegment(self, offset, length, df=None):
1426 def _getsegment(self, offset, length, df=None):
1425 """Obtain a segment of raw data from the revlog.
1427 """Obtain a segment of raw data from the revlog.
1426
1428
1427 Accepts an absolute offset, length of bytes to obtain, and an
1429 Accepts an absolute offset, length of bytes to obtain, and an
1428 optional file handle to the already-opened revlog. If the file
1430 optional file handle to the already-opened revlog. If the file
1429 handle is used, it's original seek position will not be preserved.
1431 handle is used, it's original seek position will not be preserved.
1430
1432
1431 Requests for data may be returned from a cache.
1433 Requests for data may be returned from a cache.
1432
1434
1433 Returns a str or a buffer instance of raw byte data.
1435 Returns a str or a buffer instance of raw byte data.
1434 """
1436 """
1435 o, d = self._chunkcache
1437 o, d = self._chunkcache
1436 l = len(d)
1438 l = len(d)
1437
1439
1438 # is it in the cache?
1440 # is it in the cache?
1439 cachestart = offset - o
1441 cachestart = offset - o
1440 cacheend = cachestart + length
1442 cacheend = cachestart + length
1441 if cachestart >= 0 and cacheend <= l:
1443 if cachestart >= 0 and cacheend <= l:
1442 if cachestart == 0 and cacheend == l:
1444 if cachestart == 0 and cacheend == l:
1443 return d # avoid a copy
1445 return d # avoid a copy
1444 return util.buffer(d, cachestart, cacheend - cachestart)
1446 return util.buffer(d, cachestart, cacheend - cachestart)
1445
1447
1446 return self._readsegment(offset, length, df=df)
1448 return self._readsegment(offset, length, df=df)
1447
1449
1448 def _getsegmentforrevs(self, startrev, endrev, df=None):
1450 def _getsegmentforrevs(self, startrev, endrev, df=None):
1449 """Obtain a segment of raw data corresponding to a range of revisions.
1451 """Obtain a segment of raw data corresponding to a range of revisions.
1450
1452
1451 Accepts the start and end revisions and an optional already-open
1453 Accepts the start and end revisions and an optional already-open
1452 file handle to be used for reading. If the file handle is read, its
1454 file handle to be used for reading. If the file handle is read, its
1453 seek position will not be preserved.
1455 seek position will not be preserved.
1454
1456
1455 Requests for data may be satisfied by a cache.
1457 Requests for data may be satisfied by a cache.
1456
1458
1457 Returns a 2-tuple of (offset, data) for the requested range of
1459 Returns a 2-tuple of (offset, data) for the requested range of
1458 revisions. Offset is the integer offset from the beginning of the
1460 revisions. Offset is the integer offset from the beginning of the
1459 revlog and data is a str or buffer of the raw byte data.
1461 revlog and data is a str or buffer of the raw byte data.
1460
1462
1461 Callers will need to call ``self.start(rev)`` and ``self.length(rev)``
1463 Callers will need to call ``self.start(rev)`` and ``self.length(rev)``
1462 to determine where each revision's data begins and ends.
1464 to determine where each revision's data begins and ends.
1463 """
1465 """
1464 # Inlined self.start(startrev) & self.end(endrev) for perf reasons
1466 # Inlined self.start(startrev) & self.end(endrev) for perf reasons
1465 # (functions are expensive).
1467 # (functions are expensive).
1466 index = self.index
1468 index = self.index
1467 istart = index[startrev]
1469 istart = index[startrev]
1468 start = int(istart[0] >> 16)
1470 start = int(istart[0] >> 16)
1469 if startrev == endrev:
1471 if startrev == endrev:
1470 end = start + istart[1]
1472 end = start + istart[1]
1471 else:
1473 else:
1472 iend = index[endrev]
1474 iend = index[endrev]
1473 end = int(iend[0] >> 16) + iend[1]
1475 end = int(iend[0] >> 16) + iend[1]
1474
1476
1475 if self._inline:
1477 if self._inline:
1476 start += (startrev + 1) * self._io.size
1478 start += (startrev + 1) * self._io.size
1477 end += (endrev + 1) * self._io.size
1479 end += (endrev + 1) * self._io.size
1478 length = end - start
1480 length = end - start
1479
1481
1480 return start, self._getsegment(start, length, df=df)
1482 return start, self._getsegment(start, length, df=df)
1481
1483
1482 def _chunk(self, rev, df=None):
1484 def _chunk(self, rev, df=None):
1483 """Obtain a single decompressed chunk for a revision.
1485 """Obtain a single decompressed chunk for a revision.
1484
1486
1485 Accepts an integer revision and an optional already-open file handle
1487 Accepts an integer revision and an optional already-open file handle
1486 to be used for reading. If used, the seek position of the file will not
1488 to be used for reading. If used, the seek position of the file will not
1487 be preserved.
1489 be preserved.
1488
1490
1489 Returns a str holding uncompressed data for the requested revision.
1491 Returns a str holding uncompressed data for the requested revision.
1490 """
1492 """
1491 return self.decompress(self._getsegmentforrevs(rev, rev, df=df)[1])
1493 return self.decompress(self._getsegmentforrevs(rev, rev, df=df)[1])
1492
1494
1493 def _chunks(self, revs, df=None, targetsize=None):
1495 def _chunks(self, revs, df=None, targetsize=None):
1494 """Obtain decompressed chunks for the specified revisions.
1496 """Obtain decompressed chunks for the specified revisions.
1495
1497
1496 Accepts an iterable of numeric revisions that are assumed to be in
1498 Accepts an iterable of numeric revisions that are assumed to be in
1497 ascending order. Also accepts an optional already-open file handle
1499 ascending order. Also accepts an optional already-open file handle
1498 to be used for reading. If used, the seek position of the file will
1500 to be used for reading. If used, the seek position of the file will
1499 not be preserved.
1501 not be preserved.
1500
1502
1501 This function is similar to calling ``self._chunk()`` multiple times,
1503 This function is similar to calling ``self._chunk()`` multiple times,
1502 but is faster.
1504 but is faster.
1503
1505
1504 Returns a list with decompressed data for each requested revision.
1506 Returns a list with decompressed data for each requested revision.
1505 """
1507 """
1506 if not revs:
1508 if not revs:
1507 return []
1509 return []
1508 start = self.start
1510 start = self.start
1509 length = self.length
1511 length = self.length
1510 inline = self._inline
1512 inline = self._inline
1511 iosize = self._io.size
1513 iosize = self._io.size
1512 buffer = util.buffer
1514 buffer = util.buffer
1513
1515
1514 l = []
1516 l = []
1515 ladd = l.append
1517 ladd = l.append
1516
1518
1517 if not self._withsparseread:
1519 if not self._withsparseread:
1518 slicedchunks = (revs,)
1520 slicedchunks = (revs,)
1519 else:
1521 else:
1520 slicedchunks = deltautil.slicechunk(self, revs,
1522 slicedchunks = deltautil.slicechunk(self, revs,
1521 targetsize=targetsize)
1523 targetsize=targetsize)
1522
1524
1523 for revschunk in slicedchunks:
1525 for revschunk in slicedchunks:
1524 firstrev = revschunk[0]
1526 firstrev = revschunk[0]
1525 # Skip trailing revisions with empty diff
1527 # Skip trailing revisions with empty diff
1526 for lastrev in revschunk[::-1]:
1528 for lastrev in revschunk[::-1]:
1527 if length(lastrev) != 0:
1529 if length(lastrev) != 0:
1528 break
1530 break
1529
1531
1530 try:
1532 try:
1531 offset, data = self._getsegmentforrevs(firstrev, lastrev, df=df)
1533 offset, data = self._getsegmentforrevs(firstrev, lastrev, df=df)
1532 except OverflowError:
1534 except OverflowError:
1533 # issue4215 - we can't cache a run of chunks greater than
1535 # issue4215 - we can't cache a run of chunks greater than
1534 # 2G on Windows
1536 # 2G on Windows
1535 return [self._chunk(rev, df=df) for rev in revschunk]
1537 return [self._chunk(rev, df=df) for rev in revschunk]
1536
1538
1537 decomp = self.decompress
1539 decomp = self.decompress
1538 for rev in revschunk:
1540 for rev in revschunk:
1539 chunkstart = start(rev)
1541 chunkstart = start(rev)
1540 if inline:
1542 if inline:
1541 chunkstart += (rev + 1) * iosize
1543 chunkstart += (rev + 1) * iosize
1542 chunklength = length(rev)
1544 chunklength = length(rev)
1543 ladd(decomp(buffer(data, chunkstart - offset, chunklength)))
1545 ladd(decomp(buffer(data, chunkstart - offset, chunklength)))
1544
1546
1545 return l
1547 return l
1546
1548
1547 def _chunkclear(self):
1549 def _chunkclear(self):
1548 """Clear the raw chunk cache."""
1550 """Clear the raw chunk cache."""
1549 self._chunkcache = (0, '')
1551 self._chunkcache = (0, '')
1550
1552
1551 def deltaparent(self, rev):
1553 def deltaparent(self, rev):
1552 """return deltaparent of the given revision"""
1554 """return deltaparent of the given revision"""
1553 base = self.index[rev][3]
1555 base = self.index[rev][3]
1554 if base == rev:
1556 if base == rev:
1555 return nullrev
1557 return nullrev
1556 elif self._generaldelta:
1558 elif self._generaldelta:
1557 return base
1559 return base
1558 else:
1560 else:
1559 return rev - 1
1561 return rev - 1
1560
1562
1561 def issnapshot(self, rev):
1563 def issnapshot(self, rev):
1562 """tells whether rev is a snapshot
1564 """tells whether rev is a snapshot
1563 """
1565 """
1564 if not self._sparserevlog:
1566 if not self._sparserevlog:
1565 return self.deltaparent(rev) == nullrev
1567 return self.deltaparent(rev) == nullrev
1566 elif util.safehasattr(self.index, 'issnapshot'):
1568 elif util.safehasattr(self.index, 'issnapshot'):
1567 # directly assign the method to cache the testing and access
1569 # directly assign the method to cache the testing and access
1568 self.issnapshot = self.index.issnapshot
1570 self.issnapshot = self.index.issnapshot
1569 return self.issnapshot(rev)
1571 return self.issnapshot(rev)
1570 if rev == nullrev:
1572 if rev == nullrev:
1571 return True
1573 return True
1572 entry = self.index[rev]
1574 entry = self.index[rev]
1573 base = entry[3]
1575 base = entry[3]
1574 if base == rev:
1576 if base == rev:
1575 return True
1577 return True
1576 if base == nullrev:
1578 if base == nullrev:
1577 return True
1579 return True
1578 p1 = entry[5]
1580 p1 = entry[5]
1579 p2 = entry[6]
1581 p2 = entry[6]
1580 if base == p1 or base == p2:
1582 if base == p1 or base == p2:
1581 return False
1583 return False
1582 return self.issnapshot(base)
1584 return self.issnapshot(base)
1583
1585
1584 def snapshotdepth(self, rev):
1586 def snapshotdepth(self, rev):
1585 """number of snapshot in the chain before this one"""
1587 """number of snapshot in the chain before this one"""
1586 if not self.issnapshot(rev):
1588 if not self.issnapshot(rev):
1587 raise error.ProgrammingError('revision %d not a snapshot')
1589 raise error.ProgrammingError('revision %d not a snapshot')
1588 return len(self._deltachain(rev)[0]) - 1
1590 return len(self._deltachain(rev)[0]) - 1
1589
1591
1590 def revdiff(self, rev1, rev2):
1592 def revdiff(self, rev1, rev2):
1591 """return or calculate a delta between two revisions
1593 """return or calculate a delta between two revisions
1592
1594
1593 The delta calculated is in binary form and is intended to be written to
1595 The delta calculated is in binary form and is intended to be written to
1594 revlog data directly. So this function needs raw revision data.
1596 revlog data directly. So this function needs raw revision data.
1595 """
1597 """
1596 if rev1 != nullrev and self.deltaparent(rev2) == rev1:
1598 if rev1 != nullrev and self.deltaparent(rev2) == rev1:
1597 return bytes(self._chunk(rev2))
1599 return bytes(self._chunk(rev2))
1598
1600
1599 return mdiff.textdiff(self.rawdata(rev1),
1601 return mdiff.textdiff(self.rawdata(rev1),
1600 self.rawdata(rev2))
1602 self.rawdata(rev2))
1601
1603
1602 def revision(self, nodeorrev, _df=None, raw=False):
1604 def revision(self, nodeorrev, _df=None, raw=False):
1603 """return an uncompressed revision of a given node or revision
1605 """return an uncompressed revision of a given node or revision
1604 number.
1606 number.
1605
1607
1606 _df - an existing file handle to read from. (internal-only)
1608 _df - an existing file handle to read from. (internal-only)
1607 raw - an optional argument specifying if the revision data is to be
1609 raw - an optional argument specifying if the revision data is to be
1608 treated as raw data when applying flag transforms. 'raw' should be set
1610 treated as raw data when applying flag transforms. 'raw' should be set
1609 to True when generating changegroups or in debug commands.
1611 to True when generating changegroups or in debug commands.
1610 """
1612 """
1611 return self._revisiondata(nodeorrev, _df, raw=raw)
1613 return self._revisiondata(nodeorrev, _df, raw=raw)
1612
1614
1613 def _revisiondata(self, nodeorrev, _df=None, raw=False):
1615 def _revisiondata(self, nodeorrev, _df=None, raw=False):
1614 # deal with <nodeorrev> argument type
1616 # deal with <nodeorrev> argument type
1615 if isinstance(nodeorrev, int):
1617 if isinstance(nodeorrev, int):
1616 rev = nodeorrev
1618 rev = nodeorrev
1617 node = self.node(rev)
1619 node = self.node(rev)
1618 else:
1620 else:
1619 node = nodeorrev
1621 node = nodeorrev
1620 rev = None
1622 rev = None
1621
1623
1622 # fast path the special `nullid` rev
1624 # fast path the special `nullid` rev
1623 if node == nullid:
1625 if node == nullid:
1624 return ""
1626 return ""
1625
1627
1626 # The text as stored inside the revlog. Might be the revision or might
1628 # The text as stored inside the revlog. Might be the revision or might
1627 # need to be processed to retrieve the revision.
1629 # need to be processed to retrieve the revision.
1628 rawtext = None
1630 rawtext = None
1629
1631
1630 rev, rawtext, validated = self._rawtext(node, rev, _df=_df)
1632 rev, rawtext, validated = self._rawtext(node, rev, _df=_df)
1631
1633
1632 if raw and validated:
1634 if raw and validated:
1633 # if we don't want to process the raw text and that raw
1635 # if we don't want to process the raw text and that raw
1634 # text is cached, we can exit early.
1636 # text is cached, we can exit early.
1635 return rawtext
1637 return rawtext
1636 if rev is None:
1638 if rev is None:
1637 rev = self.rev(node)
1639 rev = self.rev(node)
1638 # the revlog's flag for this revision
1640 # the revlog's flag for this revision
1639 # (usually alter its state or content)
1641 # (usually alter its state or content)
1640 flags = self.flags(rev)
1642 flags = self.flags(rev)
1641
1643
1642 if validated and flags == REVIDX_DEFAULT_FLAGS:
1644 if validated and flags == REVIDX_DEFAULT_FLAGS:
1643 # no extra flags set, no flag processor runs, text = rawtext
1645 # no extra flags set, no flag processor runs, text = rawtext
1644 return rawtext
1646 return rawtext
1645
1647
1646 text, validatehash = self._processflags(rawtext, flags, 'read', raw=raw)
1648 text, validatehash = self._processflags(rawtext, flags, 'read', raw=raw)
1647 if validatehash:
1649 if validatehash:
1648 self.checkhash(text, node, rev=rev)
1650 self.checkhash(text, node, rev=rev)
1649 if not validated:
1651 if not validated:
1650 self._revisioncache = (node, rev, rawtext)
1652 self._revisioncache = (node, rev, rawtext)
1651
1653
1652 return text
1654 return text
1653
1655
1654 def _rawtext(self, node, rev, _df=None):
1656 def _rawtext(self, node, rev, _df=None):
1655 """return the possibly unvalidated rawtext for a revision
1657 """return the possibly unvalidated rawtext for a revision
1656
1658
1657 returns (rev, rawtext, validated)
1659 returns (rev, rawtext, validated)
1658 """
1660 """
1659
1661
1660 # revision in the cache (could be useful to apply delta)
1662 # revision in the cache (could be useful to apply delta)
1661 cachedrev = None
1663 cachedrev = None
1662 # An intermediate text to apply deltas to
1664 # An intermediate text to apply deltas to
1663 basetext = None
1665 basetext = None
1664
1666
1665 # Check if we have the entry in cache
1667 # Check if we have the entry in cache
1666 # The cache entry looks like (node, rev, rawtext)
1668 # The cache entry looks like (node, rev, rawtext)
1667 if self._revisioncache:
1669 if self._revisioncache:
1668 if self._revisioncache[0] == node:
1670 if self._revisioncache[0] == node:
1669 return (rev, self._revisioncache[2], True)
1671 return (rev, self._revisioncache[2], True)
1670 cachedrev = self._revisioncache[1]
1672 cachedrev = self._revisioncache[1]
1671
1673
1672 if rev is None:
1674 if rev is None:
1673 rev = self.rev(node)
1675 rev = self.rev(node)
1674
1676
1675 chain, stopped = self._deltachain(rev, stoprev=cachedrev)
1677 chain, stopped = self._deltachain(rev, stoprev=cachedrev)
1676 if stopped:
1678 if stopped:
1677 basetext = self._revisioncache[2]
1679 basetext = self._revisioncache[2]
1678
1680
1679 # drop cache to save memory, the caller is expected to
1681 # drop cache to save memory, the caller is expected to
1680 # update self._revisioncache after validating the text
1682 # update self._revisioncache after validating the text
1681 self._revisioncache = None
1683 self._revisioncache = None
1682
1684
1683 targetsize = None
1685 targetsize = None
1684 rawsize = self.index[rev][2]
1686 rawsize = self.index[rev][2]
1685 if 0 <= rawsize:
1687 if 0 <= rawsize:
1686 targetsize = 4 * rawsize
1688 targetsize = 4 * rawsize
1687
1689
1688 bins = self._chunks(chain, df=_df, targetsize=targetsize)
1690 bins = self._chunks(chain, df=_df, targetsize=targetsize)
1689 if basetext is None:
1691 if basetext is None:
1690 basetext = bytes(bins[0])
1692 basetext = bytes(bins[0])
1691 bins = bins[1:]
1693 bins = bins[1:]
1692
1694
1693 rawtext = mdiff.patches(basetext, bins)
1695 rawtext = mdiff.patches(basetext, bins)
1694 del basetext # let us have a chance to free memory early
1696 del basetext # let us have a chance to free memory early
1695 return (rev, rawtext, False)
1697 return (rev, rawtext, False)
1696
1698
1697 def rawdata(self, nodeorrev, _df=None):
1699 def rawdata(self, nodeorrev, _df=None):
1698 """return an uncompressed raw data of a given node or revision number.
1700 """return an uncompressed raw data of a given node or revision number.
1699
1701
1700 _df - an existing file handle to read from. (internal-only)
1702 _df - an existing file handle to read from. (internal-only)
1701 """
1703 """
1702 return self._revisiondata(nodeorrev, _df, raw=True)
1704 return self._revisiondata(nodeorrev, _df, raw=True)
1703
1705
1704 def hash(self, text, p1, p2):
1706 def hash(self, text, p1, p2):
1705 """Compute a node hash.
1707 """Compute a node hash.
1706
1708
1707 Available as a function so that subclasses can replace the hash
1709 Available as a function so that subclasses can replace the hash
1708 as needed.
1710 as needed.
1709 """
1711 """
1710 return storageutil.hashrevisionsha1(text, p1, p2)
1712 return storageutil.hashrevisionsha1(text, p1, p2)
1711
1713
1712 def _processflags(self, text, flags, operation, raw=False):
1714 def _processflags(self, text, flags, operation, raw=False):
1713 """Inspect revision data flags and applies transforms defined by
1715 """Inspect revision data flags and applies transforms defined by
1714 registered flag processors.
1716 registered flag processors.
1715
1717
1716 ``text`` - the revision data to process
1718 ``text`` - the revision data to process
1717 ``flags`` - the revision flags
1719 ``flags`` - the revision flags
1718 ``operation`` - the operation being performed (read or write)
1720 ``operation`` - the operation being performed (read or write)
1719 ``raw`` - an optional argument describing if the raw transform should be
1721 ``raw`` - an optional argument describing if the raw transform should be
1720 applied.
1722 applied.
1721
1723
1722 This method processes the flags in the order (or reverse order if
1724 This method processes the flags in the order (or reverse order if
1723 ``operation`` is 'write') defined by REVIDX_FLAGS_ORDER, applying the
1725 ``operation`` is 'write') defined by REVIDX_FLAGS_ORDER, applying the
1724 flag processors registered for present flags. The order of flags defined
1726 flag processors registered for present flags. The order of flags defined
1725 in REVIDX_FLAGS_ORDER needs to be stable to allow non-commutativity.
1727 in REVIDX_FLAGS_ORDER needs to be stable to allow non-commutativity.
1726
1728
1727 Returns a 2-tuple of ``(text, validatehash)`` where ``text`` is the
1729 Returns a 2-tuple of ``(text, validatehash)`` where ``text`` is the
1728 processed text and ``validatehash`` is a bool indicating whether the
1730 processed text and ``validatehash`` is a bool indicating whether the
1729 returned text should be checked for hash integrity.
1731 returned text should be checked for hash integrity.
1730
1732
1731 Note: If the ``raw`` argument is set, it has precedence over the
1733 Note: If the ``raw`` argument is set, it has precedence over the
1732 operation and will only update the value of ``validatehash``.
1734 operation and will only update the value of ``validatehash``.
1733 """
1735 """
1734 # fast path: no flag processors will run
1736 # fast path: no flag processors will run
1735 if flags == 0:
1737 if flags == 0:
1736 return text, True
1738 return text, True
1737 if not operation in ('read', 'write'):
1739 if not operation in ('read', 'write'):
1738 raise error.ProgrammingError(_("invalid '%s' operation") %
1740 raise error.ProgrammingError(_("invalid '%s' operation") %
1739 operation)
1741 operation)
1740 # Check all flags are known.
1742 # Check all flags are known.
1741 if flags & ~flagutil.REVIDX_KNOWN_FLAGS:
1743 if flags & ~flagutil.REVIDX_KNOWN_FLAGS:
1742 raise error.RevlogError(_("incompatible revision flag '%#x'") %
1744 raise error.RevlogError(_("incompatible revision flag '%#x'") %
1743 (flags & ~flagutil.REVIDX_KNOWN_FLAGS))
1745 (flags & ~flagutil.REVIDX_KNOWN_FLAGS))
1744 validatehash = True
1746 validatehash = True
1745 # Depending on the operation (read or write), the order might be
1747 # Depending on the operation (read or write), the order might be
1746 # reversed due to non-commutative transforms.
1748 # reversed due to non-commutative transforms.
1747 orderedflags = REVIDX_FLAGS_ORDER
1749 orderedflags = REVIDX_FLAGS_ORDER
1748 if operation == 'write':
1750 if operation == 'write':
1749 orderedflags = reversed(orderedflags)
1751 orderedflags = reversed(orderedflags)
1750
1752
1751 for flag in orderedflags:
1753 for flag in orderedflags:
1752 # If a flagprocessor has been registered for a known flag, apply the
1754 # If a flagprocessor has been registered for a known flag, apply the
1753 # related operation transform and update result tuple.
1755 # related operation transform and update result tuple.
1754 if flag & flags:
1756 if flag & flags:
1755 vhash = True
1757 vhash = True
1756
1758
1757 if flag not in self._flagprocessors:
1759 if flag not in self._flagprocessors:
1758 message = _("missing processor for flag '%#x'") % (flag)
1760 message = _("missing processor for flag '%#x'") % (flag)
1759 raise error.RevlogError(message)
1761 raise error.RevlogError(message)
1760
1762
1761 processor = self._flagprocessors[flag]
1763 processor = self._flagprocessors[flag]
1762 if processor is not None:
1764 if processor is not None:
1763 readtransform, writetransform, rawtransform = processor
1765 readtransform, writetransform, rawtransform = processor
1764
1766
1765 if raw:
1767 if raw:
1766 vhash = rawtransform(self, text)
1768 vhash = rawtransform(self, text)
1767 elif operation == 'read':
1769 elif operation == 'read':
1768 text, vhash = readtransform(self, text)
1770 text, vhash = readtransform(self, text)
1769 else: # write operation
1771 else: # write operation
1770 text, vhash = writetransform(self, text)
1772 text, vhash = writetransform(self, text)
1771 validatehash = validatehash and vhash
1773 validatehash = validatehash and vhash
1772
1774
1773 return text, validatehash
1775 return text, validatehash
1774
1776
1775 def checkhash(self, text, node, p1=None, p2=None, rev=None):
1777 def checkhash(self, text, node, p1=None, p2=None, rev=None):
1776 """Check node hash integrity.
1778 """Check node hash integrity.
1777
1779
1778 Available as a function so that subclasses can extend hash mismatch
1780 Available as a function so that subclasses can extend hash mismatch
1779 behaviors as needed.
1781 behaviors as needed.
1780 """
1782 """
1781 try:
1783 try:
1782 if p1 is None and p2 is None:
1784 if p1 is None and p2 is None:
1783 p1, p2 = self.parents(node)
1785 p1, p2 = self.parents(node)
1784 if node != self.hash(text, p1, p2):
1786 if node != self.hash(text, p1, p2):
1785 # Clear the revision cache on hash failure. The revision cache
1787 # Clear the revision cache on hash failure. The revision cache
1786 # only stores the raw revision and clearing the cache does have
1788 # only stores the raw revision and clearing the cache does have
1787 # the side-effect that we won't have a cache hit when the raw
1789 # the side-effect that we won't have a cache hit when the raw
1788 # revision data is accessed. But this case should be rare and
1790 # revision data is accessed. But this case should be rare and
1789 # it is extra work to teach the cache about the hash
1791 # it is extra work to teach the cache about the hash
1790 # verification state.
1792 # verification state.
1791 if self._revisioncache and self._revisioncache[0] == node:
1793 if self._revisioncache and self._revisioncache[0] == node:
1792 self._revisioncache = None
1794 self._revisioncache = None
1793
1795
1794 revornode = rev
1796 revornode = rev
1795 if revornode is None:
1797 if revornode is None:
1796 revornode = templatefilters.short(hex(node))
1798 revornode = templatefilters.short(hex(node))
1797 raise error.RevlogError(_("integrity check failed on %s:%s")
1799 raise error.RevlogError(_("integrity check failed on %s:%s")
1798 % (self.indexfile, pycompat.bytestr(revornode)))
1800 % (self.indexfile, pycompat.bytestr(revornode)))
1799 except error.RevlogError:
1801 except error.RevlogError:
1800 if self._censorable and storageutil.iscensoredtext(text):
1802 if self._censorable and storageutil.iscensoredtext(text):
1801 raise error.CensoredNodeError(self.indexfile, node, text)
1803 raise error.CensoredNodeError(self.indexfile, node, text)
1802 raise
1804 raise
1803
1805
1804 def _enforceinlinesize(self, tr, fp=None):
1806 def _enforceinlinesize(self, tr, fp=None):
1805 """Check if the revlog is too big for inline and convert if so.
1807 """Check if the revlog is too big for inline and convert if so.
1806
1808
1807 This should be called after revisions are added to the revlog. If the
1809 This should be called after revisions are added to the revlog. If the
1808 revlog has grown too large to be an inline revlog, it will convert it
1810 revlog has grown too large to be an inline revlog, it will convert it
1809 to use multiple index and data files.
1811 to use multiple index and data files.
1810 """
1812 """
1811 tiprev = len(self) - 1
1813 tiprev = len(self) - 1
1812 if (not self._inline or
1814 if (not self._inline or
1813 (self.start(tiprev) + self.length(tiprev)) < _maxinline):
1815 (self.start(tiprev) + self.length(tiprev)) < _maxinline):
1814 return
1816 return
1815
1817
1816 trinfo = tr.find(self.indexfile)
1818 trinfo = tr.find(self.indexfile)
1817 if trinfo is None:
1819 if trinfo is None:
1818 raise error.RevlogError(_("%s not found in the transaction")
1820 raise error.RevlogError(_("%s not found in the transaction")
1819 % self.indexfile)
1821 % self.indexfile)
1820
1822
1821 trindex = trinfo[2]
1823 trindex = trinfo[2]
1822 if trindex is not None:
1824 if trindex is not None:
1823 dataoff = self.start(trindex)
1825 dataoff = self.start(trindex)
1824 else:
1826 else:
1825 # revlog was stripped at start of transaction, use all leftover data
1827 # revlog was stripped at start of transaction, use all leftover data
1826 trindex = len(self) - 1
1828 trindex = len(self) - 1
1827 dataoff = self.end(tiprev)
1829 dataoff = self.end(tiprev)
1828
1830
1829 tr.add(self.datafile, dataoff)
1831 tr.add(self.datafile, dataoff)
1830
1832
1831 if fp:
1833 if fp:
1832 fp.flush()
1834 fp.flush()
1833 fp.close()
1835 fp.close()
1834 # We can't use the cached file handle after close(). So prevent
1836 # We can't use the cached file handle after close(). So prevent
1835 # its usage.
1837 # its usage.
1836 self._writinghandles = None
1838 self._writinghandles = None
1837
1839
1838 with self._indexfp('r') as ifh, self._datafp('w') as dfh:
1840 with self._indexfp('r') as ifh, self._datafp('w') as dfh:
1839 for r in self:
1841 for r in self:
1840 dfh.write(self._getsegmentforrevs(r, r, df=ifh)[1])
1842 dfh.write(self._getsegmentforrevs(r, r, df=ifh)[1])
1841
1843
1842 with self._indexfp('w') as fp:
1844 with self._indexfp('w') as fp:
1843 self.version &= ~FLAG_INLINE_DATA
1845 self.version &= ~FLAG_INLINE_DATA
1844 self._inline = False
1846 self._inline = False
1845 io = self._io
1847 io = self._io
1846 for i in self:
1848 for i in self:
1847 e = io.packentry(self.index[i], self.node, self.version, i)
1849 e = io.packentry(self.index[i], self.node, self.version, i)
1848 fp.write(e)
1850 fp.write(e)
1849
1851
1850 # the temp file replace the real index when we exit the context
1852 # the temp file replace the real index when we exit the context
1851 # manager
1853 # manager
1852
1854
1853 tr.replace(self.indexfile, trindex * self._io.size)
1855 tr.replace(self.indexfile, trindex * self._io.size)
1854 self._chunkclear()
1856 self._chunkclear()
1855
1857
1856 def _nodeduplicatecallback(self, transaction, node):
1858 def _nodeduplicatecallback(self, transaction, node):
1857 """called when trying to add a node already stored.
1859 """called when trying to add a node already stored.
1858 """
1860 """
1859
1861
1860 def addrevision(self, text, transaction, link, p1, p2, cachedelta=None,
1862 def addrevision(self, text, transaction, link, p1, p2, cachedelta=None,
1861 node=None, flags=REVIDX_DEFAULT_FLAGS, deltacomputer=None):
1863 node=None, flags=REVIDX_DEFAULT_FLAGS, deltacomputer=None):
1862 """add a revision to the log
1864 """add a revision to the log
1863
1865
1864 text - the revision data to add
1866 text - the revision data to add
1865 transaction - the transaction object used for rollback
1867 transaction - the transaction object used for rollback
1866 link - the linkrev data to add
1868 link - the linkrev data to add
1867 p1, p2 - the parent nodeids of the revision
1869 p1, p2 - the parent nodeids of the revision
1868 cachedelta - an optional precomputed delta
1870 cachedelta - an optional precomputed delta
1869 node - nodeid of revision; typically node is not specified, and it is
1871 node - nodeid of revision; typically node is not specified, and it is
1870 computed by default as hash(text, p1, p2), however subclasses might
1872 computed by default as hash(text, p1, p2), however subclasses might
1871 use different hashing method (and override checkhash() in such case)
1873 use different hashing method (and override checkhash() in such case)
1872 flags - the known flags to set on the revision
1874 flags - the known flags to set on the revision
1873 deltacomputer - an optional deltacomputer instance shared between
1875 deltacomputer - an optional deltacomputer instance shared between
1874 multiple calls
1876 multiple calls
1875 """
1877 """
1876 if link == nullrev:
1878 if link == nullrev:
1877 raise error.RevlogError(_("attempted to add linkrev -1 to %s")
1879 raise error.RevlogError(_("attempted to add linkrev -1 to %s")
1878 % self.indexfile)
1880 % self.indexfile)
1879
1881
1880 if flags:
1882 if flags:
1881 node = node or self.hash(text, p1, p2)
1883 node = node or self.hash(text, p1, p2)
1882
1884
1883 rawtext, validatehash = self._processflags(text, flags, 'write')
1885 rawtext, validatehash = self._processflags(text, flags, 'write')
1884
1886
1885 # If the flag processor modifies the revision data, ignore any provided
1887 # If the flag processor modifies the revision data, ignore any provided
1886 # cachedelta.
1888 # cachedelta.
1887 if rawtext != text:
1889 if rawtext != text:
1888 cachedelta = None
1890 cachedelta = None
1889
1891
1890 if len(rawtext) > _maxentrysize:
1892 if len(rawtext) > _maxentrysize:
1891 raise error.RevlogError(
1893 raise error.RevlogError(
1892 _("%s: size of %d bytes exceeds maximum revlog storage of 2GiB")
1894 _("%s: size of %d bytes exceeds maximum revlog storage of 2GiB")
1893 % (self.indexfile, len(rawtext)))
1895 % (self.indexfile, len(rawtext)))
1894
1896
1895 node = node or self.hash(rawtext, p1, p2)
1897 node = node or self.hash(rawtext, p1, p2)
1896 if node in self.nodemap:
1898 if node in self.nodemap:
1897 return node
1899 return node
1898
1900
1899 if validatehash:
1901 if validatehash:
1900 self.checkhash(rawtext, node, p1=p1, p2=p2)
1902 self.checkhash(rawtext, node, p1=p1, p2=p2)
1901
1903
1902 return self.addrawrevision(rawtext, transaction, link, p1, p2, node,
1904 return self.addrawrevision(rawtext, transaction, link, p1, p2, node,
1903 flags, cachedelta=cachedelta,
1905 flags, cachedelta=cachedelta,
1904 deltacomputer=deltacomputer)
1906 deltacomputer=deltacomputer)
1905
1907
1906 def addrawrevision(self, rawtext, transaction, link, p1, p2, node, flags,
1908 def addrawrevision(self, rawtext, transaction, link, p1, p2, node, flags,
1907 cachedelta=None, deltacomputer=None):
1909 cachedelta=None, deltacomputer=None):
1908 """add a raw revision with known flags, node and parents
1910 """add a raw revision with known flags, node and parents
1909 useful when reusing a revision not stored in this revlog (ex: received
1911 useful when reusing a revision not stored in this revlog (ex: received
1910 over wire, or read from an external bundle).
1912 over wire, or read from an external bundle).
1911 """
1913 """
1912 dfh = None
1914 dfh = None
1913 if not self._inline:
1915 if not self._inline:
1914 dfh = self._datafp("a+")
1916 dfh = self._datafp("a+")
1915 ifh = self._indexfp("a+")
1917 ifh = self._indexfp("a+")
1916 try:
1918 try:
1917 return self._addrevision(node, rawtext, transaction, link, p1, p2,
1919 return self._addrevision(node, rawtext, transaction, link, p1, p2,
1918 flags, cachedelta, ifh, dfh,
1920 flags, cachedelta, ifh, dfh,
1919 deltacomputer=deltacomputer)
1921 deltacomputer=deltacomputer)
1920 finally:
1922 finally:
1921 if dfh:
1923 if dfh:
1922 dfh.close()
1924 dfh.close()
1923 ifh.close()
1925 ifh.close()
1924
1926
1925 def compress(self, data):
1927 def compress(self, data):
1926 """Generate a possibly-compressed representation of data."""
1928 """Generate a possibly-compressed representation of data."""
1927 if not data:
1929 if not data:
1928 return '', data
1930 return '', data
1929
1931
1930 compressed = self._compressor.compress(data)
1932 compressed = self._compressor.compress(data)
1931
1933
1932 if compressed:
1934 if compressed:
1933 # The revlog compressor added the header in the returned data.
1935 # The revlog compressor added the header in the returned data.
1934 return '', compressed
1936 return '', compressed
1935
1937
1936 if data[0:1] == '\0':
1938 if data[0:1] == '\0':
1937 return '', data
1939 return '', data
1938 return 'u', data
1940 return 'u', data
1939
1941
1940 def decompress(self, data):
1942 def decompress(self, data):
1941 """Decompress a revlog chunk.
1943 """Decompress a revlog chunk.
1942
1944
1943 The chunk is expected to begin with a header identifying the
1945 The chunk is expected to begin with a header identifying the
1944 format type so it can be routed to an appropriate decompressor.
1946 format type so it can be routed to an appropriate decompressor.
1945 """
1947 """
1946 if not data:
1948 if not data:
1947 return data
1949 return data
1948
1950
1949 # Revlogs are read much more frequently than they are written and many
1951 # Revlogs are read much more frequently than they are written and many
1950 # chunks only take microseconds to decompress, so performance is
1952 # chunks only take microseconds to decompress, so performance is
1951 # important here.
1953 # important here.
1952 #
1954 #
1953 # We can make a few assumptions about revlogs:
1955 # We can make a few assumptions about revlogs:
1954 #
1956 #
1955 # 1) the majority of chunks will be compressed (as opposed to inline
1957 # 1) the majority of chunks will be compressed (as opposed to inline
1956 # raw data).
1958 # raw data).
1957 # 2) decompressing *any* data will likely by at least 10x slower than
1959 # 2) decompressing *any* data will likely by at least 10x slower than
1958 # returning raw inline data.
1960 # returning raw inline data.
1959 # 3) we want to prioritize common and officially supported compression
1961 # 3) we want to prioritize common and officially supported compression
1960 # engines
1962 # engines
1961 #
1963 #
1962 # It follows that we want to optimize for "decompress compressed data
1964 # It follows that we want to optimize for "decompress compressed data
1963 # when encoded with common and officially supported compression engines"
1965 # when encoded with common and officially supported compression engines"
1964 # case over "raw data" and "data encoded by less common or non-official
1966 # case over "raw data" and "data encoded by less common or non-official
1965 # compression engines." That is why we have the inline lookup first
1967 # compression engines." That is why we have the inline lookup first
1966 # followed by the compengines lookup.
1968 # followed by the compengines lookup.
1967 #
1969 #
1968 # According to `hg perfrevlogchunks`, this is ~0.5% faster for zlib
1970 # According to `hg perfrevlogchunks`, this is ~0.5% faster for zlib
1969 # compressed chunks. And this matters for changelog and manifest reads.
1971 # compressed chunks. And this matters for changelog and manifest reads.
1970 t = data[0:1]
1972 t = data[0:1]
1971
1973
1972 if t == 'x':
1974 if t == 'x':
1973 try:
1975 try:
1974 return _zlibdecompress(data)
1976 return _zlibdecompress(data)
1975 except zlib.error as e:
1977 except zlib.error as e:
1976 raise error.RevlogError(_('revlog decompress error: %s') %
1978 raise error.RevlogError(_('revlog decompress error: %s') %
1977 stringutil.forcebytestr(e))
1979 stringutil.forcebytestr(e))
1978 # '\0' is more common than 'u' so it goes first.
1980 # '\0' is more common than 'u' so it goes first.
1979 elif t == '\0':
1981 elif t == '\0':
1980 return data
1982 return data
1981 elif t == 'u':
1983 elif t == 'u':
1982 return util.buffer(data, 1)
1984 return util.buffer(data, 1)
1983
1985
1984 try:
1986 try:
1985 compressor = self._decompressors[t]
1987 compressor = self._decompressors[t]
1986 except KeyError:
1988 except KeyError:
1987 try:
1989 try:
1988 engine = util.compengines.forrevlogheader(t)
1990 engine = util.compengines.forrevlogheader(t)
1989 compressor = engine.revlogcompressor(self._compengineopts)
1991 compressor = engine.revlogcompressor(self._compengineopts)
1990 self._decompressors[t] = compressor
1992 self._decompressors[t] = compressor
1991 except KeyError:
1993 except KeyError:
1992 raise error.RevlogError(_('unknown compression type %r') % t)
1994 raise error.RevlogError(_('unknown compression type %r') % t)
1993
1995
1994 return compressor.decompress(data)
1996 return compressor.decompress(data)
1995
1997
1996 def _addrevision(self, node, rawtext, transaction, link, p1, p2, flags,
1998 def _addrevision(self, node, rawtext, transaction, link, p1, p2, flags,
1997 cachedelta, ifh, dfh, alwayscache=False,
1999 cachedelta, ifh, dfh, alwayscache=False,
1998 deltacomputer=None):
2000 deltacomputer=None):
1999 """internal function to add revisions to the log
2001 """internal function to add revisions to the log
2000
2002
2001 see addrevision for argument descriptions.
2003 see addrevision for argument descriptions.
2002
2004
2003 note: "addrevision" takes non-raw text, "_addrevision" takes raw text.
2005 note: "addrevision" takes non-raw text, "_addrevision" takes raw text.
2004
2006
2005 if "deltacomputer" is not provided or None, a defaultdeltacomputer will
2007 if "deltacomputer" is not provided or None, a defaultdeltacomputer will
2006 be used.
2008 be used.
2007
2009
2008 invariants:
2010 invariants:
2009 - rawtext is optional (can be None); if not set, cachedelta must be set.
2011 - rawtext is optional (can be None); if not set, cachedelta must be set.
2010 if both are set, they must correspond to each other.
2012 if both are set, they must correspond to each other.
2011 """
2013 """
2012 if node == nullid:
2014 if node == nullid:
2013 raise error.RevlogError(_("%s: attempt to add null revision") %
2015 raise error.RevlogError(_("%s: attempt to add null revision") %
2014 self.indexfile)
2016 self.indexfile)
2015 if node == wdirid or node in wdirfilenodeids:
2017 if node == wdirid or node in wdirfilenodeids:
2016 raise error.RevlogError(_("%s: attempt to add wdir revision") %
2018 raise error.RevlogError(_("%s: attempt to add wdir revision") %
2017 self.indexfile)
2019 self.indexfile)
2018
2020
2019 if self._inline:
2021 if self._inline:
2020 fh = ifh
2022 fh = ifh
2021 else:
2023 else:
2022 fh = dfh
2024 fh = dfh
2023
2025
2024 btext = [rawtext]
2026 btext = [rawtext]
2025
2027
2026 curr = len(self)
2028 curr = len(self)
2027 prev = curr - 1
2029 prev = curr - 1
2028 offset = self.end(prev)
2030 offset = self.end(prev)
2029 p1r, p2r = self.rev(p1), self.rev(p2)
2031 p1r, p2r = self.rev(p1), self.rev(p2)
2030
2032
2031 # full versions are inserted when the needed deltas
2033 # full versions are inserted when the needed deltas
2032 # become comparable to the uncompressed text
2034 # become comparable to the uncompressed text
2033 if rawtext is None:
2035 if rawtext is None:
2034 # need rawtext size, before changed by flag processors, which is
2036 # need rawtext size, before changed by flag processors, which is
2035 # the non-raw size. use revlog explicitly to avoid filelog's extra
2037 # the non-raw size. use revlog explicitly to avoid filelog's extra
2036 # logic that might remove metadata size.
2038 # logic that might remove metadata size.
2037 textlen = mdiff.patchedsize(revlog.size(self, cachedelta[0]),
2039 textlen = mdiff.patchedsize(revlog.size(self, cachedelta[0]),
2038 cachedelta[1])
2040 cachedelta[1])
2039 else:
2041 else:
2040 textlen = len(rawtext)
2042 textlen = len(rawtext)
2041
2043
2042 if deltacomputer is None:
2044 if deltacomputer is None:
2043 deltacomputer = deltautil.deltacomputer(self)
2045 deltacomputer = deltautil.deltacomputer(self)
2044
2046
2045 revinfo = _revisioninfo(node, p1, p2, btext, textlen, cachedelta, flags)
2047 revinfo = _revisioninfo(node, p1, p2, btext, textlen, cachedelta, flags)
2046
2048
2047 deltainfo = deltacomputer.finddeltainfo(revinfo, fh)
2049 deltainfo = deltacomputer.finddeltainfo(revinfo, fh)
2048
2050
2049 e = (offset_type(offset, flags), deltainfo.deltalen, textlen,
2051 e = (offset_type(offset, flags), deltainfo.deltalen, textlen,
2050 deltainfo.base, link, p1r, p2r, node)
2052 deltainfo.base, link, p1r, p2r, node)
2051 self.index.append(e)
2053 self.index.append(e)
2052 self.nodemap[node] = curr
2054 self.nodemap[node] = curr
2053
2055
2054 # Reset the pure node cache start lookup offset to account for new
2056 # Reset the pure node cache start lookup offset to account for new
2055 # revision.
2057 # revision.
2056 if self._nodepos is not None:
2058 if self._nodepos is not None:
2057 self._nodepos = curr
2059 self._nodepos = curr
2058
2060
2059 entry = self._io.packentry(e, self.node, self.version, curr)
2061 entry = self._io.packentry(e, self.node, self.version, curr)
2060 self._writeentry(transaction, ifh, dfh, entry, deltainfo.data,
2062 self._writeentry(transaction, ifh, dfh, entry, deltainfo.data,
2061 link, offset)
2063 link, offset)
2062
2064
2063 rawtext = btext[0]
2065 rawtext = btext[0]
2064
2066
2065 if alwayscache and rawtext is None:
2067 if alwayscache and rawtext is None:
2066 rawtext = deltacomputer.buildtext(revinfo, fh)
2068 rawtext = deltacomputer.buildtext(revinfo, fh)
2067
2069
2068 if type(rawtext) == bytes: # only accept immutable objects
2070 if type(rawtext) == bytes: # only accept immutable objects
2069 self._revisioncache = (node, curr, rawtext)
2071 self._revisioncache = (node, curr, rawtext)
2070 self._chainbasecache[curr] = deltainfo.chainbase
2072 self._chainbasecache[curr] = deltainfo.chainbase
2071 return node
2073 return node
2072
2074
2073 def _writeentry(self, transaction, ifh, dfh, entry, data, link, offset):
2075 def _writeentry(self, transaction, ifh, dfh, entry, data, link, offset):
2074 # Files opened in a+ mode have inconsistent behavior on various
2076 # Files opened in a+ mode have inconsistent behavior on various
2075 # platforms. Windows requires that a file positioning call be made
2077 # platforms. Windows requires that a file positioning call be made
2076 # when the file handle transitions between reads and writes. See
2078 # when the file handle transitions between reads and writes. See
2077 # 3686fa2b8eee and the mixedfilemodewrapper in windows.py. On other
2079 # 3686fa2b8eee and the mixedfilemodewrapper in windows.py. On other
2078 # platforms, Python or the platform itself can be buggy. Some versions
2080 # platforms, Python or the platform itself can be buggy. Some versions
2079 # of Solaris have been observed to not append at the end of the file
2081 # of Solaris have been observed to not append at the end of the file
2080 # if the file was seeked to before the end. See issue4943 for more.
2082 # if the file was seeked to before the end. See issue4943 for more.
2081 #
2083 #
2082 # We work around this issue by inserting a seek() before writing.
2084 # We work around this issue by inserting a seek() before writing.
2083 # Note: This is likely not necessary on Python 3. However, because
2085 # Note: This is likely not necessary on Python 3. However, because
2084 # the file handle is reused for reads and may be seeked there, we need
2086 # the file handle is reused for reads and may be seeked there, we need
2085 # to be careful before changing this.
2087 # to be careful before changing this.
2086 ifh.seek(0, os.SEEK_END)
2088 ifh.seek(0, os.SEEK_END)
2087 if dfh:
2089 if dfh:
2088 dfh.seek(0, os.SEEK_END)
2090 dfh.seek(0, os.SEEK_END)
2089
2091
2090 curr = len(self) - 1
2092 curr = len(self) - 1
2091 if not self._inline:
2093 if not self._inline:
2092 transaction.add(self.datafile, offset)
2094 transaction.add(self.datafile, offset)
2093 transaction.add(self.indexfile, curr * len(entry))
2095 transaction.add(self.indexfile, curr * len(entry))
2094 if data[0]:
2096 if data[0]:
2095 dfh.write(data[0])
2097 dfh.write(data[0])
2096 dfh.write(data[1])
2098 dfh.write(data[1])
2097 ifh.write(entry)
2099 ifh.write(entry)
2098 else:
2100 else:
2099 offset += curr * self._io.size
2101 offset += curr * self._io.size
2100 transaction.add(self.indexfile, offset, curr)
2102 transaction.add(self.indexfile, offset, curr)
2101 ifh.write(entry)
2103 ifh.write(entry)
2102 ifh.write(data[0])
2104 ifh.write(data[0])
2103 ifh.write(data[1])
2105 ifh.write(data[1])
2104 self._enforceinlinesize(transaction, ifh)
2106 self._enforceinlinesize(transaction, ifh)
2105
2107
2106 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None):
2108 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None):
2107 """
2109 """
2108 add a delta group
2110 add a delta group
2109
2111
2110 given a set of deltas, add them to the revision log. the
2112 given a set of deltas, add them to the revision log. the
2111 first delta is against its parent, which should be in our
2113 first delta is against its parent, which should be in our
2112 log, the rest are against the previous delta.
2114 log, the rest are against the previous delta.
2113
2115
2114 If ``addrevisioncb`` is defined, it will be called with arguments of
2116 If ``addrevisioncb`` is defined, it will be called with arguments of
2115 this revlog and the node that was added.
2117 this revlog and the node that was added.
2116 """
2118 """
2117
2119
2118 if self._writinghandles:
2120 if self._writinghandles:
2119 raise error.ProgrammingError('cannot nest addgroup() calls')
2121 raise error.ProgrammingError('cannot nest addgroup() calls')
2120
2122
2121 nodes = []
2123 nodes = []
2122
2124
2123 r = len(self)
2125 r = len(self)
2124 end = 0
2126 end = 0
2125 if r:
2127 if r:
2126 end = self.end(r - 1)
2128 end = self.end(r - 1)
2127 ifh = self._indexfp("a+")
2129 ifh = self._indexfp("a+")
2128 isize = r * self._io.size
2130 isize = r * self._io.size
2129 if self._inline:
2131 if self._inline:
2130 transaction.add(self.indexfile, end + isize, r)
2132 transaction.add(self.indexfile, end + isize, r)
2131 dfh = None
2133 dfh = None
2132 else:
2134 else:
2133 transaction.add(self.indexfile, isize, r)
2135 transaction.add(self.indexfile, isize, r)
2134 transaction.add(self.datafile, end)
2136 transaction.add(self.datafile, end)
2135 dfh = self._datafp("a+")
2137 dfh = self._datafp("a+")
2136 def flush():
2138 def flush():
2137 if dfh:
2139 if dfh:
2138 dfh.flush()
2140 dfh.flush()
2139 ifh.flush()
2141 ifh.flush()
2140
2142
2141 self._writinghandles = (ifh, dfh)
2143 self._writinghandles = (ifh, dfh)
2142
2144
2143 try:
2145 try:
2144 deltacomputer = deltautil.deltacomputer(self)
2146 deltacomputer = deltautil.deltacomputer(self)
2145 # loop through our set of deltas
2147 # loop through our set of deltas
2146 for data in deltas:
2148 for data in deltas:
2147 node, p1, p2, linknode, deltabase, delta, flags = data
2149 node, p1, p2, linknode, deltabase, delta, flags = data
2148 link = linkmapper(linknode)
2150 link = linkmapper(linknode)
2149 flags = flags or REVIDX_DEFAULT_FLAGS
2151 flags = flags or REVIDX_DEFAULT_FLAGS
2150
2152
2151 nodes.append(node)
2153 nodes.append(node)
2152
2154
2153 if node in self.nodemap:
2155 if node in self.nodemap:
2154 self._nodeduplicatecallback(transaction, node)
2156 self._nodeduplicatecallback(transaction, node)
2155 # this can happen if two branches make the same change
2157 # this can happen if two branches make the same change
2156 continue
2158 continue
2157
2159
2158 for p in (p1, p2):
2160 for p in (p1, p2):
2159 if p not in self.nodemap:
2161 if p not in self.nodemap:
2160 raise error.LookupError(p, self.indexfile,
2162 raise error.LookupError(p, self.indexfile,
2161 _('unknown parent'))
2163 _('unknown parent'))
2162
2164
2163 if deltabase not in self.nodemap:
2165 if deltabase not in self.nodemap:
2164 raise error.LookupError(deltabase, self.indexfile,
2166 raise error.LookupError(deltabase, self.indexfile,
2165 _('unknown delta base'))
2167 _('unknown delta base'))
2166
2168
2167 baserev = self.rev(deltabase)
2169 baserev = self.rev(deltabase)
2168
2170
2169 if baserev != nullrev and self.iscensored(baserev):
2171 if baserev != nullrev and self.iscensored(baserev):
2170 # if base is censored, delta must be full replacement in a
2172 # if base is censored, delta must be full replacement in a
2171 # single patch operation
2173 # single patch operation
2172 hlen = struct.calcsize(">lll")
2174 hlen = struct.calcsize(">lll")
2173 oldlen = self.rawsize(baserev)
2175 oldlen = self.rawsize(baserev)
2174 newlen = len(delta) - hlen
2176 newlen = len(delta) - hlen
2175 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
2177 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
2176 raise error.CensoredBaseError(self.indexfile,
2178 raise error.CensoredBaseError(self.indexfile,
2177 self.node(baserev))
2179 self.node(baserev))
2178
2180
2179 if not flags and self._peek_iscensored(baserev, delta, flush):
2181 if not flags and self._peek_iscensored(baserev, delta, flush):
2180 flags |= REVIDX_ISCENSORED
2182 flags |= REVIDX_ISCENSORED
2181
2183
2182 # We assume consumers of addrevisioncb will want to retrieve
2184 # We assume consumers of addrevisioncb will want to retrieve
2183 # the added revision, which will require a call to
2185 # the added revision, which will require a call to
2184 # revision(). revision() will fast path if there is a cache
2186 # revision(). revision() will fast path if there is a cache
2185 # hit. So, we tell _addrevision() to always cache in this case.
2187 # hit. So, we tell _addrevision() to always cache in this case.
2186 # We're only using addgroup() in the context of changegroup
2188 # We're only using addgroup() in the context of changegroup
2187 # generation so the revision data can always be handled as raw
2189 # generation so the revision data can always be handled as raw
2188 # by the flagprocessor.
2190 # by the flagprocessor.
2189 self._addrevision(node, None, transaction, link,
2191 self._addrevision(node, None, transaction, link,
2190 p1, p2, flags, (baserev, delta),
2192 p1, p2, flags, (baserev, delta),
2191 ifh, dfh,
2193 ifh, dfh,
2192 alwayscache=bool(addrevisioncb),
2194 alwayscache=bool(addrevisioncb),
2193 deltacomputer=deltacomputer)
2195 deltacomputer=deltacomputer)
2194
2196
2195 if addrevisioncb:
2197 if addrevisioncb:
2196 addrevisioncb(self, node)
2198 addrevisioncb(self, node)
2197
2199
2198 if not dfh and not self._inline:
2200 if not dfh and not self._inline:
2199 # addrevision switched from inline to conventional
2201 # addrevision switched from inline to conventional
2200 # reopen the index
2202 # reopen the index
2201 ifh.close()
2203 ifh.close()
2202 dfh = self._datafp("a+")
2204 dfh = self._datafp("a+")
2203 ifh = self._indexfp("a+")
2205 ifh = self._indexfp("a+")
2204 self._writinghandles = (ifh, dfh)
2206 self._writinghandles = (ifh, dfh)
2205 finally:
2207 finally:
2206 self._writinghandles = None
2208 self._writinghandles = None
2207
2209
2208 if dfh:
2210 if dfh:
2209 dfh.close()
2211 dfh.close()
2210 ifh.close()
2212 ifh.close()
2211
2213
2212 return nodes
2214 return nodes
2213
2215
2214 def iscensored(self, rev):
2216 def iscensored(self, rev):
2215 """Check if a file revision is censored."""
2217 """Check if a file revision is censored."""
2216 if not self._censorable:
2218 if not self._censorable:
2217 return False
2219 return False
2218
2220
2219 return self.flags(rev) & REVIDX_ISCENSORED
2221 return self.flags(rev) & REVIDX_ISCENSORED
2220
2222
2221 def _peek_iscensored(self, baserev, delta, flush):
2223 def _peek_iscensored(self, baserev, delta, flush):
2222 """Quickly check if a delta produces a censored revision."""
2224 """Quickly check if a delta produces a censored revision."""
2223 if not self._censorable:
2225 if not self._censorable:
2224 return False
2226 return False
2225
2227
2226 return storageutil.deltaiscensored(delta, baserev, self.rawsize)
2228 return storageutil.deltaiscensored(delta, baserev, self.rawsize)
2227
2229
2228 def getstrippoint(self, minlink):
2230 def getstrippoint(self, minlink):
2229 """find the minimum rev that must be stripped to strip the linkrev
2231 """find the minimum rev that must be stripped to strip the linkrev
2230
2232
2231 Returns a tuple containing the minimum rev and a set of all revs that
2233 Returns a tuple containing the minimum rev and a set of all revs that
2232 have linkrevs that will be broken by this strip.
2234 have linkrevs that will be broken by this strip.
2233 """
2235 """
2234 return storageutil.resolvestripinfo(minlink, len(self) - 1,
2236 return storageutil.resolvestripinfo(minlink, len(self) - 1,
2235 self.headrevs(),
2237 self.headrevs(),
2236 self.linkrev, self.parentrevs)
2238 self.linkrev, self.parentrevs)
2237
2239
2238 def strip(self, minlink, transaction):
2240 def strip(self, minlink, transaction):
2239 """truncate the revlog on the first revision with a linkrev >= minlink
2241 """truncate the revlog on the first revision with a linkrev >= minlink
2240
2242
2241 This function is called when we're stripping revision minlink and
2243 This function is called when we're stripping revision minlink and
2242 its descendants from the repository.
2244 its descendants from the repository.
2243
2245
2244 We have to remove all revisions with linkrev >= minlink, because
2246 We have to remove all revisions with linkrev >= minlink, because
2245 the equivalent changelog revisions will be renumbered after the
2247 the equivalent changelog revisions will be renumbered after the
2246 strip.
2248 strip.
2247
2249
2248 So we truncate the revlog on the first of these revisions, and
2250 So we truncate the revlog on the first of these revisions, and
2249 trust that the caller has saved the revisions that shouldn't be
2251 trust that the caller has saved the revisions that shouldn't be
2250 removed and that it'll re-add them after this truncation.
2252 removed and that it'll re-add them after this truncation.
2251 """
2253 """
2252 if len(self) == 0:
2254 if len(self) == 0:
2253 return
2255 return
2254
2256
2255 rev, _ = self.getstrippoint(minlink)
2257 rev, _ = self.getstrippoint(minlink)
2256 if rev == len(self):
2258 if rev == len(self):
2257 return
2259 return
2258
2260
2259 # first truncate the files on disk
2261 # first truncate the files on disk
2260 end = self.start(rev)
2262 end = self.start(rev)
2261 if not self._inline:
2263 if not self._inline:
2262 transaction.add(self.datafile, end)
2264 transaction.add(self.datafile, end)
2263 end = rev * self._io.size
2265 end = rev * self._io.size
2264 else:
2266 else:
2265 end += rev * self._io.size
2267 end += rev * self._io.size
2266
2268
2267 transaction.add(self.indexfile, end)
2269 transaction.add(self.indexfile, end)
2268
2270
2269 # then reset internal state in memory to forget those revisions
2271 # then reset internal state in memory to forget those revisions
2270 self._revisioncache = None
2272 self._revisioncache = None
2271 self._chaininfocache = {}
2273 self._chaininfocache = {}
2272 self._chunkclear()
2274 self._chunkclear()
2273 for x in pycompat.xrange(rev, len(self)):
2275 for x in pycompat.xrange(rev, len(self)):
2274 del self.nodemap[self.node(x)]
2276 del self.nodemap[self.node(x)]
2275
2277
2276 del self.index[rev:-1]
2278 del self.index[rev:-1]
2277 self._nodepos = None
2279 self._nodepos = None
2278
2280
2279 def checksize(self):
2281 def checksize(self):
2280 """Check size of index and data files
2282 """Check size of index and data files
2281
2283
2282 return a (dd, di) tuple.
2284 return a (dd, di) tuple.
2283 - dd: extra bytes for the "data" file
2285 - dd: extra bytes for the "data" file
2284 - di: extra bytes for the "index" file
2286 - di: extra bytes for the "index" file
2285
2287
2286 A healthy revlog will return (0, 0).
2288 A healthy revlog will return (0, 0).
2287 """
2289 """
2288 expected = 0
2290 expected = 0
2289 if len(self):
2291 if len(self):
2290 expected = max(0, self.end(len(self) - 1))
2292 expected = max(0, self.end(len(self) - 1))
2291
2293
2292 try:
2294 try:
2293 with self._datafp() as f:
2295 with self._datafp() as f:
2294 f.seek(0, io.SEEK_END)
2296 f.seek(0, io.SEEK_END)
2295 actual = f.tell()
2297 actual = f.tell()
2296 dd = actual - expected
2298 dd = actual - expected
2297 except IOError as inst:
2299 except IOError as inst:
2298 if inst.errno != errno.ENOENT:
2300 if inst.errno != errno.ENOENT:
2299 raise
2301 raise
2300 dd = 0
2302 dd = 0
2301
2303
2302 try:
2304 try:
2303 f = self.opener(self.indexfile)
2305 f = self.opener(self.indexfile)
2304 f.seek(0, io.SEEK_END)
2306 f.seek(0, io.SEEK_END)
2305 actual = f.tell()
2307 actual = f.tell()
2306 f.close()
2308 f.close()
2307 s = self._io.size
2309 s = self._io.size
2308 i = max(0, actual // s)
2310 i = max(0, actual // s)
2309 di = actual - (i * s)
2311 di = actual - (i * s)
2310 if self._inline:
2312 if self._inline:
2311 databytes = 0
2313 databytes = 0
2312 for r in self:
2314 for r in self:
2313 databytes += max(0, self.length(r))
2315 databytes += max(0, self.length(r))
2314 dd = 0
2316 dd = 0
2315 di = actual - len(self) * s - databytes
2317 di = actual - len(self) * s - databytes
2316 except IOError as inst:
2318 except IOError as inst:
2317 if inst.errno != errno.ENOENT:
2319 if inst.errno != errno.ENOENT:
2318 raise
2320 raise
2319 di = 0
2321 di = 0
2320
2322
2321 return (dd, di)
2323 return (dd, di)
2322
2324
2323 def files(self):
2325 def files(self):
2324 res = [self.indexfile]
2326 res = [self.indexfile]
2325 if not self._inline:
2327 if not self._inline:
2326 res.append(self.datafile)
2328 res.append(self.datafile)
2327 return res
2329 return res
2328
2330
2329 def emitrevisions(self, nodes, nodesorder=None, revisiondata=False,
2331 def emitrevisions(self, nodes, nodesorder=None, revisiondata=False,
2330 assumehaveparentrevisions=False,
2332 assumehaveparentrevisions=False,
2331 deltamode=repository.CG_DELTAMODE_STD):
2333 deltamode=repository.CG_DELTAMODE_STD):
2332 if nodesorder not in ('nodes', 'storage', 'linear', None):
2334 if nodesorder not in ('nodes', 'storage', 'linear', None):
2333 raise error.ProgrammingError('unhandled value for nodesorder: %s' %
2335 raise error.ProgrammingError('unhandled value for nodesorder: %s' %
2334 nodesorder)
2336 nodesorder)
2335
2337
2336 if nodesorder is None and not self._generaldelta:
2338 if nodesorder is None and not self._generaldelta:
2337 nodesorder = 'storage'
2339 nodesorder = 'storage'
2338
2340
2339 if (not self._storedeltachains and
2341 if (not self._storedeltachains and
2340 deltamode != repository.CG_DELTAMODE_PREV):
2342 deltamode != repository.CG_DELTAMODE_PREV):
2341 deltamode = repository.CG_DELTAMODE_FULL
2343 deltamode = repository.CG_DELTAMODE_FULL
2342
2344
2343 return storageutil.emitrevisions(
2345 return storageutil.emitrevisions(
2344 self, nodes, nodesorder, revlogrevisiondelta,
2346 self, nodes, nodesorder, revlogrevisiondelta,
2345 deltaparentfn=self.deltaparent,
2347 deltaparentfn=self.deltaparent,
2346 candeltafn=self.candelta,
2348 candeltafn=self.candelta,
2347 rawsizefn=self.rawsize,
2349 rawsizefn=self.rawsize,
2348 revdifffn=self.revdiff,
2350 revdifffn=self.revdiff,
2349 flagsfn=self.flags,
2351 flagsfn=self.flags,
2350 deltamode=deltamode,
2352 deltamode=deltamode,
2351 revisiondata=revisiondata,
2353 revisiondata=revisiondata,
2352 assumehaveparentrevisions=assumehaveparentrevisions)
2354 assumehaveparentrevisions=assumehaveparentrevisions)
2353
2355
2354 DELTAREUSEALWAYS = 'always'
2356 DELTAREUSEALWAYS = 'always'
2355 DELTAREUSESAMEREVS = 'samerevs'
2357 DELTAREUSESAMEREVS = 'samerevs'
2356 DELTAREUSENEVER = 'never'
2358 DELTAREUSENEVER = 'never'
2357
2359
2358 DELTAREUSEFULLADD = 'fulladd'
2360 DELTAREUSEFULLADD = 'fulladd'
2359
2361
2360 DELTAREUSEALL = {'always', 'samerevs', 'never', 'fulladd'}
2362 DELTAREUSEALL = {'always', 'samerevs', 'never', 'fulladd'}
2361
2363
2362 def clone(self, tr, destrevlog, addrevisioncb=None,
2364 def clone(self, tr, destrevlog, addrevisioncb=None,
2363 deltareuse=DELTAREUSESAMEREVS, forcedeltabothparents=None):
2365 deltareuse=DELTAREUSESAMEREVS, forcedeltabothparents=None):
2364 """Copy this revlog to another, possibly with format changes.
2366 """Copy this revlog to another, possibly with format changes.
2365
2367
2366 The destination revlog will contain the same revisions and nodes.
2368 The destination revlog will contain the same revisions and nodes.
2367 However, it may not be bit-for-bit identical due to e.g. delta encoding
2369 However, it may not be bit-for-bit identical due to e.g. delta encoding
2368 differences.
2370 differences.
2369
2371
2370 The ``deltareuse`` argument control how deltas from the existing revlog
2372 The ``deltareuse`` argument control how deltas from the existing revlog
2371 are preserved in the destination revlog. The argument can have the
2373 are preserved in the destination revlog. The argument can have the
2372 following values:
2374 following values:
2373
2375
2374 DELTAREUSEALWAYS
2376 DELTAREUSEALWAYS
2375 Deltas will always be reused (if possible), even if the destination
2377 Deltas will always be reused (if possible), even if the destination
2376 revlog would not select the same revisions for the delta. This is the
2378 revlog would not select the same revisions for the delta. This is the
2377 fastest mode of operation.
2379 fastest mode of operation.
2378 DELTAREUSESAMEREVS
2380 DELTAREUSESAMEREVS
2379 Deltas will be reused if the destination revlog would pick the same
2381 Deltas will be reused if the destination revlog would pick the same
2380 revisions for the delta. This mode strikes a balance between speed
2382 revisions for the delta. This mode strikes a balance between speed
2381 and optimization.
2383 and optimization.
2382 DELTAREUSENEVER
2384 DELTAREUSENEVER
2383 Deltas will never be reused. This is the slowest mode of execution.
2385 Deltas will never be reused. This is the slowest mode of execution.
2384 This mode can be used to recompute deltas (e.g. if the diff/delta
2386 This mode can be used to recompute deltas (e.g. if the diff/delta
2385 algorithm changes).
2387 algorithm changes).
2386
2388
2387 Delta computation can be slow, so the choice of delta reuse policy can
2389 Delta computation can be slow, so the choice of delta reuse policy can
2388 significantly affect run time.
2390 significantly affect run time.
2389
2391
2390 The default policy (``DELTAREUSESAMEREVS``) strikes a balance between
2392 The default policy (``DELTAREUSESAMEREVS``) strikes a balance between
2391 two extremes. Deltas will be reused if they are appropriate. But if the
2393 two extremes. Deltas will be reused if they are appropriate. But if the
2392 delta could choose a better revision, it will do so. This means if you
2394 delta could choose a better revision, it will do so. This means if you
2393 are converting a non-generaldelta revlog to a generaldelta revlog,
2395 are converting a non-generaldelta revlog to a generaldelta revlog,
2394 deltas will be recomputed if the delta's parent isn't a parent of the
2396 deltas will be recomputed if the delta's parent isn't a parent of the
2395 revision.
2397 revision.
2396
2398
2397 In addition to the delta policy, the ``forcedeltabothparents``
2399 In addition to the delta policy, the ``forcedeltabothparents``
2398 argument controls whether to force compute deltas against both parents
2400 argument controls whether to force compute deltas against both parents
2399 for merges. By default, the current default is used.
2401 for merges. By default, the current default is used.
2400 """
2402 """
2401 if deltareuse not in self.DELTAREUSEALL:
2403 if deltareuse not in self.DELTAREUSEALL:
2402 raise ValueError(_('value for deltareuse invalid: %s') % deltareuse)
2404 raise ValueError(_('value for deltareuse invalid: %s') % deltareuse)
2403
2405
2404 if len(destrevlog):
2406 if len(destrevlog):
2405 raise ValueError(_('destination revlog is not empty'))
2407 raise ValueError(_('destination revlog is not empty'))
2406
2408
2407 if getattr(self, 'filteredrevs', None):
2409 if getattr(self, 'filteredrevs', None):
2408 raise ValueError(_('source revlog has filtered revisions'))
2410 raise ValueError(_('source revlog has filtered revisions'))
2409 if getattr(destrevlog, 'filteredrevs', None):
2411 if getattr(destrevlog, 'filteredrevs', None):
2410 raise ValueError(_('destination revlog has filtered revisions'))
2412 raise ValueError(_('destination revlog has filtered revisions'))
2411
2413
2412 # lazydelta and lazydeltabase controls whether to reuse a cached delta,
2414 # lazydelta and lazydeltabase controls whether to reuse a cached delta,
2413 # if possible.
2415 # if possible.
2414 oldlazydelta = destrevlog._lazydelta
2416 oldlazydelta = destrevlog._lazydelta
2415 oldlazydeltabase = destrevlog._lazydeltabase
2417 oldlazydeltabase = destrevlog._lazydeltabase
2416 oldamd = destrevlog._deltabothparents
2418 oldamd = destrevlog._deltabothparents
2417
2419
2418 try:
2420 try:
2419 if deltareuse == self.DELTAREUSEALWAYS:
2421 if deltareuse == self.DELTAREUSEALWAYS:
2420 destrevlog._lazydeltabase = True
2422 destrevlog._lazydeltabase = True
2421 destrevlog._lazydelta = True
2423 destrevlog._lazydelta = True
2422 elif deltareuse == self.DELTAREUSESAMEREVS:
2424 elif deltareuse == self.DELTAREUSESAMEREVS:
2423 destrevlog._lazydeltabase = False
2425 destrevlog._lazydeltabase = False
2424 destrevlog._lazydelta = True
2426 destrevlog._lazydelta = True
2425 elif deltareuse == self.DELTAREUSENEVER:
2427 elif deltareuse == self.DELTAREUSENEVER:
2426 destrevlog._lazydeltabase = False
2428 destrevlog._lazydeltabase = False
2427 destrevlog._lazydelta = False
2429 destrevlog._lazydelta = False
2428
2430
2429 destrevlog._deltabothparents = forcedeltabothparents or oldamd
2431 destrevlog._deltabothparents = forcedeltabothparents or oldamd
2430
2432
2431 deltacomputer = deltautil.deltacomputer(destrevlog)
2433 deltacomputer = deltautil.deltacomputer(destrevlog)
2432 index = self.index
2434 index = self.index
2433 for rev in self:
2435 for rev in self:
2434 entry = index[rev]
2436 entry = index[rev]
2435
2437
2436 # Some classes override linkrev to take filtered revs into
2438 # Some classes override linkrev to take filtered revs into
2437 # account. Use raw entry from index.
2439 # account. Use raw entry from index.
2438 flags = entry[0] & 0xffff
2440 flags = entry[0] & 0xffff
2439 linkrev = entry[4]
2441 linkrev = entry[4]
2440 p1 = index[entry[5]][7]
2442 p1 = index[entry[5]][7]
2441 p2 = index[entry[6]][7]
2443 p2 = index[entry[6]][7]
2442 node = entry[7]
2444 node = entry[7]
2443
2445
2444 # (Possibly) reuse the delta from the revlog if allowed and
2446 # (Possibly) reuse the delta from the revlog if allowed and
2445 # the revlog chunk is a delta.
2447 # the revlog chunk is a delta.
2446 cachedelta = None
2448 cachedelta = None
2447 rawtext = None
2449 rawtext = None
2448 if (deltareuse != self.DELTAREUSEFULLADD
2450 if (deltareuse != self.DELTAREUSEFULLADD
2449 and destrevlog._lazydelta):
2451 and destrevlog._lazydelta):
2450 dp = self.deltaparent(rev)
2452 dp = self.deltaparent(rev)
2451 if dp != nullrev:
2453 if dp != nullrev:
2452 cachedelta = (dp, bytes(self._chunk(rev)))
2454 cachedelta = (dp, bytes(self._chunk(rev)))
2453
2455
2454 if not cachedelta:
2456 if not cachedelta:
2455 rawtext = self.rawdata(rev)
2457 rawtext = self.rawdata(rev)
2456
2458
2457
2459
2458 if deltareuse == self.DELTAREUSEFULLADD:
2460 if deltareuse == self.DELTAREUSEFULLADD:
2459 destrevlog.addrevision(rawtext, tr, linkrev, p1, p2,
2461 destrevlog.addrevision(rawtext, tr, linkrev, p1, p2,
2460 cachedelta=cachedelta,
2462 cachedelta=cachedelta,
2461 node=node, flags=flags,
2463 node=node, flags=flags,
2462 deltacomputer=deltacomputer)
2464 deltacomputer=deltacomputer)
2463 else:
2465 else:
2464 ifh = destrevlog.opener(destrevlog.indexfile, 'a+',
2466 ifh = destrevlog.opener(destrevlog.indexfile, 'a+',
2465 checkambig=False)
2467 checkambig=False)
2466 dfh = None
2468 dfh = None
2467 if not destrevlog._inline:
2469 if not destrevlog._inline:
2468 dfh = destrevlog.opener(destrevlog.datafile, 'a+')
2470 dfh = destrevlog.opener(destrevlog.datafile, 'a+')
2469 try:
2471 try:
2470 destrevlog._addrevision(node, rawtext, tr, linkrev, p1,
2472 destrevlog._addrevision(node, rawtext, tr, linkrev, p1,
2471 p2, flags, cachedelta, ifh, dfh,
2473 p2, flags, cachedelta, ifh, dfh,
2472 deltacomputer=deltacomputer)
2474 deltacomputer=deltacomputer)
2473 finally:
2475 finally:
2474 if dfh:
2476 if dfh:
2475 dfh.close()
2477 dfh.close()
2476 ifh.close()
2478 ifh.close()
2477
2479
2478 if addrevisioncb:
2480 if addrevisioncb:
2479 addrevisioncb(self, rev, node)
2481 addrevisioncb(self, rev, node)
2480 finally:
2482 finally:
2481 destrevlog._lazydelta = oldlazydelta
2483 destrevlog._lazydelta = oldlazydelta
2482 destrevlog._lazydeltabase = oldlazydeltabase
2484 destrevlog._lazydeltabase = oldlazydeltabase
2483 destrevlog._deltabothparents = oldamd
2485 destrevlog._deltabothparents = oldamd
2484
2486
2485 def censorrevision(self, tr, censornode, tombstone=b''):
2487 def censorrevision(self, tr, censornode, tombstone=b''):
2486 if (self.version & 0xFFFF) == REVLOGV0:
2488 if (self.version & 0xFFFF) == REVLOGV0:
2487 raise error.RevlogError(_('cannot censor with version %d revlogs') %
2489 raise error.RevlogError(_('cannot censor with version %d revlogs') %
2488 self.version)
2490 self.version)
2489
2491
2490 censorrev = self.rev(censornode)
2492 censorrev = self.rev(censornode)
2491 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
2493 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
2492
2494
2493 if len(tombstone) > self.rawsize(censorrev):
2495 if len(tombstone) > self.rawsize(censorrev):
2494 raise error.Abort(_('censor tombstone must be no longer than '
2496 raise error.Abort(_('censor tombstone must be no longer than '
2495 'censored data'))
2497 'censored data'))
2496
2498
2497 # Rewriting the revlog in place is hard. Our strategy for censoring is
2499 # Rewriting the revlog in place is hard. Our strategy for censoring is
2498 # to create a new revlog, copy all revisions to it, then replace the
2500 # to create a new revlog, copy all revisions to it, then replace the
2499 # revlogs on transaction close.
2501 # revlogs on transaction close.
2500
2502
2501 newindexfile = self.indexfile + b'.tmpcensored'
2503 newindexfile = self.indexfile + b'.tmpcensored'
2502 newdatafile = self.datafile + b'.tmpcensored'
2504 newdatafile = self.datafile + b'.tmpcensored'
2503
2505
2504 # This is a bit dangerous. We could easily have a mismatch of state.
2506 # This is a bit dangerous. We could easily have a mismatch of state.
2505 newrl = revlog(self.opener, newindexfile, newdatafile,
2507 newrl = revlog(self.opener, newindexfile, newdatafile,
2506 censorable=True)
2508 censorable=True)
2507 newrl.version = self.version
2509 newrl.version = self.version
2508 newrl._generaldelta = self._generaldelta
2510 newrl._generaldelta = self._generaldelta
2509 newrl._io = self._io
2511 newrl._io = self._io
2510
2512
2511 for rev in self.revs():
2513 for rev in self.revs():
2512 node = self.node(rev)
2514 node = self.node(rev)
2513 p1, p2 = self.parents(node)
2515 p1, p2 = self.parents(node)
2514
2516
2515 if rev == censorrev:
2517 if rev == censorrev:
2516 newrl.addrawrevision(tombstone, tr, self.linkrev(censorrev),
2518 newrl.addrawrevision(tombstone, tr, self.linkrev(censorrev),
2517 p1, p2, censornode, REVIDX_ISCENSORED)
2519 p1, p2, censornode, REVIDX_ISCENSORED)
2518
2520
2519 if newrl.deltaparent(rev) != nullrev:
2521 if newrl.deltaparent(rev) != nullrev:
2520 raise error.Abort(_('censored revision stored as delta; '
2522 raise error.Abort(_('censored revision stored as delta; '
2521 'cannot censor'),
2523 'cannot censor'),
2522 hint=_('censoring of revlogs is not '
2524 hint=_('censoring of revlogs is not '
2523 'fully implemented; please report '
2525 'fully implemented; please report '
2524 'this bug'))
2526 'this bug'))
2525 continue
2527 continue
2526
2528
2527 if self.iscensored(rev):
2529 if self.iscensored(rev):
2528 if self.deltaparent(rev) != nullrev:
2530 if self.deltaparent(rev) != nullrev:
2529 raise error.Abort(_('cannot censor due to censored '
2531 raise error.Abort(_('cannot censor due to censored '
2530 'revision having delta stored'))
2532 'revision having delta stored'))
2531 rawtext = self._chunk(rev)
2533 rawtext = self._chunk(rev)
2532 else:
2534 else:
2533 rawtext = self.rawdata(rev)
2535 rawtext = self.rawdata(rev)
2534
2536
2535 newrl.addrawrevision(rawtext, tr, self.linkrev(rev), p1, p2, node,
2537 newrl.addrawrevision(rawtext, tr, self.linkrev(rev), p1, p2, node,
2536 self.flags(rev))
2538 self.flags(rev))
2537
2539
2538 tr.addbackup(self.indexfile, location='store')
2540 tr.addbackup(self.indexfile, location='store')
2539 if not self._inline:
2541 if not self._inline:
2540 tr.addbackup(self.datafile, location='store')
2542 tr.addbackup(self.datafile, location='store')
2541
2543
2542 self.opener.rename(newrl.indexfile, self.indexfile)
2544 self.opener.rename(newrl.indexfile, self.indexfile)
2543 if not self._inline:
2545 if not self._inline:
2544 self.opener.rename(newrl.datafile, self.datafile)
2546 self.opener.rename(newrl.datafile, self.datafile)
2545
2547
2546 self.clearcaches()
2548 self.clearcaches()
2547 self._loadindex()
2549 self._loadindex()
2548
2550
2549 def verifyintegrity(self, state):
2551 def verifyintegrity(self, state):
2550 """Verifies the integrity of the revlog.
2552 """Verifies the integrity of the revlog.
2551
2553
2552 Yields ``revlogproblem`` instances describing problems that are
2554 Yields ``revlogproblem`` instances describing problems that are
2553 found.
2555 found.
2554 """
2556 """
2555 dd, di = self.checksize()
2557 dd, di = self.checksize()
2556 if dd:
2558 if dd:
2557 yield revlogproblem(error=_('data length off by %d bytes') % dd)
2559 yield revlogproblem(error=_('data length off by %d bytes') % dd)
2558 if di:
2560 if di:
2559 yield revlogproblem(error=_('index contains %d extra bytes') % di)
2561 yield revlogproblem(error=_('index contains %d extra bytes') % di)
2560
2562
2561 version = self.version & 0xFFFF
2563 version = self.version & 0xFFFF
2562
2564
2563 # The verifier tells us what version revlog we should be.
2565 # The verifier tells us what version revlog we should be.
2564 if version != state['expectedversion']:
2566 if version != state['expectedversion']:
2565 yield revlogproblem(
2567 yield revlogproblem(
2566 warning=_("warning: '%s' uses revlog format %d; expected %d") %
2568 warning=_("warning: '%s' uses revlog format %d; expected %d") %
2567 (self.indexfile, version, state['expectedversion']))
2569 (self.indexfile, version, state['expectedversion']))
2568
2570
2569 state['skipread'] = set()
2571 state['skipread'] = set()
2570
2572
2571 for rev in self:
2573 for rev in self:
2572 node = self.node(rev)
2574 node = self.node(rev)
2573
2575
2574 # Verify contents. 4 cases to care about:
2576 # Verify contents. 4 cases to care about:
2575 #
2577 #
2576 # common: the most common case
2578 # common: the most common case
2577 # rename: with a rename
2579 # rename: with a rename
2578 # meta: file content starts with b'\1\n', the metadata
2580 # meta: file content starts with b'\1\n', the metadata
2579 # header defined in filelog.py, but without a rename
2581 # header defined in filelog.py, but without a rename
2580 # ext: content stored externally
2582 # ext: content stored externally
2581 #
2583 #
2582 # More formally, their differences are shown below:
2584 # More formally, their differences are shown below:
2583 #
2585 #
2584 # | common | rename | meta | ext
2586 # | common | rename | meta | ext
2585 # -------------------------------------------------------
2587 # -------------------------------------------------------
2586 # flags() | 0 | 0 | 0 | not 0
2588 # flags() | 0 | 0 | 0 | not 0
2587 # renamed() | False | True | False | ?
2589 # renamed() | False | True | False | ?
2588 # rawtext[0:2]=='\1\n'| False | True | True | ?
2590 # rawtext[0:2]=='\1\n'| False | True | True | ?
2589 #
2591 #
2590 # "rawtext" means the raw text stored in revlog data, which
2592 # "rawtext" means the raw text stored in revlog data, which
2591 # could be retrieved by "rawdata(rev)". "text"
2593 # could be retrieved by "rawdata(rev)". "text"
2592 # mentioned below is "revision(rev)".
2594 # mentioned below is "revision(rev)".
2593 #
2595 #
2594 # There are 3 different lengths stored physically:
2596 # There are 3 different lengths stored physically:
2595 # 1. L1: rawsize, stored in revlog index
2597 # 1. L1: rawsize, stored in revlog index
2596 # 2. L2: len(rawtext), stored in revlog data
2598 # 2. L2: len(rawtext), stored in revlog data
2597 # 3. L3: len(text), stored in revlog data if flags==0, or
2599 # 3. L3: len(text), stored in revlog data if flags==0, or
2598 # possibly somewhere else if flags!=0
2600 # possibly somewhere else if flags!=0
2599 #
2601 #
2600 # L1 should be equal to L2. L3 could be different from them.
2602 # L1 should be equal to L2. L3 could be different from them.
2601 # "text" may or may not affect commit hash depending on flag
2603 # "text" may or may not affect commit hash depending on flag
2602 # processors (see flagutil.addflagprocessor).
2604 # processors (see flagutil.addflagprocessor).
2603 #
2605 #
2604 # | common | rename | meta | ext
2606 # | common | rename | meta | ext
2605 # -------------------------------------------------
2607 # -------------------------------------------------
2606 # rawsize() | L1 | L1 | L1 | L1
2608 # rawsize() | L1 | L1 | L1 | L1
2607 # size() | L1 | L2-LM | L1(*) | L1 (?)
2609 # size() | L1 | L2-LM | L1(*) | L1 (?)
2608 # len(rawtext) | L2 | L2 | L2 | L2
2610 # len(rawtext) | L2 | L2 | L2 | L2
2609 # len(text) | L2 | L2 | L2 | L3
2611 # len(text) | L2 | L2 | L2 | L3
2610 # len(read()) | L2 | L2-LM | L2-LM | L3 (?)
2612 # len(read()) | L2 | L2-LM | L2-LM | L3 (?)
2611 #
2613 #
2612 # LM: length of metadata, depending on rawtext
2614 # LM: length of metadata, depending on rawtext
2613 # (*): not ideal, see comment in filelog.size
2615 # (*): not ideal, see comment in filelog.size
2614 # (?): could be "- len(meta)" if the resolved content has
2616 # (?): could be "- len(meta)" if the resolved content has
2615 # rename metadata
2617 # rename metadata
2616 #
2618 #
2617 # Checks needed to be done:
2619 # Checks needed to be done:
2618 # 1. length check: L1 == L2, in all cases.
2620 # 1. length check: L1 == L2, in all cases.
2619 # 2. hash check: depending on flag processor, we may need to
2621 # 2. hash check: depending on flag processor, we may need to
2620 # use either "text" (external), or "rawtext" (in revlog).
2622 # use either "text" (external), or "rawtext" (in revlog).
2621
2623
2622 try:
2624 try:
2623 skipflags = state.get('skipflags', 0)
2625 skipflags = state.get('skipflags', 0)
2624 if skipflags:
2626 if skipflags:
2625 skipflags &= self.flags(rev)
2627 skipflags &= self.flags(rev)
2626
2628
2627 if skipflags:
2629 if skipflags:
2628 state['skipread'].add(node)
2630 state['skipread'].add(node)
2629 else:
2631 else:
2630 # Side-effect: read content and verify hash.
2632 # Side-effect: read content and verify hash.
2631 self.revision(node)
2633 self.revision(node)
2632
2634
2633 l1 = self.rawsize(rev)
2635 l1 = self.rawsize(rev)
2634 l2 = len(self.rawdata(node))
2636 l2 = len(self.rawdata(node))
2635
2637
2636 if l1 != l2:
2638 if l1 != l2:
2637 yield revlogproblem(
2639 yield revlogproblem(
2638 error=_('unpacked size is %d, %d expected') % (l2, l1),
2640 error=_('unpacked size is %d, %d expected') % (l2, l1),
2639 node=node)
2641 node=node)
2640
2642
2641 except error.CensoredNodeError:
2643 except error.CensoredNodeError:
2642 if state['erroroncensored']:
2644 if state['erroroncensored']:
2643 yield revlogproblem(error=_('censored file data'),
2645 yield revlogproblem(error=_('censored file data'),
2644 node=node)
2646 node=node)
2645 state['skipread'].add(node)
2647 state['skipread'].add(node)
2646 except Exception as e:
2648 except Exception as e:
2647 yield revlogproblem(
2649 yield revlogproblem(
2648 error=_('unpacking %s: %s') % (short(node),
2650 error=_('unpacking %s: %s') % (short(node),
2649 stringutil.forcebytestr(e)),
2651 stringutil.forcebytestr(e)),
2650 node=node)
2652 node=node)
2651 state['skipread'].add(node)
2653 state['skipread'].add(node)
2652
2654
2653 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
2655 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
2654 revisionscount=False, trackedsize=False,
2656 revisionscount=False, trackedsize=False,
2655 storedsize=False):
2657 storedsize=False):
2656 d = {}
2658 d = {}
2657
2659
2658 if exclusivefiles:
2660 if exclusivefiles:
2659 d['exclusivefiles'] = [(self.opener, self.indexfile)]
2661 d['exclusivefiles'] = [(self.opener, self.indexfile)]
2660 if not self._inline:
2662 if not self._inline:
2661 d['exclusivefiles'].append((self.opener, self.datafile))
2663 d['exclusivefiles'].append((self.opener, self.datafile))
2662
2664
2663 if sharedfiles:
2665 if sharedfiles:
2664 d['sharedfiles'] = []
2666 d['sharedfiles'] = []
2665
2667
2666 if revisionscount:
2668 if revisionscount:
2667 d['revisionscount'] = len(self)
2669 d['revisionscount'] = len(self)
2668
2670
2669 if trackedsize:
2671 if trackedsize:
2670 d['trackedsize'] = sum(map(self.rawsize, iter(self)))
2672 d['trackedsize'] = sum(map(self.rawsize, iter(self)))
2671
2673
2672 if storedsize:
2674 if storedsize:
2673 d['storedsize'] = sum(self.opener.stat(path).st_size
2675 d['storedsize'] = sum(self.opener.stat(path).st_size
2674 for path in self.files())
2676 for path in self.files())
2675
2677
2676 return d
2678 return d
@@ -1,55 +1,55
1 # revlogdeltas.py - constant used for revlog logic
1 # revlogdeltas.py - constant used for revlog logic
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2018 Octobus <contact@octobus.net>
4 # Copyright 2018 Octobus <contact@octobus.net>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8 """Helper class to compute deltas stored inside revlogs"""
8 """Helper class to compute deltas stored inside revlogs"""
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 from .. import (
12 from ..interfaces import (
13 repository,
13 repository,
14 )
14 )
15
15
16 # revlog header flags
16 # revlog header flags
17 REVLOGV0 = 0
17 REVLOGV0 = 0
18 REVLOGV1 = 1
18 REVLOGV1 = 1
19 # Dummy value until file format is finalized.
19 # Dummy value until file format is finalized.
20 # Reminder: change the bounds check in revlog.__init__ when this is changed.
20 # Reminder: change the bounds check in revlog.__init__ when this is changed.
21 REVLOGV2 = 0xDEAD
21 REVLOGV2 = 0xDEAD
22 # Shared across v1 and v2.
22 # Shared across v1 and v2.
23 FLAG_INLINE_DATA = (1 << 16)
23 FLAG_INLINE_DATA = (1 << 16)
24 # Only used by v1, implied by v2.
24 # Only used by v1, implied by v2.
25 FLAG_GENERALDELTA = (1 << 17)
25 FLAG_GENERALDELTA = (1 << 17)
26 REVLOG_DEFAULT_FLAGS = FLAG_INLINE_DATA
26 REVLOG_DEFAULT_FLAGS = FLAG_INLINE_DATA
27 REVLOG_DEFAULT_FORMAT = REVLOGV1
27 REVLOG_DEFAULT_FORMAT = REVLOGV1
28 REVLOG_DEFAULT_VERSION = REVLOG_DEFAULT_FORMAT | REVLOG_DEFAULT_FLAGS
28 REVLOG_DEFAULT_VERSION = REVLOG_DEFAULT_FORMAT | REVLOG_DEFAULT_FLAGS
29 REVLOGV1_FLAGS = FLAG_INLINE_DATA | FLAG_GENERALDELTA
29 REVLOGV1_FLAGS = FLAG_INLINE_DATA | FLAG_GENERALDELTA
30 REVLOGV2_FLAGS = FLAG_INLINE_DATA
30 REVLOGV2_FLAGS = FLAG_INLINE_DATA
31
31
32 # revlog index flags
32 # revlog index flags
33
33
34 # For historical reasons, revlog's internal flags were exposed via the
34 # For historical reasons, revlog's internal flags were exposed via the
35 # wire protocol and are even exposed in parts of the storage APIs.
35 # wire protocol and are even exposed in parts of the storage APIs.
36
36
37 # revision has censor metadata, must be verified
37 # revision has censor metadata, must be verified
38 REVIDX_ISCENSORED = repository.REVISION_FLAG_CENSORED
38 REVIDX_ISCENSORED = repository.REVISION_FLAG_CENSORED
39 # revision hash does not match data (narrowhg)
39 # revision hash does not match data (narrowhg)
40 REVIDX_ELLIPSIS = repository.REVISION_FLAG_ELLIPSIS
40 REVIDX_ELLIPSIS = repository.REVISION_FLAG_ELLIPSIS
41 # revision data is stored externally
41 # revision data is stored externally
42 REVIDX_EXTSTORED = repository.REVISION_FLAG_EXTSTORED
42 REVIDX_EXTSTORED = repository.REVISION_FLAG_EXTSTORED
43 REVIDX_DEFAULT_FLAGS = 0
43 REVIDX_DEFAULT_FLAGS = 0
44 # stable order in which flags need to be processed and their processors applied
44 # stable order in which flags need to be processed and their processors applied
45 REVIDX_FLAGS_ORDER = [
45 REVIDX_FLAGS_ORDER = [
46 REVIDX_ISCENSORED,
46 REVIDX_ISCENSORED,
47 REVIDX_ELLIPSIS,
47 REVIDX_ELLIPSIS,
48 REVIDX_EXTSTORED,
48 REVIDX_EXTSTORED,
49 ]
49 ]
50
50
51 # bitmark for flags that could cause rawdata content change
51 # bitmark for flags that could cause rawdata content change
52 REVIDX_RAWTEXT_CHANGING_FLAGS = REVIDX_ISCENSORED | REVIDX_EXTSTORED
52 REVIDX_RAWTEXT_CHANGING_FLAGS = REVIDX_ISCENSORED | REVIDX_EXTSTORED
53
53
54 SPARSE_REVLOG_MAX_CHAIN_LENGTH = 1000
54 SPARSE_REVLOG_MAX_CHAIN_LENGTH = 1000
55
55
@@ -1,658 +1,660
1 # streamclone.py - producing and consuming streaming repository data
1 # streamclone.py - producing and consuming streaming repository data
2 #
2 #
3 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2015 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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import contextlib
10 import contextlib
11 import os
11 import os
12 import struct
12 import struct
13
13
14 from .i18n import _
14 from .i18n import _
15 from .interfaces import (
16 repository,
17 )
15 from . import (
18 from . import (
16 cacheutil,
19 cacheutil,
17 error,
20 error,
18 narrowspec,
21 narrowspec,
19 phases,
22 phases,
20 pycompat,
23 pycompat,
21 repository,
22 store,
24 store,
23 util,
25 util,
24 )
26 )
25
27
26 def canperformstreamclone(pullop, bundle2=False):
28 def canperformstreamclone(pullop, bundle2=False):
27 """Whether it is possible to perform a streaming clone as part of pull.
29 """Whether it is possible to perform a streaming clone as part of pull.
28
30
29 ``bundle2`` will cause the function to consider stream clone through
31 ``bundle2`` will cause the function to consider stream clone through
30 bundle2 and only through bundle2.
32 bundle2 and only through bundle2.
31
33
32 Returns a tuple of (supported, requirements). ``supported`` is True if
34 Returns a tuple of (supported, requirements). ``supported`` is True if
33 streaming clone is supported and False otherwise. ``requirements`` is
35 streaming clone is supported and False otherwise. ``requirements`` is
34 a set of repo requirements from the remote, or ``None`` if stream clone
36 a set of repo requirements from the remote, or ``None`` if stream clone
35 isn't supported.
37 isn't supported.
36 """
38 """
37 repo = pullop.repo
39 repo = pullop.repo
38 remote = pullop.remote
40 remote = pullop.remote
39
41
40 bundle2supported = False
42 bundle2supported = False
41 if pullop.canusebundle2:
43 if pullop.canusebundle2:
42 if 'v2' in pullop.remotebundle2caps.get('stream', []):
44 if 'v2' in pullop.remotebundle2caps.get('stream', []):
43 bundle2supported = True
45 bundle2supported = True
44 # else
46 # else
45 # Server doesn't support bundle2 stream clone or doesn't support
47 # Server doesn't support bundle2 stream clone or doesn't support
46 # the versions we support. Fall back and possibly allow legacy.
48 # the versions we support. Fall back and possibly allow legacy.
47
49
48 # Ensures legacy code path uses available bundle2.
50 # Ensures legacy code path uses available bundle2.
49 if bundle2supported and not bundle2:
51 if bundle2supported and not bundle2:
50 return False, None
52 return False, None
51 # Ensures bundle2 doesn't try to do a stream clone if it isn't supported.
53 # Ensures bundle2 doesn't try to do a stream clone if it isn't supported.
52 elif bundle2 and not bundle2supported:
54 elif bundle2 and not bundle2supported:
53 return False, None
55 return False, None
54
56
55 # Streaming clone only works on empty repositories.
57 # Streaming clone only works on empty repositories.
56 if len(repo):
58 if len(repo):
57 return False, None
59 return False, None
58
60
59 # Streaming clone only works if all data is being requested.
61 # Streaming clone only works if all data is being requested.
60 if pullop.heads:
62 if pullop.heads:
61 return False, None
63 return False, None
62
64
63 streamrequested = pullop.streamclonerequested
65 streamrequested = pullop.streamclonerequested
64
66
65 # If we don't have a preference, let the server decide for us. This
67 # If we don't have a preference, let the server decide for us. This
66 # likely only comes into play in LANs.
68 # likely only comes into play in LANs.
67 if streamrequested is None:
69 if streamrequested is None:
68 # The server can advertise whether to prefer streaming clone.
70 # The server can advertise whether to prefer streaming clone.
69 streamrequested = remote.capable('stream-preferred')
71 streamrequested = remote.capable('stream-preferred')
70
72
71 if not streamrequested:
73 if not streamrequested:
72 return False, None
74 return False, None
73
75
74 # In order for stream clone to work, the client has to support all the
76 # In order for stream clone to work, the client has to support all the
75 # requirements advertised by the server.
77 # requirements advertised by the server.
76 #
78 #
77 # The server advertises its requirements via the "stream" and "streamreqs"
79 # The server advertises its requirements via the "stream" and "streamreqs"
78 # capability. "stream" (a value-less capability) is advertised if and only
80 # capability. "stream" (a value-less capability) is advertised if and only
79 # if the only requirement is "revlogv1." Else, the "streamreqs" capability
81 # if the only requirement is "revlogv1." Else, the "streamreqs" capability
80 # is advertised and contains a comma-delimited list of requirements.
82 # is advertised and contains a comma-delimited list of requirements.
81 requirements = set()
83 requirements = set()
82 if remote.capable('stream'):
84 if remote.capable('stream'):
83 requirements.add('revlogv1')
85 requirements.add('revlogv1')
84 else:
86 else:
85 streamreqs = remote.capable('streamreqs')
87 streamreqs = remote.capable('streamreqs')
86 # This is weird and shouldn't happen with modern servers.
88 # This is weird and shouldn't happen with modern servers.
87 if not streamreqs:
89 if not streamreqs:
88 pullop.repo.ui.warn(_(
90 pullop.repo.ui.warn(_(
89 'warning: stream clone requested but server has them '
91 'warning: stream clone requested but server has them '
90 'disabled\n'))
92 'disabled\n'))
91 return False, None
93 return False, None
92
94
93 streamreqs = set(streamreqs.split(','))
95 streamreqs = set(streamreqs.split(','))
94 # Server requires something we don't support. Bail.
96 # Server requires something we don't support. Bail.
95 missingreqs = streamreqs - repo.supportedformats
97 missingreqs = streamreqs - repo.supportedformats
96 if missingreqs:
98 if missingreqs:
97 pullop.repo.ui.warn(_(
99 pullop.repo.ui.warn(_(
98 'warning: stream clone requested but client is missing '
100 'warning: stream clone requested but client is missing '
99 'requirements: %s\n') % ', '.join(sorted(missingreqs)))
101 'requirements: %s\n') % ', '.join(sorted(missingreqs)))
100 pullop.repo.ui.warn(
102 pullop.repo.ui.warn(
101 _('(see https://www.mercurial-scm.org/wiki/MissingRequirement '
103 _('(see https://www.mercurial-scm.org/wiki/MissingRequirement '
102 'for more information)\n'))
104 'for more information)\n'))
103 return False, None
105 return False, None
104 requirements = streamreqs
106 requirements = streamreqs
105
107
106 return True, requirements
108 return True, requirements
107
109
108 def maybeperformlegacystreamclone(pullop):
110 def maybeperformlegacystreamclone(pullop):
109 """Possibly perform a legacy stream clone operation.
111 """Possibly perform a legacy stream clone operation.
110
112
111 Legacy stream clones are performed as part of pull but before all other
113 Legacy stream clones are performed as part of pull but before all other
112 operations.
114 operations.
113
115
114 A legacy stream clone will not be performed if a bundle2 stream clone is
116 A legacy stream clone will not be performed if a bundle2 stream clone is
115 supported.
117 supported.
116 """
118 """
117 from . import localrepo
119 from . import localrepo
118
120
119 supported, requirements = canperformstreamclone(pullop)
121 supported, requirements = canperformstreamclone(pullop)
120
122
121 if not supported:
123 if not supported:
122 return
124 return
123
125
124 repo = pullop.repo
126 repo = pullop.repo
125 remote = pullop.remote
127 remote = pullop.remote
126
128
127 # Save remote branchmap. We will use it later to speed up branchcache
129 # Save remote branchmap. We will use it later to speed up branchcache
128 # creation.
130 # creation.
129 rbranchmap = None
131 rbranchmap = None
130 if remote.capable('branchmap'):
132 if remote.capable('branchmap'):
131 with remote.commandexecutor() as e:
133 with remote.commandexecutor() as e:
132 rbranchmap = e.callcommand('branchmap', {}).result()
134 rbranchmap = e.callcommand('branchmap', {}).result()
133
135
134 repo.ui.status(_('streaming all changes\n'))
136 repo.ui.status(_('streaming all changes\n'))
135
137
136 with remote.commandexecutor() as e:
138 with remote.commandexecutor() as e:
137 fp = e.callcommand('stream_out', {}).result()
139 fp = e.callcommand('stream_out', {}).result()
138
140
139 # TODO strictly speaking, this code should all be inside the context
141 # TODO strictly speaking, this code should all be inside the context
140 # manager because the context manager is supposed to ensure all wire state
142 # manager because the context manager is supposed to ensure all wire state
141 # is flushed when exiting. But the legacy peers don't do this, so it
143 # is flushed when exiting. But the legacy peers don't do this, so it
142 # doesn't matter.
144 # doesn't matter.
143 l = fp.readline()
145 l = fp.readline()
144 try:
146 try:
145 resp = int(l)
147 resp = int(l)
146 except ValueError:
148 except ValueError:
147 raise error.ResponseError(
149 raise error.ResponseError(
148 _('unexpected response from remote server:'), l)
150 _('unexpected response from remote server:'), l)
149 if resp == 1:
151 if resp == 1:
150 raise error.Abort(_('operation forbidden by server'))
152 raise error.Abort(_('operation forbidden by server'))
151 elif resp == 2:
153 elif resp == 2:
152 raise error.Abort(_('locking the remote repository failed'))
154 raise error.Abort(_('locking the remote repository failed'))
153 elif resp != 0:
155 elif resp != 0:
154 raise error.Abort(_('the server sent an unknown error code'))
156 raise error.Abort(_('the server sent an unknown error code'))
155
157
156 l = fp.readline()
158 l = fp.readline()
157 try:
159 try:
158 filecount, bytecount = map(int, l.split(' ', 1))
160 filecount, bytecount = map(int, l.split(' ', 1))
159 except (ValueError, TypeError):
161 except (ValueError, TypeError):
160 raise error.ResponseError(
162 raise error.ResponseError(
161 _('unexpected response from remote server:'), l)
163 _('unexpected response from remote server:'), l)
162
164
163 with repo.lock():
165 with repo.lock():
164 consumev1(repo, fp, filecount, bytecount)
166 consumev1(repo, fp, filecount, bytecount)
165
167
166 # new requirements = old non-format requirements +
168 # new requirements = old non-format requirements +
167 # new format-related remote requirements
169 # new format-related remote requirements
168 # requirements from the streamed-in repository
170 # requirements from the streamed-in repository
169 repo.requirements = requirements | (
171 repo.requirements = requirements | (
170 repo.requirements - repo.supportedformats)
172 repo.requirements - repo.supportedformats)
171 repo.svfs.options = localrepo.resolvestorevfsoptions(
173 repo.svfs.options = localrepo.resolvestorevfsoptions(
172 repo.ui, repo.requirements, repo.features)
174 repo.ui, repo.requirements, repo.features)
173 repo._writerequirements()
175 repo._writerequirements()
174
176
175 if rbranchmap:
177 if rbranchmap:
176 repo._branchcaches.replace(repo, rbranchmap)
178 repo._branchcaches.replace(repo, rbranchmap)
177
179
178 repo.invalidate()
180 repo.invalidate()
179
181
180 def allowservergeneration(repo):
182 def allowservergeneration(repo):
181 """Whether streaming clones are allowed from the server."""
183 """Whether streaming clones are allowed from the server."""
182 if repository.REPO_FEATURE_STREAM_CLONE not in repo.features:
184 if repository.REPO_FEATURE_STREAM_CLONE not in repo.features:
183 return False
185 return False
184
186
185 if not repo.ui.configbool('server', 'uncompressed', untrusted=True):
187 if not repo.ui.configbool('server', 'uncompressed', untrusted=True):
186 return False
188 return False
187
189
188 # The way stream clone works makes it impossible to hide secret changesets.
190 # The way stream clone works makes it impossible to hide secret changesets.
189 # So don't allow this by default.
191 # So don't allow this by default.
190 secret = phases.hassecret(repo)
192 secret = phases.hassecret(repo)
191 if secret:
193 if secret:
192 return repo.ui.configbool('server', 'uncompressedallowsecret')
194 return repo.ui.configbool('server', 'uncompressedallowsecret')
193
195
194 return True
196 return True
195
197
196 # This is it's own function so extensions can override it.
198 # This is it's own function so extensions can override it.
197 def _walkstreamfiles(repo, matcher=None):
199 def _walkstreamfiles(repo, matcher=None):
198 return repo.store.walk(matcher)
200 return repo.store.walk(matcher)
199
201
200 def generatev1(repo):
202 def generatev1(repo):
201 """Emit content for version 1 of a streaming clone.
203 """Emit content for version 1 of a streaming clone.
202
204
203 This returns a 3-tuple of (file count, byte size, data iterator).
205 This returns a 3-tuple of (file count, byte size, data iterator).
204
206
205 The data iterator consists of N entries for each file being transferred.
207 The data iterator consists of N entries for each file being transferred.
206 Each file entry starts as a line with the file name and integer size
208 Each file entry starts as a line with the file name and integer size
207 delimited by a null byte.
209 delimited by a null byte.
208
210
209 The raw file data follows. Following the raw file data is the next file
211 The raw file data follows. Following the raw file data is the next file
210 entry, or EOF.
212 entry, or EOF.
211
213
212 When used on the wire protocol, an additional line indicating protocol
214 When used on the wire protocol, an additional line indicating protocol
213 success will be prepended to the stream. This function is not responsible
215 success will be prepended to the stream. This function is not responsible
214 for adding it.
216 for adding it.
215
217
216 This function will obtain a repository lock to ensure a consistent view of
218 This function will obtain a repository lock to ensure a consistent view of
217 the store is captured. It therefore may raise LockError.
219 the store is captured. It therefore may raise LockError.
218 """
220 """
219 entries = []
221 entries = []
220 total_bytes = 0
222 total_bytes = 0
221 # Get consistent snapshot of repo, lock during scan.
223 # Get consistent snapshot of repo, lock during scan.
222 with repo.lock():
224 with repo.lock():
223 repo.ui.debug('scanning\n')
225 repo.ui.debug('scanning\n')
224 for name, ename, size in _walkstreamfiles(repo):
226 for name, ename, size in _walkstreamfiles(repo):
225 if size:
227 if size:
226 entries.append((name, size))
228 entries.append((name, size))
227 total_bytes += size
229 total_bytes += size
228
230
229 repo.ui.debug('%d files, %d bytes to transfer\n' %
231 repo.ui.debug('%d files, %d bytes to transfer\n' %
230 (len(entries), total_bytes))
232 (len(entries), total_bytes))
231
233
232 svfs = repo.svfs
234 svfs = repo.svfs
233 debugflag = repo.ui.debugflag
235 debugflag = repo.ui.debugflag
234
236
235 def emitrevlogdata():
237 def emitrevlogdata():
236 for name, size in entries:
238 for name, size in entries:
237 if debugflag:
239 if debugflag:
238 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
240 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
239 # partially encode name over the wire for backwards compat
241 # partially encode name over the wire for backwards compat
240 yield '%s\0%d\n' % (store.encodedir(name), size)
242 yield '%s\0%d\n' % (store.encodedir(name), size)
241 # auditing at this stage is both pointless (paths are already
243 # auditing at this stage is both pointless (paths are already
242 # trusted by the local repo) and expensive
244 # trusted by the local repo) and expensive
243 with svfs(name, 'rb', auditpath=False) as fp:
245 with svfs(name, 'rb', auditpath=False) as fp:
244 if size <= 65536:
246 if size <= 65536:
245 yield fp.read(size)
247 yield fp.read(size)
246 else:
248 else:
247 for chunk in util.filechunkiter(fp, limit=size):
249 for chunk in util.filechunkiter(fp, limit=size):
248 yield chunk
250 yield chunk
249
251
250 return len(entries), total_bytes, emitrevlogdata()
252 return len(entries), total_bytes, emitrevlogdata()
251
253
252 def generatev1wireproto(repo):
254 def generatev1wireproto(repo):
253 """Emit content for version 1 of streaming clone suitable for the wire.
255 """Emit content for version 1 of streaming clone suitable for the wire.
254
256
255 This is the data output from ``generatev1()`` with 2 header lines. The
257 This is the data output from ``generatev1()`` with 2 header lines. The
256 first line indicates overall success. The 2nd contains the file count and
258 first line indicates overall success. The 2nd contains the file count and
257 byte size of payload.
259 byte size of payload.
258
260
259 The success line contains "0" for success, "1" for stream generation not
261 The success line contains "0" for success, "1" for stream generation not
260 allowed, and "2" for error locking the repository (possibly indicating
262 allowed, and "2" for error locking the repository (possibly indicating
261 a permissions error for the server process).
263 a permissions error for the server process).
262 """
264 """
263 if not allowservergeneration(repo):
265 if not allowservergeneration(repo):
264 yield '1\n'
266 yield '1\n'
265 return
267 return
266
268
267 try:
269 try:
268 filecount, bytecount, it = generatev1(repo)
270 filecount, bytecount, it = generatev1(repo)
269 except error.LockError:
271 except error.LockError:
270 yield '2\n'
272 yield '2\n'
271 return
273 return
272
274
273 # Indicates successful response.
275 # Indicates successful response.
274 yield '0\n'
276 yield '0\n'
275 yield '%d %d\n' % (filecount, bytecount)
277 yield '%d %d\n' % (filecount, bytecount)
276 for chunk in it:
278 for chunk in it:
277 yield chunk
279 yield chunk
278
280
279 def generatebundlev1(repo, compression='UN'):
281 def generatebundlev1(repo, compression='UN'):
280 """Emit content for version 1 of a stream clone bundle.
282 """Emit content for version 1 of a stream clone bundle.
281
283
282 The first 4 bytes of the output ("HGS1") denote this as stream clone
284 The first 4 bytes of the output ("HGS1") denote this as stream clone
283 bundle version 1.
285 bundle version 1.
284
286
285 The next 2 bytes indicate the compression type. Only "UN" is currently
287 The next 2 bytes indicate the compression type. Only "UN" is currently
286 supported.
288 supported.
287
289
288 The next 16 bytes are two 64-bit big endian unsigned integers indicating
290 The next 16 bytes are two 64-bit big endian unsigned integers indicating
289 file count and byte count, respectively.
291 file count and byte count, respectively.
290
292
291 The next 2 bytes is a 16-bit big endian unsigned short declaring the length
293 The next 2 bytes is a 16-bit big endian unsigned short declaring the length
292 of the requirements string, including a trailing \0. The following N bytes
294 of the requirements string, including a trailing \0. The following N bytes
293 are the requirements string, which is ASCII containing a comma-delimited
295 are the requirements string, which is ASCII containing a comma-delimited
294 list of repo requirements that are needed to support the data.
296 list of repo requirements that are needed to support the data.
295
297
296 The remaining content is the output of ``generatev1()`` (which may be
298 The remaining content is the output of ``generatev1()`` (which may be
297 compressed in the future).
299 compressed in the future).
298
300
299 Returns a tuple of (requirements, data generator).
301 Returns a tuple of (requirements, data generator).
300 """
302 """
301 if compression != 'UN':
303 if compression != 'UN':
302 raise ValueError('we do not support the compression argument yet')
304 raise ValueError('we do not support the compression argument yet')
303
305
304 requirements = repo.requirements & repo.supportedformats
306 requirements = repo.requirements & repo.supportedformats
305 requires = ','.join(sorted(requirements))
307 requires = ','.join(sorted(requirements))
306
308
307 def gen():
309 def gen():
308 yield 'HGS1'
310 yield 'HGS1'
309 yield compression
311 yield compression
310
312
311 filecount, bytecount, it = generatev1(repo)
313 filecount, bytecount, it = generatev1(repo)
312 repo.ui.status(_('writing %d bytes for %d files\n') %
314 repo.ui.status(_('writing %d bytes for %d files\n') %
313 (bytecount, filecount))
315 (bytecount, filecount))
314
316
315 yield struct.pack('>QQ', filecount, bytecount)
317 yield struct.pack('>QQ', filecount, bytecount)
316 yield struct.pack('>H', len(requires) + 1)
318 yield struct.pack('>H', len(requires) + 1)
317 yield requires + '\0'
319 yield requires + '\0'
318
320
319 # This is where we'll add compression in the future.
321 # This is where we'll add compression in the future.
320 assert compression == 'UN'
322 assert compression == 'UN'
321
323
322 progress = repo.ui.makeprogress(_('bundle'), total=bytecount,
324 progress = repo.ui.makeprogress(_('bundle'), total=bytecount,
323 unit=_('bytes'))
325 unit=_('bytes'))
324 progress.update(0)
326 progress.update(0)
325
327
326 for chunk in it:
328 for chunk in it:
327 progress.increment(step=len(chunk))
329 progress.increment(step=len(chunk))
328 yield chunk
330 yield chunk
329
331
330 progress.complete()
332 progress.complete()
331
333
332 return requirements, gen()
334 return requirements, gen()
333
335
334 def consumev1(repo, fp, filecount, bytecount):
336 def consumev1(repo, fp, filecount, bytecount):
335 """Apply the contents from version 1 of a streaming clone file handle.
337 """Apply the contents from version 1 of a streaming clone file handle.
336
338
337 This takes the output from "stream_out" and applies it to the specified
339 This takes the output from "stream_out" and applies it to the specified
338 repository.
340 repository.
339
341
340 Like "stream_out," the status line added by the wire protocol is not
342 Like "stream_out," the status line added by the wire protocol is not
341 handled by this function.
343 handled by this function.
342 """
344 """
343 with repo.lock():
345 with repo.lock():
344 repo.ui.status(_('%d files to transfer, %s of data\n') %
346 repo.ui.status(_('%d files to transfer, %s of data\n') %
345 (filecount, util.bytecount(bytecount)))
347 (filecount, util.bytecount(bytecount)))
346 progress = repo.ui.makeprogress(_('clone'), total=bytecount,
348 progress = repo.ui.makeprogress(_('clone'), total=bytecount,
347 unit=_('bytes'))
349 unit=_('bytes'))
348 progress.update(0)
350 progress.update(0)
349 start = util.timer()
351 start = util.timer()
350
352
351 # TODO: get rid of (potential) inconsistency
353 # TODO: get rid of (potential) inconsistency
352 #
354 #
353 # If transaction is started and any @filecache property is
355 # If transaction is started and any @filecache property is
354 # changed at this point, it causes inconsistency between
356 # changed at this point, it causes inconsistency between
355 # in-memory cached property and streamclone-ed file on the
357 # in-memory cached property and streamclone-ed file on the
356 # disk. Nested transaction prevents transaction scope "clone"
358 # disk. Nested transaction prevents transaction scope "clone"
357 # below from writing in-memory changes out at the end of it,
359 # below from writing in-memory changes out at the end of it,
358 # even though in-memory changes are discarded at the end of it
360 # even though in-memory changes are discarded at the end of it
359 # regardless of transaction nesting.
361 # regardless of transaction nesting.
360 #
362 #
361 # But transaction nesting can't be simply prohibited, because
363 # But transaction nesting can't be simply prohibited, because
362 # nesting occurs also in ordinary case (e.g. enabling
364 # nesting occurs also in ordinary case (e.g. enabling
363 # clonebundles).
365 # clonebundles).
364
366
365 with repo.transaction('clone'):
367 with repo.transaction('clone'):
366 with repo.svfs.backgroundclosing(repo.ui, expectedcount=filecount):
368 with repo.svfs.backgroundclosing(repo.ui, expectedcount=filecount):
367 for i in pycompat.xrange(filecount):
369 for i in pycompat.xrange(filecount):
368 # XXX doesn't support '\n' or '\r' in filenames
370 # XXX doesn't support '\n' or '\r' in filenames
369 l = fp.readline()
371 l = fp.readline()
370 try:
372 try:
371 name, size = l.split('\0', 1)
373 name, size = l.split('\0', 1)
372 size = int(size)
374 size = int(size)
373 except (ValueError, TypeError):
375 except (ValueError, TypeError):
374 raise error.ResponseError(
376 raise error.ResponseError(
375 _('unexpected response from remote server:'), l)
377 _('unexpected response from remote server:'), l)
376 if repo.ui.debugflag:
378 if repo.ui.debugflag:
377 repo.ui.debug('adding %s (%s)\n' %
379 repo.ui.debug('adding %s (%s)\n' %
378 (name, util.bytecount(size)))
380 (name, util.bytecount(size)))
379 # for backwards compat, name was partially encoded
381 # for backwards compat, name was partially encoded
380 path = store.decodedir(name)
382 path = store.decodedir(name)
381 with repo.svfs(path, 'w', backgroundclose=True) as ofp:
383 with repo.svfs(path, 'w', backgroundclose=True) as ofp:
382 for chunk in util.filechunkiter(fp, limit=size):
384 for chunk in util.filechunkiter(fp, limit=size):
383 progress.increment(step=len(chunk))
385 progress.increment(step=len(chunk))
384 ofp.write(chunk)
386 ofp.write(chunk)
385
387
386 # force @filecache properties to be reloaded from
388 # force @filecache properties to be reloaded from
387 # streamclone-ed file at next access
389 # streamclone-ed file at next access
388 repo.invalidate(clearfilecache=True)
390 repo.invalidate(clearfilecache=True)
389
391
390 elapsed = util.timer() - start
392 elapsed = util.timer() - start
391 if elapsed <= 0:
393 if elapsed <= 0:
392 elapsed = 0.001
394 elapsed = 0.001
393 progress.complete()
395 progress.complete()
394 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
396 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
395 (util.bytecount(bytecount), elapsed,
397 (util.bytecount(bytecount), elapsed,
396 util.bytecount(bytecount / elapsed)))
398 util.bytecount(bytecount / elapsed)))
397
399
398 def readbundle1header(fp):
400 def readbundle1header(fp):
399 compression = fp.read(2)
401 compression = fp.read(2)
400 if compression != 'UN':
402 if compression != 'UN':
401 raise error.Abort(_('only uncompressed stream clone bundles are '
403 raise error.Abort(_('only uncompressed stream clone bundles are '
402 'supported; got %s') % compression)
404 'supported; got %s') % compression)
403
405
404 filecount, bytecount = struct.unpack('>QQ', fp.read(16))
406 filecount, bytecount = struct.unpack('>QQ', fp.read(16))
405 requireslen = struct.unpack('>H', fp.read(2))[0]
407 requireslen = struct.unpack('>H', fp.read(2))[0]
406 requires = fp.read(requireslen)
408 requires = fp.read(requireslen)
407
409
408 if not requires.endswith('\0'):
410 if not requires.endswith('\0'):
409 raise error.Abort(_('malformed stream clone bundle: '
411 raise error.Abort(_('malformed stream clone bundle: '
410 'requirements not properly encoded'))
412 'requirements not properly encoded'))
411
413
412 requirements = set(requires.rstrip('\0').split(','))
414 requirements = set(requires.rstrip('\0').split(','))
413
415
414 return filecount, bytecount, requirements
416 return filecount, bytecount, requirements
415
417
416 def applybundlev1(repo, fp):
418 def applybundlev1(repo, fp):
417 """Apply the content from a stream clone bundle version 1.
419 """Apply the content from a stream clone bundle version 1.
418
420
419 We assume the 4 byte header has been read and validated and the file handle
421 We assume the 4 byte header has been read and validated and the file handle
420 is at the 2 byte compression identifier.
422 is at the 2 byte compression identifier.
421 """
423 """
422 if len(repo):
424 if len(repo):
423 raise error.Abort(_('cannot apply stream clone bundle on non-empty '
425 raise error.Abort(_('cannot apply stream clone bundle on non-empty '
424 'repo'))
426 'repo'))
425
427
426 filecount, bytecount, requirements = readbundle1header(fp)
428 filecount, bytecount, requirements = readbundle1header(fp)
427 missingreqs = requirements - repo.supportedformats
429 missingreqs = requirements - repo.supportedformats
428 if missingreqs:
430 if missingreqs:
429 raise error.Abort(_('unable to apply stream clone: '
431 raise error.Abort(_('unable to apply stream clone: '
430 'unsupported format: %s') %
432 'unsupported format: %s') %
431 ', '.join(sorted(missingreqs)))
433 ', '.join(sorted(missingreqs)))
432
434
433 consumev1(repo, fp, filecount, bytecount)
435 consumev1(repo, fp, filecount, bytecount)
434
436
435 class streamcloneapplier(object):
437 class streamcloneapplier(object):
436 """Class to manage applying streaming clone bundles.
438 """Class to manage applying streaming clone bundles.
437
439
438 We need to wrap ``applybundlev1()`` in a dedicated type to enable bundle
440 We need to wrap ``applybundlev1()`` in a dedicated type to enable bundle
439 readers to perform bundle type-specific functionality.
441 readers to perform bundle type-specific functionality.
440 """
442 """
441 def __init__(self, fh):
443 def __init__(self, fh):
442 self._fh = fh
444 self._fh = fh
443
445
444 def apply(self, repo):
446 def apply(self, repo):
445 return applybundlev1(repo, self._fh)
447 return applybundlev1(repo, self._fh)
446
448
447 # type of file to stream
449 # type of file to stream
448 _fileappend = 0 # append only file
450 _fileappend = 0 # append only file
449 _filefull = 1 # full snapshot file
451 _filefull = 1 # full snapshot file
450
452
451 # Source of the file
453 # Source of the file
452 _srcstore = 's' # store (svfs)
454 _srcstore = 's' # store (svfs)
453 _srccache = 'c' # cache (cache)
455 _srccache = 'c' # cache (cache)
454
456
455 # This is it's own function so extensions can override it.
457 # This is it's own function so extensions can override it.
456 def _walkstreamfullstorefiles(repo):
458 def _walkstreamfullstorefiles(repo):
457 """list snapshot file from the store"""
459 """list snapshot file from the store"""
458 fnames = []
460 fnames = []
459 if not repo.publishing():
461 if not repo.publishing():
460 fnames.append('phaseroots')
462 fnames.append('phaseroots')
461 return fnames
463 return fnames
462
464
463 def _filterfull(entry, copy, vfsmap):
465 def _filterfull(entry, copy, vfsmap):
464 """actually copy the snapshot files"""
466 """actually copy the snapshot files"""
465 src, name, ftype, data = entry
467 src, name, ftype, data = entry
466 if ftype != _filefull:
468 if ftype != _filefull:
467 return entry
469 return entry
468 return (src, name, ftype, copy(vfsmap[src].join(name)))
470 return (src, name, ftype, copy(vfsmap[src].join(name)))
469
471
470 @contextlib.contextmanager
472 @contextlib.contextmanager
471 def maketempcopies():
473 def maketempcopies():
472 """return a function to temporary copy file"""
474 """return a function to temporary copy file"""
473 files = []
475 files = []
474 try:
476 try:
475 def copy(src):
477 def copy(src):
476 fd, dst = pycompat.mkstemp()
478 fd, dst = pycompat.mkstemp()
477 os.close(fd)
479 os.close(fd)
478 files.append(dst)
480 files.append(dst)
479 util.copyfiles(src, dst, hardlink=True)
481 util.copyfiles(src, dst, hardlink=True)
480 return dst
482 return dst
481 yield copy
483 yield copy
482 finally:
484 finally:
483 for tmp in files:
485 for tmp in files:
484 util.tryunlink(tmp)
486 util.tryunlink(tmp)
485
487
486 def _makemap(repo):
488 def _makemap(repo):
487 """make a (src -> vfs) map for the repo"""
489 """make a (src -> vfs) map for the repo"""
488 vfsmap = {
490 vfsmap = {
489 _srcstore: repo.svfs,
491 _srcstore: repo.svfs,
490 _srccache: repo.cachevfs,
492 _srccache: repo.cachevfs,
491 }
493 }
492 # we keep repo.vfs out of the on purpose, ther are too many danger there
494 # we keep repo.vfs out of the on purpose, ther are too many danger there
493 # (eg: .hg/hgrc)
495 # (eg: .hg/hgrc)
494 assert repo.vfs not in vfsmap.values()
496 assert repo.vfs not in vfsmap.values()
495
497
496 return vfsmap
498 return vfsmap
497
499
498 def _emit2(repo, entries, totalfilesize):
500 def _emit2(repo, entries, totalfilesize):
499 """actually emit the stream bundle"""
501 """actually emit the stream bundle"""
500 vfsmap = _makemap(repo)
502 vfsmap = _makemap(repo)
501 progress = repo.ui.makeprogress(_('bundle'), total=totalfilesize,
503 progress = repo.ui.makeprogress(_('bundle'), total=totalfilesize,
502 unit=_('bytes'))
504 unit=_('bytes'))
503 progress.update(0)
505 progress.update(0)
504 with maketempcopies() as copy, progress:
506 with maketempcopies() as copy, progress:
505 # copy is delayed until we are in the try
507 # copy is delayed until we are in the try
506 entries = [_filterfull(e, copy, vfsmap) for e in entries]
508 entries = [_filterfull(e, copy, vfsmap) for e in entries]
507 yield None # this release the lock on the repository
509 yield None # this release the lock on the repository
508 seen = 0
510 seen = 0
509
511
510 for src, name, ftype, data in entries:
512 for src, name, ftype, data in entries:
511 vfs = vfsmap[src]
513 vfs = vfsmap[src]
512 yield src
514 yield src
513 yield util.uvarintencode(len(name))
515 yield util.uvarintencode(len(name))
514 if ftype == _fileappend:
516 if ftype == _fileappend:
515 fp = vfs(name)
517 fp = vfs(name)
516 size = data
518 size = data
517 elif ftype == _filefull:
519 elif ftype == _filefull:
518 fp = open(data, 'rb')
520 fp = open(data, 'rb')
519 size = util.fstat(fp).st_size
521 size = util.fstat(fp).st_size
520 try:
522 try:
521 yield util.uvarintencode(size)
523 yield util.uvarintencode(size)
522 yield name
524 yield name
523 if size <= 65536:
525 if size <= 65536:
524 chunks = (fp.read(size),)
526 chunks = (fp.read(size),)
525 else:
527 else:
526 chunks = util.filechunkiter(fp, limit=size)
528 chunks = util.filechunkiter(fp, limit=size)
527 for chunk in chunks:
529 for chunk in chunks:
528 seen += len(chunk)
530 seen += len(chunk)
529 progress.update(seen)
531 progress.update(seen)
530 yield chunk
532 yield chunk
531 finally:
533 finally:
532 fp.close()
534 fp.close()
533
535
534 def generatev2(repo, includes, excludes, includeobsmarkers):
536 def generatev2(repo, includes, excludes, includeobsmarkers):
535 """Emit content for version 2 of a streaming clone.
537 """Emit content for version 2 of a streaming clone.
536
538
537 the data stream consists the following entries:
539 the data stream consists the following entries:
538 1) A char representing the file destination (eg: store or cache)
540 1) A char representing the file destination (eg: store or cache)
539 2) A varint containing the length of the filename
541 2) A varint containing the length of the filename
540 3) A varint containing the length of file data
542 3) A varint containing the length of file data
541 4) N bytes containing the filename (the internal, store-agnostic form)
543 4) N bytes containing the filename (the internal, store-agnostic form)
542 5) N bytes containing the file data
544 5) N bytes containing the file data
543
545
544 Returns a 3-tuple of (file count, file size, data iterator).
546 Returns a 3-tuple of (file count, file size, data iterator).
545 """
547 """
546
548
547 with repo.lock():
549 with repo.lock():
548
550
549 entries = []
551 entries = []
550 totalfilesize = 0
552 totalfilesize = 0
551
553
552 matcher = None
554 matcher = None
553 if includes or excludes:
555 if includes or excludes:
554 matcher = narrowspec.match(repo.root, includes, excludes)
556 matcher = narrowspec.match(repo.root, includes, excludes)
555
557
556 repo.ui.debug('scanning\n')
558 repo.ui.debug('scanning\n')
557 for name, ename, size in _walkstreamfiles(repo, matcher):
559 for name, ename, size in _walkstreamfiles(repo, matcher):
558 if size:
560 if size:
559 entries.append((_srcstore, name, _fileappend, size))
561 entries.append((_srcstore, name, _fileappend, size))
560 totalfilesize += size
562 totalfilesize += size
561 for name in _walkstreamfullstorefiles(repo):
563 for name in _walkstreamfullstorefiles(repo):
562 if repo.svfs.exists(name):
564 if repo.svfs.exists(name):
563 totalfilesize += repo.svfs.lstat(name).st_size
565 totalfilesize += repo.svfs.lstat(name).st_size
564 entries.append((_srcstore, name, _filefull, None))
566 entries.append((_srcstore, name, _filefull, None))
565 if includeobsmarkers and repo.svfs.exists('obsstore'):
567 if includeobsmarkers and repo.svfs.exists('obsstore'):
566 totalfilesize += repo.svfs.lstat('obsstore').st_size
568 totalfilesize += repo.svfs.lstat('obsstore').st_size
567 entries.append((_srcstore, 'obsstore', _filefull, None))
569 entries.append((_srcstore, 'obsstore', _filefull, None))
568 for name in cacheutil.cachetocopy(repo):
570 for name in cacheutil.cachetocopy(repo):
569 if repo.cachevfs.exists(name):
571 if repo.cachevfs.exists(name):
570 totalfilesize += repo.cachevfs.lstat(name).st_size
572 totalfilesize += repo.cachevfs.lstat(name).st_size
571 entries.append((_srccache, name, _filefull, None))
573 entries.append((_srccache, name, _filefull, None))
572
574
573 chunks = _emit2(repo, entries, totalfilesize)
575 chunks = _emit2(repo, entries, totalfilesize)
574 first = next(chunks)
576 first = next(chunks)
575 assert first is None
577 assert first is None
576
578
577 return len(entries), totalfilesize, chunks
579 return len(entries), totalfilesize, chunks
578
580
579 @contextlib.contextmanager
581 @contextlib.contextmanager
580 def nested(*ctxs):
582 def nested(*ctxs):
581 this = ctxs[0]
583 this = ctxs[0]
582 rest = ctxs[1:]
584 rest = ctxs[1:]
583 with this:
585 with this:
584 if rest:
586 if rest:
585 with nested(*rest):
587 with nested(*rest):
586 yield
588 yield
587 else:
589 else:
588 yield
590 yield
589
591
590 def consumev2(repo, fp, filecount, filesize):
592 def consumev2(repo, fp, filecount, filesize):
591 """Apply the contents from a version 2 streaming clone.
593 """Apply the contents from a version 2 streaming clone.
592
594
593 Data is read from an object that only needs to provide a ``read(size)``
595 Data is read from an object that only needs to provide a ``read(size)``
594 method.
596 method.
595 """
597 """
596 with repo.lock():
598 with repo.lock():
597 repo.ui.status(_('%d files to transfer, %s of data\n') %
599 repo.ui.status(_('%d files to transfer, %s of data\n') %
598 (filecount, util.bytecount(filesize)))
600 (filecount, util.bytecount(filesize)))
599
601
600 start = util.timer()
602 start = util.timer()
601 progress = repo.ui.makeprogress(_('clone'), total=filesize,
603 progress = repo.ui.makeprogress(_('clone'), total=filesize,
602 unit=_('bytes'))
604 unit=_('bytes'))
603 progress.update(0)
605 progress.update(0)
604
606
605 vfsmap = _makemap(repo)
607 vfsmap = _makemap(repo)
606
608
607 with repo.transaction('clone'):
609 with repo.transaction('clone'):
608 ctxs = (vfs.backgroundclosing(repo.ui)
610 ctxs = (vfs.backgroundclosing(repo.ui)
609 for vfs in vfsmap.values())
611 for vfs in vfsmap.values())
610 with nested(*ctxs):
612 with nested(*ctxs):
611 for i in range(filecount):
613 for i in range(filecount):
612 src = util.readexactly(fp, 1)
614 src = util.readexactly(fp, 1)
613 vfs = vfsmap[src]
615 vfs = vfsmap[src]
614 namelen = util.uvarintdecodestream(fp)
616 namelen = util.uvarintdecodestream(fp)
615 datalen = util.uvarintdecodestream(fp)
617 datalen = util.uvarintdecodestream(fp)
616
618
617 name = util.readexactly(fp, namelen)
619 name = util.readexactly(fp, namelen)
618
620
619 if repo.ui.debugflag:
621 if repo.ui.debugflag:
620 repo.ui.debug('adding [%s] %s (%s)\n' %
622 repo.ui.debug('adding [%s] %s (%s)\n' %
621 (src, name, util.bytecount(datalen)))
623 (src, name, util.bytecount(datalen)))
622
624
623 with vfs(name, 'w') as ofp:
625 with vfs(name, 'w') as ofp:
624 for chunk in util.filechunkiter(fp, limit=datalen):
626 for chunk in util.filechunkiter(fp, limit=datalen):
625 progress.increment(step=len(chunk))
627 progress.increment(step=len(chunk))
626 ofp.write(chunk)
628 ofp.write(chunk)
627
629
628 # force @filecache properties to be reloaded from
630 # force @filecache properties to be reloaded from
629 # streamclone-ed file at next access
631 # streamclone-ed file at next access
630 repo.invalidate(clearfilecache=True)
632 repo.invalidate(clearfilecache=True)
631
633
632 elapsed = util.timer() - start
634 elapsed = util.timer() - start
633 if elapsed <= 0:
635 if elapsed <= 0:
634 elapsed = 0.001
636 elapsed = 0.001
635 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
637 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
636 (util.bytecount(progress.pos), elapsed,
638 (util.bytecount(progress.pos), elapsed,
637 util.bytecount(progress.pos / elapsed)))
639 util.bytecount(progress.pos / elapsed)))
638 progress.complete()
640 progress.complete()
639
641
640 def applybundlev2(repo, fp, filecount, filesize, requirements):
642 def applybundlev2(repo, fp, filecount, filesize, requirements):
641 from . import localrepo
643 from . import localrepo
642
644
643 missingreqs = [r for r in requirements if r not in repo.supported]
645 missingreqs = [r for r in requirements if r not in repo.supported]
644 if missingreqs:
646 if missingreqs:
645 raise error.Abort(_('unable to apply stream clone: '
647 raise error.Abort(_('unable to apply stream clone: '
646 'unsupported format: %s') %
648 'unsupported format: %s') %
647 ', '.join(sorted(missingreqs)))
649 ', '.join(sorted(missingreqs)))
648
650
649 consumev2(repo, fp, filecount, filesize)
651 consumev2(repo, fp, filecount, filesize)
650
652
651 # new requirements = old non-format requirements +
653 # new requirements = old non-format requirements +
652 # new format-related remote requirements
654 # new format-related remote requirements
653 # requirements from the streamed-in repository
655 # requirements from the streamed-in repository
654 repo.requirements = set(requirements) | (
656 repo.requirements = set(requirements) | (
655 repo.requirements - repo.supportedformats)
657 repo.requirements - repo.supportedformats)
656 repo.svfs.options = localrepo.resolvestorevfsoptions(
658 repo.svfs.options = localrepo.resolvestorevfsoptions(
657 repo.ui, repo.requirements, repo.features)
659 repo.ui, repo.requirements, repo.features)
658 repo._writerequirements()
660 repo._writerequirements()
@@ -1,1346 +1,1348
1 # storage.py - Testing of storage primitives.
1 # storage.py - Testing of storage primitives.
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import unittest
10 import unittest
11
11
12 from ..node import (
12 from ..node import (
13 hex,
13 hex,
14 nullid,
14 nullid,
15 nullrev,
15 nullrev,
16 )
16 )
17 from .. import (
17 from .. import (
18 error,
18 error,
19 mdiff,
19 mdiff,
20 )
21 from ..interfaces import (
20 repository,
22 repository,
21 )
23 )
22 from ..utils import (
24 from ..utils import (
23 storageutil,
25 storageutil,
24 )
26 )
25
27
26 class basetestcase(unittest.TestCase):
28 class basetestcase(unittest.TestCase):
27 if not getattr(unittest.TestCase, r'assertRaisesRegex', False):
29 if not getattr(unittest.TestCase, r'assertRaisesRegex', False):
28 assertRaisesRegex = (# camelcase-required
30 assertRaisesRegex = (# camelcase-required
29 unittest.TestCase.assertRaisesRegexp)
31 unittest.TestCase.assertRaisesRegexp)
30
32
31 class ifileindextests(basetestcase):
33 class ifileindextests(basetestcase):
32 """Generic tests for the ifileindex interface.
34 """Generic tests for the ifileindex interface.
33
35
34 All file storage backends for index data should conform to the tests in this
36 All file storage backends for index data should conform to the tests in this
35 class.
37 class.
36
38
37 Use ``makeifileindextests()`` to create an instance of this type.
39 Use ``makeifileindextests()`` to create an instance of this type.
38 """
40 """
39 def testempty(self):
41 def testempty(self):
40 f = self._makefilefn()
42 f = self._makefilefn()
41 self.assertEqual(len(f), 0, 'new file store has 0 length by default')
43 self.assertEqual(len(f), 0, 'new file store has 0 length by default')
42 self.assertEqual(list(f), [], 'iter yields nothing by default')
44 self.assertEqual(list(f), [], 'iter yields nothing by default')
43
45
44 gen = iter(f)
46 gen = iter(f)
45 with self.assertRaises(StopIteration):
47 with self.assertRaises(StopIteration):
46 next(gen)
48 next(gen)
47
49
48 self.assertFalse(f.hasnode(None))
50 self.assertFalse(f.hasnode(None))
49 self.assertFalse(f.hasnode(0))
51 self.assertFalse(f.hasnode(0))
50 self.assertFalse(f.hasnode(nullrev))
52 self.assertFalse(f.hasnode(nullrev))
51 self.assertFalse(f.hasnode(nullid))
53 self.assertFalse(f.hasnode(nullid))
52 self.assertFalse(f.hasnode(b'0'))
54 self.assertFalse(f.hasnode(b'0'))
53 self.assertFalse(f.hasnode(b'a' * 20))
55 self.assertFalse(f.hasnode(b'a' * 20))
54
56
55 # revs() should evaluate to an empty list.
57 # revs() should evaluate to an empty list.
56 self.assertEqual(list(f.revs()), [])
58 self.assertEqual(list(f.revs()), [])
57
59
58 revs = iter(f.revs())
60 revs = iter(f.revs())
59 with self.assertRaises(StopIteration):
61 with self.assertRaises(StopIteration):
60 next(revs)
62 next(revs)
61
63
62 self.assertEqual(list(f.revs(start=20)), [])
64 self.assertEqual(list(f.revs(start=20)), [])
63
65
64 # parents() and parentrevs() work with nullid/nullrev.
66 # parents() and parentrevs() work with nullid/nullrev.
65 self.assertEqual(f.parents(nullid), (nullid, nullid))
67 self.assertEqual(f.parents(nullid), (nullid, nullid))
66 self.assertEqual(f.parentrevs(nullrev), (nullrev, nullrev))
68 self.assertEqual(f.parentrevs(nullrev), (nullrev, nullrev))
67
69
68 with self.assertRaises(error.LookupError):
70 with self.assertRaises(error.LookupError):
69 f.parents(b'\x01' * 20)
71 f.parents(b'\x01' * 20)
70
72
71 for i in range(-5, 5):
73 for i in range(-5, 5):
72 if i == nullrev:
74 if i == nullrev:
73 continue
75 continue
74
76
75 with self.assertRaises(IndexError):
77 with self.assertRaises(IndexError):
76 f.parentrevs(i)
78 f.parentrevs(i)
77
79
78 # nullid/nullrev lookup always works.
80 # nullid/nullrev lookup always works.
79 self.assertEqual(f.rev(nullid), nullrev)
81 self.assertEqual(f.rev(nullid), nullrev)
80 self.assertEqual(f.node(nullrev), nullid)
82 self.assertEqual(f.node(nullrev), nullid)
81
83
82 with self.assertRaises(error.LookupError):
84 with self.assertRaises(error.LookupError):
83 f.rev(b'\x01' * 20)
85 f.rev(b'\x01' * 20)
84
86
85 for i in range(-5, 5):
87 for i in range(-5, 5):
86 if i == nullrev:
88 if i == nullrev:
87 continue
89 continue
88
90
89 with self.assertRaises(IndexError):
91 with self.assertRaises(IndexError):
90 f.node(i)
92 f.node(i)
91
93
92 self.assertEqual(f.lookup(nullid), nullid)
94 self.assertEqual(f.lookup(nullid), nullid)
93 self.assertEqual(f.lookup(nullrev), nullid)
95 self.assertEqual(f.lookup(nullrev), nullid)
94 self.assertEqual(f.lookup(hex(nullid)), nullid)
96 self.assertEqual(f.lookup(hex(nullid)), nullid)
95 self.assertEqual(f.lookup(b'%d' % nullrev), nullid)
97 self.assertEqual(f.lookup(b'%d' % nullrev), nullid)
96
98
97 with self.assertRaises(error.LookupError):
99 with self.assertRaises(error.LookupError):
98 f.lookup(b'badvalue')
100 f.lookup(b'badvalue')
99
101
100 with self.assertRaises(error.LookupError):
102 with self.assertRaises(error.LookupError):
101 f.lookup(hex(nullid)[0:12])
103 f.lookup(hex(nullid)[0:12])
102
104
103 with self.assertRaises(error.LookupError):
105 with self.assertRaises(error.LookupError):
104 f.lookup(b'-2')
106 f.lookup(b'-2')
105
107
106 with self.assertRaises(error.LookupError):
108 with self.assertRaises(error.LookupError):
107 f.lookup(b'0')
109 f.lookup(b'0')
108
110
109 with self.assertRaises(error.LookupError):
111 with self.assertRaises(error.LookupError):
110 f.lookup(b'1')
112 f.lookup(b'1')
111
113
112 with self.assertRaises(error.LookupError):
114 with self.assertRaises(error.LookupError):
113 f.lookup(b'11111111111111111111111111111111111111')
115 f.lookup(b'11111111111111111111111111111111111111')
114
116
115 for i in range(-5, 5):
117 for i in range(-5, 5):
116 if i == nullrev:
118 if i == nullrev:
117 continue
119 continue
118
120
119 with self.assertRaises(LookupError):
121 with self.assertRaises(LookupError):
120 f.lookup(i)
122 f.lookup(i)
121
123
122 self.assertEqual(f.linkrev(nullrev), nullrev)
124 self.assertEqual(f.linkrev(nullrev), nullrev)
123
125
124 for i in range(-5, 5):
126 for i in range(-5, 5):
125 if i == nullrev:
127 if i == nullrev:
126 continue
128 continue
127
129
128 with self.assertRaises(IndexError):
130 with self.assertRaises(IndexError):
129 f.linkrev(i)
131 f.linkrev(i)
130
132
131 self.assertFalse(f.iscensored(nullrev))
133 self.assertFalse(f.iscensored(nullrev))
132
134
133 for i in range(-5, 5):
135 for i in range(-5, 5):
134 if i == nullrev:
136 if i == nullrev:
135 continue
137 continue
136
138
137 with self.assertRaises(IndexError):
139 with self.assertRaises(IndexError):
138 f.iscensored(i)
140 f.iscensored(i)
139
141
140 self.assertEqual(list(f.commonancestorsheads(nullid, nullid)), [])
142 self.assertEqual(list(f.commonancestorsheads(nullid, nullid)), [])
141
143
142 with self.assertRaises(ValueError):
144 with self.assertRaises(ValueError):
143 self.assertEqual(list(f.descendants([])), [])
145 self.assertEqual(list(f.descendants([])), [])
144
146
145 self.assertEqual(list(f.descendants([nullrev])), [])
147 self.assertEqual(list(f.descendants([nullrev])), [])
146
148
147 self.assertEqual(f.heads(), [nullid])
149 self.assertEqual(f.heads(), [nullid])
148 self.assertEqual(f.heads(nullid), [nullid])
150 self.assertEqual(f.heads(nullid), [nullid])
149 self.assertEqual(f.heads(None, [nullid]), [nullid])
151 self.assertEqual(f.heads(None, [nullid]), [nullid])
150 self.assertEqual(f.heads(nullid, [nullid]), [nullid])
152 self.assertEqual(f.heads(nullid, [nullid]), [nullid])
151
153
152 self.assertEqual(f.children(nullid), [])
154 self.assertEqual(f.children(nullid), [])
153
155
154 with self.assertRaises(error.LookupError):
156 with self.assertRaises(error.LookupError):
155 f.children(b'\x01' * 20)
157 f.children(b'\x01' * 20)
156
158
157 def testsinglerevision(self):
159 def testsinglerevision(self):
158 f = self._makefilefn()
160 f = self._makefilefn()
159 with self._maketransactionfn() as tr:
161 with self._maketransactionfn() as tr:
160 node = f.add(b'initial', None, tr, 0, nullid, nullid)
162 node = f.add(b'initial', None, tr, 0, nullid, nullid)
161
163
162 self.assertEqual(len(f), 1)
164 self.assertEqual(len(f), 1)
163 self.assertEqual(list(f), [0])
165 self.assertEqual(list(f), [0])
164
166
165 gen = iter(f)
167 gen = iter(f)
166 self.assertEqual(next(gen), 0)
168 self.assertEqual(next(gen), 0)
167
169
168 with self.assertRaises(StopIteration):
170 with self.assertRaises(StopIteration):
169 next(gen)
171 next(gen)
170
172
171 self.assertTrue(f.hasnode(node))
173 self.assertTrue(f.hasnode(node))
172 self.assertFalse(f.hasnode(hex(node)))
174 self.assertFalse(f.hasnode(hex(node)))
173 self.assertFalse(f.hasnode(nullrev))
175 self.assertFalse(f.hasnode(nullrev))
174 self.assertFalse(f.hasnode(nullid))
176 self.assertFalse(f.hasnode(nullid))
175 self.assertFalse(f.hasnode(node[0:12]))
177 self.assertFalse(f.hasnode(node[0:12]))
176 self.assertFalse(f.hasnode(hex(node)[0:20]))
178 self.assertFalse(f.hasnode(hex(node)[0:20]))
177
179
178 self.assertEqual(list(f.revs()), [0])
180 self.assertEqual(list(f.revs()), [0])
179 self.assertEqual(list(f.revs(start=1)), [])
181 self.assertEqual(list(f.revs(start=1)), [])
180 self.assertEqual(list(f.revs(start=0)), [0])
182 self.assertEqual(list(f.revs(start=0)), [0])
181 self.assertEqual(list(f.revs(stop=0)), [0])
183 self.assertEqual(list(f.revs(stop=0)), [0])
182 self.assertEqual(list(f.revs(stop=1)), [0])
184 self.assertEqual(list(f.revs(stop=1)), [0])
183 self.assertEqual(list(f.revs(1, 1)), [])
185 self.assertEqual(list(f.revs(1, 1)), [])
184 # TODO buggy
186 # TODO buggy
185 self.assertEqual(list(f.revs(1, 0)), [1, 0])
187 self.assertEqual(list(f.revs(1, 0)), [1, 0])
186 self.assertEqual(list(f.revs(2, 0)), [2, 1, 0])
188 self.assertEqual(list(f.revs(2, 0)), [2, 1, 0])
187
189
188 self.assertEqual(f.parents(node), (nullid, nullid))
190 self.assertEqual(f.parents(node), (nullid, nullid))
189 self.assertEqual(f.parentrevs(0), (nullrev, nullrev))
191 self.assertEqual(f.parentrevs(0), (nullrev, nullrev))
190
192
191 with self.assertRaises(error.LookupError):
193 with self.assertRaises(error.LookupError):
192 f.parents(b'\x01' * 20)
194 f.parents(b'\x01' * 20)
193
195
194 with self.assertRaises(IndexError):
196 with self.assertRaises(IndexError):
195 f.parentrevs(1)
197 f.parentrevs(1)
196
198
197 self.assertEqual(f.rev(node), 0)
199 self.assertEqual(f.rev(node), 0)
198
200
199 with self.assertRaises(error.LookupError):
201 with self.assertRaises(error.LookupError):
200 f.rev(b'\x01' * 20)
202 f.rev(b'\x01' * 20)
201
203
202 self.assertEqual(f.node(0), node)
204 self.assertEqual(f.node(0), node)
203
205
204 with self.assertRaises(IndexError):
206 with self.assertRaises(IndexError):
205 f.node(1)
207 f.node(1)
206
208
207 self.assertEqual(f.lookup(node), node)
209 self.assertEqual(f.lookup(node), node)
208 self.assertEqual(f.lookup(0), node)
210 self.assertEqual(f.lookup(0), node)
209 self.assertEqual(f.lookup(-1), nullid)
211 self.assertEqual(f.lookup(-1), nullid)
210 self.assertEqual(f.lookup(b'0'), node)
212 self.assertEqual(f.lookup(b'0'), node)
211 self.assertEqual(f.lookup(hex(node)), node)
213 self.assertEqual(f.lookup(hex(node)), node)
212
214
213 with self.assertRaises(error.LookupError):
215 with self.assertRaises(error.LookupError):
214 f.lookup(hex(node)[0:12])
216 f.lookup(hex(node)[0:12])
215
217
216 with self.assertRaises(error.LookupError):
218 with self.assertRaises(error.LookupError):
217 f.lookup(-2)
219 f.lookup(-2)
218
220
219 with self.assertRaises(error.LookupError):
221 with self.assertRaises(error.LookupError):
220 f.lookup(b'-2')
222 f.lookup(b'-2')
221
223
222 with self.assertRaises(error.LookupError):
224 with self.assertRaises(error.LookupError):
223 f.lookup(1)
225 f.lookup(1)
224
226
225 with self.assertRaises(error.LookupError):
227 with self.assertRaises(error.LookupError):
226 f.lookup(b'1')
228 f.lookup(b'1')
227
229
228 self.assertEqual(f.linkrev(0), 0)
230 self.assertEqual(f.linkrev(0), 0)
229
231
230 with self.assertRaises(IndexError):
232 with self.assertRaises(IndexError):
231 f.linkrev(1)
233 f.linkrev(1)
232
234
233 self.assertFalse(f.iscensored(0))
235 self.assertFalse(f.iscensored(0))
234
236
235 with self.assertRaises(IndexError):
237 with self.assertRaises(IndexError):
236 f.iscensored(1)
238 f.iscensored(1)
237
239
238 self.assertEqual(list(f.descendants([0])), [])
240 self.assertEqual(list(f.descendants([0])), [])
239
241
240 self.assertEqual(f.heads(), [node])
242 self.assertEqual(f.heads(), [node])
241 self.assertEqual(f.heads(node), [node])
243 self.assertEqual(f.heads(node), [node])
242 self.assertEqual(f.heads(stop=[node]), [node])
244 self.assertEqual(f.heads(stop=[node]), [node])
243
245
244 with self.assertRaises(error.LookupError):
246 with self.assertRaises(error.LookupError):
245 f.heads(stop=[b'\x01' * 20])
247 f.heads(stop=[b'\x01' * 20])
246
248
247 self.assertEqual(f.children(node), [])
249 self.assertEqual(f.children(node), [])
248
250
249 def testmultiplerevisions(self):
251 def testmultiplerevisions(self):
250 fulltext0 = b'x' * 1024
252 fulltext0 = b'x' * 1024
251 fulltext1 = fulltext0 + b'y'
253 fulltext1 = fulltext0 + b'y'
252 fulltext2 = b'y' + fulltext0 + b'z'
254 fulltext2 = b'y' + fulltext0 + b'z'
253
255
254 f = self._makefilefn()
256 f = self._makefilefn()
255 with self._maketransactionfn() as tr:
257 with self._maketransactionfn() as tr:
256 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
258 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
257 node1 = f.add(fulltext1, None, tr, 1, node0, nullid)
259 node1 = f.add(fulltext1, None, tr, 1, node0, nullid)
258 node2 = f.add(fulltext2, None, tr, 3, node1, nullid)
260 node2 = f.add(fulltext2, None, tr, 3, node1, nullid)
259
261
260 self.assertEqual(len(f), 3)
262 self.assertEqual(len(f), 3)
261 self.assertEqual(list(f), [0, 1, 2])
263 self.assertEqual(list(f), [0, 1, 2])
262
264
263 gen = iter(f)
265 gen = iter(f)
264 self.assertEqual(next(gen), 0)
266 self.assertEqual(next(gen), 0)
265 self.assertEqual(next(gen), 1)
267 self.assertEqual(next(gen), 1)
266 self.assertEqual(next(gen), 2)
268 self.assertEqual(next(gen), 2)
267
269
268 with self.assertRaises(StopIteration):
270 with self.assertRaises(StopIteration):
269 next(gen)
271 next(gen)
270
272
271 self.assertEqual(list(f.revs()), [0, 1, 2])
273 self.assertEqual(list(f.revs()), [0, 1, 2])
272 self.assertEqual(list(f.revs(0)), [0, 1, 2])
274 self.assertEqual(list(f.revs(0)), [0, 1, 2])
273 self.assertEqual(list(f.revs(1)), [1, 2])
275 self.assertEqual(list(f.revs(1)), [1, 2])
274 self.assertEqual(list(f.revs(2)), [2])
276 self.assertEqual(list(f.revs(2)), [2])
275 self.assertEqual(list(f.revs(3)), [])
277 self.assertEqual(list(f.revs(3)), [])
276 self.assertEqual(list(f.revs(stop=1)), [0, 1])
278 self.assertEqual(list(f.revs(stop=1)), [0, 1])
277 self.assertEqual(list(f.revs(stop=2)), [0, 1, 2])
279 self.assertEqual(list(f.revs(stop=2)), [0, 1, 2])
278 self.assertEqual(list(f.revs(stop=3)), [0, 1, 2])
280 self.assertEqual(list(f.revs(stop=3)), [0, 1, 2])
279 self.assertEqual(list(f.revs(2, 0)), [2, 1, 0])
281 self.assertEqual(list(f.revs(2, 0)), [2, 1, 0])
280 self.assertEqual(list(f.revs(2, 1)), [2, 1])
282 self.assertEqual(list(f.revs(2, 1)), [2, 1])
281 # TODO this is wrong
283 # TODO this is wrong
282 self.assertEqual(list(f.revs(3, 2)), [3, 2])
284 self.assertEqual(list(f.revs(3, 2)), [3, 2])
283
285
284 self.assertEqual(f.parents(node0), (nullid, nullid))
286 self.assertEqual(f.parents(node0), (nullid, nullid))
285 self.assertEqual(f.parents(node1), (node0, nullid))
287 self.assertEqual(f.parents(node1), (node0, nullid))
286 self.assertEqual(f.parents(node2), (node1, nullid))
288 self.assertEqual(f.parents(node2), (node1, nullid))
287
289
288 self.assertEqual(f.parentrevs(0), (nullrev, nullrev))
290 self.assertEqual(f.parentrevs(0), (nullrev, nullrev))
289 self.assertEqual(f.parentrevs(1), (0, nullrev))
291 self.assertEqual(f.parentrevs(1), (0, nullrev))
290 self.assertEqual(f.parentrevs(2), (1, nullrev))
292 self.assertEqual(f.parentrevs(2), (1, nullrev))
291
293
292 self.assertEqual(f.rev(node0), 0)
294 self.assertEqual(f.rev(node0), 0)
293 self.assertEqual(f.rev(node1), 1)
295 self.assertEqual(f.rev(node1), 1)
294 self.assertEqual(f.rev(node2), 2)
296 self.assertEqual(f.rev(node2), 2)
295
297
296 with self.assertRaises(error.LookupError):
298 with self.assertRaises(error.LookupError):
297 f.rev(b'\x01' * 20)
299 f.rev(b'\x01' * 20)
298
300
299 self.assertEqual(f.node(0), node0)
301 self.assertEqual(f.node(0), node0)
300 self.assertEqual(f.node(1), node1)
302 self.assertEqual(f.node(1), node1)
301 self.assertEqual(f.node(2), node2)
303 self.assertEqual(f.node(2), node2)
302
304
303 with self.assertRaises(IndexError):
305 with self.assertRaises(IndexError):
304 f.node(3)
306 f.node(3)
305
307
306 self.assertEqual(f.lookup(node0), node0)
308 self.assertEqual(f.lookup(node0), node0)
307 self.assertEqual(f.lookup(0), node0)
309 self.assertEqual(f.lookup(0), node0)
308 self.assertEqual(f.lookup(b'0'), node0)
310 self.assertEqual(f.lookup(b'0'), node0)
309 self.assertEqual(f.lookup(hex(node0)), node0)
311 self.assertEqual(f.lookup(hex(node0)), node0)
310
312
311 self.assertEqual(f.lookup(node1), node1)
313 self.assertEqual(f.lookup(node1), node1)
312 self.assertEqual(f.lookup(1), node1)
314 self.assertEqual(f.lookup(1), node1)
313 self.assertEqual(f.lookup(b'1'), node1)
315 self.assertEqual(f.lookup(b'1'), node1)
314 self.assertEqual(f.lookup(hex(node1)), node1)
316 self.assertEqual(f.lookup(hex(node1)), node1)
315
317
316 self.assertEqual(f.linkrev(0), 0)
318 self.assertEqual(f.linkrev(0), 0)
317 self.assertEqual(f.linkrev(1), 1)
319 self.assertEqual(f.linkrev(1), 1)
318 self.assertEqual(f.linkrev(2), 3)
320 self.assertEqual(f.linkrev(2), 3)
319
321
320 with self.assertRaises(IndexError):
322 with self.assertRaises(IndexError):
321 f.linkrev(3)
323 f.linkrev(3)
322
324
323 self.assertFalse(f.iscensored(0))
325 self.assertFalse(f.iscensored(0))
324 self.assertFalse(f.iscensored(1))
326 self.assertFalse(f.iscensored(1))
325 self.assertFalse(f.iscensored(2))
327 self.assertFalse(f.iscensored(2))
326
328
327 with self.assertRaises(IndexError):
329 with self.assertRaises(IndexError):
328 f.iscensored(3)
330 f.iscensored(3)
329
331
330 self.assertEqual(f.commonancestorsheads(node1, nullid), [])
332 self.assertEqual(f.commonancestorsheads(node1, nullid), [])
331 self.assertEqual(f.commonancestorsheads(node1, node0), [node0])
333 self.assertEqual(f.commonancestorsheads(node1, node0), [node0])
332 self.assertEqual(f.commonancestorsheads(node1, node1), [node1])
334 self.assertEqual(f.commonancestorsheads(node1, node1), [node1])
333 self.assertEqual(f.commonancestorsheads(node0, node1), [node0])
335 self.assertEqual(f.commonancestorsheads(node0, node1), [node0])
334 self.assertEqual(f.commonancestorsheads(node1, node2), [node1])
336 self.assertEqual(f.commonancestorsheads(node1, node2), [node1])
335 self.assertEqual(f.commonancestorsheads(node2, node1), [node1])
337 self.assertEqual(f.commonancestorsheads(node2, node1), [node1])
336
338
337 self.assertEqual(list(f.descendants([0])), [1, 2])
339 self.assertEqual(list(f.descendants([0])), [1, 2])
338 self.assertEqual(list(f.descendants([1])), [2])
340 self.assertEqual(list(f.descendants([1])), [2])
339 self.assertEqual(list(f.descendants([0, 1])), [1, 2])
341 self.assertEqual(list(f.descendants([0, 1])), [1, 2])
340
342
341 self.assertEqual(f.heads(), [node2])
343 self.assertEqual(f.heads(), [node2])
342 self.assertEqual(f.heads(node0), [node2])
344 self.assertEqual(f.heads(node0), [node2])
343 self.assertEqual(f.heads(node1), [node2])
345 self.assertEqual(f.heads(node1), [node2])
344 self.assertEqual(f.heads(node2), [node2])
346 self.assertEqual(f.heads(node2), [node2])
345
347
346 # TODO this behavior seems wonky. Is it correct? If so, the
348 # TODO this behavior seems wonky. Is it correct? If so, the
347 # docstring for heads() should be updated to reflect desired
349 # docstring for heads() should be updated to reflect desired
348 # behavior.
350 # behavior.
349 self.assertEqual(f.heads(stop=[node1]), [node1, node2])
351 self.assertEqual(f.heads(stop=[node1]), [node1, node2])
350 self.assertEqual(f.heads(stop=[node0]), [node0, node2])
352 self.assertEqual(f.heads(stop=[node0]), [node0, node2])
351 self.assertEqual(f.heads(stop=[node1, node2]), [node1, node2])
353 self.assertEqual(f.heads(stop=[node1, node2]), [node1, node2])
352
354
353 with self.assertRaises(error.LookupError):
355 with self.assertRaises(error.LookupError):
354 f.heads(stop=[b'\x01' * 20])
356 f.heads(stop=[b'\x01' * 20])
355
357
356 self.assertEqual(f.children(node0), [node1])
358 self.assertEqual(f.children(node0), [node1])
357 self.assertEqual(f.children(node1), [node2])
359 self.assertEqual(f.children(node1), [node2])
358 self.assertEqual(f.children(node2), [])
360 self.assertEqual(f.children(node2), [])
359
361
360 def testmultipleheads(self):
362 def testmultipleheads(self):
361 f = self._makefilefn()
363 f = self._makefilefn()
362
364
363 with self._maketransactionfn() as tr:
365 with self._maketransactionfn() as tr:
364 node0 = f.add(b'0', None, tr, 0, nullid, nullid)
366 node0 = f.add(b'0', None, tr, 0, nullid, nullid)
365 node1 = f.add(b'1', None, tr, 1, node0, nullid)
367 node1 = f.add(b'1', None, tr, 1, node0, nullid)
366 node2 = f.add(b'2', None, tr, 2, node1, nullid)
368 node2 = f.add(b'2', None, tr, 2, node1, nullid)
367 node3 = f.add(b'3', None, tr, 3, node0, nullid)
369 node3 = f.add(b'3', None, tr, 3, node0, nullid)
368 node4 = f.add(b'4', None, tr, 4, node3, nullid)
370 node4 = f.add(b'4', None, tr, 4, node3, nullid)
369 node5 = f.add(b'5', None, tr, 5, node0, nullid)
371 node5 = f.add(b'5', None, tr, 5, node0, nullid)
370
372
371 self.assertEqual(len(f), 6)
373 self.assertEqual(len(f), 6)
372
374
373 self.assertEqual(list(f.descendants([0])), [1, 2, 3, 4, 5])
375 self.assertEqual(list(f.descendants([0])), [1, 2, 3, 4, 5])
374 self.assertEqual(list(f.descendants([1])), [2])
376 self.assertEqual(list(f.descendants([1])), [2])
375 self.assertEqual(list(f.descendants([2])), [])
377 self.assertEqual(list(f.descendants([2])), [])
376 self.assertEqual(list(f.descendants([3])), [4])
378 self.assertEqual(list(f.descendants([3])), [4])
377 self.assertEqual(list(f.descendants([0, 1])), [1, 2, 3, 4, 5])
379 self.assertEqual(list(f.descendants([0, 1])), [1, 2, 3, 4, 5])
378 self.assertEqual(list(f.descendants([1, 3])), [2, 4])
380 self.assertEqual(list(f.descendants([1, 3])), [2, 4])
379
381
380 self.assertEqual(f.heads(), [node2, node4, node5])
382 self.assertEqual(f.heads(), [node2, node4, node5])
381 self.assertEqual(f.heads(node0), [node2, node4, node5])
383 self.assertEqual(f.heads(node0), [node2, node4, node5])
382 self.assertEqual(f.heads(node1), [node2])
384 self.assertEqual(f.heads(node1), [node2])
383 self.assertEqual(f.heads(node2), [node2])
385 self.assertEqual(f.heads(node2), [node2])
384 self.assertEqual(f.heads(node3), [node4])
386 self.assertEqual(f.heads(node3), [node4])
385 self.assertEqual(f.heads(node4), [node4])
387 self.assertEqual(f.heads(node4), [node4])
386 self.assertEqual(f.heads(node5), [node5])
388 self.assertEqual(f.heads(node5), [node5])
387
389
388 # TODO this seems wrong.
390 # TODO this seems wrong.
389 self.assertEqual(f.heads(stop=[node0]), [node0, node2, node4, node5])
391 self.assertEqual(f.heads(stop=[node0]), [node0, node2, node4, node5])
390 self.assertEqual(f.heads(stop=[node1]), [node1, node2, node4, node5])
392 self.assertEqual(f.heads(stop=[node1]), [node1, node2, node4, node5])
391
393
392 self.assertEqual(f.children(node0), [node1, node3, node5])
394 self.assertEqual(f.children(node0), [node1, node3, node5])
393 self.assertEqual(f.children(node1), [node2])
395 self.assertEqual(f.children(node1), [node2])
394 self.assertEqual(f.children(node2), [])
396 self.assertEqual(f.children(node2), [])
395 self.assertEqual(f.children(node3), [node4])
397 self.assertEqual(f.children(node3), [node4])
396 self.assertEqual(f.children(node4), [])
398 self.assertEqual(f.children(node4), [])
397 self.assertEqual(f.children(node5), [])
399 self.assertEqual(f.children(node5), [])
398
400
399 class ifiledatatests(basetestcase):
401 class ifiledatatests(basetestcase):
400 """Generic tests for the ifiledata interface.
402 """Generic tests for the ifiledata interface.
401
403
402 All file storage backends for data should conform to the tests in this
404 All file storage backends for data should conform to the tests in this
403 class.
405 class.
404
406
405 Use ``makeifiledatatests()`` to create an instance of this type.
407 Use ``makeifiledatatests()`` to create an instance of this type.
406 """
408 """
407 def testempty(self):
409 def testempty(self):
408 f = self._makefilefn()
410 f = self._makefilefn()
409
411
410 self.assertEqual(f.storageinfo(), {})
412 self.assertEqual(f.storageinfo(), {})
411 self.assertEqual(f.storageinfo(revisionscount=True, trackedsize=True),
413 self.assertEqual(f.storageinfo(revisionscount=True, trackedsize=True),
412 {'revisionscount': 0, 'trackedsize': 0})
414 {'revisionscount': 0, 'trackedsize': 0})
413
415
414 self.assertEqual(f.size(nullrev), 0)
416 self.assertEqual(f.size(nullrev), 0)
415
417
416 for i in range(-5, 5):
418 for i in range(-5, 5):
417 if i == nullrev:
419 if i == nullrev:
418 continue
420 continue
419
421
420 with self.assertRaises(IndexError):
422 with self.assertRaises(IndexError):
421 f.size(i)
423 f.size(i)
422
424
423 self.assertEqual(f.revision(nullid), b'')
425 self.assertEqual(f.revision(nullid), b'')
424 self.assertEqual(f.rawdata(nullid), b'')
426 self.assertEqual(f.rawdata(nullid), b'')
425
427
426 with self.assertRaises(error.LookupError):
428 with self.assertRaises(error.LookupError):
427 f.revision(b'\x01' * 20)
429 f.revision(b'\x01' * 20)
428
430
429 self.assertEqual(f.read(nullid), b'')
431 self.assertEqual(f.read(nullid), b'')
430
432
431 with self.assertRaises(error.LookupError):
433 with self.assertRaises(error.LookupError):
432 f.read(b'\x01' * 20)
434 f.read(b'\x01' * 20)
433
435
434 self.assertFalse(f.renamed(nullid))
436 self.assertFalse(f.renamed(nullid))
435
437
436 with self.assertRaises(error.LookupError):
438 with self.assertRaises(error.LookupError):
437 f.read(b'\x01' * 20)
439 f.read(b'\x01' * 20)
438
440
439 self.assertTrue(f.cmp(nullid, b''))
441 self.assertTrue(f.cmp(nullid, b''))
440 self.assertTrue(f.cmp(nullid, b'foo'))
442 self.assertTrue(f.cmp(nullid, b'foo'))
441
443
442 with self.assertRaises(error.LookupError):
444 with self.assertRaises(error.LookupError):
443 f.cmp(b'\x01' * 20, b'irrelevant')
445 f.cmp(b'\x01' * 20, b'irrelevant')
444
446
445 # Emitting empty list is an empty generator.
447 # Emitting empty list is an empty generator.
446 gen = f.emitrevisions([])
448 gen = f.emitrevisions([])
447 with self.assertRaises(StopIteration):
449 with self.assertRaises(StopIteration):
448 next(gen)
450 next(gen)
449
451
450 # Emitting null node yields nothing.
452 # Emitting null node yields nothing.
451 gen = f.emitrevisions([nullid])
453 gen = f.emitrevisions([nullid])
452 with self.assertRaises(StopIteration):
454 with self.assertRaises(StopIteration):
453 next(gen)
455 next(gen)
454
456
455 # Requesting unknown node fails.
457 # Requesting unknown node fails.
456 with self.assertRaises(error.LookupError):
458 with self.assertRaises(error.LookupError):
457 list(f.emitrevisions([b'\x01' * 20]))
459 list(f.emitrevisions([b'\x01' * 20]))
458
460
459 def testsinglerevision(self):
461 def testsinglerevision(self):
460 fulltext = b'initial'
462 fulltext = b'initial'
461
463
462 f = self._makefilefn()
464 f = self._makefilefn()
463 with self._maketransactionfn() as tr:
465 with self._maketransactionfn() as tr:
464 node = f.add(fulltext, None, tr, 0, nullid, nullid)
466 node = f.add(fulltext, None, tr, 0, nullid, nullid)
465
467
466 self.assertEqual(f.storageinfo(), {})
468 self.assertEqual(f.storageinfo(), {})
467 self.assertEqual(f.storageinfo(revisionscount=True, trackedsize=True),
469 self.assertEqual(f.storageinfo(revisionscount=True, trackedsize=True),
468 {'revisionscount': 1, 'trackedsize': len(fulltext)})
470 {'revisionscount': 1, 'trackedsize': len(fulltext)})
469
471
470 self.assertEqual(f.size(0), len(fulltext))
472 self.assertEqual(f.size(0), len(fulltext))
471
473
472 with self.assertRaises(IndexError):
474 with self.assertRaises(IndexError):
473 f.size(1)
475 f.size(1)
474
476
475 self.assertEqual(f.revision(node), fulltext)
477 self.assertEqual(f.revision(node), fulltext)
476 self.assertEqual(f.rawdata(node), fulltext)
478 self.assertEqual(f.rawdata(node), fulltext)
477
479
478 self.assertEqual(f.read(node), fulltext)
480 self.assertEqual(f.read(node), fulltext)
479
481
480 self.assertFalse(f.renamed(node))
482 self.assertFalse(f.renamed(node))
481
483
482 self.assertFalse(f.cmp(node, fulltext))
484 self.assertFalse(f.cmp(node, fulltext))
483 self.assertTrue(f.cmp(node, fulltext + b'extra'))
485 self.assertTrue(f.cmp(node, fulltext + b'extra'))
484
486
485 # Emitting a single revision works.
487 # Emitting a single revision works.
486 gen = f.emitrevisions([node])
488 gen = f.emitrevisions([node])
487 rev = next(gen)
489 rev = next(gen)
488
490
489 self.assertEqual(rev.node, node)
491 self.assertEqual(rev.node, node)
490 self.assertEqual(rev.p1node, nullid)
492 self.assertEqual(rev.p1node, nullid)
491 self.assertEqual(rev.p2node, nullid)
493 self.assertEqual(rev.p2node, nullid)
492 self.assertIsNone(rev.linknode)
494 self.assertIsNone(rev.linknode)
493 self.assertEqual(rev.basenode, nullid)
495 self.assertEqual(rev.basenode, nullid)
494 self.assertIsNone(rev.baserevisionsize)
496 self.assertIsNone(rev.baserevisionsize)
495 self.assertIsNone(rev.revision)
497 self.assertIsNone(rev.revision)
496 self.assertIsNone(rev.delta)
498 self.assertIsNone(rev.delta)
497
499
498 with self.assertRaises(StopIteration):
500 with self.assertRaises(StopIteration):
499 next(gen)
501 next(gen)
500
502
501 # Requesting revision data works.
503 # Requesting revision data works.
502 gen = f.emitrevisions([node], revisiondata=True)
504 gen = f.emitrevisions([node], revisiondata=True)
503 rev = next(gen)
505 rev = next(gen)
504
506
505 self.assertEqual(rev.node, node)
507 self.assertEqual(rev.node, node)
506 self.assertEqual(rev.p1node, nullid)
508 self.assertEqual(rev.p1node, nullid)
507 self.assertEqual(rev.p2node, nullid)
509 self.assertEqual(rev.p2node, nullid)
508 self.assertIsNone(rev.linknode)
510 self.assertIsNone(rev.linknode)
509 self.assertEqual(rev.basenode, nullid)
511 self.assertEqual(rev.basenode, nullid)
510 self.assertIsNone(rev.baserevisionsize)
512 self.assertIsNone(rev.baserevisionsize)
511 self.assertEqual(rev.revision, fulltext)
513 self.assertEqual(rev.revision, fulltext)
512 self.assertIsNone(rev.delta)
514 self.assertIsNone(rev.delta)
513
515
514 with self.assertRaises(StopIteration):
516 with self.assertRaises(StopIteration):
515 next(gen)
517 next(gen)
516
518
517 # Emitting an unknown node after a known revision results in error.
519 # Emitting an unknown node after a known revision results in error.
518 with self.assertRaises(error.LookupError):
520 with self.assertRaises(error.LookupError):
519 list(f.emitrevisions([node, b'\x01' * 20]))
521 list(f.emitrevisions([node, b'\x01' * 20]))
520
522
521 def testmultiplerevisions(self):
523 def testmultiplerevisions(self):
522 fulltext0 = b'x' * 1024
524 fulltext0 = b'x' * 1024
523 fulltext1 = fulltext0 + b'y'
525 fulltext1 = fulltext0 + b'y'
524 fulltext2 = b'y' + fulltext0 + b'z'
526 fulltext2 = b'y' + fulltext0 + b'z'
525
527
526 f = self._makefilefn()
528 f = self._makefilefn()
527 with self._maketransactionfn() as tr:
529 with self._maketransactionfn() as tr:
528 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
530 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
529 node1 = f.add(fulltext1, None, tr, 1, node0, nullid)
531 node1 = f.add(fulltext1, None, tr, 1, node0, nullid)
530 node2 = f.add(fulltext2, None, tr, 3, node1, nullid)
532 node2 = f.add(fulltext2, None, tr, 3, node1, nullid)
531
533
532 self.assertEqual(f.storageinfo(), {})
534 self.assertEqual(f.storageinfo(), {})
533 self.assertEqual(
535 self.assertEqual(
534 f.storageinfo(revisionscount=True, trackedsize=True),
536 f.storageinfo(revisionscount=True, trackedsize=True),
535 {
537 {
536 'revisionscount': 3,
538 'revisionscount': 3,
537 'trackedsize': len(fulltext0) + len(fulltext1) + len(fulltext2),
539 'trackedsize': len(fulltext0) + len(fulltext1) + len(fulltext2),
538 })
540 })
539
541
540 self.assertEqual(f.size(0), len(fulltext0))
542 self.assertEqual(f.size(0), len(fulltext0))
541 self.assertEqual(f.size(1), len(fulltext1))
543 self.assertEqual(f.size(1), len(fulltext1))
542 self.assertEqual(f.size(2), len(fulltext2))
544 self.assertEqual(f.size(2), len(fulltext2))
543
545
544 with self.assertRaises(IndexError):
546 with self.assertRaises(IndexError):
545 f.size(3)
547 f.size(3)
546
548
547 self.assertEqual(f.revision(node0), fulltext0)
549 self.assertEqual(f.revision(node0), fulltext0)
548 self.assertEqual(f.rawdata(node0), fulltext0)
550 self.assertEqual(f.rawdata(node0), fulltext0)
549 self.assertEqual(f.revision(node1), fulltext1)
551 self.assertEqual(f.revision(node1), fulltext1)
550 self.assertEqual(f.rawdata(node1), fulltext1)
552 self.assertEqual(f.rawdata(node1), fulltext1)
551 self.assertEqual(f.revision(node2), fulltext2)
553 self.assertEqual(f.revision(node2), fulltext2)
552 self.assertEqual(f.rawdata(node2), fulltext2)
554 self.assertEqual(f.rawdata(node2), fulltext2)
553
555
554 with self.assertRaises(error.LookupError):
556 with self.assertRaises(error.LookupError):
555 f.revision(b'\x01' * 20)
557 f.revision(b'\x01' * 20)
556
558
557 self.assertEqual(f.read(node0), fulltext0)
559 self.assertEqual(f.read(node0), fulltext0)
558 self.assertEqual(f.read(node1), fulltext1)
560 self.assertEqual(f.read(node1), fulltext1)
559 self.assertEqual(f.read(node2), fulltext2)
561 self.assertEqual(f.read(node2), fulltext2)
560
562
561 with self.assertRaises(error.LookupError):
563 with self.assertRaises(error.LookupError):
562 f.read(b'\x01' * 20)
564 f.read(b'\x01' * 20)
563
565
564 self.assertFalse(f.renamed(node0))
566 self.assertFalse(f.renamed(node0))
565 self.assertFalse(f.renamed(node1))
567 self.assertFalse(f.renamed(node1))
566 self.assertFalse(f.renamed(node2))
568 self.assertFalse(f.renamed(node2))
567
569
568 with self.assertRaises(error.LookupError):
570 with self.assertRaises(error.LookupError):
569 f.renamed(b'\x01' * 20)
571 f.renamed(b'\x01' * 20)
570
572
571 self.assertFalse(f.cmp(node0, fulltext0))
573 self.assertFalse(f.cmp(node0, fulltext0))
572 self.assertFalse(f.cmp(node1, fulltext1))
574 self.assertFalse(f.cmp(node1, fulltext1))
573 self.assertFalse(f.cmp(node2, fulltext2))
575 self.assertFalse(f.cmp(node2, fulltext2))
574
576
575 self.assertTrue(f.cmp(node1, fulltext0))
577 self.assertTrue(f.cmp(node1, fulltext0))
576 self.assertTrue(f.cmp(node2, fulltext1))
578 self.assertTrue(f.cmp(node2, fulltext1))
577
579
578 with self.assertRaises(error.LookupError):
580 with self.assertRaises(error.LookupError):
579 f.cmp(b'\x01' * 20, b'irrelevant')
581 f.cmp(b'\x01' * 20, b'irrelevant')
580
582
581 # Nodes should be emitted in order.
583 # Nodes should be emitted in order.
582 gen = f.emitrevisions([node0, node1, node2], revisiondata=True)
584 gen = f.emitrevisions([node0, node1, node2], revisiondata=True)
583
585
584 rev = next(gen)
586 rev = next(gen)
585
587
586 self.assertEqual(rev.node, node0)
588 self.assertEqual(rev.node, node0)
587 self.assertEqual(rev.p1node, nullid)
589 self.assertEqual(rev.p1node, nullid)
588 self.assertEqual(rev.p2node, nullid)
590 self.assertEqual(rev.p2node, nullid)
589 self.assertIsNone(rev.linknode)
591 self.assertIsNone(rev.linknode)
590 self.assertEqual(rev.basenode, nullid)
592 self.assertEqual(rev.basenode, nullid)
591 self.assertIsNone(rev.baserevisionsize)
593 self.assertIsNone(rev.baserevisionsize)
592 self.assertEqual(rev.revision, fulltext0)
594 self.assertEqual(rev.revision, fulltext0)
593 self.assertIsNone(rev.delta)
595 self.assertIsNone(rev.delta)
594
596
595 rev = next(gen)
597 rev = next(gen)
596
598
597 self.assertEqual(rev.node, node1)
599 self.assertEqual(rev.node, node1)
598 self.assertEqual(rev.p1node, node0)
600 self.assertEqual(rev.p1node, node0)
599 self.assertEqual(rev.p2node, nullid)
601 self.assertEqual(rev.p2node, nullid)
600 self.assertIsNone(rev.linknode)
602 self.assertIsNone(rev.linknode)
601 self.assertEqual(rev.basenode, node0)
603 self.assertEqual(rev.basenode, node0)
602 self.assertIsNone(rev.baserevisionsize)
604 self.assertIsNone(rev.baserevisionsize)
603 self.assertIsNone(rev.revision)
605 self.assertIsNone(rev.revision)
604 self.assertEqual(rev.delta,
606 self.assertEqual(rev.delta,
605 b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x01' +
607 b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x01' +
606 fulltext1)
608 fulltext1)
607
609
608 rev = next(gen)
610 rev = next(gen)
609
611
610 self.assertEqual(rev.node, node2)
612 self.assertEqual(rev.node, node2)
611 self.assertEqual(rev.p1node, node1)
613 self.assertEqual(rev.p1node, node1)
612 self.assertEqual(rev.p2node, nullid)
614 self.assertEqual(rev.p2node, nullid)
613 self.assertIsNone(rev.linknode)
615 self.assertIsNone(rev.linknode)
614 self.assertEqual(rev.basenode, node1)
616 self.assertEqual(rev.basenode, node1)
615 self.assertIsNone(rev.baserevisionsize)
617 self.assertIsNone(rev.baserevisionsize)
616 self.assertIsNone(rev.revision)
618 self.assertIsNone(rev.revision)
617 self.assertEqual(rev.delta,
619 self.assertEqual(rev.delta,
618 b'\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x04\x02' +
620 b'\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x04\x02' +
619 fulltext2)
621 fulltext2)
620
622
621 with self.assertRaises(StopIteration):
623 with self.assertRaises(StopIteration):
622 next(gen)
624 next(gen)
623
625
624 # Request not in DAG order is reordered to be in DAG order.
626 # Request not in DAG order is reordered to be in DAG order.
625 gen = f.emitrevisions([node2, node1, node0], revisiondata=True)
627 gen = f.emitrevisions([node2, node1, node0], revisiondata=True)
626
628
627 rev = next(gen)
629 rev = next(gen)
628
630
629 self.assertEqual(rev.node, node0)
631 self.assertEqual(rev.node, node0)
630 self.assertEqual(rev.p1node, nullid)
632 self.assertEqual(rev.p1node, nullid)
631 self.assertEqual(rev.p2node, nullid)
633 self.assertEqual(rev.p2node, nullid)
632 self.assertIsNone(rev.linknode)
634 self.assertIsNone(rev.linknode)
633 self.assertEqual(rev.basenode, nullid)
635 self.assertEqual(rev.basenode, nullid)
634 self.assertIsNone(rev.baserevisionsize)
636 self.assertIsNone(rev.baserevisionsize)
635 self.assertEqual(rev.revision, fulltext0)
637 self.assertEqual(rev.revision, fulltext0)
636 self.assertIsNone(rev.delta)
638 self.assertIsNone(rev.delta)
637
639
638 rev = next(gen)
640 rev = next(gen)
639
641
640 self.assertEqual(rev.node, node1)
642 self.assertEqual(rev.node, node1)
641 self.assertEqual(rev.p1node, node0)
643 self.assertEqual(rev.p1node, node0)
642 self.assertEqual(rev.p2node, nullid)
644 self.assertEqual(rev.p2node, nullid)
643 self.assertIsNone(rev.linknode)
645 self.assertIsNone(rev.linknode)
644 self.assertEqual(rev.basenode, node0)
646 self.assertEqual(rev.basenode, node0)
645 self.assertIsNone(rev.baserevisionsize)
647 self.assertIsNone(rev.baserevisionsize)
646 self.assertIsNone(rev.revision)
648 self.assertIsNone(rev.revision)
647 self.assertEqual(rev.delta,
649 self.assertEqual(rev.delta,
648 b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x01' +
650 b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x01' +
649 fulltext1)
651 fulltext1)
650
652
651 rev = next(gen)
653 rev = next(gen)
652
654
653 self.assertEqual(rev.node, node2)
655 self.assertEqual(rev.node, node2)
654 self.assertEqual(rev.p1node, node1)
656 self.assertEqual(rev.p1node, node1)
655 self.assertEqual(rev.p2node, nullid)
657 self.assertEqual(rev.p2node, nullid)
656 self.assertIsNone(rev.linknode)
658 self.assertIsNone(rev.linknode)
657 self.assertEqual(rev.basenode, node1)
659 self.assertEqual(rev.basenode, node1)
658 self.assertIsNone(rev.baserevisionsize)
660 self.assertIsNone(rev.baserevisionsize)
659 self.assertIsNone(rev.revision)
661 self.assertIsNone(rev.revision)
660 self.assertEqual(rev.delta,
662 self.assertEqual(rev.delta,
661 b'\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x04\x02' +
663 b'\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x04\x02' +
662 fulltext2)
664 fulltext2)
663
665
664 with self.assertRaises(StopIteration):
666 with self.assertRaises(StopIteration):
665 next(gen)
667 next(gen)
666
668
667 # Unrecognized nodesorder value raises ProgrammingError.
669 # Unrecognized nodesorder value raises ProgrammingError.
668 with self.assertRaises(error.ProgrammingError):
670 with self.assertRaises(error.ProgrammingError):
669 list(f.emitrevisions([], nodesorder='bad'))
671 list(f.emitrevisions([], nodesorder='bad'))
670
672
671 # nodesorder=storage is recognized. But we can't test it thoroughly
673 # nodesorder=storage is recognized. But we can't test it thoroughly
672 # because behavior is storage-dependent.
674 # because behavior is storage-dependent.
673 res = list(f.emitrevisions([node2, node1, node0],
675 res = list(f.emitrevisions([node2, node1, node0],
674 nodesorder='storage'))
676 nodesorder='storage'))
675 self.assertEqual(len(res), 3)
677 self.assertEqual(len(res), 3)
676 self.assertEqual({o.node for o in res}, {node0, node1, node2})
678 self.assertEqual({o.node for o in res}, {node0, node1, node2})
677
679
678 # nodesorder=nodes forces the order.
680 # nodesorder=nodes forces the order.
679 gen = f.emitrevisions([node2, node0], nodesorder='nodes',
681 gen = f.emitrevisions([node2, node0], nodesorder='nodes',
680 revisiondata=True)
682 revisiondata=True)
681
683
682 rev = next(gen)
684 rev = next(gen)
683 self.assertEqual(rev.node, node2)
685 self.assertEqual(rev.node, node2)
684 self.assertEqual(rev.p1node, node1)
686 self.assertEqual(rev.p1node, node1)
685 self.assertEqual(rev.p2node, nullid)
687 self.assertEqual(rev.p2node, nullid)
686 self.assertEqual(rev.basenode, nullid)
688 self.assertEqual(rev.basenode, nullid)
687 self.assertIsNone(rev.baserevisionsize)
689 self.assertIsNone(rev.baserevisionsize)
688 self.assertEqual(rev.revision, fulltext2)
690 self.assertEqual(rev.revision, fulltext2)
689 self.assertIsNone(rev.delta)
691 self.assertIsNone(rev.delta)
690
692
691 rev = next(gen)
693 rev = next(gen)
692 self.assertEqual(rev.node, node0)
694 self.assertEqual(rev.node, node0)
693 self.assertEqual(rev.p1node, nullid)
695 self.assertEqual(rev.p1node, nullid)
694 self.assertEqual(rev.p2node, nullid)
696 self.assertEqual(rev.p2node, nullid)
695 # Delta behavior is storage dependent, so we can't easily test it.
697 # Delta behavior is storage dependent, so we can't easily test it.
696
698
697 with self.assertRaises(StopIteration):
699 with self.assertRaises(StopIteration):
698 next(gen)
700 next(gen)
699
701
700 # assumehaveparentrevisions=False (the default) won't send a delta for
702 # assumehaveparentrevisions=False (the default) won't send a delta for
701 # the first revision.
703 # the first revision.
702 gen = f.emitrevisions({node2, node1}, revisiondata=True)
704 gen = f.emitrevisions({node2, node1}, revisiondata=True)
703
705
704 rev = next(gen)
706 rev = next(gen)
705 self.assertEqual(rev.node, node1)
707 self.assertEqual(rev.node, node1)
706 self.assertEqual(rev.p1node, node0)
708 self.assertEqual(rev.p1node, node0)
707 self.assertEqual(rev.p2node, nullid)
709 self.assertEqual(rev.p2node, nullid)
708 self.assertEqual(rev.basenode, nullid)
710 self.assertEqual(rev.basenode, nullid)
709 self.assertIsNone(rev.baserevisionsize)
711 self.assertIsNone(rev.baserevisionsize)
710 self.assertEqual(rev.revision, fulltext1)
712 self.assertEqual(rev.revision, fulltext1)
711 self.assertIsNone(rev.delta)
713 self.assertIsNone(rev.delta)
712
714
713 rev = next(gen)
715 rev = next(gen)
714 self.assertEqual(rev.node, node2)
716 self.assertEqual(rev.node, node2)
715 self.assertEqual(rev.p1node, node1)
717 self.assertEqual(rev.p1node, node1)
716 self.assertEqual(rev.p2node, nullid)
718 self.assertEqual(rev.p2node, nullid)
717 self.assertEqual(rev.basenode, node1)
719 self.assertEqual(rev.basenode, node1)
718 self.assertIsNone(rev.baserevisionsize)
720 self.assertIsNone(rev.baserevisionsize)
719 self.assertIsNone(rev.revision)
721 self.assertIsNone(rev.revision)
720 self.assertEqual(rev.delta,
722 self.assertEqual(rev.delta,
721 b'\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x04\x02' +
723 b'\x00\x00\x00\x00\x00\x00\x04\x01\x00\x00\x04\x02' +
722 fulltext2)
724 fulltext2)
723
725
724 with self.assertRaises(StopIteration):
726 with self.assertRaises(StopIteration):
725 next(gen)
727 next(gen)
726
728
727 # assumehaveparentrevisions=True allows delta against initial revision.
729 # assumehaveparentrevisions=True allows delta against initial revision.
728 gen = f.emitrevisions([node2, node1],
730 gen = f.emitrevisions([node2, node1],
729 revisiondata=True, assumehaveparentrevisions=True)
731 revisiondata=True, assumehaveparentrevisions=True)
730
732
731 rev = next(gen)
733 rev = next(gen)
732 self.assertEqual(rev.node, node1)
734 self.assertEqual(rev.node, node1)
733 self.assertEqual(rev.p1node, node0)
735 self.assertEqual(rev.p1node, node0)
734 self.assertEqual(rev.p2node, nullid)
736 self.assertEqual(rev.p2node, nullid)
735 self.assertEqual(rev.basenode, node0)
737 self.assertEqual(rev.basenode, node0)
736 self.assertIsNone(rev.baserevisionsize)
738 self.assertIsNone(rev.baserevisionsize)
737 self.assertIsNone(rev.revision)
739 self.assertIsNone(rev.revision)
738 self.assertEqual(rev.delta,
740 self.assertEqual(rev.delta,
739 b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x01' +
741 b'\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00\x04\x01' +
740 fulltext1)
742 fulltext1)
741
743
742 # forceprevious=True forces a delta against the previous revision.
744 # forceprevious=True forces a delta against the previous revision.
743 # Special case for initial revision.
745 # Special case for initial revision.
744 gen = f.emitrevisions([node0], revisiondata=True,
746 gen = f.emitrevisions([node0], revisiondata=True,
745 deltamode=repository.CG_DELTAMODE_PREV)
747 deltamode=repository.CG_DELTAMODE_PREV)
746
748
747 rev = next(gen)
749 rev = next(gen)
748 self.assertEqual(rev.node, node0)
750 self.assertEqual(rev.node, node0)
749 self.assertEqual(rev.p1node, nullid)
751 self.assertEqual(rev.p1node, nullid)
750 self.assertEqual(rev.p2node, nullid)
752 self.assertEqual(rev.p2node, nullid)
751 self.assertEqual(rev.basenode, nullid)
753 self.assertEqual(rev.basenode, nullid)
752 self.assertIsNone(rev.baserevisionsize)
754 self.assertIsNone(rev.baserevisionsize)
753 self.assertIsNone(rev.revision)
755 self.assertIsNone(rev.revision)
754 self.assertEqual(rev.delta,
756 self.assertEqual(rev.delta,
755 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00' +
757 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00' +
756 fulltext0)
758 fulltext0)
757
759
758 with self.assertRaises(StopIteration):
760 with self.assertRaises(StopIteration):
759 next(gen)
761 next(gen)
760
762
761 gen = f.emitrevisions([node0, node2], revisiondata=True,
763 gen = f.emitrevisions([node0, node2], revisiondata=True,
762 deltamode=repository.CG_DELTAMODE_PREV)
764 deltamode=repository.CG_DELTAMODE_PREV)
763
765
764 rev = next(gen)
766 rev = next(gen)
765 self.assertEqual(rev.node, node0)
767 self.assertEqual(rev.node, node0)
766 self.assertEqual(rev.p1node, nullid)
768 self.assertEqual(rev.p1node, nullid)
767 self.assertEqual(rev.p2node, nullid)
769 self.assertEqual(rev.p2node, nullid)
768 self.assertEqual(rev.basenode, nullid)
770 self.assertEqual(rev.basenode, nullid)
769 self.assertIsNone(rev.baserevisionsize)
771 self.assertIsNone(rev.baserevisionsize)
770 self.assertIsNone(rev.revision)
772 self.assertIsNone(rev.revision)
771 self.assertEqual(rev.delta,
773 self.assertEqual(rev.delta,
772 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00' +
774 b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00' +
773 fulltext0)
775 fulltext0)
774
776
775 rev = next(gen)
777 rev = next(gen)
776 self.assertEqual(rev.node, node2)
778 self.assertEqual(rev.node, node2)
777 self.assertEqual(rev.p1node, node1)
779 self.assertEqual(rev.p1node, node1)
778 self.assertEqual(rev.p2node, nullid)
780 self.assertEqual(rev.p2node, nullid)
779 self.assertEqual(rev.basenode, node0)
781 self.assertEqual(rev.basenode, node0)
780
782
781 with self.assertRaises(StopIteration):
783 with self.assertRaises(StopIteration):
782 next(gen)
784 next(gen)
783
785
784 def testrenamed(self):
786 def testrenamed(self):
785 fulltext0 = b'foo'
787 fulltext0 = b'foo'
786 fulltext1 = b'bar'
788 fulltext1 = b'bar'
787 fulltext2 = b'baz'
789 fulltext2 = b'baz'
788
790
789 meta1 = {
791 meta1 = {
790 b'copy': b'source0',
792 b'copy': b'source0',
791 b'copyrev': b'a' * 40,
793 b'copyrev': b'a' * 40,
792 }
794 }
793
795
794 meta2 = {
796 meta2 = {
795 b'copy': b'source1',
797 b'copy': b'source1',
796 b'copyrev': b'b' * 40,
798 b'copyrev': b'b' * 40,
797 }
799 }
798
800
799 stored1 = b''.join([
801 stored1 = b''.join([
800 b'\x01\ncopy: source0\n',
802 b'\x01\ncopy: source0\n',
801 b'copyrev: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\x01\n',
803 b'copyrev: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\x01\n',
802 fulltext1,
804 fulltext1,
803 ])
805 ])
804
806
805 stored2 = b''.join([
807 stored2 = b''.join([
806 b'\x01\ncopy: source1\n',
808 b'\x01\ncopy: source1\n',
807 b'copyrev: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\x01\n',
809 b'copyrev: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n\x01\n',
808 fulltext2,
810 fulltext2,
809 ])
811 ])
810
812
811 f = self._makefilefn()
813 f = self._makefilefn()
812 with self._maketransactionfn() as tr:
814 with self._maketransactionfn() as tr:
813 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
815 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
814 node1 = f.add(fulltext1, meta1, tr, 1, node0, nullid)
816 node1 = f.add(fulltext1, meta1, tr, 1, node0, nullid)
815 node2 = f.add(fulltext2, meta2, tr, 2, nullid, nullid)
817 node2 = f.add(fulltext2, meta2, tr, 2, nullid, nullid)
816
818
817 # Metadata header isn't recognized when parent isn't nullid.
819 # Metadata header isn't recognized when parent isn't nullid.
818 self.assertEqual(f.size(1), len(stored1))
820 self.assertEqual(f.size(1), len(stored1))
819 self.assertEqual(f.size(2), len(fulltext2))
821 self.assertEqual(f.size(2), len(fulltext2))
820
822
821 self.assertEqual(f.revision(node1), stored1)
823 self.assertEqual(f.revision(node1), stored1)
822 self.assertEqual(f.rawdata(node1), stored1)
824 self.assertEqual(f.rawdata(node1), stored1)
823 self.assertEqual(f.revision(node2), stored2)
825 self.assertEqual(f.revision(node2), stored2)
824 self.assertEqual(f.rawdata(node2), stored2)
826 self.assertEqual(f.rawdata(node2), stored2)
825
827
826 self.assertEqual(f.read(node1), fulltext1)
828 self.assertEqual(f.read(node1), fulltext1)
827 self.assertEqual(f.read(node2), fulltext2)
829 self.assertEqual(f.read(node2), fulltext2)
828
830
829 # Returns False when first parent is set.
831 # Returns False when first parent is set.
830 self.assertFalse(f.renamed(node1))
832 self.assertFalse(f.renamed(node1))
831 self.assertEqual(f.renamed(node2), (b'source1', b'\xbb' * 20))
833 self.assertEqual(f.renamed(node2), (b'source1', b'\xbb' * 20))
832
834
833 self.assertTrue(f.cmp(node1, fulltext1))
835 self.assertTrue(f.cmp(node1, fulltext1))
834 self.assertTrue(f.cmp(node1, stored1))
836 self.assertTrue(f.cmp(node1, stored1))
835 self.assertFalse(f.cmp(node2, fulltext2))
837 self.assertFalse(f.cmp(node2, fulltext2))
836 self.assertTrue(f.cmp(node2, stored2))
838 self.assertTrue(f.cmp(node2, stored2))
837
839
838 def testmetadataprefix(self):
840 def testmetadataprefix(self):
839 # Content with metadata prefix has extra prefix inserted in storage.
841 # Content with metadata prefix has extra prefix inserted in storage.
840 fulltext0 = b'\x01\nfoo'
842 fulltext0 = b'\x01\nfoo'
841 stored0 = b'\x01\n\x01\n\x01\nfoo'
843 stored0 = b'\x01\n\x01\n\x01\nfoo'
842
844
843 fulltext1 = b'\x01\nbar'
845 fulltext1 = b'\x01\nbar'
844 meta1 = {
846 meta1 = {
845 b'copy': b'source0',
847 b'copy': b'source0',
846 b'copyrev': b'b' * 40,
848 b'copyrev': b'b' * 40,
847 }
849 }
848 stored1 = b''.join([
850 stored1 = b''.join([
849 b'\x01\ncopy: source0\n',
851 b'\x01\ncopy: source0\n',
850 b'copyrev: %s\n' % (b'b' * 40),
852 b'copyrev: %s\n' % (b'b' * 40),
851 b'\x01\n\x01\nbar',
853 b'\x01\n\x01\nbar',
852 ])
854 ])
853
855
854 f = self._makefilefn()
856 f = self._makefilefn()
855 with self._maketransactionfn() as tr:
857 with self._maketransactionfn() as tr:
856 node0 = f.add(fulltext0, {}, tr, 0, nullid, nullid)
858 node0 = f.add(fulltext0, {}, tr, 0, nullid, nullid)
857 node1 = f.add(fulltext1, meta1, tr, 1, nullid, nullid)
859 node1 = f.add(fulltext1, meta1, tr, 1, nullid, nullid)
858
860
859 # TODO this is buggy.
861 # TODO this is buggy.
860 self.assertEqual(f.size(0), len(fulltext0) + 4)
862 self.assertEqual(f.size(0), len(fulltext0) + 4)
861
863
862 self.assertEqual(f.size(1), len(fulltext1))
864 self.assertEqual(f.size(1), len(fulltext1))
863
865
864 self.assertEqual(f.revision(node0), stored0)
866 self.assertEqual(f.revision(node0), stored0)
865 self.assertEqual(f.rawdata(node0), stored0)
867 self.assertEqual(f.rawdata(node0), stored0)
866
868
867 self.assertEqual(f.revision(node1), stored1)
869 self.assertEqual(f.revision(node1), stored1)
868 self.assertEqual(f.rawdata(node1), stored1)
870 self.assertEqual(f.rawdata(node1), stored1)
869
871
870 self.assertEqual(f.read(node0), fulltext0)
872 self.assertEqual(f.read(node0), fulltext0)
871 self.assertEqual(f.read(node1), fulltext1)
873 self.assertEqual(f.read(node1), fulltext1)
872
874
873 self.assertFalse(f.cmp(node0, fulltext0))
875 self.assertFalse(f.cmp(node0, fulltext0))
874 self.assertTrue(f.cmp(node0, stored0))
876 self.assertTrue(f.cmp(node0, stored0))
875
877
876 self.assertFalse(f.cmp(node1, fulltext1))
878 self.assertFalse(f.cmp(node1, fulltext1))
877 self.assertTrue(f.cmp(node1, stored0))
879 self.assertTrue(f.cmp(node1, stored0))
878
880
879 def testbadnoderead(self):
881 def testbadnoderead(self):
880 f = self._makefilefn()
882 f = self._makefilefn()
881
883
882 fulltext0 = b'foo\n' * 30
884 fulltext0 = b'foo\n' * 30
883 fulltext1 = fulltext0 + b'bar\n'
885 fulltext1 = fulltext0 + b'bar\n'
884
886
885 with self._maketransactionfn() as tr:
887 with self._maketransactionfn() as tr:
886 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
888 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
887 node1 = b'\xaa' * 20
889 node1 = b'\xaa' * 20
888
890
889 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
891 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
890 rawtext=fulltext1)
892 rawtext=fulltext1)
891
893
892 self.assertEqual(len(f), 2)
894 self.assertEqual(len(f), 2)
893 self.assertEqual(f.parents(node1), (node0, nullid))
895 self.assertEqual(f.parents(node1), (node0, nullid))
894
896
895 # revision() raises since it performs hash verification.
897 # revision() raises since it performs hash verification.
896 with self.assertRaises(error.StorageError):
898 with self.assertRaises(error.StorageError):
897 f.revision(node1)
899 f.revision(node1)
898
900
899 # rawdata() still verifies because there are no special storage
901 # rawdata() still verifies because there are no special storage
900 # settings.
902 # settings.
901 with self.assertRaises(error.StorageError):
903 with self.assertRaises(error.StorageError):
902 f.rawdata(node1)
904 f.rawdata(node1)
903
905
904 # read() behaves like revision().
906 # read() behaves like revision().
905 with self.assertRaises(error.StorageError):
907 with self.assertRaises(error.StorageError):
906 f.read(node1)
908 f.read(node1)
907
909
908 # We can't test renamed() here because some backends may not require
910 # We can't test renamed() here because some backends may not require
909 # reading/validating the fulltext to return rename metadata.
911 # reading/validating the fulltext to return rename metadata.
910
912
911 def testbadnoderevisionraw(self):
913 def testbadnoderevisionraw(self):
912 # Like above except we test rawdata() first to isolate
914 # Like above except we test rawdata() first to isolate
913 # revision caching behavior.
915 # revision caching behavior.
914 f = self._makefilefn()
916 f = self._makefilefn()
915
917
916 fulltext0 = b'foo\n' * 30
918 fulltext0 = b'foo\n' * 30
917 fulltext1 = fulltext0 + b'bar\n'
919 fulltext1 = fulltext0 + b'bar\n'
918
920
919 with self._maketransactionfn() as tr:
921 with self._maketransactionfn() as tr:
920 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
922 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
921 node1 = b'\xaa' * 20
923 node1 = b'\xaa' * 20
922
924
923 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
925 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
924 rawtext=fulltext1)
926 rawtext=fulltext1)
925
927
926 with self.assertRaises(error.StorageError):
928 with self.assertRaises(error.StorageError):
927 f.rawdata(node1)
929 f.rawdata(node1)
928
930
929 with self.assertRaises(error.StorageError):
931 with self.assertRaises(error.StorageError):
930 f.rawdata(node1)
932 f.rawdata(node1)
931
933
932 def testbadnoderevisionraw(self):
934 def testbadnoderevisionraw(self):
933 # Like above except we test read() first to isolate revision caching
935 # Like above except we test read() first to isolate revision caching
934 # behavior.
936 # behavior.
935 f = self._makefilefn()
937 f = self._makefilefn()
936
938
937 fulltext0 = b'foo\n' * 30
939 fulltext0 = b'foo\n' * 30
938 fulltext1 = fulltext0 + b'bar\n'
940 fulltext1 = fulltext0 + b'bar\n'
939
941
940 with self._maketransactionfn() as tr:
942 with self._maketransactionfn() as tr:
941 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
943 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
942 node1 = b'\xaa' * 20
944 node1 = b'\xaa' * 20
943
945
944 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
946 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
945 rawtext=fulltext1)
947 rawtext=fulltext1)
946
948
947 with self.assertRaises(error.StorageError):
949 with self.assertRaises(error.StorageError):
948 f.read(node1)
950 f.read(node1)
949
951
950 with self.assertRaises(error.StorageError):
952 with self.assertRaises(error.StorageError):
951 f.read(node1)
953 f.read(node1)
952
954
953 def testbadnodedelta(self):
955 def testbadnodedelta(self):
954 f = self._makefilefn()
956 f = self._makefilefn()
955
957
956 fulltext0 = b'foo\n' * 31
958 fulltext0 = b'foo\n' * 31
957 fulltext1 = fulltext0 + b'bar\n'
959 fulltext1 = fulltext0 + b'bar\n'
958 fulltext2 = fulltext1 + b'baz\n'
960 fulltext2 = fulltext1 + b'baz\n'
959
961
960 with self._maketransactionfn() as tr:
962 with self._maketransactionfn() as tr:
961 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
963 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
962 node1 = b'\xaa' * 20
964 node1 = b'\xaa' * 20
963
965
964 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
966 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1,
965 rawtext=fulltext1)
967 rawtext=fulltext1)
966
968
967 with self.assertRaises(error.StorageError):
969 with self.assertRaises(error.StorageError):
968 f.read(node1)
970 f.read(node1)
969
971
970 node2 = storageutil.hashrevisionsha1(fulltext2, node1, nullid)
972 node2 = storageutil.hashrevisionsha1(fulltext2, node1, nullid)
971
973
972 with self._maketransactionfn() as tr:
974 with self._maketransactionfn() as tr:
973 delta = mdiff.textdiff(fulltext1, fulltext2)
975 delta = mdiff.textdiff(fulltext1, fulltext2)
974 self._addrawrevisionfn(f, tr, node2, node1, nullid,
976 self._addrawrevisionfn(f, tr, node2, node1, nullid,
975 2, delta=(1, delta))
977 2, delta=(1, delta))
976
978
977 self.assertEqual(len(f), 3)
979 self.assertEqual(len(f), 3)
978
980
979 # Assuming a delta is stored, we shouldn't need to validate node1 in
981 # Assuming a delta is stored, we shouldn't need to validate node1 in
980 # order to retrieve node2.
982 # order to retrieve node2.
981 self.assertEqual(f.read(node2), fulltext2)
983 self.assertEqual(f.read(node2), fulltext2)
982
984
983 def testcensored(self):
985 def testcensored(self):
984 f = self._makefilefn()
986 f = self._makefilefn()
985
987
986 stored1 = storageutil.packmeta({
988 stored1 = storageutil.packmeta({
987 b'censored': b'tombstone',
989 b'censored': b'tombstone',
988 }, b'')
990 }, b'')
989
991
990 with self._maketransactionfn() as tr:
992 with self._maketransactionfn() as tr:
991 node0 = f.add(b'foo', None, tr, 0, nullid, nullid)
993 node0 = f.add(b'foo', None, tr, 0, nullid, nullid)
992
994
993 # The node value doesn't matter since we can't verify it.
995 # The node value doesn't matter since we can't verify it.
994 node1 = b'\xbb' * 20
996 node1 = b'\xbb' * 20
995
997
996 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1, stored1,
998 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1, stored1,
997 censored=True)
999 censored=True)
998
1000
999 self.assertTrue(f.iscensored(1))
1001 self.assertTrue(f.iscensored(1))
1000
1002
1001 with self.assertRaises(error.CensoredNodeError):
1003 with self.assertRaises(error.CensoredNodeError):
1002 f.revision(1)
1004 f.revision(1)
1003
1005
1004 with self.assertRaises(error.CensoredNodeError):
1006 with self.assertRaises(error.CensoredNodeError):
1005 f.rawdata(1)
1007 f.rawdata(1)
1006
1008
1007 with self.assertRaises(error.CensoredNodeError):
1009 with self.assertRaises(error.CensoredNodeError):
1008 f.read(1)
1010 f.read(1)
1009
1011
1010 def testcensoredrawrevision(self):
1012 def testcensoredrawrevision(self):
1011 # Like above, except we do the rawdata() request first to
1013 # Like above, except we do the rawdata() request first to
1012 # isolate revision caching behavior.
1014 # isolate revision caching behavior.
1013
1015
1014 f = self._makefilefn()
1016 f = self._makefilefn()
1015
1017
1016 stored1 = storageutil.packmeta({
1018 stored1 = storageutil.packmeta({
1017 b'censored': b'tombstone',
1019 b'censored': b'tombstone',
1018 }, b'')
1020 }, b'')
1019
1021
1020 with self._maketransactionfn() as tr:
1022 with self._maketransactionfn() as tr:
1021 node0 = f.add(b'foo', None, tr, 0, nullid, nullid)
1023 node0 = f.add(b'foo', None, tr, 0, nullid, nullid)
1022
1024
1023 # The node value doesn't matter since we can't verify it.
1025 # The node value doesn't matter since we can't verify it.
1024 node1 = b'\xbb' * 20
1026 node1 = b'\xbb' * 20
1025
1027
1026 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1, stored1,
1028 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1, stored1,
1027 censored=True)
1029 censored=True)
1028
1030
1029 with self.assertRaises(error.CensoredNodeError):
1031 with self.assertRaises(error.CensoredNodeError):
1030 f.rawdata(1)
1032 f.rawdata(1)
1031
1033
1032 class ifilemutationtests(basetestcase):
1034 class ifilemutationtests(basetestcase):
1033 """Generic tests for the ifilemutation interface.
1035 """Generic tests for the ifilemutation interface.
1034
1036
1035 All file storage backends that support writing should conform to this
1037 All file storage backends that support writing should conform to this
1036 interface.
1038 interface.
1037
1039
1038 Use ``makeifilemutationtests()`` to create an instance of this type.
1040 Use ``makeifilemutationtests()`` to create an instance of this type.
1039 """
1041 """
1040 def testaddnoop(self):
1042 def testaddnoop(self):
1041 f = self._makefilefn()
1043 f = self._makefilefn()
1042 with self._maketransactionfn() as tr:
1044 with self._maketransactionfn() as tr:
1043 node0 = f.add(b'foo', None, tr, 0, nullid, nullid)
1045 node0 = f.add(b'foo', None, tr, 0, nullid, nullid)
1044 node1 = f.add(b'foo', None, tr, 0, nullid, nullid)
1046 node1 = f.add(b'foo', None, tr, 0, nullid, nullid)
1045 # Varying by linkrev shouldn't impact hash.
1047 # Varying by linkrev shouldn't impact hash.
1046 node2 = f.add(b'foo', None, tr, 1, nullid, nullid)
1048 node2 = f.add(b'foo', None, tr, 1, nullid, nullid)
1047
1049
1048 self.assertEqual(node1, node0)
1050 self.assertEqual(node1, node0)
1049 self.assertEqual(node2, node0)
1051 self.assertEqual(node2, node0)
1050 self.assertEqual(len(f), 1)
1052 self.assertEqual(len(f), 1)
1051
1053
1052 def testaddrevisionbadnode(self):
1054 def testaddrevisionbadnode(self):
1053 f = self._makefilefn()
1055 f = self._makefilefn()
1054 with self._maketransactionfn() as tr:
1056 with self._maketransactionfn() as tr:
1055 # Adding a revision with bad node value fails.
1057 # Adding a revision with bad node value fails.
1056 with self.assertRaises(error.StorageError):
1058 with self.assertRaises(error.StorageError):
1057 f.addrevision(b'foo', tr, 0, nullid, nullid, node=b'\x01' * 20)
1059 f.addrevision(b'foo', tr, 0, nullid, nullid, node=b'\x01' * 20)
1058
1060
1059 def testaddrevisionunknownflag(self):
1061 def testaddrevisionunknownflag(self):
1060 f = self._makefilefn()
1062 f = self._makefilefn()
1061 with self._maketransactionfn() as tr:
1063 with self._maketransactionfn() as tr:
1062 for i in range(15, 0, -1):
1064 for i in range(15, 0, -1):
1063 if (1 << i) & ~repository.REVISION_FLAGS_KNOWN:
1065 if (1 << i) & ~repository.REVISION_FLAGS_KNOWN:
1064 flags = 1 << i
1066 flags = 1 << i
1065 break
1067 break
1066
1068
1067 with self.assertRaises(error.StorageError):
1069 with self.assertRaises(error.StorageError):
1068 f.addrevision(b'foo', tr, 0, nullid, nullid, flags=flags)
1070 f.addrevision(b'foo', tr, 0, nullid, nullid, flags=flags)
1069
1071
1070 def testaddgroupsimple(self):
1072 def testaddgroupsimple(self):
1071 f = self._makefilefn()
1073 f = self._makefilefn()
1072
1074
1073 callbackargs = []
1075 callbackargs = []
1074 def cb(*args, **kwargs):
1076 def cb(*args, **kwargs):
1075 callbackargs.append((args, kwargs))
1077 callbackargs.append((args, kwargs))
1076
1078
1077 def linkmapper(node):
1079 def linkmapper(node):
1078 return 0
1080 return 0
1079
1081
1080 with self._maketransactionfn() as tr:
1082 with self._maketransactionfn() as tr:
1081 nodes = f.addgroup([], None, tr, addrevisioncb=cb)
1083 nodes = f.addgroup([], None, tr, addrevisioncb=cb)
1082
1084
1083 self.assertEqual(nodes, [])
1085 self.assertEqual(nodes, [])
1084 self.assertEqual(callbackargs, [])
1086 self.assertEqual(callbackargs, [])
1085 self.assertEqual(len(f), 0)
1087 self.assertEqual(len(f), 0)
1086
1088
1087 fulltext0 = b'foo'
1089 fulltext0 = b'foo'
1088 delta0 = mdiff.trivialdiffheader(len(fulltext0)) + fulltext0
1090 delta0 = mdiff.trivialdiffheader(len(fulltext0)) + fulltext0
1089
1091
1090 with self._maketransactionfn() as tr:
1092 with self._maketransactionfn() as tr:
1091 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
1093 node0 = f.add(fulltext0, None, tr, 0, nullid, nullid)
1092
1094
1093 f = self._makefilefn()
1095 f = self._makefilefn()
1094
1096
1095 deltas = [
1097 deltas = [
1096 (node0, nullid, nullid, nullid, nullid, delta0, 0),
1098 (node0, nullid, nullid, nullid, nullid, delta0, 0),
1097 ]
1099 ]
1098
1100
1099 with self._maketransactionfn() as tr:
1101 with self._maketransactionfn() as tr:
1100 nodes = f.addgroup(deltas, linkmapper, tr, addrevisioncb=cb)
1102 nodes = f.addgroup(deltas, linkmapper, tr, addrevisioncb=cb)
1101
1103
1102 self.assertEqual(nodes, [
1104 self.assertEqual(nodes, [
1103 b'\x49\xd8\xcb\xb1\x5c\xe2\x57\x92\x04\x47'
1105 b'\x49\xd8\xcb\xb1\x5c\xe2\x57\x92\x04\x47'
1104 b'\x00\x6b\x46\x97\x8b\x7a\xf9\x80\xa9\x79'])
1106 b'\x00\x6b\x46\x97\x8b\x7a\xf9\x80\xa9\x79'])
1105
1107
1106 self.assertEqual(len(callbackargs), 1)
1108 self.assertEqual(len(callbackargs), 1)
1107 self.assertEqual(callbackargs[0][0][1], nodes[0])
1109 self.assertEqual(callbackargs[0][0][1], nodes[0])
1108
1110
1109 self.assertEqual(list(f.revs()), [0])
1111 self.assertEqual(list(f.revs()), [0])
1110 self.assertEqual(f.rev(nodes[0]), 0)
1112 self.assertEqual(f.rev(nodes[0]), 0)
1111 self.assertEqual(f.node(0), nodes[0])
1113 self.assertEqual(f.node(0), nodes[0])
1112
1114
1113 def testaddgroupmultiple(self):
1115 def testaddgroupmultiple(self):
1114 f = self._makefilefn()
1116 f = self._makefilefn()
1115
1117
1116 fulltexts = [
1118 fulltexts = [
1117 b'foo',
1119 b'foo',
1118 b'bar',
1120 b'bar',
1119 b'x' * 1024,
1121 b'x' * 1024,
1120 ]
1122 ]
1121
1123
1122 nodes = []
1124 nodes = []
1123 with self._maketransactionfn() as tr:
1125 with self._maketransactionfn() as tr:
1124 for fulltext in fulltexts:
1126 for fulltext in fulltexts:
1125 nodes.append(f.add(fulltext, None, tr, 0, nullid, nullid))
1127 nodes.append(f.add(fulltext, None, tr, 0, nullid, nullid))
1126
1128
1127 f = self._makefilefn()
1129 f = self._makefilefn()
1128 deltas = []
1130 deltas = []
1129 for i, fulltext in enumerate(fulltexts):
1131 for i, fulltext in enumerate(fulltexts):
1130 delta = mdiff.trivialdiffheader(len(fulltext)) + fulltext
1132 delta = mdiff.trivialdiffheader(len(fulltext)) + fulltext
1131
1133
1132 deltas.append((nodes[i], nullid, nullid, nullid, nullid, delta, 0))
1134 deltas.append((nodes[i], nullid, nullid, nullid, nullid, delta, 0))
1133
1135
1134 with self._maketransactionfn() as tr:
1136 with self._maketransactionfn() as tr:
1135 self.assertEqual(f.addgroup(deltas, lambda x: 0, tr), nodes)
1137 self.assertEqual(f.addgroup(deltas, lambda x: 0, tr), nodes)
1136
1138
1137 self.assertEqual(len(f), len(deltas))
1139 self.assertEqual(len(f), len(deltas))
1138 self.assertEqual(list(f.revs()), [0, 1, 2])
1140 self.assertEqual(list(f.revs()), [0, 1, 2])
1139 self.assertEqual(f.rev(nodes[0]), 0)
1141 self.assertEqual(f.rev(nodes[0]), 0)
1140 self.assertEqual(f.rev(nodes[1]), 1)
1142 self.assertEqual(f.rev(nodes[1]), 1)
1141 self.assertEqual(f.rev(nodes[2]), 2)
1143 self.assertEqual(f.rev(nodes[2]), 2)
1142 self.assertEqual(f.node(0), nodes[0])
1144 self.assertEqual(f.node(0), nodes[0])
1143 self.assertEqual(f.node(1), nodes[1])
1145 self.assertEqual(f.node(1), nodes[1])
1144 self.assertEqual(f.node(2), nodes[2])
1146 self.assertEqual(f.node(2), nodes[2])
1145
1147
1146 def testdeltaagainstcensored(self):
1148 def testdeltaagainstcensored(self):
1147 # Attempt to apply a delta made against a censored revision.
1149 # Attempt to apply a delta made against a censored revision.
1148 f = self._makefilefn()
1150 f = self._makefilefn()
1149
1151
1150 stored1 = storageutil.packmeta({
1152 stored1 = storageutil.packmeta({
1151 b'censored': b'tombstone',
1153 b'censored': b'tombstone',
1152 }, b'')
1154 }, b'')
1153
1155
1154 with self._maketransactionfn() as tr:
1156 with self._maketransactionfn() as tr:
1155 node0 = f.add(b'foo\n' * 30, None, tr, 0, nullid, nullid)
1157 node0 = f.add(b'foo\n' * 30, None, tr, 0, nullid, nullid)
1156
1158
1157 # The node value doesn't matter since we can't verify it.
1159 # The node value doesn't matter since we can't verify it.
1158 node1 = b'\xbb' * 20
1160 node1 = b'\xbb' * 20
1159
1161
1160 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1, stored1,
1162 self._addrawrevisionfn(f, tr, node1, node0, nullid, 1, stored1,
1161 censored=True)
1163 censored=True)
1162
1164
1163 delta = mdiff.textdiff(b'bar\n' * 30, (b'bar\n' * 30) + b'baz\n')
1165 delta = mdiff.textdiff(b'bar\n' * 30, (b'bar\n' * 30) + b'baz\n')
1164 deltas = [(b'\xcc' * 20, node1, nullid, b'\x01' * 20, node1, delta, 0)]
1166 deltas = [(b'\xcc' * 20, node1, nullid, b'\x01' * 20, node1, delta, 0)]
1165
1167
1166 with self._maketransactionfn() as tr:
1168 with self._maketransactionfn() as tr:
1167 with self.assertRaises(error.CensoredBaseError):
1169 with self.assertRaises(error.CensoredBaseError):
1168 f.addgroup(deltas, lambda x: 0, tr)
1170 f.addgroup(deltas, lambda x: 0, tr)
1169
1171
1170 def testcensorrevisionbasic(self):
1172 def testcensorrevisionbasic(self):
1171 f = self._makefilefn()
1173 f = self._makefilefn()
1172
1174
1173 with self._maketransactionfn() as tr:
1175 with self._maketransactionfn() as tr:
1174 node0 = f.add(b'foo\n' * 30, None, tr, 0, nullid, nullid)
1176 node0 = f.add(b'foo\n' * 30, None, tr, 0, nullid, nullid)
1175 node1 = f.add(b'foo\n' * 31, None, tr, 1, node0, nullid)
1177 node1 = f.add(b'foo\n' * 31, None, tr, 1, node0, nullid)
1176 node2 = f.add(b'foo\n' * 32, None, tr, 2, node1, nullid)
1178 node2 = f.add(b'foo\n' * 32, None, tr, 2, node1, nullid)
1177
1179
1178 with self._maketransactionfn() as tr:
1180 with self._maketransactionfn() as tr:
1179 f.censorrevision(tr, node1)
1181 f.censorrevision(tr, node1)
1180
1182
1181 self.assertEqual(len(f), 3)
1183 self.assertEqual(len(f), 3)
1182 self.assertEqual(list(f.revs()), [0, 1, 2])
1184 self.assertEqual(list(f.revs()), [0, 1, 2])
1183
1185
1184 self.assertEqual(f.read(node0), b'foo\n' * 30)
1186 self.assertEqual(f.read(node0), b'foo\n' * 30)
1185 self.assertEqual(f.read(node2), b'foo\n' * 32)
1187 self.assertEqual(f.read(node2), b'foo\n' * 32)
1186
1188
1187 with self.assertRaises(error.CensoredNodeError):
1189 with self.assertRaises(error.CensoredNodeError):
1188 f.read(node1)
1190 f.read(node1)
1189
1191
1190 def testgetstrippointnoparents(self):
1192 def testgetstrippointnoparents(self):
1191 # N revisions where none have parents.
1193 # N revisions where none have parents.
1192 f = self._makefilefn()
1194 f = self._makefilefn()
1193
1195
1194 with self._maketransactionfn() as tr:
1196 with self._maketransactionfn() as tr:
1195 for rev in range(10):
1197 for rev in range(10):
1196 f.add(b'%d' % rev, None, tr, rev, nullid, nullid)
1198 f.add(b'%d' % rev, None, tr, rev, nullid, nullid)
1197
1199
1198 for rev in range(10):
1200 for rev in range(10):
1199 self.assertEqual(f.getstrippoint(rev), (rev, set()))
1201 self.assertEqual(f.getstrippoint(rev), (rev, set()))
1200
1202
1201 def testgetstrippointlinear(self):
1203 def testgetstrippointlinear(self):
1202 # N revisions in a linear chain.
1204 # N revisions in a linear chain.
1203 f = self._makefilefn()
1205 f = self._makefilefn()
1204
1206
1205 with self._maketransactionfn() as tr:
1207 with self._maketransactionfn() as tr:
1206 p1 = nullid
1208 p1 = nullid
1207
1209
1208 for rev in range(10):
1210 for rev in range(10):
1209 f.add(b'%d' % rev, None, tr, rev, p1, nullid)
1211 f.add(b'%d' % rev, None, tr, rev, p1, nullid)
1210
1212
1211 for rev in range(10):
1213 for rev in range(10):
1212 self.assertEqual(f.getstrippoint(rev), (rev, set()))
1214 self.assertEqual(f.getstrippoint(rev), (rev, set()))
1213
1215
1214 def testgetstrippointmultipleheads(self):
1216 def testgetstrippointmultipleheads(self):
1215 f = self._makefilefn()
1217 f = self._makefilefn()
1216
1218
1217 with self._maketransactionfn() as tr:
1219 with self._maketransactionfn() as tr:
1218 node0 = f.add(b'0', None, tr, 0, nullid, nullid)
1220 node0 = f.add(b'0', None, tr, 0, nullid, nullid)
1219 node1 = f.add(b'1', None, tr, 1, node0, nullid)
1221 node1 = f.add(b'1', None, tr, 1, node0, nullid)
1220 f.add(b'2', None, tr, 2, node1, nullid)
1222 f.add(b'2', None, tr, 2, node1, nullid)
1221 f.add(b'3', None, tr, 3, node0, nullid)
1223 f.add(b'3', None, tr, 3, node0, nullid)
1222 f.add(b'4', None, tr, 4, node0, nullid)
1224 f.add(b'4', None, tr, 4, node0, nullid)
1223
1225
1224 for rev in range(5):
1226 for rev in range(5):
1225 self.assertEqual(f.getstrippoint(rev), (rev, set()))
1227 self.assertEqual(f.getstrippoint(rev), (rev, set()))
1226
1228
1227 def testgetstrippointearlierlinkrevs(self):
1229 def testgetstrippointearlierlinkrevs(self):
1228 f = self._makefilefn()
1230 f = self._makefilefn()
1229
1231
1230 with self._maketransactionfn() as tr:
1232 with self._maketransactionfn() as tr:
1231 node0 = f.add(b'0', None, tr, 0, nullid, nullid)
1233 node0 = f.add(b'0', None, tr, 0, nullid, nullid)
1232 f.add(b'1', None, tr, 10, node0, nullid)
1234 f.add(b'1', None, tr, 10, node0, nullid)
1233 f.add(b'2', None, tr, 5, node0, nullid)
1235 f.add(b'2', None, tr, 5, node0, nullid)
1234
1236
1235 self.assertEqual(f.getstrippoint(0), (0, set()))
1237 self.assertEqual(f.getstrippoint(0), (0, set()))
1236 self.assertEqual(f.getstrippoint(1), (1, set()))
1238 self.assertEqual(f.getstrippoint(1), (1, set()))
1237 self.assertEqual(f.getstrippoint(2), (1, set()))
1239 self.assertEqual(f.getstrippoint(2), (1, set()))
1238 self.assertEqual(f.getstrippoint(3), (1, set()))
1240 self.assertEqual(f.getstrippoint(3), (1, set()))
1239 self.assertEqual(f.getstrippoint(4), (1, set()))
1241 self.assertEqual(f.getstrippoint(4), (1, set()))
1240 self.assertEqual(f.getstrippoint(5), (1, set()))
1242 self.assertEqual(f.getstrippoint(5), (1, set()))
1241 self.assertEqual(f.getstrippoint(6), (1, {2}))
1243 self.assertEqual(f.getstrippoint(6), (1, {2}))
1242 self.assertEqual(f.getstrippoint(7), (1, {2}))
1244 self.assertEqual(f.getstrippoint(7), (1, {2}))
1243 self.assertEqual(f.getstrippoint(8), (1, {2}))
1245 self.assertEqual(f.getstrippoint(8), (1, {2}))
1244 self.assertEqual(f.getstrippoint(9), (1, {2}))
1246 self.assertEqual(f.getstrippoint(9), (1, {2}))
1245 self.assertEqual(f.getstrippoint(10), (1, {2}))
1247 self.assertEqual(f.getstrippoint(10), (1, {2}))
1246 self.assertEqual(f.getstrippoint(11), (3, set()))
1248 self.assertEqual(f.getstrippoint(11), (3, set()))
1247
1249
1248 def teststripempty(self):
1250 def teststripempty(self):
1249 f = self._makefilefn()
1251 f = self._makefilefn()
1250
1252
1251 with self._maketransactionfn() as tr:
1253 with self._maketransactionfn() as tr:
1252 f.strip(0, tr)
1254 f.strip(0, tr)
1253
1255
1254 self.assertEqual(len(f), 0)
1256 self.assertEqual(len(f), 0)
1255
1257
1256 def teststripall(self):
1258 def teststripall(self):
1257 f = self._makefilefn()
1259 f = self._makefilefn()
1258
1260
1259 with self._maketransactionfn() as tr:
1261 with self._maketransactionfn() as tr:
1260 p1 = nullid
1262 p1 = nullid
1261 for rev in range(10):
1263 for rev in range(10):
1262 p1 = f.add(b'%d' % rev, None, tr, rev, p1, nullid)
1264 p1 = f.add(b'%d' % rev, None, tr, rev, p1, nullid)
1263
1265
1264 self.assertEqual(len(f), 10)
1266 self.assertEqual(len(f), 10)
1265
1267
1266 with self._maketransactionfn() as tr:
1268 with self._maketransactionfn() as tr:
1267 f.strip(0, tr)
1269 f.strip(0, tr)
1268
1270
1269 self.assertEqual(len(f), 0)
1271 self.assertEqual(len(f), 0)
1270
1272
1271 def teststrippartial(self):
1273 def teststrippartial(self):
1272 f = self._makefilefn()
1274 f = self._makefilefn()
1273
1275
1274 with self._maketransactionfn() as tr:
1276 with self._maketransactionfn() as tr:
1275 f.add(b'0', None, tr, 0, nullid, nullid)
1277 f.add(b'0', None, tr, 0, nullid, nullid)
1276 node1 = f.add(b'1', None, tr, 5, nullid, nullid)
1278 node1 = f.add(b'1', None, tr, 5, nullid, nullid)
1277 node2 = f.add(b'2', None, tr, 10, nullid, nullid)
1279 node2 = f.add(b'2', None, tr, 10, nullid, nullid)
1278
1280
1279 self.assertEqual(len(f), 3)
1281 self.assertEqual(len(f), 3)
1280
1282
1281 with self._maketransactionfn() as tr:
1283 with self._maketransactionfn() as tr:
1282 f.strip(11, tr)
1284 f.strip(11, tr)
1283
1285
1284 self.assertEqual(len(f), 3)
1286 self.assertEqual(len(f), 3)
1285
1287
1286 with self._maketransactionfn() as tr:
1288 with self._maketransactionfn() as tr:
1287 f.strip(10, tr)
1289 f.strip(10, tr)
1288
1290
1289 self.assertEqual(len(f), 2)
1291 self.assertEqual(len(f), 2)
1290
1292
1291 with self.assertRaises(error.LookupError):
1293 with self.assertRaises(error.LookupError):
1292 f.rev(node2)
1294 f.rev(node2)
1293
1295
1294 with self._maketransactionfn() as tr:
1296 with self._maketransactionfn() as tr:
1295 f.strip(6, tr)
1297 f.strip(6, tr)
1296
1298
1297 self.assertEqual(len(f), 2)
1299 self.assertEqual(len(f), 2)
1298
1300
1299 with self._maketransactionfn() as tr:
1301 with self._maketransactionfn() as tr:
1300 f.strip(3, tr)
1302 f.strip(3, tr)
1301
1303
1302 self.assertEqual(len(f), 1)
1304 self.assertEqual(len(f), 1)
1303
1305
1304 with self.assertRaises(error.LookupError):
1306 with self.assertRaises(error.LookupError):
1305 f.rev(node1)
1307 f.rev(node1)
1306
1308
1307 def makeifileindextests(makefilefn, maketransactionfn, addrawrevisionfn):
1309 def makeifileindextests(makefilefn, maketransactionfn, addrawrevisionfn):
1308 """Create a unittest.TestCase class suitable for testing file storage.
1310 """Create a unittest.TestCase class suitable for testing file storage.
1309
1311
1310 ``makefilefn`` is a callable which receives the test case as an
1312 ``makefilefn`` is a callable which receives the test case as an
1311 argument and returns an object implementing the ``ifilestorage`` interface.
1313 argument and returns an object implementing the ``ifilestorage`` interface.
1312
1314
1313 ``maketransactionfn`` is a callable which receives the test case as an
1315 ``maketransactionfn`` is a callable which receives the test case as an
1314 argument and returns a transaction object.
1316 argument and returns a transaction object.
1315
1317
1316 ``addrawrevisionfn`` is a callable which receives arguments describing a
1318 ``addrawrevisionfn`` is a callable which receives arguments describing a
1317 low-level revision to add. This callable allows the insertion of
1319 low-level revision to add. This callable allows the insertion of
1318 potentially bad data into the store in order to facilitate testing.
1320 potentially bad data into the store in order to facilitate testing.
1319
1321
1320 Returns a type that is a ``unittest.TestCase`` that can be used for
1322 Returns a type that is a ``unittest.TestCase`` that can be used for
1321 testing the object implementing the file storage interface. Simply
1323 testing the object implementing the file storage interface. Simply
1322 assign the returned value to a module-level attribute and a test loader
1324 assign the returned value to a module-level attribute and a test loader
1323 should find and run it automatically.
1325 should find and run it automatically.
1324 """
1326 """
1325 d = {
1327 d = {
1326 r'_makefilefn': makefilefn,
1328 r'_makefilefn': makefilefn,
1327 r'_maketransactionfn': maketransactionfn,
1329 r'_maketransactionfn': maketransactionfn,
1328 r'_addrawrevisionfn': addrawrevisionfn,
1330 r'_addrawrevisionfn': addrawrevisionfn,
1329 }
1331 }
1330 return type(r'ifileindextests', (ifileindextests,), d)
1332 return type(r'ifileindextests', (ifileindextests,), d)
1331
1333
1332 def makeifiledatatests(makefilefn, maketransactionfn, addrawrevisionfn):
1334 def makeifiledatatests(makefilefn, maketransactionfn, addrawrevisionfn):
1333 d = {
1335 d = {
1334 r'_makefilefn': makefilefn,
1336 r'_makefilefn': makefilefn,
1335 r'_maketransactionfn': maketransactionfn,
1337 r'_maketransactionfn': maketransactionfn,
1336 r'_addrawrevisionfn': addrawrevisionfn,
1338 r'_addrawrevisionfn': addrawrevisionfn,
1337 }
1339 }
1338 return type(r'ifiledatatests', (ifiledatatests,), d)
1340 return type(r'ifiledatatests', (ifiledatatests,), d)
1339
1341
1340 def makeifilemutationtests(makefilefn, maketransactionfn, addrawrevisionfn):
1342 def makeifilemutationtests(makefilefn, maketransactionfn, addrawrevisionfn):
1341 d = {
1343 d = {
1342 r'_makefilefn': makefilefn,
1344 r'_makefilefn': makefilefn,
1343 r'_maketransactionfn': maketransactionfn,
1345 r'_maketransactionfn': maketransactionfn,
1344 r'_addrawrevisionfn': addrawrevisionfn,
1346 r'_addrawrevisionfn': addrawrevisionfn,
1345 }
1347 }
1346 return type(r'ifilemutationtests', (ifilemutationtests,), d)
1348 return type(r'ifilemutationtests', (ifilemutationtests,), d)
@@ -1,486 +1,486
1 # storageutil.py - Storage functionality agnostic of backend implementation.
1 # storageutil.py - Storage functionality agnostic of backend implementation.
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import hashlib
10 import hashlib
11 import re
11 import re
12 import struct
12 import struct
13
13
14 from ..i18n import _
14 from ..i18n import _
15 from ..node import (
15 from ..node import (
16 bin,
16 bin,
17 nullid,
17 nullid,
18 nullrev,
18 nullrev,
19 )
19 )
20 from .. import (
20 from .. import (
21 dagop,
21 dagop,
22 error,
22 error,
23 mdiff,
23 mdiff,
24 pycompat,
24 pycompat,
25 repository,
26 )
25 )
26 from ..interfaces import repository
27
27
28 _nullhash = hashlib.sha1(nullid)
28 _nullhash = hashlib.sha1(nullid)
29
29
30 def hashrevisionsha1(text, p1, p2):
30 def hashrevisionsha1(text, p1, p2):
31 """Compute the SHA-1 for revision data and its parents.
31 """Compute the SHA-1 for revision data and its parents.
32
32
33 This hash combines both the current file contents and its history
33 This hash combines both the current file contents and its history
34 in a manner that makes it easy to distinguish nodes with the same
34 in a manner that makes it easy to distinguish nodes with the same
35 content in the revision graph.
35 content in the revision graph.
36 """
36 """
37 # As of now, if one of the parent node is null, p2 is null
37 # As of now, if one of the parent node is null, p2 is null
38 if p2 == nullid:
38 if p2 == nullid:
39 # deep copy of a hash is faster than creating one
39 # deep copy of a hash is faster than creating one
40 s = _nullhash.copy()
40 s = _nullhash.copy()
41 s.update(p1)
41 s.update(p1)
42 else:
42 else:
43 # none of the parent nodes are nullid
43 # none of the parent nodes are nullid
44 if p1 < p2:
44 if p1 < p2:
45 a = p1
45 a = p1
46 b = p2
46 b = p2
47 else:
47 else:
48 a = p2
48 a = p2
49 b = p1
49 b = p1
50 s = hashlib.sha1(a)
50 s = hashlib.sha1(a)
51 s.update(b)
51 s.update(b)
52 s.update(text)
52 s.update(text)
53 return s.digest()
53 return s.digest()
54
54
55 METADATA_RE = re.compile(b'\x01\n')
55 METADATA_RE = re.compile(b'\x01\n')
56
56
57 def parsemeta(text):
57 def parsemeta(text):
58 """Parse metadata header from revision data.
58 """Parse metadata header from revision data.
59
59
60 Returns a 2-tuple of (metadata, offset), where both can be None if there
60 Returns a 2-tuple of (metadata, offset), where both can be None if there
61 is no metadata.
61 is no metadata.
62 """
62 """
63 # text can be buffer, so we can't use .startswith or .index
63 # text can be buffer, so we can't use .startswith or .index
64 if text[:2] != b'\x01\n':
64 if text[:2] != b'\x01\n':
65 return None, None
65 return None, None
66 s = METADATA_RE.search(text, 2).start()
66 s = METADATA_RE.search(text, 2).start()
67 mtext = text[2:s]
67 mtext = text[2:s]
68 meta = {}
68 meta = {}
69 for l in mtext.splitlines():
69 for l in mtext.splitlines():
70 k, v = l.split(b': ', 1)
70 k, v = l.split(b': ', 1)
71 meta[k] = v
71 meta[k] = v
72 return meta, s + 2
72 return meta, s + 2
73
73
74 def packmeta(meta, text):
74 def packmeta(meta, text):
75 """Add metadata to fulltext to produce revision text."""
75 """Add metadata to fulltext to produce revision text."""
76 keys = sorted(meta)
76 keys = sorted(meta)
77 metatext = b''.join(b'%s: %s\n' % (k, meta[k]) for k in keys)
77 metatext = b''.join(b'%s: %s\n' % (k, meta[k]) for k in keys)
78 return b'\x01\n%s\x01\n%s' % (metatext, text)
78 return b'\x01\n%s\x01\n%s' % (metatext, text)
79
79
80 def iscensoredtext(text):
80 def iscensoredtext(text):
81 meta = parsemeta(text)[0]
81 meta = parsemeta(text)[0]
82 return meta and b'censored' in meta
82 return meta and b'censored' in meta
83
83
84 def filtermetadata(text):
84 def filtermetadata(text):
85 """Extract just the revision data from source text.
85 """Extract just the revision data from source text.
86
86
87 Returns ``text`` unless it has a metadata header, in which case we return
87 Returns ``text`` unless it has a metadata header, in which case we return
88 a new buffer without hte metadata.
88 a new buffer without hte metadata.
89 """
89 """
90 if not text.startswith(b'\x01\n'):
90 if not text.startswith(b'\x01\n'):
91 return text
91 return text
92
92
93 offset = text.index(b'\x01\n', 2)
93 offset = text.index(b'\x01\n', 2)
94 return text[offset + 2:]
94 return text[offset + 2:]
95
95
96 def filerevisioncopied(store, node):
96 def filerevisioncopied(store, node):
97 """Resolve file revision copy metadata.
97 """Resolve file revision copy metadata.
98
98
99 Returns ``False`` if the file has no copy metadata. Otherwise a
99 Returns ``False`` if the file has no copy metadata. Otherwise a
100 2-tuple of the source filename and node.
100 2-tuple of the source filename and node.
101 """
101 """
102 if store.parents(node)[0] != nullid:
102 if store.parents(node)[0] != nullid:
103 return False
103 return False
104
104
105 meta = parsemeta(store.revision(node))[0]
105 meta = parsemeta(store.revision(node))[0]
106
106
107 # copy and copyrev occur in pairs. In rare cases due to old bugs,
107 # copy and copyrev occur in pairs. In rare cases due to old bugs,
108 # one can occur without the other. So ensure both are present to flag
108 # one can occur without the other. So ensure both are present to flag
109 # as a copy.
109 # as a copy.
110 if meta and b'copy' in meta and b'copyrev' in meta:
110 if meta and b'copy' in meta and b'copyrev' in meta:
111 return meta[b'copy'], bin(meta[b'copyrev'])
111 return meta[b'copy'], bin(meta[b'copyrev'])
112
112
113 return False
113 return False
114
114
115 def filedataequivalent(store, node, filedata):
115 def filedataequivalent(store, node, filedata):
116 """Determines whether file data is equivalent to a stored node.
116 """Determines whether file data is equivalent to a stored node.
117
117
118 Returns True if the passed file data would hash to the same value
118 Returns True if the passed file data would hash to the same value
119 as a stored revision and False otherwise.
119 as a stored revision and False otherwise.
120
120
121 When a stored revision is censored, filedata must be empty to have
121 When a stored revision is censored, filedata must be empty to have
122 equivalence.
122 equivalence.
123
123
124 When a stored revision has copy metadata, it is ignored as part
124 When a stored revision has copy metadata, it is ignored as part
125 of the compare.
125 of the compare.
126 """
126 """
127
127
128 if filedata.startswith(b'\x01\n'):
128 if filedata.startswith(b'\x01\n'):
129 revisiontext = b'\x01\n\x01\n' + filedata
129 revisiontext = b'\x01\n\x01\n' + filedata
130 else:
130 else:
131 revisiontext = filedata
131 revisiontext = filedata
132
132
133 p1, p2 = store.parents(node)
133 p1, p2 = store.parents(node)
134
134
135 computednode = hashrevisionsha1(revisiontext, p1, p2)
135 computednode = hashrevisionsha1(revisiontext, p1, p2)
136
136
137 if computednode == node:
137 if computednode == node:
138 return True
138 return True
139
139
140 # Censored files compare against the empty file.
140 # Censored files compare against the empty file.
141 if store.iscensored(store.rev(node)):
141 if store.iscensored(store.rev(node)):
142 return filedata == b''
142 return filedata == b''
143
143
144 # Renaming a file produces a different hash, even if the data
144 # Renaming a file produces a different hash, even if the data
145 # remains unchanged. Check if that's the case.
145 # remains unchanged. Check if that's the case.
146 if store.renamed(node):
146 if store.renamed(node):
147 return store.read(node) == filedata
147 return store.read(node) == filedata
148
148
149 return False
149 return False
150
150
151 def iterrevs(storelen, start=0, stop=None):
151 def iterrevs(storelen, start=0, stop=None):
152 """Iterate over revision numbers in a store."""
152 """Iterate over revision numbers in a store."""
153 step = 1
153 step = 1
154
154
155 if stop is not None:
155 if stop is not None:
156 if start > stop:
156 if start > stop:
157 step = -1
157 step = -1
158 stop += step
158 stop += step
159 if stop > storelen:
159 if stop > storelen:
160 stop = storelen
160 stop = storelen
161 else:
161 else:
162 stop = storelen
162 stop = storelen
163
163
164 return pycompat.xrange(start, stop, step)
164 return pycompat.xrange(start, stop, step)
165
165
166 def fileidlookup(store, fileid, identifier):
166 def fileidlookup(store, fileid, identifier):
167 """Resolve the file node for a value.
167 """Resolve the file node for a value.
168
168
169 ``store`` is an object implementing the ``ifileindex`` interface.
169 ``store`` is an object implementing the ``ifileindex`` interface.
170
170
171 ``fileid`` can be:
171 ``fileid`` can be:
172
172
173 * A 20 byte binary node.
173 * A 20 byte binary node.
174 * An integer revision number
174 * An integer revision number
175 * A 40 byte hex node.
175 * A 40 byte hex node.
176 * A bytes that can be parsed as an integer representing a revision number.
176 * A bytes that can be parsed as an integer representing a revision number.
177
177
178 ``identifier`` is used to populate ``error.LookupError`` with an identifier
178 ``identifier`` is used to populate ``error.LookupError`` with an identifier
179 for the store.
179 for the store.
180
180
181 Raises ``error.LookupError`` on failure.
181 Raises ``error.LookupError`` on failure.
182 """
182 """
183 if isinstance(fileid, int):
183 if isinstance(fileid, int):
184 try:
184 try:
185 return store.node(fileid)
185 return store.node(fileid)
186 except IndexError:
186 except IndexError:
187 raise error.LookupError('%d' % fileid, identifier,
187 raise error.LookupError('%d' % fileid, identifier,
188 _('no match found'))
188 _('no match found'))
189
189
190 if len(fileid) == 20:
190 if len(fileid) == 20:
191 try:
191 try:
192 store.rev(fileid)
192 store.rev(fileid)
193 return fileid
193 return fileid
194 except error.LookupError:
194 except error.LookupError:
195 pass
195 pass
196
196
197 if len(fileid) == 40:
197 if len(fileid) == 40:
198 try:
198 try:
199 rawnode = bin(fileid)
199 rawnode = bin(fileid)
200 store.rev(rawnode)
200 store.rev(rawnode)
201 return rawnode
201 return rawnode
202 except TypeError:
202 except TypeError:
203 pass
203 pass
204
204
205 try:
205 try:
206 rev = int(fileid)
206 rev = int(fileid)
207
207
208 if b'%d' % rev != fileid:
208 if b'%d' % rev != fileid:
209 raise ValueError
209 raise ValueError
210
210
211 try:
211 try:
212 return store.node(rev)
212 return store.node(rev)
213 except (IndexError, TypeError):
213 except (IndexError, TypeError):
214 pass
214 pass
215 except (ValueError, OverflowError):
215 except (ValueError, OverflowError):
216 pass
216 pass
217
217
218 raise error.LookupError(fileid, identifier, _('no match found'))
218 raise error.LookupError(fileid, identifier, _('no match found'))
219
219
220 def resolvestripinfo(minlinkrev, tiprev, headrevs, linkrevfn, parentrevsfn):
220 def resolvestripinfo(minlinkrev, tiprev, headrevs, linkrevfn, parentrevsfn):
221 """Resolve information needed to strip revisions.
221 """Resolve information needed to strip revisions.
222
222
223 Finds the minimum revision number that must be stripped in order to
223 Finds the minimum revision number that must be stripped in order to
224 strip ``minlinkrev``.
224 strip ``minlinkrev``.
225
225
226 Returns a 2-tuple of the minimum revision number to do that and a set
226 Returns a 2-tuple of the minimum revision number to do that and a set
227 of all revision numbers that have linkrevs that would be broken
227 of all revision numbers that have linkrevs that would be broken
228 by that strip.
228 by that strip.
229
229
230 ``tiprev`` is the current tip-most revision. It is ``len(store) - 1``.
230 ``tiprev`` is the current tip-most revision. It is ``len(store) - 1``.
231 ``headrevs`` is an iterable of head revisions.
231 ``headrevs`` is an iterable of head revisions.
232 ``linkrevfn`` is a callable that receives a revision and returns a linked
232 ``linkrevfn`` is a callable that receives a revision and returns a linked
233 revision.
233 revision.
234 ``parentrevsfn`` is a callable that receives a revision number and returns
234 ``parentrevsfn`` is a callable that receives a revision number and returns
235 an iterable of its parent revision numbers.
235 an iterable of its parent revision numbers.
236 """
236 """
237 brokenrevs = set()
237 brokenrevs = set()
238 strippoint = tiprev + 1
238 strippoint = tiprev + 1
239
239
240 heads = {}
240 heads = {}
241 futurelargelinkrevs = set()
241 futurelargelinkrevs = set()
242 for head in headrevs:
242 for head in headrevs:
243 headlinkrev = linkrevfn(head)
243 headlinkrev = linkrevfn(head)
244 heads[head] = headlinkrev
244 heads[head] = headlinkrev
245 if headlinkrev >= minlinkrev:
245 if headlinkrev >= minlinkrev:
246 futurelargelinkrevs.add(headlinkrev)
246 futurelargelinkrevs.add(headlinkrev)
247
247
248 # This algorithm involves walking down the rev graph, starting at the
248 # This algorithm involves walking down the rev graph, starting at the
249 # heads. Since the revs are topologically sorted according to linkrev,
249 # heads. Since the revs are topologically sorted according to linkrev,
250 # once all head linkrevs are below the minlink, we know there are
250 # once all head linkrevs are below the minlink, we know there are
251 # no more revs that could have a linkrev greater than minlink.
251 # no more revs that could have a linkrev greater than minlink.
252 # So we can stop walking.
252 # So we can stop walking.
253 while futurelargelinkrevs:
253 while futurelargelinkrevs:
254 strippoint -= 1
254 strippoint -= 1
255 linkrev = heads.pop(strippoint)
255 linkrev = heads.pop(strippoint)
256
256
257 if linkrev < minlinkrev:
257 if linkrev < minlinkrev:
258 brokenrevs.add(strippoint)
258 brokenrevs.add(strippoint)
259 else:
259 else:
260 futurelargelinkrevs.remove(linkrev)
260 futurelargelinkrevs.remove(linkrev)
261
261
262 for p in parentrevsfn(strippoint):
262 for p in parentrevsfn(strippoint):
263 if p != nullrev:
263 if p != nullrev:
264 plinkrev = linkrevfn(p)
264 plinkrev = linkrevfn(p)
265 heads[p] = plinkrev
265 heads[p] = plinkrev
266 if plinkrev >= minlinkrev:
266 if plinkrev >= minlinkrev:
267 futurelargelinkrevs.add(plinkrev)
267 futurelargelinkrevs.add(plinkrev)
268
268
269 return strippoint, brokenrevs
269 return strippoint, brokenrevs
270
270
271 def emitrevisions(store, nodes, nodesorder, resultcls, deltaparentfn=None,
271 def emitrevisions(store, nodes, nodesorder, resultcls, deltaparentfn=None,
272 candeltafn=None, rawsizefn=None, revdifffn=None, flagsfn=None,
272 candeltafn=None, rawsizefn=None, revdifffn=None, flagsfn=None,
273 deltamode=repository.CG_DELTAMODE_STD,
273 deltamode=repository.CG_DELTAMODE_STD,
274 revisiondata=False, assumehaveparentrevisions=False):
274 revisiondata=False, assumehaveparentrevisions=False):
275 """Generic implementation of ifiledata.emitrevisions().
275 """Generic implementation of ifiledata.emitrevisions().
276
276
277 Emitting revision data is subtly complex. This function attempts to
277 Emitting revision data is subtly complex. This function attempts to
278 encapsulate all the logic for doing so in a backend-agnostic way.
278 encapsulate all the logic for doing so in a backend-agnostic way.
279
279
280 ``store``
280 ``store``
281 Object conforming to ``ifilestorage`` interface.
281 Object conforming to ``ifilestorage`` interface.
282
282
283 ``nodes``
283 ``nodes``
284 List of revision nodes whose data to emit.
284 List of revision nodes whose data to emit.
285
285
286 ``resultcls``
286 ``resultcls``
287 A type implementing the ``irevisiondelta`` interface that will be
287 A type implementing the ``irevisiondelta`` interface that will be
288 constructed and returned.
288 constructed and returned.
289
289
290 ``deltaparentfn`` (optional)
290 ``deltaparentfn`` (optional)
291 Callable receiving a revision number and returning the revision number
291 Callable receiving a revision number and returning the revision number
292 of a revision that the internal delta is stored against. This delta
292 of a revision that the internal delta is stored against. This delta
293 will be preferred over computing a new arbitrary delta.
293 will be preferred over computing a new arbitrary delta.
294
294
295 If not defined, a delta will always be computed from raw revision
295 If not defined, a delta will always be computed from raw revision
296 data.
296 data.
297
297
298 ``candeltafn`` (optional)
298 ``candeltafn`` (optional)
299 Callable receiving a pair of revision numbers that returns a bool
299 Callable receiving a pair of revision numbers that returns a bool
300 indicating whether a delta between them can be produced.
300 indicating whether a delta between them can be produced.
301
301
302 If not defined, it is assumed that any two revisions can delta with
302 If not defined, it is assumed that any two revisions can delta with
303 each other.
303 each other.
304
304
305 ``rawsizefn`` (optional)
305 ``rawsizefn`` (optional)
306 Callable receiving a revision number and returning the length of the
306 Callable receiving a revision number and returning the length of the
307 ``store.rawdata(rev)``.
307 ``store.rawdata(rev)``.
308
308
309 If not defined, ``len(store.rawdata(rev))`` will be called.
309 If not defined, ``len(store.rawdata(rev))`` will be called.
310
310
311 ``revdifffn`` (optional)
311 ``revdifffn`` (optional)
312 Callable receiving a pair of revision numbers that returns a delta
312 Callable receiving a pair of revision numbers that returns a delta
313 between them.
313 between them.
314
314
315 If not defined, a delta will be computed by invoking mdiff code
315 If not defined, a delta will be computed by invoking mdiff code
316 on ``store.revision()`` results.
316 on ``store.revision()`` results.
317
317
318 Defining this function allows a precomputed or stored delta to be
318 Defining this function allows a precomputed or stored delta to be
319 used without having to compute on.
319 used without having to compute on.
320
320
321 ``flagsfn`` (optional)
321 ``flagsfn`` (optional)
322 Callable receiving a revision number and returns the integer flags
322 Callable receiving a revision number and returns the integer flags
323 value for it. If not defined, flags value will be 0.
323 value for it. If not defined, flags value will be 0.
324
324
325 ``deltamode``
325 ``deltamode``
326 constaint on delta to be sent:
326 constaint on delta to be sent:
327 * CG_DELTAMODE_STD - normal mode, try to reuse storage deltas,
327 * CG_DELTAMODE_STD - normal mode, try to reuse storage deltas,
328 * CG_DELTAMODE_PREV - only delta against "prev",
328 * CG_DELTAMODE_PREV - only delta against "prev",
329 * CG_DELTAMODE_FULL - only issue full snapshot.
329 * CG_DELTAMODE_FULL - only issue full snapshot.
330
330
331 Whether to send fulltext revisions instead of deltas, if allowed.
331 Whether to send fulltext revisions instead of deltas, if allowed.
332
332
333 ``nodesorder``
333 ``nodesorder``
334 ``revisiondata``
334 ``revisiondata``
335 ``assumehaveparentrevisions``
335 ``assumehaveparentrevisions``
336 """
336 """
337
337
338 fnode = store.node
338 fnode = store.node
339 frev = store.rev
339 frev = store.rev
340
340
341 if nodesorder == 'nodes':
341 if nodesorder == 'nodes':
342 revs = [frev(n) for n in nodes]
342 revs = [frev(n) for n in nodes]
343 elif nodesorder == 'linear':
343 elif nodesorder == 'linear':
344 revs = set(frev(n) for n in nodes)
344 revs = set(frev(n) for n in nodes)
345 revs = dagop.linearize(revs, store.parentrevs)
345 revs = dagop.linearize(revs, store.parentrevs)
346 else: # storage and default
346 else: # storage and default
347 revs = sorted(frev(n) for n in nodes)
347 revs = sorted(frev(n) for n in nodes)
348
348
349 prevrev = None
349 prevrev = None
350
350
351 if deltamode == repository.CG_DELTAMODE_PREV or assumehaveparentrevisions:
351 if deltamode == repository.CG_DELTAMODE_PREV or assumehaveparentrevisions:
352 prevrev = store.parentrevs(revs[0])[0]
352 prevrev = store.parentrevs(revs[0])[0]
353
353
354 # Set of revs available to delta against.
354 # Set of revs available to delta against.
355 available = set()
355 available = set()
356
356
357 for rev in revs:
357 for rev in revs:
358 if rev == nullrev:
358 if rev == nullrev:
359 continue
359 continue
360
360
361 node = fnode(rev)
361 node = fnode(rev)
362 p1rev, p2rev = store.parentrevs(rev)
362 p1rev, p2rev = store.parentrevs(rev)
363
363
364 if deltaparentfn:
364 if deltaparentfn:
365 deltaparentrev = deltaparentfn(rev)
365 deltaparentrev = deltaparentfn(rev)
366 else:
366 else:
367 deltaparentrev = nullrev
367 deltaparentrev = nullrev
368
368
369 # Forced delta against previous mode.
369 # Forced delta against previous mode.
370 if deltamode == repository.CG_DELTAMODE_PREV:
370 if deltamode == repository.CG_DELTAMODE_PREV:
371 baserev = prevrev
371 baserev = prevrev
372
372
373 # We're instructed to send fulltext. Honor that.
373 # We're instructed to send fulltext. Honor that.
374 elif deltamode == repository.CG_DELTAMODE_FULL:
374 elif deltamode == repository.CG_DELTAMODE_FULL:
375 baserev = nullrev
375 baserev = nullrev
376 # We're instructed to use p1. Honor that
376 # We're instructed to use p1. Honor that
377 elif deltamode == repository.CG_DELTAMODE_P1:
377 elif deltamode == repository.CG_DELTAMODE_P1:
378 baserev = p1rev
378 baserev = p1rev
379
379
380 # There is a delta in storage. We try to use that because it
380 # There is a delta in storage. We try to use that because it
381 # amounts to effectively copying data from storage and is
381 # amounts to effectively copying data from storage and is
382 # therefore the fastest.
382 # therefore the fastest.
383 elif deltaparentrev != nullrev:
383 elif deltaparentrev != nullrev:
384 # Base revision was already emitted in this group. We can
384 # Base revision was already emitted in this group. We can
385 # always safely use the delta.
385 # always safely use the delta.
386 if deltaparentrev in available:
386 if deltaparentrev in available:
387 baserev = deltaparentrev
387 baserev = deltaparentrev
388
388
389 # Base revision is a parent that hasn't been emitted already.
389 # Base revision is a parent that hasn't been emitted already.
390 # Use it if we can assume the receiver has the parent revision.
390 # Use it if we can assume the receiver has the parent revision.
391 elif (assumehaveparentrevisions
391 elif (assumehaveparentrevisions
392 and deltaparentrev in (p1rev, p2rev)):
392 and deltaparentrev in (p1rev, p2rev)):
393 baserev = deltaparentrev
393 baserev = deltaparentrev
394
394
395 # No guarantee the receiver has the delta parent. Send delta
395 # No guarantee the receiver has the delta parent. Send delta
396 # against last revision (if possible), which in the common case
396 # against last revision (if possible), which in the common case
397 # should be similar enough to this revision that the delta is
397 # should be similar enough to this revision that the delta is
398 # reasonable.
398 # reasonable.
399 elif prevrev is not None:
399 elif prevrev is not None:
400 baserev = prevrev
400 baserev = prevrev
401 else:
401 else:
402 baserev = nullrev
402 baserev = nullrev
403
403
404 # Storage has a fulltext revision.
404 # Storage has a fulltext revision.
405
405
406 # Let's use the previous revision, which is as good a guess as any.
406 # Let's use the previous revision, which is as good a guess as any.
407 # There is definitely room to improve this logic.
407 # There is definitely room to improve this logic.
408 elif prevrev is not None:
408 elif prevrev is not None:
409 baserev = prevrev
409 baserev = prevrev
410 else:
410 else:
411 baserev = nullrev
411 baserev = nullrev
412
412
413 # But we can't actually use our chosen delta base for whatever
413 # But we can't actually use our chosen delta base for whatever
414 # reason. Reset to fulltext.
414 # reason. Reset to fulltext.
415 if baserev != nullrev and (candeltafn and not candeltafn(baserev, rev)):
415 if baserev != nullrev and (candeltafn and not candeltafn(baserev, rev)):
416 baserev = nullrev
416 baserev = nullrev
417
417
418 revision = None
418 revision = None
419 delta = None
419 delta = None
420 baserevisionsize = None
420 baserevisionsize = None
421
421
422 if revisiondata:
422 if revisiondata:
423 if store.iscensored(baserev) or store.iscensored(rev):
423 if store.iscensored(baserev) or store.iscensored(rev):
424 try:
424 try:
425 revision = store.rawdata(node)
425 revision = store.rawdata(node)
426 except error.CensoredNodeError as e:
426 except error.CensoredNodeError as e:
427 revision = e.tombstone
427 revision = e.tombstone
428
428
429 if baserev != nullrev:
429 if baserev != nullrev:
430 if rawsizefn:
430 if rawsizefn:
431 baserevisionsize = rawsizefn(baserev)
431 baserevisionsize = rawsizefn(baserev)
432 else:
432 else:
433 baserevisionsize = len(store.rawdata(baserev))
433 baserevisionsize = len(store.rawdata(baserev))
434
434
435 elif (baserev == nullrev
435 elif (baserev == nullrev
436 and deltamode != repository.CG_DELTAMODE_PREV):
436 and deltamode != repository.CG_DELTAMODE_PREV):
437 revision = store.rawdata(node)
437 revision = store.rawdata(node)
438 available.add(rev)
438 available.add(rev)
439 else:
439 else:
440 if revdifffn:
440 if revdifffn:
441 delta = revdifffn(baserev, rev)
441 delta = revdifffn(baserev, rev)
442 else:
442 else:
443 delta = mdiff.textdiff(store.rawdata(baserev),
443 delta = mdiff.textdiff(store.rawdata(baserev),
444 store.rawdata(rev))
444 store.rawdata(rev))
445
445
446 available.add(rev)
446 available.add(rev)
447
447
448 yield resultcls(
448 yield resultcls(
449 node=node,
449 node=node,
450 p1node=fnode(p1rev),
450 p1node=fnode(p1rev),
451 p2node=fnode(p2rev),
451 p2node=fnode(p2rev),
452 basenode=fnode(baserev),
452 basenode=fnode(baserev),
453 flags=flagsfn(rev) if flagsfn else 0,
453 flags=flagsfn(rev) if flagsfn else 0,
454 baserevisionsize=baserevisionsize,
454 baserevisionsize=baserevisionsize,
455 revision=revision,
455 revision=revision,
456 delta=delta)
456 delta=delta)
457
457
458 prevrev = rev
458 prevrev = rev
459
459
460 def deltaiscensored(delta, baserev, baselenfn):
460 def deltaiscensored(delta, baserev, baselenfn):
461 """Determine if a delta represents censored revision data.
461 """Determine if a delta represents censored revision data.
462
462
463 ``baserev`` is the base revision this delta is encoded against.
463 ``baserev`` is the base revision this delta is encoded against.
464 ``baselenfn`` is a callable receiving a revision number that resolves the
464 ``baselenfn`` is a callable receiving a revision number that resolves the
465 length of the revision fulltext.
465 length of the revision fulltext.
466
466
467 Returns a bool indicating if the result of the delta represents a censored
467 Returns a bool indicating if the result of the delta represents a censored
468 revision.
468 revision.
469 """
469 """
470 # Fragile heuristic: unless new file meta keys are added alphabetically
470 # Fragile heuristic: unless new file meta keys are added alphabetically
471 # preceding "censored", all censored revisions are prefixed by
471 # preceding "censored", all censored revisions are prefixed by
472 # "\1\ncensored:". A delta producing such a censored revision must be a
472 # "\1\ncensored:". A delta producing such a censored revision must be a
473 # full-replacement delta, so we inspect the first and only patch in the
473 # full-replacement delta, so we inspect the first and only patch in the
474 # delta for this prefix.
474 # delta for this prefix.
475 hlen = struct.calcsize(">lll")
475 hlen = struct.calcsize(">lll")
476 if len(delta) <= hlen:
476 if len(delta) <= hlen:
477 return False
477 return False
478
478
479 oldlen = baselenfn(baserev)
479 oldlen = baselenfn(baserev)
480 newlen = len(delta) - hlen
480 newlen = len(delta) - hlen
481 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
481 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
482 return False
482 return False
483
483
484 add = "\1\ncensored:"
484 add = "\1\ncensored:"
485 addlen = len(add)
485 addlen = len(add)
486 return newlen >= addlen and delta[hlen:hlen + addlen] == add
486 return newlen >= addlen and delta[hlen:hlen + addlen] == add
@@ -1,624 +1,626
1 # wireprotov1peer.py - Client-side functionality for wire protocol version 1.
1 # wireprotov1peer.py - Client-side functionality for wire protocol version 1.
2 #
2 #
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2010 Matt Mackall <mpm@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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import hashlib
10 import hashlib
11 import sys
11 import sys
12 import weakref
12 import weakref
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import (
15 from .node import (
16 bin,
16 bin,
17 )
17 )
18 from . import (
18 from . import (
19 bundle2,
19 bundle2,
20 changegroup as changegroupmod,
20 changegroup as changegroupmod,
21 encoding,
21 encoding,
22 error,
22 error,
23 pushkey as pushkeymod,
23 pushkey as pushkeymod,
24 pycompat,
24 pycompat,
25 repository,
26 util,
25 util,
27 wireprototypes,
26 wireprototypes,
28 )
27 )
28 from .interfaces import (
29 repository,
30 )
29 from .utils import (
31 from .utils import (
30 interfaceutil,
32 interfaceutil,
31 )
33 )
32
34
33 urlreq = util.urlreq
35 urlreq = util.urlreq
34
36
35 def batchable(f):
37 def batchable(f):
36 '''annotation for batchable methods
38 '''annotation for batchable methods
37
39
38 Such methods must implement a coroutine as follows:
40 Such methods must implement a coroutine as follows:
39
41
40 @batchable
42 @batchable
41 def sample(self, one, two=None):
43 def sample(self, one, two=None):
42 # Build list of encoded arguments suitable for your wire protocol:
44 # Build list of encoded arguments suitable for your wire protocol:
43 encargs = [('one', encode(one),), ('two', encode(two),)]
45 encargs = [('one', encode(one),), ('two', encode(two),)]
44 # Create future for injection of encoded result:
46 # Create future for injection of encoded result:
45 encresref = future()
47 encresref = future()
46 # Return encoded arguments and future:
48 # Return encoded arguments and future:
47 yield encargs, encresref
49 yield encargs, encresref
48 # Assuming the future to be filled with the result from the batched
50 # Assuming the future to be filled with the result from the batched
49 # request now. Decode it:
51 # request now. Decode it:
50 yield decode(encresref.value)
52 yield decode(encresref.value)
51
53
52 The decorator returns a function which wraps this coroutine as a plain
54 The decorator returns a function which wraps this coroutine as a plain
53 method, but adds the original method as an attribute called "batchable",
55 method, but adds the original method as an attribute called "batchable",
54 which is used by remotebatch to split the call into separate encoding and
56 which is used by remotebatch to split the call into separate encoding and
55 decoding phases.
57 decoding phases.
56 '''
58 '''
57 def plain(*args, **opts):
59 def plain(*args, **opts):
58 batchable = f(*args, **opts)
60 batchable = f(*args, **opts)
59 encargsorres, encresref = next(batchable)
61 encargsorres, encresref = next(batchable)
60 if not encresref:
62 if not encresref:
61 return encargsorres # a local result in this case
63 return encargsorres # a local result in this case
62 self = args[0]
64 self = args[0]
63 cmd = pycompat.bytesurl(f.__name__) # ensure cmd is ascii bytestr
65 cmd = pycompat.bytesurl(f.__name__) # ensure cmd is ascii bytestr
64 encresref.set(self._submitone(cmd, encargsorres))
66 encresref.set(self._submitone(cmd, encargsorres))
65 return next(batchable)
67 return next(batchable)
66 setattr(plain, 'batchable', f)
68 setattr(plain, 'batchable', f)
67 setattr(plain, '__name__', f.__name__)
69 setattr(plain, '__name__', f.__name__)
68 return plain
70 return plain
69
71
70 class future(object):
72 class future(object):
71 '''placeholder for a value to be set later'''
73 '''placeholder for a value to be set later'''
72 def set(self, value):
74 def set(self, value):
73 if util.safehasattr(self, 'value'):
75 if util.safehasattr(self, 'value'):
74 raise error.RepoError("future is already set")
76 raise error.RepoError("future is already set")
75 self.value = value
77 self.value = value
76
78
77 def encodebatchcmds(req):
79 def encodebatchcmds(req):
78 """Return a ``cmds`` argument value for the ``batch`` command."""
80 """Return a ``cmds`` argument value for the ``batch`` command."""
79 escapearg = wireprototypes.escapebatcharg
81 escapearg = wireprototypes.escapebatcharg
80
82
81 cmds = []
83 cmds = []
82 for op, argsdict in req:
84 for op, argsdict in req:
83 # Old servers didn't properly unescape argument names. So prevent
85 # Old servers didn't properly unescape argument names. So prevent
84 # the sending of argument names that may not be decoded properly by
86 # the sending of argument names that may not be decoded properly by
85 # servers.
87 # servers.
86 assert all(escapearg(k) == k for k in argsdict)
88 assert all(escapearg(k) == k for k in argsdict)
87
89
88 args = ','.join('%s=%s' % (escapearg(k), escapearg(v))
90 args = ','.join('%s=%s' % (escapearg(k), escapearg(v))
89 for k, v in argsdict.iteritems())
91 for k, v in argsdict.iteritems())
90 cmds.append('%s %s' % (op, args))
92 cmds.append('%s %s' % (op, args))
91
93
92 return ';'.join(cmds)
94 return ';'.join(cmds)
93
95
94 class unsentfuture(pycompat.futures.Future):
96 class unsentfuture(pycompat.futures.Future):
95 """A Future variation to represent an unsent command.
97 """A Future variation to represent an unsent command.
96
98
97 Because we buffer commands and don't submit them immediately, calling
99 Because we buffer commands and don't submit them immediately, calling
98 ``result()`` on an unsent future could deadlock. Futures for buffered
100 ``result()`` on an unsent future could deadlock. Futures for buffered
99 commands are represented by this type, which wraps ``result()`` to
101 commands are represented by this type, which wraps ``result()`` to
100 call ``sendcommands()``.
102 call ``sendcommands()``.
101 """
103 """
102
104
103 def result(self, timeout=None):
105 def result(self, timeout=None):
104 if self.done():
106 if self.done():
105 return pycompat.futures.Future.result(self, timeout)
107 return pycompat.futures.Future.result(self, timeout)
106
108
107 self._peerexecutor.sendcommands()
109 self._peerexecutor.sendcommands()
108
110
109 # This looks like it will infinitely recurse. However,
111 # This looks like it will infinitely recurse. However,
110 # sendcommands() should modify __class__. This call serves as a check
112 # sendcommands() should modify __class__. This call serves as a check
111 # on that.
113 # on that.
112 return self.result(timeout)
114 return self.result(timeout)
113
115
114 @interfaceutil.implementer(repository.ipeercommandexecutor)
116 @interfaceutil.implementer(repository.ipeercommandexecutor)
115 class peerexecutor(object):
117 class peerexecutor(object):
116 def __init__(self, peer):
118 def __init__(self, peer):
117 self._peer = peer
119 self._peer = peer
118 self._sent = False
120 self._sent = False
119 self._closed = False
121 self._closed = False
120 self._calls = []
122 self._calls = []
121 self._futures = weakref.WeakSet()
123 self._futures = weakref.WeakSet()
122 self._responseexecutor = None
124 self._responseexecutor = None
123 self._responsef = None
125 self._responsef = None
124
126
125 def __enter__(self):
127 def __enter__(self):
126 return self
128 return self
127
129
128 def __exit__(self, exctype, excvalee, exctb):
130 def __exit__(self, exctype, excvalee, exctb):
129 self.close()
131 self.close()
130
132
131 def callcommand(self, command, args):
133 def callcommand(self, command, args):
132 if self._sent:
134 if self._sent:
133 raise error.ProgrammingError('callcommand() cannot be used '
135 raise error.ProgrammingError('callcommand() cannot be used '
134 'after commands are sent')
136 'after commands are sent')
135
137
136 if self._closed:
138 if self._closed:
137 raise error.ProgrammingError('callcommand() cannot be used '
139 raise error.ProgrammingError('callcommand() cannot be used '
138 'after close()')
140 'after close()')
139
141
140 # Commands are dispatched through methods on the peer.
142 # Commands are dispatched through methods on the peer.
141 fn = getattr(self._peer, pycompat.sysstr(command), None)
143 fn = getattr(self._peer, pycompat.sysstr(command), None)
142
144
143 if not fn:
145 if not fn:
144 raise error.ProgrammingError(
146 raise error.ProgrammingError(
145 'cannot call command %s: method of same name not available '
147 'cannot call command %s: method of same name not available '
146 'on peer' % command)
148 'on peer' % command)
147
149
148 # Commands are either batchable or they aren't. If a command
150 # Commands are either batchable or they aren't. If a command
149 # isn't batchable, we send it immediately because the executor
151 # isn't batchable, we send it immediately because the executor
150 # can no longer accept new commands after a non-batchable command.
152 # can no longer accept new commands after a non-batchable command.
151 # If a command is batchable, we queue it for later. But we have
153 # If a command is batchable, we queue it for later. But we have
152 # to account for the case of a non-batchable command arriving after
154 # to account for the case of a non-batchable command arriving after
153 # a batchable one and refuse to service it.
155 # a batchable one and refuse to service it.
154
156
155 def addcall():
157 def addcall():
156 f = pycompat.futures.Future()
158 f = pycompat.futures.Future()
157 self._futures.add(f)
159 self._futures.add(f)
158 self._calls.append((command, args, fn, f))
160 self._calls.append((command, args, fn, f))
159 return f
161 return f
160
162
161 if getattr(fn, 'batchable', False):
163 if getattr(fn, 'batchable', False):
162 f = addcall()
164 f = addcall()
163
165
164 # But since we don't issue it immediately, we wrap its result()
166 # But since we don't issue it immediately, we wrap its result()
165 # to trigger sending so we avoid deadlocks.
167 # to trigger sending so we avoid deadlocks.
166 f.__class__ = unsentfuture
168 f.__class__ = unsentfuture
167 f._peerexecutor = self
169 f._peerexecutor = self
168 else:
170 else:
169 if self._calls:
171 if self._calls:
170 raise error.ProgrammingError(
172 raise error.ProgrammingError(
171 '%s is not batchable and cannot be called on a command '
173 '%s is not batchable and cannot be called on a command '
172 'executor along with other commands' % command)
174 'executor along with other commands' % command)
173
175
174 f = addcall()
176 f = addcall()
175
177
176 # Non-batchable commands can never coexist with another command
178 # Non-batchable commands can never coexist with another command
177 # in this executor. So send the command immediately.
179 # in this executor. So send the command immediately.
178 self.sendcommands()
180 self.sendcommands()
179
181
180 return f
182 return f
181
183
182 def sendcommands(self):
184 def sendcommands(self):
183 if self._sent:
185 if self._sent:
184 return
186 return
185
187
186 if not self._calls:
188 if not self._calls:
187 return
189 return
188
190
189 self._sent = True
191 self._sent = True
190
192
191 # Unhack any future types so caller seens a clean type and to break
193 # Unhack any future types so caller seens a clean type and to break
192 # cycle between us and futures.
194 # cycle between us and futures.
193 for f in self._futures:
195 for f in self._futures:
194 if isinstance(f, unsentfuture):
196 if isinstance(f, unsentfuture):
195 f.__class__ = pycompat.futures.Future
197 f.__class__ = pycompat.futures.Future
196 f._peerexecutor = None
198 f._peerexecutor = None
197
199
198 calls = self._calls
200 calls = self._calls
199 # Mainly to destroy references to futures.
201 # Mainly to destroy references to futures.
200 self._calls = None
202 self._calls = None
201
203
202 # Simple case of a single command. We call it synchronously.
204 # Simple case of a single command. We call it synchronously.
203 if len(calls) == 1:
205 if len(calls) == 1:
204 command, args, fn, f = calls[0]
206 command, args, fn, f = calls[0]
205
207
206 # Future was cancelled. Ignore it.
208 # Future was cancelled. Ignore it.
207 if not f.set_running_or_notify_cancel():
209 if not f.set_running_or_notify_cancel():
208 return
210 return
209
211
210 try:
212 try:
211 result = fn(**pycompat.strkwargs(args))
213 result = fn(**pycompat.strkwargs(args))
212 except Exception:
214 except Exception:
213 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
215 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
214 else:
216 else:
215 f.set_result(result)
217 f.set_result(result)
216
218
217 return
219 return
218
220
219 # Batch commands are a bit harder. First, we have to deal with the
221 # Batch commands are a bit harder. First, we have to deal with the
220 # @batchable coroutine. That's a bit annoying. Furthermore, we also
222 # @batchable coroutine. That's a bit annoying. Furthermore, we also
221 # need to preserve streaming. i.e. it should be possible for the
223 # need to preserve streaming. i.e. it should be possible for the
222 # futures to resolve as data is coming in off the wire without having
224 # futures to resolve as data is coming in off the wire without having
223 # to wait for the final byte of the final response. We do this by
225 # to wait for the final byte of the final response. We do this by
224 # spinning up a thread to read the responses.
226 # spinning up a thread to read the responses.
225
227
226 requests = []
228 requests = []
227 states = []
229 states = []
228
230
229 for command, args, fn, f in calls:
231 for command, args, fn, f in calls:
230 # Future was cancelled. Ignore it.
232 # Future was cancelled. Ignore it.
231 if not f.set_running_or_notify_cancel():
233 if not f.set_running_or_notify_cancel():
232 continue
234 continue
233
235
234 try:
236 try:
235 batchable = fn.batchable(fn.__self__,
237 batchable = fn.batchable(fn.__self__,
236 **pycompat.strkwargs(args))
238 **pycompat.strkwargs(args))
237 except Exception:
239 except Exception:
238 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
240 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
239 return
241 return
240
242
241 # Encoded arguments and future holding remote result.
243 # Encoded arguments and future holding remote result.
242 try:
244 try:
243 encargsorres, fremote = next(batchable)
245 encargsorres, fremote = next(batchable)
244 except Exception:
246 except Exception:
245 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
247 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
246 return
248 return
247
249
248 if not fremote:
250 if not fremote:
249 f.set_result(encargsorres)
251 f.set_result(encargsorres)
250 else:
252 else:
251 requests.append((command, encargsorres))
253 requests.append((command, encargsorres))
252 states.append((command, f, batchable, fremote))
254 states.append((command, f, batchable, fremote))
253
255
254 if not requests:
256 if not requests:
255 return
257 return
256
258
257 # This will emit responses in order they were executed.
259 # This will emit responses in order they were executed.
258 wireresults = self._peer._submitbatch(requests)
260 wireresults = self._peer._submitbatch(requests)
259
261
260 # The use of a thread pool executor here is a bit weird for something
262 # The use of a thread pool executor here is a bit weird for something
261 # that only spins up a single thread. However, thread management is
263 # that only spins up a single thread. However, thread management is
262 # hard and it is easy to encounter race conditions, deadlocks, etc.
264 # hard and it is easy to encounter race conditions, deadlocks, etc.
263 # concurrent.futures already solves these problems and its thread pool
265 # concurrent.futures already solves these problems and its thread pool
264 # executor has minimal overhead. So we use it.
266 # executor has minimal overhead. So we use it.
265 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
267 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
266 self._responsef = self._responseexecutor.submit(self._readbatchresponse,
268 self._responsef = self._responseexecutor.submit(self._readbatchresponse,
267 states, wireresults)
269 states, wireresults)
268
270
269 def close(self):
271 def close(self):
270 self.sendcommands()
272 self.sendcommands()
271
273
272 if self._closed:
274 if self._closed:
273 return
275 return
274
276
275 self._closed = True
277 self._closed = True
276
278
277 if not self._responsef:
279 if not self._responsef:
278 return
280 return
279
281
280 # We need to wait on our in-flight response and then shut down the
282 # We need to wait on our in-flight response and then shut down the
281 # executor once we have a result.
283 # executor once we have a result.
282 try:
284 try:
283 self._responsef.result()
285 self._responsef.result()
284 finally:
286 finally:
285 self._responseexecutor.shutdown(wait=True)
287 self._responseexecutor.shutdown(wait=True)
286 self._responsef = None
288 self._responsef = None
287 self._responseexecutor = None
289 self._responseexecutor = None
288
290
289 # If any of our futures are still in progress, mark them as
291 # If any of our futures are still in progress, mark them as
290 # errored. Otherwise a result() could wait indefinitely.
292 # errored. Otherwise a result() could wait indefinitely.
291 for f in self._futures:
293 for f in self._futures:
292 if not f.done():
294 if not f.done():
293 f.set_exception(error.ResponseError(
295 f.set_exception(error.ResponseError(
294 _('unfulfilled batch command response')))
296 _('unfulfilled batch command response')))
295
297
296 self._futures = None
298 self._futures = None
297
299
298 def _readbatchresponse(self, states, wireresults):
300 def _readbatchresponse(self, states, wireresults):
299 # Executes in a thread to read data off the wire.
301 # Executes in a thread to read data off the wire.
300
302
301 for command, f, batchable, fremote in states:
303 for command, f, batchable, fremote in states:
302 # Grab raw result off the wire and teach the internal future
304 # Grab raw result off the wire and teach the internal future
303 # about it.
305 # about it.
304 remoteresult = next(wireresults)
306 remoteresult = next(wireresults)
305 fremote.set(remoteresult)
307 fremote.set(remoteresult)
306
308
307 # And ask the coroutine to decode that value.
309 # And ask the coroutine to decode that value.
308 try:
310 try:
309 result = next(batchable)
311 result = next(batchable)
310 except Exception:
312 except Exception:
311 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
313 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
312 else:
314 else:
313 f.set_result(result)
315 f.set_result(result)
314
316
315 @interfaceutil.implementer(repository.ipeercommands,
317 @interfaceutil.implementer(repository.ipeercommands,
316 repository.ipeerlegacycommands)
318 repository.ipeerlegacycommands)
317 class wirepeer(repository.peer):
319 class wirepeer(repository.peer):
318 """Client-side interface for communicating with a peer repository.
320 """Client-side interface for communicating with a peer repository.
319
321
320 Methods commonly call wire protocol commands of the same name.
322 Methods commonly call wire protocol commands of the same name.
321
323
322 See also httppeer.py and sshpeer.py for protocol-specific
324 See also httppeer.py and sshpeer.py for protocol-specific
323 implementations of this interface.
325 implementations of this interface.
324 """
326 """
325 def commandexecutor(self):
327 def commandexecutor(self):
326 return peerexecutor(self)
328 return peerexecutor(self)
327
329
328 # Begin of ipeercommands interface.
330 # Begin of ipeercommands interface.
329
331
330 def clonebundles(self):
332 def clonebundles(self):
331 self.requirecap('clonebundles', _('clone bundles'))
333 self.requirecap('clonebundles', _('clone bundles'))
332 return self._call('clonebundles')
334 return self._call('clonebundles')
333
335
334 @batchable
336 @batchable
335 def lookup(self, key):
337 def lookup(self, key):
336 self.requirecap('lookup', _('look up remote revision'))
338 self.requirecap('lookup', _('look up remote revision'))
337 f = future()
339 f = future()
338 yield {'key': encoding.fromlocal(key)}, f
340 yield {'key': encoding.fromlocal(key)}, f
339 d = f.value
341 d = f.value
340 success, data = d[:-1].split(" ", 1)
342 success, data = d[:-1].split(" ", 1)
341 if int(success):
343 if int(success):
342 yield bin(data)
344 yield bin(data)
343 else:
345 else:
344 self._abort(error.RepoError(data))
346 self._abort(error.RepoError(data))
345
347
346 @batchable
348 @batchable
347 def heads(self):
349 def heads(self):
348 f = future()
350 f = future()
349 yield {}, f
351 yield {}, f
350 d = f.value
352 d = f.value
351 try:
353 try:
352 yield wireprototypes.decodelist(d[:-1])
354 yield wireprototypes.decodelist(d[:-1])
353 except ValueError:
355 except ValueError:
354 self._abort(error.ResponseError(_("unexpected response:"), d))
356 self._abort(error.ResponseError(_("unexpected response:"), d))
355
357
356 @batchable
358 @batchable
357 def known(self, nodes):
359 def known(self, nodes):
358 f = future()
360 f = future()
359 yield {'nodes': wireprototypes.encodelist(nodes)}, f
361 yield {'nodes': wireprototypes.encodelist(nodes)}, f
360 d = f.value
362 d = f.value
361 try:
363 try:
362 yield [bool(int(b)) for b in pycompat.iterbytestr(d)]
364 yield [bool(int(b)) for b in pycompat.iterbytestr(d)]
363 except ValueError:
365 except ValueError:
364 self._abort(error.ResponseError(_("unexpected response:"), d))
366 self._abort(error.ResponseError(_("unexpected response:"), d))
365
367
366 @batchable
368 @batchable
367 def branchmap(self):
369 def branchmap(self):
368 f = future()
370 f = future()
369 yield {}, f
371 yield {}, f
370 d = f.value
372 d = f.value
371 try:
373 try:
372 branchmap = {}
374 branchmap = {}
373 for branchpart in d.splitlines():
375 for branchpart in d.splitlines():
374 branchname, branchheads = branchpart.split(' ', 1)
376 branchname, branchheads = branchpart.split(' ', 1)
375 branchname = encoding.tolocal(urlreq.unquote(branchname))
377 branchname = encoding.tolocal(urlreq.unquote(branchname))
376 branchheads = wireprototypes.decodelist(branchheads)
378 branchheads = wireprototypes.decodelist(branchheads)
377 branchmap[branchname] = branchheads
379 branchmap[branchname] = branchheads
378 yield branchmap
380 yield branchmap
379 except TypeError:
381 except TypeError:
380 self._abort(error.ResponseError(_("unexpected response:"), d))
382 self._abort(error.ResponseError(_("unexpected response:"), d))
381
383
382 @batchable
384 @batchable
383 def listkeys(self, namespace):
385 def listkeys(self, namespace):
384 if not self.capable('pushkey'):
386 if not self.capable('pushkey'):
385 yield {}, None
387 yield {}, None
386 f = future()
388 f = future()
387 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
389 self.ui.debug('preparing listkeys for "%s"\n' % namespace)
388 yield {'namespace': encoding.fromlocal(namespace)}, f
390 yield {'namespace': encoding.fromlocal(namespace)}, f
389 d = f.value
391 d = f.value
390 self.ui.debug('received listkey for "%s": %i bytes\n'
392 self.ui.debug('received listkey for "%s": %i bytes\n'
391 % (namespace, len(d)))
393 % (namespace, len(d)))
392 yield pushkeymod.decodekeys(d)
394 yield pushkeymod.decodekeys(d)
393
395
394 @batchable
396 @batchable
395 def pushkey(self, namespace, key, old, new):
397 def pushkey(self, namespace, key, old, new):
396 if not self.capable('pushkey'):
398 if not self.capable('pushkey'):
397 yield False, None
399 yield False, None
398 f = future()
400 f = future()
399 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
401 self.ui.debug('preparing pushkey for "%s:%s"\n' % (namespace, key))
400 yield {'namespace': encoding.fromlocal(namespace),
402 yield {'namespace': encoding.fromlocal(namespace),
401 'key': encoding.fromlocal(key),
403 'key': encoding.fromlocal(key),
402 'old': encoding.fromlocal(old),
404 'old': encoding.fromlocal(old),
403 'new': encoding.fromlocal(new)}, f
405 'new': encoding.fromlocal(new)}, f
404 d = f.value
406 d = f.value
405 d, output = d.split('\n', 1)
407 d, output = d.split('\n', 1)
406 try:
408 try:
407 d = bool(int(d))
409 d = bool(int(d))
408 except ValueError:
410 except ValueError:
409 raise error.ResponseError(
411 raise error.ResponseError(
410 _('push failed (unexpected response):'), d)
412 _('push failed (unexpected response):'), d)
411 for l in output.splitlines(True):
413 for l in output.splitlines(True):
412 self.ui.status(_('remote: '), l)
414 self.ui.status(_('remote: '), l)
413 yield d
415 yield d
414
416
415 def stream_out(self):
417 def stream_out(self):
416 return self._callstream('stream_out')
418 return self._callstream('stream_out')
417
419
418 def getbundle(self, source, **kwargs):
420 def getbundle(self, source, **kwargs):
419 kwargs = pycompat.byteskwargs(kwargs)
421 kwargs = pycompat.byteskwargs(kwargs)
420 self.requirecap('getbundle', _('look up remote changes'))
422 self.requirecap('getbundle', _('look up remote changes'))
421 opts = {}
423 opts = {}
422 bundlecaps = kwargs.get('bundlecaps') or set()
424 bundlecaps = kwargs.get('bundlecaps') or set()
423 for key, value in kwargs.iteritems():
425 for key, value in kwargs.iteritems():
424 if value is None:
426 if value is None:
425 continue
427 continue
426 keytype = wireprototypes.GETBUNDLE_ARGUMENTS.get(key)
428 keytype = wireprototypes.GETBUNDLE_ARGUMENTS.get(key)
427 if keytype is None:
429 if keytype is None:
428 raise error.ProgrammingError(
430 raise error.ProgrammingError(
429 'Unexpectedly None keytype for key %s' % key)
431 'Unexpectedly None keytype for key %s' % key)
430 elif keytype == 'nodes':
432 elif keytype == 'nodes':
431 value = wireprototypes.encodelist(value)
433 value = wireprototypes.encodelist(value)
432 elif keytype == 'csv':
434 elif keytype == 'csv':
433 value = ','.join(value)
435 value = ','.join(value)
434 elif keytype == 'scsv':
436 elif keytype == 'scsv':
435 value = ','.join(sorted(value))
437 value = ','.join(sorted(value))
436 elif keytype == 'boolean':
438 elif keytype == 'boolean':
437 value = '%i' % bool(value)
439 value = '%i' % bool(value)
438 elif keytype != 'plain':
440 elif keytype != 'plain':
439 raise KeyError('unknown getbundle option type %s'
441 raise KeyError('unknown getbundle option type %s'
440 % keytype)
442 % keytype)
441 opts[key] = value
443 opts[key] = value
442 f = self._callcompressable("getbundle", **pycompat.strkwargs(opts))
444 f = self._callcompressable("getbundle", **pycompat.strkwargs(opts))
443 if any((cap.startswith('HG2') for cap in bundlecaps)):
445 if any((cap.startswith('HG2') for cap in bundlecaps)):
444 return bundle2.getunbundler(self.ui, f)
446 return bundle2.getunbundler(self.ui, f)
445 else:
447 else:
446 return changegroupmod.cg1unpacker(f, 'UN')
448 return changegroupmod.cg1unpacker(f, 'UN')
447
449
448 def unbundle(self, bundle, heads, url):
450 def unbundle(self, bundle, heads, url):
449 '''Send cg (a readable file-like object representing the
451 '''Send cg (a readable file-like object representing the
450 changegroup to push, typically a chunkbuffer object) to the
452 changegroup to push, typically a chunkbuffer object) to the
451 remote server as a bundle.
453 remote server as a bundle.
452
454
453 When pushing a bundle10 stream, return an integer indicating the
455 When pushing a bundle10 stream, return an integer indicating the
454 result of the push (see changegroup.apply()).
456 result of the push (see changegroup.apply()).
455
457
456 When pushing a bundle20 stream, return a bundle20 stream.
458 When pushing a bundle20 stream, return a bundle20 stream.
457
459
458 `url` is the url the client thinks it's pushing to, which is
460 `url` is the url the client thinks it's pushing to, which is
459 visible to hooks.
461 visible to hooks.
460 '''
462 '''
461
463
462 if heads != ['force'] and self.capable('unbundlehash'):
464 if heads != ['force'] and self.capable('unbundlehash'):
463 heads = wireprototypes.encodelist(
465 heads = wireprototypes.encodelist(
464 ['hashed', hashlib.sha1(''.join(sorted(heads))).digest()])
466 ['hashed', hashlib.sha1(''.join(sorted(heads))).digest()])
465 else:
467 else:
466 heads = wireprototypes.encodelist(heads)
468 heads = wireprototypes.encodelist(heads)
467
469
468 if util.safehasattr(bundle, 'deltaheader'):
470 if util.safehasattr(bundle, 'deltaheader'):
469 # this a bundle10, do the old style call sequence
471 # this a bundle10, do the old style call sequence
470 ret, output = self._callpush("unbundle", bundle, heads=heads)
472 ret, output = self._callpush("unbundle", bundle, heads=heads)
471 if ret == "":
473 if ret == "":
472 raise error.ResponseError(
474 raise error.ResponseError(
473 _('push failed:'), output)
475 _('push failed:'), output)
474 try:
476 try:
475 ret = int(ret)
477 ret = int(ret)
476 except ValueError:
478 except ValueError:
477 raise error.ResponseError(
479 raise error.ResponseError(
478 _('push failed (unexpected response):'), ret)
480 _('push failed (unexpected response):'), ret)
479
481
480 for l in output.splitlines(True):
482 for l in output.splitlines(True):
481 self.ui.status(_('remote: '), l)
483 self.ui.status(_('remote: '), l)
482 else:
484 else:
483 # bundle2 push. Send a stream, fetch a stream.
485 # bundle2 push. Send a stream, fetch a stream.
484 stream = self._calltwowaystream('unbundle', bundle, heads=heads)
486 stream = self._calltwowaystream('unbundle', bundle, heads=heads)
485 ret = bundle2.getunbundler(self.ui, stream)
487 ret = bundle2.getunbundler(self.ui, stream)
486 return ret
488 return ret
487
489
488 # End of ipeercommands interface.
490 # End of ipeercommands interface.
489
491
490 # Begin of ipeerlegacycommands interface.
492 # Begin of ipeerlegacycommands interface.
491
493
492 def branches(self, nodes):
494 def branches(self, nodes):
493 n = wireprototypes.encodelist(nodes)
495 n = wireprototypes.encodelist(nodes)
494 d = self._call("branches", nodes=n)
496 d = self._call("branches", nodes=n)
495 try:
497 try:
496 br = [tuple(wireprototypes.decodelist(b)) for b in d.splitlines()]
498 br = [tuple(wireprototypes.decodelist(b)) for b in d.splitlines()]
497 return br
499 return br
498 except ValueError:
500 except ValueError:
499 self._abort(error.ResponseError(_("unexpected response:"), d))
501 self._abort(error.ResponseError(_("unexpected response:"), d))
500
502
501 def between(self, pairs):
503 def between(self, pairs):
502 batch = 8 # avoid giant requests
504 batch = 8 # avoid giant requests
503 r = []
505 r = []
504 for i in pycompat.xrange(0, len(pairs), batch):
506 for i in pycompat.xrange(0, len(pairs), batch):
505 n = " ".join([wireprototypes.encodelist(p, '-')
507 n = " ".join([wireprototypes.encodelist(p, '-')
506 for p in pairs[i:i + batch]])
508 for p in pairs[i:i + batch]])
507 d = self._call("between", pairs=n)
509 d = self._call("between", pairs=n)
508 try:
510 try:
509 r.extend(l and wireprototypes.decodelist(l) or []
511 r.extend(l and wireprototypes.decodelist(l) or []
510 for l in d.splitlines())
512 for l in d.splitlines())
511 except ValueError:
513 except ValueError:
512 self._abort(error.ResponseError(_("unexpected response:"), d))
514 self._abort(error.ResponseError(_("unexpected response:"), d))
513 return r
515 return r
514
516
515 def changegroup(self, nodes, source):
517 def changegroup(self, nodes, source):
516 n = wireprototypes.encodelist(nodes)
518 n = wireprototypes.encodelist(nodes)
517 f = self._callcompressable("changegroup", roots=n)
519 f = self._callcompressable("changegroup", roots=n)
518 return changegroupmod.cg1unpacker(f, 'UN')
520 return changegroupmod.cg1unpacker(f, 'UN')
519
521
520 def changegroupsubset(self, bases, heads, source):
522 def changegroupsubset(self, bases, heads, source):
521 self.requirecap('changegroupsubset', _('look up remote changes'))
523 self.requirecap('changegroupsubset', _('look up remote changes'))
522 bases = wireprototypes.encodelist(bases)
524 bases = wireprototypes.encodelist(bases)
523 heads = wireprototypes.encodelist(heads)
525 heads = wireprototypes.encodelist(heads)
524 f = self._callcompressable("changegroupsubset",
526 f = self._callcompressable("changegroupsubset",
525 bases=bases, heads=heads)
527 bases=bases, heads=heads)
526 return changegroupmod.cg1unpacker(f, 'UN')
528 return changegroupmod.cg1unpacker(f, 'UN')
527
529
528 # End of ipeerlegacycommands interface.
530 # End of ipeerlegacycommands interface.
529
531
530 def _submitbatch(self, req):
532 def _submitbatch(self, req):
531 """run batch request <req> on the server
533 """run batch request <req> on the server
532
534
533 Returns an iterator of the raw responses from the server.
535 Returns an iterator of the raw responses from the server.
534 """
536 """
535 ui = self.ui
537 ui = self.ui
536 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
538 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
537 ui.debug('devel-peer-request: batched-content\n')
539 ui.debug('devel-peer-request: batched-content\n')
538 for op, args in req:
540 for op, args in req:
539 msg = 'devel-peer-request: - %s (%d arguments)\n'
541 msg = 'devel-peer-request: - %s (%d arguments)\n'
540 ui.debug(msg % (op, len(args)))
542 ui.debug(msg % (op, len(args)))
541
543
542 unescapearg = wireprototypes.unescapebatcharg
544 unescapearg = wireprototypes.unescapebatcharg
543
545
544 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
546 rsp = self._callstream("batch", cmds=encodebatchcmds(req))
545 chunk = rsp.read(1024)
547 chunk = rsp.read(1024)
546 work = [chunk]
548 work = [chunk]
547 while chunk:
549 while chunk:
548 while ';' not in chunk and chunk:
550 while ';' not in chunk and chunk:
549 chunk = rsp.read(1024)
551 chunk = rsp.read(1024)
550 work.append(chunk)
552 work.append(chunk)
551 merged = ''.join(work)
553 merged = ''.join(work)
552 while ';' in merged:
554 while ';' in merged:
553 one, merged = merged.split(';', 1)
555 one, merged = merged.split(';', 1)
554 yield unescapearg(one)
556 yield unescapearg(one)
555 chunk = rsp.read(1024)
557 chunk = rsp.read(1024)
556 work = [merged, chunk]
558 work = [merged, chunk]
557 yield unescapearg(''.join(work))
559 yield unescapearg(''.join(work))
558
560
559 def _submitone(self, op, args):
561 def _submitone(self, op, args):
560 return self._call(op, **pycompat.strkwargs(args))
562 return self._call(op, **pycompat.strkwargs(args))
561
563
562 def debugwireargs(self, one, two, three=None, four=None, five=None):
564 def debugwireargs(self, one, two, three=None, four=None, five=None):
563 # don't pass optional arguments left at their default value
565 # don't pass optional arguments left at their default value
564 opts = {}
566 opts = {}
565 if three is not None:
567 if three is not None:
566 opts[r'three'] = three
568 opts[r'three'] = three
567 if four is not None:
569 if four is not None:
568 opts[r'four'] = four
570 opts[r'four'] = four
569 return self._call('debugwireargs', one=one, two=two, **opts)
571 return self._call('debugwireargs', one=one, two=two, **opts)
570
572
571 def _call(self, cmd, **args):
573 def _call(self, cmd, **args):
572 """execute <cmd> on the server
574 """execute <cmd> on the server
573
575
574 The command is expected to return a simple string.
576 The command is expected to return a simple string.
575
577
576 returns the server reply as a string."""
578 returns the server reply as a string."""
577 raise NotImplementedError()
579 raise NotImplementedError()
578
580
579 def _callstream(self, cmd, **args):
581 def _callstream(self, cmd, **args):
580 """execute <cmd> on the server
582 """execute <cmd> on the server
581
583
582 The command is expected to return a stream. Note that if the
584 The command is expected to return a stream. Note that if the
583 command doesn't return a stream, _callstream behaves
585 command doesn't return a stream, _callstream behaves
584 differently for ssh and http peers.
586 differently for ssh and http peers.
585
587
586 returns the server reply as a file like object.
588 returns the server reply as a file like object.
587 """
589 """
588 raise NotImplementedError()
590 raise NotImplementedError()
589
591
590 def _callcompressable(self, cmd, **args):
592 def _callcompressable(self, cmd, **args):
591 """execute <cmd> on the server
593 """execute <cmd> on the server
592
594
593 The command is expected to return a stream.
595 The command is expected to return a stream.
594
596
595 The stream may have been compressed in some implementations. This
597 The stream may have been compressed in some implementations. This
596 function takes care of the decompression. This is the only difference
598 function takes care of the decompression. This is the only difference
597 with _callstream.
599 with _callstream.
598
600
599 returns the server reply as a file like object.
601 returns the server reply as a file like object.
600 """
602 """
601 raise NotImplementedError()
603 raise NotImplementedError()
602
604
603 def _callpush(self, cmd, fp, **args):
605 def _callpush(self, cmd, fp, **args):
604 """execute a <cmd> on server
606 """execute a <cmd> on server
605
607
606 The command is expected to be related to a push. Push has a special
608 The command is expected to be related to a push. Push has a special
607 return method.
609 return method.
608
610
609 returns the server reply as a (ret, output) tuple. ret is either
611 returns the server reply as a (ret, output) tuple. ret is either
610 empty (error) or a stringified int.
612 empty (error) or a stringified int.
611 """
613 """
612 raise NotImplementedError()
614 raise NotImplementedError()
613
615
614 def _calltwowaystream(self, cmd, fp, **args):
616 def _calltwowaystream(self, cmd, fp, **args):
615 """execute <cmd> on server
617 """execute <cmd> on server
616
618
617 The command will send a stream to the server and get a stream in reply.
619 The command will send a stream to the server and get a stream in reply.
618 """
620 """
619 raise NotImplementedError()
621 raise NotImplementedError()
620
622
621 def _abort(self, exception):
623 def _abort(self, exception):
622 """clearly abort the wire protocol connection and raise the exception
624 """clearly abort the wire protocol connection and raise the exception
623 """
625 """
624 raise NotImplementedError()
626 raise NotImplementedError()
@@ -1,1512 +1,1513
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
6
7 import os
7 import os
8
8
9 supportedpy = '~= 2.7'
9 supportedpy = '~= 2.7'
10 if os.environ.get('HGALLOWPYTHON3', ''):
10 if os.environ.get('HGALLOWPYTHON3', ''):
11 # Mercurial will never work on Python 3 before 3.5 due to a lack
11 # Mercurial will never work on Python 3 before 3.5 due to a lack
12 # of % formatting on bytestrings, and can't work on 3.6.0 or 3.6.1
12 # of % formatting on bytestrings, and can't work on 3.6.0 or 3.6.1
13 # due to a bug in % formatting in bytestrings.
13 # due to a bug in % formatting in bytestrings.
14 # We cannot support Python 3.5.0, 3.5.1, 3.5.2 because of bug in
14 # We cannot support Python 3.5.0, 3.5.1, 3.5.2 because of bug in
15 # codecs.escape_encode() where it raises SystemError on empty bytestring
15 # codecs.escape_encode() where it raises SystemError on empty bytestring
16 # bug link: https://bugs.python.org/issue25270
16 # bug link: https://bugs.python.org/issue25270
17 #
17 #
18 # TODO: when we actually work on Python 3, use this string as the
18 # TODO: when we actually work on Python 3, use this string as the
19 # actual supportedpy string.
19 # actual supportedpy string.
20 supportedpy = ','.join([
20 supportedpy = ','.join([
21 '>=2.7',
21 '>=2.7',
22 '!=3.0.*',
22 '!=3.0.*',
23 '!=3.1.*',
23 '!=3.1.*',
24 '!=3.2.*',
24 '!=3.2.*',
25 '!=3.3.*',
25 '!=3.3.*',
26 '!=3.4.*',
26 '!=3.4.*',
27 '!=3.5.0',
27 '!=3.5.0',
28 '!=3.5.1',
28 '!=3.5.1',
29 '!=3.5.2',
29 '!=3.5.2',
30 '!=3.6.0',
30 '!=3.6.0',
31 '!=3.6.1',
31 '!=3.6.1',
32 ])
32 ])
33
33
34 import sys, platform
34 import sys, platform
35 import sysconfig
35 import sysconfig
36 if sys.version_info[0] >= 3:
36 if sys.version_info[0] >= 3:
37 printf = eval('print')
37 printf = eval('print')
38 libdir_escape = 'unicode_escape'
38 libdir_escape = 'unicode_escape'
39 def sysstr(s):
39 def sysstr(s):
40 return s.decode('latin-1')
40 return s.decode('latin-1')
41 else:
41 else:
42 libdir_escape = 'string_escape'
42 libdir_escape = 'string_escape'
43 def printf(*args, **kwargs):
43 def printf(*args, **kwargs):
44 f = kwargs.get('file', sys.stdout)
44 f = kwargs.get('file', sys.stdout)
45 end = kwargs.get('end', '\n')
45 end = kwargs.get('end', '\n')
46 f.write(b' '.join(args) + end)
46 f.write(b' '.join(args) + end)
47 def sysstr(s):
47 def sysstr(s):
48 return s
48 return s
49
49
50 # Attempt to guide users to a modern pip - this means that 2.6 users
50 # Attempt to guide users to a modern pip - this means that 2.6 users
51 # should have a chance of getting a 4.2 release, and when we ratchet
51 # should have a chance of getting a 4.2 release, and when we ratchet
52 # the version requirement forward again hopefully everyone will get
52 # the version requirement forward again hopefully everyone will get
53 # something that works for them.
53 # something that works for them.
54 if sys.version_info < (2, 7, 0, 'final'):
54 if sys.version_info < (2, 7, 0, 'final'):
55 pip_message = ('This may be due to an out of date pip. '
55 pip_message = ('This may be due to an out of date pip. '
56 'Make sure you have pip >= 9.0.1.')
56 'Make sure you have pip >= 9.0.1.')
57 try:
57 try:
58 import pip
58 import pip
59 pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
59 pip_version = tuple([int(x) for x in pip.__version__.split('.')[:3]])
60 if pip_version < (9, 0, 1) :
60 if pip_version < (9, 0, 1) :
61 pip_message = (
61 pip_message = (
62 'Your pip version is out of date, please install '
62 'Your pip version is out of date, please install '
63 'pip >= 9.0.1. pip {} detected.'.format(pip.__version__))
63 'pip >= 9.0.1. pip {} detected.'.format(pip.__version__))
64 else:
64 else:
65 # pip is new enough - it must be something else
65 # pip is new enough - it must be something else
66 pip_message = ''
66 pip_message = ''
67 except Exception:
67 except Exception:
68 pass
68 pass
69 error = """
69 error = """
70 Mercurial does not support Python older than 2.7.
70 Mercurial does not support Python older than 2.7.
71 Python {py} detected.
71 Python {py} detected.
72 {pip}
72 {pip}
73 """.format(py=sys.version_info, pip=pip_message)
73 """.format(py=sys.version_info, pip=pip_message)
74 printf(error, file=sys.stderr)
74 printf(error, file=sys.stderr)
75 sys.exit(1)
75 sys.exit(1)
76
76
77 # We don't yet officially support Python 3. But we want to allow developers to
77 # We don't yet officially support Python 3. But we want to allow developers to
78 # hack on. Detect and disallow running on Python 3 by default. But provide a
78 # hack on. Detect and disallow running on Python 3 by default. But provide a
79 # backdoor to enable working on Python 3.
79 # backdoor to enable working on Python 3.
80 if sys.version_info[0] != 2:
80 if sys.version_info[0] != 2:
81 badpython = True
81 badpython = True
82
82
83 # Allow Python 3 from source checkouts.
83 # Allow Python 3 from source checkouts.
84 if os.path.isdir('.hg') or 'HGPYTHON3' in os.environ:
84 if os.path.isdir('.hg') or 'HGPYTHON3' in os.environ:
85 badpython = False
85 badpython = False
86
86
87 if badpython:
87 if badpython:
88 error = """
88 error = """
89 Python {py} detected.
89 Python {py} detected.
90
90
91 Mercurial currently has beta support for Python 3 and use of Python 2.7 is
91 Mercurial currently has beta support for Python 3 and use of Python 2.7 is
92 recommended for the best experience.
92 recommended for the best experience.
93
93
94 Please re-run with Python 2.7 for a faster, less buggy experience.
94 Please re-run with Python 2.7 for a faster, less buggy experience.
95
95
96 If you would like to beta test Mercurial with Python 3, this error can
96 If you would like to beta test Mercurial with Python 3, this error can
97 be suppressed by defining the HGPYTHON3 environment variable when invoking
97 be suppressed by defining the HGPYTHON3 environment variable when invoking
98 this command. No special environment variables or configuration changes are
98 this command. No special environment variables or configuration changes are
99 necessary to run `hg` with Python 3.
99 necessary to run `hg` with Python 3.
100
100
101 See https://www.mercurial-scm.org/wiki/Python3 for more on Mercurial's
101 See https://www.mercurial-scm.org/wiki/Python3 for more on Mercurial's
102 Python 3 support.
102 Python 3 support.
103 """.format(py='.'.join('%d' % x for x in sys.version_info[0:2]))
103 """.format(py='.'.join('%d' % x for x in sys.version_info[0:2]))
104
104
105 printf(error, file=sys.stderr)
105 printf(error, file=sys.stderr)
106 sys.exit(1)
106 sys.exit(1)
107
107
108 if sys.version_info[0] >= 3:
108 if sys.version_info[0] >= 3:
109 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
109 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
110 else:
110 else:
111 # deprecated in Python 3
111 # deprecated in Python 3
112 DYLIB_SUFFIX = sysconfig.get_config_vars()['SO']
112 DYLIB_SUFFIX = sysconfig.get_config_vars()['SO']
113
113
114 # Solaris Python packaging brain damage
114 # Solaris Python packaging brain damage
115 try:
115 try:
116 import hashlib
116 import hashlib
117 sha = hashlib.sha1()
117 sha = hashlib.sha1()
118 except ImportError:
118 except ImportError:
119 try:
119 try:
120 import sha
120 import sha
121 sha.sha # silence unused import warning
121 sha.sha # silence unused import warning
122 except ImportError:
122 except ImportError:
123 raise SystemExit(
123 raise SystemExit(
124 "Couldn't import standard hashlib (incomplete Python install).")
124 "Couldn't import standard hashlib (incomplete Python install).")
125
125
126 try:
126 try:
127 import zlib
127 import zlib
128 zlib.compressobj # silence unused import warning
128 zlib.compressobj # silence unused import warning
129 except ImportError:
129 except ImportError:
130 raise SystemExit(
130 raise SystemExit(
131 "Couldn't import standard zlib (incomplete Python install).")
131 "Couldn't import standard zlib (incomplete Python install).")
132
132
133 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
133 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
134 isironpython = False
134 isironpython = False
135 try:
135 try:
136 isironpython = (platform.python_implementation()
136 isironpython = (platform.python_implementation()
137 .lower().find("ironpython") != -1)
137 .lower().find("ironpython") != -1)
138 except AttributeError:
138 except AttributeError:
139 pass
139 pass
140
140
141 if isironpython:
141 if isironpython:
142 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
142 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
143 else:
143 else:
144 try:
144 try:
145 import bz2
145 import bz2
146 bz2.BZ2Compressor # silence unused import warning
146 bz2.BZ2Compressor # silence unused import warning
147 except ImportError:
147 except ImportError:
148 raise SystemExit(
148 raise SystemExit(
149 "Couldn't import standard bz2 (incomplete Python install).")
149 "Couldn't import standard bz2 (incomplete Python install).")
150
150
151 ispypy = "PyPy" in sys.version
151 ispypy = "PyPy" in sys.version
152
152
153 hgrustext = os.environ.get('HGWITHRUSTEXT')
153 hgrustext = os.environ.get('HGWITHRUSTEXT')
154 # TODO record it for proper rebuild upon changes
154 # TODO record it for proper rebuild upon changes
155 # (see mercurial/__modulepolicy__.py)
155 # (see mercurial/__modulepolicy__.py)
156 if hgrustext != 'cpython' and hgrustext is not None:
156 if hgrustext != 'cpython' and hgrustext is not None:
157 hgrustext = 'direct-ffi'
157 hgrustext = 'direct-ffi'
158
158
159 import ctypes
159 import ctypes
160 import errno
160 import errno
161 import stat, subprocess, time
161 import stat, subprocess, time
162 import re
162 import re
163 import shutil
163 import shutil
164 import tempfile
164 import tempfile
165 from distutils import log
165 from distutils import log
166 # We have issues with setuptools on some platforms and builders. Until
166 # We have issues with setuptools on some platforms and builders. Until
167 # those are resolved, setuptools is opt-in except for platforms where
167 # those are resolved, setuptools is opt-in except for platforms where
168 # we don't have issues.
168 # we don't have issues.
169 issetuptools = (os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ)
169 issetuptools = (os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ)
170 if issetuptools:
170 if issetuptools:
171 from setuptools import setup
171 from setuptools import setup
172 else:
172 else:
173 from distutils.core import setup
173 from distutils.core import setup
174 from distutils.ccompiler import new_compiler
174 from distutils.ccompiler import new_compiler
175 from distutils.core import Command, Extension
175 from distutils.core import Command, Extension
176 from distutils.dist import Distribution
176 from distutils.dist import Distribution
177 from distutils.command.build import build
177 from distutils.command.build import build
178 from distutils.command.build_ext import build_ext
178 from distutils.command.build_ext import build_ext
179 from distutils.command.build_py import build_py
179 from distutils.command.build_py import build_py
180 from distutils.command.build_scripts import build_scripts
180 from distutils.command.build_scripts import build_scripts
181 from distutils.command.install import install
181 from distutils.command.install import install
182 from distutils.command.install_lib import install_lib
182 from distutils.command.install_lib import install_lib
183 from distutils.command.install_scripts import install_scripts
183 from distutils.command.install_scripts import install_scripts
184 from distutils.spawn import spawn, find_executable
184 from distutils.spawn import spawn, find_executable
185 from distutils import file_util
185 from distutils import file_util
186 from distutils.errors import (
186 from distutils.errors import (
187 CCompilerError,
187 CCompilerError,
188 DistutilsError,
188 DistutilsError,
189 DistutilsExecError,
189 DistutilsExecError,
190 )
190 )
191 from distutils.sysconfig import get_python_inc, get_config_var
191 from distutils.sysconfig import get_python_inc, get_config_var
192 from distutils.version import StrictVersion
192 from distutils.version import StrictVersion
193
193
194 # Explain to distutils.StrictVersion how our release candidates are versionned
194 # Explain to distutils.StrictVersion how our release candidates are versionned
195 StrictVersion.version_re = re.compile(r'^(\d+)\.(\d+)(\.(\d+))?-?(rc(\d+))?$')
195 StrictVersion.version_re = re.compile(r'^(\d+)\.(\d+)(\.(\d+))?-?(rc(\d+))?$')
196
196
197 def write_if_changed(path, content):
197 def write_if_changed(path, content):
198 """Write content to a file iff the content hasn't changed."""
198 """Write content to a file iff the content hasn't changed."""
199 if os.path.exists(path):
199 if os.path.exists(path):
200 with open(path, 'rb') as fh:
200 with open(path, 'rb') as fh:
201 current = fh.read()
201 current = fh.read()
202 else:
202 else:
203 current = b''
203 current = b''
204
204
205 if current != content:
205 if current != content:
206 with open(path, 'wb') as fh:
206 with open(path, 'wb') as fh:
207 fh.write(content)
207 fh.write(content)
208
208
209 scripts = ['hg']
209 scripts = ['hg']
210 if os.name == 'nt':
210 if os.name == 'nt':
211 # We remove hg.bat if we are able to build hg.exe.
211 # We remove hg.bat if we are able to build hg.exe.
212 scripts.append('contrib/win32/hg.bat')
212 scripts.append('contrib/win32/hg.bat')
213
213
214 def cancompile(cc, code):
214 def cancompile(cc, code):
215 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
215 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
216 devnull = oldstderr = None
216 devnull = oldstderr = None
217 try:
217 try:
218 fname = os.path.join(tmpdir, 'testcomp.c')
218 fname = os.path.join(tmpdir, 'testcomp.c')
219 f = open(fname, 'w')
219 f = open(fname, 'w')
220 f.write(code)
220 f.write(code)
221 f.close()
221 f.close()
222 # Redirect stderr to /dev/null to hide any error messages
222 # Redirect stderr to /dev/null to hide any error messages
223 # from the compiler.
223 # from the compiler.
224 # This will have to be changed if we ever have to check
224 # This will have to be changed if we ever have to check
225 # for a function on Windows.
225 # for a function on Windows.
226 devnull = open('/dev/null', 'w')
226 devnull = open('/dev/null', 'w')
227 oldstderr = os.dup(sys.stderr.fileno())
227 oldstderr = os.dup(sys.stderr.fileno())
228 os.dup2(devnull.fileno(), sys.stderr.fileno())
228 os.dup2(devnull.fileno(), sys.stderr.fileno())
229 objects = cc.compile([fname], output_dir=tmpdir)
229 objects = cc.compile([fname], output_dir=tmpdir)
230 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
230 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
231 return True
231 return True
232 except Exception:
232 except Exception:
233 return False
233 return False
234 finally:
234 finally:
235 if oldstderr is not None:
235 if oldstderr is not None:
236 os.dup2(oldstderr, sys.stderr.fileno())
236 os.dup2(oldstderr, sys.stderr.fileno())
237 if devnull is not None:
237 if devnull is not None:
238 devnull.close()
238 devnull.close()
239 shutil.rmtree(tmpdir)
239 shutil.rmtree(tmpdir)
240
240
241 # simplified version of distutils.ccompiler.CCompiler.has_function
241 # simplified version of distutils.ccompiler.CCompiler.has_function
242 # that actually removes its temporary files.
242 # that actually removes its temporary files.
243 def hasfunction(cc, funcname):
243 def hasfunction(cc, funcname):
244 code = 'int main(void) { %s(); }\n' % funcname
244 code = 'int main(void) { %s(); }\n' % funcname
245 return cancompile(cc, code)
245 return cancompile(cc, code)
246
246
247 def hasheader(cc, headername):
247 def hasheader(cc, headername):
248 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
248 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
249 return cancompile(cc, code)
249 return cancompile(cc, code)
250
250
251 # py2exe needs to be installed to work
251 # py2exe needs to be installed to work
252 try:
252 try:
253 import py2exe
253 import py2exe
254 py2exe.Distribution # silence unused import warning
254 py2exe.Distribution # silence unused import warning
255 py2exeloaded = True
255 py2exeloaded = True
256 # import py2exe's patched Distribution class
256 # import py2exe's patched Distribution class
257 from distutils.core import Distribution
257 from distutils.core import Distribution
258 except ImportError:
258 except ImportError:
259 py2exeloaded = False
259 py2exeloaded = False
260
260
261 def runcmd(cmd, env, cwd=None):
261 def runcmd(cmd, env, cwd=None):
262 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
262 p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
263 stderr=subprocess.PIPE, env=env, cwd=cwd)
263 stderr=subprocess.PIPE, env=env, cwd=cwd)
264 out, err = p.communicate()
264 out, err = p.communicate()
265 return p.returncode, out, err
265 return p.returncode, out, err
266
266
267 class hgcommand(object):
267 class hgcommand(object):
268 def __init__(self, cmd, env):
268 def __init__(self, cmd, env):
269 self.cmd = cmd
269 self.cmd = cmd
270 self.env = env
270 self.env = env
271
271
272 def run(self, args):
272 def run(self, args):
273 cmd = self.cmd + args
273 cmd = self.cmd + args
274 returncode, out, err = runcmd(cmd, self.env)
274 returncode, out, err = runcmd(cmd, self.env)
275 err = filterhgerr(err)
275 err = filterhgerr(err)
276 if err or returncode != 0:
276 if err or returncode != 0:
277 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
277 printf("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
278 printf(err, file=sys.stderr)
278 printf(err, file=sys.stderr)
279 return ''
279 return ''
280 return out
280 return out
281
281
282 def filterhgerr(err):
282 def filterhgerr(err):
283 # If root is executing setup.py, but the repository is owned by
283 # If root is executing setup.py, but the repository is owned by
284 # another user (as in "sudo python setup.py install") we will get
284 # another user (as in "sudo python setup.py install") we will get
285 # trust warnings since the .hg/hgrc file is untrusted. That is
285 # trust warnings since the .hg/hgrc file is untrusted. That is
286 # fine, we don't want to load it anyway. Python may warn about
286 # fine, we don't want to load it anyway. Python may warn about
287 # a missing __init__.py in mercurial/locale, we also ignore that.
287 # a missing __init__.py in mercurial/locale, we also ignore that.
288 err = [e for e in err.splitlines()
288 err = [e for e in err.splitlines()
289 if (not e.startswith(b'not trusting file')
289 if (not e.startswith(b'not trusting file')
290 and not e.startswith(b'warning: Not importing')
290 and not e.startswith(b'warning: Not importing')
291 and not e.startswith(b'obsolete feature not enabled')
291 and not e.startswith(b'obsolete feature not enabled')
292 and not e.startswith(b'*** failed to import extension')
292 and not e.startswith(b'*** failed to import extension')
293 and not e.startswith(b'devel-warn:')
293 and not e.startswith(b'devel-warn:')
294 and not (e.startswith(b'(third party extension')
294 and not (e.startswith(b'(third party extension')
295 and e.endswith(b'or newer of Mercurial; disabling)')))]
295 and e.endswith(b'or newer of Mercurial; disabling)')))]
296 return b'\n'.join(b' ' + e for e in err)
296 return b'\n'.join(b' ' + e for e in err)
297
297
298 def findhg():
298 def findhg():
299 """Try to figure out how we should invoke hg for examining the local
299 """Try to figure out how we should invoke hg for examining the local
300 repository contents.
300 repository contents.
301
301
302 Returns an hgcommand object."""
302 Returns an hgcommand object."""
303 # By default, prefer the "hg" command in the user's path. This was
303 # By default, prefer the "hg" command in the user's path. This was
304 # presumably the hg command that the user used to create this repository.
304 # presumably the hg command that the user used to create this repository.
305 #
305 #
306 # This repository may require extensions or other settings that would not
306 # This repository may require extensions or other settings that would not
307 # be enabled by running the hg script directly from this local repository.
307 # be enabled by running the hg script directly from this local repository.
308 hgenv = os.environ.copy()
308 hgenv = os.environ.copy()
309 # Use HGPLAIN to disable hgrc settings that would change output formatting,
309 # Use HGPLAIN to disable hgrc settings that would change output formatting,
310 # and disable localization for the same reasons.
310 # and disable localization for the same reasons.
311 hgenv['HGPLAIN'] = '1'
311 hgenv['HGPLAIN'] = '1'
312 hgenv['LANGUAGE'] = 'C'
312 hgenv['LANGUAGE'] = 'C'
313 hgcmd = ['hg']
313 hgcmd = ['hg']
314 # Run a simple "hg log" command just to see if using hg from the user's
314 # Run a simple "hg log" command just to see if using hg from the user's
315 # path works and can successfully interact with this repository. Windows
315 # path works and can successfully interact with this repository. Windows
316 # gives precedence to hg.exe in the current directory, so fall back to the
316 # gives precedence to hg.exe in the current directory, so fall back to the
317 # python invocation of local hg, where pythonXY.dll can always be found.
317 # python invocation of local hg, where pythonXY.dll can always be found.
318 check_cmd = ['log', '-r.', '-Ttest']
318 check_cmd = ['log', '-r.', '-Ttest']
319 if os.name != 'nt':
319 if os.name != 'nt':
320 try:
320 try:
321 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
321 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
322 except EnvironmentError:
322 except EnvironmentError:
323 retcode = -1
323 retcode = -1
324 if retcode == 0 and not filterhgerr(err):
324 if retcode == 0 and not filterhgerr(err):
325 return hgcommand(hgcmd, hgenv)
325 return hgcommand(hgcmd, hgenv)
326
326
327 # Fall back to trying the local hg installation.
327 # Fall back to trying the local hg installation.
328 hgenv = localhgenv()
328 hgenv = localhgenv()
329 hgcmd = [sys.executable, 'hg']
329 hgcmd = [sys.executable, 'hg']
330 try:
330 try:
331 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
331 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
332 except EnvironmentError:
332 except EnvironmentError:
333 retcode = -1
333 retcode = -1
334 if retcode == 0 and not filterhgerr(err):
334 if retcode == 0 and not filterhgerr(err):
335 return hgcommand(hgcmd, hgenv)
335 return hgcommand(hgcmd, hgenv)
336
336
337 raise SystemExit('Unable to find a working hg binary to extract the '
337 raise SystemExit('Unable to find a working hg binary to extract the '
338 'version from the repository tags')
338 'version from the repository tags')
339
339
340 def localhgenv():
340 def localhgenv():
341 """Get an environment dictionary to use for invoking or importing
341 """Get an environment dictionary to use for invoking or importing
342 mercurial from the local repository."""
342 mercurial from the local repository."""
343 # Execute hg out of this directory with a custom environment which takes
343 # Execute hg out of this directory with a custom environment which takes
344 # care to not use any hgrc files and do no localization.
344 # care to not use any hgrc files and do no localization.
345 env = {'HGMODULEPOLICY': 'py',
345 env = {'HGMODULEPOLICY': 'py',
346 'HGRCPATH': '',
346 'HGRCPATH': '',
347 'LANGUAGE': 'C',
347 'LANGUAGE': 'C',
348 'PATH': ''} # make pypi modules that use os.environ['PATH'] happy
348 'PATH': ''} # make pypi modules that use os.environ['PATH'] happy
349 if 'LD_LIBRARY_PATH' in os.environ:
349 if 'LD_LIBRARY_PATH' in os.environ:
350 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
350 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
351 if 'SystemRoot' in os.environ:
351 if 'SystemRoot' in os.environ:
352 # SystemRoot is required by Windows to load various DLLs. See:
352 # SystemRoot is required by Windows to load various DLLs. See:
353 # https://bugs.python.org/issue13524#msg148850
353 # https://bugs.python.org/issue13524#msg148850
354 env['SystemRoot'] = os.environ['SystemRoot']
354 env['SystemRoot'] = os.environ['SystemRoot']
355 return env
355 return env
356
356
357 version = ''
357 version = ''
358
358
359 if os.path.isdir('.hg'):
359 if os.path.isdir('.hg'):
360 hg = findhg()
360 hg = findhg()
361 cmd = ['log', '-r', '.', '--template', '{tags}\n']
361 cmd = ['log', '-r', '.', '--template', '{tags}\n']
362 numerictags = [t for t in sysstr(hg.run(cmd)).split() if t[0:1].isdigit()]
362 numerictags = [t for t in sysstr(hg.run(cmd)).split() if t[0:1].isdigit()]
363 hgid = sysstr(hg.run(['id', '-i'])).strip()
363 hgid = sysstr(hg.run(['id', '-i'])).strip()
364 if not hgid:
364 if not hgid:
365 # Bail out if hg is having problems interacting with this repository,
365 # Bail out if hg is having problems interacting with this repository,
366 # rather than falling through and producing a bogus version number.
366 # rather than falling through and producing a bogus version number.
367 # Continuing with an invalid version number will break extensions
367 # Continuing with an invalid version number will break extensions
368 # that define minimumhgversion.
368 # that define minimumhgversion.
369 raise SystemExit('Unable to determine hg version from local repository')
369 raise SystemExit('Unable to determine hg version from local repository')
370 if numerictags: # tag(s) found
370 if numerictags: # tag(s) found
371 version = numerictags[-1]
371 version = numerictags[-1]
372 if hgid.endswith('+'): # propagate the dirty status to the tag
372 if hgid.endswith('+'): # propagate the dirty status to the tag
373 version += '+'
373 version += '+'
374 else: # no tag found
374 else: # no tag found
375 ltagcmd = ['parents', '--template', '{latesttag}']
375 ltagcmd = ['parents', '--template', '{latesttag}']
376 ltag = sysstr(hg.run(ltagcmd))
376 ltag = sysstr(hg.run(ltagcmd))
377 changessincecmd = ['log', '-T', 'x\n', '-r', "only(.,'%s')" % ltag]
377 changessincecmd = ['log', '-T', 'x\n', '-r', "only(.,'%s')" % ltag]
378 changessince = len(hg.run(changessincecmd).splitlines())
378 changessince = len(hg.run(changessincecmd).splitlines())
379 version = '%s+%s-%s' % (ltag, changessince, hgid)
379 version = '%s+%s-%s' % (ltag, changessince, hgid)
380 if version.endswith('+'):
380 if version.endswith('+'):
381 version += time.strftime('%Y%m%d')
381 version += time.strftime('%Y%m%d')
382 elif os.path.exists('.hg_archival.txt'):
382 elif os.path.exists('.hg_archival.txt'):
383 kw = dict([[t.strip() for t in l.split(':', 1)]
383 kw = dict([[t.strip() for t in l.split(':', 1)]
384 for l in open('.hg_archival.txt')])
384 for l in open('.hg_archival.txt')])
385 if 'tag' in kw:
385 if 'tag' in kw:
386 version = kw['tag']
386 version = kw['tag']
387 elif 'latesttag' in kw:
387 elif 'latesttag' in kw:
388 if 'changessincelatesttag' in kw:
388 if 'changessincelatesttag' in kw:
389 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
389 version = '%(latesttag)s+%(changessincelatesttag)s-%(node).12s' % kw
390 else:
390 else:
391 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
391 version = '%(latesttag)s+%(latesttagdistance)s-%(node).12s' % kw
392 else:
392 else:
393 version = kw.get('node', '')[:12]
393 version = kw.get('node', '')[:12]
394
394
395 if version:
395 if version:
396 versionb = version
396 versionb = version
397 if not isinstance(versionb, bytes):
397 if not isinstance(versionb, bytes):
398 versionb = versionb.encode('ascii')
398 versionb = versionb.encode('ascii')
399
399
400 write_if_changed('mercurial/__version__.py', b''.join([
400 write_if_changed('mercurial/__version__.py', b''.join([
401 b'# this file is autogenerated by setup.py\n'
401 b'# this file is autogenerated by setup.py\n'
402 b'version = b"%s"\n' % versionb,
402 b'version = b"%s"\n' % versionb,
403 ]))
403 ]))
404
404
405 try:
405 try:
406 oldpolicy = os.environ.get('HGMODULEPOLICY', None)
406 oldpolicy = os.environ.get('HGMODULEPOLICY', None)
407 os.environ['HGMODULEPOLICY'] = 'py'
407 os.environ['HGMODULEPOLICY'] = 'py'
408 from mercurial import __version__
408 from mercurial import __version__
409 version = __version__.version
409 version = __version__.version
410 except ImportError:
410 except ImportError:
411 version = b'unknown'
411 version = b'unknown'
412 finally:
412 finally:
413 if oldpolicy is None:
413 if oldpolicy is None:
414 del os.environ['HGMODULEPOLICY']
414 del os.environ['HGMODULEPOLICY']
415 else:
415 else:
416 os.environ['HGMODULEPOLICY'] = oldpolicy
416 os.environ['HGMODULEPOLICY'] = oldpolicy
417
417
418 class hgbuild(build):
418 class hgbuild(build):
419 # Insert hgbuildmo first so that files in mercurial/locale/ are found
419 # Insert hgbuildmo first so that files in mercurial/locale/ are found
420 # when build_py is run next.
420 # when build_py is run next.
421 sub_commands = [('build_mo', None)] + build.sub_commands
421 sub_commands = [('build_mo', None)] + build.sub_commands
422
422
423 class hgbuildmo(build):
423 class hgbuildmo(build):
424
424
425 description = "build translations (.mo files)"
425 description = "build translations (.mo files)"
426
426
427 def run(self):
427 def run(self):
428 if not find_executable('msgfmt'):
428 if not find_executable('msgfmt'):
429 self.warn("could not find msgfmt executable, no translations "
429 self.warn("could not find msgfmt executable, no translations "
430 "will be built")
430 "will be built")
431 return
431 return
432
432
433 podir = 'i18n'
433 podir = 'i18n'
434 if not os.path.isdir(podir):
434 if not os.path.isdir(podir):
435 self.warn("could not find %s/ directory" % podir)
435 self.warn("could not find %s/ directory" % podir)
436 return
436 return
437
437
438 join = os.path.join
438 join = os.path.join
439 for po in os.listdir(podir):
439 for po in os.listdir(podir):
440 if not po.endswith('.po'):
440 if not po.endswith('.po'):
441 continue
441 continue
442 pofile = join(podir, po)
442 pofile = join(podir, po)
443 modir = join('locale', po[:-3], 'LC_MESSAGES')
443 modir = join('locale', po[:-3], 'LC_MESSAGES')
444 mofile = join(modir, 'hg.mo')
444 mofile = join(modir, 'hg.mo')
445 mobuildfile = join('mercurial', mofile)
445 mobuildfile = join('mercurial', mofile)
446 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
446 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
447 if sys.platform != 'sunos5':
447 if sys.platform != 'sunos5':
448 # msgfmt on Solaris does not know about -c
448 # msgfmt on Solaris does not know about -c
449 cmd.append('-c')
449 cmd.append('-c')
450 self.mkpath(join('mercurial', modir))
450 self.mkpath(join('mercurial', modir))
451 self.make_file([pofile], mobuildfile, spawn, (cmd,))
451 self.make_file([pofile], mobuildfile, spawn, (cmd,))
452
452
453
453
454 class hgdist(Distribution):
454 class hgdist(Distribution):
455 pure = False
455 pure = False
456 rust = hgrustext is not None
456 rust = hgrustext is not None
457 cffi = ispypy
457 cffi = ispypy
458
458
459 global_options = Distribution.global_options + [
459 global_options = Distribution.global_options + [
460 ('pure', None, "use pure (slow) Python code instead of C extensions"),
460 ('pure', None, "use pure (slow) Python code instead of C extensions"),
461 ('rust', None, "use Rust extensions additionally to C extensions"),
461 ('rust', None, "use Rust extensions additionally to C extensions"),
462 ]
462 ]
463
463
464 def has_ext_modules(self):
464 def has_ext_modules(self):
465 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
465 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
466 # too late for some cases
466 # too late for some cases
467 return not self.pure and Distribution.has_ext_modules(self)
467 return not self.pure and Distribution.has_ext_modules(self)
468
468
469 # This is ugly as a one-liner. So use a variable.
469 # This is ugly as a one-liner. So use a variable.
470 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
470 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
471 buildextnegops['no-zstd'] = 'zstd'
471 buildextnegops['no-zstd'] = 'zstd'
472 buildextnegops['no-rust'] = 'rust'
472 buildextnegops['no-rust'] = 'rust'
473
473
474 class hgbuildext(build_ext):
474 class hgbuildext(build_ext):
475 user_options = build_ext.user_options + [
475 user_options = build_ext.user_options + [
476 ('zstd', None, 'compile zstd bindings [default]'),
476 ('zstd', None, 'compile zstd bindings [default]'),
477 ('no-zstd', None, 'do not compile zstd bindings'),
477 ('no-zstd', None, 'do not compile zstd bindings'),
478 ('rust', None,
478 ('rust', None,
479 'compile Rust extensions if they are in use '
479 'compile Rust extensions if they are in use '
480 '(requires Cargo) [default]'),
480 '(requires Cargo) [default]'),
481 ('no-rust', None, 'do not compile Rust extensions'),
481 ('no-rust', None, 'do not compile Rust extensions'),
482 ]
482 ]
483
483
484 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
484 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
485 negative_opt = buildextnegops
485 negative_opt = buildextnegops
486
486
487 def initialize_options(self):
487 def initialize_options(self):
488 self.zstd = True
488 self.zstd = True
489 self.rust = True
489 self.rust = True
490
490
491 return build_ext.initialize_options(self)
491 return build_ext.initialize_options(self)
492
492
493 def build_extensions(self):
493 def build_extensions(self):
494 ruststandalones = [e for e in self.extensions
494 ruststandalones = [e for e in self.extensions
495 if isinstance(e, RustStandaloneExtension)]
495 if isinstance(e, RustStandaloneExtension)]
496 self.extensions = [e for e in self.extensions
496 self.extensions = [e for e in self.extensions
497 if e not in ruststandalones]
497 if e not in ruststandalones]
498 # Filter out zstd if disabled via argument.
498 # Filter out zstd if disabled via argument.
499 if not self.zstd:
499 if not self.zstd:
500 self.extensions = [e for e in self.extensions
500 self.extensions = [e for e in self.extensions
501 if e.name != 'mercurial.zstd']
501 if e.name != 'mercurial.zstd']
502
502
503 # Build Rust standalon extensions if it'll be used
503 # Build Rust standalon extensions if it'll be used
504 # and its build is not explictely disabled (for external build
504 # and its build is not explictely disabled (for external build
505 # as Linux distributions would do)
505 # as Linux distributions would do)
506 if self.distribution.rust and self.rust and hgrustext != 'direct-ffi':
506 if self.distribution.rust and self.rust and hgrustext != 'direct-ffi':
507 for rustext in ruststandalones:
507 for rustext in ruststandalones:
508 rustext.build('' if self.inplace else self.build_lib)
508 rustext.build('' if self.inplace else self.build_lib)
509
509
510 return build_ext.build_extensions(self)
510 return build_ext.build_extensions(self)
511
511
512 def build_extension(self, ext):
512 def build_extension(self, ext):
513 if (self.distribution.rust and self.rust
513 if (self.distribution.rust and self.rust
514 and isinstance(ext, RustExtension)):
514 and isinstance(ext, RustExtension)):
515 ext.rustbuild()
515 ext.rustbuild()
516 try:
516 try:
517 build_ext.build_extension(self, ext)
517 build_ext.build_extension(self, ext)
518 except CCompilerError:
518 except CCompilerError:
519 if not getattr(ext, 'optional', False):
519 if not getattr(ext, 'optional', False):
520 raise
520 raise
521 log.warn("Failed to build optional extension '%s' (skipping)",
521 log.warn("Failed to build optional extension '%s' (skipping)",
522 ext.name)
522 ext.name)
523
523
524 class hgbuildscripts(build_scripts):
524 class hgbuildscripts(build_scripts):
525 def run(self):
525 def run(self):
526 if os.name != 'nt' or self.distribution.pure:
526 if os.name != 'nt' or self.distribution.pure:
527 return build_scripts.run(self)
527 return build_scripts.run(self)
528
528
529 exebuilt = False
529 exebuilt = False
530 try:
530 try:
531 self.run_command('build_hgexe')
531 self.run_command('build_hgexe')
532 exebuilt = True
532 exebuilt = True
533 except (DistutilsError, CCompilerError):
533 except (DistutilsError, CCompilerError):
534 log.warn('failed to build optional hg.exe')
534 log.warn('failed to build optional hg.exe')
535
535
536 if exebuilt:
536 if exebuilt:
537 # Copying hg.exe to the scripts build directory ensures it is
537 # Copying hg.exe to the scripts build directory ensures it is
538 # installed by the install_scripts command.
538 # installed by the install_scripts command.
539 hgexecommand = self.get_finalized_command('build_hgexe')
539 hgexecommand = self.get_finalized_command('build_hgexe')
540 dest = os.path.join(self.build_dir, 'hg.exe')
540 dest = os.path.join(self.build_dir, 'hg.exe')
541 self.mkpath(self.build_dir)
541 self.mkpath(self.build_dir)
542 self.copy_file(hgexecommand.hgexepath, dest)
542 self.copy_file(hgexecommand.hgexepath, dest)
543
543
544 # Remove hg.bat because it is redundant with hg.exe.
544 # Remove hg.bat because it is redundant with hg.exe.
545 self.scripts.remove('contrib/win32/hg.bat')
545 self.scripts.remove('contrib/win32/hg.bat')
546
546
547 return build_scripts.run(self)
547 return build_scripts.run(self)
548
548
549 class hgbuildpy(build_py):
549 class hgbuildpy(build_py):
550 def finalize_options(self):
550 def finalize_options(self):
551 build_py.finalize_options(self)
551 build_py.finalize_options(self)
552
552
553 if self.distribution.pure:
553 if self.distribution.pure:
554 self.distribution.ext_modules = []
554 self.distribution.ext_modules = []
555 elif self.distribution.cffi:
555 elif self.distribution.cffi:
556 from mercurial.cffi import (
556 from mercurial.cffi import (
557 bdiffbuild,
557 bdiffbuild,
558 mpatchbuild,
558 mpatchbuild,
559 )
559 )
560 exts = [mpatchbuild.ffi.distutils_extension(),
560 exts = [mpatchbuild.ffi.distutils_extension(),
561 bdiffbuild.ffi.distutils_extension()]
561 bdiffbuild.ffi.distutils_extension()]
562 # cffi modules go here
562 # cffi modules go here
563 if sys.platform == 'darwin':
563 if sys.platform == 'darwin':
564 from mercurial.cffi import osutilbuild
564 from mercurial.cffi import osutilbuild
565 exts.append(osutilbuild.ffi.distutils_extension())
565 exts.append(osutilbuild.ffi.distutils_extension())
566 self.distribution.ext_modules = exts
566 self.distribution.ext_modules = exts
567 else:
567 else:
568 h = os.path.join(get_python_inc(), 'Python.h')
568 h = os.path.join(get_python_inc(), 'Python.h')
569 if not os.path.exists(h):
569 if not os.path.exists(h):
570 raise SystemExit('Python headers are required to build '
570 raise SystemExit('Python headers are required to build '
571 'Mercurial but weren\'t found in %s' % h)
571 'Mercurial but weren\'t found in %s' % h)
572
572
573 def run(self):
573 def run(self):
574 basepath = os.path.join(self.build_lib, 'mercurial')
574 basepath = os.path.join(self.build_lib, 'mercurial')
575 self.mkpath(basepath)
575 self.mkpath(basepath)
576
576
577 rust = self.distribution.rust
577 rust = self.distribution.rust
578 if self.distribution.pure:
578 if self.distribution.pure:
579 modulepolicy = 'py'
579 modulepolicy = 'py'
580 elif self.build_lib == '.':
580 elif self.build_lib == '.':
581 # in-place build should run without rebuilding and Rust extensions
581 # in-place build should run without rebuilding and Rust extensions
582 modulepolicy = 'rust+c-allow' if rust else 'allow'
582 modulepolicy = 'rust+c-allow' if rust else 'allow'
583 else:
583 else:
584 modulepolicy = 'rust+c' if rust else 'c'
584 modulepolicy = 'rust+c' if rust else 'c'
585
585
586 content = b''.join([
586 content = b''.join([
587 b'# this file is autogenerated by setup.py\n',
587 b'# this file is autogenerated by setup.py\n',
588 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
588 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
589 ])
589 ])
590 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'),
590 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'),
591 content)
591 content)
592
592
593 build_py.run(self)
593 build_py.run(self)
594
594
595 class buildhgextindex(Command):
595 class buildhgextindex(Command):
596 description = 'generate prebuilt index of hgext (for frozen package)'
596 description = 'generate prebuilt index of hgext (for frozen package)'
597 user_options = []
597 user_options = []
598 _indexfilename = 'hgext/__index__.py'
598 _indexfilename = 'hgext/__index__.py'
599
599
600 def initialize_options(self):
600 def initialize_options(self):
601 pass
601 pass
602
602
603 def finalize_options(self):
603 def finalize_options(self):
604 pass
604 pass
605
605
606 def run(self):
606 def run(self):
607 if os.path.exists(self._indexfilename):
607 if os.path.exists(self._indexfilename):
608 with open(self._indexfilename, 'w') as f:
608 with open(self._indexfilename, 'w') as f:
609 f.write('# empty\n')
609 f.write('# empty\n')
610
610
611 # here no extension enabled, disabled() lists up everything
611 # here no extension enabled, disabled() lists up everything
612 code = ('import pprint; from mercurial import extensions; '
612 code = ('import pprint; from mercurial import extensions; '
613 'pprint.pprint(extensions.disabled())')
613 'pprint.pprint(extensions.disabled())')
614 returncode, out, err = runcmd([sys.executable, '-c', code],
614 returncode, out, err = runcmd([sys.executable, '-c', code],
615 localhgenv())
615 localhgenv())
616 if err or returncode != 0:
616 if err or returncode != 0:
617 raise DistutilsExecError(err)
617 raise DistutilsExecError(err)
618
618
619 with open(self._indexfilename, 'wb') as f:
619 with open(self._indexfilename, 'wb') as f:
620 f.write(b'# this file is autogenerated by setup.py\n')
620 f.write(b'# this file is autogenerated by setup.py\n')
621 f.write(b'docs = ')
621 f.write(b'docs = ')
622 f.write(out)
622 f.write(out)
623
623
624 class buildhgexe(build_ext):
624 class buildhgexe(build_ext):
625 description = 'compile hg.exe from mercurial/exewrapper.c'
625 description = 'compile hg.exe from mercurial/exewrapper.c'
626 user_options = build_ext.user_options + [
626 user_options = build_ext.user_options + [
627 ('long-paths-support', None, 'enable support for long paths on '
627 ('long-paths-support', None, 'enable support for long paths on '
628 'Windows (off by default and '
628 'Windows (off by default and '
629 'experimental)'),
629 'experimental)'),
630 ]
630 ]
631
631
632 LONG_PATHS_MANIFEST = """
632 LONG_PATHS_MANIFEST = """
633 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
633 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
634 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
634 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
635 <application>
635 <application>
636 <windowsSettings
636 <windowsSettings
637 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
637 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
638 <ws2:longPathAware>true</ws2:longPathAware>
638 <ws2:longPathAware>true</ws2:longPathAware>
639 </windowsSettings>
639 </windowsSettings>
640 </application>
640 </application>
641 </assembly>"""
641 </assembly>"""
642
642
643 def initialize_options(self):
643 def initialize_options(self):
644 build_ext.initialize_options(self)
644 build_ext.initialize_options(self)
645 self.long_paths_support = False
645 self.long_paths_support = False
646
646
647 def build_extensions(self):
647 def build_extensions(self):
648 if os.name != 'nt':
648 if os.name != 'nt':
649 return
649 return
650 if isinstance(self.compiler, HackedMingw32CCompiler):
650 if isinstance(self.compiler, HackedMingw32CCompiler):
651 self.compiler.compiler_so = self.compiler.compiler # no -mdll
651 self.compiler.compiler_so = self.compiler.compiler # no -mdll
652 self.compiler.dll_libraries = [] # no -lmsrvc90
652 self.compiler.dll_libraries = [] # no -lmsrvc90
653
653
654 # Different Python installs can have different Python library
654 # Different Python installs can have different Python library
655 # names. e.g. the official CPython distribution uses pythonXY.dll
655 # names. e.g. the official CPython distribution uses pythonXY.dll
656 # and MinGW uses libpythonX.Y.dll.
656 # and MinGW uses libpythonX.Y.dll.
657 _kernel32 = ctypes.windll.kernel32
657 _kernel32 = ctypes.windll.kernel32
658 _kernel32.GetModuleFileNameA.argtypes = [ctypes.c_void_p,
658 _kernel32.GetModuleFileNameA.argtypes = [ctypes.c_void_p,
659 ctypes.c_void_p,
659 ctypes.c_void_p,
660 ctypes.c_ulong]
660 ctypes.c_ulong]
661 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
661 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
662 size = 1000
662 size = 1000
663 buf = ctypes.create_string_buffer(size + 1)
663 buf = ctypes.create_string_buffer(size + 1)
664 filelen = _kernel32.GetModuleFileNameA(sys.dllhandle, ctypes.byref(buf),
664 filelen = _kernel32.GetModuleFileNameA(sys.dllhandle, ctypes.byref(buf),
665 size)
665 size)
666
666
667 if filelen > 0 and filelen != size:
667 if filelen > 0 and filelen != size:
668 dllbasename = os.path.basename(buf.value)
668 dllbasename = os.path.basename(buf.value)
669 if not dllbasename.lower().endswith(b'.dll'):
669 if not dllbasename.lower().endswith(b'.dll'):
670 raise SystemExit('Python DLL does not end with .dll: %s' %
670 raise SystemExit('Python DLL does not end with .dll: %s' %
671 dllbasename)
671 dllbasename)
672 pythonlib = dllbasename[:-4]
672 pythonlib = dllbasename[:-4]
673 else:
673 else:
674 log.warn('could not determine Python DLL filename; '
674 log.warn('could not determine Python DLL filename; '
675 'assuming pythonXY')
675 'assuming pythonXY')
676
676
677 hv = sys.hexversion
677 hv = sys.hexversion
678 pythonlib = 'python%d%d' % (hv >> 24, (hv >> 16) & 0xff)
678 pythonlib = 'python%d%d' % (hv >> 24, (hv >> 16) & 0xff)
679
679
680 log.info('using %s as Python library name' % pythonlib)
680 log.info('using %s as Python library name' % pythonlib)
681 with open('mercurial/hgpythonlib.h', 'wb') as f:
681 with open('mercurial/hgpythonlib.h', 'wb') as f:
682 f.write(b'/* this file is autogenerated by setup.py */\n')
682 f.write(b'/* this file is autogenerated by setup.py */\n')
683 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
683 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
684
684
685 macros = None
685 macros = None
686 if sys.version_info[0] >= 3:
686 if sys.version_info[0] >= 3:
687 macros = [('_UNICODE', None), ('UNICODE', None)]
687 macros = [('_UNICODE', None), ('UNICODE', None)]
688
688
689 objects = self.compiler.compile(['mercurial/exewrapper.c'],
689 objects = self.compiler.compile(['mercurial/exewrapper.c'],
690 output_dir=self.build_temp,
690 output_dir=self.build_temp,
691 macros=macros)
691 macros=macros)
692 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
692 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
693 self.hgtarget = os.path.join(dir, 'hg')
693 self.hgtarget = os.path.join(dir, 'hg')
694 self.compiler.link_executable(objects, self.hgtarget,
694 self.compiler.link_executable(objects, self.hgtarget,
695 libraries=[],
695 libraries=[],
696 output_dir=self.build_temp)
696 output_dir=self.build_temp)
697 if self.long_paths_support:
697 if self.long_paths_support:
698 self.addlongpathsmanifest()
698 self.addlongpathsmanifest()
699
699
700 def addlongpathsmanifest(self):
700 def addlongpathsmanifest(self):
701 r"""Add manifest pieces so that hg.exe understands long paths
701 r"""Add manifest pieces so that hg.exe understands long paths
702
702
703 This is an EXPERIMENTAL feature, use with care.
703 This is an EXPERIMENTAL feature, use with care.
704 To enable long paths support, one needs to do two things:
704 To enable long paths support, one needs to do two things:
705 - build Mercurial with --long-paths-support option
705 - build Mercurial with --long-paths-support option
706 - change HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\
706 - change HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\
707 LongPathsEnabled to have value 1.
707 LongPathsEnabled to have value 1.
708
708
709 Please ignore 'warning 81010002: Unrecognized Element "longPathAware"';
709 Please ignore 'warning 81010002: Unrecognized Element "longPathAware"';
710 it happens because Mercurial uses mt.exe circa 2008, which is not
710 it happens because Mercurial uses mt.exe circa 2008, which is not
711 yet aware of long paths support in the manifest (I think so at least).
711 yet aware of long paths support in the manifest (I think so at least).
712 This does not stop mt.exe from embedding/merging the XML properly.
712 This does not stop mt.exe from embedding/merging the XML properly.
713
713
714 Why resource #1 should be used for .exe manifests? I don't know and
714 Why resource #1 should be used for .exe manifests? I don't know and
715 wasn't able to find an explanation for mortals. But it seems to work.
715 wasn't able to find an explanation for mortals. But it seems to work.
716 """
716 """
717 exefname = self.compiler.executable_filename(self.hgtarget)
717 exefname = self.compiler.executable_filename(self.hgtarget)
718 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
718 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
719 os.close(fdauto)
719 os.close(fdauto)
720 with open(manfname, 'w') as f:
720 with open(manfname, 'w') as f:
721 f.write(self.LONG_PATHS_MANIFEST)
721 f.write(self.LONG_PATHS_MANIFEST)
722 log.info("long paths manifest is written to '%s'" % manfname)
722 log.info("long paths manifest is written to '%s'" % manfname)
723 inputresource = '-inputresource:%s;#1' % exefname
723 inputresource = '-inputresource:%s;#1' % exefname
724 outputresource = '-outputresource:%s;#1' % exefname
724 outputresource = '-outputresource:%s;#1' % exefname
725 log.info("running mt.exe to update hg.exe's manifest in-place")
725 log.info("running mt.exe to update hg.exe's manifest in-place")
726 # supplying both -manifest and -inputresource to mt.exe makes
726 # supplying both -manifest and -inputresource to mt.exe makes
727 # it merge the embedded and supplied manifests in the -outputresource
727 # it merge the embedded and supplied manifests in the -outputresource
728 self.spawn(['mt.exe', '-nologo', '-manifest', manfname,
728 self.spawn(['mt.exe', '-nologo', '-manifest', manfname,
729 inputresource, outputresource])
729 inputresource, outputresource])
730 log.info("done updating hg.exe's manifest")
730 log.info("done updating hg.exe's manifest")
731 os.remove(manfname)
731 os.remove(manfname)
732
732
733 @property
733 @property
734 def hgexepath(self):
734 def hgexepath(self):
735 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
735 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
736 return os.path.join(self.build_temp, dir, 'hg.exe')
736 return os.path.join(self.build_temp, dir, 'hg.exe')
737
737
738 class hgbuilddoc(Command):
738 class hgbuilddoc(Command):
739 description = 'build documentation'
739 description = 'build documentation'
740 user_options = [
740 user_options = [
741 ('man', None, 'generate man pages'),
741 ('man', None, 'generate man pages'),
742 ('html', None, 'generate html pages'),
742 ('html', None, 'generate html pages'),
743 ]
743 ]
744
744
745 def initialize_options(self):
745 def initialize_options(self):
746 self.man = None
746 self.man = None
747 self.html = None
747 self.html = None
748
748
749 def finalize_options(self):
749 def finalize_options(self):
750 # If --man or --html are set, only generate what we're told to.
750 # If --man or --html are set, only generate what we're told to.
751 # Otherwise generate everything.
751 # Otherwise generate everything.
752 have_subset = self.man is not None or self.html is not None
752 have_subset = self.man is not None or self.html is not None
753
753
754 if have_subset:
754 if have_subset:
755 self.man = True if self.man else False
755 self.man = True if self.man else False
756 self.html = True if self.html else False
756 self.html = True if self.html else False
757 else:
757 else:
758 self.man = True
758 self.man = True
759 self.html = True
759 self.html = True
760
760
761 def run(self):
761 def run(self):
762 def normalizecrlf(p):
762 def normalizecrlf(p):
763 with open(p, 'rb') as fh:
763 with open(p, 'rb') as fh:
764 orig = fh.read()
764 orig = fh.read()
765
765
766 if b'\r\n' not in orig:
766 if b'\r\n' not in orig:
767 return
767 return
768
768
769 log.info('normalizing %s to LF line endings' % p)
769 log.info('normalizing %s to LF line endings' % p)
770 with open(p, 'wb') as fh:
770 with open(p, 'wb') as fh:
771 fh.write(orig.replace(b'\r\n', b'\n'))
771 fh.write(orig.replace(b'\r\n', b'\n'))
772
772
773 def gentxt(root):
773 def gentxt(root):
774 txt = 'doc/%s.txt' % root
774 txt = 'doc/%s.txt' % root
775 log.info('generating %s' % txt)
775 log.info('generating %s' % txt)
776 res, out, err = runcmd(
776 res, out, err = runcmd(
777 [sys.executable, 'gendoc.py', root],
777 [sys.executable, 'gendoc.py', root],
778 os.environ,
778 os.environ,
779 cwd='doc')
779 cwd='doc')
780 if res:
780 if res:
781 raise SystemExit('error running gendoc.py: %s' %
781 raise SystemExit('error running gendoc.py: %s' %
782 '\n'.join([out, err]))
782 '\n'.join([out, err]))
783
783
784 with open(txt, 'wb') as fh:
784 with open(txt, 'wb') as fh:
785 fh.write(out)
785 fh.write(out)
786
786
787 def gengendoc(root):
787 def gengendoc(root):
788 gendoc = 'doc/%s.gendoc.txt' % root
788 gendoc = 'doc/%s.gendoc.txt' % root
789
789
790 log.info('generating %s' % gendoc)
790 log.info('generating %s' % gendoc)
791 res, out, err = runcmd(
791 res, out, err = runcmd(
792 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
792 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
793 os.environ,
793 os.environ,
794 cwd='doc')
794 cwd='doc')
795 if res:
795 if res:
796 raise SystemExit('error running gendoc: %s' %
796 raise SystemExit('error running gendoc: %s' %
797 '\n'.join([out, err]))
797 '\n'.join([out, err]))
798
798
799 with open(gendoc, 'wb') as fh:
799 with open(gendoc, 'wb') as fh:
800 fh.write(out)
800 fh.write(out)
801
801
802 def genman(root):
802 def genman(root):
803 log.info('generating doc/%s' % root)
803 log.info('generating doc/%s' % root)
804 res, out, err = runcmd(
804 res, out, err = runcmd(
805 [sys.executable, 'runrst', 'hgmanpage', '--halt', 'warning',
805 [sys.executable, 'runrst', 'hgmanpage', '--halt', 'warning',
806 '--strip-elements-with-class', 'htmlonly',
806 '--strip-elements-with-class', 'htmlonly',
807 '%s.txt' % root, root],
807 '%s.txt' % root, root],
808 os.environ,
808 os.environ,
809 cwd='doc')
809 cwd='doc')
810 if res:
810 if res:
811 raise SystemExit('error running runrst: %s' %
811 raise SystemExit('error running runrst: %s' %
812 '\n'.join([out, err]))
812 '\n'.join([out, err]))
813
813
814 normalizecrlf('doc/%s' % root)
814 normalizecrlf('doc/%s' % root)
815
815
816 def genhtml(root):
816 def genhtml(root):
817 log.info('generating doc/%s.html' % root)
817 log.info('generating doc/%s.html' % root)
818 res, out, err = runcmd(
818 res, out, err = runcmd(
819 [sys.executable, 'runrst', 'html', '--halt', 'warning',
819 [sys.executable, 'runrst', 'html', '--halt', 'warning',
820 '--link-stylesheet', '--stylesheet-path', 'style.css',
820 '--link-stylesheet', '--stylesheet-path', 'style.css',
821 '%s.txt' % root, '%s.html' % root],
821 '%s.txt' % root, '%s.html' % root],
822 os.environ,
822 os.environ,
823 cwd='doc')
823 cwd='doc')
824 if res:
824 if res:
825 raise SystemExit('error running runrst: %s' %
825 raise SystemExit('error running runrst: %s' %
826 '\n'.join([out, err]))
826 '\n'.join([out, err]))
827
827
828 normalizecrlf('doc/%s.html' % root)
828 normalizecrlf('doc/%s.html' % root)
829
829
830 # This logic is duplicated in doc/Makefile.
830 # This logic is duplicated in doc/Makefile.
831 sources = set(f for f in os.listdir('mercurial/help')
831 sources = set(f for f in os.listdir('mercurial/help')
832 if re.search(r'[0-9]\.txt$', f))
832 if re.search(r'[0-9]\.txt$', f))
833
833
834 # common.txt is a one-off.
834 # common.txt is a one-off.
835 gentxt('common')
835 gentxt('common')
836
836
837 for source in sorted(sources):
837 for source in sorted(sources):
838 assert source[-4:] == '.txt'
838 assert source[-4:] == '.txt'
839 root = source[:-4]
839 root = source[:-4]
840
840
841 gentxt(root)
841 gentxt(root)
842 gengendoc(root)
842 gengendoc(root)
843
843
844 if self.man:
844 if self.man:
845 genman(root)
845 genman(root)
846 if self.html:
846 if self.html:
847 genhtml(root)
847 genhtml(root)
848
848
849 class hginstall(install):
849 class hginstall(install):
850
850
851 user_options = install.user_options + [
851 user_options = install.user_options + [
852 ('old-and-unmanageable', None,
852 ('old-and-unmanageable', None,
853 'noop, present for eggless setuptools compat'),
853 'noop, present for eggless setuptools compat'),
854 ('single-version-externally-managed', None,
854 ('single-version-externally-managed', None,
855 'noop, present for eggless setuptools compat'),
855 'noop, present for eggless setuptools compat'),
856 ]
856 ]
857
857
858 # Also helps setuptools not be sad while we refuse to create eggs.
858 # Also helps setuptools not be sad while we refuse to create eggs.
859 single_version_externally_managed = True
859 single_version_externally_managed = True
860
860
861 def get_sub_commands(self):
861 def get_sub_commands(self):
862 # Screen out egg related commands to prevent egg generation. But allow
862 # Screen out egg related commands to prevent egg generation. But allow
863 # mercurial.egg-info generation, since that is part of modern
863 # mercurial.egg-info generation, since that is part of modern
864 # packaging.
864 # packaging.
865 excl = set(['bdist_egg'])
865 excl = set(['bdist_egg'])
866 return filter(lambda x: x not in excl, install.get_sub_commands(self))
866 return filter(lambda x: x not in excl, install.get_sub_commands(self))
867
867
868 class hginstalllib(install_lib):
868 class hginstalllib(install_lib):
869 '''
869 '''
870 This is a specialization of install_lib that replaces the copy_file used
870 This is a specialization of install_lib that replaces the copy_file used
871 there so that it supports setting the mode of files after copying them,
871 there so that it supports setting the mode of files after copying them,
872 instead of just preserving the mode that the files originally had. If your
872 instead of just preserving the mode that the files originally had. If your
873 system has a umask of something like 027, preserving the permissions when
873 system has a umask of something like 027, preserving the permissions when
874 copying will lead to a broken install.
874 copying will lead to a broken install.
875
875
876 Note that just passing keep_permissions=False to copy_file would be
876 Note that just passing keep_permissions=False to copy_file would be
877 insufficient, as it might still be applying a umask.
877 insufficient, as it might still be applying a umask.
878 '''
878 '''
879
879
880 def run(self):
880 def run(self):
881 realcopyfile = file_util.copy_file
881 realcopyfile = file_util.copy_file
882 def copyfileandsetmode(*args, **kwargs):
882 def copyfileandsetmode(*args, **kwargs):
883 src, dst = args[0], args[1]
883 src, dst = args[0], args[1]
884 dst, copied = realcopyfile(*args, **kwargs)
884 dst, copied = realcopyfile(*args, **kwargs)
885 if copied:
885 if copied:
886 st = os.stat(src)
886 st = os.stat(src)
887 # Persist executable bit (apply it to group and other if user
887 # Persist executable bit (apply it to group and other if user
888 # has it)
888 # has it)
889 if st[stat.ST_MODE] & stat.S_IXUSR:
889 if st[stat.ST_MODE] & stat.S_IXUSR:
890 setmode = int('0755', 8)
890 setmode = int('0755', 8)
891 else:
891 else:
892 setmode = int('0644', 8)
892 setmode = int('0644', 8)
893 m = stat.S_IMODE(st[stat.ST_MODE])
893 m = stat.S_IMODE(st[stat.ST_MODE])
894 m = (m & ~int('0777', 8)) | setmode
894 m = (m & ~int('0777', 8)) | setmode
895 os.chmod(dst, m)
895 os.chmod(dst, m)
896 file_util.copy_file = copyfileandsetmode
896 file_util.copy_file = copyfileandsetmode
897 try:
897 try:
898 install_lib.run(self)
898 install_lib.run(self)
899 finally:
899 finally:
900 file_util.copy_file = realcopyfile
900 file_util.copy_file = realcopyfile
901
901
902 class hginstallscripts(install_scripts):
902 class hginstallscripts(install_scripts):
903 '''
903 '''
904 This is a specialization of install_scripts that replaces the @LIBDIR@ with
904 This is a specialization of install_scripts that replaces the @LIBDIR@ with
905 the configured directory for modules. If possible, the path is made relative
905 the configured directory for modules. If possible, the path is made relative
906 to the directory for scripts.
906 to the directory for scripts.
907 '''
907 '''
908
908
909 def initialize_options(self):
909 def initialize_options(self):
910 install_scripts.initialize_options(self)
910 install_scripts.initialize_options(self)
911
911
912 self.install_lib = None
912 self.install_lib = None
913
913
914 def finalize_options(self):
914 def finalize_options(self):
915 install_scripts.finalize_options(self)
915 install_scripts.finalize_options(self)
916 self.set_undefined_options('install',
916 self.set_undefined_options('install',
917 ('install_lib', 'install_lib'))
917 ('install_lib', 'install_lib'))
918
918
919 def run(self):
919 def run(self):
920 install_scripts.run(self)
920 install_scripts.run(self)
921
921
922 # It only makes sense to replace @LIBDIR@ with the install path if
922 # It only makes sense to replace @LIBDIR@ with the install path if
923 # the install path is known. For wheels, the logic below calculates
923 # the install path is known. For wheels, the logic below calculates
924 # the libdir to be "../..". This is because the internal layout of a
924 # the libdir to be "../..". This is because the internal layout of a
925 # wheel archive looks like:
925 # wheel archive looks like:
926 #
926 #
927 # mercurial-3.6.1.data/scripts/hg
927 # mercurial-3.6.1.data/scripts/hg
928 # mercurial/__init__.py
928 # mercurial/__init__.py
929 #
929 #
930 # When installing wheels, the subdirectories of the "<pkg>.data"
930 # When installing wheels, the subdirectories of the "<pkg>.data"
931 # directory are translated to system local paths and files therein
931 # directory are translated to system local paths and files therein
932 # are copied in place. The mercurial/* files are installed into the
932 # are copied in place. The mercurial/* files are installed into the
933 # site-packages directory. However, the site-packages directory
933 # site-packages directory. However, the site-packages directory
934 # isn't known until wheel install time. This means we have no clue
934 # isn't known until wheel install time. This means we have no clue
935 # at wheel generation time what the installed site-packages directory
935 # at wheel generation time what the installed site-packages directory
936 # will be. And, wheels don't appear to provide the ability to register
936 # will be. And, wheels don't appear to provide the ability to register
937 # custom code to run during wheel installation. This all means that
937 # custom code to run during wheel installation. This all means that
938 # we can't reliably set the libdir in wheels: the default behavior
938 # we can't reliably set the libdir in wheels: the default behavior
939 # of looking in sys.path must do.
939 # of looking in sys.path must do.
940
940
941 if (os.path.splitdrive(self.install_dir)[0] !=
941 if (os.path.splitdrive(self.install_dir)[0] !=
942 os.path.splitdrive(self.install_lib)[0]):
942 os.path.splitdrive(self.install_lib)[0]):
943 # can't make relative paths from one drive to another, so use an
943 # can't make relative paths from one drive to another, so use an
944 # absolute path instead
944 # absolute path instead
945 libdir = self.install_lib
945 libdir = self.install_lib
946 else:
946 else:
947 common = os.path.commonprefix((self.install_dir, self.install_lib))
947 common = os.path.commonprefix((self.install_dir, self.install_lib))
948 rest = self.install_dir[len(common):]
948 rest = self.install_dir[len(common):]
949 uplevel = len([n for n in os.path.split(rest) if n])
949 uplevel = len([n for n in os.path.split(rest) if n])
950
950
951 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
951 libdir = uplevel * ('..' + os.sep) + self.install_lib[len(common):]
952
952
953 for outfile in self.outfiles:
953 for outfile in self.outfiles:
954 with open(outfile, 'rb') as fp:
954 with open(outfile, 'rb') as fp:
955 data = fp.read()
955 data = fp.read()
956
956
957 # skip binary files
957 # skip binary files
958 if b'\0' in data:
958 if b'\0' in data:
959 continue
959 continue
960
960
961 # During local installs, the shebang will be rewritten to the final
961 # During local installs, the shebang will be rewritten to the final
962 # install path. During wheel packaging, the shebang has a special
962 # install path. During wheel packaging, the shebang has a special
963 # value.
963 # value.
964 if data.startswith(b'#!python'):
964 if data.startswith(b'#!python'):
965 log.info('not rewriting @LIBDIR@ in %s because install path '
965 log.info('not rewriting @LIBDIR@ in %s because install path '
966 'not known' % outfile)
966 'not known' % outfile)
967 continue
967 continue
968
968
969 data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
969 data = data.replace(b'@LIBDIR@', libdir.encode(libdir_escape))
970 with open(outfile, 'wb') as fp:
970 with open(outfile, 'wb') as fp:
971 fp.write(data)
971 fp.write(data)
972
972
973 # virtualenv installs custom distutils/__init__.py and
973 # virtualenv installs custom distutils/__init__.py and
974 # distutils/distutils.cfg files which essentially proxy back to the
974 # distutils/distutils.cfg files which essentially proxy back to the
975 # "real" distutils in the main Python install. The presence of this
975 # "real" distutils in the main Python install. The presence of this
976 # directory causes py2exe to pick up the "hacked" distutils package
976 # directory causes py2exe to pick up the "hacked" distutils package
977 # from the virtualenv and "import distutils" will fail from the py2exe
977 # from the virtualenv and "import distutils" will fail from the py2exe
978 # build because the "real" distutils files can't be located.
978 # build because the "real" distutils files can't be located.
979 #
979 #
980 # We work around this by monkeypatching the py2exe code finding Python
980 # We work around this by monkeypatching the py2exe code finding Python
981 # modules to replace the found virtualenv distutils modules with the
981 # modules to replace the found virtualenv distutils modules with the
982 # original versions via filesystem scanning. This is a bit hacky. But
982 # original versions via filesystem scanning. This is a bit hacky. But
983 # it allows us to use virtualenvs for py2exe packaging, which is more
983 # it allows us to use virtualenvs for py2exe packaging, which is more
984 # deterministic and reproducible.
984 # deterministic and reproducible.
985 #
985 #
986 # It's worth noting that the common StackOverflow suggestions for this
986 # It's worth noting that the common StackOverflow suggestions for this
987 # problem involve copying the original distutils files into the
987 # problem involve copying the original distutils files into the
988 # virtualenv or into the staging directory after setup() is invoked.
988 # virtualenv or into the staging directory after setup() is invoked.
989 # The former is very brittle and can easily break setup(). Our hacking
989 # The former is very brittle and can easily break setup(). Our hacking
990 # of the found modules routine has a similar result as copying the files
990 # of the found modules routine has a similar result as copying the files
991 # manually. But it makes fewer assumptions about how py2exe works and
991 # manually. But it makes fewer assumptions about how py2exe works and
992 # is less brittle.
992 # is less brittle.
993
993
994 # This only catches virtualenvs made with virtualenv (as opposed to
994 # This only catches virtualenvs made with virtualenv (as opposed to
995 # venv, which is likely what Python 3 uses).
995 # venv, which is likely what Python 3 uses).
996 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
996 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
997
997
998 if py2exehacked:
998 if py2exehacked:
999 from distutils.command.py2exe import py2exe as buildpy2exe
999 from distutils.command.py2exe import py2exe as buildpy2exe
1000 from py2exe.mf import Module as py2exemodule
1000 from py2exe.mf import Module as py2exemodule
1001
1001
1002 class hgbuildpy2exe(buildpy2exe):
1002 class hgbuildpy2exe(buildpy2exe):
1003 def find_needed_modules(self, mf, files, modules):
1003 def find_needed_modules(self, mf, files, modules):
1004 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1004 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1005
1005
1006 # Replace virtualenv's distutils modules with the real ones.
1006 # Replace virtualenv's distutils modules with the real ones.
1007 modules = {}
1007 modules = {}
1008 for k, v in res.modules.items():
1008 for k, v in res.modules.items():
1009 if k != 'distutils' and not k.startswith('distutils.'):
1009 if k != 'distutils' and not k.startswith('distutils.'):
1010 modules[k] = v
1010 modules[k] = v
1011
1011
1012 res.modules = modules
1012 res.modules = modules
1013
1013
1014 import opcode
1014 import opcode
1015 distutilsreal = os.path.join(os.path.dirname(opcode.__file__),
1015 distutilsreal = os.path.join(os.path.dirname(opcode.__file__),
1016 'distutils')
1016 'distutils')
1017
1017
1018 for root, dirs, files in os.walk(distutilsreal):
1018 for root, dirs, files in os.walk(distutilsreal):
1019 for f in sorted(files):
1019 for f in sorted(files):
1020 if not f.endswith('.py'):
1020 if not f.endswith('.py'):
1021 continue
1021 continue
1022
1022
1023 full = os.path.join(root, f)
1023 full = os.path.join(root, f)
1024
1024
1025 parents = ['distutils']
1025 parents = ['distutils']
1026
1026
1027 if root != distutilsreal:
1027 if root != distutilsreal:
1028 rel = os.path.relpath(root, distutilsreal)
1028 rel = os.path.relpath(root, distutilsreal)
1029 parents.extend(p for p in rel.split(os.sep))
1029 parents.extend(p for p in rel.split(os.sep))
1030
1030
1031 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1031 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1032
1032
1033 if modname.startswith('distutils.tests.'):
1033 if modname.startswith('distutils.tests.'):
1034 continue
1034 continue
1035
1035
1036 if modname.endswith('.__init__'):
1036 if modname.endswith('.__init__'):
1037 modname = modname[:-len('.__init__')]
1037 modname = modname[:-len('.__init__')]
1038 path = os.path.dirname(full)
1038 path = os.path.dirname(full)
1039 else:
1039 else:
1040 path = None
1040 path = None
1041
1041
1042 res.modules[modname] = py2exemodule(modname, full,
1042 res.modules[modname] = py2exemodule(modname, full,
1043 path=path)
1043 path=path)
1044
1044
1045 if 'distutils' not in res.modules:
1045 if 'distutils' not in res.modules:
1046 raise SystemExit('could not find distutils modules')
1046 raise SystemExit('could not find distutils modules')
1047
1047
1048 return res
1048 return res
1049
1049
1050 cmdclass = {'build': hgbuild,
1050 cmdclass = {'build': hgbuild,
1051 'build_doc': hgbuilddoc,
1051 'build_doc': hgbuilddoc,
1052 'build_mo': hgbuildmo,
1052 'build_mo': hgbuildmo,
1053 'build_ext': hgbuildext,
1053 'build_ext': hgbuildext,
1054 'build_py': hgbuildpy,
1054 'build_py': hgbuildpy,
1055 'build_scripts': hgbuildscripts,
1055 'build_scripts': hgbuildscripts,
1056 'build_hgextindex': buildhgextindex,
1056 'build_hgextindex': buildhgextindex,
1057 'install': hginstall,
1057 'install': hginstall,
1058 'install_lib': hginstalllib,
1058 'install_lib': hginstalllib,
1059 'install_scripts': hginstallscripts,
1059 'install_scripts': hginstallscripts,
1060 'build_hgexe': buildhgexe,
1060 'build_hgexe': buildhgexe,
1061 }
1061 }
1062
1062
1063 if py2exehacked:
1063 if py2exehacked:
1064 cmdclass['py2exe'] = hgbuildpy2exe
1064 cmdclass['py2exe'] = hgbuildpy2exe
1065
1065
1066 packages = ['mercurial',
1066 packages = ['mercurial',
1067 'mercurial.cext',
1067 'mercurial.cext',
1068 'mercurial.cffi',
1068 'mercurial.cffi',
1069 'mercurial.hgweb',
1069 'mercurial.hgweb',
1070 'mercurial.interfaces',
1070 'mercurial.pure',
1071 'mercurial.pure',
1071 'mercurial.thirdparty',
1072 'mercurial.thirdparty',
1072 'mercurial.thirdparty.attr',
1073 'mercurial.thirdparty.attr',
1073 'mercurial.thirdparty.zope',
1074 'mercurial.thirdparty.zope',
1074 'mercurial.thirdparty.zope.interface',
1075 'mercurial.thirdparty.zope.interface',
1075 'mercurial.utils',
1076 'mercurial.utils',
1076 'mercurial.revlogutils',
1077 'mercurial.revlogutils',
1077 'mercurial.testing',
1078 'mercurial.testing',
1078 'hgext', 'hgext.convert', 'hgext.fsmonitor',
1079 'hgext', 'hgext.convert', 'hgext.fsmonitor',
1079 'hgext.fastannotate',
1080 'hgext.fastannotate',
1080 'hgext.fsmonitor.pywatchman',
1081 'hgext.fsmonitor.pywatchman',
1081 'hgext.highlight',
1082 'hgext.highlight',
1082 'hgext.infinitepush',
1083 'hgext.infinitepush',
1083 'hgext.largefiles', 'hgext.lfs', 'hgext.narrow',
1084 'hgext.largefiles', 'hgext.lfs', 'hgext.narrow',
1084 'hgext.remotefilelog',
1085 'hgext.remotefilelog',
1085 'hgext.zeroconf', 'hgext3rd',
1086 'hgext.zeroconf', 'hgext3rd',
1086 'hgdemandimport']
1087 'hgdemandimport']
1087 if sys.version_info[0] == 2:
1088 if sys.version_info[0] == 2:
1088 packages.extend(['mercurial.thirdparty.concurrent',
1089 packages.extend(['mercurial.thirdparty.concurrent',
1089 'mercurial.thirdparty.concurrent.futures'])
1090 'mercurial.thirdparty.concurrent.futures'])
1090
1091
1091 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1092 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1092 # py2exe can't cope with namespace packages very well, so we have to
1093 # py2exe can't cope with namespace packages very well, so we have to
1093 # install any hgext3rd.* extensions that we want in the final py2exe
1094 # install any hgext3rd.* extensions that we want in the final py2exe
1094 # image here. This is gross, but you gotta do what you gotta do.
1095 # image here. This is gross, but you gotta do what you gotta do.
1095 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1096 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1096
1097
1097 common_depends = ['mercurial/bitmanipulation.h',
1098 common_depends = ['mercurial/bitmanipulation.h',
1098 'mercurial/compat.h',
1099 'mercurial/compat.h',
1099 'mercurial/cext/util.h']
1100 'mercurial/cext/util.h']
1100 common_include_dirs = ['mercurial']
1101 common_include_dirs = ['mercurial']
1101
1102
1102 osutil_cflags = []
1103 osutil_cflags = []
1103 osutil_ldflags = []
1104 osutil_ldflags = []
1104
1105
1105 # platform specific macros
1106 # platform specific macros
1106 for plat, func in [('bsd', 'setproctitle')]:
1107 for plat, func in [('bsd', 'setproctitle')]:
1107 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1108 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1108 osutil_cflags.append('-DHAVE_%s' % func.upper())
1109 osutil_cflags.append('-DHAVE_%s' % func.upper())
1109
1110
1110 for plat, macro, code in [
1111 for plat, macro, code in [
1111 ('bsd|darwin', 'BSD_STATFS', '''
1112 ('bsd|darwin', 'BSD_STATFS', '''
1112 #include <sys/param.h>
1113 #include <sys/param.h>
1113 #include <sys/mount.h>
1114 #include <sys/mount.h>
1114 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1115 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1115 '''),
1116 '''),
1116 ('linux', 'LINUX_STATFS', '''
1117 ('linux', 'LINUX_STATFS', '''
1117 #include <linux/magic.h>
1118 #include <linux/magic.h>
1118 #include <sys/vfs.h>
1119 #include <sys/vfs.h>
1119 int main() { struct statfs s; return sizeof(s.f_type); }
1120 int main() { struct statfs s; return sizeof(s.f_type); }
1120 '''),
1121 '''),
1121 ]:
1122 ]:
1122 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1123 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1123 osutil_cflags.append('-DHAVE_%s' % macro)
1124 osutil_cflags.append('-DHAVE_%s' % macro)
1124
1125
1125 if sys.platform == 'darwin':
1126 if sys.platform == 'darwin':
1126 osutil_ldflags += ['-framework', 'ApplicationServices']
1127 osutil_ldflags += ['-framework', 'ApplicationServices']
1127
1128
1128 xdiff_srcs = [
1129 xdiff_srcs = [
1129 'mercurial/thirdparty/xdiff/xdiffi.c',
1130 'mercurial/thirdparty/xdiff/xdiffi.c',
1130 'mercurial/thirdparty/xdiff/xprepare.c',
1131 'mercurial/thirdparty/xdiff/xprepare.c',
1131 'mercurial/thirdparty/xdiff/xutils.c',
1132 'mercurial/thirdparty/xdiff/xutils.c',
1132 ]
1133 ]
1133
1134
1134 xdiff_headers = [
1135 xdiff_headers = [
1135 'mercurial/thirdparty/xdiff/xdiff.h',
1136 'mercurial/thirdparty/xdiff/xdiff.h',
1136 'mercurial/thirdparty/xdiff/xdiffi.h',
1137 'mercurial/thirdparty/xdiff/xdiffi.h',
1137 'mercurial/thirdparty/xdiff/xinclude.h',
1138 'mercurial/thirdparty/xdiff/xinclude.h',
1138 'mercurial/thirdparty/xdiff/xmacros.h',
1139 'mercurial/thirdparty/xdiff/xmacros.h',
1139 'mercurial/thirdparty/xdiff/xprepare.h',
1140 'mercurial/thirdparty/xdiff/xprepare.h',
1140 'mercurial/thirdparty/xdiff/xtypes.h',
1141 'mercurial/thirdparty/xdiff/xtypes.h',
1141 'mercurial/thirdparty/xdiff/xutils.h',
1142 'mercurial/thirdparty/xdiff/xutils.h',
1142 ]
1143 ]
1143
1144
1144 class RustCompilationError(CCompilerError):
1145 class RustCompilationError(CCompilerError):
1145 """Exception class for Rust compilation errors."""
1146 """Exception class for Rust compilation errors."""
1146
1147
1147 class RustExtension(Extension):
1148 class RustExtension(Extension):
1148 """Base classes for concrete Rust Extension classes.
1149 """Base classes for concrete Rust Extension classes.
1149 """
1150 """
1150
1151
1151 rusttargetdir = os.path.join('rust', 'target', 'release')
1152 rusttargetdir = os.path.join('rust', 'target', 'release')
1152
1153
1153 def __init__(self, mpath, sources, rustlibname, subcrate,
1154 def __init__(self, mpath, sources, rustlibname, subcrate,
1154 py3_features=None, **kw):
1155 py3_features=None, **kw):
1155 Extension.__init__(self, mpath, sources, **kw)
1156 Extension.__init__(self, mpath, sources, **kw)
1156 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1157 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1157 self.py3_features = py3_features
1158 self.py3_features = py3_features
1158
1159
1159 # adding Rust source and control files to depends so that the extension
1160 # adding Rust source and control files to depends so that the extension
1160 # gets rebuilt if they've changed
1161 # gets rebuilt if they've changed
1161 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1162 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1162 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1163 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1163 if os.path.exists(cargo_lock):
1164 if os.path.exists(cargo_lock):
1164 self.depends.append(cargo_lock)
1165 self.depends.append(cargo_lock)
1165 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1166 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1166 self.depends.extend(os.path.join(dirpath, fname)
1167 self.depends.extend(os.path.join(dirpath, fname)
1167 for fname in fnames
1168 for fname in fnames
1168 if os.path.splitext(fname)[1] == '.rs')
1169 if os.path.splitext(fname)[1] == '.rs')
1169
1170
1170 @staticmethod
1171 @staticmethod
1171 def rustdylibsuffix():
1172 def rustdylibsuffix():
1172 """Return the suffix for shared libraries produced by rustc.
1173 """Return the suffix for shared libraries produced by rustc.
1173
1174
1174 See also: https://doc.rust-lang.org/reference/linkage.html
1175 See also: https://doc.rust-lang.org/reference/linkage.html
1175 """
1176 """
1176 if sys.platform == 'darwin':
1177 if sys.platform == 'darwin':
1177 return '.dylib'
1178 return '.dylib'
1178 elif os.name == 'nt':
1179 elif os.name == 'nt':
1179 return '.dll'
1180 return '.dll'
1180 else:
1181 else:
1181 return '.so'
1182 return '.so'
1182
1183
1183 def rustbuild(self):
1184 def rustbuild(self):
1184 env = os.environ.copy()
1185 env = os.environ.copy()
1185 if 'HGTEST_RESTOREENV' in env:
1186 if 'HGTEST_RESTOREENV' in env:
1186 # Mercurial tests change HOME to a temporary directory,
1187 # Mercurial tests change HOME to a temporary directory,
1187 # but, if installed with rustup, the Rust toolchain needs
1188 # but, if installed with rustup, the Rust toolchain needs
1188 # HOME to be correct (otherwise the 'no default toolchain'
1189 # HOME to be correct (otherwise the 'no default toolchain'
1189 # error message is issued and the build fails).
1190 # error message is issued and the build fails).
1190 # This happens currently with test-hghave.t, which does
1191 # This happens currently with test-hghave.t, which does
1191 # invoke this build.
1192 # invoke this build.
1192
1193
1193 # Unix only fix (os.path.expanduser not really reliable if
1194 # Unix only fix (os.path.expanduser not really reliable if
1194 # HOME is shadowed like this)
1195 # HOME is shadowed like this)
1195 import pwd
1196 import pwd
1196 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1197 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1197
1198
1198 cargocmd = ['cargo', 'rustc', '-vv', '--release']
1199 cargocmd = ['cargo', 'rustc', '-vv', '--release']
1199 if sys.version_info[0] == 3 and self.py3_features is not None:
1200 if sys.version_info[0] == 3 and self.py3_features is not None:
1200 cargocmd.extend(('--features', self.py3_features,
1201 cargocmd.extend(('--features', self.py3_features,
1201 '--no-default-features'))
1202 '--no-default-features'))
1202 cargocmd.append('--')
1203 cargocmd.append('--')
1203 if sys.platform == 'darwin':
1204 if sys.platform == 'darwin':
1204 cargocmd.extend(("-C", "link-arg=-undefined",
1205 cargocmd.extend(("-C", "link-arg=-undefined",
1205 "-C", "link-arg=dynamic_lookup"))
1206 "-C", "link-arg=dynamic_lookup"))
1206 try:
1207 try:
1207 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1208 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1208 except OSError as exc:
1209 except OSError as exc:
1209 if exc.errno == errno.ENOENT:
1210 if exc.errno == errno.ENOENT:
1210 raise RustCompilationError("Cargo not found")
1211 raise RustCompilationError("Cargo not found")
1211 elif exc.errno == errno.EACCES:
1212 elif exc.errno == errno.EACCES:
1212 raise RustCompilationError(
1213 raise RustCompilationError(
1213 "Cargo found, but permisssion to execute it is denied")
1214 "Cargo found, but permisssion to execute it is denied")
1214 else:
1215 else:
1215 raise
1216 raise
1216 except subprocess.CalledProcessError:
1217 except subprocess.CalledProcessError:
1217 raise RustCompilationError(
1218 raise RustCompilationError(
1218 "Cargo failed. Working directory: %r, "
1219 "Cargo failed. Working directory: %r, "
1219 "command: %r, environment: %r"
1220 "command: %r, environment: %r"
1220 % (self.rustsrcdir, cargocmd, env))
1221 % (self.rustsrcdir, cargocmd, env))
1221
1222
1222 class RustEnhancedExtension(RustExtension):
1223 class RustEnhancedExtension(RustExtension):
1223 """A C Extension, conditionally enhanced with Rust code.
1224 """A C Extension, conditionally enhanced with Rust code.
1224
1225
1225 If the HGRUSTEXT environment variable is set to something else
1226 If the HGRUSTEXT environment variable is set to something else
1226 than 'cpython', the Rust sources get compiled and linked within the
1227 than 'cpython', the Rust sources get compiled and linked within the
1227 C target shared library object.
1228 C target shared library object.
1228 """
1229 """
1229
1230
1230 def __init__(self, mpath, sources, rustlibname, subcrate, **kw):
1231 def __init__(self, mpath, sources, rustlibname, subcrate, **kw):
1231 RustExtension.__init__(self, mpath, sources, rustlibname, subcrate,
1232 RustExtension.__init__(self, mpath, sources, rustlibname, subcrate,
1232 **kw)
1233 **kw)
1233 if hgrustext != 'direct-ffi':
1234 if hgrustext != 'direct-ffi':
1234 return
1235 return
1235 self.extra_compile_args.append('-DWITH_RUST')
1236 self.extra_compile_args.append('-DWITH_RUST')
1236 self.libraries.append(rustlibname)
1237 self.libraries.append(rustlibname)
1237 self.library_dirs.append(self.rusttargetdir)
1238 self.library_dirs.append(self.rusttargetdir)
1238
1239
1239 def rustbuild(self):
1240 def rustbuild(self):
1240 if hgrustext == 'direct-ffi':
1241 if hgrustext == 'direct-ffi':
1241 RustExtension.rustbuild(self)
1242 RustExtension.rustbuild(self)
1242
1243
1243 class RustStandaloneExtension(RustExtension):
1244 class RustStandaloneExtension(RustExtension):
1244
1245
1245 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1246 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1246 RustExtension.__init__(self, pydottedname, [], dylibname, rustcrate,
1247 RustExtension.__init__(self, pydottedname, [], dylibname, rustcrate,
1247 **kw)
1248 **kw)
1248 self.dylibname = dylibname
1249 self.dylibname = dylibname
1249
1250
1250 def build(self, target_dir):
1251 def build(self, target_dir):
1251 self.rustbuild()
1252 self.rustbuild()
1252 target = [target_dir]
1253 target = [target_dir]
1253 target.extend(self.name.split('.'))
1254 target.extend(self.name.split('.'))
1254 target[-1] += DYLIB_SUFFIX
1255 target[-1] += DYLIB_SUFFIX
1255 shutil.copy2(os.path.join(self.rusttargetdir,
1256 shutil.copy2(os.path.join(self.rusttargetdir,
1256 self.dylibname + self.rustdylibsuffix()),
1257 self.dylibname + self.rustdylibsuffix()),
1257 os.path.join(*target))
1258 os.path.join(*target))
1258
1259
1259
1260
1260 extmodules = [
1261 extmodules = [
1261 Extension('mercurial.cext.base85', ['mercurial/cext/base85.c'],
1262 Extension('mercurial.cext.base85', ['mercurial/cext/base85.c'],
1262 include_dirs=common_include_dirs,
1263 include_dirs=common_include_dirs,
1263 depends=common_depends),
1264 depends=common_depends),
1264 Extension('mercurial.cext.bdiff', ['mercurial/bdiff.c',
1265 Extension('mercurial.cext.bdiff', ['mercurial/bdiff.c',
1265 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1266 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1266 include_dirs=common_include_dirs,
1267 include_dirs=common_include_dirs,
1267 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers),
1268 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers),
1268 Extension('mercurial.cext.mpatch', ['mercurial/mpatch.c',
1269 Extension('mercurial.cext.mpatch', ['mercurial/mpatch.c',
1269 'mercurial/cext/mpatch.c'],
1270 'mercurial/cext/mpatch.c'],
1270 include_dirs=common_include_dirs,
1271 include_dirs=common_include_dirs,
1271 depends=common_depends),
1272 depends=common_depends),
1272 RustEnhancedExtension(
1273 RustEnhancedExtension(
1273 'mercurial.cext.parsers', ['mercurial/cext/charencode.c',
1274 'mercurial.cext.parsers', ['mercurial/cext/charencode.c',
1274 'mercurial/cext/dirs.c',
1275 'mercurial/cext/dirs.c',
1275 'mercurial/cext/manifest.c',
1276 'mercurial/cext/manifest.c',
1276 'mercurial/cext/parsers.c',
1277 'mercurial/cext/parsers.c',
1277 'mercurial/cext/pathencode.c',
1278 'mercurial/cext/pathencode.c',
1278 'mercurial/cext/revlog.c'],
1279 'mercurial/cext/revlog.c'],
1279 'hgdirectffi',
1280 'hgdirectffi',
1280 'hg-direct-ffi',
1281 'hg-direct-ffi',
1281 include_dirs=common_include_dirs,
1282 include_dirs=common_include_dirs,
1282 depends=common_depends + ['mercurial/cext/charencode.h',
1283 depends=common_depends + ['mercurial/cext/charencode.h',
1283 'mercurial/cext/revlog.h',
1284 'mercurial/cext/revlog.h',
1284 'rust/hg-core/src/ancestors.rs',
1285 'rust/hg-core/src/ancestors.rs',
1285 'rust/hg-core/src/lib.rs']),
1286 'rust/hg-core/src/lib.rs']),
1286 Extension('mercurial.cext.osutil', ['mercurial/cext/osutil.c'],
1287 Extension('mercurial.cext.osutil', ['mercurial/cext/osutil.c'],
1287 include_dirs=common_include_dirs,
1288 include_dirs=common_include_dirs,
1288 extra_compile_args=osutil_cflags,
1289 extra_compile_args=osutil_cflags,
1289 extra_link_args=osutil_ldflags,
1290 extra_link_args=osutil_ldflags,
1290 depends=common_depends),
1291 depends=common_depends),
1291 Extension(
1292 Extension(
1292 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations', [
1293 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations', [
1293 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1294 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1294 ]),
1295 ]),
1295 Extension('hgext.fsmonitor.pywatchman.bser',
1296 Extension('hgext.fsmonitor.pywatchman.bser',
1296 ['hgext/fsmonitor/pywatchman/bser.c']),
1297 ['hgext/fsmonitor/pywatchman/bser.c']),
1297 RustStandaloneExtension('mercurial.rustext', 'hg-cpython', 'librusthg',
1298 RustStandaloneExtension('mercurial.rustext', 'hg-cpython', 'librusthg',
1298 py3_features='python3'),
1299 py3_features='python3'),
1299 ]
1300 ]
1300
1301
1301
1302
1302 sys.path.insert(0, 'contrib/python-zstandard')
1303 sys.path.insert(0, 'contrib/python-zstandard')
1303 import setup_zstd
1304 import setup_zstd
1304 extmodules.append(setup_zstd.get_c_extension(
1305 extmodules.append(setup_zstd.get_c_extension(
1305 name='mercurial.zstd',
1306 name='mercurial.zstd',
1306 root=os.path.abspath(os.path.dirname(__file__))))
1307 root=os.path.abspath(os.path.dirname(__file__))))
1307
1308
1308 try:
1309 try:
1309 from distutils import cygwinccompiler
1310 from distutils import cygwinccompiler
1310
1311
1311 # the -mno-cygwin option has been deprecated for years
1312 # the -mno-cygwin option has been deprecated for years
1312 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1313 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1313
1314
1314 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1315 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1315 def __init__(self, *args, **kwargs):
1316 def __init__(self, *args, **kwargs):
1316 mingw32compilerclass.__init__(self, *args, **kwargs)
1317 mingw32compilerclass.__init__(self, *args, **kwargs)
1317 for i in 'compiler compiler_so linker_exe linker_so'.split():
1318 for i in 'compiler compiler_so linker_exe linker_so'.split():
1318 try:
1319 try:
1319 getattr(self, i).remove('-mno-cygwin')
1320 getattr(self, i).remove('-mno-cygwin')
1320 except ValueError:
1321 except ValueError:
1321 pass
1322 pass
1322
1323
1323 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1324 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1324 except ImportError:
1325 except ImportError:
1325 # the cygwinccompiler package is not available on some Python
1326 # the cygwinccompiler package is not available on some Python
1326 # distributions like the ones from the optware project for Synology
1327 # distributions like the ones from the optware project for Synology
1327 # DiskStation boxes
1328 # DiskStation boxes
1328 class HackedMingw32CCompiler(object):
1329 class HackedMingw32CCompiler(object):
1329 pass
1330 pass
1330
1331
1331 if os.name == 'nt':
1332 if os.name == 'nt':
1332 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1333 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1333 # extra_link_args to distutils.extensions.Extension() doesn't have any
1334 # extra_link_args to distutils.extensions.Extension() doesn't have any
1334 # effect.
1335 # effect.
1335 from distutils import msvccompiler
1336 from distutils import msvccompiler
1336
1337
1337 msvccompilerclass = msvccompiler.MSVCCompiler
1338 msvccompilerclass = msvccompiler.MSVCCompiler
1338
1339
1339 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1340 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1340 def initialize(self):
1341 def initialize(self):
1341 msvccompilerclass.initialize(self)
1342 msvccompilerclass.initialize(self)
1342 # "warning LNK4197: export 'func' specified multiple times"
1343 # "warning LNK4197: export 'func' specified multiple times"
1343 self.ldflags_shared.append('/ignore:4197')
1344 self.ldflags_shared.append('/ignore:4197')
1344 self.ldflags_shared_debug.append('/ignore:4197')
1345 self.ldflags_shared_debug.append('/ignore:4197')
1345
1346
1346 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1347 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1347
1348
1348 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
1349 packagedata = {'mercurial': ['locale/*/LC_MESSAGES/hg.mo',
1349 'help/*.txt',
1350 'help/*.txt',
1350 'help/internals/*.txt',
1351 'help/internals/*.txt',
1351 'default.d/*.rc',
1352 'default.d/*.rc',
1352 'dummycert.pem']}
1353 'dummycert.pem']}
1353
1354
1354 def ordinarypath(p):
1355 def ordinarypath(p):
1355 return p and p[0] != '.' and p[-1] != '~'
1356 return p and p[0] != '.' and p[-1] != '~'
1356
1357
1357 for root in ('templates',):
1358 for root in ('templates',):
1358 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1359 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1359 curdir = curdir.split(os.sep, 1)[1]
1360 curdir = curdir.split(os.sep, 1)[1]
1360 dirs[:] = filter(ordinarypath, dirs)
1361 dirs[:] = filter(ordinarypath, dirs)
1361 for f in filter(ordinarypath, files):
1362 for f in filter(ordinarypath, files):
1362 f = os.path.join(curdir, f)
1363 f = os.path.join(curdir, f)
1363 packagedata['mercurial'].append(f)
1364 packagedata['mercurial'].append(f)
1364
1365
1365 datafiles = []
1366 datafiles = []
1366
1367
1367 # distutils expects version to be str/unicode. Converting it to
1368 # distutils expects version to be str/unicode. Converting it to
1368 # unicode on Python 2 still works because it won't contain any
1369 # unicode on Python 2 still works because it won't contain any
1369 # non-ascii bytes and will be implicitly converted back to bytes
1370 # non-ascii bytes and will be implicitly converted back to bytes
1370 # when operated on.
1371 # when operated on.
1371 assert isinstance(version, bytes)
1372 assert isinstance(version, bytes)
1372 setupversion = version.decode('ascii')
1373 setupversion = version.decode('ascii')
1373
1374
1374 extra = {}
1375 extra = {}
1375
1376
1376 py2exepackages = [
1377 py2exepackages = [
1377 'hgdemandimport',
1378 'hgdemandimport',
1378 'hgext3rd',
1379 'hgext3rd',
1379 'hgext',
1380 'hgext',
1380 'email',
1381 'email',
1381 # implicitly imported per module policy
1382 # implicitly imported per module policy
1382 # (cffi wouldn't be used as a frozen exe)
1383 # (cffi wouldn't be used as a frozen exe)
1383 'mercurial.cext',
1384 'mercurial.cext',
1384 #'mercurial.cffi',
1385 #'mercurial.cffi',
1385 'mercurial.pure',
1386 'mercurial.pure',
1386 ]
1387 ]
1387
1388
1388 py2exeexcludes = []
1389 py2exeexcludes = []
1389 py2exedllexcludes = ['crypt32.dll']
1390 py2exedllexcludes = ['crypt32.dll']
1390
1391
1391 if issetuptools:
1392 if issetuptools:
1392 extra['python_requires'] = supportedpy
1393 extra['python_requires'] = supportedpy
1393
1394
1394 if py2exeloaded:
1395 if py2exeloaded:
1395 extra['console'] = [
1396 extra['console'] = [
1396 {'script':'hg',
1397 {'script':'hg',
1397 'copyright':'Copyright (C) 2005-2019 Matt Mackall and others',
1398 'copyright':'Copyright (C) 2005-2019 Matt Mackall and others',
1398 'product_version':version}]
1399 'product_version':version}]
1399 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1400 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1400 # Need to override hgbuild because it has a private copy of
1401 # Need to override hgbuild because it has a private copy of
1401 # build.sub_commands.
1402 # build.sub_commands.
1402 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1403 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1403 # put dlls in sub directory so that they won't pollute PATH
1404 # put dlls in sub directory so that they won't pollute PATH
1404 extra['zipfile'] = 'lib/library.zip'
1405 extra['zipfile'] = 'lib/library.zip'
1405
1406
1406 # We allow some configuration to be supplemented via environment
1407 # We allow some configuration to be supplemented via environment
1407 # variables. This is better than setup.cfg files because it allows
1408 # variables. This is better than setup.cfg files because it allows
1408 # supplementing configs instead of replacing them.
1409 # supplementing configs instead of replacing them.
1409 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1410 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1410 if extrapackages:
1411 if extrapackages:
1411 py2exepackages.extend(extrapackages.split(' '))
1412 py2exepackages.extend(extrapackages.split(' '))
1412
1413
1413 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1414 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1414 if excludes:
1415 if excludes:
1415 py2exeexcludes.extend(excludes.split(' '))
1416 py2exeexcludes.extend(excludes.split(' '))
1416
1417
1417 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1418 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1418 if dllexcludes:
1419 if dllexcludes:
1419 py2exedllexcludes.extend(dllexcludes.split(' '))
1420 py2exedllexcludes.extend(dllexcludes.split(' '))
1420
1421
1421 if os.name == 'nt':
1422 if os.name == 'nt':
1422 # Windows binary file versions for exe/dll files must have the
1423 # Windows binary file versions for exe/dll files must have the
1423 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1424 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1424 setupversion = setupversion.split(r'+', 1)[0]
1425 setupversion = setupversion.split(r'+', 1)[0]
1425
1426
1426 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
1427 if sys.platform == 'darwin' and os.path.exists('/usr/bin/xcodebuild'):
1427 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[1].splitlines()
1428 version = runcmd(['/usr/bin/xcodebuild', '-version'], {})[1].splitlines()
1428 if version:
1429 if version:
1429 version = version[0]
1430 version = version[0]
1430 if sys.version_info[0] == 3:
1431 if sys.version_info[0] == 3:
1431 version = version.decode('utf-8')
1432 version = version.decode('utf-8')
1432 xcode4 = (version.startswith('Xcode') and
1433 xcode4 = (version.startswith('Xcode') and
1433 StrictVersion(version.split()[1]) >= StrictVersion('4.0'))
1434 StrictVersion(version.split()[1]) >= StrictVersion('4.0'))
1434 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
1435 xcode51 = re.match(r'^Xcode\s+5\.1', version) is not None
1435 else:
1436 else:
1436 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
1437 # xcodebuild returns empty on OS X Lion with XCode 4.3 not
1437 # installed, but instead with only command-line tools. Assume
1438 # installed, but instead with only command-line tools. Assume
1438 # that only happens on >= Lion, thus no PPC support.
1439 # that only happens on >= Lion, thus no PPC support.
1439 xcode4 = True
1440 xcode4 = True
1440 xcode51 = False
1441 xcode51 = False
1441
1442
1442 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
1443 # XCode 4.0 dropped support for ppc architecture, which is hardcoded in
1443 # distutils.sysconfig
1444 # distutils.sysconfig
1444 if xcode4:
1445 if xcode4:
1445 os.environ['ARCHFLAGS'] = ''
1446 os.environ['ARCHFLAGS'] = ''
1446
1447
1447 # XCode 5.1 changes clang such that it now fails to compile if the
1448 # XCode 5.1 changes clang such that it now fails to compile if the
1448 # -mno-fused-madd flag is passed, but the version of Python shipped with
1449 # -mno-fused-madd flag is passed, but the version of Python shipped with
1449 # OS X 10.9 Mavericks includes this flag. This causes problems in all
1450 # OS X 10.9 Mavericks includes this flag. This causes problems in all
1450 # C extension modules, and a bug has been filed upstream at
1451 # C extension modules, and a bug has been filed upstream at
1451 # http://bugs.python.org/issue21244. We also need to patch this here
1452 # http://bugs.python.org/issue21244. We also need to patch this here
1452 # so Mercurial can continue to compile in the meantime.
1453 # so Mercurial can continue to compile in the meantime.
1453 if xcode51:
1454 if xcode51:
1454 cflags = get_config_var('CFLAGS')
1455 cflags = get_config_var('CFLAGS')
1455 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
1456 if cflags and re.search(r'-mno-fused-madd\b', cflags) is not None:
1456 os.environ['CFLAGS'] = (
1457 os.environ['CFLAGS'] = (
1457 os.environ.get('CFLAGS', '') + ' -Qunused-arguments')
1458 os.environ.get('CFLAGS', '') + ' -Qunused-arguments')
1458
1459
1459 setup(name='mercurial',
1460 setup(name='mercurial',
1460 version=setupversion,
1461 version=setupversion,
1461 author='Matt Mackall and many others',
1462 author='Matt Mackall and many others',
1462 author_email='mercurial@mercurial-scm.org',
1463 author_email='mercurial@mercurial-scm.org',
1463 url='https://mercurial-scm.org/',
1464 url='https://mercurial-scm.org/',
1464 download_url='https://mercurial-scm.org/release/',
1465 download_url='https://mercurial-scm.org/release/',
1465 description=('Fast scalable distributed SCM (revision control, version '
1466 description=('Fast scalable distributed SCM (revision control, version '
1466 'control) system'),
1467 'control) system'),
1467 long_description=('Mercurial is a distributed SCM tool written in Python.'
1468 long_description=('Mercurial is a distributed SCM tool written in Python.'
1468 ' It is used by a number of large projects that require'
1469 ' It is used by a number of large projects that require'
1469 ' fast, reliable distributed revision control, such as '
1470 ' fast, reliable distributed revision control, such as '
1470 'Mozilla.'),
1471 'Mozilla.'),
1471 license='GNU GPLv2 or any later version',
1472 license='GNU GPLv2 or any later version',
1472 classifiers=[
1473 classifiers=[
1473 'Development Status :: 6 - Mature',
1474 'Development Status :: 6 - Mature',
1474 'Environment :: Console',
1475 'Environment :: Console',
1475 'Intended Audience :: Developers',
1476 'Intended Audience :: Developers',
1476 'Intended Audience :: System Administrators',
1477 'Intended Audience :: System Administrators',
1477 'License :: OSI Approved :: GNU General Public License (GPL)',
1478 'License :: OSI Approved :: GNU General Public License (GPL)',
1478 'Natural Language :: Danish',
1479 'Natural Language :: Danish',
1479 'Natural Language :: English',
1480 'Natural Language :: English',
1480 'Natural Language :: German',
1481 'Natural Language :: German',
1481 'Natural Language :: Italian',
1482 'Natural Language :: Italian',
1482 'Natural Language :: Japanese',
1483 'Natural Language :: Japanese',
1483 'Natural Language :: Portuguese (Brazilian)',
1484 'Natural Language :: Portuguese (Brazilian)',
1484 'Operating System :: Microsoft :: Windows',
1485 'Operating System :: Microsoft :: Windows',
1485 'Operating System :: OS Independent',
1486 'Operating System :: OS Independent',
1486 'Operating System :: POSIX',
1487 'Operating System :: POSIX',
1487 'Programming Language :: C',
1488 'Programming Language :: C',
1488 'Programming Language :: Python',
1489 'Programming Language :: Python',
1489 'Topic :: Software Development :: Version Control',
1490 'Topic :: Software Development :: Version Control',
1490 ],
1491 ],
1491 scripts=scripts,
1492 scripts=scripts,
1492 packages=packages,
1493 packages=packages,
1493 ext_modules=extmodules,
1494 ext_modules=extmodules,
1494 data_files=datafiles,
1495 data_files=datafiles,
1495 package_data=packagedata,
1496 package_data=packagedata,
1496 cmdclass=cmdclass,
1497 cmdclass=cmdclass,
1497 distclass=hgdist,
1498 distclass=hgdist,
1498 options={
1499 options={
1499 'py2exe': {
1500 'py2exe': {
1500 'bundle_files': 3,
1501 'bundle_files': 3,
1501 'dll_excludes': py2exedllexcludes,
1502 'dll_excludes': py2exedllexcludes,
1502 'excludes': py2exeexcludes,
1503 'excludes': py2exeexcludes,
1503 'packages': py2exepackages,
1504 'packages': py2exepackages,
1504 },
1505 },
1505 'bdist_mpkg': {
1506 'bdist_mpkg': {
1506 'zipdist': False,
1507 'zipdist': False,
1507 'license': 'COPYING',
1508 'license': 'COPYING',
1508 'readme': 'contrib/packaging/macosx/Readme.html',
1509 'readme': 'contrib/packaging/macosx/Readme.html',
1509 'welcome': 'contrib/packaging/macosx/Welcome.html',
1510 'welcome': 'contrib/packaging/macosx/Welcome.html',
1510 },
1511 },
1511 },
1512 },
1512 **extra)
1513 **extra)
@@ -1,24 +1,25
1 # Disable the $CAP wire protocol capability.
1 # Disable the $CAP wire protocol capability.
2
2
3 if test -z "$CAP"
3 if test -z "$CAP"
4 then
4 then
5 echo "CAP environment variable not set."
5 echo "CAP environment variable not set."
6 fi
6 fi
7
7
8 cat > notcapable-$CAP.py << EOF
8 cat > notcapable-$CAP.py << EOF
9 from mercurial import extensions, localrepo, repository
9 from mercurial import extensions, localrepo
10 from mercurial.interfaces import repository
10 def extsetup(ui):
11 def extsetup(ui):
11 extensions.wrapfunction(repository.peer, 'capable', wrapcapable)
12 extensions.wrapfunction(repository.peer, 'capable', wrapcapable)
12 extensions.wrapfunction(localrepo.localrepository, 'peer', wrappeer)
13 extensions.wrapfunction(localrepo.localrepository, 'peer', wrappeer)
13 def wrapcapable(orig, self, name, *args, **kwargs):
14 def wrapcapable(orig, self, name, *args, **kwargs):
14 if name in b'$CAP'.split(b' '):
15 if name in b'$CAP'.split(b' '):
15 return False
16 return False
16 return orig(self, name, *args, **kwargs)
17 return orig(self, name, *args, **kwargs)
17 def wrappeer(orig, self):
18 def wrappeer(orig, self):
18 # Since we're disabling some newer features, we need to make sure local
19 # Since we're disabling some newer features, we need to make sure local
19 # repos add in the legacy features again.
20 # repos add in the legacy features again.
20 return localrepo.locallegacypeer(self)
21 return localrepo.locallegacypeer(self)
21 EOF
22 EOF
22
23
23 echo '[extensions]' >> $HGRCPATH
24 echo '[extensions]' >> $HGRCPATH
24 echo "notcapable-$CAP = `pwd`/notcapable-$CAP.py" >> $HGRCPATH
25 echo "notcapable-$CAP = `pwd`/notcapable-$CAP.py" >> $HGRCPATH
@@ -1,50 +1,52
1 # pullext.py - Simple extension to test pulling
1 # pullext.py - Simple extension to test pulling
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import (
11 from mercurial import (
12 commands,
12 commands,
13 error,
13 error,
14 extensions,
14 extensions,
15 localrepo,
15 localrepo,
16 )
17 from mercurial.interfaces import (
16 repository,
18 repository,
17 )
19 )
18
20
19 def clonecommand(orig, ui, repo, *args, **kwargs):
21 def clonecommand(orig, ui, repo, *args, **kwargs):
20 if kwargs.get(r'include') or kwargs.get(r'exclude'):
22 if kwargs.get(r'include') or kwargs.get(r'exclude'):
21 kwargs[r'narrow'] = True
23 kwargs[r'narrow'] = True
22
24
23 if kwargs.get(r'depth'):
25 if kwargs.get(r'depth'):
24 try:
26 try:
25 kwargs[r'depth'] = int(kwargs[r'depth'])
27 kwargs[r'depth'] = int(kwargs[r'depth'])
26 except ValueError:
28 except ValueError:
27 raise error.Abort(_('--depth must be an integer'))
29 raise error.Abort(_('--depth must be an integer'))
28
30
29 return orig(ui, repo, *args, **kwargs)
31 return orig(ui, repo, *args, **kwargs)
30
32
31 def featuresetup(ui, features):
33 def featuresetup(ui, features):
32 features.add(repository.NARROW_REQUIREMENT)
34 features.add(repository.NARROW_REQUIREMENT)
33
35
34 def extsetup(ui):
36 def extsetup(ui):
35 entry = extensions.wrapcommand(commands.table, b'clone', clonecommand)
37 entry = extensions.wrapcommand(commands.table, b'clone', clonecommand)
36
38
37 hasinclude = any(x[1] == b'include' for x in entry[1])
39 hasinclude = any(x[1] == b'include' for x in entry[1])
38 hasdepth = any(x[1] == b'depth' for x in entry[1])
40 hasdepth = any(x[1] == b'depth' for x in entry[1])
39
41
40 if not hasinclude:
42 if not hasinclude:
41 entry[1].append((b'', b'include', [],
43 entry[1].append((b'', b'include', [],
42 _(b'pattern of file/directory to clone')))
44 _(b'pattern of file/directory to clone')))
43 entry[1].append((b'', b'exclude', [],
45 entry[1].append((b'', b'exclude', [],
44 _(b'pattern of file/directory to not clone')))
46 _(b'pattern of file/directory to not clone')))
45
47
46 if not hasdepth:
48 if not hasdepth:
47 entry[1].append((b'', b'depth', b'',
49 entry[1].append((b'', b'depth', b'',
48 _(b'ancestry depth of changesets to fetch')))
50 _(b'ancestry depth of changesets to fetch')))
49
51
50 localrepo.featuresetupfuncs.add(featuresetup)
52 localrepo.featuresetupfuncs.add(featuresetup)
@@ -1,711 +1,713
1 # simplestorerepo.py - Extension that swaps in alternate repository storage.
1 # simplestorerepo.py - Extension that swaps in alternate repository storage.
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 # To use this with the test suite:
8 # To use this with the test suite:
9 #
9 #
10 # $ HGREPOFEATURES="simplestore" ./run-tests.py \
10 # $ HGREPOFEATURES="simplestore" ./run-tests.py \
11 # --extra-config-opt extensions.simplestore=`pwd`/simplestorerepo.py
11 # --extra-config-opt extensions.simplestore=`pwd`/simplestorerepo.py
12
12
13 from __future__ import absolute_import
13 from __future__ import absolute_import
14
14
15 import stat
15 import stat
16
16
17 from mercurial.i18n import _
17 from mercurial.i18n import _
18 from mercurial.node import (
18 from mercurial.node import (
19 bin,
19 bin,
20 hex,
20 hex,
21 nullid,
21 nullid,
22 nullrev,
22 nullrev,
23 )
23 )
24 from mercurial.thirdparty import (
24 from mercurial.thirdparty import (
25 attr,
25 attr,
26 )
26 )
27 from mercurial import (
27 from mercurial import (
28 ancestor,
28 ancestor,
29 bundlerepo,
29 bundlerepo,
30 error,
30 error,
31 extensions,
31 extensions,
32 localrepo,
32 localrepo,
33 mdiff,
33 mdiff,
34 pycompat,
34 pycompat,
35 repository,
36 revlog,
35 revlog,
37 store,
36 store,
38 verify,
37 verify,
39 )
38 )
39 from mercurial.interfaces import (
40 repository,
41 )
40 from mercurial.utils import (
42 from mercurial.utils import (
41 cborutil,
43 cborutil,
42 interfaceutil,
44 interfaceutil,
43 storageutil,
45 storageutil,
44 )
46 )
45 from mercurial.revlogutils import (
47 from mercurial.revlogutils import (
46 flagutil,
48 flagutil,
47 )
49 )
48
50
49 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
51 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
50 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
52 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
51 # be specifying the version(s) of Mercurial they are tested with, or
53 # be specifying the version(s) of Mercurial they are tested with, or
52 # leave the attribute unspecified.
54 # leave the attribute unspecified.
53 testedwith = 'ships-with-hg-core'
55 testedwith = 'ships-with-hg-core'
54
56
55 REQUIREMENT = 'testonly-simplestore'
57 REQUIREMENT = 'testonly-simplestore'
56
58
57 def validatenode(node):
59 def validatenode(node):
58 if isinstance(node, int):
60 if isinstance(node, int):
59 raise ValueError('expected node; got int')
61 raise ValueError('expected node; got int')
60
62
61 if len(node) != 20:
63 if len(node) != 20:
62 raise ValueError('expected 20 byte node')
64 raise ValueError('expected 20 byte node')
63
65
64 def validaterev(rev):
66 def validaterev(rev):
65 if not isinstance(rev, int):
67 if not isinstance(rev, int):
66 raise ValueError('expected int')
68 raise ValueError('expected int')
67
69
68 class simplestoreerror(error.StorageError):
70 class simplestoreerror(error.StorageError):
69 pass
71 pass
70
72
71 @interfaceutil.implementer(repository.irevisiondelta)
73 @interfaceutil.implementer(repository.irevisiondelta)
72 @attr.s(slots=True)
74 @attr.s(slots=True)
73 class simplestorerevisiondelta(object):
75 class simplestorerevisiondelta(object):
74 node = attr.ib()
76 node = attr.ib()
75 p1node = attr.ib()
77 p1node = attr.ib()
76 p2node = attr.ib()
78 p2node = attr.ib()
77 basenode = attr.ib()
79 basenode = attr.ib()
78 flags = attr.ib()
80 flags = attr.ib()
79 baserevisionsize = attr.ib()
81 baserevisionsize = attr.ib()
80 revision = attr.ib()
82 revision = attr.ib()
81 delta = attr.ib()
83 delta = attr.ib()
82 linknode = attr.ib(default=None)
84 linknode = attr.ib(default=None)
83
85
84 @interfaceutil.implementer(repository.iverifyproblem)
86 @interfaceutil.implementer(repository.iverifyproblem)
85 @attr.s(frozen=True)
87 @attr.s(frozen=True)
86 class simplefilestoreproblem(object):
88 class simplefilestoreproblem(object):
87 warning = attr.ib(default=None)
89 warning = attr.ib(default=None)
88 error = attr.ib(default=None)
90 error = attr.ib(default=None)
89 node = attr.ib(default=None)
91 node = attr.ib(default=None)
90
92
91 @interfaceutil.implementer(repository.ifilestorage)
93 @interfaceutil.implementer(repository.ifilestorage)
92 class filestorage(object):
94 class filestorage(object):
93 """Implements storage for a tracked path.
95 """Implements storage for a tracked path.
94
96
95 Data is stored in the VFS in a directory corresponding to the tracked
97 Data is stored in the VFS in a directory corresponding to the tracked
96 path.
98 path.
97
99
98 Index data is stored in an ``index`` file using CBOR.
100 Index data is stored in an ``index`` file using CBOR.
99
101
100 Fulltext data is stored in files having names of the node.
102 Fulltext data is stored in files having names of the node.
101 """
103 """
102
104
103 def __init__(self, svfs, path):
105 def __init__(self, svfs, path):
104 self._svfs = svfs
106 self._svfs = svfs
105 self._path = path
107 self._path = path
106
108
107 self._storepath = b'/'.join([b'data', path])
109 self._storepath = b'/'.join([b'data', path])
108 self._indexpath = b'/'.join([self._storepath, b'index'])
110 self._indexpath = b'/'.join([self._storepath, b'index'])
109
111
110 indexdata = self._svfs.tryread(self._indexpath)
112 indexdata = self._svfs.tryread(self._indexpath)
111 if indexdata:
113 if indexdata:
112 indexdata = cborutil.decodeall(indexdata)
114 indexdata = cborutil.decodeall(indexdata)
113
115
114 self._indexdata = indexdata or []
116 self._indexdata = indexdata or []
115 self._indexbynode = {}
117 self._indexbynode = {}
116 self._indexbyrev = {}
118 self._indexbyrev = {}
117 self._index = []
119 self._index = []
118 self._refreshindex()
120 self._refreshindex()
119
121
120 def _refreshindex(self):
122 def _refreshindex(self):
121 self._indexbynode.clear()
123 self._indexbynode.clear()
122 self._indexbyrev.clear()
124 self._indexbyrev.clear()
123 self._index = []
125 self._index = []
124
126
125 for i, entry in enumerate(self._indexdata):
127 for i, entry in enumerate(self._indexdata):
126 self._indexbynode[entry[b'node']] = entry
128 self._indexbynode[entry[b'node']] = entry
127 self._indexbyrev[i] = entry
129 self._indexbyrev[i] = entry
128
130
129 self._indexbynode[nullid] = {
131 self._indexbynode[nullid] = {
130 b'node': nullid,
132 b'node': nullid,
131 b'p1': nullid,
133 b'p1': nullid,
132 b'p2': nullid,
134 b'p2': nullid,
133 b'linkrev': nullrev,
135 b'linkrev': nullrev,
134 b'flags': 0,
136 b'flags': 0,
135 }
137 }
136
138
137 self._indexbyrev[nullrev] = {
139 self._indexbyrev[nullrev] = {
138 b'node': nullid,
140 b'node': nullid,
139 b'p1': nullid,
141 b'p1': nullid,
140 b'p2': nullid,
142 b'p2': nullid,
141 b'linkrev': nullrev,
143 b'linkrev': nullrev,
142 b'flags': 0,
144 b'flags': 0,
143 }
145 }
144
146
145 for i, entry in enumerate(self._indexdata):
147 for i, entry in enumerate(self._indexdata):
146 p1rev, p2rev = self.parentrevs(self.rev(entry[b'node']))
148 p1rev, p2rev = self.parentrevs(self.rev(entry[b'node']))
147
149
148 # start, length, rawsize, chainbase, linkrev, p1, p2, node
150 # start, length, rawsize, chainbase, linkrev, p1, p2, node
149 self._index.append((0, 0, 0, -1, entry[b'linkrev'], p1rev, p2rev,
151 self._index.append((0, 0, 0, -1, entry[b'linkrev'], p1rev, p2rev,
150 entry[b'node']))
152 entry[b'node']))
151
153
152 self._index.append((0, 0, 0, -1, -1, -1, -1, nullid))
154 self._index.append((0, 0, 0, -1, -1, -1, -1, nullid))
153
155
154 def __len__(self):
156 def __len__(self):
155 return len(self._indexdata)
157 return len(self._indexdata)
156
158
157 def __iter__(self):
159 def __iter__(self):
158 return iter(range(len(self)))
160 return iter(range(len(self)))
159
161
160 def revs(self, start=0, stop=None):
162 def revs(self, start=0, stop=None):
161 step = 1
163 step = 1
162 if stop is not None:
164 if stop is not None:
163 if start > stop:
165 if start > stop:
164 step = -1
166 step = -1
165
167
166 stop += step
168 stop += step
167 else:
169 else:
168 stop = len(self)
170 stop = len(self)
169
171
170 return range(start, stop, step)
172 return range(start, stop, step)
171
173
172 def parents(self, node):
174 def parents(self, node):
173 validatenode(node)
175 validatenode(node)
174
176
175 if node not in self._indexbynode:
177 if node not in self._indexbynode:
176 raise KeyError('unknown node')
178 raise KeyError('unknown node')
177
179
178 entry = self._indexbynode[node]
180 entry = self._indexbynode[node]
179
181
180 return entry[b'p1'], entry[b'p2']
182 return entry[b'p1'], entry[b'p2']
181
183
182 def parentrevs(self, rev):
184 def parentrevs(self, rev):
183 p1, p2 = self.parents(self._indexbyrev[rev][b'node'])
185 p1, p2 = self.parents(self._indexbyrev[rev][b'node'])
184 return self.rev(p1), self.rev(p2)
186 return self.rev(p1), self.rev(p2)
185
187
186 def rev(self, node):
188 def rev(self, node):
187 validatenode(node)
189 validatenode(node)
188
190
189 try:
191 try:
190 self._indexbynode[node]
192 self._indexbynode[node]
191 except KeyError:
193 except KeyError:
192 raise error.LookupError(node, self._indexpath, _('no node'))
194 raise error.LookupError(node, self._indexpath, _('no node'))
193
195
194 for rev, entry in self._indexbyrev.items():
196 for rev, entry in self._indexbyrev.items():
195 if entry[b'node'] == node:
197 if entry[b'node'] == node:
196 return rev
198 return rev
197
199
198 raise error.ProgrammingError('this should not occur')
200 raise error.ProgrammingError('this should not occur')
199
201
200 def node(self, rev):
202 def node(self, rev):
201 validaterev(rev)
203 validaterev(rev)
202
204
203 return self._indexbyrev[rev][b'node']
205 return self._indexbyrev[rev][b'node']
204
206
205 def hasnode(self, node):
207 def hasnode(self, node):
206 validatenode(node)
208 validatenode(node)
207 return node in self._indexbynode
209 return node in self._indexbynode
208
210
209 def censorrevision(self, tr, censornode, tombstone=b''):
211 def censorrevision(self, tr, censornode, tombstone=b''):
210 raise NotImplementedError('TODO')
212 raise NotImplementedError('TODO')
211
213
212 def lookup(self, node):
214 def lookup(self, node):
213 if isinstance(node, int):
215 if isinstance(node, int):
214 return self.node(node)
216 return self.node(node)
215
217
216 if len(node) == 20:
218 if len(node) == 20:
217 self.rev(node)
219 self.rev(node)
218 return node
220 return node
219
221
220 try:
222 try:
221 rev = int(node)
223 rev = int(node)
222 if '%d' % rev != node:
224 if '%d' % rev != node:
223 raise ValueError
225 raise ValueError
224
226
225 if rev < 0:
227 if rev < 0:
226 rev = len(self) + rev
228 rev = len(self) + rev
227 if rev < 0 or rev >= len(self):
229 if rev < 0 or rev >= len(self):
228 raise ValueError
230 raise ValueError
229
231
230 return self.node(rev)
232 return self.node(rev)
231 except (ValueError, OverflowError):
233 except (ValueError, OverflowError):
232 pass
234 pass
233
235
234 if len(node) == 40:
236 if len(node) == 40:
235 try:
237 try:
236 rawnode = bin(node)
238 rawnode = bin(node)
237 self.rev(rawnode)
239 self.rev(rawnode)
238 return rawnode
240 return rawnode
239 except TypeError:
241 except TypeError:
240 pass
242 pass
241
243
242 raise error.LookupError(node, self._path, _('invalid lookup input'))
244 raise error.LookupError(node, self._path, _('invalid lookup input'))
243
245
244 def linkrev(self, rev):
246 def linkrev(self, rev):
245 validaterev(rev)
247 validaterev(rev)
246
248
247 return self._indexbyrev[rev][b'linkrev']
249 return self._indexbyrev[rev][b'linkrev']
248
250
249 def _flags(self, rev):
251 def _flags(self, rev):
250 validaterev(rev)
252 validaterev(rev)
251
253
252 return self._indexbyrev[rev][b'flags']
254 return self._indexbyrev[rev][b'flags']
253
255
254 def _candelta(self, baserev, rev):
256 def _candelta(self, baserev, rev):
255 validaterev(baserev)
257 validaterev(baserev)
256 validaterev(rev)
258 validaterev(rev)
257
259
258 if ((self._flags(baserev) & revlog.REVIDX_RAWTEXT_CHANGING_FLAGS)
260 if ((self._flags(baserev) & revlog.REVIDX_RAWTEXT_CHANGING_FLAGS)
259 or (self._flags(rev) & revlog.REVIDX_RAWTEXT_CHANGING_FLAGS)):
261 or (self._flags(rev) & revlog.REVIDX_RAWTEXT_CHANGING_FLAGS)):
260 return False
262 return False
261
263
262 return True
264 return True
263
265
264 def _processflags(self, text, flags, operation, raw=False):
266 def _processflags(self, text, flags, operation, raw=False):
265 if flags == 0:
267 if flags == 0:
266 return text, True
268 return text, True
267
269
268 if flags & ~flagutil.REVIDX_KNOWN_FLAGS:
270 if flags & ~flagutil.REVIDX_KNOWN_FLAGS:
269 raise simplestoreerror(_("incompatible revision flag '%#x'") %
271 raise simplestoreerror(_("incompatible revision flag '%#x'") %
270 (flags & ~flagutil.REVIDX_KNOWN_FLAGS))
272 (flags & ~flagutil.REVIDX_KNOWN_FLAGS))
271
273
272 validatehash = True
274 validatehash = True
273 # Depending on the operation (read or write), the order might be
275 # Depending on the operation (read or write), the order might be
274 # reversed due to non-commutative transforms.
276 # reversed due to non-commutative transforms.
275 orderedflags = revlog.REVIDX_FLAGS_ORDER
277 orderedflags = revlog.REVIDX_FLAGS_ORDER
276 if operation == 'write':
278 if operation == 'write':
277 orderedflags = reversed(orderedflags)
279 orderedflags = reversed(orderedflags)
278
280
279 for flag in orderedflags:
281 for flag in orderedflags:
280 # If a flagprocessor has been registered for a known flag, apply the
282 # If a flagprocessor has been registered for a known flag, apply the
281 # related operation transform and update result tuple.
283 # related operation transform and update result tuple.
282 if flag & flags:
284 if flag & flags:
283 vhash = True
285 vhash = True
284
286
285 if flag not in revlog._flagprocessors:
287 if flag not in revlog._flagprocessors:
286 message = _("missing processor for flag '%#x'") % (flag)
288 message = _("missing processor for flag '%#x'") % (flag)
287 raise simplestoreerror(message)
289 raise simplestoreerror(message)
288
290
289 processor = revlog._flagprocessors[flag]
291 processor = revlog._flagprocessors[flag]
290 if processor is not None:
292 if processor is not None:
291 readtransform, writetransform, rawtransform = processor
293 readtransform, writetransform, rawtransform = processor
292
294
293 if raw:
295 if raw:
294 vhash = rawtransform(self, text)
296 vhash = rawtransform(self, text)
295 elif operation == 'read':
297 elif operation == 'read':
296 text, vhash = readtransform(self, text)
298 text, vhash = readtransform(self, text)
297 else: # write operation
299 else: # write operation
298 text, vhash = writetransform(self, text)
300 text, vhash = writetransform(self, text)
299 validatehash = validatehash and vhash
301 validatehash = validatehash and vhash
300
302
301 return text, validatehash
303 return text, validatehash
302
304
303 def checkhash(self, text, node, p1=None, p2=None, rev=None):
305 def checkhash(self, text, node, p1=None, p2=None, rev=None):
304 if p1 is None and p2 is None:
306 if p1 is None and p2 is None:
305 p1, p2 = self.parents(node)
307 p1, p2 = self.parents(node)
306 if node != storageutil.hashrevisionsha1(text, p1, p2):
308 if node != storageutil.hashrevisionsha1(text, p1, p2):
307 raise simplestoreerror(_("integrity check failed on %s") %
309 raise simplestoreerror(_("integrity check failed on %s") %
308 self._path)
310 self._path)
309
311
310 def revision(self, nodeorrev, raw=False):
312 def revision(self, nodeorrev, raw=False):
311 if isinstance(nodeorrev, int):
313 if isinstance(nodeorrev, int):
312 node = self.node(nodeorrev)
314 node = self.node(nodeorrev)
313 else:
315 else:
314 node = nodeorrev
316 node = nodeorrev
315 validatenode(node)
317 validatenode(node)
316
318
317 if node == nullid:
319 if node == nullid:
318 return b''
320 return b''
319
321
320 rev = self.rev(node)
322 rev = self.rev(node)
321 flags = self._flags(rev)
323 flags = self._flags(rev)
322
324
323 path = b'/'.join([self._storepath, hex(node)])
325 path = b'/'.join([self._storepath, hex(node)])
324 rawtext = self._svfs.read(path)
326 rawtext = self._svfs.read(path)
325
327
326 text, validatehash = self._processflags(rawtext, flags, 'read', raw=raw)
328 text, validatehash = self._processflags(rawtext, flags, 'read', raw=raw)
327 if validatehash:
329 if validatehash:
328 self.checkhash(text, node, rev=rev)
330 self.checkhash(text, node, rev=rev)
329
331
330 return text
332 return text
331
333
332 def rawdata(self, nodeorrev):
334 def rawdata(self, nodeorrev):
333 return self.revision(raw=True)
335 return self.revision(raw=True)
334
336
335 def read(self, node):
337 def read(self, node):
336 validatenode(node)
338 validatenode(node)
337
339
338 revision = self.revision(node)
340 revision = self.revision(node)
339
341
340 if not revision.startswith(b'\1\n'):
342 if not revision.startswith(b'\1\n'):
341 return revision
343 return revision
342
344
343 start = revision.index(b'\1\n', 2)
345 start = revision.index(b'\1\n', 2)
344 return revision[start + 2:]
346 return revision[start + 2:]
345
347
346 def renamed(self, node):
348 def renamed(self, node):
347 validatenode(node)
349 validatenode(node)
348
350
349 if self.parents(node)[0] != nullid:
351 if self.parents(node)[0] != nullid:
350 return False
352 return False
351
353
352 fulltext = self.revision(node)
354 fulltext = self.revision(node)
353 m = storageutil.parsemeta(fulltext)[0]
355 m = storageutil.parsemeta(fulltext)[0]
354
356
355 if m and 'copy' in m:
357 if m and 'copy' in m:
356 return m['copy'], bin(m['copyrev'])
358 return m['copy'], bin(m['copyrev'])
357
359
358 return False
360 return False
359
361
360 def cmp(self, node, text):
362 def cmp(self, node, text):
361 validatenode(node)
363 validatenode(node)
362
364
363 t = text
365 t = text
364
366
365 if text.startswith(b'\1\n'):
367 if text.startswith(b'\1\n'):
366 t = b'\1\n\1\n' + text
368 t = b'\1\n\1\n' + text
367
369
368 p1, p2 = self.parents(node)
370 p1, p2 = self.parents(node)
369
371
370 if storageutil.hashrevisionsha1(t, p1, p2) == node:
372 if storageutil.hashrevisionsha1(t, p1, p2) == node:
371 return False
373 return False
372
374
373 if self.iscensored(self.rev(node)):
375 if self.iscensored(self.rev(node)):
374 return text != b''
376 return text != b''
375
377
376 if self.renamed(node):
378 if self.renamed(node):
377 t2 = self.read(node)
379 t2 = self.read(node)
378 return t2 != text
380 return t2 != text
379
381
380 return True
382 return True
381
383
382 def size(self, rev):
384 def size(self, rev):
383 validaterev(rev)
385 validaterev(rev)
384
386
385 node = self._indexbyrev[rev][b'node']
387 node = self._indexbyrev[rev][b'node']
386
388
387 if self.renamed(node):
389 if self.renamed(node):
388 return len(self.read(node))
390 return len(self.read(node))
389
391
390 if self.iscensored(rev):
392 if self.iscensored(rev):
391 return 0
393 return 0
392
394
393 return len(self.revision(node))
395 return len(self.revision(node))
394
396
395 def iscensored(self, rev):
397 def iscensored(self, rev):
396 validaterev(rev)
398 validaterev(rev)
397
399
398 return self._flags(rev) & repository.REVISION_FLAG_CENSORED
400 return self._flags(rev) & repository.REVISION_FLAG_CENSORED
399
401
400 def commonancestorsheads(self, a, b):
402 def commonancestorsheads(self, a, b):
401 validatenode(a)
403 validatenode(a)
402 validatenode(b)
404 validatenode(b)
403
405
404 a = self.rev(a)
406 a = self.rev(a)
405 b = self.rev(b)
407 b = self.rev(b)
406
408
407 ancestors = ancestor.commonancestorsheads(self.parentrevs, a, b)
409 ancestors = ancestor.commonancestorsheads(self.parentrevs, a, b)
408 return pycompat.maplist(self.node, ancestors)
410 return pycompat.maplist(self.node, ancestors)
409
411
410 def descendants(self, revs):
412 def descendants(self, revs):
411 # This is a copy of revlog.descendants()
413 # This is a copy of revlog.descendants()
412 first = min(revs)
414 first = min(revs)
413 if first == nullrev:
415 if first == nullrev:
414 for i in self:
416 for i in self:
415 yield i
417 yield i
416 return
418 return
417
419
418 seen = set(revs)
420 seen = set(revs)
419 for i in self.revs(start=first + 1):
421 for i in self.revs(start=first + 1):
420 for x in self.parentrevs(i):
422 for x in self.parentrevs(i):
421 if x != nullrev and x in seen:
423 if x != nullrev and x in seen:
422 seen.add(i)
424 seen.add(i)
423 yield i
425 yield i
424 break
426 break
425
427
426 # Required by verify.
428 # Required by verify.
427 def files(self):
429 def files(self):
428 entries = self._svfs.listdir(self._storepath)
430 entries = self._svfs.listdir(self._storepath)
429
431
430 # Strip out undo.backup.* files created as part of transaction
432 # Strip out undo.backup.* files created as part of transaction
431 # recording.
433 # recording.
432 entries = [f for f in entries if not f.startswith('undo.backup.')]
434 entries = [f for f in entries if not f.startswith('undo.backup.')]
433
435
434 return [b'/'.join((self._storepath, f)) for f in entries]
436 return [b'/'.join((self._storepath, f)) for f in entries]
435
437
436 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
438 def storageinfo(self, exclusivefiles=False, sharedfiles=False,
437 revisionscount=False, trackedsize=False,
439 revisionscount=False, trackedsize=False,
438 storedsize=False):
440 storedsize=False):
439 # TODO do a real implementation of this
441 # TODO do a real implementation of this
440 return {
442 return {
441 'exclusivefiles': [],
443 'exclusivefiles': [],
442 'sharedfiles': [],
444 'sharedfiles': [],
443 'revisionscount': len(self),
445 'revisionscount': len(self),
444 'trackedsize': 0,
446 'trackedsize': 0,
445 'storedsize': None,
447 'storedsize': None,
446 }
448 }
447
449
448 def verifyintegrity(self, state):
450 def verifyintegrity(self, state):
449 state['skipread'] = set()
451 state['skipread'] = set()
450 for rev in self:
452 for rev in self:
451 node = self.node(rev)
453 node = self.node(rev)
452 try:
454 try:
453 self.revision(node)
455 self.revision(node)
454 except Exception as e:
456 except Exception as e:
455 yield simplefilestoreproblem(
457 yield simplefilestoreproblem(
456 error='unpacking %s: %s' % (node, e),
458 error='unpacking %s: %s' % (node, e),
457 node=node)
459 node=node)
458 state['skipread'].add(node)
460 state['skipread'].add(node)
459
461
460 def emitrevisions(self, nodes, nodesorder=None, revisiondata=False,
462 def emitrevisions(self, nodes, nodesorder=None, revisiondata=False,
461 assumehaveparentrevisions=False,
463 assumehaveparentrevisions=False,
462 deltamode=repository.CG_DELTAMODE_STD):
464 deltamode=repository.CG_DELTAMODE_STD):
463 # TODO this will probably break on some ordering options.
465 # TODO this will probably break on some ordering options.
464 nodes = [n for n in nodes if n != nullid]
466 nodes = [n for n in nodes if n != nullid]
465 if not nodes:
467 if not nodes:
466 return
468 return
467 for delta in storageutil.emitrevisions(
469 for delta in storageutil.emitrevisions(
468 self, nodes, nodesorder, simplestorerevisiondelta,
470 self, nodes, nodesorder, simplestorerevisiondelta,
469 revisiondata=revisiondata,
471 revisiondata=revisiondata,
470 assumehaveparentrevisions=assumehaveparentrevisions,
472 assumehaveparentrevisions=assumehaveparentrevisions,
471 deltamode=deltamode):
473 deltamode=deltamode):
472 yield delta
474 yield delta
473
475
474 def add(self, text, meta, transaction, linkrev, p1, p2):
476 def add(self, text, meta, transaction, linkrev, p1, p2):
475 if meta or text.startswith(b'\1\n'):
477 if meta or text.startswith(b'\1\n'):
476 text = storageutil.packmeta(meta, text)
478 text = storageutil.packmeta(meta, text)
477
479
478 return self.addrevision(text, transaction, linkrev, p1, p2)
480 return self.addrevision(text, transaction, linkrev, p1, p2)
479
481
480 def addrevision(self, text, transaction, linkrev, p1, p2, node=None,
482 def addrevision(self, text, transaction, linkrev, p1, p2, node=None,
481 flags=revlog.REVIDX_DEFAULT_FLAGS, cachedelta=None):
483 flags=revlog.REVIDX_DEFAULT_FLAGS, cachedelta=None):
482 validatenode(p1)
484 validatenode(p1)
483 validatenode(p2)
485 validatenode(p2)
484
486
485 if flags:
487 if flags:
486 node = node or storageutil.hashrevisionsha1(text, p1, p2)
488 node = node or storageutil.hashrevisionsha1(text, p1, p2)
487
489
488 rawtext, validatehash = self._processflags(text, flags, 'write')
490 rawtext, validatehash = self._processflags(text, flags, 'write')
489
491
490 node = node or storageutil.hashrevisionsha1(text, p1, p2)
492 node = node or storageutil.hashrevisionsha1(text, p1, p2)
491
493
492 if node in self._indexbynode:
494 if node in self._indexbynode:
493 return node
495 return node
494
496
495 if validatehash:
497 if validatehash:
496 self.checkhash(rawtext, node, p1=p1, p2=p2)
498 self.checkhash(rawtext, node, p1=p1, p2=p2)
497
499
498 return self._addrawrevision(node, rawtext, transaction, linkrev, p1, p2,
500 return self._addrawrevision(node, rawtext, transaction, linkrev, p1, p2,
499 flags)
501 flags)
500
502
501 def _addrawrevision(self, node, rawtext, transaction, link, p1, p2, flags):
503 def _addrawrevision(self, node, rawtext, transaction, link, p1, p2, flags):
502 transaction.addbackup(self._indexpath)
504 transaction.addbackup(self._indexpath)
503
505
504 path = b'/'.join([self._storepath, hex(node)])
506 path = b'/'.join([self._storepath, hex(node)])
505
507
506 self._svfs.write(path, rawtext)
508 self._svfs.write(path, rawtext)
507
509
508 self._indexdata.append({
510 self._indexdata.append({
509 b'node': node,
511 b'node': node,
510 b'p1': p1,
512 b'p1': p1,
511 b'p2': p2,
513 b'p2': p2,
512 b'linkrev': link,
514 b'linkrev': link,
513 b'flags': flags,
515 b'flags': flags,
514 })
516 })
515
517
516 self._reflectindexupdate()
518 self._reflectindexupdate()
517
519
518 return node
520 return node
519
521
520 def _reflectindexupdate(self):
522 def _reflectindexupdate(self):
521 self._refreshindex()
523 self._refreshindex()
522 self._svfs.write(self._indexpath,
524 self._svfs.write(self._indexpath,
523 ''.join(cborutil.streamencode(self._indexdata)))
525 ''.join(cborutil.streamencode(self._indexdata)))
524
526
525 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None,
527 def addgroup(self, deltas, linkmapper, transaction, addrevisioncb=None,
526 maybemissingparents=False):
528 maybemissingparents=False):
527 if maybemissingparents:
529 if maybemissingparents:
528 raise error.Abort(_('simple store does not support missing parents '
530 raise error.Abort(_('simple store does not support missing parents '
529 'write mode'))
531 'write mode'))
530
532
531 nodes = []
533 nodes = []
532
534
533 transaction.addbackup(self._indexpath)
535 transaction.addbackup(self._indexpath)
534
536
535 for node, p1, p2, linknode, deltabase, delta, flags in deltas:
537 for node, p1, p2, linknode, deltabase, delta, flags in deltas:
536 linkrev = linkmapper(linknode)
538 linkrev = linkmapper(linknode)
537 flags = flags or revlog.REVIDX_DEFAULT_FLAGS
539 flags = flags or revlog.REVIDX_DEFAULT_FLAGS
538
540
539 nodes.append(node)
541 nodes.append(node)
540
542
541 if node in self._indexbynode:
543 if node in self._indexbynode:
542 continue
544 continue
543
545
544 # Need to resolve the fulltext from the delta base.
546 # Need to resolve the fulltext from the delta base.
545 if deltabase == nullid:
547 if deltabase == nullid:
546 text = mdiff.patch(b'', delta)
548 text = mdiff.patch(b'', delta)
547 else:
549 else:
548 text = mdiff.patch(self.revision(deltabase), delta)
550 text = mdiff.patch(self.revision(deltabase), delta)
549
551
550 self._addrawrevision(node, text, transaction, linkrev, p1, p2,
552 self._addrawrevision(node, text, transaction, linkrev, p1, p2,
551 flags)
553 flags)
552
554
553 if addrevisioncb:
555 if addrevisioncb:
554 addrevisioncb(self, node)
556 addrevisioncb(self, node)
555 return nodes
557 return nodes
556
558
557 def _headrevs(self):
559 def _headrevs(self):
558 # Assume all revisions are heads by default.
560 # Assume all revisions are heads by default.
559 revishead = {rev: True for rev in self._indexbyrev}
561 revishead = {rev: True for rev in self._indexbyrev}
560
562
561 for rev, entry in self._indexbyrev.items():
563 for rev, entry in self._indexbyrev.items():
562 # Unset head flag for all seen parents.
564 # Unset head flag for all seen parents.
563 revishead[self.rev(entry[b'p1'])] = False
565 revishead[self.rev(entry[b'p1'])] = False
564 revishead[self.rev(entry[b'p2'])] = False
566 revishead[self.rev(entry[b'p2'])] = False
565
567
566 return [rev for rev, ishead in sorted(revishead.items())
568 return [rev for rev, ishead in sorted(revishead.items())
567 if ishead]
569 if ishead]
568
570
569 def heads(self, start=None, stop=None):
571 def heads(self, start=None, stop=None):
570 # This is copied from revlog.py.
572 # This is copied from revlog.py.
571 if start is None and stop is None:
573 if start is None and stop is None:
572 if not len(self):
574 if not len(self):
573 return [nullid]
575 return [nullid]
574 return [self.node(r) for r in self._headrevs()]
576 return [self.node(r) for r in self._headrevs()]
575
577
576 if start is None:
578 if start is None:
577 start = nullid
579 start = nullid
578 if stop is None:
580 if stop is None:
579 stop = []
581 stop = []
580 stoprevs = set([self.rev(n) for n in stop])
582 stoprevs = set([self.rev(n) for n in stop])
581 startrev = self.rev(start)
583 startrev = self.rev(start)
582 reachable = {startrev}
584 reachable = {startrev}
583 heads = {startrev}
585 heads = {startrev}
584
586
585 parentrevs = self.parentrevs
587 parentrevs = self.parentrevs
586 for r in self.revs(start=startrev + 1):
588 for r in self.revs(start=startrev + 1):
587 for p in parentrevs(r):
589 for p in parentrevs(r):
588 if p in reachable:
590 if p in reachable:
589 if r not in stoprevs:
591 if r not in stoprevs:
590 reachable.add(r)
592 reachable.add(r)
591 heads.add(r)
593 heads.add(r)
592 if p in heads and p not in stoprevs:
594 if p in heads and p not in stoprevs:
593 heads.remove(p)
595 heads.remove(p)
594
596
595 return [self.node(r) for r in heads]
597 return [self.node(r) for r in heads]
596
598
597 def children(self, node):
599 def children(self, node):
598 validatenode(node)
600 validatenode(node)
599
601
600 # This is a copy of revlog.children().
602 # This is a copy of revlog.children().
601 c = []
603 c = []
602 p = self.rev(node)
604 p = self.rev(node)
603 for r in self.revs(start=p + 1):
605 for r in self.revs(start=p + 1):
604 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
606 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
605 if prevs:
607 if prevs:
606 for pr in prevs:
608 for pr in prevs:
607 if pr == p:
609 if pr == p:
608 c.append(self.node(r))
610 c.append(self.node(r))
609 elif p == nullrev:
611 elif p == nullrev:
610 c.append(self.node(r))
612 c.append(self.node(r))
611 return c
613 return c
612
614
613 def getstrippoint(self, minlink):
615 def getstrippoint(self, minlink):
614 return storageutil.resolvestripinfo(
616 return storageutil.resolvestripinfo(
615 minlink, len(self) - 1, self._headrevs(), self.linkrev,
617 minlink, len(self) - 1, self._headrevs(), self.linkrev,
616 self.parentrevs)
618 self.parentrevs)
617
619
618 def strip(self, minlink, transaction):
620 def strip(self, minlink, transaction):
619 if not len(self):
621 if not len(self):
620 return
622 return
621
623
622 rev, _ignored = self.getstrippoint(minlink)
624 rev, _ignored = self.getstrippoint(minlink)
623 if rev == len(self):
625 if rev == len(self):
624 return
626 return
625
627
626 # Purge index data starting at the requested revision.
628 # Purge index data starting at the requested revision.
627 self._indexdata[rev:] = []
629 self._indexdata[rev:] = []
628 self._reflectindexupdate()
630 self._reflectindexupdate()
629
631
630 def issimplestorefile(f, kind, st):
632 def issimplestorefile(f, kind, st):
631 if kind != stat.S_IFREG:
633 if kind != stat.S_IFREG:
632 return False
634 return False
633
635
634 if store.isrevlog(f, kind, st):
636 if store.isrevlog(f, kind, st):
635 return False
637 return False
636
638
637 # Ignore transaction undo files.
639 # Ignore transaction undo files.
638 if f.startswith('undo.'):
640 if f.startswith('undo.'):
639 return False
641 return False
640
642
641 # Otherwise assume it belongs to the simple store.
643 # Otherwise assume it belongs to the simple store.
642 return True
644 return True
643
645
644 class simplestore(store.encodedstore):
646 class simplestore(store.encodedstore):
645 def datafiles(self):
647 def datafiles(self):
646 for x in super(simplestore, self).datafiles():
648 for x in super(simplestore, self).datafiles():
647 yield x
649 yield x
648
650
649 # Supplement with non-revlog files.
651 # Supplement with non-revlog files.
650 extrafiles = self._walk('data', True, filefilter=issimplestorefile)
652 extrafiles = self._walk('data', True, filefilter=issimplestorefile)
651
653
652 for unencoded, encoded, size in extrafiles:
654 for unencoded, encoded, size in extrafiles:
653 try:
655 try:
654 unencoded = store.decodefilename(unencoded)
656 unencoded = store.decodefilename(unencoded)
655 except KeyError:
657 except KeyError:
656 unencoded = None
658 unencoded = None
657
659
658 yield unencoded, encoded, size
660 yield unencoded, encoded, size
659
661
660 def reposetup(ui, repo):
662 def reposetup(ui, repo):
661 if not repo.local():
663 if not repo.local():
662 return
664 return
663
665
664 if isinstance(repo, bundlerepo.bundlerepository):
666 if isinstance(repo, bundlerepo.bundlerepository):
665 raise error.Abort(_('cannot use simple store with bundlerepo'))
667 raise error.Abort(_('cannot use simple store with bundlerepo'))
666
668
667 class simplestorerepo(repo.__class__):
669 class simplestorerepo(repo.__class__):
668 def file(self, f):
670 def file(self, f):
669 return filestorage(self.svfs, f)
671 return filestorage(self.svfs, f)
670
672
671 repo.__class__ = simplestorerepo
673 repo.__class__ = simplestorerepo
672
674
673 def featuresetup(ui, supported):
675 def featuresetup(ui, supported):
674 supported.add(REQUIREMENT)
676 supported.add(REQUIREMENT)
675
677
676 def newreporequirements(orig, ui, createopts):
678 def newreporequirements(orig, ui, createopts):
677 """Modifies default requirements for new repos to use the simple store."""
679 """Modifies default requirements for new repos to use the simple store."""
678 requirements = orig(ui, createopts)
680 requirements = orig(ui, createopts)
679
681
680 # These requirements are only used to affect creation of the store
682 # These requirements are only used to affect creation of the store
681 # object. We have our own store. So we can remove them.
683 # object. We have our own store. So we can remove them.
682 # TODO do this once we feel like taking the test hit.
684 # TODO do this once we feel like taking the test hit.
683 #if 'fncache' in requirements:
685 #if 'fncache' in requirements:
684 # requirements.remove('fncache')
686 # requirements.remove('fncache')
685 #if 'dotencode' in requirements:
687 #if 'dotencode' in requirements:
686 # requirements.remove('dotencode')
688 # requirements.remove('dotencode')
687
689
688 requirements.add(REQUIREMENT)
690 requirements.add(REQUIREMENT)
689
691
690 return requirements
692 return requirements
691
693
692 def makestore(orig, requirements, path, vfstype):
694 def makestore(orig, requirements, path, vfstype):
693 if REQUIREMENT not in requirements:
695 if REQUIREMENT not in requirements:
694 return orig(requirements, path, vfstype)
696 return orig(requirements, path, vfstype)
695
697
696 return simplestore(path, vfstype)
698 return simplestore(path, vfstype)
697
699
698 def verifierinit(orig, self, *args, **kwargs):
700 def verifierinit(orig, self, *args, **kwargs):
699 orig(self, *args, **kwargs)
701 orig(self, *args, **kwargs)
700
702
701 # We don't care that files in the store don't align with what is
703 # We don't care that files in the store don't align with what is
702 # advertised. So suppress these warnings.
704 # advertised. So suppress these warnings.
703 self.warnorphanstorefiles = False
705 self.warnorphanstorefiles = False
704
706
705 def extsetup(ui):
707 def extsetup(ui):
706 localrepo.featuresetupfuncs.add(featuresetup)
708 localrepo.featuresetupfuncs.add(featuresetup)
707
709
708 extensions.wrapfunction(localrepo, 'newreporequirements',
710 extensions.wrapfunction(localrepo, 'newreporequirements',
709 newreporequirements)
711 newreporequirements)
710 extensions.wrapfunction(localrepo, 'makestore', makestore)
712 extensions.wrapfunction(localrepo, 'makestore', makestore)
711 extensions.wrapfunction(verify.verifier, '__init__', verifierinit)
713 extensions.wrapfunction(verify.verifier, '__init__', verifierinit)
@@ -1,233 +1,235
1 # Test that certain objects conform to well-defined interfaces.
1 # Test that certain objects conform to well-defined interfaces.
2
2
3 from __future__ import absolute_import, print_function
3 from __future__ import absolute_import, print_function
4
4
5 from mercurial import encoding
5 from mercurial import encoding
6 encoding.environ[b'HGREALINTERFACES'] = b'1'
6 encoding.environ[b'HGREALINTERFACES'] = b'1'
7
7
8 import os
8 import os
9 import subprocess
9 import subprocess
10 import sys
10 import sys
11
11
12 # Only run if tests are run in a repo
12 # Only run if tests are run in a repo
13 if subprocess.call(['python', '%s/hghave' % os.environ['TESTDIR'],
13 if subprocess.call(['python', '%s/hghave' % os.environ['TESTDIR'],
14 'test-repo']):
14 'test-repo']):
15 sys.exit(80)
15 sys.exit(80)
16
16
17 from mercurial.interfaces import (
18 repository,
19 )
17 from mercurial.thirdparty.zope import (
20 from mercurial.thirdparty.zope import (
18 interface as zi,
21 interface as zi,
19 )
22 )
20 from mercurial.thirdparty.zope.interface import (
23 from mercurial.thirdparty.zope.interface import (
21 verify as ziverify,
24 verify as ziverify,
22 )
25 )
23 from mercurial import (
26 from mercurial import (
24 bundlerepo,
27 bundlerepo,
25 filelog,
28 filelog,
26 httppeer,
29 httppeer,
27 localrepo,
30 localrepo,
28 manifest,
31 manifest,
29 pycompat,
32 pycompat,
30 repository,
31 revlog,
33 revlog,
32 sshpeer,
34 sshpeer,
33 statichttprepo,
35 statichttprepo,
34 ui as uimod,
36 ui as uimod,
35 unionrepo,
37 unionrepo,
36 vfs as vfsmod,
38 vfs as vfsmod,
37 wireprotoserver,
39 wireprotoserver,
38 wireprototypes,
40 wireprototypes,
39 wireprotov1peer,
41 wireprotov1peer,
40 wireprotov2server,
42 wireprotov2server,
41 )
43 )
42
44
43 testdir = os.path.dirname(__file__)
45 testdir = os.path.dirname(__file__)
44 rootdir = pycompat.fsencode(os.path.normpath(os.path.join(testdir, '..')))
46 rootdir = pycompat.fsencode(os.path.normpath(os.path.join(testdir, '..')))
45
47
46 sys.path[0:0] = [testdir]
48 sys.path[0:0] = [testdir]
47 import simplestorerepo
49 import simplestorerepo
48 del sys.path[0]
50 del sys.path[0]
49
51
50 def checkzobject(o, allowextra=False):
52 def checkzobject(o, allowextra=False):
51 """Verify an object with a zope interface."""
53 """Verify an object with a zope interface."""
52 ifaces = zi.providedBy(o)
54 ifaces = zi.providedBy(o)
53 if not ifaces:
55 if not ifaces:
54 print('%r does not provide any zope interfaces' % o)
56 print('%r does not provide any zope interfaces' % o)
55 return
57 return
56
58
57 # Run zope.interface's built-in verification routine. This verifies that
59 # Run zope.interface's built-in verification routine. This verifies that
58 # everything that is supposed to be present is present.
60 # everything that is supposed to be present is present.
59 for iface in ifaces:
61 for iface in ifaces:
60 ziverify.verifyObject(iface, o)
62 ziverify.verifyObject(iface, o)
61
63
62 if allowextra:
64 if allowextra:
63 return
65 return
64
66
65 # Now verify that the object provides no extra public attributes that
67 # Now verify that the object provides no extra public attributes that
66 # aren't declared as part of interfaces.
68 # aren't declared as part of interfaces.
67 allowed = set()
69 allowed = set()
68 for iface in ifaces:
70 for iface in ifaces:
69 allowed |= set(iface.names(all=True))
71 allowed |= set(iface.names(all=True))
70
72
71 public = {a for a in dir(o) if not a.startswith('_')}
73 public = {a for a in dir(o) if not a.startswith('_')}
72
74
73 for attr in sorted(public - allowed):
75 for attr in sorted(public - allowed):
74 print('public attribute not declared in interfaces: %s.%s' % (
76 print('public attribute not declared in interfaces: %s.%s' % (
75 o.__class__.__name__, attr))
77 o.__class__.__name__, attr))
76
78
77 # Facilitates testing localpeer.
79 # Facilitates testing localpeer.
78 class dummyrepo(object):
80 class dummyrepo(object):
79 def __init__(self):
81 def __init__(self):
80 self.ui = uimod.ui()
82 self.ui = uimod.ui()
81 def filtered(self, name):
83 def filtered(self, name):
82 pass
84 pass
83 def _restrictcapabilities(self, caps):
85 def _restrictcapabilities(self, caps):
84 pass
86 pass
85
87
86 class dummyopener(object):
88 class dummyopener(object):
87 handlers = []
89 handlers = []
88
90
89 # Facilitates testing sshpeer without requiring a server.
91 # Facilitates testing sshpeer without requiring a server.
90 class badpeer(httppeer.httppeer):
92 class badpeer(httppeer.httppeer):
91 def __init__(self):
93 def __init__(self):
92 super(badpeer, self).__init__(None, None, None, dummyopener(), None,
94 super(badpeer, self).__init__(None, None, None, dummyopener(), None,
93 None)
95 None)
94 self.badattribute = True
96 self.badattribute = True
95
97
96 def badmethod(self):
98 def badmethod(self):
97 pass
99 pass
98
100
99 class dummypipe(object):
101 class dummypipe(object):
100 def close(self):
102 def close(self):
101 pass
103 pass
102
104
103 def main():
105 def main():
104 ui = uimod.ui()
106 ui = uimod.ui()
105 # Needed so we can open a local repo with obsstore without a warning.
107 # Needed so we can open a local repo with obsstore without a warning.
106 ui.setconfig(b'experimental', b'evolution.createmarkers', True)
108 ui.setconfig(b'experimental', b'evolution.createmarkers', True)
107
109
108 checkzobject(badpeer())
110 checkzobject(badpeer())
109
111
110 ziverify.verifyClass(repository.ipeerbase, httppeer.httppeer)
112 ziverify.verifyClass(repository.ipeerbase, httppeer.httppeer)
111 checkzobject(httppeer.httppeer(None, None, None, dummyopener(), None, None))
113 checkzobject(httppeer.httppeer(None, None, None, dummyopener(), None, None))
112
114
113 ziverify.verifyClass(repository.ipeerv2, httppeer.httpv2peer)
115 ziverify.verifyClass(repository.ipeerv2, httppeer.httpv2peer)
114 checkzobject(httppeer.httpv2peer(None, b'', b'', None, None, None))
116 checkzobject(httppeer.httpv2peer(None, b'', b'', None, None, None))
115
117
116 ziverify.verifyClass(repository.ipeerbase,
118 ziverify.verifyClass(repository.ipeerbase,
117 localrepo.localpeer)
119 localrepo.localpeer)
118 checkzobject(localrepo.localpeer(dummyrepo()))
120 checkzobject(localrepo.localpeer(dummyrepo()))
119
121
120 ziverify.verifyClass(repository.ipeercommandexecutor,
122 ziverify.verifyClass(repository.ipeercommandexecutor,
121 localrepo.localcommandexecutor)
123 localrepo.localcommandexecutor)
122 checkzobject(localrepo.localcommandexecutor(None))
124 checkzobject(localrepo.localcommandexecutor(None))
123
125
124 ziverify.verifyClass(repository.ipeercommandexecutor,
126 ziverify.verifyClass(repository.ipeercommandexecutor,
125 wireprotov1peer.peerexecutor)
127 wireprotov1peer.peerexecutor)
126 checkzobject(wireprotov1peer.peerexecutor(None))
128 checkzobject(wireprotov1peer.peerexecutor(None))
127
129
128 ziverify.verifyClass(repository.ipeerbase, sshpeer.sshv1peer)
130 ziverify.verifyClass(repository.ipeerbase, sshpeer.sshv1peer)
129 checkzobject(sshpeer.sshv1peer(ui, b'ssh://localhost/foo', b'', dummypipe(),
131 checkzobject(sshpeer.sshv1peer(ui, b'ssh://localhost/foo', b'', dummypipe(),
130 dummypipe(), None, None))
132 dummypipe(), None, None))
131
133
132 ziverify.verifyClass(repository.ipeerbase, sshpeer.sshv2peer)
134 ziverify.verifyClass(repository.ipeerbase, sshpeer.sshv2peer)
133 checkzobject(sshpeer.sshv2peer(ui, b'ssh://localhost/foo', b'', dummypipe(),
135 checkzobject(sshpeer.sshv2peer(ui, b'ssh://localhost/foo', b'', dummypipe(),
134 dummypipe(), None, None))
136 dummypipe(), None, None))
135
137
136 ziverify.verifyClass(repository.ipeerbase, bundlerepo.bundlepeer)
138 ziverify.verifyClass(repository.ipeerbase, bundlerepo.bundlepeer)
137 checkzobject(bundlerepo.bundlepeer(dummyrepo()))
139 checkzobject(bundlerepo.bundlepeer(dummyrepo()))
138
140
139 ziverify.verifyClass(repository.ipeerbase, statichttprepo.statichttppeer)
141 ziverify.verifyClass(repository.ipeerbase, statichttprepo.statichttppeer)
140 checkzobject(statichttprepo.statichttppeer(dummyrepo()))
142 checkzobject(statichttprepo.statichttppeer(dummyrepo()))
141
143
142 ziverify.verifyClass(repository.ipeerbase, unionrepo.unionpeer)
144 ziverify.verifyClass(repository.ipeerbase, unionrepo.unionpeer)
143 checkzobject(unionrepo.unionpeer(dummyrepo()))
145 checkzobject(unionrepo.unionpeer(dummyrepo()))
144
146
145 ziverify.verifyClass(repository.ilocalrepositorymain,
147 ziverify.verifyClass(repository.ilocalrepositorymain,
146 localrepo.localrepository)
148 localrepo.localrepository)
147 ziverify.verifyClass(repository.ilocalrepositoryfilestorage,
149 ziverify.verifyClass(repository.ilocalrepositoryfilestorage,
148 localrepo.revlogfilestorage)
150 localrepo.revlogfilestorage)
149 repo = localrepo.makelocalrepository(ui, rootdir)
151 repo = localrepo.makelocalrepository(ui, rootdir)
150 checkzobject(repo)
152 checkzobject(repo)
151
153
152 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
154 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
153 wireprotoserver.sshv1protocolhandler)
155 wireprotoserver.sshv1protocolhandler)
154 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
156 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
155 wireprotoserver.sshv2protocolhandler)
157 wireprotoserver.sshv2protocolhandler)
156 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
158 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
157 wireprotoserver.httpv1protocolhandler)
159 wireprotoserver.httpv1protocolhandler)
158 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
160 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
159 wireprotov2server.httpv2protocolhandler)
161 wireprotov2server.httpv2protocolhandler)
160
162
161 sshv1 = wireprotoserver.sshv1protocolhandler(None, None, None)
163 sshv1 = wireprotoserver.sshv1protocolhandler(None, None, None)
162 checkzobject(sshv1)
164 checkzobject(sshv1)
163 sshv2 = wireprotoserver.sshv2protocolhandler(None, None, None)
165 sshv2 = wireprotoserver.sshv2protocolhandler(None, None, None)
164 checkzobject(sshv2)
166 checkzobject(sshv2)
165
167
166 httpv1 = wireprotoserver.httpv1protocolhandler(None, None, None)
168 httpv1 = wireprotoserver.httpv1protocolhandler(None, None, None)
167 checkzobject(httpv1)
169 checkzobject(httpv1)
168 httpv2 = wireprotov2server.httpv2protocolhandler(None, None)
170 httpv2 = wireprotov2server.httpv2protocolhandler(None, None)
169 checkzobject(httpv2)
171 checkzobject(httpv2)
170
172
171 ziverify.verifyClass(repository.ifilestorage, filelog.filelog)
173 ziverify.verifyClass(repository.ifilestorage, filelog.filelog)
172 ziverify.verifyClass(repository.imanifestdict, manifest.manifestdict)
174 ziverify.verifyClass(repository.imanifestdict, manifest.manifestdict)
173 ziverify.verifyClass(repository.imanifestrevisionstored,
175 ziverify.verifyClass(repository.imanifestrevisionstored,
174 manifest.manifestctx)
176 manifest.manifestctx)
175 ziverify.verifyClass(repository.imanifestrevisionwritable,
177 ziverify.verifyClass(repository.imanifestrevisionwritable,
176 manifest.memmanifestctx)
178 manifest.memmanifestctx)
177 ziverify.verifyClass(repository.imanifestrevisionstored,
179 ziverify.verifyClass(repository.imanifestrevisionstored,
178 manifest.treemanifestctx)
180 manifest.treemanifestctx)
179 ziverify.verifyClass(repository.imanifestrevisionwritable,
181 ziverify.verifyClass(repository.imanifestrevisionwritable,
180 manifest.memtreemanifestctx)
182 manifest.memtreemanifestctx)
181 ziverify.verifyClass(repository.imanifestlog, manifest.manifestlog)
183 ziverify.verifyClass(repository.imanifestlog, manifest.manifestlog)
182 ziverify.verifyClass(repository.imanifeststorage, manifest.manifestrevlog)
184 ziverify.verifyClass(repository.imanifeststorage, manifest.manifestrevlog)
183
185
184 ziverify.verifyClass(repository.irevisiondelta,
186 ziverify.verifyClass(repository.irevisiondelta,
185 simplestorerepo.simplestorerevisiondelta)
187 simplestorerepo.simplestorerevisiondelta)
186 ziverify.verifyClass(repository.ifilestorage, simplestorerepo.filestorage)
188 ziverify.verifyClass(repository.ifilestorage, simplestorerepo.filestorage)
187 ziverify.verifyClass(repository.iverifyproblem,
189 ziverify.verifyClass(repository.iverifyproblem,
188 simplestorerepo.simplefilestoreproblem)
190 simplestorerepo.simplefilestoreproblem)
189
191
190 vfs = vfsmod.vfs(b'.')
192 vfs = vfsmod.vfs(b'.')
191 fl = filelog.filelog(vfs, b'dummy.i')
193 fl = filelog.filelog(vfs, b'dummy.i')
192 checkzobject(fl, allowextra=True)
194 checkzobject(fl, allowextra=True)
193
195
194 # Conforms to imanifestlog.
196 # Conforms to imanifestlog.
195 ml = manifest.manifestlog(vfs, repo, manifest.manifestrevlog(repo.svfs),
197 ml = manifest.manifestlog(vfs, repo, manifest.manifestrevlog(repo.svfs),
196 repo.narrowmatch())
198 repo.narrowmatch())
197 checkzobject(ml)
199 checkzobject(ml)
198 checkzobject(repo.manifestlog)
200 checkzobject(repo.manifestlog)
199
201
200 # Conforms to imanifestrevision.
202 # Conforms to imanifestrevision.
201 mctx = ml[repo[0].manifestnode()]
203 mctx = ml[repo[0].manifestnode()]
202 checkzobject(mctx)
204 checkzobject(mctx)
203
205
204 # Conforms to imanifestrevisionwritable.
206 # Conforms to imanifestrevisionwritable.
205 checkzobject(mctx.new())
207 checkzobject(mctx.new())
206 checkzobject(mctx.copy())
208 checkzobject(mctx.copy())
207
209
208 # Conforms to imanifestdict.
210 # Conforms to imanifestdict.
209 checkzobject(mctx.read())
211 checkzobject(mctx.read())
210
212
211 mrl = manifest.manifestrevlog(vfs)
213 mrl = manifest.manifestrevlog(vfs)
212 checkzobject(mrl)
214 checkzobject(mrl)
213
215
214 ziverify.verifyClass(repository.irevisiondelta,
216 ziverify.verifyClass(repository.irevisiondelta,
215 revlog.revlogrevisiondelta)
217 revlog.revlogrevisiondelta)
216
218
217 rd = revlog.revlogrevisiondelta(
219 rd = revlog.revlogrevisiondelta(
218 node=b'',
220 node=b'',
219 p1node=b'',
221 p1node=b'',
220 p2node=b'',
222 p2node=b'',
221 basenode=b'',
223 basenode=b'',
222 linknode=b'',
224 linknode=b'',
223 flags=b'',
225 flags=b'',
224 baserevisionsize=None,
226 baserevisionsize=None,
225 revision=b'',
227 revision=b'',
226 delta=None)
228 delta=None)
227 checkzobject(rd)
229 checkzobject(rd)
228
230
229 ziverify.verifyClass(repository.iverifyproblem,
231 ziverify.verifyClass(repository.iverifyproblem,
230 revlog.revlogproblem)
232 revlog.revlogproblem)
231 checkzobject(revlog.revlogproblem())
233 checkzobject(revlog.revlogproblem())
232
234
233 main()
235 main()
@@ -1,193 +1,195
1 # wireprotosimplecache.py - Extension providing in-memory wire protocol cache
1 # wireprotosimplecache.py - Extension providing in-memory wire protocol cache
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 from mercurial import (
10 from mercurial import (
11 extensions,
11 extensions,
12 registrar,
12 registrar,
13 repository,
14 util,
13 util,
15 wireprotoserver,
14 wireprotoserver,
16 wireprototypes,
15 wireprototypes,
17 wireprotov2server,
16 wireprotov2server,
18 )
17 )
18 from mercurial.interfaces import (
19 repository,
20 )
19 from mercurial.utils import (
21 from mercurial.utils import (
20 interfaceutil,
22 interfaceutil,
21 stringutil,
23 stringutil,
22 )
24 )
23
25
24 CACHE = None
26 CACHE = None
25
27
26 configtable = {}
28 configtable = {}
27 configitem = registrar.configitem(configtable)
29 configitem = registrar.configitem(configtable)
28
30
29 configitem(b'simplecache', b'cacheapi',
31 configitem(b'simplecache', b'cacheapi',
30 default=False)
32 default=False)
31 configitem(b'simplecache', b'cacheobjects',
33 configitem(b'simplecache', b'cacheobjects',
32 default=False)
34 default=False)
33 configitem(b'simplecache', b'redirectsfile',
35 configitem(b'simplecache', b'redirectsfile',
34 default=None)
36 default=None)
35
37
36 # API handler that makes cached keys available.
38 # API handler that makes cached keys available.
37 def handlecacherequest(rctx, req, res, checkperm, urlparts):
39 def handlecacherequest(rctx, req, res, checkperm, urlparts):
38 if rctx.repo.ui.configbool(b'simplecache', b'cacheobjects'):
40 if rctx.repo.ui.configbool(b'simplecache', b'cacheobjects'):
39 res.status = b'500 Internal Server Error'
41 res.status = b'500 Internal Server Error'
40 res.setbodybytes(b'cacheobjects not supported for api server')
42 res.setbodybytes(b'cacheobjects not supported for api server')
41 return
43 return
42
44
43 if not urlparts:
45 if not urlparts:
44 res.status = b'200 OK'
46 res.status = b'200 OK'
45 res.headers[b'Content-Type'] = b'text/plain'
47 res.headers[b'Content-Type'] = b'text/plain'
46 res.setbodybytes(b'simple cache server')
48 res.setbodybytes(b'simple cache server')
47 return
49 return
48
50
49 key = b'/'.join(urlparts)
51 key = b'/'.join(urlparts)
50
52
51 if key not in CACHE:
53 if key not in CACHE:
52 res.status = b'404 Not Found'
54 res.status = b'404 Not Found'
53 res.headers[b'Content-Type'] = b'text/plain'
55 res.headers[b'Content-Type'] = b'text/plain'
54 res.setbodybytes(b'key not found in cache')
56 res.setbodybytes(b'key not found in cache')
55 return
57 return
56
58
57 res.status = b'200 OK'
59 res.status = b'200 OK'
58 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
60 res.headers[b'Content-Type'] = b'application/mercurial-cbor'
59 res.setbodybytes(CACHE[key])
61 res.setbodybytes(CACHE[key])
60
62
61 def cachedescriptor(req, repo):
63 def cachedescriptor(req, repo):
62 return {}
64 return {}
63
65
64 wireprotoserver.API_HANDLERS[b'simplecache'] = {
66 wireprotoserver.API_HANDLERS[b'simplecache'] = {
65 b'config': (b'simplecache', b'cacheapi'),
67 b'config': (b'simplecache', b'cacheapi'),
66 b'handler': handlecacherequest,
68 b'handler': handlecacherequest,
67 b'apidescriptor': cachedescriptor,
69 b'apidescriptor': cachedescriptor,
68 }
70 }
69
71
70 @interfaceutil.implementer(repository.iwireprotocolcommandcacher)
72 @interfaceutil.implementer(repository.iwireprotocolcommandcacher)
71 class memorycacher(object):
73 class memorycacher(object):
72 def __init__(self, ui, command, encodefn, redirecttargets, redirecthashes,
74 def __init__(self, ui, command, encodefn, redirecttargets, redirecthashes,
73 req):
75 req):
74 self.ui = ui
76 self.ui = ui
75 self.encodefn = encodefn
77 self.encodefn = encodefn
76 self.redirecttargets = redirecttargets
78 self.redirecttargets = redirecttargets
77 self.redirecthashes = redirecthashes
79 self.redirecthashes = redirecthashes
78 self.req = req
80 self.req = req
79 self.key = None
81 self.key = None
80 self.cacheobjects = ui.configbool(b'simplecache', b'cacheobjects')
82 self.cacheobjects = ui.configbool(b'simplecache', b'cacheobjects')
81 self.cacheapi = ui.configbool(b'simplecache', b'cacheapi')
83 self.cacheapi = ui.configbool(b'simplecache', b'cacheapi')
82 self.buffered = []
84 self.buffered = []
83
85
84 ui.log(b'simplecache', b'cacher constructed for %s\n', command)
86 ui.log(b'simplecache', b'cacher constructed for %s\n', command)
85
87
86 def __enter__(self):
88 def __enter__(self):
87 return self
89 return self
88
90
89 def __exit__(self, exctype, excvalue, exctb):
91 def __exit__(self, exctype, excvalue, exctb):
90 if exctype:
92 if exctype:
91 self.ui.log(b'simplecache', b'cacher exiting due to error\n')
93 self.ui.log(b'simplecache', b'cacher exiting due to error\n')
92
94
93 def adjustcachekeystate(self, state):
95 def adjustcachekeystate(self, state):
94 # Needed in order to make tests deterministic. Don't copy this
96 # Needed in order to make tests deterministic. Don't copy this
95 # pattern for production caches!
97 # pattern for production caches!
96 del state[b'repo']
98 del state[b'repo']
97
99
98 def setcachekey(self, key):
100 def setcachekey(self, key):
99 self.key = key
101 self.key = key
100 return True
102 return True
101
103
102 def lookup(self):
104 def lookup(self):
103 if self.key not in CACHE:
105 if self.key not in CACHE:
104 self.ui.log(b'simplecache', b'cache miss for %s\n', self.key)
106 self.ui.log(b'simplecache', b'cache miss for %s\n', self.key)
105 return None
107 return None
106
108
107 entry = CACHE[self.key]
109 entry = CACHE[self.key]
108 self.ui.log(b'simplecache', b'cache hit for %s\n', self.key)
110 self.ui.log(b'simplecache', b'cache hit for %s\n', self.key)
109
111
110 redirectable = True
112 redirectable = True
111
113
112 if not self.cacheapi:
114 if not self.cacheapi:
113 redirectable = False
115 redirectable = False
114 elif not self.redirecttargets:
116 elif not self.redirecttargets:
115 redirectable = False
117 redirectable = False
116 else:
118 else:
117 clienttargets = set(self.redirecttargets)
119 clienttargets = set(self.redirecttargets)
118 ourtargets = set(t[b'name'] for t in loadredirecttargets(self.ui))
120 ourtargets = set(t[b'name'] for t in loadredirecttargets(self.ui))
119
121
120 # We only ever redirect to a single target (for now). So we don't
122 # We only ever redirect to a single target (for now). So we don't
121 # need to store which target matched.
123 # need to store which target matched.
122 if not clienttargets & ourtargets:
124 if not clienttargets & ourtargets:
123 redirectable = False
125 redirectable = False
124
126
125 if redirectable:
127 if redirectable:
126 paths = self.req.dispatchparts[:-3]
128 paths = self.req.dispatchparts[:-3]
127 paths.append(b'simplecache')
129 paths.append(b'simplecache')
128 paths.append(self.key)
130 paths.append(self.key)
129
131
130 url = b'%s/%s' % (self.req.baseurl, b'/'.join(paths))
132 url = b'%s/%s' % (self.req.baseurl, b'/'.join(paths))
131
133
132 #url = b'http://example.com/%s' % self.key
134 #url = b'http://example.com/%s' % self.key
133 self.ui.log(b'simplecache', b'sending content redirect for %s to '
135 self.ui.log(b'simplecache', b'sending content redirect for %s to '
134 b'%s\n', self.key, url)
136 b'%s\n', self.key, url)
135 response = wireprototypes.alternatelocationresponse(
137 response = wireprototypes.alternatelocationresponse(
136 url=url,
138 url=url,
137 mediatype=b'application/mercurial-cbor')
139 mediatype=b'application/mercurial-cbor')
138
140
139 return {b'objs': [response]}
141 return {b'objs': [response]}
140
142
141 if self.cacheobjects:
143 if self.cacheobjects:
142 return {
144 return {
143 b'objs': entry,
145 b'objs': entry,
144 }
146 }
145 else:
147 else:
146 return {
148 return {
147 b'objs': [wireprototypes.encodedresponse(entry)],
149 b'objs': [wireprototypes.encodedresponse(entry)],
148 }
150 }
149
151
150 def onobject(self, obj):
152 def onobject(self, obj):
151 if self.cacheobjects:
153 if self.cacheobjects:
152 self.buffered.append(obj)
154 self.buffered.append(obj)
153 else:
155 else:
154 self.buffered.extend(self.encodefn(obj))
156 self.buffered.extend(self.encodefn(obj))
155
157
156 yield obj
158 yield obj
157
159
158 def onfinished(self):
160 def onfinished(self):
159 self.ui.log(b'simplecache', b'storing cache entry for %s\n', self.key)
161 self.ui.log(b'simplecache', b'storing cache entry for %s\n', self.key)
160 if self.cacheobjects:
162 if self.cacheobjects:
161 CACHE[self.key] = self.buffered
163 CACHE[self.key] = self.buffered
162 else:
164 else:
163 CACHE[self.key] = b''.join(self.buffered)
165 CACHE[self.key] = b''.join(self.buffered)
164
166
165 return []
167 return []
166
168
167 def makeresponsecacher(orig, repo, proto, command, args, objencoderfn,
169 def makeresponsecacher(orig, repo, proto, command, args, objencoderfn,
168 redirecttargets, redirecthashes):
170 redirecttargets, redirecthashes):
169 return memorycacher(repo.ui, command, objencoderfn, redirecttargets,
171 return memorycacher(repo.ui, command, objencoderfn, redirecttargets,
170 redirecthashes, proto._req)
172 redirecthashes, proto._req)
171
173
172 def loadredirecttargets(ui):
174 def loadredirecttargets(ui):
173 path = ui.config(b'simplecache', b'redirectsfile')
175 path = ui.config(b'simplecache', b'redirectsfile')
174 if not path:
176 if not path:
175 return []
177 return []
176
178
177 with open(path, 'rb') as fh:
179 with open(path, 'rb') as fh:
178 s = fh.read()
180 s = fh.read()
179
181
180 return stringutil.evalpythonliteral(s)
182 return stringutil.evalpythonliteral(s)
181
183
182 def getadvertisedredirecttargets(orig, repo, proto):
184 def getadvertisedredirecttargets(orig, repo, proto):
183 return loadredirecttargets(repo.ui)
185 return loadredirecttargets(repo.ui)
184
186
185 def extsetup(ui):
187 def extsetup(ui):
186 global CACHE
188 global CACHE
187
189
188 CACHE = util.lrucachedict(10000)
190 CACHE = util.lrucachedict(10000)
189
191
190 extensions.wrapfunction(wireprotov2server, b'makeresponsecacher',
192 extensions.wrapfunction(wireprotov2server, b'makeresponsecacher',
191 makeresponsecacher)
193 makeresponsecacher)
192 extensions.wrapfunction(wireprotov2server, b'getadvertisedredirecttargets',
194 extensions.wrapfunction(wireprotov2server, b'getadvertisedredirecttargets',
193 getadvertisedredirecttargets)
195 getadvertisedredirecttargets)
General Comments 0
You need to be logged in to leave comments. Login now