##// END OF EJS Templates
revlog: move configuration attribute into dedicated object...
marmoute -
r51922:498afb62 default
parent child Browse files
Show More
@@ -235,6 +235,15 b' class bundlemanifest(bundlerevlog, manif'
235 )
235 )
236 return super(bundlemanifest, self).dirlog(d)
236 return super(bundlemanifest, self).dirlog(d)
237
237
238 # XXX small hack to work around the use of manifest.manifestrevlog
239 @property
240 def _generaldelta(self):
241 return self._revlog.delta_config.general_delta
242
243 @_generaldelta.setter
244 def _generaldelta(self, value):
245 self._revlog.delta_config.general_delta = value
246
238
247
239 class bundlefilelog(filelog.filelog):
248 class bundlefilelog(filelog.filelog):
240 def __init__(self, opener, path, cgunpacker, linkmapper):
249 def __init__(self, opener, path, cgunpacker, linkmapper):
@@ -416,7 +416,7 b' class changelog(revlog.revlog):'
416 # changelogs don't benefit from generaldelta.
416 # changelogs don't benefit from generaldelta.
417
417
418 self._format_flags &= ~revlog.FLAG_GENERALDELTA
418 self._format_flags &= ~revlog.FLAG_GENERALDELTA
419 self._generaldelta = False
419 self.delta_config.general_delta = False
420
420
421 # Delta chains for changelogs tend to be very small because entries
421 # Delta chains for changelogs tend to be very small because entries
422 # tend to be small and don't delta well with each. So disable delta
422 # tend to be small and don't delta well with each. So disable delta
@@ -1614,7 +1614,7 b' class manifestrevlog:'
1614 )
1614 )
1615
1615
1616 self.index = self._revlog.index
1616 self.index = self._revlog.index
1617 self._generaldelta = self._revlog._generaldelta
1617 self._generaldelta = self._revlog.delta_config.general_delta
1618
1618
1619 def get_revlog(self):
1619 def get_revlog(self):
1620 """return an actual revlog instance if any
1620 """return an actual revlog instance if any
@@ -241,6 +241,92 b' FILE_TOO_SHORT_MSG = _('
241 hexdigits = b'0123456789abcdefABCDEF'
241 hexdigits = b'0123456789abcdefABCDEF'
242
242
243
243
244 @attr.s()
245 class FeatureConfig:
246 """Hold configuration values about the available revlog features"""
247
248 # the default compression engine
249 compression_engine = attr.ib(default=b'zlib')
250 # compression engines options
251 compression_engine_options = attr.ib(default=attr.Factory(dict))
252
253 # can we use censor on this revlog
254 censorable = attr.ib(default=False)
255 # does this revlog use the "side data" feature
256 has_side_data = attr.ib(default=False)
257 # might remove rank configuration once the computation has no impact
258 compute_rank = attr.ib(default=False)
259 # parent order is supposed to be semantically irrelevant, so we
260 # normally resort parents to ensure that the first parent is non-null,
261 # if there is a non-null parent at all.
262 # filelog abuses the parent order as flag to mark some instances of
263 # meta-encoded files, so allow it to disable this behavior.
264 canonical_parent_order = attr.ib(default=False)
265 # can ellipsis commit be used
266 enable_ellipsis = attr.ib(default=False)
267
268
269 @attr.s()
270 class DataConfig:
271 """Hold configuration value about how the revlog data are read"""
272
273 # should we try to open the "pending" version of the revlog
274 try_pending = attr.ib(default=False)
275 # should we try to open the "splitted" version of the revlog
276 try_split = attr.ib(default=False)
277 # When True, indexfile should be opened with checkambig=True at writing,
278 # to avoid file stat ambiguity.
279 check_ambig = attr.ib(default=False)
280
281 # If true, use mmap instead of reading to deal with large index
282 mmap_large_index = attr.ib(default=False)
283 # how much data is large
284 mmap_index_threshold = attr.ib(default=None)
285 # How much data to read and cache into the raw revlog data cache.
286 chunk_cache_size = attr.ib(default=65536)
287
288 # Allow sparse reading of the revlog data
289 with_sparse_read = attr.ib(default=False)
290 # minimal density of a sparse read chunk
291 sr_density_threshold = attr.ib(default=0.50)
292 # minimal size of data we skip when performing sparse read
293 sr_min_gap_size = attr.ib(default=262144)
294
295 # are delta encoded against arbitrary bases.
296 generaldelta = attr.ib(default=False)
297
298
299 @attr.s()
300 class DeltaConfig:
301 """Hold configuration value about how new delta are computed
302
303 Some attributes are duplicated from DataConfig to help havign each object
304 self contained.
305 """
306
307 # can delta be encoded against arbitrary bases.
308 general_delta = attr.ib(default=False)
309 # Allow sparse writing of the revlog data
310 sparse_revlog = attr.ib(default=False)
311 # maximum length of a delta chain
312 max_chain_len = attr.ib(default=None)
313 # Maximum distance between delta chain base start and end
314 max_deltachain_span = attr.ib(default=-1)
315 # If `upper_bound_comp` is not None, this is the expected maximal gain from
316 # compression for the data content.
317 upper_bound_comp = attr.ib(default=None)
318 # Should we try a delta against both parent
319 delta_both_parents = attr.ib(default=True)
320 # Test delta base candidate group by chunk of this maximal size.
321 candidate_group_chunk_size = attr.ib(default=0)
322 # Should we display debug information about delta computation
323 debug_delta = attr.ib(default=False)
324 # trust incoming delta by default
325 lazy_delta = attr.ib(default=True)
326 # trust the base of incoming delta by default
327 lazy_delta_base = attr.ib(default=False)
328
329
244 class revlog:
330 class revlog:
245 """
331 """
246 the underlying revision storage object
332 the underlying revision storage object
@@ -348,43 +434,31 b' class revlog:'
348 assert target[0] in ALL_KINDS
434 assert target[0] in ALL_KINDS
349 assert len(target) == 2
435 assert len(target) == 2
350 self.target = target
436 self.target = target
351 # When True, indexfile is opened with checkambig=True at writing, to
437 self.feature_config = FeatureConfig(
352 # avoid file stat ambiguity.
438 censorable=censorable,
353 self._checkambig = checkambig
439 canonical_parent_order=canonical_parent_order,
354 self._mmaplargeindex = mmaplargeindex
440 )
355 self._censorable = censorable
441 self.data_config = DataConfig(
442 check_ambig=checkambig,
443 mmap_large_index=mmaplargeindex,
444 )
445 self.delta_config = DeltaConfig()
446
356 # 3-tuple of (node, rev, text) for a raw revision.
447 # 3-tuple of (node, rev, text) for a raw revision.
357 self._revisioncache = None
448 self._revisioncache = None
358 # Maps rev to chain base rev.
449 # Maps rev to chain base rev.
359 self._chainbasecache = util.lrucachedict(100)
450 self._chainbasecache = util.lrucachedict(100)
360 # 2-tuple of (offset, data) of raw data from the revlog at an offset.
451 # 2-tuple of (offset, data) of raw data from the revlog at an offset.
361 self._chunkcache = (0, b'')
452 self._chunkcache = (0, b'')
362 # How much data to read and cache into the raw revlog data cache.
453
363 self._chunkcachesize = 65536
364 self._maxchainlen = None
365 self._deltabothparents = True
366 self._candidate_group_chunk_size = 0
367 self._debug_delta = False
368 self.index = None
454 self.index = None
369 self._docket = None
455 self._docket = None
370 self._nodemap_docket = None
456 self._nodemap_docket = None
371 # Mapping of partial identifiers to full nodes.
457 # Mapping of partial identifiers to full nodes.
372 self._pcache = {}
458 self._pcache = {}
373 # Mapping of revision integer to full node.
374 self._compengine = b'zlib'
375 self._compengineopts = {}
376 self._maxdeltachainspan = -1
377 self._withsparseread = False
378 self._sparserevlog = False
379 self.hassidedata = False
380 self._srdensitythreshold = 0.50
381 self._srmingapsize = 262144
382
459
383 # other optionnals features
460 # other optionnals features
384
461
385 # might remove rank configuration once the computation has no impact
386 self._compute_rank = False
387
388 # Make copy of flag processors so each revlog instance can support
462 # Make copy of flag processors so each revlog instance can support
389 # custom flags.
463 # custom flags.
390 self._flagprocessors = dict(flagutil.flagprocessors)
464 self._flagprocessors = dict(flagutil.flagprocessors)
@@ -398,12 +472,110 b' class revlog:'
398
472
399 self._concurrencychecker = concurrencychecker
473 self._concurrencychecker = concurrencychecker
400
474
401 # parent order is supposed to be semantically irrelevant, so we
475 @property
402 # normally resort parents to ensure that the first parent is non-null,
476 def _generaldelta(self):
403 # if there is a non-null parent at all.
477 """temporary compatibility proxy"""
404 # filelog abuses the parent order as flag to mark some instances of
478 return self.delta_config.general_delta
405 # meta-encoded files, so allow it to disable this behavior.
479
406 self.canonical_parent_order = canonical_parent_order
480 @property
481 def _checkambig(self):
482 """temporary compatibility proxy"""
483 return self.data_config.check_ambig
484
485 @property
486 def _mmaplargeindex(self):
487 """temporary compatibility proxy"""
488 return self.data_config.mmap_large_index
489
490 @property
491 def _censorable(self):
492 """temporary compatibility proxy"""
493 return self.feature_config.censorable
494
495 @property
496 def _chunkcachesize(self):
497 """temporary compatibility proxy"""
498 return self.data_config.chunk_cache_size
499
500 @property
501 def _maxchainlen(self):
502 """temporary compatibility proxy"""
503 return self.delta_config.max_chain_len
504
505 @property
506 def _deltabothparents(self):
507 """temporary compatibility proxy"""
508 return self.delta_config.delta_both_parents
509
510 @property
511 def _candidate_group_chunk_size(self):
512 """temporary compatibility proxy"""
513 return self.delta_config.candidate_group_chunk_size
514
515 @property
516 def _debug_delta(self):
517 """temporary compatibility proxy"""
518 return self.delta_config.debug_delta
519
520 @property
521 def _compengine(self):
522 """temporary compatibility proxy"""
523 return self.feature_config.compression_engine
524
525 @property
526 def _compengineopts(self):
527 """temporary compatibility proxy"""
528 return self.feature_config.compression_engine_options
529
530 @property
531 def _maxdeltachainspan(self):
532 """temporary compatibility proxy"""
533 return self.delta_config.max_deltachain_span
534
535 @property
536 def _withsparseread(self):
537 """temporary compatibility proxy"""
538 return self.data_config.with_sparse_read
539
540 @property
541 def _sparserevlog(self):
542 """temporary compatibility proxy"""
543 return self.delta_config.sparse_revlog
544
545 @property
546 def hassidedata(self):
547 """temporary compatibility proxy"""
548 return self.feature_config.has_side_data
549
550 @property
551 def _srdensitythreshold(self):
552 """temporary compatibility proxy"""
553 return self.data_config.sr_density_threshold
554
555 @property
556 def _srmingapsize(self):
557 """temporary compatibility proxy"""
558 return self.data_config.sr_min_gap_size
559
560 @property
561 def _compute_rank(self):
562 """temporary compatibility proxy"""
563 return self.feature_config.compute_rank
564
565 @property
566 def canonical_parent_order(self):
567 """temporary compatibility proxy"""
568 return self.feature_config.canonical_parent_order
569
570 @property
571 def _lazydelta(self):
572 """temporary compatibility proxy"""
573 return self.delta_config.lazy_delta
574
575 @property
576 def _lazydeltabase(self):
577 """temporary compatibility proxy"""
578 return self.delta_config.lazy_delta_base
407
579
408 def _init_opts(self):
580 def _init_opts(self):
409 """process options (from above/config) to setup associated default revlog mode
581 """process options (from above/config) to setup associated default revlog mode
@@ -426,7 +598,8 b' class revlog:'
426
598
427 if b'changelogv2' in opts and self.revlog_kind == KIND_CHANGELOG:
599 if b'changelogv2' in opts and self.revlog_kind == KIND_CHANGELOG:
428 new_header = CHANGELOGV2
600 new_header = CHANGELOGV2
429 self._compute_rank = opts.get(b'changelogv2.compute-rank', True)
601 compute_rank = opts.get(b'changelogv2.compute-rank', True)
602 self.feature_config.compute_rank = compute_rank
430 elif b'revlogv2' in opts:
603 elif b'revlogv2' in opts:
431 new_header = REVLOGV2
604 new_header = REVLOGV2
432 elif b'revlogv1' in opts:
605 elif b'revlogv1' in opts:
@@ -439,54 +612,63 b' class revlog:'
439 new_header = REVLOG_DEFAULT_VERSION
612 new_header = REVLOG_DEFAULT_VERSION
440
613
441 if b'chunkcachesize' in opts:
614 if b'chunkcachesize' in opts:
442 self._chunkcachesize = opts[b'chunkcachesize']
615 self.data_config.chunk_cache_size = opts[b'chunkcachesize']
443 if b'maxchainlen' in opts:
616 if b'maxchainlen' in opts:
444 self._maxchainlen = opts[b'maxchainlen']
617 self.delta_config.max_chain_len = opts[b'maxchainlen']
445 if b'deltabothparents' in opts:
618 if b'deltabothparents' in opts:
446 self._deltabothparents = opts[b'deltabothparents']
619 self.delta_config.delta_both_parents = opts[b'deltabothparents']
447 dps_cgds = opts.get(b'delta-parent-search.candidate-group-chunk-size')
620 dps_cgds = opts.get(b'delta-parent-search.candidate-group-chunk-size')
448 if dps_cgds:
621 if dps_cgds:
449 self._candidate_group_chunk_size = dps_cgds
622 self.delta_config.candidate_group_chunk_size = dps_cgds
450 self._lazydelta = bool(opts.get(b'lazydelta', True))
623 if b'lazydelta' in opts:
451 self._lazydeltabase = False
624 self.delta_config.lazy_delta = bool(opts[b'lazydelta'])
452 if self._lazydelta:
625 if self._lazydelta and b'lazydeltabase' in opts:
453 self._lazydeltabase = bool(opts.get(b'lazydeltabase', False))
626 self.delta_config.lazy_delta_base = opts[b'lazydeltabase']
454 if b'debug-delta' in opts:
627 if b'debug-delta' in opts:
455 self._debug_delta = opts[b'debug-delta']
628 self.delta_config.debug_delta = opts[b'debug-delta']
456 if b'compengine' in opts:
629 if b'compengine' in opts:
457 self._compengine = opts[b'compengine']
630 self.feature_config.compression_engine = opts[b'compengine']
631 comp_engine_opts = self.feature_config.compression_engine_options
458 if b'zlib.level' in opts:
632 if b'zlib.level' in opts:
459 self._compengineopts[b'zlib.level'] = opts[b'zlib.level']
633 comp_engine_opts[b'zlib.level'] = opts[b'zlib.level']
460 if b'zstd.level' in opts:
634 if b'zstd.level' in opts:
461 self._compengineopts[b'zstd.level'] = opts[b'zstd.level']
635 comp_engine_opts[b'zstd.level'] = opts[b'zstd.level']
462 if b'maxdeltachainspan' in opts:
636 if b'maxdeltachainspan' in opts:
463 self._maxdeltachainspan = opts[b'maxdeltachainspan']
637 self.delta_config.max_deltachain_span = opts[b'maxdeltachainspan']
464 if self._mmaplargeindex and b'mmapindexthreshold' in opts:
638 if self._mmaplargeindex and b'mmapindexthreshold' in opts:
465 mmapindexthreshold = opts[b'mmapindexthreshold']
639 mmapindexthreshold = opts[b'mmapindexthreshold']
466 self._sparserevlog = bool(opts.get(b'sparse-revlog', False))
640 self.data_config.mmap_index_threshold = mmapindexthreshold
467 withsparseread = bool(opts.get(b'with-sparse-read', False))
641 if b'sparse-revlog' in opts:
468 # sparse-revlog forces sparse-read
642 self.delta_config.sparse_revlog = bool(opts[b'sparse-revlog'])
469 self._withsparseread = self._sparserevlog or withsparseread
643 if self.delta_config.sparse_revlog:
644 # sparse-revlog forces sparse-read
645 self.data_config.with_sparse_read = True
646 elif b'with-sparse-read' in opts:
647 self.data_config.with_sparse_read = bool(opts[b'with-sparse-read'])
470 if b'sparse-read-density-threshold' in opts:
648 if b'sparse-read-density-threshold' in opts:
471 self._srdensitythreshold = opts[b'sparse-read-density-threshold']
649 self.data_config.sr_density_threshold = opts[
650 b'sparse-read-density-threshold'
651 ]
472 if b'sparse-read-min-gap-size' in opts:
652 if b'sparse-read-min-gap-size' in opts:
473 self._srmingapsize = opts[b'sparse-read-min-gap-size']
653 self.data_config.sr_min_gap_size = opts[b'sparse-read-min-gap-size']
474 if opts.get(b'enableellipsis'):
654 if opts.get(b'enableellipsis'):
655 self.feature_config.enable_ellipsis = True
475 self._flagprocessors[REVIDX_ELLIPSIS] = ellipsisprocessor
656 self._flagprocessors[REVIDX_ELLIPSIS] = ellipsisprocessor
476
657
477 # revlog v0 doesn't have flag processors
658 # revlog v0 doesn't have flag processors
478 for flag, processor in opts.get(b'flagprocessors', {}).items():
659 for flag, processor in opts.get(b'flagprocessors', {}).items():
479 flagutil.insertflagprocessor(flag, processor, self._flagprocessors)
660 flagutil.insertflagprocessor(flag, processor, self._flagprocessors)
480
661
481 if self._chunkcachesize <= 0:
662 chunk_cache_size = self.data_config.chunk_cache_size
663 if chunk_cache_size <= 0:
482 raise error.RevlogError(
664 raise error.RevlogError(
483 _(b'revlog chunk cache size %r is not greater than 0')
665 _(b'revlog chunk cache size %r is not greater than 0')
484 % self._chunkcachesize
666 % chunk_cache_size
485 )
667 )
486 elif self._chunkcachesize & (self._chunkcachesize - 1):
668 elif chunk_cache_size & (chunk_cache_size - 1):
487 raise error.RevlogError(
669 raise error.RevlogError(
488 _(b'revlog chunk cache size %r is not a power of 2')
670 _(b'revlog chunk cache size %r is not a power of 2')
489 % self._chunkcachesize
671 % chunk_cache_size
490 )
672 )
491 force_nodemap = opts.get(b'devel-force-nodemap', False)
673 force_nodemap = opts.get(b'devel-force-nodemap', False)
492 return new_header, mmapindexthreshold, force_nodemap
674 return new_header, mmapindexthreshold, force_nodemap
@@ -664,8 +846,10 b' class revlog:'
664
846
665 features = FEATURES_BY_VERSION[self._format_version]
847 features = FEATURES_BY_VERSION[self._format_version]
666 self._inline = features[b'inline'](self._format_flags)
848 self._inline = features[b'inline'](self._format_flags)
667 self._generaldelta = features[b'generaldelta'](self._format_flags)
849 self.delta_config.general_delta = features[b'generaldelta'](
668 self.hassidedata = features[b'sidedata']
850 self._format_flags
851 )
852 self.feature_config.has_side_data = features[b'sidedata']
669
853
670 if not features[b'docket']:
854 if not features[b'docket']:
671 self._indexfile = entry_point
855 self._indexfile = entry_point
@@ -694,7 +878,7 b' class revlog:'
694
878
695 self._inline = False
879 self._inline = False
696 # generaldelta implied by version 2 revlogs.
880 # generaldelta implied by version 2 revlogs.
697 self._generaldelta = True
881 self.delta_config.general_delta = True
698 # the logic for persistent nodemap will be dealt with within the
882 # the logic for persistent nodemap will be dealt with within the
699 # main docket, so disable it for now.
883 # main docket, so disable it for now.
700 self._nodemap_file = None
884 self._nodemap_file = None
@@ -712,7 +896,7 b' class revlog:'
712
896
713 # sparse-revlog can't be on without general-delta (issue6056)
897 # sparse-revlog can't be on without general-delta (issue6056)
714 if not self._generaldelta:
898 if not self._generaldelta:
715 self._sparserevlog = False
899 self.delta_config.sparse_revlog = False
716
900
717 self._storedeltachains = True
901 self._storedeltachains = True
718
902
@@ -3197,16 +3381,17 b' class revlog:'
3197
3381
3198 try:
3382 try:
3199 if deltareuse == self.DELTAREUSEALWAYS:
3383 if deltareuse == self.DELTAREUSEALWAYS:
3200 destrevlog._lazydeltabase = True
3384 destrevlog.delta_config.lazy_delta_base = True
3201 destrevlog._lazydelta = True
3385 destrevlog.delta_config.lazy_delta = True
3202 elif deltareuse == self.DELTAREUSESAMEREVS:
3386 elif deltareuse == self.DELTAREUSESAMEREVS:
3203 destrevlog._lazydeltabase = False
3387 destrevlog.delta_config.lazy_delta_base = False
3204 destrevlog._lazydelta = True
3388 destrevlog.delta_config.lazy_delta = True
3205 elif deltareuse == self.DELTAREUSENEVER:
3389 elif deltareuse == self.DELTAREUSENEVER:
3206 destrevlog._lazydeltabase = False
3390 destrevlog.delta_config.lazy_delta_base = False
3207 destrevlog._lazydelta = False
3391 destrevlog.delta_config.lazy_delta = False
3208
3392
3209 destrevlog._deltabothparents = forcedeltabothparents or oldamd
3393 delta_both_parents = forcedeltabothparents or oldamd
3394 destrevlog.delta_config.delta_both_parents = delta_both_parents
3210
3395
3211 with self.reading():
3396 with self.reading():
3212 self._clone(
3397 self._clone(
@@ -3219,9 +3404,9 b' class revlog:'
3219 )
3404 )
3220
3405
3221 finally:
3406 finally:
3222 destrevlog._lazydelta = oldlazydelta
3407 destrevlog.delta_config.lazy_delta = oldlazydelta
3223 destrevlog._lazydeltabase = oldlazydeltabase
3408 destrevlog.delta_config.lazy_delta_base = oldlazydeltabase
3224 destrevlog._deltabothparents = oldamd
3409 destrevlog.delta_config.delta_both_parents = oldamd
3225
3410
3226 def _clone(
3411 def _clone(
3227 self,
3412 self,
@@ -75,7 +75,7 b' def v1_censor(rl, tr, censornode, tombst'
75 )
75 )
76 newrl._format_version = rl._format_version
76 newrl._format_version = rl._format_version
77 newrl._format_flags = rl._format_flags
77 newrl._format_flags = rl._format_flags
78 newrl._generaldelta = rl._generaldelta
78 newrl.delta_config.general_delta = rl._generaldelta
79 newrl._parse_index = rl._parse_index
79 newrl._parse_index = rl._parse_index
80
80
81 for rev in rl.revs():
81 for rev in rl.revs():
@@ -213,6 +213,15 b' class unionmanifest(unionrevlog, manifes'
213 self, opener, self._revlog.radix, manifest2, linkmapper
213 self, opener, self._revlog.radix, manifest2, linkmapper
214 )
214 )
215
215
216 # XXX small hack to work around the use of manifest.manifestrevlog
217 @property
218 def _generaldelta(self):
219 return self._revlog.delta_config.general_delta
220
221 @_generaldelta.setter
222 def _generaldelta(self, value):
223 self._revlog.delta_config.general_delta = value
224
216
225
217 class unionfilelog(filelog.filelog):
226 class unionfilelog(filelog.filelog):
218 def __init__(self, opener, path, opener2, linkmapper, repo):
227 def __init__(self, opener, path, opener2, linkmapper, repo):
@@ -375,7 +375,8 b' def slicingtest(rlog):'
375 try:
375 try:
376 # the test revlog is small, we remove the floor under which we
376 # the test revlog is small, we remove the floor under which we
377 # slicing is diregarded.
377 # slicing is diregarded.
378 rlog._srmingapsize = 0
378 rlog.data_config.sr_min_gap_size = 0
379 rlog.delta_config.sr_min_gap_size = 0
379 for item in slicingdata:
380 for item in slicingdata:
380 chain, expected, target = item
381 chain, expected, target = item
381 result = deltas.slicechunk(rlog, chain, targetsize=target)
382 result = deltas.slicechunk(rlog, chain, targetsize=target)
@@ -387,7 +388,8 b' def slicingtest(rlog):'
387 print(' expected: %s' % expected)
388 print(' expected: %s' % expected)
388 print(' result: %s' % result)
389 print(' result: %s' % result)
389 finally:
390 finally:
390 rlog._srmingapsize = oldmin
391 rlog.data_config.sr_min_gap_size = oldmin
392 rlog.delta_config.sr_min_gap_size = oldmin
391
393
392
394
393 def md5sum(s):
395 def md5sum(s):
General Comments 0
You need to be logged in to leave comments. Login now