##// END OF EJS Templates
store: introduce _matchtrackedpath() and use it to filter store files...
Pulkit Goyal -
r40529:9aeb9e2d default
parent child Browse files
Show More
@@ -1,591 +1,609
1 # store.py - repository store handling for Mercurial
1 # store.py - repository store handling for Mercurial
2 #
2 #
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2008 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 stat
13 import stat
14
14
15 from .i18n import _
15 from .i18n import _
16 from . import (
16 from . import (
17 error,
17 error,
18 node,
18 node,
19 policy,
19 policy,
20 pycompat,
20 pycompat,
21 util,
21 util,
22 vfs as vfsmod,
22 vfs as vfsmod,
23 )
23 )
24
24
25 parsers = policy.importmod(r'parsers')
25 parsers = policy.importmod(r'parsers')
26
26
27 def _matchtrackedpath(path, matcher):
28 """parses a fncache entry and returns whether the entry is tracking a path
29 matched by matcher or not.
30
31 If matcher is None, returns True"""
32
33 if matcher is None:
34 return True
35 path = decodedir(path)
36 if path.startswith('data/'):
37 return matcher(path[len('data/'):-len('.i')])
38 elif path.startswith('meta/'):
39 return matcher.visitdir(path[len('meta/'):-len('/00manifest.i')] or '.')
40
27 # This avoids a collision between a file named foo and a dir named
41 # This avoids a collision between a file named foo and a dir named
28 # foo.i or foo.d
42 # foo.i or foo.d
29 def _encodedir(path):
43 def _encodedir(path):
30 '''
44 '''
31 >>> _encodedir(b'data/foo.i')
45 >>> _encodedir(b'data/foo.i')
32 'data/foo.i'
46 'data/foo.i'
33 >>> _encodedir(b'data/foo.i/bla.i')
47 >>> _encodedir(b'data/foo.i/bla.i')
34 'data/foo.i.hg/bla.i'
48 'data/foo.i.hg/bla.i'
35 >>> _encodedir(b'data/foo.i.hg/bla.i')
49 >>> _encodedir(b'data/foo.i.hg/bla.i')
36 'data/foo.i.hg.hg/bla.i'
50 'data/foo.i.hg.hg/bla.i'
37 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
51 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
38 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
52 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
39 '''
53 '''
40 return (path
54 return (path
41 .replace(".hg/", ".hg.hg/")
55 .replace(".hg/", ".hg.hg/")
42 .replace(".i/", ".i.hg/")
56 .replace(".i/", ".i.hg/")
43 .replace(".d/", ".d.hg/"))
57 .replace(".d/", ".d.hg/"))
44
58
45 encodedir = getattr(parsers, 'encodedir', _encodedir)
59 encodedir = getattr(parsers, 'encodedir', _encodedir)
46
60
47 def decodedir(path):
61 def decodedir(path):
48 '''
62 '''
49 >>> decodedir(b'data/foo.i')
63 >>> decodedir(b'data/foo.i')
50 'data/foo.i'
64 'data/foo.i'
51 >>> decodedir(b'data/foo.i.hg/bla.i')
65 >>> decodedir(b'data/foo.i.hg/bla.i')
52 'data/foo.i/bla.i'
66 'data/foo.i/bla.i'
53 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
67 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
54 'data/foo.i.hg/bla.i'
68 'data/foo.i.hg/bla.i'
55 '''
69 '''
56 if ".hg/" not in path:
70 if ".hg/" not in path:
57 return path
71 return path
58 return (path
72 return (path
59 .replace(".d.hg/", ".d/")
73 .replace(".d.hg/", ".d/")
60 .replace(".i.hg/", ".i/")
74 .replace(".i.hg/", ".i/")
61 .replace(".hg.hg/", ".hg/"))
75 .replace(".hg.hg/", ".hg/"))
62
76
63 def _reserved():
77 def _reserved():
64 ''' characters that are problematic for filesystems
78 ''' characters that are problematic for filesystems
65
79
66 * ascii escapes (0..31)
80 * ascii escapes (0..31)
67 * ascii hi (126..255)
81 * ascii hi (126..255)
68 * windows specials
82 * windows specials
69
83
70 these characters will be escaped by encodefunctions
84 these characters will be escaped by encodefunctions
71 '''
85 '''
72 winreserved = [ord(x) for x in u'\\:*?"<>|']
86 winreserved = [ord(x) for x in u'\\:*?"<>|']
73 for x in range(32):
87 for x in range(32):
74 yield x
88 yield x
75 for x in range(126, 256):
89 for x in range(126, 256):
76 yield x
90 yield x
77 for x in winreserved:
91 for x in winreserved:
78 yield x
92 yield x
79
93
80 def _buildencodefun():
94 def _buildencodefun():
81 '''
95 '''
82 >>> enc, dec = _buildencodefun()
96 >>> enc, dec = _buildencodefun()
83
97
84 >>> enc(b'nothing/special.txt')
98 >>> enc(b'nothing/special.txt')
85 'nothing/special.txt'
99 'nothing/special.txt'
86 >>> dec(b'nothing/special.txt')
100 >>> dec(b'nothing/special.txt')
87 'nothing/special.txt'
101 'nothing/special.txt'
88
102
89 >>> enc(b'HELLO')
103 >>> enc(b'HELLO')
90 '_h_e_l_l_o'
104 '_h_e_l_l_o'
91 >>> dec(b'_h_e_l_l_o')
105 >>> dec(b'_h_e_l_l_o')
92 'HELLO'
106 'HELLO'
93
107
94 >>> enc(b'hello:world?')
108 >>> enc(b'hello:world?')
95 'hello~3aworld~3f'
109 'hello~3aworld~3f'
96 >>> dec(b'hello~3aworld~3f')
110 >>> dec(b'hello~3aworld~3f')
97 'hello:world?'
111 'hello:world?'
98
112
99 >>> enc(b'the\\x07quick\\xADshot')
113 >>> enc(b'the\\x07quick\\xADshot')
100 'the~07quick~adshot'
114 'the~07quick~adshot'
101 >>> dec(b'the~07quick~adshot')
115 >>> dec(b'the~07quick~adshot')
102 'the\\x07quick\\xadshot'
116 'the\\x07quick\\xadshot'
103 '''
117 '''
104 e = '_'
118 e = '_'
105 xchr = pycompat.bytechr
119 xchr = pycompat.bytechr
106 asciistr = list(map(xchr, range(127)))
120 asciistr = list(map(xchr, range(127)))
107 capitals = list(range(ord("A"), ord("Z") + 1))
121 capitals = list(range(ord("A"), ord("Z") + 1))
108
122
109 cmap = dict((x, x) for x in asciistr)
123 cmap = dict((x, x) for x in asciistr)
110 for x in _reserved():
124 for x in _reserved():
111 cmap[xchr(x)] = "~%02x" % x
125 cmap[xchr(x)] = "~%02x" % x
112 for x in capitals + [ord(e)]:
126 for x in capitals + [ord(e)]:
113 cmap[xchr(x)] = e + xchr(x).lower()
127 cmap[xchr(x)] = e + xchr(x).lower()
114
128
115 dmap = {}
129 dmap = {}
116 for k, v in cmap.iteritems():
130 for k, v in cmap.iteritems():
117 dmap[v] = k
131 dmap[v] = k
118 def decode(s):
132 def decode(s):
119 i = 0
133 i = 0
120 while i < len(s):
134 while i < len(s):
121 for l in pycompat.xrange(1, 4):
135 for l in pycompat.xrange(1, 4):
122 try:
136 try:
123 yield dmap[s[i:i + l]]
137 yield dmap[s[i:i + l]]
124 i += l
138 i += l
125 break
139 break
126 except KeyError:
140 except KeyError:
127 pass
141 pass
128 else:
142 else:
129 raise KeyError
143 raise KeyError
130 return (lambda s: ''.join([cmap[s[c:c + 1]]
144 return (lambda s: ''.join([cmap[s[c:c + 1]]
131 for c in pycompat.xrange(len(s))]),
145 for c in pycompat.xrange(len(s))]),
132 lambda s: ''.join(list(decode(s))))
146 lambda s: ''.join(list(decode(s))))
133
147
134 _encodefname, _decodefname = _buildencodefun()
148 _encodefname, _decodefname = _buildencodefun()
135
149
136 def encodefilename(s):
150 def encodefilename(s):
137 '''
151 '''
138 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
152 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
139 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
153 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
140 '''
154 '''
141 return _encodefname(encodedir(s))
155 return _encodefname(encodedir(s))
142
156
143 def decodefilename(s):
157 def decodefilename(s):
144 '''
158 '''
145 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
159 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
146 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
160 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
147 '''
161 '''
148 return decodedir(_decodefname(s))
162 return decodedir(_decodefname(s))
149
163
150 def _buildlowerencodefun():
164 def _buildlowerencodefun():
151 '''
165 '''
152 >>> f = _buildlowerencodefun()
166 >>> f = _buildlowerencodefun()
153 >>> f(b'nothing/special.txt')
167 >>> f(b'nothing/special.txt')
154 'nothing/special.txt'
168 'nothing/special.txt'
155 >>> f(b'HELLO')
169 >>> f(b'HELLO')
156 'hello'
170 'hello'
157 >>> f(b'hello:world?')
171 >>> f(b'hello:world?')
158 'hello~3aworld~3f'
172 'hello~3aworld~3f'
159 >>> f(b'the\\x07quick\\xADshot')
173 >>> f(b'the\\x07quick\\xADshot')
160 'the~07quick~adshot'
174 'the~07quick~adshot'
161 '''
175 '''
162 xchr = pycompat.bytechr
176 xchr = pycompat.bytechr
163 cmap = dict([(xchr(x), xchr(x)) for x in pycompat.xrange(127)])
177 cmap = dict([(xchr(x), xchr(x)) for x in pycompat.xrange(127)])
164 for x in _reserved():
178 for x in _reserved():
165 cmap[xchr(x)] = "~%02x" % x
179 cmap[xchr(x)] = "~%02x" % x
166 for x in range(ord("A"), ord("Z") + 1):
180 for x in range(ord("A"), ord("Z") + 1):
167 cmap[xchr(x)] = xchr(x).lower()
181 cmap[xchr(x)] = xchr(x).lower()
168 def lowerencode(s):
182 def lowerencode(s):
169 return "".join([cmap[c] for c in pycompat.iterbytestr(s)])
183 return "".join([cmap[c] for c in pycompat.iterbytestr(s)])
170 return lowerencode
184 return lowerencode
171
185
172 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
186 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
173
187
174 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
188 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
175 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
189 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
176 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
190 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
177 def _auxencode(path, dotencode):
191 def _auxencode(path, dotencode):
178 '''
192 '''
179 Encodes filenames containing names reserved by Windows or which end in
193 Encodes filenames containing names reserved by Windows or which end in
180 period or space. Does not touch other single reserved characters c.
194 period or space. Does not touch other single reserved characters c.
181 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
195 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
182 Additionally encodes space or period at the beginning, if dotencode is
196 Additionally encodes space or period at the beginning, if dotencode is
183 True. Parameter path is assumed to be all lowercase.
197 True. Parameter path is assumed to be all lowercase.
184 A segment only needs encoding if a reserved name appears as a
198 A segment only needs encoding if a reserved name appears as a
185 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
199 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
186 doesn't need encoding.
200 doesn't need encoding.
187
201
188 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
202 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
189 >>> _auxencode(s.split(b'/'), True)
203 >>> _auxencode(s.split(b'/'), True)
190 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
204 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
191 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
205 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
192 >>> _auxencode(s.split(b'/'), False)
206 >>> _auxencode(s.split(b'/'), False)
193 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
207 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
194 >>> _auxencode([b'foo. '], True)
208 >>> _auxencode([b'foo. '], True)
195 ['foo.~20']
209 ['foo.~20']
196 >>> _auxencode([b' .foo'], True)
210 >>> _auxencode([b' .foo'], True)
197 ['~20.foo']
211 ['~20.foo']
198 '''
212 '''
199 for i, n in enumerate(path):
213 for i, n in enumerate(path):
200 if not n:
214 if not n:
201 continue
215 continue
202 if dotencode and n[0] in '. ':
216 if dotencode and n[0] in '. ':
203 n = "~%02x" % ord(n[0:1]) + n[1:]
217 n = "~%02x" % ord(n[0:1]) + n[1:]
204 path[i] = n
218 path[i] = n
205 else:
219 else:
206 l = n.find('.')
220 l = n.find('.')
207 if l == -1:
221 if l == -1:
208 l = len(n)
222 l = len(n)
209 if ((l == 3 and n[:3] in _winres3) or
223 if ((l == 3 and n[:3] in _winres3) or
210 (l == 4 and n[3:4] <= '9' and n[3:4] >= '1'
224 (l == 4 and n[3:4] <= '9' and n[3:4] >= '1'
211 and n[:3] in _winres4)):
225 and n[:3] in _winres4)):
212 # encode third letter ('aux' -> 'au~78')
226 # encode third letter ('aux' -> 'au~78')
213 ec = "~%02x" % ord(n[2:3])
227 ec = "~%02x" % ord(n[2:3])
214 n = n[0:2] + ec + n[3:]
228 n = n[0:2] + ec + n[3:]
215 path[i] = n
229 path[i] = n
216 if n[-1] in '. ':
230 if n[-1] in '. ':
217 # encode last period or space ('foo...' -> 'foo..~2e')
231 # encode last period or space ('foo...' -> 'foo..~2e')
218 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
232 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
219 return path
233 return path
220
234
221 _maxstorepathlen = 120
235 _maxstorepathlen = 120
222 _dirprefixlen = 8
236 _dirprefixlen = 8
223 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
237 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
224
238
225 def _hashencode(path, dotencode):
239 def _hashencode(path, dotencode):
226 digest = node.hex(hashlib.sha1(path).digest())
240 digest = node.hex(hashlib.sha1(path).digest())
227 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
241 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
228 parts = _auxencode(le, dotencode)
242 parts = _auxencode(le, dotencode)
229 basename = parts[-1]
243 basename = parts[-1]
230 _root, ext = os.path.splitext(basename)
244 _root, ext = os.path.splitext(basename)
231 sdirs = []
245 sdirs = []
232 sdirslen = 0
246 sdirslen = 0
233 for p in parts[:-1]:
247 for p in parts[:-1]:
234 d = p[:_dirprefixlen]
248 d = p[:_dirprefixlen]
235 if d[-1] in '. ':
249 if d[-1] in '. ':
236 # Windows can't access dirs ending in period or space
250 # Windows can't access dirs ending in period or space
237 d = d[:-1] + '_'
251 d = d[:-1] + '_'
238 if sdirslen == 0:
252 if sdirslen == 0:
239 t = len(d)
253 t = len(d)
240 else:
254 else:
241 t = sdirslen + 1 + len(d)
255 t = sdirslen + 1 + len(d)
242 if t > _maxshortdirslen:
256 if t > _maxshortdirslen:
243 break
257 break
244 sdirs.append(d)
258 sdirs.append(d)
245 sdirslen = t
259 sdirslen = t
246 dirs = '/'.join(sdirs)
260 dirs = '/'.join(sdirs)
247 if len(dirs) > 0:
261 if len(dirs) > 0:
248 dirs += '/'
262 dirs += '/'
249 res = 'dh/' + dirs + digest + ext
263 res = 'dh/' + dirs + digest + ext
250 spaceleft = _maxstorepathlen - len(res)
264 spaceleft = _maxstorepathlen - len(res)
251 if spaceleft > 0:
265 if spaceleft > 0:
252 filler = basename[:spaceleft]
266 filler = basename[:spaceleft]
253 res = 'dh/' + dirs + filler + digest + ext
267 res = 'dh/' + dirs + filler + digest + ext
254 return res
268 return res
255
269
256 def _hybridencode(path, dotencode):
270 def _hybridencode(path, dotencode):
257 '''encodes path with a length limit
271 '''encodes path with a length limit
258
272
259 Encodes all paths that begin with 'data/', according to the following.
273 Encodes all paths that begin with 'data/', according to the following.
260
274
261 Default encoding (reversible):
275 Default encoding (reversible):
262
276
263 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
277 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
264 characters are encoded as '~xx', where xx is the two digit hex code
278 characters are encoded as '~xx', where xx is the two digit hex code
265 of the character (see encodefilename).
279 of the character (see encodefilename).
266 Relevant path components consisting of Windows reserved filenames are
280 Relevant path components consisting of Windows reserved filenames are
267 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
281 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
268
282
269 Hashed encoding (not reversible):
283 Hashed encoding (not reversible):
270
284
271 If the default-encoded path is longer than _maxstorepathlen, a
285 If the default-encoded path is longer than _maxstorepathlen, a
272 non-reversible hybrid hashing of the path is done instead.
286 non-reversible hybrid hashing of the path is done instead.
273 This encoding uses up to _dirprefixlen characters of all directory
287 This encoding uses up to _dirprefixlen characters of all directory
274 levels of the lowerencoded path, but not more levels than can fit into
288 levels of the lowerencoded path, but not more levels than can fit into
275 _maxshortdirslen.
289 _maxshortdirslen.
276 Then follows the filler followed by the sha digest of the full path.
290 Then follows the filler followed by the sha digest of the full path.
277 The filler is the beginning of the basename of the lowerencoded path
291 The filler is the beginning of the basename of the lowerencoded path
278 (the basename is everything after the last path separator). The filler
292 (the basename is everything after the last path separator). The filler
279 is as long as possible, filling in characters from the basename until
293 is as long as possible, filling in characters from the basename until
280 the encoded path has _maxstorepathlen characters (or all chars of the
294 the encoded path has _maxstorepathlen characters (or all chars of the
281 basename have been taken).
295 basename have been taken).
282 The extension (e.g. '.i' or '.d') is preserved.
296 The extension (e.g. '.i' or '.d') is preserved.
283
297
284 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
298 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
285 encoding was used.
299 encoding was used.
286 '''
300 '''
287 path = encodedir(path)
301 path = encodedir(path)
288 ef = _encodefname(path).split('/')
302 ef = _encodefname(path).split('/')
289 res = '/'.join(_auxencode(ef, dotencode))
303 res = '/'.join(_auxencode(ef, dotencode))
290 if len(res) > _maxstorepathlen:
304 if len(res) > _maxstorepathlen:
291 res = _hashencode(path, dotencode)
305 res = _hashencode(path, dotencode)
292 return res
306 return res
293
307
294 def _pathencode(path):
308 def _pathencode(path):
295 de = encodedir(path)
309 de = encodedir(path)
296 if len(path) > _maxstorepathlen:
310 if len(path) > _maxstorepathlen:
297 return _hashencode(de, True)
311 return _hashencode(de, True)
298 ef = _encodefname(de).split('/')
312 ef = _encodefname(de).split('/')
299 res = '/'.join(_auxencode(ef, True))
313 res = '/'.join(_auxencode(ef, True))
300 if len(res) > _maxstorepathlen:
314 if len(res) > _maxstorepathlen:
301 return _hashencode(de, True)
315 return _hashencode(de, True)
302 return res
316 return res
303
317
304 _pathencode = getattr(parsers, 'pathencode', _pathencode)
318 _pathencode = getattr(parsers, 'pathencode', _pathencode)
305
319
306 def _plainhybridencode(f):
320 def _plainhybridencode(f):
307 return _hybridencode(f, False)
321 return _hybridencode(f, False)
308
322
309 def _calcmode(vfs):
323 def _calcmode(vfs):
310 try:
324 try:
311 # files in .hg/ will be created using this mode
325 # files in .hg/ will be created using this mode
312 mode = vfs.stat().st_mode
326 mode = vfs.stat().st_mode
313 # avoid some useless chmods
327 # avoid some useless chmods
314 if (0o777 & ~util.umask) == (0o777 & mode):
328 if (0o777 & ~util.umask) == (0o777 & mode):
315 mode = None
329 mode = None
316 except OSError:
330 except OSError:
317 mode = None
331 mode = None
318 return mode
332 return mode
319
333
320 _data = ('narrowspec data meta 00manifest.d 00manifest.i'
334 _data = ('narrowspec data meta 00manifest.d 00manifest.i'
321 ' 00changelog.d 00changelog.i phaseroots obsstore')
335 ' 00changelog.d 00changelog.i phaseroots obsstore')
322
336
323 def isrevlog(f, kind, st):
337 def isrevlog(f, kind, st):
324 return kind == stat.S_IFREG and f[-2:] in ('.i', '.d')
338 return kind == stat.S_IFREG and f[-2:] in ('.i', '.d')
325
339
326 class basicstore(object):
340 class basicstore(object):
327 '''base class for local repository stores'''
341 '''base class for local repository stores'''
328 def __init__(self, path, vfstype):
342 def __init__(self, path, vfstype):
329 vfs = vfstype(path)
343 vfs = vfstype(path)
330 self.path = vfs.base
344 self.path = vfs.base
331 self.createmode = _calcmode(vfs)
345 self.createmode = _calcmode(vfs)
332 vfs.createmode = self.createmode
346 vfs.createmode = self.createmode
333 self.rawvfs = vfs
347 self.rawvfs = vfs
334 self.vfs = vfsmod.filtervfs(vfs, encodedir)
348 self.vfs = vfsmod.filtervfs(vfs, encodedir)
335 self.opener = self.vfs
349 self.opener = self.vfs
336
350
337 def join(self, f):
351 def join(self, f):
338 return self.path + '/' + encodedir(f)
352 return self.path + '/' + encodedir(f)
339
353
340 def _walk(self, relpath, recurse, filefilter=isrevlog):
354 def _walk(self, relpath, recurse, filefilter=isrevlog):
341 '''yields (unencoded, encoded, size)'''
355 '''yields (unencoded, encoded, size)'''
342 path = self.path
356 path = self.path
343 if relpath:
357 if relpath:
344 path += '/' + relpath
358 path += '/' + relpath
345 striplen = len(self.path) + 1
359 striplen = len(self.path) + 1
346 l = []
360 l = []
347 if self.rawvfs.isdir(path):
361 if self.rawvfs.isdir(path):
348 visit = [path]
362 visit = [path]
349 readdir = self.rawvfs.readdir
363 readdir = self.rawvfs.readdir
350 while visit:
364 while visit:
351 p = visit.pop()
365 p = visit.pop()
352 for f, kind, st in readdir(p, stat=True):
366 for f, kind, st in readdir(p, stat=True):
353 fp = p + '/' + f
367 fp = p + '/' + f
354 if filefilter(f, kind, st):
368 if filefilter(f, kind, st):
355 n = util.pconvert(fp[striplen:])
369 n = util.pconvert(fp[striplen:])
356 l.append((decodedir(n), n, st.st_size))
370 l.append((decodedir(n), n, st.st_size))
357 elif kind == stat.S_IFDIR and recurse:
371 elif kind == stat.S_IFDIR and recurse:
358 visit.append(fp)
372 visit.append(fp)
359 l.sort()
373 l.sort()
360 return l
374 return l
361
375
362 def datafiles(self, matcher=None):
376 def datafiles(self, matcher=None):
363 return self._walk('data', True) + self._walk('meta', True)
377 return self._walk('data', True) + self._walk('meta', True)
364
378
365 def topfiles(self):
379 def topfiles(self):
366 # yield manifest before changelog
380 # yield manifest before changelog
367 return reversed(self._walk('', False))
381 return reversed(self._walk('', False))
368
382
369 def walk(self, matcher=None):
383 def walk(self, matcher=None):
370 '''yields (unencoded, encoded, size)
384 '''yields (unencoded, encoded, size)
371
385
372 if a matcher is passed, storage files of only those tracked paths
386 if a matcher is passed, storage files of only those tracked paths
373 are passed with matches the matcher
387 are passed with matches the matcher
374 '''
388 '''
375 # yield data files first
389 # yield data files first
376 for x in self.datafiles(matcher):
390 for x in self.datafiles(matcher):
377 yield x
391 yield x
378 for x in self.topfiles():
392 for x in self.topfiles():
379 yield x
393 yield x
380
394
381 def copylist(self):
395 def copylist(self):
382 return ['requires'] + _data.split()
396 return ['requires'] + _data.split()
383
397
384 def write(self, tr):
398 def write(self, tr):
385 pass
399 pass
386
400
387 def invalidatecaches(self):
401 def invalidatecaches(self):
388 pass
402 pass
389
403
390 def markremoved(self, fn):
404 def markremoved(self, fn):
391 pass
405 pass
392
406
393 def __contains__(self, path):
407 def __contains__(self, path):
394 '''Checks if the store contains path'''
408 '''Checks if the store contains path'''
395 path = "/".join(("data", path))
409 path = "/".join(("data", path))
396 # file?
410 # file?
397 if self.vfs.exists(path + ".i"):
411 if self.vfs.exists(path + ".i"):
398 return True
412 return True
399 # dir?
413 # dir?
400 if not path.endswith("/"):
414 if not path.endswith("/"):
401 path = path + "/"
415 path = path + "/"
402 return self.vfs.exists(path)
416 return self.vfs.exists(path)
403
417
404 class encodedstore(basicstore):
418 class encodedstore(basicstore):
405 def __init__(self, path, vfstype):
419 def __init__(self, path, vfstype):
406 vfs = vfstype(path + '/store')
420 vfs = vfstype(path + '/store')
407 self.path = vfs.base
421 self.path = vfs.base
408 self.createmode = _calcmode(vfs)
422 self.createmode = _calcmode(vfs)
409 vfs.createmode = self.createmode
423 vfs.createmode = self.createmode
410 self.rawvfs = vfs
424 self.rawvfs = vfs
411 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
425 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
412 self.opener = self.vfs
426 self.opener = self.vfs
413
427
414 def datafiles(self, matcher=None):
428 def datafiles(self, matcher=None):
415 for a, b, size in super(encodedstore, self).datafiles():
429 for a, b, size in super(encodedstore, self).datafiles():
430 if not _matchtrackedpath(a, matcher):
431 continue
416 try:
432 try:
417 a = decodefilename(a)
433 a = decodefilename(a)
418 except KeyError:
434 except KeyError:
419 a = None
435 a = None
420 yield a, b, size
436 yield a, b, size
421
437
422 def join(self, f):
438 def join(self, f):
423 return self.path + '/' + encodefilename(f)
439 return self.path + '/' + encodefilename(f)
424
440
425 def copylist(self):
441 def copylist(self):
426 return (['requires', '00changelog.i'] +
442 return (['requires', '00changelog.i'] +
427 ['store/' + f for f in _data.split()])
443 ['store/' + f for f in _data.split()])
428
444
429 class fncache(object):
445 class fncache(object):
430 # the filename used to be partially encoded
446 # the filename used to be partially encoded
431 # hence the encodedir/decodedir dance
447 # hence the encodedir/decodedir dance
432 def __init__(self, vfs):
448 def __init__(self, vfs):
433 self.vfs = vfs
449 self.vfs = vfs
434 self.entries = None
450 self.entries = None
435 self._dirty = False
451 self._dirty = False
436
452
437 def _load(self):
453 def _load(self):
438 '''fill the entries from the fncache file'''
454 '''fill the entries from the fncache file'''
439 self._dirty = False
455 self._dirty = False
440 try:
456 try:
441 fp = self.vfs('fncache', mode='rb')
457 fp = self.vfs('fncache', mode='rb')
442 except IOError:
458 except IOError:
443 # skip nonexistent file
459 # skip nonexistent file
444 self.entries = set()
460 self.entries = set()
445 return
461 return
446 self.entries = set(decodedir(fp.read()).splitlines())
462 self.entries = set(decodedir(fp.read()).splitlines())
447 if '' in self.entries:
463 if '' in self.entries:
448 fp.seek(0)
464 fp.seek(0)
449 for n, line in enumerate(util.iterfile(fp)):
465 for n, line in enumerate(util.iterfile(fp)):
450 if not line.rstrip('\n'):
466 if not line.rstrip('\n'):
451 t = _('invalid entry in fncache, line %d') % (n + 1)
467 t = _('invalid entry in fncache, line %d') % (n + 1)
452 raise error.Abort(t)
468 raise error.Abort(t)
453 fp.close()
469 fp.close()
454
470
455 def write(self, tr):
471 def write(self, tr):
456 if self._dirty:
472 if self._dirty:
457 assert self.entries is not None
473 assert self.entries is not None
458 tr.addbackup('fncache')
474 tr.addbackup('fncache')
459 fp = self.vfs('fncache', mode='wb', atomictemp=True)
475 fp = self.vfs('fncache', mode='wb', atomictemp=True)
460 if self.entries:
476 if self.entries:
461 fp.write(encodedir('\n'.join(self.entries) + '\n'))
477 fp.write(encodedir('\n'.join(self.entries) + '\n'))
462 fp.close()
478 fp.close()
463 self._dirty = False
479 self._dirty = False
464
480
465 def add(self, fn):
481 def add(self, fn):
466 if self.entries is None:
482 if self.entries is None:
467 self._load()
483 self._load()
468 if fn not in self.entries:
484 if fn not in self.entries:
469 self._dirty = True
485 self._dirty = True
470 self.entries.add(fn)
486 self.entries.add(fn)
471
487
472 def remove(self, fn):
488 def remove(self, fn):
473 if self.entries is None:
489 if self.entries is None:
474 self._load()
490 self._load()
475 try:
491 try:
476 self.entries.remove(fn)
492 self.entries.remove(fn)
477 self._dirty = True
493 self._dirty = True
478 except KeyError:
494 except KeyError:
479 pass
495 pass
480
496
481 def __contains__(self, fn):
497 def __contains__(self, fn):
482 if self.entries is None:
498 if self.entries is None:
483 self._load()
499 self._load()
484 return fn in self.entries
500 return fn in self.entries
485
501
486 def __iter__(self):
502 def __iter__(self):
487 if self.entries is None:
503 if self.entries is None:
488 self._load()
504 self._load()
489 return iter(self.entries)
505 return iter(self.entries)
490
506
491 class _fncachevfs(vfsmod.abstractvfs, vfsmod.proxyvfs):
507 class _fncachevfs(vfsmod.abstractvfs, vfsmod.proxyvfs):
492 def __init__(self, vfs, fnc, encode):
508 def __init__(self, vfs, fnc, encode):
493 vfsmod.proxyvfs.__init__(self, vfs)
509 vfsmod.proxyvfs.__init__(self, vfs)
494 self.fncache = fnc
510 self.fncache = fnc
495 self.encode = encode
511 self.encode = encode
496
512
497 def __call__(self, path, mode='r', *args, **kw):
513 def __call__(self, path, mode='r', *args, **kw):
498 encoded = self.encode(path)
514 encoded = self.encode(path)
499 if mode not in ('r', 'rb') and (path.startswith('data/') or
515 if mode not in ('r', 'rb') and (path.startswith('data/') or
500 path.startswith('meta/')):
516 path.startswith('meta/')):
501 # do not trigger a fncache load when adding a file that already is
517 # do not trigger a fncache load when adding a file that already is
502 # known to exist.
518 # known to exist.
503 notload = self.fncache.entries is None and self.vfs.exists(encoded)
519 notload = self.fncache.entries is None and self.vfs.exists(encoded)
504 if notload and 'a' in mode and not self.vfs.stat(encoded).st_size:
520 if notload and 'a' in mode and not self.vfs.stat(encoded).st_size:
505 # when appending to an existing file, if the file has size zero,
521 # when appending to an existing file, if the file has size zero,
506 # it should be considered as missing. Such zero-size files are
522 # it should be considered as missing. Such zero-size files are
507 # the result of truncation when a transaction is aborted.
523 # the result of truncation when a transaction is aborted.
508 notload = False
524 notload = False
509 if not notload:
525 if not notload:
510 self.fncache.add(path)
526 self.fncache.add(path)
511 return self.vfs(encoded, mode, *args, **kw)
527 return self.vfs(encoded, mode, *args, **kw)
512
528
513 def join(self, path):
529 def join(self, path):
514 if path:
530 if path:
515 return self.vfs.join(self.encode(path))
531 return self.vfs.join(self.encode(path))
516 else:
532 else:
517 return self.vfs.join(path)
533 return self.vfs.join(path)
518
534
519 class fncachestore(basicstore):
535 class fncachestore(basicstore):
520 def __init__(self, path, vfstype, dotencode):
536 def __init__(self, path, vfstype, dotencode):
521 if dotencode:
537 if dotencode:
522 encode = _pathencode
538 encode = _pathencode
523 else:
539 else:
524 encode = _plainhybridencode
540 encode = _plainhybridencode
525 self.encode = encode
541 self.encode = encode
526 vfs = vfstype(path + '/store')
542 vfs = vfstype(path + '/store')
527 self.path = vfs.base
543 self.path = vfs.base
528 self.pathsep = self.path + '/'
544 self.pathsep = self.path + '/'
529 self.createmode = _calcmode(vfs)
545 self.createmode = _calcmode(vfs)
530 vfs.createmode = self.createmode
546 vfs.createmode = self.createmode
531 self.rawvfs = vfs
547 self.rawvfs = vfs
532 fnc = fncache(vfs)
548 fnc = fncache(vfs)
533 self.fncache = fnc
549 self.fncache = fnc
534 self.vfs = _fncachevfs(vfs, fnc, encode)
550 self.vfs = _fncachevfs(vfs, fnc, encode)
535 self.opener = self.vfs
551 self.opener = self.vfs
536
552
537 def join(self, f):
553 def join(self, f):
538 return self.pathsep + self.encode(f)
554 return self.pathsep + self.encode(f)
539
555
540 def getsize(self, path):
556 def getsize(self, path):
541 return self.rawvfs.stat(path).st_size
557 return self.rawvfs.stat(path).st_size
542
558
543 def datafiles(self, matcher=None):
559 def datafiles(self, matcher=None):
544 for f in sorted(self.fncache):
560 for f in sorted(self.fncache):
561 if not _matchtrackedpath(f, matcher):
562 continue
545 ef = self.encode(f)
563 ef = self.encode(f)
546 try:
564 try:
547 yield f, ef, self.getsize(ef)
565 yield f, ef, self.getsize(ef)
548 except OSError as err:
566 except OSError as err:
549 if err.errno != errno.ENOENT:
567 if err.errno != errno.ENOENT:
550 raise
568 raise
551
569
552 def copylist(self):
570 def copylist(self):
553 d = ('narrowspec data meta dh fncache phaseroots obsstore'
571 d = ('narrowspec data meta dh fncache phaseroots obsstore'
554 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
572 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
555 return (['requires', '00changelog.i'] +
573 return (['requires', '00changelog.i'] +
556 ['store/' + f for f in d.split()])
574 ['store/' + f for f in d.split()])
557
575
558 def write(self, tr):
576 def write(self, tr):
559 self.fncache.write(tr)
577 self.fncache.write(tr)
560
578
561 def invalidatecaches(self):
579 def invalidatecaches(self):
562 self.fncache.entries = None
580 self.fncache.entries = None
563
581
564 def markremoved(self, fn):
582 def markremoved(self, fn):
565 self.fncache.remove(fn)
583 self.fncache.remove(fn)
566
584
567 def _exists(self, f):
585 def _exists(self, f):
568 ef = self.encode(f)
586 ef = self.encode(f)
569 try:
587 try:
570 self.getsize(ef)
588 self.getsize(ef)
571 return True
589 return True
572 except OSError as err:
590 except OSError as err:
573 if err.errno != errno.ENOENT:
591 if err.errno != errno.ENOENT:
574 raise
592 raise
575 # nonexistent entry
593 # nonexistent entry
576 return False
594 return False
577
595
578 def __contains__(self, path):
596 def __contains__(self, path):
579 '''Checks if the store contains path'''
597 '''Checks if the store contains path'''
580 path = "/".join(("data", path))
598 path = "/".join(("data", path))
581 # check for files (exact match)
599 # check for files (exact match)
582 e = path + '.i'
600 e = path + '.i'
583 if e in self.fncache and self._exists(e):
601 if e in self.fncache and self._exists(e):
584 return True
602 return True
585 # now check for directories (prefix match)
603 # now check for directories (prefix match)
586 if not path.endswith('/'):
604 if not path.endswith('/'):
587 path += '/'
605 path += '/'
588 for e in self.fncache:
606 for e in self.fncache:
589 if e.startswith(path) and self._exists(e):
607 if e.startswith(path) and self._exists(e):
590 return True
608 return True
591 return False
609 return False
@@ -1,663 +1,659
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 . import (
15 from . import (
16 branchmap,
16 branchmap,
17 cacheutil,
17 cacheutil,
18 error,
18 error,
19 narrowspec,
19 narrowspec,
20 phases,
20 phases,
21 pycompat,
21 pycompat,
22 repository,
22 repository,
23 store,
23 store,
24 util,
24 util,
25 )
25 )
26
26
27 def canperformstreamclone(pullop, bundle2=False):
27 def canperformstreamclone(pullop, bundle2=False):
28 """Whether it is possible to perform a streaming clone as part of pull.
28 """Whether it is possible to perform a streaming clone as part of pull.
29
29
30 ``bundle2`` will cause the function to consider stream clone through
30 ``bundle2`` will cause the function to consider stream clone through
31 bundle2 and only through bundle2.
31 bundle2 and only through bundle2.
32
32
33 Returns a tuple of (supported, requirements). ``supported`` is True if
33 Returns a tuple of (supported, requirements). ``supported`` is True if
34 streaming clone is supported and False otherwise. ``requirements`` is
34 streaming clone is supported and False otherwise. ``requirements`` is
35 a set of repo requirements from the remote, or ``None`` if stream clone
35 a set of repo requirements from the remote, or ``None`` if stream clone
36 isn't supported.
36 isn't supported.
37 """
37 """
38 repo = pullop.repo
38 repo = pullop.repo
39 remote = pullop.remote
39 remote = pullop.remote
40
40
41 bundle2supported = False
41 bundle2supported = False
42 if pullop.canusebundle2:
42 if pullop.canusebundle2:
43 if 'v2' in pullop.remotebundle2caps.get('stream', []):
43 if 'v2' in pullop.remotebundle2caps.get('stream', []):
44 bundle2supported = True
44 bundle2supported = True
45 # else
45 # else
46 # Server doesn't support bundle2 stream clone or doesn't support
46 # Server doesn't support bundle2 stream clone or doesn't support
47 # the versions we support. Fall back and possibly allow legacy.
47 # the versions we support. Fall back and possibly allow legacy.
48
48
49 # Ensures legacy code path uses available bundle2.
49 # Ensures legacy code path uses available bundle2.
50 if bundle2supported and not bundle2:
50 if bundle2supported and not bundle2:
51 return False, None
51 return False, None
52 # Ensures bundle2 doesn't try to do a stream clone if it isn't supported.
52 # Ensures bundle2 doesn't try to do a stream clone if it isn't supported.
53 elif bundle2 and not bundle2supported:
53 elif bundle2 and not bundle2supported:
54 return False, None
54 return False, None
55
55
56 # Streaming clone only works on empty repositories.
56 # Streaming clone only works on empty repositories.
57 if len(repo):
57 if len(repo):
58 return False, None
58 return False, None
59
59
60 # Streaming clone only works if all data is being requested.
60 # Streaming clone only works if all data is being requested.
61 if pullop.heads:
61 if pullop.heads:
62 return False, None
62 return False, None
63
63
64 streamrequested = pullop.streamclonerequested
64 streamrequested = pullop.streamclonerequested
65
65
66 # If we don't have a preference, let the server decide for us. This
66 # If we don't have a preference, let the server decide for us. This
67 # likely only comes into play in LANs.
67 # likely only comes into play in LANs.
68 if streamrequested is None:
68 if streamrequested is None:
69 # The server can advertise whether to prefer streaming clone.
69 # The server can advertise whether to prefer streaming clone.
70 streamrequested = remote.capable('stream-preferred')
70 streamrequested = remote.capable('stream-preferred')
71
71
72 if not streamrequested:
72 if not streamrequested:
73 return False, None
73 return False, None
74
74
75 # In order for stream clone to work, the client has to support all the
75 # In order for stream clone to work, the client has to support all the
76 # requirements advertised by the server.
76 # requirements advertised by the server.
77 #
77 #
78 # The server advertises its requirements via the "stream" and "streamreqs"
78 # The server advertises its requirements via the "stream" and "streamreqs"
79 # capability. "stream" (a value-less capability) is advertised if and only
79 # capability. "stream" (a value-less capability) is advertised if and only
80 # if the only requirement is "revlogv1." Else, the "streamreqs" capability
80 # if the only requirement is "revlogv1." Else, the "streamreqs" capability
81 # is advertised and contains a comma-delimited list of requirements.
81 # is advertised and contains a comma-delimited list of requirements.
82 requirements = set()
82 requirements = set()
83 if remote.capable('stream'):
83 if remote.capable('stream'):
84 requirements.add('revlogv1')
84 requirements.add('revlogv1')
85 else:
85 else:
86 streamreqs = remote.capable('streamreqs')
86 streamreqs = remote.capable('streamreqs')
87 # This is weird and shouldn't happen with modern servers.
87 # This is weird and shouldn't happen with modern servers.
88 if not streamreqs:
88 if not streamreqs:
89 pullop.repo.ui.warn(_(
89 pullop.repo.ui.warn(_(
90 'warning: stream clone requested but server has them '
90 'warning: stream clone requested but server has them '
91 'disabled\n'))
91 'disabled\n'))
92 return False, None
92 return False, None
93
93
94 streamreqs = set(streamreqs.split(','))
94 streamreqs = set(streamreqs.split(','))
95 # Server requires something we don't support. Bail.
95 # Server requires something we don't support. Bail.
96 missingreqs = streamreqs - repo.supportedformats
96 missingreqs = streamreqs - repo.supportedformats
97 if missingreqs:
97 if missingreqs:
98 pullop.repo.ui.warn(_(
98 pullop.repo.ui.warn(_(
99 'warning: stream clone requested but client is missing '
99 'warning: stream clone requested but client is missing '
100 'requirements: %s\n') % ', '.join(sorted(missingreqs)))
100 'requirements: %s\n') % ', '.join(sorted(missingreqs)))
101 pullop.repo.ui.warn(
101 pullop.repo.ui.warn(
102 _('(see https://www.mercurial-scm.org/wiki/MissingRequirement '
102 _('(see https://www.mercurial-scm.org/wiki/MissingRequirement '
103 'for more information)\n'))
103 'for more information)\n'))
104 return False, None
104 return False, None
105 requirements = streamreqs
105 requirements = streamreqs
106
106
107 return True, requirements
107 return True, requirements
108
108
109 def maybeperformlegacystreamclone(pullop):
109 def maybeperformlegacystreamclone(pullop):
110 """Possibly perform a legacy stream clone operation.
110 """Possibly perform a legacy stream clone operation.
111
111
112 Legacy stream clones are performed as part of pull but before all other
112 Legacy stream clones are performed as part of pull but before all other
113 operations.
113 operations.
114
114
115 A legacy stream clone will not be performed if a bundle2 stream clone is
115 A legacy stream clone will not be performed if a bundle2 stream clone is
116 supported.
116 supported.
117 """
117 """
118 from . import localrepo
118 from . import localrepo
119
119
120 supported, requirements = canperformstreamclone(pullop)
120 supported, requirements = canperformstreamclone(pullop)
121
121
122 if not supported:
122 if not supported:
123 return
123 return
124
124
125 repo = pullop.repo
125 repo = pullop.repo
126 remote = pullop.remote
126 remote = pullop.remote
127
127
128 # Save remote branchmap. We will use it later to speed up branchcache
128 # Save remote branchmap. We will use it later to speed up branchcache
129 # creation.
129 # creation.
130 rbranchmap = None
130 rbranchmap = None
131 if remote.capable('branchmap'):
131 if remote.capable('branchmap'):
132 with remote.commandexecutor() as e:
132 with remote.commandexecutor() as e:
133 rbranchmap = e.callcommand('branchmap', {}).result()
133 rbranchmap = e.callcommand('branchmap', {}).result()
134
134
135 repo.ui.status(_('streaming all changes\n'))
135 repo.ui.status(_('streaming all changes\n'))
136
136
137 with remote.commandexecutor() as e:
137 with remote.commandexecutor() as e:
138 fp = e.callcommand('stream_out', {}).result()
138 fp = e.callcommand('stream_out', {}).result()
139
139
140 # TODO strictly speaking, this code should all be inside the context
140 # TODO strictly speaking, this code should all be inside the context
141 # manager because the context manager is supposed to ensure all wire state
141 # manager because the context manager is supposed to ensure all wire state
142 # is flushed when exiting. But the legacy peers don't do this, so it
142 # is flushed when exiting. But the legacy peers don't do this, so it
143 # doesn't matter.
143 # doesn't matter.
144 l = fp.readline()
144 l = fp.readline()
145 try:
145 try:
146 resp = int(l)
146 resp = int(l)
147 except ValueError:
147 except ValueError:
148 raise error.ResponseError(
148 raise error.ResponseError(
149 _('unexpected response from remote server:'), l)
149 _('unexpected response from remote server:'), l)
150 if resp == 1:
150 if resp == 1:
151 raise error.Abort(_('operation forbidden by server'))
151 raise error.Abort(_('operation forbidden by server'))
152 elif resp == 2:
152 elif resp == 2:
153 raise error.Abort(_('locking the remote repository failed'))
153 raise error.Abort(_('locking the remote repository failed'))
154 elif resp != 0:
154 elif resp != 0:
155 raise error.Abort(_('the server sent an unknown error code'))
155 raise error.Abort(_('the server sent an unknown error code'))
156
156
157 l = fp.readline()
157 l = fp.readline()
158 try:
158 try:
159 filecount, bytecount = map(int, l.split(' ', 1))
159 filecount, bytecount = map(int, l.split(' ', 1))
160 except (ValueError, TypeError):
160 except (ValueError, TypeError):
161 raise error.ResponseError(
161 raise error.ResponseError(
162 _('unexpected response from remote server:'), l)
162 _('unexpected response from remote server:'), l)
163
163
164 with repo.lock():
164 with repo.lock():
165 consumev1(repo, fp, filecount, bytecount)
165 consumev1(repo, fp, filecount, bytecount)
166
166
167 # new requirements = old non-format requirements +
167 # new requirements = old non-format requirements +
168 # new format-related remote requirements
168 # new format-related remote requirements
169 # requirements from the streamed-in repository
169 # requirements from the streamed-in repository
170 repo.requirements = requirements | (
170 repo.requirements = requirements | (
171 repo.requirements - repo.supportedformats)
171 repo.requirements - repo.supportedformats)
172 repo.svfs.options = localrepo.resolvestorevfsoptions(
172 repo.svfs.options = localrepo.resolvestorevfsoptions(
173 repo.ui, repo.requirements, repo.features)
173 repo.ui, repo.requirements, repo.features)
174 repo._writerequirements()
174 repo._writerequirements()
175
175
176 if rbranchmap:
176 if rbranchmap:
177 branchmap.replacecache(repo, rbranchmap)
177 branchmap.replacecache(repo, rbranchmap)
178
178
179 repo.invalidate()
179 repo.invalidate()
180
180
181 def allowservergeneration(repo):
181 def allowservergeneration(repo):
182 """Whether streaming clones are allowed from the server."""
182 """Whether streaming clones are allowed from the server."""
183 if repository.REPO_FEATURE_STREAM_CLONE not in repo.features:
183 if repository.REPO_FEATURE_STREAM_CLONE not in repo.features:
184 return False
184 return False
185
185
186 if not repo.ui.configbool('server', 'uncompressed', untrusted=True):
186 if not repo.ui.configbool('server', 'uncompressed', untrusted=True):
187 return False
187 return False
188
188
189 # The way stream clone works makes it impossible to hide secret changesets.
189 # The way stream clone works makes it impossible to hide secret changesets.
190 # So don't allow this by default.
190 # So don't allow this by default.
191 secret = phases.hassecret(repo)
191 secret = phases.hassecret(repo)
192 if secret:
192 if secret:
193 return repo.ui.configbool('server', 'uncompressedallowsecret')
193 return repo.ui.configbool('server', 'uncompressedallowsecret')
194
194
195 return True
195 return True
196
196
197 # This is it's own function so extensions can override it.
197 # This is it's own function so extensions can override it.
198 def _walkstreamfiles(repo, matcher=None):
198 def _walkstreamfiles(repo, matcher=None):
199 return repo.store.walk(matcher)
199 return repo.store.walk(matcher)
200
200
201 def generatev1(repo):
201 def generatev1(repo):
202 """Emit content for version 1 of a streaming clone.
202 """Emit content for version 1 of a streaming clone.
203
203
204 This returns a 3-tuple of (file count, byte size, data iterator).
204 This returns a 3-tuple of (file count, byte size, data iterator).
205
205
206 The data iterator consists of N entries for each file being transferred.
206 The data iterator consists of N entries for each file being transferred.
207 Each file entry starts as a line with the file name and integer size
207 Each file entry starts as a line with the file name and integer size
208 delimited by a null byte.
208 delimited by a null byte.
209
209
210 The raw file data follows. Following the raw file data is the next file
210 The raw file data follows. Following the raw file data is the next file
211 entry, or EOF.
211 entry, or EOF.
212
212
213 When used on the wire protocol, an additional line indicating protocol
213 When used on the wire protocol, an additional line indicating protocol
214 success will be prepended to the stream. This function is not responsible
214 success will be prepended to the stream. This function is not responsible
215 for adding it.
215 for adding it.
216
216
217 This function will obtain a repository lock to ensure a consistent view of
217 This function will obtain a repository lock to ensure a consistent view of
218 the store is captured. It therefore may raise LockError.
218 the store is captured. It therefore may raise LockError.
219 """
219 """
220 entries = []
220 entries = []
221 total_bytes = 0
221 total_bytes = 0
222 # Get consistent snapshot of repo, lock during scan.
222 # Get consistent snapshot of repo, lock during scan.
223 with repo.lock():
223 with repo.lock():
224 repo.ui.debug('scanning\n')
224 repo.ui.debug('scanning\n')
225 for name, ename, size in _walkstreamfiles(repo):
225 for name, ename, size in _walkstreamfiles(repo):
226 if size:
226 if size:
227 entries.append((name, size))
227 entries.append((name, size))
228 total_bytes += size
228 total_bytes += size
229
229
230 repo.ui.debug('%d files, %d bytes to transfer\n' %
230 repo.ui.debug('%d files, %d bytes to transfer\n' %
231 (len(entries), total_bytes))
231 (len(entries), total_bytes))
232
232
233 svfs = repo.svfs
233 svfs = repo.svfs
234 debugflag = repo.ui.debugflag
234 debugflag = repo.ui.debugflag
235
235
236 def emitrevlogdata():
236 def emitrevlogdata():
237 for name, size in entries:
237 for name, size in entries:
238 if debugflag:
238 if debugflag:
239 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
239 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
240 # partially encode name over the wire for backwards compat
240 # partially encode name over the wire for backwards compat
241 yield '%s\0%d\n' % (store.encodedir(name), size)
241 yield '%s\0%d\n' % (store.encodedir(name), size)
242 # auditing at this stage is both pointless (paths are already
242 # auditing at this stage is both pointless (paths are already
243 # trusted by the local repo) and expensive
243 # trusted by the local repo) and expensive
244 with svfs(name, 'rb', auditpath=False) as fp:
244 with svfs(name, 'rb', auditpath=False) as fp:
245 if size <= 65536:
245 if size <= 65536:
246 yield fp.read(size)
246 yield fp.read(size)
247 else:
247 else:
248 for chunk in util.filechunkiter(fp, limit=size):
248 for chunk in util.filechunkiter(fp, limit=size):
249 yield chunk
249 yield chunk
250
250
251 return len(entries), total_bytes, emitrevlogdata()
251 return len(entries), total_bytes, emitrevlogdata()
252
252
253 def generatev1wireproto(repo):
253 def generatev1wireproto(repo):
254 """Emit content for version 1 of streaming clone suitable for the wire.
254 """Emit content for version 1 of streaming clone suitable for the wire.
255
255
256 This is the data output from ``generatev1()`` with 2 header lines. The
256 This is the data output from ``generatev1()`` with 2 header lines. The
257 first line indicates overall success. The 2nd contains the file count and
257 first line indicates overall success. The 2nd contains the file count and
258 byte size of payload.
258 byte size of payload.
259
259
260 The success line contains "0" for success, "1" for stream generation not
260 The success line contains "0" for success, "1" for stream generation not
261 allowed, and "2" for error locking the repository (possibly indicating
261 allowed, and "2" for error locking the repository (possibly indicating
262 a permissions error for the server process).
262 a permissions error for the server process).
263 """
263 """
264 if not allowservergeneration(repo):
264 if not allowservergeneration(repo):
265 yield '1\n'
265 yield '1\n'
266 return
266 return
267
267
268 try:
268 try:
269 filecount, bytecount, it = generatev1(repo)
269 filecount, bytecount, it = generatev1(repo)
270 except error.LockError:
270 except error.LockError:
271 yield '2\n'
271 yield '2\n'
272 return
272 return
273
273
274 # Indicates successful response.
274 # Indicates successful response.
275 yield '0\n'
275 yield '0\n'
276 yield '%d %d\n' % (filecount, bytecount)
276 yield '%d %d\n' % (filecount, bytecount)
277 for chunk in it:
277 for chunk in it:
278 yield chunk
278 yield chunk
279
279
280 def generatebundlev1(repo, compression='UN'):
280 def generatebundlev1(repo, compression='UN'):
281 """Emit content for version 1 of a stream clone bundle.
281 """Emit content for version 1 of a stream clone bundle.
282
282
283 The first 4 bytes of the output ("HGS1") denote this as stream clone
283 The first 4 bytes of the output ("HGS1") denote this as stream clone
284 bundle version 1.
284 bundle version 1.
285
285
286 The next 2 bytes indicate the compression type. Only "UN" is currently
286 The next 2 bytes indicate the compression type. Only "UN" is currently
287 supported.
287 supported.
288
288
289 The next 16 bytes are two 64-bit big endian unsigned integers indicating
289 The next 16 bytes are two 64-bit big endian unsigned integers indicating
290 file count and byte count, respectively.
290 file count and byte count, respectively.
291
291
292 The next 2 bytes is a 16-bit big endian unsigned short declaring the length
292 The next 2 bytes is a 16-bit big endian unsigned short declaring the length
293 of the requirements string, including a trailing \0. The following N bytes
293 of the requirements string, including a trailing \0. The following N bytes
294 are the requirements string, which is ASCII containing a comma-delimited
294 are the requirements string, which is ASCII containing a comma-delimited
295 list of repo requirements that are needed to support the data.
295 list of repo requirements that are needed to support the data.
296
296
297 The remaining content is the output of ``generatev1()`` (which may be
297 The remaining content is the output of ``generatev1()`` (which may be
298 compressed in the future).
298 compressed in the future).
299
299
300 Returns a tuple of (requirements, data generator).
300 Returns a tuple of (requirements, data generator).
301 """
301 """
302 if compression != 'UN':
302 if compression != 'UN':
303 raise ValueError('we do not support the compression argument yet')
303 raise ValueError('we do not support the compression argument yet')
304
304
305 requirements = repo.requirements & repo.supportedformats
305 requirements = repo.requirements & repo.supportedformats
306 requires = ','.join(sorted(requirements))
306 requires = ','.join(sorted(requirements))
307
307
308 def gen():
308 def gen():
309 yield 'HGS1'
309 yield 'HGS1'
310 yield compression
310 yield compression
311
311
312 filecount, bytecount, it = generatev1(repo)
312 filecount, bytecount, it = generatev1(repo)
313 repo.ui.status(_('writing %d bytes for %d files\n') %
313 repo.ui.status(_('writing %d bytes for %d files\n') %
314 (bytecount, filecount))
314 (bytecount, filecount))
315
315
316 yield struct.pack('>QQ', filecount, bytecount)
316 yield struct.pack('>QQ', filecount, bytecount)
317 yield struct.pack('>H', len(requires) + 1)
317 yield struct.pack('>H', len(requires) + 1)
318 yield requires + '\0'
318 yield requires + '\0'
319
319
320 # This is where we'll add compression in the future.
320 # This is where we'll add compression in the future.
321 assert compression == 'UN'
321 assert compression == 'UN'
322
322
323 progress = repo.ui.makeprogress(_('bundle'), total=bytecount,
323 progress = repo.ui.makeprogress(_('bundle'), total=bytecount,
324 unit=_('bytes'))
324 unit=_('bytes'))
325 progress.update(0)
325 progress.update(0)
326
326
327 for chunk in it:
327 for chunk in it:
328 progress.increment(step=len(chunk))
328 progress.increment(step=len(chunk))
329 yield chunk
329 yield chunk
330
330
331 progress.complete()
331 progress.complete()
332
332
333 return requirements, gen()
333 return requirements, gen()
334
334
335 def consumev1(repo, fp, filecount, bytecount):
335 def consumev1(repo, fp, filecount, bytecount):
336 """Apply the contents from version 1 of a streaming clone file handle.
336 """Apply the contents from version 1 of a streaming clone file handle.
337
337
338 This takes the output from "stream_out" and applies it to the specified
338 This takes the output from "stream_out" and applies it to the specified
339 repository.
339 repository.
340
340
341 Like "stream_out," the status line added by the wire protocol is not
341 Like "stream_out," the status line added by the wire protocol is not
342 handled by this function.
342 handled by this function.
343 """
343 """
344 with repo.lock():
344 with repo.lock():
345 repo.ui.status(_('%d files to transfer, %s of data\n') %
345 repo.ui.status(_('%d files to transfer, %s of data\n') %
346 (filecount, util.bytecount(bytecount)))
346 (filecount, util.bytecount(bytecount)))
347 progress = repo.ui.makeprogress(_('clone'), total=bytecount,
347 progress = repo.ui.makeprogress(_('clone'), total=bytecount,
348 unit=_('bytes'))
348 unit=_('bytes'))
349 progress.update(0)
349 progress.update(0)
350 start = util.timer()
350 start = util.timer()
351
351
352 # TODO: get rid of (potential) inconsistency
352 # TODO: get rid of (potential) inconsistency
353 #
353 #
354 # If transaction is started and any @filecache property is
354 # If transaction is started and any @filecache property is
355 # changed at this point, it causes inconsistency between
355 # changed at this point, it causes inconsistency between
356 # in-memory cached property and streamclone-ed file on the
356 # in-memory cached property and streamclone-ed file on the
357 # disk. Nested transaction prevents transaction scope "clone"
357 # disk. Nested transaction prevents transaction scope "clone"
358 # below from writing in-memory changes out at the end of it,
358 # below from writing in-memory changes out at the end of it,
359 # even though in-memory changes are discarded at the end of it
359 # even though in-memory changes are discarded at the end of it
360 # regardless of transaction nesting.
360 # regardless of transaction nesting.
361 #
361 #
362 # But transaction nesting can't be simply prohibited, because
362 # But transaction nesting can't be simply prohibited, because
363 # nesting occurs also in ordinary case (e.g. enabling
363 # nesting occurs also in ordinary case (e.g. enabling
364 # clonebundles).
364 # clonebundles).
365
365
366 with repo.transaction('clone'):
366 with repo.transaction('clone'):
367 with repo.svfs.backgroundclosing(repo.ui, expectedcount=filecount):
367 with repo.svfs.backgroundclosing(repo.ui, expectedcount=filecount):
368 for i in pycompat.xrange(filecount):
368 for i in pycompat.xrange(filecount):
369 # XXX doesn't support '\n' or '\r' in filenames
369 # XXX doesn't support '\n' or '\r' in filenames
370 l = fp.readline()
370 l = fp.readline()
371 try:
371 try:
372 name, size = l.split('\0', 1)
372 name, size = l.split('\0', 1)
373 size = int(size)
373 size = int(size)
374 except (ValueError, TypeError):
374 except (ValueError, TypeError):
375 raise error.ResponseError(
375 raise error.ResponseError(
376 _('unexpected response from remote server:'), l)
376 _('unexpected response from remote server:'), l)
377 if repo.ui.debugflag:
377 if repo.ui.debugflag:
378 repo.ui.debug('adding %s (%s)\n' %
378 repo.ui.debug('adding %s (%s)\n' %
379 (name, util.bytecount(size)))
379 (name, util.bytecount(size)))
380 # for backwards compat, name was partially encoded
380 # for backwards compat, name was partially encoded
381 path = store.decodedir(name)
381 path = store.decodedir(name)
382 with repo.svfs(path, 'w', backgroundclose=True) as ofp:
382 with repo.svfs(path, 'w', backgroundclose=True) as ofp:
383 for chunk in util.filechunkiter(fp, limit=size):
383 for chunk in util.filechunkiter(fp, limit=size):
384 progress.increment(step=len(chunk))
384 progress.increment(step=len(chunk))
385 ofp.write(chunk)
385 ofp.write(chunk)
386
386
387 # force @filecache properties to be reloaded from
387 # force @filecache properties to be reloaded from
388 # streamclone-ed file at next access
388 # streamclone-ed file at next access
389 repo.invalidate(clearfilecache=True)
389 repo.invalidate(clearfilecache=True)
390
390
391 elapsed = util.timer() - start
391 elapsed = util.timer() - start
392 if elapsed <= 0:
392 if elapsed <= 0:
393 elapsed = 0.001
393 elapsed = 0.001
394 progress.complete()
394 progress.complete()
395 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
395 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
396 (util.bytecount(bytecount), elapsed,
396 (util.bytecount(bytecount), elapsed,
397 util.bytecount(bytecount / elapsed)))
397 util.bytecount(bytecount / elapsed)))
398
398
399 def readbundle1header(fp):
399 def readbundle1header(fp):
400 compression = fp.read(2)
400 compression = fp.read(2)
401 if compression != 'UN':
401 if compression != 'UN':
402 raise error.Abort(_('only uncompressed stream clone bundles are '
402 raise error.Abort(_('only uncompressed stream clone bundles are '
403 'supported; got %s') % compression)
403 'supported; got %s') % compression)
404
404
405 filecount, bytecount = struct.unpack('>QQ', fp.read(16))
405 filecount, bytecount = struct.unpack('>QQ', fp.read(16))
406 requireslen = struct.unpack('>H', fp.read(2))[0]
406 requireslen = struct.unpack('>H', fp.read(2))[0]
407 requires = fp.read(requireslen)
407 requires = fp.read(requireslen)
408
408
409 if not requires.endswith('\0'):
409 if not requires.endswith('\0'):
410 raise error.Abort(_('malformed stream clone bundle: '
410 raise error.Abort(_('malformed stream clone bundle: '
411 'requirements not properly encoded'))
411 'requirements not properly encoded'))
412
412
413 requirements = set(requires.rstrip('\0').split(','))
413 requirements = set(requires.rstrip('\0').split(','))
414
414
415 return filecount, bytecount, requirements
415 return filecount, bytecount, requirements
416
416
417 def applybundlev1(repo, fp):
417 def applybundlev1(repo, fp):
418 """Apply the content from a stream clone bundle version 1.
418 """Apply the content from a stream clone bundle version 1.
419
419
420 We assume the 4 byte header has been read and validated and the file handle
420 We assume the 4 byte header has been read and validated and the file handle
421 is at the 2 byte compression identifier.
421 is at the 2 byte compression identifier.
422 """
422 """
423 if len(repo):
423 if len(repo):
424 raise error.Abort(_('cannot apply stream clone bundle on non-empty '
424 raise error.Abort(_('cannot apply stream clone bundle on non-empty '
425 'repo'))
425 'repo'))
426
426
427 filecount, bytecount, requirements = readbundle1header(fp)
427 filecount, bytecount, requirements = readbundle1header(fp)
428 missingreqs = requirements - repo.supportedformats
428 missingreqs = requirements - repo.supportedformats
429 if missingreqs:
429 if missingreqs:
430 raise error.Abort(_('unable to apply stream clone: '
430 raise error.Abort(_('unable to apply stream clone: '
431 'unsupported format: %s') %
431 'unsupported format: %s') %
432 ', '.join(sorted(missingreqs)))
432 ', '.join(sorted(missingreqs)))
433
433
434 consumev1(repo, fp, filecount, bytecount)
434 consumev1(repo, fp, filecount, bytecount)
435
435
436 class streamcloneapplier(object):
436 class streamcloneapplier(object):
437 """Class to manage applying streaming clone bundles.
437 """Class to manage applying streaming clone bundles.
438
438
439 We need to wrap ``applybundlev1()`` in a dedicated type to enable bundle
439 We need to wrap ``applybundlev1()`` in a dedicated type to enable bundle
440 readers to perform bundle type-specific functionality.
440 readers to perform bundle type-specific functionality.
441 """
441 """
442 def __init__(self, fh):
442 def __init__(self, fh):
443 self._fh = fh
443 self._fh = fh
444
444
445 def apply(self, repo):
445 def apply(self, repo):
446 return applybundlev1(repo, self._fh)
446 return applybundlev1(repo, self._fh)
447
447
448 # type of file to stream
448 # type of file to stream
449 _fileappend = 0 # append only file
449 _fileappend = 0 # append only file
450 _filefull = 1 # full snapshot file
450 _filefull = 1 # full snapshot file
451
451
452 # Source of the file
452 # Source of the file
453 _srcstore = 's' # store (svfs)
453 _srcstore = 's' # store (svfs)
454 _srccache = 'c' # cache (cache)
454 _srccache = 'c' # cache (cache)
455
455
456 # This is it's own function so extensions can override it.
456 # This is it's own function so extensions can override it.
457 def _walkstreamfullstorefiles(repo):
457 def _walkstreamfullstorefiles(repo):
458 """list snapshot file from the store"""
458 """list snapshot file from the store"""
459 fnames = []
459 fnames = []
460 if not repo.publishing():
460 if not repo.publishing():
461 fnames.append('phaseroots')
461 fnames.append('phaseroots')
462 return fnames
462 return fnames
463
463
464 def _filterfull(entry, copy, vfsmap):
464 def _filterfull(entry, copy, vfsmap):
465 """actually copy the snapshot files"""
465 """actually copy the snapshot files"""
466 src, name, ftype, data = entry
466 src, name, ftype, data = entry
467 if ftype != _filefull:
467 if ftype != _filefull:
468 return entry
468 return entry
469 return (src, name, ftype, copy(vfsmap[src].join(name)))
469 return (src, name, ftype, copy(vfsmap[src].join(name)))
470
470
471 @contextlib.contextmanager
471 @contextlib.contextmanager
472 def maketempcopies():
472 def maketempcopies():
473 """return a function to temporary copy file"""
473 """return a function to temporary copy file"""
474 files = []
474 files = []
475 try:
475 try:
476 def copy(src):
476 def copy(src):
477 fd, dst = pycompat.mkstemp()
477 fd, dst = pycompat.mkstemp()
478 os.close(fd)
478 os.close(fd)
479 files.append(dst)
479 files.append(dst)
480 util.copyfiles(src, dst, hardlink=True)
480 util.copyfiles(src, dst, hardlink=True)
481 return dst
481 return dst
482 yield copy
482 yield copy
483 finally:
483 finally:
484 for tmp in files:
484 for tmp in files:
485 util.tryunlink(tmp)
485 util.tryunlink(tmp)
486
486
487 def _makemap(repo):
487 def _makemap(repo):
488 """make a (src -> vfs) map for the repo"""
488 """make a (src -> vfs) map for the repo"""
489 vfsmap = {
489 vfsmap = {
490 _srcstore: repo.svfs,
490 _srcstore: repo.svfs,
491 _srccache: repo.cachevfs,
491 _srccache: repo.cachevfs,
492 }
492 }
493 # we keep repo.vfs out of the on purpose, ther are too many danger there
493 # we keep repo.vfs out of the on purpose, ther are too many danger there
494 # (eg: .hg/hgrc)
494 # (eg: .hg/hgrc)
495 assert repo.vfs not in vfsmap.values()
495 assert repo.vfs not in vfsmap.values()
496
496
497 return vfsmap
497 return vfsmap
498
498
499 def _emit2(repo, entries, totalfilesize):
499 def _emit2(repo, entries, totalfilesize):
500 """actually emit the stream bundle"""
500 """actually emit the stream bundle"""
501 vfsmap = _makemap(repo)
501 vfsmap = _makemap(repo)
502 progress = repo.ui.makeprogress(_('bundle'), total=totalfilesize,
502 progress = repo.ui.makeprogress(_('bundle'), total=totalfilesize,
503 unit=_('bytes'))
503 unit=_('bytes'))
504 progress.update(0)
504 progress.update(0)
505 with maketempcopies() as copy, progress:
505 with maketempcopies() as copy, progress:
506 # copy is delayed until we are in the try
506 # copy is delayed until we are in the try
507 entries = [_filterfull(e, copy, vfsmap) for e in entries]
507 entries = [_filterfull(e, copy, vfsmap) for e in entries]
508 yield None # this release the lock on the repository
508 yield None # this release the lock on the repository
509 seen = 0
509 seen = 0
510
510
511 for src, name, ftype, data in entries:
511 for src, name, ftype, data in entries:
512 vfs = vfsmap[src]
512 vfs = vfsmap[src]
513 yield src
513 yield src
514 yield util.uvarintencode(len(name))
514 yield util.uvarintencode(len(name))
515 if ftype == _fileappend:
515 if ftype == _fileappend:
516 fp = vfs(name)
516 fp = vfs(name)
517 size = data
517 size = data
518 elif ftype == _filefull:
518 elif ftype == _filefull:
519 fp = open(data, 'rb')
519 fp = open(data, 'rb')
520 size = util.fstat(fp).st_size
520 size = util.fstat(fp).st_size
521 try:
521 try:
522 yield util.uvarintencode(size)
522 yield util.uvarintencode(size)
523 yield name
523 yield name
524 if size <= 65536:
524 if size <= 65536:
525 chunks = (fp.read(size),)
525 chunks = (fp.read(size),)
526 else:
526 else:
527 chunks = util.filechunkiter(fp, limit=size)
527 chunks = util.filechunkiter(fp, limit=size)
528 for chunk in chunks:
528 for chunk in chunks:
529 seen += len(chunk)
529 seen += len(chunk)
530 progress.update(seen)
530 progress.update(seen)
531 yield chunk
531 yield chunk
532 finally:
532 finally:
533 fp.close()
533 fp.close()
534
534
535 def generatev2(repo, includes, excludes, includeobsmarkers):
535 def generatev2(repo, includes, excludes, includeobsmarkers):
536 """Emit content for version 2 of a streaming clone.
536 """Emit content for version 2 of a streaming clone.
537
537
538 the data stream consists the following entries:
538 the data stream consists the following entries:
539 1) A char representing the file destination (eg: store or cache)
539 1) A char representing the file destination (eg: store or cache)
540 2) A varint containing the length of the filename
540 2) A varint containing the length of the filename
541 3) A varint containing the length of file data
541 3) A varint containing the length of file data
542 4) N bytes containing the filename (the internal, store-agnostic form)
542 4) N bytes containing the filename (the internal, store-agnostic form)
543 5) N bytes containing the file data
543 5) N bytes containing the file data
544
544
545 Returns a 3-tuple of (file count, file size, data iterator).
545 Returns a 3-tuple of (file count, file size, data iterator).
546 """
546 """
547
547
548 # temporarily raise error until we add storage level logic
549 if includes or excludes:
550 raise error.Abort(_("server does not support narrow stream clones"))
551
552 with repo.lock():
548 with repo.lock():
553
549
554 entries = []
550 entries = []
555 totalfilesize = 0
551 totalfilesize = 0
556
552
557 matcher = None
553 matcher = None
558 if includes or excludes:
554 if includes or excludes:
559 matcher = narrowspec.match(repo.root, includes, excludes)
555 matcher = narrowspec.match(repo.root, includes, excludes)
560
556
561 repo.ui.debug('scanning\n')
557 repo.ui.debug('scanning\n')
562 for name, ename, size in _walkstreamfiles(repo, matcher):
558 for name, ename, size in _walkstreamfiles(repo, matcher):
563 if size:
559 if size:
564 entries.append((_srcstore, name, _fileappend, size))
560 entries.append((_srcstore, name, _fileappend, size))
565 totalfilesize += size
561 totalfilesize += size
566 for name in _walkstreamfullstorefiles(repo):
562 for name in _walkstreamfullstorefiles(repo):
567 if repo.svfs.exists(name):
563 if repo.svfs.exists(name):
568 totalfilesize += repo.svfs.lstat(name).st_size
564 totalfilesize += repo.svfs.lstat(name).st_size
569 entries.append((_srcstore, name, _filefull, None))
565 entries.append((_srcstore, name, _filefull, None))
570 if includeobsmarkers and repo.svfs.exists('obsstore'):
566 if includeobsmarkers and repo.svfs.exists('obsstore'):
571 totalfilesize += repo.svfs.lstat('obsstore').st_size
567 totalfilesize += repo.svfs.lstat('obsstore').st_size
572 entries.append((_srcstore, 'obsstore', _filefull, None))
568 entries.append((_srcstore, 'obsstore', _filefull, None))
573 for name in cacheutil.cachetocopy(repo):
569 for name in cacheutil.cachetocopy(repo):
574 if repo.cachevfs.exists(name):
570 if repo.cachevfs.exists(name):
575 totalfilesize += repo.cachevfs.lstat(name).st_size
571 totalfilesize += repo.cachevfs.lstat(name).st_size
576 entries.append((_srccache, name, _filefull, None))
572 entries.append((_srccache, name, _filefull, None))
577
573
578 chunks = _emit2(repo, entries, totalfilesize)
574 chunks = _emit2(repo, entries, totalfilesize)
579 first = next(chunks)
575 first = next(chunks)
580 assert first is None
576 assert first is None
581
577
582 return len(entries), totalfilesize, chunks
578 return len(entries), totalfilesize, chunks
583
579
584 @contextlib.contextmanager
580 @contextlib.contextmanager
585 def nested(*ctxs):
581 def nested(*ctxs):
586 this = ctxs[0]
582 this = ctxs[0]
587 rest = ctxs[1:]
583 rest = ctxs[1:]
588 with this:
584 with this:
589 if rest:
585 if rest:
590 with nested(*rest):
586 with nested(*rest):
591 yield
587 yield
592 else:
588 else:
593 yield
589 yield
594
590
595 def consumev2(repo, fp, filecount, filesize):
591 def consumev2(repo, fp, filecount, filesize):
596 """Apply the contents from a version 2 streaming clone.
592 """Apply the contents from a version 2 streaming clone.
597
593
598 Data is read from an object that only needs to provide a ``read(size)``
594 Data is read from an object that only needs to provide a ``read(size)``
599 method.
595 method.
600 """
596 """
601 with repo.lock():
597 with repo.lock():
602 repo.ui.status(_('%d files to transfer, %s of data\n') %
598 repo.ui.status(_('%d files to transfer, %s of data\n') %
603 (filecount, util.bytecount(filesize)))
599 (filecount, util.bytecount(filesize)))
604
600
605 start = util.timer()
601 start = util.timer()
606 progress = repo.ui.makeprogress(_('clone'), total=filesize,
602 progress = repo.ui.makeprogress(_('clone'), total=filesize,
607 unit=_('bytes'))
603 unit=_('bytes'))
608 progress.update(0)
604 progress.update(0)
609
605
610 vfsmap = _makemap(repo)
606 vfsmap = _makemap(repo)
611
607
612 with repo.transaction('clone'):
608 with repo.transaction('clone'):
613 ctxs = (vfs.backgroundclosing(repo.ui)
609 ctxs = (vfs.backgroundclosing(repo.ui)
614 for vfs in vfsmap.values())
610 for vfs in vfsmap.values())
615 with nested(*ctxs):
611 with nested(*ctxs):
616 for i in range(filecount):
612 for i in range(filecount):
617 src = util.readexactly(fp, 1)
613 src = util.readexactly(fp, 1)
618 vfs = vfsmap[src]
614 vfs = vfsmap[src]
619 namelen = util.uvarintdecodestream(fp)
615 namelen = util.uvarintdecodestream(fp)
620 datalen = util.uvarintdecodestream(fp)
616 datalen = util.uvarintdecodestream(fp)
621
617
622 name = util.readexactly(fp, namelen)
618 name = util.readexactly(fp, namelen)
623
619
624 if repo.ui.debugflag:
620 if repo.ui.debugflag:
625 repo.ui.debug('adding [%s] %s (%s)\n' %
621 repo.ui.debug('adding [%s] %s (%s)\n' %
626 (src, name, util.bytecount(datalen)))
622 (src, name, util.bytecount(datalen)))
627
623
628 with vfs(name, 'w') as ofp:
624 with vfs(name, 'w') as ofp:
629 for chunk in util.filechunkiter(fp, limit=datalen):
625 for chunk in util.filechunkiter(fp, limit=datalen):
630 progress.increment(step=len(chunk))
626 progress.increment(step=len(chunk))
631 ofp.write(chunk)
627 ofp.write(chunk)
632
628
633 # force @filecache properties to be reloaded from
629 # force @filecache properties to be reloaded from
634 # streamclone-ed file at next access
630 # streamclone-ed file at next access
635 repo.invalidate(clearfilecache=True)
631 repo.invalidate(clearfilecache=True)
636
632
637 elapsed = util.timer() - start
633 elapsed = util.timer() - start
638 if elapsed <= 0:
634 if elapsed <= 0:
639 elapsed = 0.001
635 elapsed = 0.001
640 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
636 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
641 (util.bytecount(progress.pos), elapsed,
637 (util.bytecount(progress.pos), elapsed,
642 util.bytecount(progress.pos / elapsed)))
638 util.bytecount(progress.pos / elapsed)))
643 progress.complete()
639 progress.complete()
644
640
645 def applybundlev2(repo, fp, filecount, filesize, requirements):
641 def applybundlev2(repo, fp, filecount, filesize, requirements):
646 from . import localrepo
642 from . import localrepo
647
643
648 missingreqs = [r for r in requirements if r not in repo.supported]
644 missingreqs = [r for r in requirements if r not in repo.supported]
649 if missingreqs:
645 if missingreqs:
650 raise error.Abort(_('unable to apply stream clone: '
646 raise error.Abort(_('unable to apply stream clone: '
651 'unsupported format: %s') %
647 'unsupported format: %s') %
652 ', '.join(sorted(missingreqs)))
648 ', '.join(sorted(missingreqs)))
653
649
654 consumev2(repo, fp, filecount, filesize)
650 consumev2(repo, fp, filecount, filesize)
655
651
656 # new requirements = old non-format requirements +
652 # new requirements = old non-format requirements +
657 # new format-related remote requirements
653 # new format-related remote requirements
658 # requirements from the streamed-in repository
654 # requirements from the streamed-in repository
659 repo.requirements = set(requirements) | (
655 repo.requirements = set(requirements) | (
660 repo.requirements - repo.supportedformats)
656 repo.requirements - repo.supportedformats)
661 repo.svfs.options = localrepo.resolvestorevfsoptions(
657 repo.svfs.options = localrepo.resolvestorevfsoptions(
662 repo.ui, repo.requirements, repo.features)
658 repo.ui, repo.requirements, repo.features)
663 repo._writerequirements()
659 repo._writerequirements()
@@ -1,39 +1,86
1 #testcases tree flat
2
1 Tests narrow stream clones
3 Tests narrow stream clones
2
4
3 $ . "$TESTDIR/narrow-library.sh"
5 $ . "$TESTDIR/narrow-library.sh"
4
6
7 #if tree
8 $ cat << EOF >> $HGRCPATH
9 > [experimental]
10 > treemanifest = 1
11 > EOF
12 #endif
13
5 Server setup
14 Server setup
6
15
7 $ hg init master
16 $ hg init master
8 $ cd master
17 $ cd master
9 $ mkdir dir
18 $ mkdir dir
10 $ mkdir dir/src
19 $ mkdir dir/src
11 $ cd dir/src
20 $ cd dir/src
12 $ for x in `$TESTDIR/seq.py 20`; do echo $x > "f$x"; hg add "f$x"; hg commit -m "Commit src $x"; done
21 $ for x in `$TESTDIR/seq.py 20`; do echo $x > "f$x"; hg add "f$x"; hg commit -m "Commit src $x"; done
13
22
14 $ cd ..
23 $ cd ..
15 $ mkdir tests
24 $ mkdir tests
16 $ cd tests
25 $ cd tests
17 $ for x in `$TESTDIR/seq.py 20`; do echo $x > "f$x"; hg add "f$x"; hg commit -m "Commit src $x"; done
26 $ for x in `$TESTDIR/seq.py 20`; do echo $x > "f$x"; hg add "f$x"; hg commit -m "Commit src $x"; done
18 $ cd ../../..
27 $ cd ../../..
19
28
20 Trying to stream clone when the server does not support it
29 Trying to stream clone when the server does not support it
21
30
22 $ hg clone --narrow ssh://user@dummy/master narrow --noupdate --include "dir/src/f10" --stream
31 $ hg clone --narrow ssh://user@dummy/master narrow --noupdate --include "dir/src/f10" --stream
23 streaming all changes
32 streaming all changes
24 remote: abort: server does not support narrow stream clones
33 remote: abort: server does not support narrow stream clones
25 abort: pull failed on remote
34 abort: pull failed on remote
26 [255]
35 [255]
27
36
28 Enable stream clone on the server
37 Enable stream clone on the server
29
38
30 $ echo "[server]" >> master/.hg/hgrc
39 $ echo "[experimental.server]" >> master/.hg/hgrc
31 $ echo "stream-narrow-clones=True" >> master/.hg/hgrc
40 $ echo "stream-narrow-clones=True" >> master/.hg/hgrc
32
41
33 Cloning a specific file when stream clone is supported
42 Cloning a specific file when stream clone is supported
34
43
35 $ hg clone --narrow ssh://user@dummy/master narrow --noupdate --include "dir/src/f10" --stream
44 $ hg clone --narrow ssh://user@dummy/master narrow --noupdate --include "dir/src/f10" --stream
36 streaming all changes
45 streaming all changes
37 remote: abort: server does not support narrow stream clones
46 * files to transfer, * KB of data (glob)
38 abort: pull failed on remote
47 transferred * KB in * seconds (* */sec) (glob)
39 [255]
48
49 $ cd narrow
50 $ ls
51 $ hg tracked
52 I path:dir/src/f10
53
54 Making sure we have the correct set of requirements
55
56 $ cat .hg/requires
57 dotencode
58 fncache
59 generaldelta
60 narrowhg-experimental
61 revlogv1
62 store
63 treemanifest (tree !)
64
65 Making sure store has the required files
66
67 $ ls .hg/store/
68 00changelog.i
69 00manifest.i
70 data
71 fncache
72 meta (tree !)
73 narrowspec
74 undo
75 undo.backupfiles
76 undo.phaseroots
77
78 Checking that repository has all the required data and not broken
79
80 $ hg verify
81 checking changesets
82 checking manifests
83 checking directory manifests (tree !)
84 crosschecking files in changesets and manifests
85 checking files
86 checked 40 changesets with 1 changes to 1 files
General Comments 0
You need to be logged in to leave comments. Login now