##// END OF EJS Templates
sqlitestore: add an `ancestors` method...
marmoute -
r50684:df750b81 stable
parent child Browse files
Show More
@@ -1,1324 +1,1343 b''
1 # sqlitestore.py - Storage backend that uses SQLite
1 # sqlitestore.py - Storage backend that uses SQLite
2 #
2 #
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """store repository data in SQLite (EXPERIMENTAL)
8 """store repository data in SQLite (EXPERIMENTAL)
9
9
10 The sqlitestore extension enables the storage of repository data in SQLite.
10 The sqlitestore extension enables the storage of repository data in SQLite.
11
11
12 This extension is HIGHLY EXPERIMENTAL. There are NO BACKWARDS COMPATIBILITY
12 This extension is HIGHLY EXPERIMENTAL. There are NO BACKWARDS COMPATIBILITY
13 GUARANTEES. This means that repositories created with this extension may
13 GUARANTEES. This means that repositories created with this extension may
14 only be usable with the exact version of this extension/Mercurial that was
14 only be usable with the exact version of this extension/Mercurial that was
15 used. The extension attempts to enforce this in order to prevent repository
15 used. The extension attempts to enforce this in order to prevent repository
16 corruption.
16 corruption.
17
17
18 In addition, several features are not yet supported or have known bugs:
18 In addition, several features are not yet supported or have known bugs:
19
19
20 * Only some data is stored in SQLite. Changeset, manifest, and other repository
20 * Only some data is stored in SQLite. Changeset, manifest, and other repository
21 data is not yet stored in SQLite.
21 data is not yet stored in SQLite.
22 * Transactions are not robust. If the process is aborted at the right time
22 * Transactions are not robust. If the process is aborted at the right time
23 during transaction close/rollback, the repository could be in an inconsistent
23 during transaction close/rollback, the repository could be in an inconsistent
24 state. This problem will diminish once all repository data is tracked by
24 state. This problem will diminish once all repository data is tracked by
25 SQLite.
25 SQLite.
26 * Bundle repositories do not work (the ability to use e.g.
26 * Bundle repositories do not work (the ability to use e.g.
27 `hg -R <bundle-file> log` to automatically overlay a bundle on top of the
27 `hg -R <bundle-file> log` to automatically overlay a bundle on top of the
28 existing repository).
28 existing repository).
29 * Various other features don't work.
29 * Various other features don't work.
30
30
31 This extension should work for basic clone/pull, update, and commit workflows.
31 This extension should work for basic clone/pull, update, and commit workflows.
32 Some history rewriting operations may fail due to lack of support for bundle
32 Some history rewriting operations may fail due to lack of support for bundle
33 repositories.
33 repositories.
34
34
35 To use, activate the extension and set the ``storage.new-repo-backend`` config
35 To use, activate the extension and set the ``storage.new-repo-backend`` config
36 option to ``sqlite`` to enable new repositories to use SQLite for storage.
36 option to ``sqlite`` to enable new repositories to use SQLite for storage.
37 """
37 """
38
38
39 # To run the test suite with repos using SQLite by default, execute the
39 # To run the test suite with repos using SQLite by default, execute the
40 # following:
40 # following:
41 #
41 #
42 # HGREPOFEATURES="sqlitestore" run-tests.py \
42 # HGREPOFEATURES="sqlitestore" run-tests.py \
43 # --extra-config-opt extensions.sqlitestore= \
43 # --extra-config-opt extensions.sqlitestore= \
44 # --extra-config-opt storage.new-repo-backend=sqlite
44 # --extra-config-opt storage.new-repo-backend=sqlite
45
45
46
46
47 import sqlite3
47 import sqlite3
48 import struct
48 import struct
49 import threading
49 import threading
50 import zlib
50 import zlib
51
51
52 from mercurial.i18n import _
52 from mercurial.i18n import _
53 from mercurial.node import (
53 from mercurial.node import (
54 nullrev,
54 nullrev,
55 sha1nodeconstants,
55 sha1nodeconstants,
56 short,
56 short,
57 )
57 )
58 from mercurial.thirdparty import attr
58 from mercurial.thirdparty import attr
59 from mercurial import (
59 from mercurial import (
60 ancestor,
60 ancestor,
61 dagop,
61 dagop,
62 encoding,
62 encoding,
63 error,
63 error,
64 extensions,
64 extensions,
65 localrepo,
65 localrepo,
66 mdiff,
66 mdiff,
67 pycompat,
67 pycompat,
68 registrar,
68 registrar,
69 requirements,
69 requirements,
70 util,
70 util,
71 verify,
71 verify,
72 )
72 )
73 from mercurial.interfaces import (
73 from mercurial.interfaces import (
74 repository,
74 repository,
75 util as interfaceutil,
75 util as interfaceutil,
76 )
76 )
77 from mercurial.utils import (
77 from mercurial.utils import (
78 hashutil,
78 hashutil,
79 storageutil,
79 storageutil,
80 )
80 )
81
81
82 try:
82 try:
83 from mercurial import zstd
83 from mercurial import zstd
84
84
85 zstd.__version__
85 zstd.__version__
86 except ImportError:
86 except ImportError:
87 zstd = None
87 zstd = None
88
88
89 configtable = {}
89 configtable = {}
90 configitem = registrar.configitem(configtable)
90 configitem = registrar.configitem(configtable)
91
91
92 # experimental config: storage.sqlite.compression
92 # experimental config: storage.sqlite.compression
93 configitem(
93 configitem(
94 b'storage',
94 b'storage',
95 b'sqlite.compression',
95 b'sqlite.compression',
96 default=b'zstd' if zstd else b'zlib',
96 default=b'zstd' if zstd else b'zlib',
97 experimental=True,
97 experimental=True,
98 )
98 )
99
99
100 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
100 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
101 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
101 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
102 # be specifying the version(s) of Mercurial they are tested with, or
102 # be specifying the version(s) of Mercurial they are tested with, or
103 # leave the attribute unspecified.
103 # leave the attribute unspecified.
104 testedwith = b'ships-with-hg-core'
104 testedwith = b'ships-with-hg-core'
105
105
106 REQUIREMENT = b'exp-sqlite-001'
106 REQUIREMENT = b'exp-sqlite-001'
107 REQUIREMENT_ZSTD = b'exp-sqlite-comp-001=zstd'
107 REQUIREMENT_ZSTD = b'exp-sqlite-comp-001=zstd'
108 REQUIREMENT_ZLIB = b'exp-sqlite-comp-001=zlib'
108 REQUIREMENT_ZLIB = b'exp-sqlite-comp-001=zlib'
109 REQUIREMENT_NONE = b'exp-sqlite-comp-001=none'
109 REQUIREMENT_NONE = b'exp-sqlite-comp-001=none'
110 REQUIREMENT_SHALLOW_FILES = b'exp-sqlite-shallow-files'
110 REQUIREMENT_SHALLOW_FILES = b'exp-sqlite-shallow-files'
111
111
112 CURRENT_SCHEMA_VERSION = 1
112 CURRENT_SCHEMA_VERSION = 1
113
113
114 COMPRESSION_NONE = 1
114 COMPRESSION_NONE = 1
115 COMPRESSION_ZSTD = 2
115 COMPRESSION_ZSTD = 2
116 COMPRESSION_ZLIB = 3
116 COMPRESSION_ZLIB = 3
117
117
118 FLAG_CENSORED = 1
118 FLAG_CENSORED = 1
119 FLAG_MISSING_P1 = 2
119 FLAG_MISSING_P1 = 2
120 FLAG_MISSING_P2 = 4
120 FLAG_MISSING_P2 = 4
121
121
122 CREATE_SCHEMA = [
122 CREATE_SCHEMA = [
123 # Deltas are stored as content-indexed blobs.
123 # Deltas are stored as content-indexed blobs.
124 # compression column holds COMPRESSION_* constant for how the
124 # compression column holds COMPRESSION_* constant for how the
125 # delta is encoded.
125 # delta is encoded.
126 'CREATE TABLE delta ('
126 'CREATE TABLE delta ('
127 ' id INTEGER PRIMARY KEY, '
127 ' id INTEGER PRIMARY KEY, '
128 ' compression INTEGER NOT NULL, '
128 ' compression INTEGER NOT NULL, '
129 ' hash BLOB UNIQUE ON CONFLICT ABORT, '
129 ' hash BLOB UNIQUE ON CONFLICT ABORT, '
130 ' delta BLOB NOT NULL '
130 ' delta BLOB NOT NULL '
131 ')',
131 ')',
132 # Tracked paths are denormalized to integers to avoid redundant
132 # Tracked paths are denormalized to integers to avoid redundant
133 # storage of the path name.
133 # storage of the path name.
134 'CREATE TABLE filepath ('
134 'CREATE TABLE filepath ('
135 ' id INTEGER PRIMARY KEY, '
135 ' id INTEGER PRIMARY KEY, '
136 ' path BLOB NOT NULL '
136 ' path BLOB NOT NULL '
137 ')',
137 ')',
138 'CREATE UNIQUE INDEX filepath_path ON filepath (path)',
138 'CREATE UNIQUE INDEX filepath_path ON filepath (path)',
139 # We have a single table for all file revision data.
139 # We have a single table for all file revision data.
140 # Each file revision is uniquely described by a (path, rev) and
140 # Each file revision is uniquely described by a (path, rev) and
141 # (path, node).
141 # (path, node).
142 #
142 #
143 # Revision data is stored as a pointer to the delta producing this
143 # Revision data is stored as a pointer to the delta producing this
144 # revision and the file revision whose delta should be applied before
144 # revision and the file revision whose delta should be applied before
145 # that one. One can reconstruct the delta chain by recursively following
145 # that one. One can reconstruct the delta chain by recursively following
146 # the delta base revision pointers until one encounters NULL.
146 # the delta base revision pointers until one encounters NULL.
147 #
147 #
148 # flags column holds bitwise integer flags controlling storage options.
148 # flags column holds bitwise integer flags controlling storage options.
149 # These flags are defined by the FLAG_* constants.
149 # These flags are defined by the FLAG_* constants.
150 'CREATE TABLE fileindex ('
150 'CREATE TABLE fileindex ('
151 ' id INTEGER PRIMARY KEY, '
151 ' id INTEGER PRIMARY KEY, '
152 ' pathid INTEGER REFERENCES filepath(id), '
152 ' pathid INTEGER REFERENCES filepath(id), '
153 ' revnum INTEGER NOT NULL, '
153 ' revnum INTEGER NOT NULL, '
154 ' p1rev INTEGER NOT NULL, '
154 ' p1rev INTEGER NOT NULL, '
155 ' p2rev INTEGER NOT NULL, '
155 ' p2rev INTEGER NOT NULL, '
156 ' linkrev INTEGER NOT NULL, '
156 ' linkrev INTEGER NOT NULL, '
157 ' flags INTEGER NOT NULL, '
157 ' flags INTEGER NOT NULL, '
158 ' deltaid INTEGER REFERENCES delta(id), '
158 ' deltaid INTEGER REFERENCES delta(id), '
159 ' deltabaseid INTEGER REFERENCES fileindex(id), '
159 ' deltabaseid INTEGER REFERENCES fileindex(id), '
160 ' node BLOB NOT NULL '
160 ' node BLOB NOT NULL '
161 ')',
161 ')',
162 'CREATE UNIQUE INDEX fileindex_pathrevnum '
162 'CREATE UNIQUE INDEX fileindex_pathrevnum '
163 ' ON fileindex (pathid, revnum)',
163 ' ON fileindex (pathid, revnum)',
164 'CREATE UNIQUE INDEX fileindex_pathnode ON fileindex (pathid, node)',
164 'CREATE UNIQUE INDEX fileindex_pathnode ON fileindex (pathid, node)',
165 # Provide a view over all file data for convenience.
165 # Provide a view over all file data for convenience.
166 'CREATE VIEW filedata AS '
166 'CREATE VIEW filedata AS '
167 'SELECT '
167 'SELECT '
168 ' fileindex.id AS id, '
168 ' fileindex.id AS id, '
169 ' filepath.id AS pathid, '
169 ' filepath.id AS pathid, '
170 ' filepath.path AS path, '
170 ' filepath.path AS path, '
171 ' fileindex.revnum AS revnum, '
171 ' fileindex.revnum AS revnum, '
172 ' fileindex.node AS node, '
172 ' fileindex.node AS node, '
173 ' fileindex.p1rev AS p1rev, '
173 ' fileindex.p1rev AS p1rev, '
174 ' fileindex.p2rev AS p2rev, '
174 ' fileindex.p2rev AS p2rev, '
175 ' fileindex.linkrev AS linkrev, '
175 ' fileindex.linkrev AS linkrev, '
176 ' fileindex.flags AS flags, '
176 ' fileindex.flags AS flags, '
177 ' fileindex.deltaid AS deltaid, '
177 ' fileindex.deltaid AS deltaid, '
178 ' fileindex.deltabaseid AS deltabaseid '
178 ' fileindex.deltabaseid AS deltabaseid '
179 'FROM filepath, fileindex '
179 'FROM filepath, fileindex '
180 'WHERE fileindex.pathid=filepath.id',
180 'WHERE fileindex.pathid=filepath.id',
181 'PRAGMA user_version=%d' % CURRENT_SCHEMA_VERSION,
181 'PRAGMA user_version=%d' % CURRENT_SCHEMA_VERSION,
182 ]
182 ]
183
183
184
184
185 def resolvedeltachain(db, pathid, node, revisioncache, stoprids, zstddctx=None):
185 def resolvedeltachain(db, pathid, node, revisioncache, stoprids, zstddctx=None):
186 """Resolve a delta chain for a file node."""
186 """Resolve a delta chain for a file node."""
187
187
188 # TODO the "not in ({stops})" here is possibly slowing down the query
188 # TODO the "not in ({stops})" here is possibly slowing down the query
189 # because it needs to perform the lookup on every recursive invocation.
189 # because it needs to perform the lookup on every recursive invocation.
190 # This could possibly be faster if we created a temporary query with
190 # This could possibly be faster if we created a temporary query with
191 # baseid "poisoned" to null and limited the recursive filter to
191 # baseid "poisoned" to null and limited the recursive filter to
192 # "is not null".
192 # "is not null".
193 res = db.execute(
193 res = db.execute(
194 'WITH RECURSIVE '
194 'WITH RECURSIVE '
195 ' deltachain(deltaid, baseid) AS ('
195 ' deltachain(deltaid, baseid) AS ('
196 ' SELECT deltaid, deltabaseid FROM fileindex '
196 ' SELECT deltaid, deltabaseid FROM fileindex '
197 ' WHERE pathid=? AND node=? '
197 ' WHERE pathid=? AND node=? '
198 ' UNION ALL '
198 ' UNION ALL '
199 ' SELECT fileindex.deltaid, deltabaseid '
199 ' SELECT fileindex.deltaid, deltabaseid '
200 ' FROM fileindex, deltachain '
200 ' FROM fileindex, deltachain '
201 ' WHERE '
201 ' WHERE '
202 ' fileindex.id=deltachain.baseid '
202 ' fileindex.id=deltachain.baseid '
203 ' AND deltachain.baseid IS NOT NULL '
203 ' AND deltachain.baseid IS NOT NULL '
204 ' AND fileindex.id NOT IN ({stops}) '
204 ' AND fileindex.id NOT IN ({stops}) '
205 ' ) '
205 ' ) '
206 'SELECT deltachain.baseid, compression, delta '
206 'SELECT deltachain.baseid, compression, delta '
207 'FROM deltachain, delta '
207 'FROM deltachain, delta '
208 'WHERE delta.id=deltachain.deltaid'.format(
208 'WHERE delta.id=deltachain.deltaid'.format(
209 stops=','.join(['?'] * len(stoprids))
209 stops=','.join(['?'] * len(stoprids))
210 ),
210 ),
211 tuple([pathid, node] + list(stoprids.keys())),
211 tuple([pathid, node] + list(stoprids.keys())),
212 )
212 )
213
213
214 deltas = []
214 deltas = []
215 lastdeltabaseid = None
215 lastdeltabaseid = None
216
216
217 for deltabaseid, compression, delta in res:
217 for deltabaseid, compression, delta in res:
218 lastdeltabaseid = deltabaseid
218 lastdeltabaseid = deltabaseid
219
219
220 if compression == COMPRESSION_ZSTD:
220 if compression == COMPRESSION_ZSTD:
221 delta = zstddctx.decompress(delta)
221 delta = zstddctx.decompress(delta)
222 elif compression == COMPRESSION_NONE:
222 elif compression == COMPRESSION_NONE:
223 delta = delta
223 delta = delta
224 elif compression == COMPRESSION_ZLIB:
224 elif compression == COMPRESSION_ZLIB:
225 delta = zlib.decompress(delta)
225 delta = zlib.decompress(delta)
226 else:
226 else:
227 raise SQLiteStoreError(
227 raise SQLiteStoreError(
228 b'unhandled compression type: %d' % compression
228 b'unhandled compression type: %d' % compression
229 )
229 )
230
230
231 deltas.append(delta)
231 deltas.append(delta)
232
232
233 if lastdeltabaseid in stoprids:
233 if lastdeltabaseid in stoprids:
234 basetext = revisioncache[stoprids[lastdeltabaseid]]
234 basetext = revisioncache[stoprids[lastdeltabaseid]]
235 else:
235 else:
236 basetext = deltas.pop()
236 basetext = deltas.pop()
237
237
238 deltas.reverse()
238 deltas.reverse()
239 fulltext = mdiff.patches(basetext, deltas)
239 fulltext = mdiff.patches(basetext, deltas)
240
240
241 # SQLite returns buffer instances for blob columns on Python 2. This
241 # SQLite returns buffer instances for blob columns on Python 2. This
242 # type can propagate through the delta application layer. Because
242 # type can propagate through the delta application layer. Because
243 # downstream callers assume revisions are bytes, cast as needed.
243 # downstream callers assume revisions are bytes, cast as needed.
244 if not isinstance(fulltext, bytes):
244 if not isinstance(fulltext, bytes):
245 fulltext = bytes(delta)
245 fulltext = bytes(delta)
246
246
247 return fulltext
247 return fulltext
248
248
249
249
250 def insertdelta(db, compression, hash, delta):
250 def insertdelta(db, compression, hash, delta):
251 try:
251 try:
252 return db.execute(
252 return db.execute(
253 'INSERT INTO delta (compression, hash, delta) VALUES (?, ?, ?)',
253 'INSERT INTO delta (compression, hash, delta) VALUES (?, ?, ?)',
254 (compression, hash, delta),
254 (compression, hash, delta),
255 ).lastrowid
255 ).lastrowid
256 except sqlite3.IntegrityError:
256 except sqlite3.IntegrityError:
257 return db.execute(
257 return db.execute(
258 'SELECT id FROM delta WHERE hash=?', (hash,)
258 'SELECT id FROM delta WHERE hash=?', (hash,)
259 ).fetchone()[0]
259 ).fetchone()[0]
260
260
261
261
262 class SQLiteStoreError(error.StorageError):
262 class SQLiteStoreError(error.StorageError):
263 pass
263 pass
264
264
265
265
266 @attr.s
266 @attr.s
267 class revisionentry:
267 class revisionentry:
268 rid = attr.ib()
268 rid = attr.ib()
269 rev = attr.ib()
269 rev = attr.ib()
270 node = attr.ib()
270 node = attr.ib()
271 p1rev = attr.ib()
271 p1rev = attr.ib()
272 p2rev = attr.ib()
272 p2rev = attr.ib()
273 p1node = attr.ib()
273 p1node = attr.ib()
274 p2node = attr.ib()
274 p2node = attr.ib()
275 linkrev = attr.ib()
275 linkrev = attr.ib()
276 flags = attr.ib()
276 flags = attr.ib()
277
277
278
278
279 @interfaceutil.implementer(repository.irevisiondelta)
279 @interfaceutil.implementer(repository.irevisiondelta)
280 @attr.s(slots=True)
280 @attr.s(slots=True)
281 class sqliterevisiondelta:
281 class sqliterevisiondelta:
282 node = attr.ib()
282 node = attr.ib()
283 p1node = attr.ib()
283 p1node = attr.ib()
284 p2node = attr.ib()
284 p2node = attr.ib()
285 basenode = attr.ib()
285 basenode = attr.ib()
286 flags = attr.ib()
286 flags = attr.ib()
287 baserevisionsize = attr.ib()
287 baserevisionsize = attr.ib()
288 revision = attr.ib()
288 revision = attr.ib()
289 delta = attr.ib()
289 delta = attr.ib()
290 sidedata = attr.ib()
290 sidedata = attr.ib()
291 protocol_flags = attr.ib()
291 protocol_flags = attr.ib()
292 linknode = attr.ib(default=None)
292 linknode = attr.ib(default=None)
293
293
294
294
295 @interfaceutil.implementer(repository.iverifyproblem)
295 @interfaceutil.implementer(repository.iverifyproblem)
296 @attr.s(frozen=True)
296 @attr.s(frozen=True)
297 class sqliteproblem:
297 class sqliteproblem:
298 warning = attr.ib(default=None)
298 warning = attr.ib(default=None)
299 error = attr.ib(default=None)
299 error = attr.ib(default=None)
300 node = attr.ib(default=None)
300 node = attr.ib(default=None)
301
301
302
302
303 @interfaceutil.implementer(repository.ifilestorage)
303 @interfaceutil.implementer(repository.ifilestorage)
304 class sqlitefilestore:
304 class sqlitefilestore:
305 """Implements storage for an individual tracked path."""
305 """Implements storage for an individual tracked path."""
306
306
307 def __init__(self, db, path, compression):
307 def __init__(self, db, path, compression):
308 self.nullid = sha1nodeconstants.nullid
308 self.nullid = sha1nodeconstants.nullid
309 self._db = db
309 self._db = db
310 self._path = path
310 self._path = path
311
311
312 self._pathid = None
312 self._pathid = None
313
313
314 # revnum -> node
314 # revnum -> node
315 self._revtonode = {}
315 self._revtonode = {}
316 # node -> revnum
316 # node -> revnum
317 self._nodetorev = {}
317 self._nodetorev = {}
318 # node -> data structure
318 # node -> data structure
319 self._revisions = {}
319 self._revisions = {}
320
320
321 self._revisioncache = util.lrucachedict(10)
321 self._revisioncache = util.lrucachedict(10)
322
322
323 self._compengine = compression
323 self._compengine = compression
324
324
325 if compression == b'zstd':
325 if compression == b'zstd':
326 self._cctx = zstd.ZstdCompressor(level=3)
326 self._cctx = zstd.ZstdCompressor(level=3)
327 self._dctx = zstd.ZstdDecompressor()
327 self._dctx = zstd.ZstdDecompressor()
328 else:
328 else:
329 self._cctx = None
329 self._cctx = None
330 self._dctx = None
330 self._dctx = None
331
331
332 self._refreshindex()
332 self._refreshindex()
333
333
334 def _refreshindex(self):
334 def _refreshindex(self):
335 self._revtonode = {}
335 self._revtonode = {}
336 self._nodetorev = {}
336 self._nodetorev = {}
337 self._revisions = {}
337 self._revisions = {}
338
338
339 res = list(
339 res = list(
340 self._db.execute(
340 self._db.execute(
341 'SELECT id FROM filepath WHERE path=?', (self._path,)
341 'SELECT id FROM filepath WHERE path=?', (self._path,)
342 )
342 )
343 )
343 )
344
344
345 if not res:
345 if not res:
346 self._pathid = None
346 self._pathid = None
347 return
347 return
348
348
349 self._pathid = res[0][0]
349 self._pathid = res[0][0]
350
350
351 res = self._db.execute(
351 res = self._db.execute(
352 'SELECT id, revnum, node, p1rev, p2rev, linkrev, flags '
352 'SELECT id, revnum, node, p1rev, p2rev, linkrev, flags '
353 'FROM fileindex '
353 'FROM fileindex '
354 'WHERE pathid=? '
354 'WHERE pathid=? '
355 'ORDER BY revnum ASC',
355 'ORDER BY revnum ASC',
356 (self._pathid,),
356 (self._pathid,),
357 )
357 )
358
358
359 for i, row in enumerate(res):
359 for i, row in enumerate(res):
360 rid, rev, node, p1rev, p2rev, linkrev, flags = row
360 rid, rev, node, p1rev, p2rev, linkrev, flags = row
361
361
362 if i != rev:
362 if i != rev:
363 raise SQLiteStoreError(
363 raise SQLiteStoreError(
364 _(b'sqlite database has inconsistent revision numbers')
364 _(b'sqlite database has inconsistent revision numbers')
365 )
365 )
366
366
367 if p1rev == nullrev:
367 if p1rev == nullrev:
368 p1node = sha1nodeconstants.nullid
368 p1node = sha1nodeconstants.nullid
369 else:
369 else:
370 p1node = self._revtonode[p1rev]
370 p1node = self._revtonode[p1rev]
371
371
372 if p2rev == nullrev:
372 if p2rev == nullrev:
373 p2node = sha1nodeconstants.nullid
373 p2node = sha1nodeconstants.nullid
374 else:
374 else:
375 p2node = self._revtonode[p2rev]
375 p2node = self._revtonode[p2rev]
376
376
377 entry = revisionentry(
377 entry = revisionentry(
378 rid=rid,
378 rid=rid,
379 rev=rev,
379 rev=rev,
380 node=node,
380 node=node,
381 p1rev=p1rev,
381 p1rev=p1rev,
382 p2rev=p2rev,
382 p2rev=p2rev,
383 p1node=p1node,
383 p1node=p1node,
384 p2node=p2node,
384 p2node=p2node,
385 linkrev=linkrev,
385 linkrev=linkrev,
386 flags=flags,
386 flags=flags,
387 )
387 )
388
388
389 self._revtonode[rev] = node
389 self._revtonode[rev] = node
390 self._nodetorev[node] = rev
390 self._nodetorev[node] = rev
391 self._revisions[node] = entry
391 self._revisions[node] = entry
392
392
393 # Start of ifileindex interface.
393 # Start of ifileindex interface.
394
394
395 def __len__(self):
395 def __len__(self):
396 return len(self._revisions)
396 return len(self._revisions)
397
397
398 def __iter__(self):
398 def __iter__(self):
399 return iter(range(len(self._revisions)))
399 return iter(range(len(self._revisions)))
400
400
401 def hasnode(self, node):
401 def hasnode(self, node):
402 if node == sha1nodeconstants.nullid:
402 if node == sha1nodeconstants.nullid:
403 return False
403 return False
404
404
405 return node in self._nodetorev
405 return node in self._nodetorev
406
406
407 def revs(self, start=0, stop=None):
407 def revs(self, start=0, stop=None):
408 return storageutil.iterrevs(
408 return storageutil.iterrevs(
409 len(self._revisions), start=start, stop=stop
409 len(self._revisions), start=start, stop=stop
410 )
410 )
411
411
412 def parents(self, node):
412 def parents(self, node):
413 if node == sha1nodeconstants.nullid:
413 if node == sha1nodeconstants.nullid:
414 return sha1nodeconstants.nullid, sha1nodeconstants.nullid
414 return sha1nodeconstants.nullid, sha1nodeconstants.nullid
415
415
416 if node not in self._revisions:
416 if node not in self._revisions:
417 raise error.LookupError(node, self._path, _(b'no node'))
417 raise error.LookupError(node, self._path, _(b'no node'))
418
418
419 entry = self._revisions[node]
419 entry = self._revisions[node]
420 return entry.p1node, entry.p2node
420 return entry.p1node, entry.p2node
421
421
422 def parentrevs(self, rev):
422 def parentrevs(self, rev):
423 if rev == nullrev:
423 if rev == nullrev:
424 return nullrev, nullrev
424 return nullrev, nullrev
425
425
426 if rev not in self._revtonode:
426 if rev not in self._revtonode:
427 raise IndexError(rev)
427 raise IndexError(rev)
428
428
429 entry = self._revisions[self._revtonode[rev]]
429 entry = self._revisions[self._revtonode[rev]]
430 return entry.p1rev, entry.p2rev
430 return entry.p1rev, entry.p2rev
431
431
432 def ancestors(self, revs, stoprev=0, inclusive=False):
433 """Generate the ancestors of 'revs' in reverse revision order.
434 Does not generate revs lower than stoprev.
435
436 See the documentation for ancestor.lazyancestors for more details."""
437
438 # first, make sure start revisions aren't filtered
439 revs = list(revs)
440 checkrev = self.node
441 for r in revs:
442 checkrev(r)
443
444 return ancestor.lazyancestors(
445 self.parentrevs,
446 revs,
447 stoprev=stoprev,
448 inclusive=inclusive,
449 )
450
432 def rev(self, node):
451 def rev(self, node):
433 if node == sha1nodeconstants.nullid:
452 if node == sha1nodeconstants.nullid:
434 return nullrev
453 return nullrev
435
454
436 if node not in self._nodetorev:
455 if node not in self._nodetorev:
437 raise error.LookupError(node, self._path, _(b'no node'))
456 raise error.LookupError(node, self._path, _(b'no node'))
438
457
439 return self._nodetorev[node]
458 return self._nodetorev[node]
440
459
441 def node(self, rev):
460 def node(self, rev):
442 if rev == nullrev:
461 if rev == nullrev:
443 return sha1nodeconstants.nullid
462 return sha1nodeconstants.nullid
444
463
445 if rev not in self._revtonode:
464 if rev not in self._revtonode:
446 raise IndexError(rev)
465 raise IndexError(rev)
447
466
448 return self._revtonode[rev]
467 return self._revtonode[rev]
449
468
450 def lookup(self, node):
469 def lookup(self, node):
451 return storageutil.fileidlookup(self, node, self._path)
470 return storageutil.fileidlookup(self, node, self._path)
452
471
453 def linkrev(self, rev):
472 def linkrev(self, rev):
454 if rev == nullrev:
473 if rev == nullrev:
455 return nullrev
474 return nullrev
456
475
457 if rev not in self._revtonode:
476 if rev not in self._revtonode:
458 raise IndexError(rev)
477 raise IndexError(rev)
459
478
460 entry = self._revisions[self._revtonode[rev]]
479 entry = self._revisions[self._revtonode[rev]]
461 return entry.linkrev
480 return entry.linkrev
462
481
463 def iscensored(self, rev):
482 def iscensored(self, rev):
464 if rev == nullrev:
483 if rev == nullrev:
465 return False
484 return False
466
485
467 if rev not in self._revtonode:
486 if rev not in self._revtonode:
468 raise IndexError(rev)
487 raise IndexError(rev)
469
488
470 return self._revisions[self._revtonode[rev]].flags & FLAG_CENSORED
489 return self._revisions[self._revtonode[rev]].flags & FLAG_CENSORED
471
490
472 def commonancestorsheads(self, node1, node2):
491 def commonancestorsheads(self, node1, node2):
473 rev1 = self.rev(node1)
492 rev1 = self.rev(node1)
474 rev2 = self.rev(node2)
493 rev2 = self.rev(node2)
475
494
476 ancestors = ancestor.commonancestorsheads(self.parentrevs, rev1, rev2)
495 ancestors = ancestor.commonancestorsheads(self.parentrevs, rev1, rev2)
477 return pycompat.maplist(self.node, ancestors)
496 return pycompat.maplist(self.node, ancestors)
478
497
479 def descendants(self, revs):
498 def descendants(self, revs):
480 # TODO we could implement this using a recursive SQL query, which
499 # TODO we could implement this using a recursive SQL query, which
481 # might be faster.
500 # might be faster.
482 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
501 return dagop.descendantrevs(revs, self.revs, self.parentrevs)
483
502
484 def heads(self, start=None, stop=None):
503 def heads(self, start=None, stop=None):
485 if start is None and stop is None:
504 if start is None and stop is None:
486 if not len(self):
505 if not len(self):
487 return [sha1nodeconstants.nullid]
506 return [sha1nodeconstants.nullid]
488
507
489 startrev = self.rev(start) if start is not None else nullrev
508 startrev = self.rev(start) if start is not None else nullrev
490 stoprevs = {self.rev(n) for n in stop or []}
509 stoprevs = {self.rev(n) for n in stop or []}
491
510
492 revs = dagop.headrevssubset(
511 revs = dagop.headrevssubset(
493 self.revs, self.parentrevs, startrev=startrev, stoprevs=stoprevs
512 self.revs, self.parentrevs, startrev=startrev, stoprevs=stoprevs
494 )
513 )
495
514
496 return [self.node(rev) for rev in revs]
515 return [self.node(rev) for rev in revs]
497
516
498 def children(self, node):
517 def children(self, node):
499 rev = self.rev(node)
518 rev = self.rev(node)
500
519
501 res = self._db.execute(
520 res = self._db.execute(
502 'SELECT'
521 'SELECT'
503 ' node '
522 ' node '
504 ' FROM filedata '
523 ' FROM filedata '
505 ' WHERE path=? AND (p1rev=? OR p2rev=?) '
524 ' WHERE path=? AND (p1rev=? OR p2rev=?) '
506 ' ORDER BY revnum ASC',
525 ' ORDER BY revnum ASC',
507 (self._path, rev, rev),
526 (self._path, rev, rev),
508 )
527 )
509
528
510 return [row[0] for row in res]
529 return [row[0] for row in res]
511
530
512 # End of ifileindex interface.
531 # End of ifileindex interface.
513
532
514 # Start of ifiledata interface.
533 # Start of ifiledata interface.
515
534
516 def size(self, rev):
535 def size(self, rev):
517 if rev == nullrev:
536 if rev == nullrev:
518 return 0
537 return 0
519
538
520 if rev not in self._revtonode:
539 if rev not in self._revtonode:
521 raise IndexError(rev)
540 raise IndexError(rev)
522
541
523 node = self._revtonode[rev]
542 node = self._revtonode[rev]
524
543
525 if self.renamed(node):
544 if self.renamed(node):
526 return len(self.read(node))
545 return len(self.read(node))
527
546
528 return len(self.revision(node))
547 return len(self.revision(node))
529
548
530 def revision(self, node, raw=False, _verifyhash=True):
549 def revision(self, node, raw=False, _verifyhash=True):
531 if node in (sha1nodeconstants.nullid, nullrev):
550 if node in (sha1nodeconstants.nullid, nullrev):
532 return b''
551 return b''
533
552
534 if isinstance(node, int):
553 if isinstance(node, int):
535 node = self.node(node)
554 node = self.node(node)
536
555
537 if node not in self._nodetorev:
556 if node not in self._nodetorev:
538 raise error.LookupError(node, self._path, _(b'no node'))
557 raise error.LookupError(node, self._path, _(b'no node'))
539
558
540 if node in self._revisioncache:
559 if node in self._revisioncache:
541 return self._revisioncache[node]
560 return self._revisioncache[node]
542
561
543 # Because we have a fulltext revision cache, we are able to
562 # Because we have a fulltext revision cache, we are able to
544 # short-circuit delta chain traversal and decompression as soon as
563 # short-circuit delta chain traversal and decompression as soon as
545 # we encounter a revision in the cache.
564 # we encounter a revision in the cache.
546
565
547 stoprids = {self._revisions[n].rid: n for n in self._revisioncache}
566 stoprids = {self._revisions[n].rid: n for n in self._revisioncache}
548
567
549 if not stoprids:
568 if not stoprids:
550 stoprids[-1] = None
569 stoprids[-1] = None
551
570
552 fulltext = resolvedeltachain(
571 fulltext = resolvedeltachain(
553 self._db,
572 self._db,
554 self._pathid,
573 self._pathid,
555 node,
574 node,
556 self._revisioncache,
575 self._revisioncache,
557 stoprids,
576 stoprids,
558 zstddctx=self._dctx,
577 zstddctx=self._dctx,
559 )
578 )
560
579
561 # Don't verify hashes if parent nodes were rewritten, as the hash
580 # Don't verify hashes if parent nodes were rewritten, as the hash
562 # wouldn't verify.
581 # wouldn't verify.
563 if self._revisions[node].flags & (FLAG_MISSING_P1 | FLAG_MISSING_P2):
582 if self._revisions[node].flags & (FLAG_MISSING_P1 | FLAG_MISSING_P2):
564 _verifyhash = False
583 _verifyhash = False
565
584
566 if _verifyhash:
585 if _verifyhash:
567 self._checkhash(fulltext, node)
586 self._checkhash(fulltext, node)
568 self._revisioncache[node] = fulltext
587 self._revisioncache[node] = fulltext
569
588
570 return fulltext
589 return fulltext
571
590
572 def rawdata(self, *args, **kwargs):
591 def rawdata(self, *args, **kwargs):
573 return self.revision(*args, **kwargs)
592 return self.revision(*args, **kwargs)
574
593
575 def read(self, node):
594 def read(self, node):
576 return storageutil.filtermetadata(self.revision(node))
595 return storageutil.filtermetadata(self.revision(node))
577
596
578 def renamed(self, node):
597 def renamed(self, node):
579 return storageutil.filerevisioncopied(self, node)
598 return storageutil.filerevisioncopied(self, node)
580
599
581 def cmp(self, node, fulltext):
600 def cmp(self, node, fulltext):
582 return not storageutil.filedataequivalent(self, node, fulltext)
601 return not storageutil.filedataequivalent(self, node, fulltext)
583
602
584 def emitrevisions(
603 def emitrevisions(
585 self,
604 self,
586 nodes,
605 nodes,
587 nodesorder=None,
606 nodesorder=None,
588 revisiondata=False,
607 revisiondata=False,
589 assumehaveparentrevisions=False,
608 assumehaveparentrevisions=False,
590 deltamode=repository.CG_DELTAMODE_STD,
609 deltamode=repository.CG_DELTAMODE_STD,
591 sidedata_helpers=None,
610 sidedata_helpers=None,
592 ):
611 ):
593 if nodesorder not in (b'nodes', b'storage', b'linear', None):
612 if nodesorder not in (b'nodes', b'storage', b'linear', None):
594 raise error.ProgrammingError(
613 raise error.ProgrammingError(
595 b'unhandled value for nodesorder: %s' % nodesorder
614 b'unhandled value for nodesorder: %s' % nodesorder
596 )
615 )
597
616
598 nodes = [n for n in nodes if n != sha1nodeconstants.nullid]
617 nodes = [n for n in nodes if n != sha1nodeconstants.nullid]
599
618
600 if not nodes:
619 if not nodes:
601 return
620 return
602
621
603 # TODO perform in a single query.
622 # TODO perform in a single query.
604 res = self._db.execute(
623 res = self._db.execute(
605 'SELECT revnum, deltaid FROM fileindex '
624 'SELECT revnum, deltaid FROM fileindex '
606 'WHERE pathid=? '
625 'WHERE pathid=? '
607 ' AND node in (%s)' % (','.join(['?'] * len(nodes))),
626 ' AND node in (%s)' % (','.join(['?'] * len(nodes))),
608 tuple([self._pathid] + nodes),
627 tuple([self._pathid] + nodes),
609 )
628 )
610
629
611 deltabases = {}
630 deltabases = {}
612
631
613 for rev, deltaid in res:
632 for rev, deltaid in res:
614 res = self._db.execute(
633 res = self._db.execute(
615 'SELECT revnum from fileindex WHERE pathid=? AND deltaid=?',
634 'SELECT revnum from fileindex WHERE pathid=? AND deltaid=?',
616 (self._pathid, deltaid),
635 (self._pathid, deltaid),
617 )
636 )
618 deltabases[rev] = res.fetchone()[0]
637 deltabases[rev] = res.fetchone()[0]
619
638
620 # TODO define revdifffn so we can use delta from storage.
639 # TODO define revdifffn so we can use delta from storage.
621 for delta in storageutil.emitrevisions(
640 for delta in storageutil.emitrevisions(
622 self,
641 self,
623 nodes,
642 nodes,
624 nodesorder,
643 nodesorder,
625 sqliterevisiondelta,
644 sqliterevisiondelta,
626 deltaparentfn=deltabases.__getitem__,
645 deltaparentfn=deltabases.__getitem__,
627 revisiondata=revisiondata,
646 revisiondata=revisiondata,
628 assumehaveparentrevisions=assumehaveparentrevisions,
647 assumehaveparentrevisions=assumehaveparentrevisions,
629 deltamode=deltamode,
648 deltamode=deltamode,
630 sidedata_helpers=sidedata_helpers,
649 sidedata_helpers=sidedata_helpers,
631 ):
650 ):
632
651
633 yield delta
652 yield delta
634
653
635 # End of ifiledata interface.
654 # End of ifiledata interface.
636
655
637 # Start of ifilemutation interface.
656 # Start of ifilemutation interface.
638
657
639 def add(self, filedata, meta, transaction, linkrev, p1, p2):
658 def add(self, filedata, meta, transaction, linkrev, p1, p2):
640 if meta or filedata.startswith(b'\x01\n'):
659 if meta or filedata.startswith(b'\x01\n'):
641 filedata = storageutil.packmeta(meta, filedata)
660 filedata = storageutil.packmeta(meta, filedata)
642
661
643 rev = self.addrevision(filedata, transaction, linkrev, p1, p2)
662 rev = self.addrevision(filedata, transaction, linkrev, p1, p2)
644 return self.node(rev)
663 return self.node(rev)
645
664
646 def addrevision(
665 def addrevision(
647 self,
666 self,
648 revisiondata,
667 revisiondata,
649 transaction,
668 transaction,
650 linkrev,
669 linkrev,
651 p1,
670 p1,
652 p2,
671 p2,
653 node=None,
672 node=None,
654 flags=0,
673 flags=0,
655 cachedelta=None,
674 cachedelta=None,
656 ):
675 ):
657 if flags:
676 if flags:
658 raise SQLiteStoreError(_(b'flags not supported on revisions'))
677 raise SQLiteStoreError(_(b'flags not supported on revisions'))
659
678
660 validatehash = node is not None
679 validatehash = node is not None
661 node = node or storageutil.hashrevisionsha1(revisiondata, p1, p2)
680 node = node or storageutil.hashrevisionsha1(revisiondata, p1, p2)
662
681
663 if validatehash:
682 if validatehash:
664 self._checkhash(revisiondata, node, p1, p2)
683 self._checkhash(revisiondata, node, p1, p2)
665
684
666 rev = self._nodetorev.get(node)
685 rev = self._nodetorev.get(node)
667 if rev is not None:
686 if rev is not None:
668 return rev
687 return rev
669
688
670 rev = self._addrawrevision(
689 rev = self._addrawrevision(
671 node, revisiondata, transaction, linkrev, p1, p2
690 node, revisiondata, transaction, linkrev, p1, p2
672 )
691 )
673
692
674 self._revisioncache[node] = revisiondata
693 self._revisioncache[node] = revisiondata
675 return rev
694 return rev
676
695
677 def addgroup(
696 def addgroup(
678 self,
697 self,
679 deltas,
698 deltas,
680 linkmapper,
699 linkmapper,
681 transaction,
700 transaction,
682 addrevisioncb=None,
701 addrevisioncb=None,
683 duplicaterevisioncb=None,
702 duplicaterevisioncb=None,
684 maybemissingparents=False,
703 maybemissingparents=False,
685 ):
704 ):
686 empty = True
705 empty = True
687
706
688 for (
707 for (
689 node,
708 node,
690 p1,
709 p1,
691 p2,
710 p2,
692 linknode,
711 linknode,
693 deltabase,
712 deltabase,
694 delta,
713 delta,
695 wireflags,
714 wireflags,
696 sidedata,
715 sidedata,
697 ) in deltas:
716 ) in deltas:
698 storeflags = 0
717 storeflags = 0
699
718
700 if wireflags & repository.REVISION_FLAG_CENSORED:
719 if wireflags & repository.REVISION_FLAG_CENSORED:
701 storeflags |= FLAG_CENSORED
720 storeflags |= FLAG_CENSORED
702
721
703 if wireflags & ~repository.REVISION_FLAG_CENSORED:
722 if wireflags & ~repository.REVISION_FLAG_CENSORED:
704 raise SQLiteStoreError(b'unhandled revision flag')
723 raise SQLiteStoreError(b'unhandled revision flag')
705
724
706 if maybemissingparents:
725 if maybemissingparents:
707 if p1 != sha1nodeconstants.nullid and not self.hasnode(p1):
726 if p1 != sha1nodeconstants.nullid and not self.hasnode(p1):
708 p1 = sha1nodeconstants.nullid
727 p1 = sha1nodeconstants.nullid
709 storeflags |= FLAG_MISSING_P1
728 storeflags |= FLAG_MISSING_P1
710
729
711 if p2 != sha1nodeconstants.nullid and not self.hasnode(p2):
730 if p2 != sha1nodeconstants.nullid and not self.hasnode(p2):
712 p2 = sha1nodeconstants.nullid
731 p2 = sha1nodeconstants.nullid
713 storeflags |= FLAG_MISSING_P2
732 storeflags |= FLAG_MISSING_P2
714
733
715 baserev = self.rev(deltabase)
734 baserev = self.rev(deltabase)
716
735
717 # If base is censored, delta must be full replacement in a single
736 # If base is censored, delta must be full replacement in a single
718 # patch operation.
737 # patch operation.
719 if baserev != nullrev and self.iscensored(baserev):
738 if baserev != nullrev and self.iscensored(baserev):
720 hlen = struct.calcsize(b'>lll')
739 hlen = struct.calcsize(b'>lll')
721 oldlen = len(self.rawdata(deltabase, _verifyhash=False))
740 oldlen = len(self.rawdata(deltabase, _verifyhash=False))
722 newlen = len(delta) - hlen
741 newlen = len(delta) - hlen
723
742
724 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
743 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
725 raise error.CensoredBaseError(self._path, deltabase)
744 raise error.CensoredBaseError(self._path, deltabase)
726
745
727 if not (storeflags & FLAG_CENSORED) and storageutil.deltaiscensored(
746 if not (storeflags & FLAG_CENSORED) and storageutil.deltaiscensored(
728 delta, baserev, lambda x: len(self.rawdata(x))
747 delta, baserev, lambda x: len(self.rawdata(x))
729 ):
748 ):
730 storeflags |= FLAG_CENSORED
749 storeflags |= FLAG_CENSORED
731
750
732 linkrev = linkmapper(linknode)
751 linkrev = linkmapper(linknode)
733
752
734 if node in self._revisions:
753 if node in self._revisions:
735 # Possibly reset parents to make them proper.
754 # Possibly reset parents to make them proper.
736 entry = self._revisions[node]
755 entry = self._revisions[node]
737
756
738 if (
757 if (
739 entry.flags & FLAG_MISSING_P1
758 entry.flags & FLAG_MISSING_P1
740 and p1 != sha1nodeconstants.nullid
759 and p1 != sha1nodeconstants.nullid
741 ):
760 ):
742 entry.p1node = p1
761 entry.p1node = p1
743 entry.p1rev = self._nodetorev[p1]
762 entry.p1rev = self._nodetorev[p1]
744 entry.flags &= ~FLAG_MISSING_P1
763 entry.flags &= ~FLAG_MISSING_P1
745
764
746 self._db.execute(
765 self._db.execute(
747 'UPDATE fileindex SET p1rev=?, flags=? WHERE id=?',
766 'UPDATE fileindex SET p1rev=?, flags=? WHERE id=?',
748 (self._nodetorev[p1], entry.flags, entry.rid),
767 (self._nodetorev[p1], entry.flags, entry.rid),
749 )
768 )
750
769
751 if (
770 if (
752 entry.flags & FLAG_MISSING_P2
771 entry.flags & FLAG_MISSING_P2
753 and p2 != sha1nodeconstants.nullid
772 and p2 != sha1nodeconstants.nullid
754 ):
773 ):
755 entry.p2node = p2
774 entry.p2node = p2
756 entry.p2rev = self._nodetorev[p2]
775 entry.p2rev = self._nodetorev[p2]
757 entry.flags &= ~FLAG_MISSING_P2
776 entry.flags &= ~FLAG_MISSING_P2
758
777
759 self._db.execute(
778 self._db.execute(
760 'UPDATE fileindex SET p2rev=?, flags=? WHERE id=?',
779 'UPDATE fileindex SET p2rev=?, flags=? WHERE id=?',
761 (self._nodetorev[p1], entry.flags, entry.rid),
780 (self._nodetorev[p1], entry.flags, entry.rid),
762 )
781 )
763
782
764 if duplicaterevisioncb:
783 if duplicaterevisioncb:
765 duplicaterevisioncb(self, self.rev(node))
784 duplicaterevisioncb(self, self.rev(node))
766 empty = False
785 empty = False
767 continue
786 continue
768
787
769 if deltabase == sha1nodeconstants.nullid:
788 if deltabase == sha1nodeconstants.nullid:
770 text = mdiff.patch(b'', delta)
789 text = mdiff.patch(b'', delta)
771 storedelta = None
790 storedelta = None
772 else:
791 else:
773 text = None
792 text = None
774 storedelta = (deltabase, delta)
793 storedelta = (deltabase, delta)
775
794
776 rev = self._addrawrevision(
795 rev = self._addrawrevision(
777 node,
796 node,
778 text,
797 text,
779 transaction,
798 transaction,
780 linkrev,
799 linkrev,
781 p1,
800 p1,
782 p2,
801 p2,
783 storedelta=storedelta,
802 storedelta=storedelta,
784 flags=storeflags,
803 flags=storeflags,
785 )
804 )
786
805
787 if addrevisioncb:
806 if addrevisioncb:
788 addrevisioncb(self, rev)
807 addrevisioncb(self, rev)
789 empty = False
808 empty = False
790
809
791 return not empty
810 return not empty
792
811
793 def censorrevision(self, tr, censornode, tombstone=b''):
812 def censorrevision(self, tr, censornode, tombstone=b''):
794 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
813 tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
795
814
796 # This restriction is cargo culted from revlogs and makes no sense for
815 # This restriction is cargo culted from revlogs and makes no sense for
797 # SQLite, since columns can be resized at will.
816 # SQLite, since columns can be resized at will.
798 if len(tombstone) > len(self.rawdata(censornode)):
817 if len(tombstone) > len(self.rawdata(censornode)):
799 raise error.Abort(
818 raise error.Abort(
800 _(b'censor tombstone must be no longer than censored data')
819 _(b'censor tombstone must be no longer than censored data')
801 )
820 )
802
821
803 # We need to replace the censored revision's data with the tombstone.
822 # We need to replace the censored revision's data with the tombstone.
804 # But replacing that data will have implications for delta chains that
823 # But replacing that data will have implications for delta chains that
805 # reference it.
824 # reference it.
806 #
825 #
807 # While "better," more complex strategies are possible, we do something
826 # While "better," more complex strategies are possible, we do something
808 # simple: we find delta chain children of the censored revision and we
827 # simple: we find delta chain children of the censored revision and we
809 # replace those incremental deltas with fulltexts of their corresponding
828 # replace those incremental deltas with fulltexts of their corresponding
810 # revision. Then we delete the now-unreferenced delta and original
829 # revision. Then we delete the now-unreferenced delta and original
811 # revision and insert a replacement.
830 # revision and insert a replacement.
812
831
813 # Find the delta to be censored.
832 # Find the delta to be censored.
814 censoreddeltaid = self._db.execute(
833 censoreddeltaid = self._db.execute(
815 'SELECT deltaid FROM fileindex WHERE id=?',
834 'SELECT deltaid FROM fileindex WHERE id=?',
816 (self._revisions[censornode].rid,),
835 (self._revisions[censornode].rid,),
817 ).fetchone()[0]
836 ).fetchone()[0]
818
837
819 # Find all its delta chain children.
838 # Find all its delta chain children.
820 # TODO once we support storing deltas for !files, we'll need to look
839 # TODO once we support storing deltas for !files, we'll need to look
821 # for those delta chains too.
840 # for those delta chains too.
822 rows = list(
841 rows = list(
823 self._db.execute(
842 self._db.execute(
824 'SELECT id, pathid, node FROM fileindex '
843 'SELECT id, pathid, node FROM fileindex '
825 'WHERE deltabaseid=? OR deltaid=?',
844 'WHERE deltabaseid=? OR deltaid=?',
826 (censoreddeltaid, censoreddeltaid),
845 (censoreddeltaid, censoreddeltaid),
827 )
846 )
828 )
847 )
829
848
830 for row in rows:
849 for row in rows:
831 rid, pathid, node = row
850 rid, pathid, node = row
832
851
833 fulltext = resolvedeltachain(
852 fulltext = resolvedeltachain(
834 self._db, pathid, node, {}, {-1: None}, zstddctx=self._dctx
853 self._db, pathid, node, {}, {-1: None}, zstddctx=self._dctx
835 )
854 )
836
855
837 deltahash = hashutil.sha1(fulltext).digest()
856 deltahash = hashutil.sha1(fulltext).digest()
838
857
839 if self._compengine == b'zstd':
858 if self._compengine == b'zstd':
840 deltablob = self._cctx.compress(fulltext)
859 deltablob = self._cctx.compress(fulltext)
841 compression = COMPRESSION_ZSTD
860 compression = COMPRESSION_ZSTD
842 elif self._compengine == b'zlib':
861 elif self._compengine == b'zlib':
843 deltablob = zlib.compress(fulltext)
862 deltablob = zlib.compress(fulltext)
844 compression = COMPRESSION_ZLIB
863 compression = COMPRESSION_ZLIB
845 elif self._compengine == b'none':
864 elif self._compengine == b'none':
846 deltablob = fulltext
865 deltablob = fulltext
847 compression = COMPRESSION_NONE
866 compression = COMPRESSION_NONE
848 else:
867 else:
849 raise error.ProgrammingError(
868 raise error.ProgrammingError(
850 b'unhandled compression engine: %s' % self._compengine
869 b'unhandled compression engine: %s' % self._compengine
851 )
870 )
852
871
853 if len(deltablob) >= len(fulltext):
872 if len(deltablob) >= len(fulltext):
854 deltablob = fulltext
873 deltablob = fulltext
855 compression = COMPRESSION_NONE
874 compression = COMPRESSION_NONE
856
875
857 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
876 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
858
877
859 self._db.execute(
878 self._db.execute(
860 'UPDATE fileindex SET deltaid=?, deltabaseid=NULL '
879 'UPDATE fileindex SET deltaid=?, deltabaseid=NULL '
861 'WHERE id=?',
880 'WHERE id=?',
862 (deltaid, rid),
881 (deltaid, rid),
863 )
882 )
864
883
865 # Now create the tombstone delta and replace the delta on the censored
884 # Now create the tombstone delta and replace the delta on the censored
866 # node.
885 # node.
867 deltahash = hashutil.sha1(tombstone).digest()
886 deltahash = hashutil.sha1(tombstone).digest()
868 tombstonedeltaid = insertdelta(
887 tombstonedeltaid = insertdelta(
869 self._db, COMPRESSION_NONE, deltahash, tombstone
888 self._db, COMPRESSION_NONE, deltahash, tombstone
870 )
889 )
871
890
872 flags = self._revisions[censornode].flags
891 flags = self._revisions[censornode].flags
873 flags |= FLAG_CENSORED
892 flags |= FLAG_CENSORED
874
893
875 self._db.execute(
894 self._db.execute(
876 'UPDATE fileindex SET flags=?, deltaid=?, deltabaseid=NULL '
895 'UPDATE fileindex SET flags=?, deltaid=?, deltabaseid=NULL '
877 'WHERE pathid=? AND node=?',
896 'WHERE pathid=? AND node=?',
878 (flags, tombstonedeltaid, self._pathid, censornode),
897 (flags, tombstonedeltaid, self._pathid, censornode),
879 )
898 )
880
899
881 self._db.execute('DELETE FROM delta WHERE id=?', (censoreddeltaid,))
900 self._db.execute('DELETE FROM delta WHERE id=?', (censoreddeltaid,))
882
901
883 self._refreshindex()
902 self._refreshindex()
884 self._revisioncache.clear()
903 self._revisioncache.clear()
885
904
886 def getstrippoint(self, minlink):
905 def getstrippoint(self, minlink):
887 return storageutil.resolvestripinfo(
906 return storageutil.resolvestripinfo(
888 minlink,
907 minlink,
889 len(self) - 1,
908 len(self) - 1,
890 [self.rev(n) for n in self.heads()],
909 [self.rev(n) for n in self.heads()],
891 self.linkrev,
910 self.linkrev,
892 self.parentrevs,
911 self.parentrevs,
893 )
912 )
894
913
895 def strip(self, minlink, transaction):
914 def strip(self, minlink, transaction):
896 if not len(self):
915 if not len(self):
897 return
916 return
898
917
899 rev, _ignored = self.getstrippoint(minlink)
918 rev, _ignored = self.getstrippoint(minlink)
900
919
901 if rev == len(self):
920 if rev == len(self):
902 return
921 return
903
922
904 for rev in self.revs(rev):
923 for rev in self.revs(rev):
905 self._db.execute(
924 self._db.execute(
906 'DELETE FROM fileindex WHERE pathid=? AND node=?',
925 'DELETE FROM fileindex WHERE pathid=? AND node=?',
907 (self._pathid, self.node(rev)),
926 (self._pathid, self.node(rev)),
908 )
927 )
909
928
910 # TODO how should we garbage collect data in delta table?
929 # TODO how should we garbage collect data in delta table?
911
930
912 self._refreshindex()
931 self._refreshindex()
913
932
914 # End of ifilemutation interface.
933 # End of ifilemutation interface.
915
934
916 # Start of ifilestorage interface.
935 # Start of ifilestorage interface.
917
936
918 def files(self):
937 def files(self):
919 return []
938 return []
920
939
921 def sidedata(self, nodeorrev, _df=None):
940 def sidedata(self, nodeorrev, _df=None):
922 # Not supported for now
941 # Not supported for now
923 return {}
942 return {}
924
943
925 def storageinfo(
944 def storageinfo(
926 self,
945 self,
927 exclusivefiles=False,
946 exclusivefiles=False,
928 sharedfiles=False,
947 sharedfiles=False,
929 revisionscount=False,
948 revisionscount=False,
930 trackedsize=False,
949 trackedsize=False,
931 storedsize=False,
950 storedsize=False,
932 ):
951 ):
933 d = {}
952 d = {}
934
953
935 if exclusivefiles:
954 if exclusivefiles:
936 d[b'exclusivefiles'] = []
955 d[b'exclusivefiles'] = []
937
956
938 if sharedfiles:
957 if sharedfiles:
939 # TODO list sqlite file(s) here.
958 # TODO list sqlite file(s) here.
940 d[b'sharedfiles'] = []
959 d[b'sharedfiles'] = []
941
960
942 if revisionscount:
961 if revisionscount:
943 d[b'revisionscount'] = len(self)
962 d[b'revisionscount'] = len(self)
944
963
945 if trackedsize:
964 if trackedsize:
946 d[b'trackedsize'] = sum(
965 d[b'trackedsize'] = sum(
947 len(self.revision(node)) for node in self._nodetorev
966 len(self.revision(node)) for node in self._nodetorev
948 )
967 )
949
968
950 if storedsize:
969 if storedsize:
951 # TODO implement this?
970 # TODO implement this?
952 d[b'storedsize'] = None
971 d[b'storedsize'] = None
953
972
954 return d
973 return d
955
974
956 def verifyintegrity(self, state):
975 def verifyintegrity(self, state):
957 state[b'skipread'] = set()
976 state[b'skipread'] = set()
958
977
959 for rev in self:
978 for rev in self:
960 node = self.node(rev)
979 node = self.node(rev)
961
980
962 try:
981 try:
963 self.revision(node)
982 self.revision(node)
964 except Exception as e:
983 except Exception as e:
965 yield sqliteproblem(
984 yield sqliteproblem(
966 error=_(b'unpacking %s: %s') % (short(node), e), node=node
985 error=_(b'unpacking %s: %s') % (short(node), e), node=node
967 )
986 )
968
987
969 state[b'skipread'].add(node)
988 state[b'skipread'].add(node)
970
989
971 # End of ifilestorage interface.
990 # End of ifilestorage interface.
972
991
973 def _checkhash(self, fulltext, node, p1=None, p2=None):
992 def _checkhash(self, fulltext, node, p1=None, p2=None):
974 if p1 is None and p2 is None:
993 if p1 is None and p2 is None:
975 p1, p2 = self.parents(node)
994 p1, p2 = self.parents(node)
976
995
977 if node == storageutil.hashrevisionsha1(fulltext, p1, p2):
996 if node == storageutil.hashrevisionsha1(fulltext, p1, p2):
978 return
997 return
979
998
980 try:
999 try:
981 del self._revisioncache[node]
1000 del self._revisioncache[node]
982 except KeyError:
1001 except KeyError:
983 pass
1002 pass
984
1003
985 if storageutil.iscensoredtext(fulltext):
1004 if storageutil.iscensoredtext(fulltext):
986 raise error.CensoredNodeError(self._path, node, fulltext)
1005 raise error.CensoredNodeError(self._path, node, fulltext)
987
1006
988 raise SQLiteStoreError(_(b'integrity check failed on %s') % self._path)
1007 raise SQLiteStoreError(_(b'integrity check failed on %s') % self._path)
989
1008
990 def _addrawrevision(
1009 def _addrawrevision(
991 self,
1010 self,
992 node,
1011 node,
993 revisiondata,
1012 revisiondata,
994 transaction,
1013 transaction,
995 linkrev,
1014 linkrev,
996 p1,
1015 p1,
997 p2,
1016 p2,
998 storedelta=None,
1017 storedelta=None,
999 flags=0,
1018 flags=0,
1000 ):
1019 ):
1001 if self._pathid is None:
1020 if self._pathid is None:
1002 res = self._db.execute(
1021 res = self._db.execute(
1003 'INSERT INTO filepath (path) VALUES (?)', (self._path,)
1022 'INSERT INTO filepath (path) VALUES (?)', (self._path,)
1004 )
1023 )
1005 self._pathid = res.lastrowid
1024 self._pathid = res.lastrowid
1006
1025
1007 # For simplicity, always store a delta against p1.
1026 # For simplicity, always store a delta against p1.
1008 # TODO we need a lot more logic here to make behavior reasonable.
1027 # TODO we need a lot more logic here to make behavior reasonable.
1009
1028
1010 if storedelta:
1029 if storedelta:
1011 deltabase, delta = storedelta
1030 deltabase, delta = storedelta
1012
1031
1013 if isinstance(deltabase, int):
1032 if isinstance(deltabase, int):
1014 deltabase = self.node(deltabase)
1033 deltabase = self.node(deltabase)
1015
1034
1016 else:
1035 else:
1017 assert revisiondata is not None
1036 assert revisiondata is not None
1018 deltabase = p1
1037 deltabase = p1
1019
1038
1020 if deltabase == sha1nodeconstants.nullid:
1039 if deltabase == sha1nodeconstants.nullid:
1021 delta = revisiondata
1040 delta = revisiondata
1022 else:
1041 else:
1023 delta = mdiff.textdiff(
1042 delta = mdiff.textdiff(
1024 self.revision(self.rev(deltabase)), revisiondata
1043 self.revision(self.rev(deltabase)), revisiondata
1025 )
1044 )
1026
1045
1027 # File index stores a pointer to its delta and the parent delta.
1046 # File index stores a pointer to its delta and the parent delta.
1028 # The parent delta is stored via a pointer to the fileindex PK.
1047 # The parent delta is stored via a pointer to the fileindex PK.
1029 if deltabase == sha1nodeconstants.nullid:
1048 if deltabase == sha1nodeconstants.nullid:
1030 baseid = None
1049 baseid = None
1031 else:
1050 else:
1032 baseid = self._revisions[deltabase].rid
1051 baseid = self._revisions[deltabase].rid
1033
1052
1034 # Deltas are stored with a hash of their content. This allows
1053 # Deltas are stored with a hash of their content. This allows
1035 # us to de-duplicate. The table is configured to ignore conflicts
1054 # us to de-duplicate. The table is configured to ignore conflicts
1036 # and it is faster to just insert and silently noop than to look
1055 # and it is faster to just insert and silently noop than to look
1037 # first.
1056 # first.
1038 deltahash = hashutil.sha1(delta).digest()
1057 deltahash = hashutil.sha1(delta).digest()
1039
1058
1040 if self._compengine == b'zstd':
1059 if self._compengine == b'zstd':
1041 deltablob = self._cctx.compress(delta)
1060 deltablob = self._cctx.compress(delta)
1042 compression = COMPRESSION_ZSTD
1061 compression = COMPRESSION_ZSTD
1043 elif self._compengine == b'zlib':
1062 elif self._compengine == b'zlib':
1044 deltablob = zlib.compress(delta)
1063 deltablob = zlib.compress(delta)
1045 compression = COMPRESSION_ZLIB
1064 compression = COMPRESSION_ZLIB
1046 elif self._compengine == b'none':
1065 elif self._compengine == b'none':
1047 deltablob = delta
1066 deltablob = delta
1048 compression = COMPRESSION_NONE
1067 compression = COMPRESSION_NONE
1049 else:
1068 else:
1050 raise error.ProgrammingError(
1069 raise error.ProgrammingError(
1051 b'unhandled compression engine: %s' % self._compengine
1070 b'unhandled compression engine: %s' % self._compengine
1052 )
1071 )
1053
1072
1054 # Don't store compressed data if it isn't practical.
1073 # Don't store compressed data if it isn't practical.
1055 if len(deltablob) >= len(delta):
1074 if len(deltablob) >= len(delta):
1056 deltablob = delta
1075 deltablob = delta
1057 compression = COMPRESSION_NONE
1076 compression = COMPRESSION_NONE
1058
1077
1059 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
1078 deltaid = insertdelta(self._db, compression, deltahash, deltablob)
1060
1079
1061 rev = len(self)
1080 rev = len(self)
1062
1081
1063 if p1 == sha1nodeconstants.nullid:
1082 if p1 == sha1nodeconstants.nullid:
1064 p1rev = nullrev
1083 p1rev = nullrev
1065 else:
1084 else:
1066 p1rev = self._nodetorev[p1]
1085 p1rev = self._nodetorev[p1]
1067
1086
1068 if p2 == sha1nodeconstants.nullid:
1087 if p2 == sha1nodeconstants.nullid:
1069 p2rev = nullrev
1088 p2rev = nullrev
1070 else:
1089 else:
1071 p2rev = self._nodetorev[p2]
1090 p2rev = self._nodetorev[p2]
1072
1091
1073 rid = self._db.execute(
1092 rid = self._db.execute(
1074 'INSERT INTO fileindex ('
1093 'INSERT INTO fileindex ('
1075 ' pathid, revnum, node, p1rev, p2rev, linkrev, flags, '
1094 ' pathid, revnum, node, p1rev, p2rev, linkrev, flags, '
1076 ' deltaid, deltabaseid) '
1095 ' deltaid, deltabaseid) '
1077 ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
1096 ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
1078 (
1097 (
1079 self._pathid,
1098 self._pathid,
1080 rev,
1099 rev,
1081 node,
1100 node,
1082 p1rev,
1101 p1rev,
1083 p2rev,
1102 p2rev,
1084 linkrev,
1103 linkrev,
1085 flags,
1104 flags,
1086 deltaid,
1105 deltaid,
1087 baseid,
1106 baseid,
1088 ),
1107 ),
1089 ).lastrowid
1108 ).lastrowid
1090
1109
1091 entry = revisionentry(
1110 entry = revisionentry(
1092 rid=rid,
1111 rid=rid,
1093 rev=rev,
1112 rev=rev,
1094 node=node,
1113 node=node,
1095 p1rev=p1rev,
1114 p1rev=p1rev,
1096 p2rev=p2rev,
1115 p2rev=p2rev,
1097 p1node=p1,
1116 p1node=p1,
1098 p2node=p2,
1117 p2node=p2,
1099 linkrev=linkrev,
1118 linkrev=linkrev,
1100 flags=flags,
1119 flags=flags,
1101 )
1120 )
1102
1121
1103 self._nodetorev[node] = rev
1122 self._nodetorev[node] = rev
1104 self._revtonode[rev] = node
1123 self._revtonode[rev] = node
1105 self._revisions[node] = entry
1124 self._revisions[node] = entry
1106
1125
1107 return rev
1126 return rev
1108
1127
1109
1128
1110 class sqliterepository(localrepo.localrepository):
1129 class sqliterepository(localrepo.localrepository):
1111 def cancopy(self):
1130 def cancopy(self):
1112 return False
1131 return False
1113
1132
1114 def transaction(self, *args, **kwargs):
1133 def transaction(self, *args, **kwargs):
1115 current = self.currenttransaction()
1134 current = self.currenttransaction()
1116
1135
1117 tr = super(sqliterepository, self).transaction(*args, **kwargs)
1136 tr = super(sqliterepository, self).transaction(*args, **kwargs)
1118
1137
1119 if current:
1138 if current:
1120 return tr
1139 return tr
1121
1140
1122 self._dbconn.execute('BEGIN TRANSACTION')
1141 self._dbconn.execute('BEGIN TRANSACTION')
1123
1142
1124 def committransaction(_):
1143 def committransaction(_):
1125 self._dbconn.commit()
1144 self._dbconn.commit()
1126
1145
1127 tr.addfinalize(b'sqlitestore', committransaction)
1146 tr.addfinalize(b'sqlitestore', committransaction)
1128
1147
1129 return tr
1148 return tr
1130
1149
1131 @property
1150 @property
1132 def _dbconn(self):
1151 def _dbconn(self):
1133 # SQLite connections can only be used on the thread that created
1152 # SQLite connections can only be used on the thread that created
1134 # them. In most cases, this "just works." However, hgweb uses
1153 # them. In most cases, this "just works." However, hgweb uses
1135 # multiple threads.
1154 # multiple threads.
1136 tid = threading.current_thread().ident
1155 tid = threading.current_thread().ident
1137
1156
1138 if self._db:
1157 if self._db:
1139 if self._db[0] == tid:
1158 if self._db[0] == tid:
1140 return self._db[1]
1159 return self._db[1]
1141
1160
1142 db = makedb(self.svfs.join(b'db.sqlite'))
1161 db = makedb(self.svfs.join(b'db.sqlite'))
1143 self._db = (tid, db)
1162 self._db = (tid, db)
1144
1163
1145 return db
1164 return db
1146
1165
1147
1166
1148 def makedb(path):
1167 def makedb(path):
1149 """Construct a database handle for a database at path."""
1168 """Construct a database handle for a database at path."""
1150
1169
1151 db = sqlite3.connect(encoding.strfromlocal(path))
1170 db = sqlite3.connect(encoding.strfromlocal(path))
1152 db.text_factory = bytes
1171 db.text_factory = bytes
1153
1172
1154 res = db.execute('PRAGMA user_version').fetchone()[0]
1173 res = db.execute('PRAGMA user_version').fetchone()[0]
1155
1174
1156 # New database.
1175 # New database.
1157 if res == 0:
1176 if res == 0:
1158 for statement in CREATE_SCHEMA:
1177 for statement in CREATE_SCHEMA:
1159 db.execute(statement)
1178 db.execute(statement)
1160
1179
1161 db.commit()
1180 db.commit()
1162
1181
1163 elif res == CURRENT_SCHEMA_VERSION:
1182 elif res == CURRENT_SCHEMA_VERSION:
1164 pass
1183 pass
1165
1184
1166 else:
1185 else:
1167 raise error.Abort(_(b'sqlite database has unrecognized version'))
1186 raise error.Abort(_(b'sqlite database has unrecognized version'))
1168
1187
1169 db.execute('PRAGMA journal_mode=WAL')
1188 db.execute('PRAGMA journal_mode=WAL')
1170
1189
1171 return db
1190 return db
1172
1191
1173
1192
1174 def featuresetup(ui, supported):
1193 def featuresetup(ui, supported):
1175 supported.add(REQUIREMENT)
1194 supported.add(REQUIREMENT)
1176
1195
1177 if zstd:
1196 if zstd:
1178 supported.add(REQUIREMENT_ZSTD)
1197 supported.add(REQUIREMENT_ZSTD)
1179
1198
1180 supported.add(REQUIREMENT_ZLIB)
1199 supported.add(REQUIREMENT_ZLIB)
1181 supported.add(REQUIREMENT_NONE)
1200 supported.add(REQUIREMENT_NONE)
1182 supported.add(REQUIREMENT_SHALLOW_FILES)
1201 supported.add(REQUIREMENT_SHALLOW_FILES)
1183 supported.add(requirements.NARROW_REQUIREMENT)
1202 supported.add(requirements.NARROW_REQUIREMENT)
1184
1203
1185
1204
1186 def newreporequirements(orig, ui, createopts):
1205 def newreporequirements(orig, ui, createopts):
1187 if createopts[b'backend'] != b'sqlite':
1206 if createopts[b'backend'] != b'sqlite':
1188 return orig(ui, createopts)
1207 return orig(ui, createopts)
1189
1208
1190 # This restriction can be lifted once we have more confidence.
1209 # This restriction can be lifted once we have more confidence.
1191 if b'sharedrepo' in createopts:
1210 if b'sharedrepo' in createopts:
1192 raise error.Abort(
1211 raise error.Abort(
1193 _(b'shared repositories not supported with SQLite store')
1212 _(b'shared repositories not supported with SQLite store')
1194 )
1213 )
1195
1214
1196 # This filtering is out of an abundance of caution: we want to ensure
1215 # This filtering is out of an abundance of caution: we want to ensure
1197 # we honor creation options and we do that by annotating exactly the
1216 # we honor creation options and we do that by annotating exactly the
1198 # creation options we recognize.
1217 # creation options we recognize.
1199 known = {
1218 known = {
1200 b'narrowfiles',
1219 b'narrowfiles',
1201 b'backend',
1220 b'backend',
1202 b'shallowfilestore',
1221 b'shallowfilestore',
1203 }
1222 }
1204
1223
1205 unsupported = set(createopts) - known
1224 unsupported = set(createopts) - known
1206 if unsupported:
1225 if unsupported:
1207 raise error.Abort(
1226 raise error.Abort(
1208 _(b'SQLite store does not support repo creation option: %s')
1227 _(b'SQLite store does not support repo creation option: %s')
1209 % b', '.join(sorted(unsupported))
1228 % b', '.join(sorted(unsupported))
1210 )
1229 )
1211
1230
1212 # Since we're a hybrid store that still relies on revlogs, we fall back
1231 # Since we're a hybrid store that still relies on revlogs, we fall back
1213 # to using the revlogv1 backend's storage requirements then adding our
1232 # to using the revlogv1 backend's storage requirements then adding our
1214 # own requirement.
1233 # own requirement.
1215 createopts[b'backend'] = b'revlogv1'
1234 createopts[b'backend'] = b'revlogv1'
1216 requirements = orig(ui, createopts)
1235 requirements = orig(ui, createopts)
1217 requirements.add(REQUIREMENT)
1236 requirements.add(REQUIREMENT)
1218
1237
1219 compression = ui.config(b'storage', b'sqlite.compression')
1238 compression = ui.config(b'storage', b'sqlite.compression')
1220
1239
1221 if compression == b'zstd' and not zstd:
1240 if compression == b'zstd' and not zstd:
1222 raise error.Abort(
1241 raise error.Abort(
1223 _(
1242 _(
1224 b'storage.sqlite.compression set to "zstd" but '
1243 b'storage.sqlite.compression set to "zstd" but '
1225 b'zstandard compression not available to this '
1244 b'zstandard compression not available to this '
1226 b'Mercurial install'
1245 b'Mercurial install'
1227 )
1246 )
1228 )
1247 )
1229
1248
1230 if compression == b'zstd':
1249 if compression == b'zstd':
1231 requirements.add(REQUIREMENT_ZSTD)
1250 requirements.add(REQUIREMENT_ZSTD)
1232 elif compression == b'zlib':
1251 elif compression == b'zlib':
1233 requirements.add(REQUIREMENT_ZLIB)
1252 requirements.add(REQUIREMENT_ZLIB)
1234 elif compression == b'none':
1253 elif compression == b'none':
1235 requirements.add(REQUIREMENT_NONE)
1254 requirements.add(REQUIREMENT_NONE)
1236 else:
1255 else:
1237 raise error.Abort(
1256 raise error.Abort(
1238 _(
1257 _(
1239 b'unknown compression engine defined in '
1258 b'unknown compression engine defined in '
1240 b'storage.sqlite.compression: %s'
1259 b'storage.sqlite.compression: %s'
1241 )
1260 )
1242 % compression
1261 % compression
1243 )
1262 )
1244
1263
1245 if createopts.get(b'shallowfilestore'):
1264 if createopts.get(b'shallowfilestore'):
1246 requirements.add(REQUIREMENT_SHALLOW_FILES)
1265 requirements.add(REQUIREMENT_SHALLOW_FILES)
1247
1266
1248 return requirements
1267 return requirements
1249
1268
1250
1269
1251 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
1270 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
1252 class sqlitefilestorage:
1271 class sqlitefilestorage:
1253 """Repository file storage backed by SQLite."""
1272 """Repository file storage backed by SQLite."""
1254
1273
1255 def file(self, path):
1274 def file(self, path):
1256 if path[0] == b'/':
1275 if path[0] == b'/':
1257 path = path[1:]
1276 path = path[1:]
1258
1277
1259 if REQUIREMENT_ZSTD in self.requirements:
1278 if REQUIREMENT_ZSTD in self.requirements:
1260 compression = b'zstd'
1279 compression = b'zstd'
1261 elif REQUIREMENT_ZLIB in self.requirements:
1280 elif REQUIREMENT_ZLIB in self.requirements:
1262 compression = b'zlib'
1281 compression = b'zlib'
1263 elif REQUIREMENT_NONE in self.requirements:
1282 elif REQUIREMENT_NONE in self.requirements:
1264 compression = b'none'
1283 compression = b'none'
1265 else:
1284 else:
1266 raise error.Abort(
1285 raise error.Abort(
1267 _(
1286 _(
1268 b'unable to determine what compression engine '
1287 b'unable to determine what compression engine '
1269 b'to use for SQLite storage'
1288 b'to use for SQLite storage'
1270 )
1289 )
1271 )
1290 )
1272
1291
1273 return sqlitefilestore(self._dbconn, path, compression)
1292 return sqlitefilestore(self._dbconn, path, compression)
1274
1293
1275
1294
1276 def makefilestorage(orig, requirements, features, **kwargs):
1295 def makefilestorage(orig, requirements, features, **kwargs):
1277 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
1296 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
1278 if REQUIREMENT in requirements:
1297 if REQUIREMENT in requirements:
1279 if REQUIREMENT_SHALLOW_FILES in requirements:
1298 if REQUIREMENT_SHALLOW_FILES in requirements:
1280 features.add(repository.REPO_FEATURE_SHALLOW_FILE_STORAGE)
1299 features.add(repository.REPO_FEATURE_SHALLOW_FILE_STORAGE)
1281
1300
1282 return sqlitefilestorage
1301 return sqlitefilestorage
1283 else:
1302 else:
1284 return orig(requirements=requirements, features=features, **kwargs)
1303 return orig(requirements=requirements, features=features, **kwargs)
1285
1304
1286
1305
1287 def makemain(orig, ui, requirements, **kwargs):
1306 def makemain(orig, ui, requirements, **kwargs):
1288 if REQUIREMENT in requirements:
1307 if REQUIREMENT in requirements:
1289 if REQUIREMENT_ZSTD in requirements and not zstd:
1308 if REQUIREMENT_ZSTD in requirements and not zstd:
1290 raise error.Abort(
1309 raise error.Abort(
1291 _(
1310 _(
1292 b'repository uses zstandard compression, which '
1311 b'repository uses zstandard compression, which '
1293 b'is not available to this Mercurial install'
1312 b'is not available to this Mercurial install'
1294 )
1313 )
1295 )
1314 )
1296
1315
1297 return sqliterepository
1316 return sqliterepository
1298
1317
1299 return orig(requirements=requirements, **kwargs)
1318 return orig(requirements=requirements, **kwargs)
1300
1319
1301
1320
1302 def verifierinit(orig, self, *args, **kwargs):
1321 def verifierinit(orig, self, *args, **kwargs):
1303 orig(self, *args, **kwargs)
1322 orig(self, *args, **kwargs)
1304
1323
1305 # We don't care that files in the store don't align with what is
1324 # We don't care that files in the store don't align with what is
1306 # advertised. So suppress these warnings.
1325 # advertised. So suppress these warnings.
1307 self.warnorphanstorefiles = False
1326 self.warnorphanstorefiles = False
1308
1327
1309
1328
1310 def extsetup(ui):
1329 def extsetup(ui):
1311 localrepo.featuresetupfuncs.add(featuresetup)
1330 localrepo.featuresetupfuncs.add(featuresetup)
1312 extensions.wrapfunction(
1331 extensions.wrapfunction(
1313 localrepo, b'newreporequirements', newreporequirements
1332 localrepo, b'newreporequirements', newreporequirements
1314 )
1333 )
1315 extensions.wrapfunction(localrepo, b'makefilestorage', makefilestorage)
1334 extensions.wrapfunction(localrepo, b'makefilestorage', makefilestorage)
1316 extensions.wrapfunction(localrepo, b'makemain', makemain)
1335 extensions.wrapfunction(localrepo, b'makemain', makemain)
1317 extensions.wrapfunction(verify.verifier, b'__init__', verifierinit)
1336 extensions.wrapfunction(verify.verifier, b'__init__', verifierinit)
1318
1337
1319
1338
1320 def reposetup(ui, repo):
1339 def reposetup(ui, repo):
1321 if isinstance(repo, sqliterepository):
1340 if isinstance(repo, sqliterepository):
1322 repo._db = None
1341 repo._db = None
1323
1342
1324 # TODO check for bundlerepository?
1343 # TODO check for bundlerepository?
General Comments 0
You need to be logged in to leave comments. Login now