Show More
@@ -16,7 +16,7 import merge as merge_ | |||||
16 |
|
16 | |||
17 | class localrepository(repo.repository): |
|
17 | class localrepository(repo.repository): | |
18 | capabilities = util.set(('lookup', 'changegroupsubset')) |
|
18 | capabilities = util.set(('lookup', 'changegroupsubset')) | |
19 | supported = ('revlogv1', 'store') |
|
19 | supported = ('revlogv1', 'store', 'fncache') | |
20 |
|
20 | |||
21 | def __init__(self, parentui, path=None, create=0): |
|
21 | def __init__(self, parentui, path=None, create=0): | |
22 | repo.repository.__init__(self) |
|
22 | repo.repository.__init__(self) | |
@@ -35,6 +35,7 class localrepository(repo.repository): | |||||
35 | if parentui.configbool('format', 'usestore', True): |
|
35 | if parentui.configbool('format', 'usestore', True): | |
36 | os.mkdir(os.path.join(self.path, "store")) |
|
36 | os.mkdir(os.path.join(self.path, "store")) | |
37 | requirements.append("store") |
|
37 | requirements.append("store") | |
|
38 | requirements.append("fncache") | |||
38 | # create an invalid changelog |
|
39 | # create an invalid changelog | |
39 | self.opener("00changelog.i", "a").write( |
|
40 | self.opener("00changelog.i", "a").write( | |
40 | '\0\0\0\2' # represents revlogv2 |
|
41 | '\0\0\0\2' # represents revlogv2 |
@@ -5,8 +5,11 | |||||
5 | # This software may be used and distributed according to the terms |
|
5 | # This software may be used and distributed according to the terms | |
6 | # of the GNU General Public License, incorporated herein by reference. |
|
6 | # of the GNU General Public License, incorporated herein by reference. | |
7 |
|
7 | |||
|
8 | from i18n import _ | |||
8 | import os, stat, osutil, util |
|
9 | import os, stat, osutil, util | |
9 |
|
10 | |||
|
11 | _sha = util.sha1 | |||
|
12 | ||||
10 | def _buildencodefun(): |
|
13 | def _buildencodefun(): | |
11 | e = '_' |
|
14 | e = '_' | |
12 | win_reserved = [ord(x) for x in '\\:*?"<>|'] |
|
15 | win_reserved = [ord(x) for x in '\\:*?"<>|'] | |
@@ -35,6 +38,93 def _buildencodefun(): | |||||
35 |
|
38 | |||
36 | encodefilename, decodefilename = _buildencodefun() |
|
39 | encodefilename, decodefilename = _buildencodefun() | |
37 |
|
40 | |||
|
41 | def _build_lower_encodefun(): | |||
|
42 | win_reserved = [ord(x) for x in '\\:*?"<>|'] | |||
|
43 | cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ]) | |||
|
44 | for x in (range(32) + range(126, 256) + win_reserved): | |||
|
45 | cmap[chr(x)] = "~%02x" % x | |||
|
46 | for x in range(ord("A"), ord("Z")+1): | |||
|
47 | cmap[chr(x)] = chr(x).lower() | |||
|
48 | return lambda s: "".join([cmap[c] for c in s]) | |||
|
49 | ||||
|
50 | lowerencode = _build_lower_encodefun() | |||
|
51 | ||||
|
52 | _windows_reserved_filenames = '''con prn aux nul | |||
|
53 | com1 com2 com3 com4 com5 com6 com7 com8 com9 | |||
|
54 | lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split() | |||
|
55 | def auxencode(path): | |||
|
56 | res = [] | |||
|
57 | for n in path.split('/'): | |||
|
58 | if n: | |||
|
59 | base = n.split('.')[0] | |||
|
60 | if base and (base in _windows_reserved_filenames): | |||
|
61 | # encode third letter ('aux' -> 'au~78') | |||
|
62 | ec = "~%02x" % ord(n[2]) | |||
|
63 | n = n[0:2] + ec + n[3:] | |||
|
64 | res.append(n) | |||
|
65 | return '/'.join(res) | |||
|
66 | ||||
|
67 | MAX_PATH_LEN_IN_HGSTORE = 120 | |||
|
68 | DIR_PREFIX_LEN = 8 | |||
|
69 | _MAX_SHORTENED_DIRS_LEN = 8 * (DIR_PREFIX_LEN + 1) - 4 | |||
|
70 | def hybridencode(path): | |||
|
71 | '''encodes path with a length limit | |||
|
72 | ||||
|
73 | Encodes all paths that begin with 'data/', according to the following. | |||
|
74 | ||||
|
75 | Default encoding (reversible): | |||
|
76 | ||||
|
77 | Encodes all uppercase letters 'X' as '_x'. All reserved or illegal | |||
|
78 | characters are encoded as '~xx', where xx is the two digit hex code | |||
|
79 | of the character (see encodefilename). | |||
|
80 | Relevant path components consisting of Windows reserved filenames are | |||
|
81 | masked by encoding the third character ('aux' -> 'au~78', see auxencode). | |||
|
82 | ||||
|
83 | Hashed encoding (not reversible): | |||
|
84 | ||||
|
85 | If the default-encoded path is longer than MAX_PATH_LEN_IN_HGSTORE, a | |||
|
86 | non-reversible hybrid hashing of the path is done instead. | |||
|
87 | This encoding uses up to DIR_PREFIX_LEN characters of all directory | |||
|
88 | levels of the lowerencoded path, but not more levels than can fit into | |||
|
89 | _MAX_SHORTENED_DIRS_LEN. | |||
|
90 | Then follows the filler followed by the sha digest of the full path. | |||
|
91 | The filler is the beginning of the basename of the lowerencoded path | |||
|
92 | (the basename is everything after the last path separator). The filler | |||
|
93 | is as long as possible, filling in characters from the basename until | |||
|
94 | the encoded path has MAX_PATH_LEN_IN_HGSTORE characters (or all chars | |||
|
95 | of the basename have been taken). | |||
|
96 | The extension (e.g. '.i' or '.d') is preserved. | |||
|
97 | ||||
|
98 | The string 'data/' at the beginning is replaced with 'dh/', if the hashed | |||
|
99 | encoding was used. | |||
|
100 | ''' | |||
|
101 | if not path.startswith('data/'): | |||
|
102 | return path | |||
|
103 | ndpath = path[len('data/'):] | |||
|
104 | res = 'data/' + auxencode(encodefilename(ndpath)) | |||
|
105 | if len(res) > MAX_PATH_LEN_IN_HGSTORE: | |||
|
106 | digest = _sha(path).hexdigest() | |||
|
107 | aep = auxencode(lowerencode(ndpath)) | |||
|
108 | _root, ext = os.path.splitext(aep) | |||
|
109 | parts = aep.split('/') | |||
|
110 | basename = parts[-1] | |||
|
111 | sdirs = [] | |||
|
112 | for p in parts[:-1]: | |||
|
113 | d = p[:DIR_PREFIX_LEN] | |||
|
114 | t = '/'.join(sdirs) + '/' + d | |||
|
115 | if len(t) > _MAX_SHORTENED_DIRS_LEN: | |||
|
116 | break | |||
|
117 | sdirs.append(d) | |||
|
118 | dirs = '/'.join(sdirs) | |||
|
119 | if len(dirs) > 0: | |||
|
120 | dirs += '/' | |||
|
121 | res = 'dh/' + dirs + digest + ext | |||
|
122 | space_left = MAX_PATH_LEN_IN_HGSTORE - len(res) | |||
|
123 | if space_left > 0: | |||
|
124 | filler = basename[:space_left] | |||
|
125 | res = 'dh/' + dirs + filler + digest + ext | |||
|
126 | return res | |||
|
127 | ||||
38 | def _calcmode(path): |
|
128 | def _calcmode(path): | |
39 | try: |
|
129 | try: | |
40 | # files in .hg/ will be created using this mode |
|
130 | # files in .hg/ will be created using this mode | |
@@ -120,8 +210,83 class encodedstore(basicstore): | |||||
120 | return (['requires', '00changelog.i'] + |
|
210 | return (['requires', '00changelog.i'] + | |
121 | [self.pathjoiner('store', f) for f in _data.split()]) |
|
211 | [self.pathjoiner('store', f) for f in _data.split()]) | |
122 |
|
212 | |||
|
213 | def fncache(opener): | |||
|
214 | '''yields the entries in the fncache file''' | |||
|
215 | try: | |||
|
216 | fp = opener('fncache', mode='rb') | |||
|
217 | except IOError: | |||
|
218 | # skip nonexistent file | |||
|
219 | return | |||
|
220 | for n, line in enumerate(fp): | |||
|
221 | if (len(line) < 2) or (line[-1] != '\n'): | |||
|
222 | t = _('invalid entry in fncache, line %s') % (n + 1) | |||
|
223 | raise util.Abort(t) | |||
|
224 | yield line[:-1] | |||
|
225 | fp.close() | |||
|
226 | ||||
|
227 | class fncacheopener(object): | |||
|
228 | def __init__(self, opener): | |||
|
229 | self.opener = opener | |||
|
230 | self.entries = None | |||
|
231 | ||||
|
232 | def loadfncache(self): | |||
|
233 | self.entries = {} | |||
|
234 | for f in fncache(self.opener): | |||
|
235 | self.entries[f] = True | |||
|
236 | ||||
|
237 | def __call__(self, path, mode='r', *args, **kw): | |||
|
238 | if mode not in ('r', 'rb') and path.startswith('data/'): | |||
|
239 | if self.entries is None: | |||
|
240 | self.loadfncache() | |||
|
241 | if path not in self.entries: | |||
|
242 | self.opener('fncache', 'ab').write(path + '\n') | |||
|
243 | # fncache may contain non-existent files after rollback / strip | |||
|
244 | self.entries[path] = True | |||
|
245 | return self.opener(hybridencode(path), mode, *args, **kw) | |||
|
246 | ||||
|
247 | class fncachestore(basicstore): | |||
|
248 | def __init__(self, path, opener, pathjoiner): | |||
|
249 | self.pathjoiner = pathjoiner | |||
|
250 | self.path = self.pathjoiner(path, 'store') | |||
|
251 | self.createmode = _calcmode(self.path) | |||
|
252 | self._op = opener(self.path) | |||
|
253 | self._op.createmode = self.createmode | |||
|
254 | self.opener = fncacheopener(self._op) | |||
|
255 | ||||
|
256 | def join(self, f): | |||
|
257 | return self.pathjoiner(self.path, hybridencode(f)) | |||
|
258 | ||||
|
259 | def datafiles(self): | |||
|
260 | rewrite = False | |||
|
261 | existing = [] | |||
|
262 | pjoin = self.pathjoiner | |||
|
263 | spath = self.path | |||
|
264 | for f in fncache(self._op): | |||
|
265 | ef = hybridencode(f) | |||
|
266 | try: | |||
|
267 | st = os.stat(pjoin(spath, ef)) | |||
|
268 | yield f, ef, st.st_size | |||
|
269 | existing.append(f) | |||
|
270 | except OSError: | |||
|
271 | # nonexistent entry | |||
|
272 | rewrite = True | |||
|
273 | if rewrite: | |||
|
274 | # rewrite fncache to remove nonexistent entries | |||
|
275 | # (may be caused by rollback / strip) | |||
|
276 | fp = self._op('fncache', mode='wb') | |||
|
277 | for p in existing: | |||
|
278 | fp.write(p + '\n') | |||
|
279 | fp.close() | |||
|
280 | ||||
|
281 | def copylist(self): | |||
|
282 | d = _data + ' dh fncache' | |||
|
283 | return (['requires', '00changelog.i'] + | |||
|
284 | [self.pathjoiner('store', f) for f in d.split()]) | |||
|
285 | ||||
123 | def store(requirements, path, opener, pathjoiner=None): |
|
286 | def store(requirements, path, opener, pathjoiner=None): | |
124 | pathjoiner = pathjoiner or os.path.join |
|
287 | pathjoiner = pathjoiner or os.path.join | |
125 | if 'store' in requirements: |
|
288 | if 'store' in requirements: | |
|
289 | if 'fncache' in requirements: | |||
|
290 | return fncachestore(path, opener, pathjoiner) | |||
126 | return encodedstore(path, opener, pathjoiner) |
|
291 | return encodedstore(path, opener, pathjoiner) | |
127 | return basicstore(path, opener, pathjoiner) |
|
292 | return basicstore(path, opener, pathjoiner) |
@@ -2,6 +2,7 | |||||
2 |
|
2 | |||
3 | CONTRIBDIR=$TESTDIR/../contrib |
|
3 | CONTRIBDIR=$TESTDIR/../contrib | |
4 |
|
4 | |||
|
5 | echo % prepare repo-a | |||
5 | mkdir repo-a |
|
6 | mkdir repo-a | |
6 | cd repo-a |
|
7 | cd repo-a | |
7 | hg init |
|
8 | hg init | |
@@ -18,11 +19,13 hg commit -m third -d '0 0' | |||||
18 |
|
19 | |||
19 | hg verify |
|
20 | hg verify | |
20 |
|
21 | |||
21 | echo dumping revlog of file a to stdout: |
|
22 | echo | |
|
23 | echo % dumping revlog of file a to stdout | |||
22 | python $CONTRIBDIR/dumprevlog .hg/store/data/a.i |
|
24 | python $CONTRIBDIR/dumprevlog .hg/store/data/a.i | |
23 | echo dumprevlog done |
|
25 | echo % dumprevlog done | |
24 |
|
26 | |||
25 | # dump all revlogs to file repo.dump |
|
27 | echo | |
|
28 | echo % dump all revlogs to file repo.dump | |||
26 | find .hg/store -name "*.i" | sort | xargs python $CONTRIBDIR/dumprevlog > ../repo.dump |
|
29 | find .hg/store -name "*.i" | sort | xargs python $CONTRIBDIR/dumprevlog > ../repo.dump | |
27 |
|
30 | |||
28 | cd .. |
|
31 | cd .. | |
@@ -31,17 +34,28 mkdir repo-b | |||||
31 | cd repo-b |
|
34 | cd repo-b | |
32 | hg init |
|
35 | hg init | |
33 |
|
36 | |||
34 | echo undumping: |
|
37 | echo | |
|
38 | echo % undumping into repo-b | |||
35 | python $CONTRIBDIR/undumprevlog < ../repo.dump |
|
39 | python $CONTRIBDIR/undumprevlog < ../repo.dump | |
36 | echo undumping done |
|
40 | echo % undumping done | |
|
41 | ||||
|
42 | cd .. | |||
37 |
|
43 | |||
|
44 | echo | |||
|
45 | echo % clone --pull repo-b repo-c to rebuild fncache | |||
|
46 | hg clone --pull -U repo-b repo-c | |||
|
47 | ||||
|
48 | cd repo-c | |||
|
49 | ||||
|
50 | echo | |||
|
51 | echo % verify repo-c | |||
38 | hg verify |
|
52 | hg verify | |
39 |
|
53 | |||
40 | cd .. |
|
54 | cd .. | |
41 |
|
55 | |||
42 | echo comparing repos: |
|
56 | echo | |
43 | hg -R repo-b incoming repo-a |
|
57 | echo % comparing repos | |
44 |
hg -R repo- |
|
58 | hg -R repo-c incoming repo-a | |
45 | echo comparing done |
|
59 | hg -R repo-a incoming repo-c | |
46 |
|
60 | |||
47 | exit 0 |
|
61 | exit 0 |
@@ -1,9 +1,11 | |||||
|
1 | % prepare repo-a | |||
1 | checking changesets |
|
2 | checking changesets | |
2 | checking manifests |
|
3 | checking manifests | |
3 | crosschecking files in changesets and manifests |
|
4 | crosschecking files in changesets and manifests | |
4 | checking files |
|
5 | checking files | |
5 | 1 files, 3 changesets, 3 total revisions |
|
6 | 1 files, 3 changesets, 3 total revisions | |
6 | dumping revlog of file a to stdout: |
|
7 | ||
|
8 | % dumping revlog of file a to stdout | |||
7 | file: .hg/store/data/a.i |
|
9 | file: .hg/store/data/a.i | |
8 | node: 183d2312b35066fb6b3b449b84efc370d50993d0 |
|
10 | node: 183d2312b35066fb6b3b449b84efc370d50993d0 | |
9 | linkrev: 0 |
|
11 | linkrev: 0 | |
@@ -32,22 +34,34 adding to file a | |||||
32 | adding more to file a |
|
34 | adding more to file a | |
33 |
|
35 | |||
34 | -end- |
|
36 | -end- | |
35 | dumprevlog done |
|
37 | % dumprevlog done | |
36 | undumping: |
|
38 | ||
|
39 | % dump all revlogs to file repo.dump | |||
|
40 | ||||
|
41 | % undumping into repo-b | |||
37 | .hg/store/00changelog.i |
|
42 | .hg/store/00changelog.i | |
38 | .hg/store/00manifest.i |
|
43 | .hg/store/00manifest.i | |
39 | .hg/store/data/a.i |
|
44 | .hg/store/data/a.i | |
40 | undumping done |
|
45 | % undumping done | |
|
46 | ||||
|
47 | % clone --pull repo-b repo-c to rebuild fncache | |||
|
48 | requesting all changes | |||
|
49 | adding changesets | |||
|
50 | adding manifests | |||
|
51 | adding file changes | |||
|
52 | added 3 changesets with 3 changes to 1 files | |||
|
53 | ||||
|
54 | % verify repo-c | |||
41 | checking changesets |
|
55 | checking changesets | |
42 | checking manifests |
|
56 | checking manifests | |
43 | crosschecking files in changesets and manifests |
|
57 | crosschecking files in changesets and manifests | |
44 | checking files |
|
58 | checking files | |
45 | 1 files, 3 changesets, 3 total revisions |
|
59 | 1 files, 3 changesets, 3 total revisions | |
46 | comparing repos: |
|
60 | ||
|
61 | % comparing repos | |||
47 | comparing with repo-a |
|
62 | comparing with repo-a | |
48 | searching for changes |
|
63 | searching for changes | |
49 | no changes found |
|
64 | no changes found | |
50 |
comparing with repo- |
|
65 | comparing with repo-c | |
51 | searching for changes |
|
66 | searching for changes | |
52 | no changes found |
|
67 | no changes found | |
53 | comparing done |
|
@@ -22,6 +22,7 00770 ./.hg/store/data/ | |||||
22 | 00770 ./.hg/store/data/dir/ |
|
22 | 00770 ./.hg/store/data/dir/ | |
23 | 00660 ./.hg/store/data/dir/bar.i |
|
23 | 00660 ./.hg/store/data/dir/bar.i | |
24 | 00660 ./.hg/store/data/foo.i |
|
24 | 00660 ./.hg/store/data/foo.i | |
|
25 | 00660 ./.hg/store/fncache | |||
25 | 00660 ./.hg/store/undo |
|
26 | 00660 ./.hg/store/undo | |
26 | 00660 ./.hg/undo.branch |
|
27 | 00660 ./.hg/undo.branch | |
27 | 00660 ./.hg/undo.dirstate |
|
28 | 00660 ./.hg/undo.dirstate | |
@@ -49,6 +50,7 00770 ../push/.hg/store/data/ | |||||
49 | 00770 ../push/.hg/store/data/dir/ |
|
50 | 00770 ../push/.hg/store/data/dir/ | |
50 | 00660 ../push/.hg/store/data/dir/bar.i |
|
51 | 00660 ../push/.hg/store/data/dir/bar.i | |
51 | 00660 ../push/.hg/store/data/foo.i |
|
52 | 00660 ../push/.hg/store/data/foo.i | |
|
53 | 00660 ../push/.hg/store/fncache | |||
52 | 00660 ../push/.hg/store/undo |
|
54 | 00660 ../push/.hg/store/undo | |
53 | 00660 ../push/.hg/undo.branch |
|
55 | 00660 ../push/.hg/undo.branch | |
54 | 00660 ../push/.hg/undo.dirstate |
|
56 | 00660 ../push/.hg/undo.dirstate |
@@ -3,6 +3,7 store created | |||||
3 | 00changelog.i created |
|
3 | 00changelog.i created | |
4 | revlogv1 |
|
4 | revlogv1 | |
5 | store |
|
5 | store | |
|
6 | fncache | |||
6 | adding foo |
|
7 | adding foo | |
7 | # creating repo with old format |
|
8 | # creating repo with old format | |
8 | revlogv1 |
|
9 | revlogv1 |
@@ -17,7 +17,6 checking changesets | |||||
17 | checking manifests |
|
17 | checking manifests | |
18 | crosschecking files in changesets and manifests |
|
18 | crosschecking files in changesets and manifests | |
19 | checking files |
|
19 | checking files | |
20 | ?: cannot decode filename 'data/X_f_o_o.txt.i' |
|
|||
21 | data/FOO.txt.i@0: missing revlog! |
|
20 | data/FOO.txt.i@0: missing revlog! | |
22 | 0: empty or missing FOO.txt |
|
21 | 0: empty or missing FOO.txt | |
23 | FOO.txt@0: f62022d3d590 in manifests not found |
|
22 | FOO.txt@0: f62022d3d590 in manifests not found | |
@@ -27,8 +26,6 checking files | |||||
27 | data/bar.txt.i@0: missing revlog! |
|
26 | data/bar.txt.i@0: missing revlog! | |
28 | 0: empty or missing bar.txt |
|
27 | 0: empty or missing bar.txt | |
29 | bar.txt@0: 256559129457 in manifests not found |
|
28 | bar.txt@0: 256559129457 in manifests not found | |
30 | warning: orphan revlog 'data/xbar.txt.i' |
|
|||
31 | 3 files, 1 changesets, 0 total revisions |
|
29 | 3 files, 1 changesets, 0 total revisions | |
32 |
|
|
30 | 9 integrity errors encountered! | |
33 | 10 integrity errors encountered! |
|
|||
34 | (first damaged changeset appears to be 0) |
|
31 | (first damaged changeset appears to be 0) |
General Comments 0
You need to be logged in to leave comments.
Login now