Show More
@@ -16,7 +16,7 b' import merge as merge_' | |||
|
16 | 16 | |
|
17 | 17 | class localrepository(repo.repository): |
|
18 | 18 | capabilities = util.set(('lookup', 'changegroupsubset')) |
|
19 | supported = ('revlogv1', 'store') | |
|
19 | supported = ('revlogv1', 'store', 'fncache') | |
|
20 | 20 | |
|
21 | 21 | def __init__(self, parentui, path=None, create=0): |
|
22 | 22 | repo.repository.__init__(self) |
@@ -35,6 +35,7 b' class localrepository(repo.repository):' | |||
|
35 | 35 | if parentui.configbool('format', 'usestore', True): |
|
36 | 36 | os.mkdir(os.path.join(self.path, "store")) |
|
37 | 37 | requirements.append("store") |
|
38 | requirements.append("fncache") | |
|
38 | 39 | # create an invalid changelog |
|
39 | 40 | self.opener("00changelog.i", "a").write( |
|
40 | 41 | '\0\0\0\2' # represents revlogv2 |
@@ -5,8 +5,11 b'' | |||
|
5 | 5 | # This software may be used and distributed according to the terms |
|
6 | 6 | # of the GNU General Public License, incorporated herein by reference. |
|
7 | 7 | |
|
8 | from i18n import _ | |
|
8 | 9 | import os, stat, osutil, util |
|
9 | 10 | |
|
11 | _sha = util.sha1 | |
|
12 | ||
|
10 | 13 | def _buildencodefun(): |
|
11 | 14 | e = '_' |
|
12 | 15 | win_reserved = [ord(x) for x in '\\:*?"<>|'] |
@@ -35,6 +38,93 b' def _buildencodefun():' | |||
|
35 | 38 | |
|
36 | 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 | 128 | def _calcmode(path): |
|
39 | 129 | try: |
|
40 | 130 | # files in .hg/ will be created using this mode |
@@ -120,8 +210,83 b' class encodedstore(basicstore):' | |||
|
120 | 210 | return (['requires', '00changelog.i'] + |
|
121 | 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 | 286 | def store(requirements, path, opener, pathjoiner=None): |
|
124 | 287 | pathjoiner = pathjoiner or os.path.join |
|
125 | 288 | if 'store' in requirements: |
|
289 | if 'fncache' in requirements: | |
|
290 | return fncachestore(path, opener, pathjoiner) | |
|
126 | 291 | return encodedstore(path, opener, pathjoiner) |
|
127 | 292 | return basicstore(path, opener, pathjoiner) |
@@ -2,6 +2,7 b'' | |||
|
2 | 2 | |
|
3 | 3 | CONTRIBDIR=$TESTDIR/../contrib |
|
4 | 4 | |
|
5 | echo % prepare repo-a | |
|
5 | 6 | mkdir repo-a |
|
6 | 7 | cd repo-a |
|
7 | 8 | hg init |
@@ -18,11 +19,13 b" hg commit -m third -d '0 0'" | |||
|
18 | 19 | |
|
19 | 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 | 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 | 29 | find .hg/store -name "*.i" | sort | xargs python $CONTRIBDIR/dumprevlog > ../repo.dump |
|
27 | 30 | |
|
28 | 31 | cd .. |
@@ -31,17 +34,28 b' mkdir repo-b' | |||
|
31 | 34 | cd repo-b |
|
32 | 35 | hg init |
|
33 | 36 | |
|
34 | echo undumping: | |
|
37 | echo | |
|
38 | echo % undumping into repo-b | |
|
35 | 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 | 52 | hg verify |
|
39 | 53 | |
|
40 | 54 | cd .. |
|
41 | 55 | |
|
42 | echo comparing repos: | |
|
43 | hg -R repo-b incoming repo-a | |
|
44 |
hg -R repo- |
|
|
45 | echo comparing done | |
|
56 | echo | |
|
57 | echo % comparing repos | |
|
58 | hg -R repo-c incoming repo-a | |
|
59 | hg -R repo-a incoming repo-c | |
|
46 | 60 | |
|
47 | 61 | exit 0 |
@@ -1,9 +1,11 b'' | |||
|
1 | % prepare repo-a | |
|
1 | 2 | checking changesets |
|
2 | 3 | checking manifests |
|
3 | 4 | crosschecking files in changesets and manifests |
|
4 | 5 | checking files |
|
5 | 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 | 9 | file: .hg/store/data/a.i |
|
8 | 10 | node: 183d2312b35066fb6b3b449b84efc370d50993d0 |
|
9 | 11 | linkrev: 0 |
@@ -32,22 +34,34 b' adding to file a' | |||
|
32 | 34 | adding more to file a |
|
33 | 35 | |
|
34 | 36 | -end- |
|
35 | dumprevlog done | |
|
36 | undumping: | |
|
37 | % dumprevlog done | |
|
38 | ||
|
39 | % dump all revlogs to file repo.dump | |
|
40 | ||
|
41 | % undumping into repo-b | |
|
37 | 42 | .hg/store/00changelog.i |
|
38 | 43 | .hg/store/00manifest.i |
|
39 | 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 | 55 | checking changesets |
|
42 | 56 | checking manifests |
|
43 | 57 | crosschecking files in changesets and manifests |
|
44 | 58 | checking files |
|
45 | 59 | 1 files, 3 changesets, 3 total revisions |
|
46 | comparing repos: | |
|
60 | ||
|
61 | % comparing repos | |
|
47 | 62 | comparing with repo-a |
|
48 | 63 | searching for changes |
|
49 | 64 | no changes found |
|
50 |
comparing with repo- |
|
|
65 | comparing with repo-c | |
|
51 | 66 | searching for changes |
|
52 | 67 | no changes found |
|
53 | comparing done |
@@ -22,6 +22,7 b' 00770 ./.hg/store/data/' | |||
|
22 | 22 | 00770 ./.hg/store/data/dir/ |
|
23 | 23 | 00660 ./.hg/store/data/dir/bar.i |
|
24 | 24 | 00660 ./.hg/store/data/foo.i |
|
25 | 00660 ./.hg/store/fncache | |
|
25 | 26 | 00660 ./.hg/store/undo |
|
26 | 27 | 00660 ./.hg/undo.branch |
|
27 | 28 | 00660 ./.hg/undo.dirstate |
@@ -49,6 +50,7 b' 00770 ../push/.hg/store/data/' | |||
|
49 | 50 | 00770 ../push/.hg/store/data/dir/ |
|
50 | 51 | 00660 ../push/.hg/store/data/dir/bar.i |
|
51 | 52 | 00660 ../push/.hg/store/data/foo.i |
|
53 | 00660 ../push/.hg/store/fncache | |
|
52 | 54 | 00660 ../push/.hg/store/undo |
|
53 | 55 | 00660 ../push/.hg/undo.branch |
|
54 | 56 | 00660 ../push/.hg/undo.dirstate |
@@ -3,6 +3,7 b' store created' | |||
|
3 | 3 | 00changelog.i created |
|
4 | 4 | revlogv1 |
|
5 | 5 | store |
|
6 | fncache | |
|
6 | 7 | adding foo |
|
7 | 8 | # creating repo with old format |
|
8 | 9 | revlogv1 |
@@ -17,7 +17,6 b' checking changesets' | |||
|
17 | 17 | checking manifests |
|
18 | 18 | crosschecking files in changesets and manifests |
|
19 | 19 | checking files |
|
20 | ?: cannot decode filename 'data/X_f_o_o.txt.i' | |
|
21 | 20 | data/FOO.txt.i@0: missing revlog! |
|
22 | 21 | 0: empty or missing FOO.txt |
|
23 | 22 | FOO.txt@0: f62022d3d590 in manifests not found |
@@ -27,8 +26,6 b' checking files' | |||
|
27 | 26 | data/bar.txt.i@0: missing revlog! |
|
28 | 27 | 0: empty or missing bar.txt |
|
29 | 28 | bar.txt@0: 256559129457 in manifests not found |
|
30 | warning: orphan revlog 'data/xbar.txt.i' | |
|
31 | 29 | 3 files, 1 changesets, 0 total revisions |
|
32 |
|
|
|
33 | 10 integrity errors encountered! | |
|
30 | 9 integrity errors encountered! | |
|
34 | 31 | (first damaged changeset appears to be 0) |
General Comments 0
You need to be logged in to leave comments.
Login now